fix: rendering in a2ui surface and viewport aligned
This commit is contained in:
@@ -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,7 +292,14 @@ 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(
|
||||||
|
surface,
|
||||||
|
child,
|
||||||
|
context,
|
||||||
|
context_path,
|
||||||
|
ancestors,
|
||||||
|
surface_height,
|
||||||
|
))
|
||||||
.padding(14)
|
.padding(14)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(overview_style)
|
.style(overview_style)
|
||||||
@@ -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 (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);
|
let color = chart_color(index);
|
||||||
legend = legend.push(
|
column = column.push(
|
||||||
row![
|
row![
|
||||||
container(Space::new().width(10).height(10))
|
container(Space::new().width(10).height(10))
|
||||||
.style(move |_| container::Style::default().background(color)),
|
.style(move |_| { container::Style::default().background(color) }),
|
||||||
text(label).size(11),
|
text(format!(
|
||||||
text(format!("{value} · {:.0}%", values[index] / total * 100.0))
|
"{label} {value} · {:.0}%",
|
||||||
.size(10)
|
values[index] / total * 100.0
|
||||||
.color(muted_text()),
|
))
|
||||||
|
.size(11),
|
||||||
]
|
]
|
||||||
.spacing(5)
|
.spacing(5)
|
||||||
.align_y(Alignment::Center),
|
.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 {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user