use geometry_core::Rect;
use platform_core::{AccessNode, Role};
use renderer_core::DrawCommand;
use crate::focus;
pub fn snapshot(commands: &[DrawCommand]) -> Vec<AccessNode> {
let controls: Vec<(focus::Exposed, Rect)> = focus::exposed()
.into_iter()
.filter_map(|e| layout_reactive::absolute_rect(e.node).map(|rect| (e, rect)))
.collect();
let focused = focus::current();
let mut nodes: Vec<AccessNode> = controls
.iter()
.map(|(e, rect)| AccessNode {
id: Some(e.id),
role: e.role,
name: String::new(),
rect: *rect,
focused: focused == Some(e.id),
enabled: e.enabled,
toggled: e.toggled,
value: e.value,
})
.collect();
for (text, rect) in drawn_text(commands) {
let owner = controls
.iter()
.enumerate()
.filter(|(_, (_, bounds))| contains(*bounds, rect))
.min_by(|(_, (_, a)), (_, (_, b))| area(*a).total_cmp(&area(*b)));
match owner {
Some((i, _)) => append(&mut nodes[i].name, &text),
None => nodes.push(AccessNode {
id: None,
role: Role::Label,
name: text,
rect,
focused: false,
enabled: true,
toggled: None,
value: None,
}),
}
}
nodes.sort_by(|a, b| {
a.rect
.y
.total_cmp(&b.rect.y)
.then(a.rect.x.total_cmp(&b.rect.x))
});
nodes.retain(|n| n.id.is_some() || !n.name.is_empty());
nodes
}
fn drawn_text(commands: &[DrawCommand]) -> Vec<(String, Rect)> {
commands
.iter()
.filter_map(|command| match command {
DrawCommand::Text { text, rect, .. } => Some((text.to_string(), *rect)),
DrawCommand::RichText { runs, rect, .. } => {
Some((runs.iter().map(|r| r.text.as_ref()).collect(), *rect))
}
_ => None,
})
.filter(|(text, _)| !text.trim().is_empty())
.collect()
}
fn contains(outer: Rect, inner: Rect) -> bool {
let (x, y) = (inner.x + inner.width / 2.0, inner.y + inner.height / 2.0);
x >= outer.x && x <= outer.x + outer.width && y >= outer.y && y <= outer.y + outer.height
}
fn area(rect: Rect) -> f32 {
rect.width * rect.height
}
fn append(name: &mut String, piece: &str) {
if !name.is_empty() {
name.push(' ');
}
name.push_str(piece.trim());
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use layout_core::{AvailableSpace, LayoutStyle};
use renderer_core::{RectStyle, TextStyle};
use super::*;
use crate::container::Container;
use crate::context::{compute_layout, reset_layout_runtime};
use crate::layout_item::{LayoutItem, box_item};
use crate::styled_container::StyledContainer;
use crate::text::Text;
fn text_at(text: &str, rect: Rect) -> DrawCommand {
DrawCommand::Text {
text: Arc::from(text),
rect,
style: Arc::new(TextStyle::new(12.0, renderer_core::Color::BLACK)),
}
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect::new(x, y, w, h)
}
#[test]
fn a_control_is_named_by_the_text_it_draws() {
reset_layout_runtime();
focus::clear();
let label = Text::new(
|| "Save".to_string(),
LayoutStyle::new().width(40.0).height(16.0),
|| TextStyle::new(12.0, renderer_core::Color::BLACK),
)
.unwrap();
let button = StyledContainer::new(
LayoutStyle::new().width(80.0).height(30.0),
|_r| RectStyle::default(),
vec![box_item(label)],
)
.unwrap()
.control(Role::Button)
.on_press(|| {});
let root =
Container::new(LayoutStyle::new().flex_column(), vec![box_item(button)]).unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let nodes = snapshot(&[text_at("Save", rect(10.0, 5.0, 40.0, 16.0))]);
let button = nodes
.iter()
.find(|n| n.id.is_some())
.expect("the button is exposed");
assert_eq!(button.name, "Save");
assert_eq!(button.role, Role::Button);
assert!(button.enabled);
}
#[test]
fn text_outside_any_control_is_still_announced() {
reset_layout_runtime();
focus::clear();
let nodes = snapshot(&[text_at("Delete everything?", rect(0.0, 0.0, 200.0, 20.0))]);
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].role, Role::Label);
assert_eq!(nodes[0].name, "Delete everything?");
assert_eq!(nodes[0].id, None);
}
#[test]
fn a_label_belongs_to_the_smallest_control_around_it() {
let outer = rect(0.0, 0.0, 200.0, 100.0);
let inner = rect(10.0, 10.0, 50.0, 20.0);
assert!(contains(outer, inner));
assert!(contains(inner, rect(20.0, 15.0, 10.0, 8.0)));
assert!(!contains(inner, rect(120.0, 15.0, 10.0, 8.0)));
assert!(area(inner) < area(outer));
}
#[test]
fn a_valued_control_reports_where_it_stands() {
reset_layout_runtime();
focus::clear();
let track = StyledContainer::new(
LayoutStyle::new().width(200.0).height(20.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.control(Role::Slider)
.valued(|| platform_core::NumericValue {
now: 0.25,
min: 0.0,
max: 1.0,
});
let root = Container::new(LayoutStyle::new().flex_column(), vec![box_item(track)]).unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let nodes = snapshot(&[]);
let slider = nodes.iter().find(|n| n.id.is_some()).expect("exposed");
let value = slider.value.expect("a slider carries a number");
assert_eq!(value.now, 0.25);
assert_eq!((value.min, value.max), (0.0, 1.0));
}
#[test]
fn a_control_that_carries_a_state_reports_it() {
reset_layout_runtime();
focus::clear();
let tab = StyledContainer::new(
LayoutStyle::new().width(80.0).height(30.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap()
.control(Role::Tab)
.toggled(|| true);
let root = Container::new(LayoutStyle::new().flex_column(), vec![box_item(tab)]).unwrap();
compute_layout(
root.layout_node(),
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let nodes = snapshot(&[]);
let tab = nodes.iter().find(|n| n.id.is_some()).expect("exposed");
assert_eq!(tab.toggled, Some(true));
}
#[test]
fn nodes_come_back_in_reading_order() {
reset_layout_runtime();
focus::clear();
let nodes = snapshot(&[
text_at("second", rect(0.0, 40.0, 50.0, 16.0)),
text_at("first", rect(0.0, 10.0, 50.0, 16.0)),
text_at("third", rect(60.0, 40.0, 50.0, 16.0)),
]);
let names: Vec<&str> = nodes.iter().map(|n| n.name.as_str()).collect();
assert_eq!(names, vec!["first", "second", "third"]);
}
}