use std::any::Any;
use std::collections::VecDeque;
use skia_safe::{Canvas, Color, Matrix, Paint, Point, RRect, Rect, Size};
use crate::control::{GestureCx, Handled, PaintCx};
use crate::fonts::Fonts;
use crate::gestures::{ContextMenuSource, Gesture, GestureKind, MouseButton, Recognizer};
use crate::gpu::Gpu;
use crate::keyboard::{Cursor, InputKey, KeyEvent, KeyKind, Keyboard, Modifiers};
use crate::tree::{Build, ControlId, Cx, Detached, Raw, Tree};
use crate::types::{Dirty, GesturesMode, GpuBackend, LockTouch, RenderingModeType};
use crate::controls::label::LabelSet;
use crate::controls::label_fps::SkiaLabelFps;
use crate::{App, Control, Decoded, Frame, Host, PointerKind, animators, images, layout, paint};
pub const ACCESSIBILITY_INTERVAL_MS: f64 = 1000.0;
pub(crate) type LongPressed = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>)>;
pub(crate) type ContextMenuHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &ContextMenu) -> bool>;
pub(crate) type KeyHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &KeyEvent<'_>) -> bool>;
pub(crate) type FocusHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, bool)>;
pub(crate) type GestureHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &Gesture) -> bool>;
#[derive(Default)]
pub(crate) struct InputHandlers {
pub long_pressing: Option<LongPressed>,
pub context_menu: Option<ContextMenuHandler>,
pub key_down: Option<KeyHandler>,
pub key_up: Option<KeyHandler>,
pub key_char: Option<KeyHandler>,
pub focus_changed: Option<FocusHandler>,
pub hovered: Option<FocusHandler>,
pub consume_gestures: Option<GestureHandler>,
pub listen_keys: bool,
pub listen_history: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ContextMenu {
pub location: Point,
pub pixels: Point,
pub source: ContextMenuSource,
pub control: Option<ControlId>,
pub local: Point,
}
#[derive(Clone, Debug, PartialEq)]
pub enum HistoryOp {
Push { depth: u32, hash: Option<String> },
Replace { depth: u32, hash: Option<String> },
Back,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FrameStats {
pub frame_time_ms: f32,
pub fps: u32,
}
pub struct Aria;
impl Aria {
pub const BUTTON: &'static str = "button";
pub const LINK: &'static str = "link";
pub const CHECKBOX: &'static str = "checkbox";
pub const RADIO: &'static str = "radio";
pub const SWITCH: &'static str = "switch";
pub const SLIDER: &'static str = "slider";
pub const SPIN_BUTTON: &'static str = "spinbutton";
pub const TEXT_BOX: &'static str = "textbox";
pub const SEARCH_BOX: &'static str = "searchbox";
pub const COMBO_BOX: &'static str = "combobox";
pub const LIST_BOX: &'static str = "listbox";
pub const OPTION: &'static str = "option";
pub const TAB: &'static str = "tab";
pub const TAB_PANEL: &'static str = "tabpanel";
pub const TAB_LIST: &'static str = "tablist";
pub const MENU: &'static str = "menu";
pub const MENU_ITEM: &'static str = "menuitem";
pub const MENU_ITEM_CHECKBOX: &'static str = "menuitemcheckbox";
pub const MENU_ITEM_RADIO: &'static str = "menuitemradio";
pub const SCROLL_BAR: &'static str = "scrollbar";
pub const TEXT: &'static str = "text";
pub const HEADING: &'static str = "heading";
pub const IMG: &'static str = "img";
pub const LIST: &'static str = "list";
pub const LIST_ITEM: &'static str = "listitem";
pub const SEPARATOR: &'static str = "separator";
pub const PROGRESS_BAR: &'static str = "progressbar";
pub const TOOLTIP: &'static str = "tooltip";
pub const DIALOG: &'static str = "dialog";
pub const ALERT_DIALOG: &'static str = "alertdialog";
pub const STATUS: &'static str = "status";
pub const ALERT: &'static str = "alert";
pub const GROUP: &'static str = "group";
pub const REGION: &'static str = "region";
pub const NAVIGATION: &'static str = "navigation";
pub const MAIN: &'static str = "main";
pub const GRID: &'static str = "grid";
pub const TOOLBAR: &'static str = "toolbar";
pub const RADIO_GROUP: &'static str = "radiogroup";
pub const MENU_BAR: &'static str = "menubar";
pub const PRESENTATION: &'static str = "presentation";
pub const SCROLL_VIEW: &'static str = "scrollview";
pub const LIVE_POLITE: &'static str = "polite";
pub const LIVE_ASSERTIVE: &'static str = "assertive";
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessibilityTextLine {
pub text: String,
pub left: f32,
pub top: f32,
pub width: f32,
pub height: f32,
pub font_family: String,
pub font_weight: i32,
pub font_size: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessibilityValue {
pub now: f64,
pub min: f64,
pub max: f64,
pub step: f64,
pub text: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessibilityNode {
pub id: u32,
pub control: ControlId,
pub role: &'static str,
pub label: String,
pub hint: String,
pub rect: Rect,
pub can_interact: bool,
pub is_pressed: Option<bool>,
pub value: Option<AccessibilityValue>,
pub scrolls: (bool, bool),
pub live: &'static str,
pub text_lines: Vec<AccessibilityTextLine>,
pub parent: Option<u32>,
pub group: Option<(u32, u32)>,
}
pub(crate) fn is_control_role(role: &str) -> bool {
matches!(
role,
Aria::BUTTON | Aria::LINK | Aria::CHECKBOX | Aria::RADIO | Aria::SWITCH | Aria::SLIDER | Aria::SPIN_BUTTON | Aria::TEXT_BOX | Aria::SEARCH_BOX | Aria::COMBO_BOX | Aria::OPTION | Aria::TAB | Aria::MENU_ITEM | Aria::MENU_ITEM_CHECKBOX | Aria::MENU_ITEM_RADIO
)
}
pub(crate) fn said_by_child(nodes: &[AccessibilityNode], node: &AccessibilityNode) -> bool {
nodes.iter().any(|c| c.parent == Some(node.id) && (c.role == Aria::TEXT || c.role == Aria::HEADING) && c.text_lines.is_empty() && c.label == node.label)
}
type UiKeyHandler<S> = Box<dyn FnMut(&mut S, &KeyEvent<'_>, &mut Cx<'_>) -> bool>;
pub struct Ui<S: 'static> {
pub tree: Tree,
pub state: S,
pub fonts: Fonts,
pub background: Color,
rendering_mode: RenderingModeType,
gpu_backend: GpuBackend,
gestures: GesturesMode,
input: VecDeque<(PointerKind, MouseButton, bool, Point, f64)>,
recognizer: Recognizer,
owner: Option<ControlId>,
state_dirty: bool,
font_urls: Vec<(String, String, i32, bool, bool)>,
fonts_pending: usize,
scale: f32,
fps: Fps,
insets_handler: Option<Box<dyn FnMut(&mut S, crate::types::Thickness, &mut Cx<'_>)>>,
pub keyboard: Keyboard,
key_down: Vec<UiKeyHandler<S>>,
key_up: Vec<UiKeyHandler<S>>,
key_char: Vec<UiKeyHandler<S>>,
context_menu: Option<Box<dyn FnMut(&mut S, &ContextMenu, &mut Cx<'_>) -> bool>>,
visibility: Option<Box<dyn FnMut(&mut S, bool, &mut Cx<'_>)>>,
resized: Option<Box<dyn FnMut(&mut S, Size, &mut Cx<'_>)>>,
insets_changed: bool,
frame_times: VecDeque<f64>,
prepared: Option<(f64, Rect, std::time::Instant, bool)>,
over: Vec<ControlId>,
over_next: Vec<ControlId>,
hovered: Vec<ControlId>,
hovered_next: Vec<ControlId>,
last_pointer: Option<Point>,
hover_paused: bool,
cursor: Cursor,
cursor_changed: bool,
text_input: Option<Rect>,
text_input_changed: bool,
accessibility_built_ms: f64,
accessibility_changed: bool,
accessibility_on: bool,
accessibility_stale: bool,
accessibility_stack: Vec<(ControlId, Matrix, Option<u32>)>,
keyboard_navigation: bool,
focus_ring: bool,
group_items: Vec<(ControlId, ControlId)>,
keyboard_moved: Option<u32>,
accessibility_next: Vec<AccessibilityNode>,
accessibility_revision: u32,
accessibility_roles: Vec<(std::any::TypeId, &'static str)>,
by_z: Vec<ControlId>,
}
#[derive(Default)]
struct Fps {
on: bool,
chained: bool,
since_ms: f64,
frames: u32,
value: f32,
shown: f32,
}
impl Fps {
fn measure(&mut self, time_ms: f64) {
if !self.chained {
(self.since_ms, self.frames) = (time_ms, 0);
}
self.frames += 1;
let span = time_ms - self.since_ms;
if span >= 500.0 {
self.value = ((self.frames - 1) as f64 * 1000.0 / span) as f32;
(self.since_ms, self.frames) = (time_ms, 1);
}
}
fn draw(&mut self, canvas: &Canvas, fonts: &Fonts, height: f32, scale: f32) {
let Some(font) = fonts.font("", 16.0 * scale) else { return };
let text = if self.value > 0.0 { format!("FPS {:.0}", self.value) } else { "FPS -".to_owned() };
let (text_width, _) = font.measure_str(&text, None);
let pill = Rect::from_xywh(8.0 * scale, height - 34.0 * scale, text_width + 20.0 * scale, 26.0 * scale);
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_color(Color::from_argb(220, 0, 0, 0));
canvas.draw_rrect(RRect::new_rect_xy(pill, 6.0 * scale, 6.0 * scale), &paint);
paint.set_color(Color::from_argb(255, 80, 255, 120));
canvas.draw_str(text, (pill.left + 10.0 * scale, pill.top + 19.0 * scale), &font, &paint);
}
}
impl<S: 'static> Ui<S> {
pub fn new<B: Into<Detached>>(mut state: S, build: impl FnOnce(&mut S) -> B) -> Self {
let mut tree = Tree::default();
tree.mount(None, build(&mut state));
Self {
tree,
state,
fonts: Fonts::default(),
background: Color::TRANSPARENT,
rendering_mode: RenderingModeType::Accelerated,
gpu_backend: GpuBackend::Auto,
gestures: GesturesMode::Enabled,
input: VecDeque::new(),
recognizer: Recognizer::default(),
owner: None,
state_dirty: true,
font_urls: Vec::new(),
fonts_pending: 0,
scale: 1.0,
fps: Fps::default(),
insets_handler: None,
keyboard: Keyboard::default(),
key_down: Vec::new(),
key_up: Vec::new(),
key_char: Vec::new(),
context_menu: None,
resized: None,
visibility: None,
insets_changed: false,
frame_times: VecDeque::with_capacity(256),
prepared: None,
over: Vec::with_capacity(32),
over_next: Vec::with_capacity(32),
hovered: Vec::with_capacity(8),
hovered_next: Vec::with_capacity(8),
last_pointer: None,
hover_paused: false,
cursor: Cursor::Default,
cursor_changed: false,
text_input: None,
text_input_changed: false,
accessibility_built_ms: f64::NEG_INFINITY,
accessibility_changed: false,
accessibility_on: false,
accessibility_stale: false,
accessibility_stack: Vec::new(),
keyboard_navigation: false,
focus_ring: false,
group_items: Vec::new(),
keyboard_moved: None,
accessibility_next: Vec::new(),
accessibility_revision: 0,
accessibility_roles: Vec::new(),
by_z: Vec::new(),
}
}
pub fn font(self, alias: &str, url: &str) -> Self {
self.font_weight(alias, url, 400)
}
pub fn font_weight(mut self, alias: &str, url: &str, weight: i32) -> Self {
self.fonts.register(alias);
self.font_urls.push((alias.to_owned(), url.to_owned(), weight, true, false));
self.fonts_pending += 1;
self
}
pub fn font_fallback(mut self, alias: &str, url: &str) -> Self {
self.font_urls.push((alias.to_owned(), url.to_owned(), 400, false, false));
self
}
pub fn font_bytes(mut self, alias: &str, bytes: &[u8]) -> Self {
self.fonts.add(alias, bytes);
self
}
pub fn preload_images<I: AsRef<str>>(mut self, sources: impl IntoIterator<Item = I>) -> Self {
self.tree.images.preload(sources);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = color;
self
}
pub fn rendering_mode(mut self, mode: RenderingModeType) -> Self {
self.rendering_mode = mode;
self
}
pub fn gpu_backend(mut self, backend: GpuBackend) -> Self {
self.gpu_backend = backend;
self
}
pub fn gestures(mut self, mode: GesturesMode) -> Self {
self.gestures = mode;
self
}
pub fn mobile_fullscreen(mut self, on: bool) -> Self {
self.tree.mobile_fullscreen = on;
self
}
pub fn on_safe_insets_changed(mut self, f: impl FnMut(&mut S, crate::types::Thickness, &mut Cx<'_>) + 'static) -> Self {
self.insets_handler = Some(Box::new(f));
self
}
pub fn show_fps(mut self, on: bool) -> Self {
self.fps.on = on;
self
}
pub fn on_key_down(mut self, f: impl FnMut(&mut S, &KeyEvent<'_>, &mut Cx<'_>) -> bool + 'static) -> Self {
self.key_down.push(Box::new(f));
self
}
pub fn on_key_up(mut self, f: impl FnMut(&mut S, &KeyEvent<'_>, &mut Cx<'_>) -> bool + 'static) -> Self {
self.key_up.push(Box::new(f));
self
}
pub fn on_key_char(mut self, f: impl FnMut(&mut S, &KeyEvent<'_>, &mut Cx<'_>) -> bool + 'static) -> Self {
self.key_char.push(Box::new(f));
self
}
pub fn on_context_menu(mut self, f: impl FnMut(&mut S, &ContextMenu, &mut Cx<'_>) -> bool + 'static) -> Self {
self.context_menu = Some(Box::new(f));
self
}
pub fn on_canvas_resized(mut self, f: impl FnMut(&mut S, Size, &mut Cx<'_>) + 'static) -> Self {
self.resized = Some(Box::new(f));
self
}
pub fn on_visibility_changed(mut self, f: impl FnMut(&mut S, bool, &mut Cx<'_>) + 'static) -> Self {
self.visibility = Some(Box::new(f));
self
}
pub fn frame_stats(&self) -> FrameStats {
self.tree.frame_stats
}
pub fn set_history_enabled(&mut self, on: bool) {
self.tree.history_enabled = on;
}
pub fn canvas_size(&self) -> Size {
self.tree.canvas_size
}
pub fn state_changed(&mut self) {
self.state_dirty = true;
}
pub fn needs_frame(&self) -> bool {
if self.fonts_pending > 0 {
return false;
}
let animating = animators::running(&self.tree);
let long_press = self.recognizer.long_press_due().is_some_and(|due| due <= self.tree.time_ms);
let changed = self.tree.queue.iter().any(|&id| !self.tree.hidden_above(id));
self.state_dirty || self.tree.needs_frame || animating || changed || !self.input.is_empty() || long_press
}
pub fn wake_at(&self) -> Option<f64> {
let accessibility = (self.accessibility_on && self.accessibility_stale).then(|| self.accessibility_built_ms + ACCESSIBILITY_INTERVAL_MS);
[animators::next_wake(&self.tree), self.recognizer.long_press_due(), accessibility]
.into_iter()
.flatten()
.filter(|t| *t > self.tree.time_ms)
.min_by(f64::total_cmp)
}
pub fn pointer(&mut self, kind: PointerKind, x: f32, y: f32, time_ms: f64) {
self.pointer_button(kind, MouseButton::Left, x, y, time_ms);
}
pub fn pointer_button(&mut self, kind: PointerKind, button: MouseButton, x: f32, y: f32, time_ms: f64) {
self.queue_pointer(kind, button, false, x, y, time_ms);
}
pub fn pointer_touch(&mut self, kind: PointerKind, x: f32, y: f32, time_ms: f64) {
self.queue_pointer(kind, MouseButton::Left, true, x, y, time_ms);
}
fn queue_pointer(&mut self, kind: PointerKind, button: MouseButton, touch: bool, x: f32, y: f32, time_ms: f64) {
if kind == PointerKind::Down && std::mem::take(&mut self.focus_ring) {
self.tree.needs_frame = true;
}
let event = (kind, button, touch, Point::new(x, y), time_ms);
if kind == PointerKind::Hover
&& let Some(last) = self.input.back_mut().filter(|last| last.0 == PointerKind::Hover)
{
*last = event;
return;
}
self.input.push_back(event);
}
pub fn wheel(&mut self, x: f32, y: f32, delta: f32, time_ms: f64) -> bool {
self.wheel_on_axis(x, y, delta, false, time_ms)
}
pub fn wheel_horizontal(&mut self, x: f32, y: f32, delta: f32, time_ms: f64) -> bool {
self.wheel_on_axis(x, y, delta, true, time_ms)
}
fn wheel_on_axis(&mut self, x: f32, y: f32, delta: f32, horizontal: bool, time_ms: f64) -> bool {
if self.fonts_pending > 0 || delta == 0.0 {
return false;
}
self.flush_input();
self.tree.time_ms = self.tree.time_ms.max(time_ms);
let mut gesture = plain_gesture(GestureKind::Wheel, Point::new(x, y), time_ms);
(gesture.wheel, gesture.wheel_horizontal) = (delta, horizontal);
let consumed = self.process_gesture(gesture);
self.used(consumed)
}
pub fn context_menu(&mut self, x: f32, y: f32, source: ContextMenuSource, time_ms: f64) -> bool {
if self.fonts_pending > 0 {
return false;
}
self.flush_input();
self.tree.time_ms = self.tree.time_ms.max(time_ms);
let mut gesture = plain_gesture(GestureKind::ContextMenu, Point::new(x, y), time_ms);
gesture.source = source;
let consumed = self.process_gesture(gesture);
if self.used(consumed) {
return true;
}
let Some(fallback) = self.context_menu.as_mut() else { return false };
let pixels = Point::new(x, y);
let scale = self.scale.max(0.1);
let menu = ContextMenu { location: Point::new(x / scale, y / scale), pixels, source, control: None, local: Point::default() };
let taken = fallback(&mut self.state, &menu, &mut Cx { tree: &mut self.tree });
self.state_dirty = true;
self.apply_focus_request();
taken
}
fn used(&self, consumed: Option<ControlId>) -> bool {
consumed.is_some_and(|id| self.tree.base(id).is_some_and(|b| !b.p.block_gestures_below))
}
pub fn key(&mut self, kind: KeyKind, key: InputKey, text: &str, modifiers: Modifiers, repeat: bool) -> bool {
if self.fonts_pending > 0 {
return false;
}
self.flush_input();
self.keyboard.apply(kind, key, modifiers);
let Ui { tree, state, keyboard, key_down, key_up, key_char, state_dirty, .. } = self;
let event = KeyEvent { kind, key, text, repeat, keyboard };
let focused = tree.focused.filter(|f| tree.node(*f).is_some());
let mut handled = focused.is_some_and(|f| key_to_control(tree, state, state_dirty, f, &event));
let overlay = tree.accessibility_focused.filter(|a| Some(*a) != focused && tree.node(*a).is_some());
if let Some(overlay) = overlay.filter(|_| !handled) {
handled = key_to_control(tree, state, state_dirty, overlay, &event);
}
tree.key_listeners.retain(|id| tree.nodes.get(id.index as usize).is_some_and(|n| n.as_ref().is_some_and(|n| n.id == *id)));
for i in 0..tree.key_listeners.len() {
let Some(&id) = tree.key_listeners.get(i) else { break };
if Some(id) != focused {
handled |= key_to_control(tree, state, state_dirty, id, &event);
}
}
let handlers = match kind {
KeyKind::Down => key_down,
KeyKind::Up => key_up,
KeyKind::Char => key_char,
};
for handler in handlers.iter_mut() {
handled |= handler(&mut *state, &event, &mut Cx { tree: &mut *tree });
*state_dirty = true;
}
self.apply_focus_request();
if !handled && kind == KeyKind::Down {
if self.keyboard_navigation {
handled = self.navigate(key, modifiers.shift);
} else if self.accessibility_on && GROUP_KEYS.contains(&key) {
handled = self.move_in_group(key);
}
}
handled
}
fn navigate(&mut self, key: InputKey, back: bool) -> bool {
match key {
"Tab" => self.tab(back),
"Enter" | "NumpadEnter" | "Space" => match self.accessibility_focused() {
Some(focused) => {
self.focus_ring = true;
self.accessibility_activate(focused.index);
true
}
None => false,
},
"Escape" => {
let had = self.tree.accessibility_focused.take().is_some();
self.focus_ring = false;
self.tree.needs_frame = true;
had
}
_ if GROUP_KEYS.contains(&key) => self.move_in_group(key),
_ => false,
}
}
fn fresh_accessibility(&mut self) {
if self.accessibility_stale || self.tree.accessibility.is_empty() {
let scale = self.scale.max(0.1);
let size = self.tree.canvas_size;
self.build_accessibility(size.width * scale, size.height * scale, scale, self.tree.time_ms);
}
}
fn group_of(&self, control: ControlId) -> Option<ControlId> {
let nodes = &self.tree.accessibility;
let mut at = nodes.iter().find(|n| n.control == control)?.parent;
while let Some(id) = at {
let node = nodes.iter().find(|n| n.id == id)?;
if GROUP_ROLES.contains(&node.role) {
return Some(node.control);
}
at = node.parent;
}
None
}
fn tab_stops(&self) -> Vec<ControlId> {
let mut stops = Vec::new();
let mut groups = Vec::new();
for n in self.tree.accessibility.iter().filter(|n| n.can_interact) {
match self.group_of(n.control) {
None => stops.push(n.control),
Some(group) if !groups.contains(&group) => {
groups.push(group);
let current = self.group_items.iter().find(|(g, _)| *g == group).map(|(_, item)| *item);
let alive = current.filter(|c| self.tree.accessibility.iter().any(|m| m.control == *c && m.can_interact));
stops.push(alive.unwrap_or(n.control));
}
Some(_) => {}
}
}
stops
}
fn tab(&mut self, back: bool) -> bool {
self.fresh_accessibility();
let stops = self.tab_stops();
let focused = self.accessibility_focused().or_else(|| self.focused());
let group = focused.and_then(|f| self.group_of(f));
let current = focused.and_then(|f| stops.iter().position(|s| *s == f || (group.is_some() && self.group_of(*s) == group)));
let next = match (current, back) {
(None, false) => (!stops.is_empty()).then_some(0),
(None, true) => stops.len().checked_sub(1),
(Some(i), false) => (i + 1 < stops.len()).then_some(i + 1),
(Some(i), true) => i.checked_sub(1),
};
match next {
Some(i) => self.keyboard_focus(stops[i]),
None => {
self.tree.accessibility_focused = None;
self.focus_ring = false;
self.tree.needs_frame = true;
}
}
true
}
fn move_in_group(&mut self, key: InputKey) -> bool {
use crate::controls::layout::{LayoutType, SkiaLayout};
#[derive(PartialEq)]
enum Axis {
Vertical,
Horizontal,
Both,
}
let Some(focused) = self.accessibility_focused() else { return false };
self.fresh_accessibility();
let Some(group) = self.group_of(focused) else { return false };
let tree = &self.tree;
let mut item = focused;
while let Some(parent) = tree.parent(item).filter(|p| *p != group) {
item = parent;
}
let items = tree.children(group).to_vec();
let Some(current) = items.iter().position(|i| *i == item) else { return false };
let layout = tree.find::<SkiaLayout>(group).map(|l| (l.p.layout_type, l.p.split));
let axis = match layout {
Some((LayoutType::Column, split)) if split <= 1 => Axis::Vertical,
Some((LayoutType::Row, _)) => Axis::Horizontal,
Some((LayoutType::Absolute, _)) | None => Axis::Vertical,
Some(_) => Axis::Both,
};
let row = match layout {
_ if axis != Axis::Both => 1,
Some((_, split)) if split > 1 => split as usize,
_ => {
let at = tree.base(items[0]).map(|b| b.rect).unwrap_or_default();
let on_row = |i: &&ControlId| tree.base(**i).is_some_and(|b| b.p.is_visible && (b.rect.center_y() - at.center_y()).abs() < at.height() / 2.0);
items.iter().filter(on_row).count().max(1)
}
};
let page = || {
let mut scroll = tree.parent(group);
while let Some(s) = scroll.filter(|s| tree.find::<crate::controls::scroll::SkiaScroll>(*s).is_none()) {
scroll = tree.parent(s);
}
let (viewport, size) = (tree.base(scroll?)?.rect, tree.base(item)?.rect);
let (viewport, size) = if axis == Axis::Horizontal { (viewport.width(), size.width()) } else { (viewport.height(), size.height()) };
Some(if size > 0.0 { ((viewport / size) as usize).max(1) } else { 1 })
};
let (count, last) = (items.len(), items.len() - 1);
let (target, step) = match key {
"ArrowDown" if axis == Axis::Vertical => (current + 1, 1),
"ArrowRight" if axis != Axis::Vertical => (current + 1, 1),
"ArrowUp" if axis == Axis::Vertical => (current.wrapping_sub(1), -1),
"ArrowLeft" if axis != Axis::Vertical => (current.wrapping_sub(1), -1),
"ArrowDown" if axis == Axis::Both => ((current + row).min(last), 1),
"ArrowUp" if axis == Axis::Both => (current.saturating_sub(row), -1),
"Home" => (0, 1),
"End" => (last, -1),
"PageDown" => match page() {
Some(page) => ((current + page * row).min(last), 1),
None => return false,
},
"PageUp" => match page() {
Some(page) => (current.saturating_sub(page * row), -1),
None => return false,
},
_ => return false,
};
if target >= count || target == current {
return true;
}
let mut at = target as isize;
while let Some(&candidate) = usize::try_from(at).ok().and_then(|i| items.get(i)) {
if let Some(node) = self.item_target(candidate) {
self.keyboard_focus(node);
return true;
}
at += step;
}
true
}
fn item_target(&self, item: ControlId) -> Option<ControlId> {
let inside = |mut at: ControlId| loop {
if at == item {
return true;
}
match self.tree.parent(at) {
Some(parent) => at = parent,
None => return false,
}
};
let nodes = &self.tree.accessibility;
let node = nodes.iter().find(|n| n.can_interact && n.control == item).or_else(|| nodes.iter().find(|n| n.can_interact && inside(n.control)));
node.map(|n| n.control)
}
fn keyboard_focus(&mut self, control: ControlId) {
if let Some(previous) = self.tree.accessibility_focused.filter(|p| *p != control) {
self.accessibility_focus(previous.index, false);
}
self.accessibility_focus(control.index, true);
if let Some(group) = self.group_of(control) {
self.group_items.retain(|(g, _)| *g != group);
self.group_items.push((group, control));
}
self.keyboard_moved = Some(control.index);
self.focus_ring = true;
self.tree.needs_frame = true;
}
fn draw_focus_ring(&self, canvas: &Canvas, scale: f32) {
let Some(focused) = self.accessibility_focused().filter(|_| self.focus_ring && self.keyboard_navigation) else { return };
if !shown(&self.tree, focused) || !self.tree.accessibility.iter().any(|n| n.control == focused && n.can_interact) {
return;
}
let Some(rect) = drawn_rect(&self.tree, focused) else { return };
let (out, radius) = (2.0 * scale, 6.0 * scale);
let ring = Rect::new(rect.left - out, rect.top - out, rect.right + out, rect.bottom + out);
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_style(skia_safe::paint::Style::Stroke);
paint.set_stroke_width(2.0 * scale);
paint.set_color(Color::from_rgb(0x6E, 0xA8, 0xFE));
canvas.draw_round_rect(ring, radius, radius, &paint);
}
pub fn blur(&mut self) {
self.keyboard.reset();
}
pub fn system_back(&mut self) -> bool {
use crate::controls::shell::SkiaShell;
let tree = &self.tree;
let depth = |id: ControlId| std::iter::successors(tree.parent(id), |p| tree.parent(*p)).count();
let shell = tree
.nodes
.iter()
.flatten()
.map(|n| n.id)
.filter(|id| shown(tree, *id) && tree.find::<SkiaShell>(*id).is_some_and(|s| s.can_go_back()))
.max_by_key(|id| depth(*id));
let Some(shell) = shell else { return false };
Cx { tree: &mut self.tree }.go_back(shell, true);
self.tree.needs_frame = true;
true
}
pub fn focus_out(&mut self) {
self.focus(None);
self.tree.accessibility_focused = None;
self.focus_ring = false;
self.tree.needs_frame = true;
}
pub fn focused(&self) -> Option<ControlId> {
self.tree.focused.filter(|f| self.tree.node(*f).is_some())
}
pub fn focus(&mut self, id: Option<ControlId>) {
let id = id.filter(|id| self.tree.node(*id).is_some());
let old = self.focused();
if old == id {
return;
}
self.tree.focused = id;
if let Some(old) = old {
self.fire_focus(old, false);
}
if let Some(new) = id {
self.fire_focus(new, true);
}
self.update_text_input();
self.tree.needs_frame = true;
}
fn fire_focus(&mut self, id: ControlId, focused: bool) {
with_kind(&mut self.tree, id, |kind, cx| kind.on_focus_changed(cx, focused));
if !self.tree.node(id).is_some_and(|n| n.handlers.input.as_ref().is_some_and(|h| h.focus_changed.is_some())) {
return;
}
let Some(mut node) = self.tree.take(id) else { return };
if let Some(control) = node.kind.as_deref_mut()
&& let Some(handler) = node.handlers.input.as_deref_mut().and_then(|h| h.focus_changed.as_mut())
{
let mut queue = Vec::new();
let raw = Raw { id, control, base: &mut node.base, queue: &mut queue };
handler(raw, &mut self.state, &mut Cx { tree: &mut self.tree }, focused);
self.tree.queue.append(&mut queue);
self.state_dirty = true;
}
self.tree.put_back(node);
}
fn apply_focus_request(&mut self) {
if let Some(request) = self.tree.focus_request.take() {
self.focus(request);
}
}
fn update_text_input(&mut self) {
let area = self.focused().and_then(|f| {
let node = self.tree.node(f)?;
node.kind.as_deref().is_some_and(|k| k.wants_text_input()).then(|| {
let (r, s) = (node.base.rect, node.base.scale.max(0.1));
Rect::new(r.left / s, r.top / s, r.right / s, r.bottom / s)
})
});
if area != self.text_input {
self.text_input = area;
self.text_input_changed = true;
}
}
pub fn text_input(&self) -> Option<Rect> {
self.text_input
}
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn pointer_over(&self) -> &[ControlId] {
&self.over
}
pub fn hovered(&self) -> &[ControlId] {
&self.hovered
}
pub fn accessibility_nodes(&self) -> &[AccessibilityNode] {
&self.tree.accessibility
}
pub fn accessibility_focused(&self) -> Option<ControlId> {
self.tree.accessibility_focused.filter(|f| self.tree.node(*f).is_some())
}
pub fn accessibility_revision(&self) -> u32 {
self.accessibility_revision
}
pub fn default_accessibility_role<T: Control>(mut self, role: &'static str) -> Self {
let type_id = std::any::TypeId::of::<T>();
self.accessibility_roles.retain(|(t, _)| *t != type_id);
self.accessibility_roles.push((type_id, role));
self
}
pub fn accessibility_activate(&mut self, id: u32) {
let Some(control) = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id) else { return };
let Some(rect) = self.tree.base(control).map(|b| b.rect) else { return };
let scale = self.scale.max(0.1);
let location = match self.tree.accessibility.iter().find(|n| n.control == control) {
Some(n) => Point::new(n.rect.center_x() * scale, n.rect.center_y() * scale),
None => Point::new(rect.center_x(), rect.center_y()),
};
let mut gesture = plain_gesture(GestureKind::Tapped, location, self.tree.time_ms);
gesture.start = location;
let point = to_local(&self.tree, control, location);
let claimed_before = self.tree.focus_request.is_some();
self.tree.gesture = Some(gesture);
let routing = Routing { state: &mut self.state, state_dirty: &mut self.state_dirty, over: &mut self.over_next, by_z: &mut self.by_z };
let consumed = Router { tree: &mut self.tree, r: routing }.route(control, &gesture, point);
self.tree.gesture = None;
self.tree.needs_frame = true;
if !claimed_before && self.tree.focus_request.is_none() && consumed.is_some() {
self.focus_after_tap(consumed);
}
self.apply_focus_request();
self.accessibility_now();
}
pub fn accessibility_focus(&mut self, id: u32, focused: bool) {
let control = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id);
let text_input = |ui: &Self, id: ControlId| ui.tree.node(id).and_then(|n| n.kind.as_deref()).is_some_and(|k| k.wants_text_input());
if focused {
self.tree.accessibility_focused = control;
let Some(control) = control else { return };
ensure_visible(&mut self.tree, control);
if text_input(self, control) {
self.focus(Some(control));
} else if self.focused().is_some_and(|f| text_input(self, f)) {
self.focus(None);
}
} else if self.tree.accessibility_focused == control {
self.tree.accessibility_focused = None;
if control.is_some() && self.focused() == control && control.is_some_and(|c| text_input(self, c)) {
self.focus(None);
}
}
}
pub fn accessibility_adjust(&mut self, id: u32, up: bool) {
let Some(control) = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id) else { return };
let Ui { tree, state, keyboard, state_dirty, .. } = self;
let key = if up { "ArrowUp" } else { "ArrowDown" };
let event = KeyEvent { kind: KeyKind::Down, key, text: "", repeat: false, keyboard };
key_to_control(tree, state, state_dirty, control, &event);
self.accessibility_now();
}
fn accessibility_now(&mut self) {
self.accessibility_built_ms = f64::NEG_INFINITY;
self.tree.needs_frame = true;
}
pub fn accessibility_set_value(&mut self, id: u32, value: f64) {
let Some(control) = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id) else { return };
with_kind(&mut self.tree, control, |kind, cx| kind.accessibility_set_value(cx, value));
self.accessibility_now();
}
pub fn accessibility_scroll(&mut self, id: u32, x: f32, y: f32) {
use crate::controls::scroll::SkiaScroll;
let Some(control) = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id) else { return };
let (Some(scroll), Some(base)) = (self.tree.find::<SkiaScroll>(control), self.tree.base(control)) else { return };
let page = base.rect.size() * (0.9 / base.scale.max(0.1));
let (to_x, to_y) = (scroll.viewport_offset_x() - x * page.width, scroll.viewport_offset_y() - y * page.height);
Cx { tree: &mut self.tree }.scroll_to(control, to_x, to_y, 250);
self.tree.needs_frame = true;
}
pub fn accessibility_scroll_into_view(&mut self, id: u32) {
let Some(control) = self.tree.nodes.get(id as usize).and_then(|n| n.as_ref()).map(|n| n.id) else { return };
ensure_visible(&mut self.tree, control);
self.tree.needs_frame = true;
}
fn flush_input(&mut self) {
while let Some((kind, button, touch, location, time_ms)) = self.input.pop_front() {
match kind {
PointerKind::Hover if self.hover_paused => self.last_pointer = Some(location),
PointerKind::Hover => self.hover(Some(location), time_ms),
PointerKind::Leave => self.hover(None, time_ms),
_ => {
for gesture in self.recognizer.feed(kind, button, touch, location, time_ms, self.scale).into_iter().flatten() {
self.process_gesture(gesture);
}
}
}
}
if let Some(gesture) = self.recognizer.long_press(self.tree.time_ms) {
self.process_gesture(gesture);
}
}
fn hover(&mut self, location: Option<Point>, time_ms: f64) {
self.last_pointer = location;
self.tree.hover_check = false;
self.over_next.clear();
if let Some(location) = location {
self.process_gesture(plain_gesture(GestureKind::Pointer, location, time_ms));
}
let Ui { tree, state, state_dirty, over, over_next, by_z, .. } = self;
let mut router = Router { tree: &mut *tree, r: Routing { state, state_dirty, over: over_next, by_z } };
for i in 0..router.r.over.len() {
let id = router.r.over[i];
if !over.contains(&id) {
let local = location.map_or(Point::default(), |l| to_local(router.tree, id, l));
router.deliver(id, &plain_gesture(GestureKind::PointerEnter, location.unwrap_or_default(), time_ms), local);
}
}
for i in 0..over.len() {
let id = over[i];
if !router.r.over.contains(&id) {
router.deliver(id, &plain_gesture(GestureKind::PointerExit, location.unwrap_or_default(), time_ms), Point::default());
}
}
std::mem::swap(over, over_next);
self.update_hovered();
let Ui { tree, over, .. } = self;
let mut cursor = Cursor::Default;
for &id in over.iter().rev() {
let Some(node) = tree.node(id) else { continue };
if let Some(c) = location.and_then(|l| node.kind.as_deref()?.cursor(to_local(tree, id, l))) {
cursor = c;
break;
}
let interactive = node.base.p.accessibility_can_interact.unwrap_or(node.handlers.tapped.is_some());
if interactive {
cursor = Cursor::Pointer;
break;
}
}
if cursor != self.cursor {
self.cursor = cursor;
self.cursor_changed = true;
}
}
fn update_hovered(&mut self) {
let Ui { tree, over, hovered_next, .. } = self;
hovered_next.clear();
for &id in over.iter() {
let Some(node) = tree.node(id) else { continue };
let takes = node.base.p.receives_hover.unwrap_or_else(|| node.kind.as_deref().is_some_and(|k| k.receives_hover()));
if takes {
hovered_next.push(id);
}
}
for i in 0..self.hovered.len() {
let id = self.hovered[i];
if !self.hovered_next.contains(&id) {
self.set_hovered(id, false);
}
}
for i in 0..self.hovered_next.len() {
let id = self.hovered_next[i];
if !self.hovered.contains(&id) {
self.set_hovered(id, true);
}
}
std::mem::swap(&mut self.hovered, &mut self.hovered_next);
}
fn set_hovered(&mut self, id: ControlId, on: bool) {
let Some(node) = self.tree.node_mut(id) else { return };
if node.base.hovered == on {
return;
}
node.base.hovered = on;
if !node.handlers.input.as_ref().is_some_and(|h| h.hovered.is_some()) {
return;
}
let Some(mut node) = self.tree.take(id) else { return };
if let Some(control) = node.kind.as_deref_mut()
&& let Some(handler) = node.handlers.input.as_deref_mut().and_then(|h| h.hovered.as_mut())
{
let mut queue = Vec::new();
let raw = Raw { id, control, base: &mut node.base, queue: &mut queue };
handler(raw, &mut self.state, &mut Cx { tree: &mut self.tree }, on);
self.tree.queue.append(&mut queue);
self.state_dirty = true;
}
self.tree.put_back(node);
self.tree.needs_frame = true;
}
fn hover_after_frame(&mut self, time_ms: f64) {
let tree = &self.tree;
let moving = tree.movers.iter().any(|&id| {
tree.node(id).is_some_and(|n| n.base.p.is_visible && n.kind.as_deref().and_then(|k| k.moves_content()) == Some(true))
});
if moving {
self.hover_paused = true;
return;
}
if std::mem::take(&mut self.hover_paused) {
self.tree.hover_check = true;
}
let tree = &self.tree;
let shown = |mut id: Option<ControlId>| {
while let Some(node) = id.and_then(|id| tree.node(id)) {
if !node.base.p.is_visible {
return false;
}
id = node.parent;
}
true
};
if self.hovered.iter().any(|&id| !shown(Some(id))) {
self.tree.hover_check = true;
}
if !std::mem::take(&mut self.tree.hover_check) || self.last_pointer.is_none() || self.recognizer.is_pressed() {
return;
}
self.hover(self.last_pointer, time_ms);
}
fn update_fps_labels(&mut self) {
if self.fps.value == self.fps.shown {
return;
}
self.fps.shown = self.fps.value;
let mut text = None;
for index in 0..self.tree.nodes.len() {
let Some(id) = self.tree.nodes[index].as_ref().map(|n| n.id) else { continue };
if let Some(mut label) = self.tree.find_mut::<SkiaLabelFps>(id) {
label.set_text(text.get_or_insert_with(|| SkiaLabelFps::text(self.fps.value)).clone());
}
}
}
pub fn prepare(&mut self, width: f32, height: f32, scale: f32, time_ms: f64) {
self.tree.needs_frame = false;
self.tree.time_ms = time_ms;
self.scale = scale;
if self.fonts_pending > 0 {
self.input.clear();
self.prepared = Some((time_ms, Rect::default(), std::time::Instant::now(), false));
return;
}
let started = std::time::Instant::now();
let inset = if self.tree.mobile_fullscreen { crate::types::Thickness::default() } else { self.tree.safe_insets };
let content = Rect::from_ltrb(inset.left * scale, inset.top * scale, width - inset.right * scale, height - inset.bottom * scale);
let canvas_size = Size::new(content.width().max(0.0) / scale.max(0.1), content.height().max(0.0) / scale.max(0.1));
let insets_changed = std::mem::take(&mut self.insets_changed);
if insets_changed {
for index in 0..self.tree.nodes.len() {
let Some(id) = self.tree.nodes[index].as_ref().map(|n| n.id) else { continue };
if self.tree.find::<crate::controls::shell::SkiaShell>(id).is_some() {
self.tree.invalidate(id, crate::types::Dirty::APPLY);
}
}
if let Some(handler) = self.insets_handler.as_mut() {
handler(&mut self.state, self.tree.safe_insets, &mut Cx { tree: &mut self.tree });
}
self.state_dirty = true;
}
if canvas_size != self.tree.canvas_size || insets_changed {
self.tree.canvas_size = canvas_size;
if let Some(resized) = self.resized.as_mut() {
resized(&mut self.state, canvas_size, &mut Cx { tree: &mut self.tree });
}
self.state_dirty = true;
}
self.flush_input();
if animators::tick(&mut self.tree, &mut self.state, time_ms) {
self.state_dirty = true;
}
if self.state_dirty {
self.state_dirty = false;
self.tree.run_observers(&self.state);
}
self.apply_focus_request();
self.update_fps_labels();
layout::commit(&mut self.tree);
let root = self.tree.root;
if let Some(root) = root {
layout::measure(&mut self.tree, &self.fonts, &self.state, root, content.width(), content.height(), scale);
layout::arrange(&mut self.tree, &self.fonts, &self.state, root, content, scale);
if images::dispatch(&mut self.tree, &mut self.state) {
self.state_dirty = true;
}
}
self.prepared = Some((time_ms, content, started, root.is_some()));
}
pub fn draw(&mut self, canvas: &Canvas, gpu: &mut Gpu, width: f32, height: f32, scale: f32, time_ms: f64) {
if self.prepared.is_none_or(|p| p.0 != time_ms) {
self.prepare(width, height, scale, time_ms);
}
let Some((_, content, started, laid_out)) = self.prepared.take() else { return };
self.tree.drops.clear();
if gpu.epoch() != self.tree.gpu_epoch {
self.tree.gpu_epoch = gpu.epoch();
paint::gpu_changed(&mut self.tree);
}
canvas.clear(self.background);
let (Some(root), true) = (self.tree.root, laid_out) else { return };
let full = content;
let Tree { nodes, render, drops, animators, bakes, .. } = &mut self.tree;
let fonts = &self.fonts;
let mut cx = PaintCx { canvas, rect: full, scale, id: root, fonts, nodes, animators, render, drops, gpu, bakes, offthread: false, target: None };
paint::render(&mut cx, root);
crate::effects::after_paint(&mut self.tree);
if crate::effects::dispatch_errors(&mut self.tree, &mut self.state) {
self.state_dirty = true;
}
self.frame_times.push_back(time_ms);
while self.frame_times.front().is_some_and(|t| *t <= time_ms - 1000.0) {
self.frame_times.pop_front();
}
let frame_time_ms = started.elapsed().as_secs_f32() * 1000.0;
self.tree.frame_stats = FrameStats { frame_time_ms, fps: self.frame_times.len() as u32 };
self.draw_focus_ring(canvas, scale);
self.fps.measure(time_ms);
if self.fps.on {
self.fps.draw(canvas, &self.fonts, content.bottom(), scale);
}
self.hover_after_frame(time_ms);
self.fps.chained = self.needs_frame();
self.update_text_input();
if self.accessibility_on {
if time_ms - self.accessibility_built_ms >= ACCESSIBILITY_INTERVAL_MS {
self.build_accessibility(width, height, scale, time_ms);
} else {
self.accessibility_stale = true;
}
}
}
fn build_accessibility(&mut self, width: f32, height: f32, scale: f32, time_ms: f64) {
self.accessibility_built_ms = time_ms;
self.accessibility_stale = false;
let Some(root) = self.tree.root else { return };
let mut list = std::mem::take(&mut self.accessibility_next);
list.clear();
let mut stack = std::mem::take(&mut self.accessibility_stack);
stack.push((root, Matrix::new_identity(), None));
while let Some((id, matrix, parent)) = stack.pop() {
let mut below_parent = parent;
let Some(node) = self.tree.node(id) else { continue };
if !node.base.p.is_visible || node.base.p.opacity <= 0.0 {
continue;
}
let matrix = match self.tree.render.get(id.index as usize).and_then(|r| r.matrix) {
Some(own) => Matrix::concat(&matrix, &own),
None => matrix,
};
let (p, kind) = (&node.base.p, node.kind.as_deref());
let role = match p.accessibility_role {
"" => kind.and_then(|k| type_role(&self.accessibility_roles, k).or_else(|| k.accessibility_role())).unwrap_or(""),
role => role,
};
if !role.is_empty() && role != Aria::PRESENTATION {
let rect = matrix.map_rect(node.base.rect).0;
let outside = rect.right < -width || rect.bottom < -height || rect.left > 2.0 * width || rect.top > 2.0 * height;
if rect.width() > 0.0 && rect.height() > 0.0 && !outside {
let label = match p.accessibility_label.as_str() {
"" => kind.and_then(|k| k.accessibility_label()).map(|l| l.into_owned()).unwrap_or_default(),
label => label.to_owned(),
};
let can_interact = p.accessibility_can_interact.or_else(|| kind.and_then(|k| k.accessibility_can_interact()));
let s = scale.max(0.1);
below_parent = Some(id.index);
list.push(AccessibilityNode {
id: id.index,
control: id,
role,
label,
hint: p.accessibility_hint.clone(),
rect: Rect::new(rect.left / s, rect.top / s, rect.right / s, rect.bottom / s),
can_interact: can_interact.unwrap_or(node.handlers.tapped.is_some()),
is_pressed: p.accessibility_is_pressed.or_else(|| kind.and_then(|k| k.accessibility_is_pressed())),
value: kind.and_then(|k| k.accessibility_value()),
scrolls: self.tree.find::<crate::controls::scroll::SkiaScroll>(id).map_or((false, false), |s| s.scroll_axes()),
live: p.accessibility_live,
text_lines: match (p.accessibility_text_selectable, kind) {
(true, Some(k)) => k.accessibility_text_lines(scale),
_ => Vec::new(),
},
parent,
group: None,
});
}
}
let mut below = matrix;
below.pre_translate(node.base.content_offset);
stack.extend(node.children.iter().rev().map(|c| (*c, below, below_parent)));
}
list.sort_by(|a, b| a.rect.top.total_cmp(&b.rect.top).then(a.rect.left.total_cmp(&b.rect.left)));
let mut row = 0;
for i in 1..=list.len() {
if let Some(next) = list.get(i).map(|n| n.rect) {
let first = list[row].rect;
if next.top - first.top < first.height().min(next.height()) / 2.0 {
continue;
}
}
list[row..i].sort_by(|a, b| a.rect.left.total_cmp(&b.rect.left));
row = i;
}
for i in 0..list.len() {
let (mut at, mut group) = (list[i].parent, None);
while let Some(id) = at {
let Some(node) = list.iter().find(|n| n.id == id) else { break };
if GROUP_ROLES.contains(&node.role) {
group = Some(node.control);
break;
}
at = node.parent;
}
list[i].group = group.and_then(|group| {
let mut item = list[i].control;
while let Some(parent) = self.tree.parent(item) {
if parent == group {
return Some((group.index, item.index));
}
item = parent;
}
None
});
}
let same = list.len() == self.tree.accessibility.len()
&& list.iter().zip(&self.tree.accessibility).all(|(a, b)| {
a.control == b.control
&& a.role == b.role
&& a.label == b.label
&& a.hint == b.hint
&& a.can_interact == b.can_interact
&& a.is_pressed == b.is_pressed
&& a.value == b.value
&& a.scrolls == b.scrolls
&& a.live == b.live
&& a.parent == b.parent
&& a.group == b.group
&& a.text_lines.len() == b.text_lines.len()
&& a.text_lines.iter().zip(&b.text_lines).all(|(x, y)| {
x.text == y.text
&& (x.left - y.left).abs() <= 0.5
&& (x.top - y.top).abs() <= 0.5
&& x.font_size == y.font_size
})
&& (a.rect.left - b.rect.left).abs() <= 0.5
&& (a.rect.top - b.rect.top).abs() <= 0.5
&& (a.rect.right - b.rect.right).abs() <= 0.5
&& (a.rect.bottom - b.rect.bottom).abs() <= 0.5
});
self.accessibility_stack = stack;
if same {
self.accessibility_next = list;
} else {
self.accessibility_next = std::mem::replace(&mut self.tree.accessibility, list);
self.accessibility_changed = true;
self.accessibility_revision += 1;
}
}
pub fn set_keyboard_navigation(&mut self, on: bool) {
self.keyboard_navigation = on;
}
pub fn set_accessibility_enabled(&mut self, on: bool) {
self.accessibility_on = on;
if on {
self.accessibility_built_ms = f64::NEG_INFINITY;
self.tree.needs_frame = true;
}
}
pub fn process_gesture(&mut self, gesture: Gesture) -> Option<ControlId> {
self.tree.gesture = Some(gesture);
let consumed = self.route_gesture(gesture);
self.tree.gesture = None;
consumed
}
fn route_gesture(&mut self, gesture: Gesture) -> Option<ControlId> {
let root = self.tree.root?;
let claimed_before = self.tree.focus_request.is_some();
let routing = Routing { state: &mut self.state, state_dirty: &mut self.state_dirty, over: &mut self.over_next, by_z: &mut self.by_z };
let mut router = Router { tree: &mut self.tree, r: routing };
if gesture.kind == GestureKind::Down {
self.owner = None;
} else if let Some(owner) = self.owner
&& matches!(gesture.kind, GestureKind::Panning | GestureKind::Up | GestureKind::Wheel)
{
let alive = router.tree.node(owner).is_some_and(|n| n.base.p.is_visible && !n.base.p.input_transparent);
let consumed = if alive { router.route(owner, &gesture, to_local(router.tree, owner, gesture.location)) } else { None };
if consumed.is_some() {
self.owner = if gesture.kind == GestureKind::Up { None } else { consumed };
self.apply_focus_request();
return consumed;
}
if gesture.kind != GestureKind::Wheel {
self.owner = None;
}
}
let point = to_local(router.tree, root, gesture.location);
let consumed = router.route(root, &gesture, point);
match gesture.kind {
GestureKind::Down | GestureKind::Panning => self.owner = consumed,
GestureKind::Up => self.owner = None,
_ => {}
}
if gesture.kind == GestureKind::Tapped && !claimed_before && self.tree.focus_request.is_none() {
self.focus_after_tap(consumed);
}
self.apply_focus_request();
consumed
}
fn focus_after_tap(&mut self, consumed: Option<ControlId>) {
let focused = self.focused();
if focused.is_some_and(|f| self.tree.base(f).is_some_and(|b| b.p.lock_focus)) {
return;
}
match consumed {
Some(c) if Some(c) == focused => {}
Some(c) => {
if self.tree.base(c).is_some_and(|b| b.p.can_be_focused) {
self.focus(Some(c));
}
}
None => self.focus(None),
}
}
}
const GROUP_KEYS: [&str; 8] = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"];
const GROUP_ROLES: [&str; 8] = [Aria::LIST, Aria::LIST_BOX, Aria::GRID, Aria::TOOLBAR, Aria::RADIO_GROUP, Aria::TAB_LIST, Aria::MENU, Aria::MENU_BAR];
fn shown(tree: &Tree, id: ControlId) -> bool {
let mut at = Some(id);
while let Some(control) = at {
match tree.base(control) {
Some(base) if base.p.is_visible && base.p.opacity > 0.0 => at = tree.parent(control),
_ => return false,
}
}
true
}
fn drawn_rect(tree: &Tree, id: ControlId) -> Option<Rect> {
let mut path = vec![id];
while let Some(parent) = tree.parent(*path.last()?) {
path.push(parent);
}
path.reverse();
let mut matrix = Matrix::new_identity();
for (i, &at) in path.iter().enumerate() {
if let Some(own) = tree.render.get(at.index as usize).and_then(|r| r.matrix) {
matrix = Matrix::concat(&matrix, &own);
}
if i + 1 < path.len() {
matrix.pre_translate(tree.base(at)?.content_offset);
}
}
Some(matrix.map_rect(tree.base(id)?.rect).0)
}
fn ensure_visible(tree: &mut Tree, id: ControlId) {
use crate::controls::scroll::SkiaScroll;
const PADDING: f32 = 8.0;
const MS: f32 = 250.0;
let Some(mut rect) = tree.base(id).map(|b| b.rect) else { return };
let mut child = id;
while let Some(parent) = tree.parent(child) {
let Some(base) = tree.base(parent) else { return };
rect.offset(base.content_offset);
let (viewport, scale) = (base.rect, base.scale.max(0.1));
if let Some(scroll) = tree.find::<SkiaScroll>(parent) {
let (x, y) = (scroll.viewport_offset_x(), scroll.viewport_offset_y());
let mut delta = Point::default();
if rect.top < viewport.top {
delta.y = (viewport.top - rect.top) / scale + PADDING;
} else if rect.bottom > viewport.bottom {
delta.y = -((rect.bottom - viewport.bottom) / scale + PADDING);
}
if rect.left < viewport.left {
delta.x = (viewport.left - rect.left) / scale + PADDING;
} else if rect.right > viewport.right {
delta.x = -((rect.right - viewport.right) / scale + PADDING);
}
if !delta.is_zero() {
Cx { tree: &mut *tree }.scroll_to(parent, x + delta.x, y + delta.y, MS);
}
}
child = parent;
}
}
fn type_role(roles: &[(std::any::TypeId, &'static str)], control: &dyn Control) -> Option<&'static str> {
let mut part = Some(control);
while let Some(c) = part {
let type_id = (c as &dyn Any).type_id();
if let Some((_, role)) = roles.iter().find(|(t, _)| *t == type_id) {
return Some(role);
}
part = c.inner();
}
None
}
fn with_kind<R>(tree: &mut Tree, id: ControlId, run: impl FnOnce(&mut dyn Control, &mut GestureCx) -> R) -> Option<R> {
let mut kind = tree.node_mut(id)?.kind.take()?;
let result = run(&mut *kind, &mut GestureCx::new(&mut *tree, id, Point::default()));
if let Some(node) = tree.node_mut(id) {
node.kind = Some(kind);
}
Some(result)
}
fn key_to_control(tree: &mut Tree, state: &mut dyn Any, state_dirty: &mut bool, id: ControlId, event: &KeyEvent<'_>) -> bool {
let Some(mut handled) = with_kind(tree, id, |kind, cx| kind.on_key(cx, event)) else { return false };
tree.needs_frame = true;
let Some(mut node) = tree.take(id) else { return handled };
if let Some(control) = node.kind.as_deref_mut() {
let handler = node.handlers.input.as_deref_mut().and_then(|h| match event.kind {
KeyKind::Down => h.key_down.as_mut(),
KeyKind::Up => h.key_up.as_mut(),
KeyKind::Char => h.key_char.as_mut(),
});
if let Some(handler) = handler {
let mut queue = Vec::new();
let raw = Raw { id, control, base: &mut node.base, queue: &mut queue };
handled |= handler(raw, state, &mut Cx { tree: &mut *tree }, event);
tree.queue.append(&mut queue);
*state_dirty = true;
}
}
tree.put_back(node);
tree.needs_frame = true;
handled
}
fn plain_gesture(kind: GestureKind, location: Point, time_ms: f64) -> Gesture {
let zero = Point::default();
let (start, delta, total, velocity, wheel, cancelled) = (location, zero, zero, zero, 0.0, false);
let (button, source) = (MouseButton::Left, ContextMenuSource::Mouse);
let (wheel_horizontal, touch) = (false, false);
Gesture { kind, location, start, delta, total, time_ms, velocity, wheel, wheel_horizontal, cancelled, button, source, touch }
}
fn to_local(tree: &Tree, id: ControlId, point: Point) -> Point {
let Some(parent) = tree.parent(id) else { return map_into(tree, id, point) };
let offset = tree.base(parent).map_or(Point::default(), |b| b.content_offset);
map_into(tree, id, to_local(tree, parent, point) - offset)
}
fn map_into(tree: &Tree, id: ControlId, point: Point) -> Point {
match tree.render[id.index as usize].matrix.and_then(|m| m.invert()) {
Some(inverse) => inverse.map_point(point),
None => point,
}
}
pub(crate) struct Routing<'a> {
state: &'a mut dyn Any,
state_dirty: &'a mut bool,
over: &'a mut Vec<ControlId>,
by_z: &'a mut Vec<ControlId>,
}
impl Routing<'_> {
fn reborrow(&mut self) -> Routing<'_> {
Routing { state: &mut *self.state, state_dirty: &mut *self.state_dirty, over: &mut *self.over, by_z: &mut *self.by_z }
}
}
pub(crate) fn route_children(tree: &mut Tree, routing: &mut Routing<'_>, id: ControlId, gesture: &Gesture, point: Point) -> Option<ControlId> {
Router { tree, r: routing.reborrow() }.route_children(id, gesture, point)
}
struct Router<'a> {
tree: &'a mut Tree,
r: Routing<'a>,
}
impl Router<'_> {
fn route(&mut self, id: ControlId, gesture: &Gesture, point: Point) -> Option<ControlId> {
let node = self.tree.node(id)?;
let (block, consumes) = (node.base.p.block_gestures_below, node.handlers.input.as_ref().is_some_and(|h| h.consume_gestures.is_some()));
if gesture.kind == GestureKind::Pointer {
self.r.over.push(id);
}
if consumes && self.fire_consume_gestures(id, gesture, point) {
return Some(id);
}
if crate::effects::route(self.tree, id, gesture, point) {
return Some(id);
}
let mut children_routed = false;
if let Some(mut kind) = self.tree.node_mut(id).and_then(|n| n.kind.take()) {
let mut cx = GestureCx { tree: &mut *self.tree, id, point, routing: Some(self.r.reborrow()), children_routed: false };
let handled = kind.on_gesture(&mut cx, gesture);
children_routed = cx.children_routed;
if let Some(node) = self.tree.node_mut(id) {
node.kind = Some(kind);
}
match handled {
Handled::Yes => return Some(id),
Handled::Tapped => {
self.fire_tapped(id, point);
return Some(id);
}
Handled::By(child) => return Some(child),
Handled::No => {}
}
}
if !children_routed && let Some(consumed) = self.route_children(id, gesture, point) {
return Some(consumed);
}
match gesture.kind {
GestureKind::Tapped if self.fire_tapped(id, point) => return Some(id),
GestureKind::LongPressing if self.fire_long_pressing(id, point) => return Some(id),
GestureKind::ContextMenu if self.fire_context_menu(id, gesture, point) => return Some(id),
_ => {}
}
block.then_some(id)
}
fn route_children(&mut self, id: ControlId, gesture: &Gesture, point: Point) -> Option<ControlId> {
let base = self.tree.base(id)?;
let locked = match base.p.lock_children_gestures {
LockTouch::Disabled => false,
LockTouch::Enabled | LockTouch::PassNone => true,
LockTouch::PassTap => gesture.kind != GestureKind::Tapped,
LockTouch::PassTapAndLongPress => !matches!(gesture.kind, GestureKind::Tapped | GestureKind::LongPressing),
};
if locked {
return None;
}
let point = point - base.content_offset;
let tree = &*self.tree;
if tree.children(id).iter().any(|c| tree.node(*c).is_some_and(|n| n.base.p.z_index != 0)) {
let start = self.r.by_z.len();
self.r.by_z.extend_from_slice(tree.children(id));
self.r.by_z[start..].sort_by_key(|c| tree.node(*c).map_or(0, |n| n.base.p.z_index));
let mut consumed = None;
for i in (start..self.r.by_z.len()).rev() {
consumed = self.route_child(self.r.by_z[i], gesture, point);
if consumed.is_some() {
break;
}
}
self.r.by_z.truncate(start);
return consumed;
}
let mut i = self.tree.children(id).len();
while i > 0 {
i -= 1;
let Some(&child) = self.tree.children(id).get(i) else { continue };
if let Some(consumed) = self.route_child(child, gesture, point) {
return Some(consumed);
}
}
None
}
fn route_child(&mut self, child: ControlId, gesture: &Gesture, point: Point) -> Option<ControlId> {
let base = self.tree.base(child)?;
if !base.p.is_visible || base.p.input_transparent {
return None;
}
let local = map_into(self.tree, child, point);
let r = base.rect;
if local.x < r.left || local.x >= r.right || local.y < r.top || local.y >= r.bottom {
return None;
}
self.route(child, gesture, local)
}
fn deliver(&mut self, id: ControlId, gesture: &Gesture, point: Point) {
let Some(mut kind) = self.tree.node_mut(id).and_then(|n| n.kind.take()) else { return };
kind.on_gesture(&mut GestureCx::new(&mut *self.tree, id, point), gesture);
if let Some(node) = self.tree.node_mut(id) {
node.kind = Some(kind);
}
}
fn fire(
&mut self,
id: ControlId,
point: Point,
run: impl FnOnce(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &mut crate::tree::Handlers) -> bool,
) -> bool {
let Some(mut node) = self.tree.take(id) else { return false };
let mut fired = false;
let mut queue = Vec::new();
if let Some(control) = node.kind.as_deref_mut() {
let (rect, scale) = (node.base.rect, node.base.scale.max(0.1));
self.tree.gesture_point = Some(Point::new((point.x - rect.left) / scale, (point.y - rect.top) / scale));
let raw = Raw { id, control, base: &mut node.base, queue: &mut queue };
fired = run(raw, &mut *self.r.state, &mut Cx { tree: self.tree }, &mut node.handlers);
self.tree.gesture_point = None;
if fired {
*self.r.state_dirty = true;
}
}
self.tree.put_back(node);
self.tree.queue.append(&mut queue);
fired
}
fn fire_tapped(&mut self, id: ControlId, point: Point) -> bool {
self.fire(id, point, |raw, state, cx, handlers| match handlers.tapped.as_mut() {
Some(tapped) => {
let (p, rect, scale) = (&raw.base.p, raw.base.rect, raw.base.scale.max(0.1));
if p.animation_tapped == crate::controls::button::SkiaTouchAnimation::Ripple {
let (x, y) = ((point.x - rect.left) / scale, (point.y - rect.top) / scale);
cx.play_ripple(id, p.touch_effect_color, x, y, p.animation_tapped_speed);
}
tapped(raw, state, cx);
true
}
None => false,
})
}
fn fire_consume_gestures(&mut self, id: ControlId, gesture: &Gesture, point: Point) -> bool {
let mut consumed = false;
self.fire(id, point, |raw, state, cx, handlers| match handlers.input.as_deref_mut().and_then(|h| h.consume_gestures.as_mut()) {
Some(handler) => {
consumed = handler(raw, state, cx, gesture);
true
}
None => false,
});
consumed
}
fn fire_long_pressing(&mut self, id: ControlId, point: Point) -> bool {
self.fire(id, point, |raw, state, cx, handlers| match handlers.input.as_deref_mut().and_then(|h| h.long_pressing.as_mut()) {
Some(long_pressing) => {
long_pressing(raw, state, cx);
true
}
None => false,
})
}
fn fire_context_menu(&mut self, id: ControlId, gesture: &Gesture, point: Point) -> bool {
let Some(base) = self.tree.base(id) else { return false };
let scale = base.scale.max(0.1);
let pixels = gesture.location;
let menu = ContextMenu {
location: Point::new(pixels.x / scale, pixels.y / scale),
pixels,
source: gesture.source,
control: Some(id),
local: Point::new(point.x - base.rect.left, point.y - base.rect.top),
};
self.fire(id, point, |raw, state, cx, handlers| match handlers.input.as_deref_mut().and_then(|h| h.context_menu.as_mut()) {
Some(context_menu) => context_menu(raw, state, cx, &menu),
None => false,
})
}
}
impl<T: Control> Build<T> {
fn input(&mut self) -> &mut InputHandlers {
self.handlers.input.get_or_insert_with(Default::default)
}
pub fn listen_keys(mut self) -> Self {
self.input().listen_keys = true;
self
}
pub fn listen_history(mut self) -> Self {
self.input().listen_history = true;
self
}
pub fn consume_gestures<S: Any>(
mut self,
mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, &Gesture) -> bool + 'static,
) -> Self {
self.input().consume_gestures = Some(Box::new(move |raw, state, cx, gesture| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, gesture)
}));
self
}
pub fn on_long_pressing<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>) + 'static) -> Self {
self.input().long_pressing = Some(Box::new(move |raw, state, cx| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx)
}));
self
}
pub fn on_context_menu<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, &ContextMenu) -> bool + 'static) -> Self {
self.input().context_menu = Some(Box::new(move |raw, state, cx, menu| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, menu)
}));
self
}
pub fn on_key_down<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, &KeyEvent<'_>) -> bool + 'static) -> Self {
self.input().key_down = Some(Box::new(move |raw, state, cx, event| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, event)
}));
self
}
pub fn on_key_up<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, &KeyEvent<'_>) -> bool + 'static) -> Self {
self.input().key_up = Some(Box::new(move |raw, state, cx, event| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, event)
}));
self
}
pub fn on_key_char<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, &KeyEvent<'_>) -> bool + 'static) -> Self {
self.input().key_char = Some(Box::new(move |raw, state, cx, event| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, event)
}));
self
}
pub fn on_hovered<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, bool) + 'static) -> Self {
if self.base.p.receives_hover.is_none() {
self.base.p.receives_hover = Some(true);
}
self.input().hovered = Some(Box::new(move |raw, state, cx, on| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, on)
}));
self
}
pub fn on_focus_changed<S: Any>(mut self, mut f: impl FnMut(&mut crate::Mut<'_, T>, &mut S, &mut Cx<'_>, bool) + 'static) -> Self {
self.input().focus_changed = Some(Box::new(move |raw, state, cx, focused| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| crate::tree::wrong_state::<S>());
f(&mut raw.typed(), state, cx, focused)
}));
self
}
}
impl Cx<'_> {
pub fn gesture(&self) -> Option<&Gesture> {
self.tree.gesture.as_ref()
}
pub fn gesture_point(&self) -> Option<Point> {
self.tree.gesture_point
}
pub fn listen_keys(&mut self, id: impl Into<ControlId>, on: bool) {
let id = id.into();
self.tree.key_listeners.retain(|l| *l != id);
if on {
self.tree.key_listeners.push(id);
}
}
pub fn set_clipboard(&mut self, text: impl Into<String>) {
self.tree.clipboard = Some(text.into());
self.tree.needs_frame = true;
}
pub fn open_url(&mut self, url: impl Into<String>) {
self.tree.open_urls.push(url.into());
self.tree.needs_frame = true;
}
pub fn request_paste(&mut self) {
self.tree.paste_requested = true;
self.tree.needs_frame = true;
}
pub fn accessibility_nodes(&self) -> &[AccessibilityNode] {
&self.tree.accessibility
}
pub fn accessibility_focused(&self) -> Option<ControlId> {
self.tree.accessibility_focused.filter(|f| self.tree.node(*f).is_some())
}
pub fn canvas_size(&self) -> Size {
self.tree.canvas_size
}
pub fn safe_insets(&self) -> crate::types::Thickness {
self.tree.safe_insets
}
pub fn mobile_fullscreen(&self) -> bool {
self.tree.mobile_fullscreen
}
pub fn content_insets(&self) -> crate::types::Thickness {
if self.tree.mobile_fullscreen { self.tree.safe_insets } else { crate::types::Thickness::default() }
}
pub fn location_hash(&self) -> &str {
&self.tree.location_hash
}
pub fn has_history(&self) -> bool {
self.tree.history_enabled
}
pub fn history(&mut self, op: HistoryOp) {
self.tree.history_ops.push(op);
self.tree.needs_frame = true;
}
pub fn listen_history(&mut self, id: impl Into<ControlId>, on: bool) {
let id = id.into();
self.tree.history_listeners.retain(|l| *l != id);
if on {
self.tree.history_listeners.push(id);
}
}
pub fn frame_stats(&self) -> FrameStats {
self.tree.frame_stats
}
pub fn focus(&mut self, id: Option<impl Into<ControlId>>) {
self.tree.focus_request = Some(id.map(Into::into));
}
pub fn focused(&self) -> Option<ControlId> {
self.tree.focused.filter(|f| self.tree.node(*f).is_some())
}
}
impl<S: 'static> App for Ui<S> {
fn rendering_mode(&self) -> RenderingModeType {
self.rendering_mode
}
fn gpu_backend(&self) -> GpuBackend {
self.gpu_backend
}
fn background(&self) -> crate::skia::Color {
self.background
}
fn gestures(&self) -> GesturesMode {
self.gestures
}
fn prepare(&mut self, width: f32, height: f32, scale: f32, time_ms: f64) {
Ui::prepare(self, width, height, scale, time_ms)
}
fn page_font(&mut self, alias: &str, url: &str, weight: i32, host: &mut Host) {
let index = self.font_urls.len();
self.font_urls.push((alias.to_owned(), url.to_owned(), weight, true, true));
self.fonts_pending += 1;
host.fetch(index as u32, url);
}
fn init(&mut self, host: &mut Host) {
for (index, (_, url, ..)) in self.font_urls.iter().enumerate() {
host.fetch(index as u32, url);
}
for request in self.tree.images.take_requests() {
host.fetch_image(request);
}
for (id, url) in self.tree.assets.take_requests() {
host.fetch(id, &url);
}
}
fn baked(&mut self, id: u32, image: Option<crate::skia::Image>) {
crate::paint::bake_arrived(&mut self.tree, id, image);
}
fn image(&mut self, id: u32, decoded: Option<Decoded>) {
images::deliver(&mut self.tree, id, decoded);
}
fn asset(&mut self, id: u32, bytes: Vec<u8>) {
if id >= crate::assets::ASSET_BASE {
if let Some(deliver) = self.tree.assets.take(id) {
deliver(&mut self.tree, bytes);
self.tree.needs_frame = true;
}
return;
}
let Some((alias, url, weight, gates, page)) = self.font_urls.get(id as usize) else { return };
if *gates {
self.fonts_pending = self.fonts_pending.saturating_sub(1);
}
self.tree.needs_frame = true;
if bytes.is_empty() {
eprintln!("drawnui: font {url} did not load");
} else if self.fonts.add_face(alias, *weight, &bytes, !page) {
let ids: Vec<ControlId> = self.tree.nodes.iter().flatten().map(|n| n.id).collect();
for id in ids {
self.tree.invalidate(id, Dirty::MEASURE);
}
}
}
fn pointer(&mut self, kind: PointerKind, x: f32, y: f32, time_ms: f64) {
Ui::pointer(self, kind, x, y, time_ms)
}
fn pointer_button(&mut self, kind: PointerKind, button: MouseButton, x: f32, y: f32, time_ms: f64) {
Ui::pointer_button(self, kind, button, x, y, time_ms)
}
fn pointer_touch(&mut self, kind: PointerKind, x: f32, y: f32, time_ms: f64) {
Ui::pointer_touch(self, kind, x, y, time_ms)
}
fn wheel(&mut self, x: f32, y: f32, delta: f32, horizontal: bool, time_ms: f64) -> bool {
Ui::wheel_on_axis(self, x, y, delta, horizontal, time_ms)
}
fn context_menu(&mut self, x: f32, y: f32, source: ContextMenuSource, time_ms: f64) -> bool {
Ui::context_menu(self, x, y, source, time_ms)
}
fn key(&mut self, kind: KeyKind, key: InputKey, text: &str, modifiers: Modifiers, repeat: bool) -> bool {
Ui::key(self, kind, key, text, modifiers, repeat)
}
fn focus_out(&mut self) {
Ui::focus_out(self)
}
fn back(&mut self) -> bool {
Ui::system_back(self)
}
fn blur(&mut self) {
Ui::blur(self)
}
fn accessibility_activate(&mut self, id: u32) {
Ui::accessibility_activate(self, id)
}
fn accessibility_focused_id(&self) -> Option<u32> {
Ui::accessibility_focused(self).map(|f| f.index)
}
fn accessibility_focus(&mut self, id: u32, focused: bool) {
Ui::accessibility_focus(self, id, focused)
}
fn accessibility_adjust(&mut self, id: u32, up: bool) {
Ui::accessibility_adjust(self, id, up)
}
fn accessibility_scroll_into_view(&mut self, id: u32) {
Ui::accessibility_scroll_into_view(self, id)
}
fn accessibility_set_value(&mut self, id: u32, value: f64) {
Ui::accessibility_set_value(self, id, value)
}
fn accessibility_scroll(&mut self, id: u32, x: f32, y: f32) {
Ui::accessibility_scroll(self, id, x, y)
}
fn visibility(&mut self, visible: bool) {
if let Some(handler) = self.visibility.as_mut() {
handler(&mut self.state, visible, &mut Cx { tree: &mut self.tree });
self.state_dirty = true;
self.tree.needs_frame = true;
self.apply_focus_request();
}
}
fn safe_insets(&mut self, insets: crate::types::Thickness) {
if insets != self.tree.safe_insets {
self.tree.safe_insets = insets;
self.insets_changed = true;
self.tree.needs_frame = true;
}
}
fn location(&mut self, hash: &str, popped: Option<u32>) {
hash.clone_into(&mut self.tree.location_hash);
let Some(depth) = popped else { return };
let tree = &mut self.tree;
tree.history_listeners.retain(|id| tree.nodes.get(id.index as usize).is_some_and(|n| n.as_ref().is_some_and(|n| n.id == *id)));
for i in 0..tree.history_listeners.len() {
let Some(&id) = tree.history_listeners.get(i) else { break };
let hash = std::mem::take(&mut tree.location_hash);
with_kind(tree, id, |kind, cx| kind.on_history(cx, depth, &hash));
tree.location_hash = hash;
}
self.state_dirty = true;
self.tree.needs_frame = true;
self.apply_focus_request();
}
fn frame(&mut self, frame: &mut Frame) -> bool {
self.tree.history_enabled = frame.host.history_on;
self.keyboard_navigation = frame.host.keyboard_navigation;
self.tree.bakes.enabled = frame.host.bake_workers;
if frame.host.accessibility_on != self.accessibility_on {
self.set_accessibility_enabled(frame.host.accessibility_on);
}
self.draw(frame.surface.canvas(), frame.gpu, frame.width, frame.height, frame.scale, frame.time_ms);
for request in self.tree.images.take_requests() {
frame.host.fetch_image(request);
}
for (id, url) in self.tree.assets.take_requests() {
frame.host.fetch(id, &url);
}
frame.host.bakes.append(&mut self.tree.bakes.requests);
if std::mem::take(&mut self.cursor_changed) {
frame.host.set_cursor(self.cursor);
}
if std::mem::take(&mut self.text_input_changed) {
frame.host.set_text_input(self.text_input);
frame.host.text_input_node = self.focused().map(|f| f.index);
}
if let Some(id) = self.keyboard_moved.take() {
frame.host.keyboard_moved = Some(id);
}
if std::mem::take(&mut self.accessibility_changed) {
frame.host.set_accessibility(self.tree.accessibility.clone());
}
if let Some(text) = self.tree.clipboard.take() {
frame.host.set_clipboard(text);
}
for url in self.tree.open_urls.drain(..) {
frame.host.open_url(&url);
}
frame.host.history.append(&mut self.tree.history_ops);
if std::mem::take(&mut self.tree.paste_requested) {
frame.host.request_paste();
}
frame.host.wake_ms = self.wake_at();
self.needs_frame()
}
}
#[cfg(test)]
mod page_font_tests {
use super::*;
const OPEN_SANS: &[u8] = include_bytes!("../../examples/bench/assets/OpenSans-Regular.ttf");
const INTER: &[u8] = include_bytes!("../tests/fonts/Inter-Regular.ttf");
#[test]
fn a_page_font_is_waited_for_and_never_the_default() {
for own in [true, false] {
let ui = Ui::new((), |_| crate::controls::label::SkiaLabel::new("Hello"));
let mut ui = if own { ui.font_bytes("Default", OPEN_SANS) } else { ui };
let mut host = Host::default();
ui.init(&mut host);
App::page_font(&mut ui, "MyFont", "fonts/my.ttf", 400, &mut host);
assert_eq!(host.requests.last(), Some(&(0, "fonts/my.ttf".to_owned())));
assert!(!ui.needs_frame(), "nothing is laid out before the page's font is there");
App::asset(&mut ui, 0, INTER.to_vec());
assert!(ui.needs_frame());
let family = ui.fonts.font("MyFont", 12.0).map(|f| f.typeface().family_name());
assert_eq!(family.as_deref(), Some("Inter"));
assert_eq!(ui.fonts.default_alias(), own.then_some("Default"), "own fonts: {own}");
}
}
}