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