fix: rendering in a2ui surface and viewport aligned

This commit is contained in:
Georg Bauer
2026-07-27 13:53:01 +02:00
parent f69623ad02
commit 6a541b51f9

View File

@@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::a2ui::{Surface, binding_path, bound_value_at, display_value}; use crate::a2ui::{Surface, binding_path, bound_value_at, display_value};
use iced::widget::{Column, Row, column, radio, text_editor}; use iced::widget::{Column, Row, column, radio, responsive, text_editor};
use serde_json::Value; use serde_json::Value;
use std::collections::{BTreeSet, HashMap, HashSet}; use std::collections::{BTreeSet, HashMap, HashSet};
use time::{Month, OffsetDateTime}; use time::{Month, OffsetDateTime};
@@ -23,7 +23,6 @@ impl App {
.into() .into()
}); });
container(content) container(content)
.max_width(860)
.center_x(Length::Fill) .center_x(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(24) .padding(24)
@@ -80,8 +79,20 @@ impl App {
.get("agentDisplayName") .get("agentDisplayName")
.and_then(Value::as_str) .and_then(Value::as_str)
.unwrap_or("A2UI"); .unwrap_or("A2UI");
let content = if surface.components.contains_key("root") { let content: Element<'_, Message> = if surface.components.contains_key("root") {
self.a2ui_component(surface, "root", &surface.data, None, BTreeSet::new()) responsive(move |size| {
scrollable(self.a2ui_component(
surface,
"root",
&surface.data,
None,
BTreeSet::new(),
size.height,
))
.height(Length::Fill)
.into()
})
.into()
} else { } else {
text("Building interactive surface…") text("Building interactive surface…")
.size(13) .size(13)
@@ -118,7 +129,7 @@ impl App {
] ]
.spacing(8) .spacing(8)
.align_y(Alignment::Center), .align_y(Alignment::Center),
scrollable(content).height(Length::Fill), content,
] ]
.height(Length::Fill) .height(Length::Fill)
.spacing(10), .spacing(10),
@@ -137,6 +148,7 @@ impl App {
context: &'a Value, context: &'a Value,
context_path: Option<String>, context_path: Option<String>,
mut ancestors: BTreeSet<String>, mut ancestors: BTreeSet<String>,
surface_height: f32,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
if !ancestors.insert(id.to_owned()) { if !ancestors.insert(id.to_owned()) {
return text(format!("Cyclic component reference: {id}")) return text(format!("Cyclic component reference: {id}"))
@@ -254,6 +266,7 @@ impl App {
context_path, context_path,
ancestors, ancestors,
Some((component, kind == "Row")), Some((component, kind == "Row")),
surface_height,
), ),
"List" => { "List" => {
let horizontal = let horizontal =
@@ -265,6 +278,7 @@ impl App {
context_path, context_path,
ancestors, ancestors,
Some((component, horizontal)), Some((component, horizontal)),
surface_height,
); );
let list = scrollable(content).height(280); let list = scrollable(content).height(280);
if horizontal { if horizontal {
@@ -278,11 +292,18 @@ impl App {
} }
"Card" => { "Card" => {
let child = component.get("child").and_then(Value::as_str).unwrap_or(""); let child = component.get("child").and_then(Value::as_str).unwrap_or("");
container(self.a2ui_component(surface, child, context, context_path, ancestors)) container(self.a2ui_component(
.padding(14) surface,
.width(Length::Fill) child,
.style(overview_style) context,
.into() context_path,
ancestors,
surface_height,
))
.padding(14)
.width(Length::Fill)
.style(overview_style)
.into()
} }
"Tabs" => { "Tabs" => {
let tabs = component let tabs = component
@@ -317,7 +338,14 @@ impl App {
.unwrap_or(""); .unwrap_or("");
column![ column![
labels, labels,
self.a2ui_component(surface, child, context, context_path, ancestors,) self.a2ui_component(
surface,
child,
context,
context_path,
ancestors,
surface_height,
)
] ]
.spacing(10) .spacing(10)
.into() .into()
@@ -338,6 +366,7 @@ impl App {
context, context,
context_path.clone(), context_path.clone(),
ancestors.clone(), ancestors.clone(),
surface_height,
)) ))
.on_press(Message::A2uiToggleModal(key.0.clone(), key.1.clone())); .on_press(Message::A2uiToggleModal(key.0.clone(), key.1.clone()));
let _ = (content, context_path, ancestors); let _ = (content, context_path, ancestors);
@@ -345,8 +374,14 @@ impl App {
} }
"Button" => { "Button" => {
let child = component.get("child").and_then(Value::as_str).unwrap_or(""); let child = component.get("child").and_then(Value::as_str).unwrap_or("");
let content = let content = self.a2ui_component(
self.a2ui_component(surface, child, context, context_path.clone(), ancestors); surface,
child,
context,
context_path.clone(),
ancestors,
surface_height,
);
let button = let button =
if component.get("variant").and_then(Value::as_str) == Some("borderless") { if component.get("variant").and_then(Value::as_str) == Some("borderless") {
button(content).style(button::text) button(content).style(button::text)
@@ -640,7 +675,7 @@ impl App {
}; };
picker.push(choices).into() picker.push(choices).into()
} }
"Chart" => research_chart(component, &surface.data, context), "Chart" => research_chart(component, &surface.data, context, surface_height),
"Table" => research_table(component, &surface.data, context), "Table" => research_table(component, &surface.data, context),
"Metric" => research_metric(component, &surface.data, context), "Metric" => research_metric(component, &surface.data, context),
"Timeline" => research_timeline(component, &surface.data, context), "Timeline" => research_timeline(component, &surface.data, context),
@@ -654,6 +689,7 @@ impl App {
context_path.clone(), context_path.clone(),
ancestors, ancestors,
None, None,
surface_height,
); );
let label = component let label = component
.get("submitLabel") .get("submitLabel")
@@ -687,6 +723,7 @@ impl App {
} }
} }
#[allow(clippy::too_many_arguments)]
fn a2ui_children<'a>( fn a2ui_children<'a>(
&'a self, &'a self,
surface: &'a Surface, surface: &'a Surface,
@@ -695,6 +732,7 @@ impl App {
context_path: Option<String>, context_path: Option<String>,
ancestors: BTreeSet<String>, ancestors: BTreeSet<String>,
layout: Option<(&serde_json::Map<String, Value>, bool)>, layout: Option<(&serde_json::Map<String, Value>, bool)>,
surface_height: f32,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let horizontal = layout.is_some_and(|(_, horizontal)| horizontal); let horizontal = layout.is_some_and(|(_, horizontal)| horizontal);
let layout = layout.map(|(component, _)| component); let layout = layout.map(|(component, _)| component);
@@ -708,6 +746,7 @@ impl App {
context, context,
context_path.clone(), context_path.clone(),
ancestors.clone(), ancestors.clone(),
surface_height,
), ),
component_weight(surface, child), component_weight(surface, child),
)); ));
@@ -728,6 +767,7 @@ impl App {
item, item,
Some(format!("{}/{index}", path.trim_end_matches('/'))), Some(format!("{}/{index}", path.trim_end_matches('/'))),
ancestors.clone(), ancestors.clone(),
surface_height,
) )
}), }),
component_weight(surface, component_id), component_weight(surface, component_id),
@@ -906,8 +946,14 @@ impl App {
let surface = self.displayed_a2ui_store().surface(surface_id)?; let surface = self.displayed_a2ui_store().surface(surface_id)?;
let component = surface.components.get(component_id)?.as_object()?; let component = surface.components.get(component_id)?.as_object()?;
let content_id = component.get("content")?.as_str()?; let content_id = component.get("content")?.as_str()?;
let content = let content = self.a2ui_component(
self.a2ui_component(surface, content_id, &surface.data, None, BTreeSet::new()); surface,
content_id,
&surface.data,
None,
BTreeSet::new(),
480.0,
);
let dialog = container( let dialog = container(
column![ column![
row![ row![
@@ -1405,6 +1451,7 @@ fn research_chart<'a>(
component: &serde_json::Map<String, Value>, component: &serde_json::Map<String, Value>,
data: &Value, data: &Value,
context: &Value, context: &Value,
surface_height: f32,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let series = bound_value_at(component.get("series"), data, context); let series = bound_value_at(component.get("series"), data, context);
let series = series.as_array().map(Vec::as_slice).unwrap_or(&[]); let series = series.as_array().map(Vec::as_slice).unwrap_or(&[]);
@@ -1413,7 +1460,7 @@ fn research_chart<'a>(
.and_then(Value::as_str) .and_then(Value::as_str)
.unwrap_or("bar"); .unwrap_or("bar");
if matches!(chart_type, "pie" | "donut") { if matches!(chart_type, "pie" | "donut") {
return research_pie_chart(component, series, chart_type == "donut"); return research_pie_chart(component, series, chart_type == "donut", surface_height);
} }
let max = series let max = series
.iter() .iter()
@@ -1464,6 +1511,7 @@ fn research_pie_chart<'a>(
component: &serde_json::Map<String, Value>, component: &serde_json::Map<String, Value>,
series: &[Value], series: &[Value],
donut: bool, donut: bool,
surface_height: f32,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let slices = series let slices = series
.iter() .iter()
@@ -1482,7 +1530,7 @@ fn research_pie_chart<'a>(
.iter() .iter()
.map(|(_, value, _)| *value) .map(|(_, value, _)| *value)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut chart = Column::new().spacing(8); let mut chart = Column::new().spacing(8).width(Length::Fill);
if let Some(title) = title(component) { if let Some(title) = title(component) {
chart = chart.push(title); chart = chart.push(title);
} }
@@ -1496,35 +1544,56 @@ fn research_pie_chart<'a>(
.push(text("No positive values").size(11).color(muted_text())) .push(text("No positive values").size(11).color(muted_text()))
.into(); .into();
} }
chart = chart.push( let handle = svg::Handle::from_memory(pie_chart_svg(&values, donut).into_bytes());
container(
svg(svg::Handle::from_memory(
pie_chart_svg(&values, donut).into_bytes(),
))
.width(180)
.height(180),
)
.width(Length::Fill)
.align_x(Alignment::Center),
);
let total = values.iter().sum::<f64>(); let total = values.iter().sum::<f64>();
let mut legend: Row<'_, Message> = Row::new().spacing(12); chart = chart.push(
for (index, (label, _, value)) in slices.into_iter().enumerate() { responsive(move |size| {
let color = chart_color(index); let (body_height, side, rows_per_column) = pie_chart_layout(size.width, surface_height);
legend = legend.push( let mut legend = Row::new().spacing(14);
for (column_index, entries) in slices.chunks(rows_per_column).enumerate() {
let mut column = Column::new().spacing(6);
for (row_index, (label, _, value)) in entries.iter().enumerate() {
let index = column_index * rows_per_column + row_index;
let color = chart_color(index);
column = column.push(
row![
container(Space::new().width(10).height(10))
.style(move |_| { container::Style::default().background(color) }),
text(format!(
"{label} {value} · {:.0}%",
values[index] / total * 100.0
))
.size(11),
]
.spacing(5)
.align_y(Alignment::Center),
);
}
legend = legend.push(column);
}
row![ row![
container(Space::new().width(10).height(10)) container(svg(handle.clone()).width(side).height(side))
.style(move |_| container::Style::default().background(color)), .width(side)
text(label).size(11), .align_x(Alignment::Center),
text(format!("{value} · {:.0}%", values[index] / total * 100.0)) container(legend).width(Length::Fill),
.size(10)
.color(muted_text()),
] ]
.spacing(5) .height(body_height)
.align_y(Alignment::Center), .spacing(18)
); .align_y(Alignment::Center)
} .into()
chart.push(legend.wrap()).into() })
.height(Length::Shrink),
);
chart.into()
}
fn pie_chart_layout(width: f32, surface_height: f32) -> (f32, f32, usize) {
let body_height = (surface_height - 80.0).max(0.0);
(
body_height,
(width * 0.68).min(body_height).min(560.0),
((body_height + 6.0) / 20.0).max(1.0) as usize,
)
} }
fn pie_chart_svg(values: &[f64], donut: bool) -> String { fn pie_chart_svg(values: &[f64], donut: bool) -> String {
@@ -1952,5 +2021,7 @@ mod tests {
let donut = pie_chart_svg(&[41.0], true); let donut = pie_chart_svg(&[41.0], true);
assert!(donut.contains("stroke-dasharray")); assert!(donut.contains("stroke-dasharray"));
assert!(donut.contains(">41</text>")); assert!(donut.contains(">41</text>"));
assert_eq!(pie_chart_layout(1_000.0, 400.0), (320.0, 320.0, 16));
assert_eq!(pie_chart_layout(1_000.0, 700.0), (620.0, 560.0, 31));
} }
} }