use crate::core::{ObjectId, Point, Rect, Size};
use crate::event::Event;
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SurfaceMountError {
NoRegistryOnThread,
UnsupportedByBackend(&'static str),
RejectedByBackend(&'static str),
UnknownWidgetName,
}
impl core::fmt::Display for SurfaceMountError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NoRegistryOnThread => {
write!(f, "widgets must be mounted on the UI thread (no registry on this thread)")
}
Self::UnsupportedByBackend(backend) => {
write!(f, "backend '{backend}' has no surface to mount widgets onto")
}
Self::RejectedByBackend(backend) => {
write!(f, "backend '{backend}' refused the mount (see logs for the reason)")
}
Self::UnknownWidgetName => {
write!(f, "the widget factory has no widget registered under that name")
}
}
}
}
impl std::error::Error for SurfaceMountError {}
use crate::widget::Widget;
use std::cell::RefCell;
use std::collections::HashMap;
struct Mounted {
widget: Box<dyn Widget>,
}
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static MOUNTED: RefCell<HashMap<ObjectId, Mounted>> = RefCell::new(HashMap::new());
#[allow(clippy::missing_const_for_thread_local)]
static NEXT_ID: RefCell<ObjectId> = const { RefCell::new(0x5345_4C46_0000_0001) };
#[allow(clippy::missing_const_for_thread_local)]
static HOST_WINDOWS: RefCell<HashMap<ObjectId, ObjectId>> = RefCell::new(HashMap::new());
#[allow(clippy::missing_const_for_thread_local)]
static FOCUS: RefCell<crate::event::FocusManager> =
RefCell::new(crate::event::FocusManager::new());
#[allow(clippy::missing_const_for_thread_local)]
static HOVERED: RefCell<Option<ObjectId>> = const { RefCell::new(None) };
#[allow(clippy::missing_const_for_thread_local)]
static CAPTURE: RefCell<crate::event::PointerCaptureManager> =
RefCell::new(crate::event::PointerCaptureManager::new());
}
pub fn register(widget: Box<dyn Widget>) -> Option<ObjectId> {
let id = NEXT_ID.try_with(|next| {
let id = *next.borrow();
*next.borrow_mut() = id.wrapping_add(1);
id
});
let Ok(id) = id else { return None };
let stored = MOUNTED.try_with(|map| {
map.borrow_mut().insert(id, Mounted { widget });
});
if stored.is_err() {
return None;
}
let focusable = with_widget(id, |widget| widget.is_focusable()).unwrap_or(false);
if focusable {
register_focusable(id);
}
Some(id)
}
pub fn unregister(id: ObjectId) -> bool {
let _ = HOST_WINDOWS.try_with(|map| map.borrow_mut().remove(&id));
let _ = FOCUS.try_with(|focus| focus.borrow_mut().unregister_focusable(id));
let _ = HOVERED.try_with(|hovered| {
let mut hovered = hovered.borrow_mut();
if *hovered == Some(id) {
*hovered = None;
}
});
let _ = CAPTURE.try_with(|capture| {
let mut capture = capture.borrow_mut();
if capture.capturing_widget() == Some(id) {
capture.release_capture();
}
});
MOUNTED.try_with(|map| map.borrow_mut().remove(&id).is_some()).unwrap_or(false)
}
pub fn focused_widget() -> Option<ObjectId> {
FOCUS.try_with(|focus| focus.borrow().focused_widget()).unwrap_or(None)
}
pub fn has_focus(id: ObjectId) -> bool {
FOCUS.try_with(|focus| focus.borrow().has_focus(id)).unwrap_or(false)
}
pub fn focus_widget(id: ObjectId) -> bool {
let previous = focused_widget();
let moved = FOCUS
.try_with(|focus| {
let mut focus = focus.borrow_mut();
if focus.focused_widget() == Some(id) {
return false;
}
if !is_mounted(id) {
return false;
}
focus.set_focus(id);
true
})
.unwrap_or(false);
if !moved {
return false;
}
if let Some(previous) = previous {
notify_ime_focus_out(previous);
let _ = dispatch_event(previous, &Event::FocusLost);
}
notify_ime_focus_in(id);
let _ = dispatch_event(id, &Event::FocusGained);
true
}
fn notify_ime_focus_in(id: ObjectId) {
#[cfg(not(alloc_frugal))]
if let Some(bridge) = crate::platform::platform_facts().ime_bridge() {
bridge.focus_in(id);
}
#[cfg(alloc_frugal)]
let _ = id;
}
fn notify_ime_focus_out(id: ObjectId) {
#[cfg(not(alloc_frugal))]
if let Some(bridge) = crate::platform::platform_facts().ime_bridge() {
bridge.focus_out(id);
}
#[cfg(alloc_frugal)]
let _ = id;
}
pub fn clear_focus() -> bool {
let previous = focused_widget();
let cleared = FOCUS
.try_with(|focus| {
let mut focus = focus.borrow_mut();
if focus.focused_widget().is_none() {
return false;
}
focus.clear_focus();
true
})
.unwrap_or(false);
if !cleared {
return false;
}
if let Some(previous) = previous {
notify_ime_focus_out(previous);
let _ = dispatch_event(previous, &Event::FocusLost);
}
true
}
pub fn focus_next(forward: bool) -> Option<ObjectId> {
let previous = focused_widget();
let next = FOCUS
.try_with(|focus| {
let mut focus = focus.borrow_mut();
if forward {
focus.focus_next()
} else {
focus.focus_previous()
}
})
.ok()
.flatten()?;
if let Some(previous) = previous.filter(|&p| p != next) {
notify_ime_focus_out(previous);
let _ = dispatch_event(previous, &Event::FocusLost);
}
notify_ime_focus_in(next);
let _ = dispatch_event(next, &Event::FocusGained);
Some(next)
}
pub fn register_focusable(id: ObjectId) {
let _ = FOCUS.try_with(|focus| focus.borrow_mut().register_focusable(id));
}
pub fn focusable_widgets() -> Vec<ObjectId> {
FOCUS.try_with(|focus| focus.borrow().focusable_widgets().to_vec()).unwrap_or_default()
}
pub fn with_focus_manager<R>(f: impl FnOnce(&mut crate::event::FocusManager) -> R) -> Option<R> {
FOCUS.try_with(|focus| f(&mut focus.borrow_mut())).ok()
}
pub fn set_focus_traversal(strategy: crate::event::FocusTraversalStrategy) -> bool {
FOCUS
.try_with(|focus| {
focus.borrow_mut().set_traversal_strategy(strategy);
})
.is_ok()
}
pub fn set_focusable_position(id: ObjectId, x: i32, y: i32) -> bool {
FOCUS
.try_with(|focus| {
let mut focus = focus.borrow_mut();
if !focus.focusable_widgets().contains(&id) {
return false;
}
focus.set_widget_position(id, x, y);
true
})
.unwrap_or(false)
}
pub fn widget_at(root: ObjectId, point: Point) -> Option<ObjectId> {
let mut current = root;
loop {
let children = with_widget(current, |widget| widget.children().to_vec())?;
if !widget_accepts_point(current, point) {
return None;
}
let deepest = children
.iter()
.rev()
.copied()
.find(|child| is_visible(*child) && widget_accepts_point(*child, point));
match deepest {
Some(child) => current = child,
None => return Some(current),
}
}
}
fn widget_accepts_point(id: ObjectId, point: Point) -> bool {
with_widget(id, |widget| widget.contains_point(point)).unwrap_or(false)
}
fn is_visible(id: ObjectId) -> bool {
with_widget(id, |widget| widget.is_visible()).unwrap_or(false)
}
pub fn dispatch_pointer_event(root: ObjectId, event: &Event, point: Point) -> bool {
let target = widget_at(root, point);
dispatch_hover_transition(target, point);
if let Some(captured) = capturing_widget() {
if is_mounted(captured) {
return dispatch_event(captured, event);
}
let _ = release_pointer_capture();
}
match target {
Some(target) => dispatch_event(target, event),
None => false,
}
}
pub fn capture_pointer(id: ObjectId) -> bool {
if !is_mounted(id) {
return false;
}
CAPTURE.try_with(|capture| capture.borrow_mut().set_capture(id)).unwrap_or(false)
}
pub fn release_pointer_capture() -> bool {
CAPTURE.try_with(|capture| capture.borrow_mut().release_capture()).unwrap_or(false)
}
pub fn capturing_widget() -> Option<ObjectId> {
CAPTURE.try_with(|capture| capture.borrow().capturing_widget()).unwrap_or(None)
}
pub fn has_pointer_capture(id: ObjectId) -> bool {
CAPTURE.try_with(|capture| capture.borrow().has_capture(id)).unwrap_or(false)
}
fn dispatch_hover_transition(hovered: Option<ObjectId>, point: Point) {
HOVERED.with(|last| {
let previous = *last.borrow();
if previous == hovered {
return;
}
*last.borrow_mut() = hovered;
if let Some(previous) = previous {
let _ = dispatch_event(previous, &Event::MouseLeave { pos: point });
}
if let Some(hovered) = hovered {
let _ = dispatch_event(hovered, &Event::MouseEnter { pos: point });
}
});
}
pub fn clear_hover(point: Point) {
dispatch_hover_transition(None, point);
}
pub fn hovered_widget() -> Option<ObjectId> {
HOVERED.with(|last| *last.borrow())
}
pub fn set_host_window(id: ObjectId, host: ObjectId) -> bool {
let stored = HOST_WINDOWS.try_with(|map| {
if MOUNTED.try_with(|m| m.borrow().contains_key(&id)).unwrap_or(false) {
map.borrow_mut().insert(id, host);
true
} else {
false
}
});
stored.unwrap_or(false)
}
pub fn host_window_for(id: ObjectId) -> Option<ObjectId> {
HOST_WINDOWS.try_with(|map| map.borrow().get(&id).copied()).unwrap_or(None)
}
pub fn is_mounted(id: ObjectId) -> bool {
MOUNTED.try_with(|map| map.borrow().contains_key(&id)).unwrap_or(false)
}
pub fn widget_is_checkable(id: ObjectId) -> bool {
with_widget(id, |widget| {
crate::widget::capability::properties_trait::widget_property_names(widget)
.is_some_and(|names| names.contains(&"checked"))
})
.unwrap_or(false)
}
pub fn mounted_count() -> usize {
MOUNTED.try_with(|map| map.borrow().len()).unwrap_or(0)
}
pub fn geometry_of(id: ObjectId) -> Option<Rect> {
MOUNTED
.try_with(|map| map.borrow().get(&id).map(|entry| entry.widget.geometry()))
.ok()
.flatten()
}
pub fn set_geometry(id: ObjectId, geometry: Rect) -> bool {
let updated = MOUNTED.try_with(|map| {
map.borrow_mut().get_mut(&id).map(|entry| entry.widget.set_geometry(geometry)).is_some()
});
updated.unwrap_or(false)
}
pub fn with_widget_mut<R>(id: ObjectId, f: impl FnOnce(&mut dyn Widget) -> R) -> Option<R> {
MOUNTED
.try_with(|map| map.borrow_mut().get_mut(&id).map(|entry| f(entry.widget.as_mut())))
.ok()
.flatten()
}
pub fn with_widget<R>(id: ObjectId, f: impl FnOnce(&dyn Widget) -> R) -> Option<R> {
MOUNTED
.try_with(|map| map.borrow().get(&id).map(|entry| f(entry.widget.as_ref())))
.ok()
.flatten()
}
pub fn dispatch_event(id: ObjectId, event: &Event) -> bool {
with_widget_mut(id, |widget| widget.handle_event(event)).is_some()
}
#[cfg(full_widgets)]
pub fn open_context_menu(menu_id: ObjectId, position: Point, viewport: Rect) -> bool {
with_widget_mut(menu_id, |widget| {
let Some(menu) = (widget as &mut dyn core::any::Any).downcast_mut::<crate::Menu>() else {
log::warn!(
"widget::runtime: id {menu_id} is not a menu, so a context menu cannot be opened \
at {position:?}"
);
return false;
};
menu.open_at(position, viewport);
true
})
.unwrap_or_else(|| {
log::warn!("widget::runtime: no widget is mounted as id {menu_id}");
false
})
}
#[cfg(full_widgets)]
pub fn open_context_menu_for_event(menu_id: ObjectId, event: &Event, viewport: Rect) -> bool {
let Event::MousePress { pos, button } = event else {
return false;
};
if *button != crate::event::mouse_button::SECONDARY {
return false;
}
open_context_menu(menu_id, *pos, viewport)
}
pub fn request_repaint(id: ObjectId) {
crate::invalidate_surface(id);
}
pub fn render_frame(id: ObjectId, size: Size, clear: crate::core::Color) -> Option<Vec<u8>> {
if size.width == 0 || size.height == 0 {
return None;
}
let mut backend = SoftwarePaintBackend::new(size, 1.0);
let painted = with_widget_mut(id, |widget| {
let Some(drawable) = widget.as_draw_mut() else {
return false;
};
backend.begin_frame(clear);
{
let mut context = RenderContext::new(&mut backend);
drawable.draw(&mut context);
}
backend.end_frame();
true
})?;
if !painted {
return None;
}
Some(backend.frame_rgba().to_vec())
}
pub fn render_frame_at_geometry(id: ObjectId, clear: crate::core::Color) -> Option<Vec<u8>> {
let geometry = geometry_of(id)?;
render_frame(id, Size::new(geometry.width, geometry.height), clear)
}
#[cfg(all(test, full_widgets))]
mod tests {
use super::*;
use crate::core::{Color, Point};
use crate::widget::special_widgets::code_editor::CodeEditor;
fn editor_of(widget: &mut dyn Widget) -> Option<&mut CodeEditor> {
(widget as &mut dyn std::any::Any).downcast_mut::<CodeEditor>()
}
fn sample_editor(text: &str) -> Box<dyn Widget> {
let mut editor = CodeEditor::new(Rect::new(0, 0, 320, 200));
editor.set_text(text);
Box::new(editor)
}
#[test]
fn register_then_unregister_round_trips() {
let before = mounted_count();
let id = register(sample_editor("fn main() {}")).expect("ui thread has a registry");
assert!(is_mounted(id));
assert_eq!(mounted_count(), before + 1);
assert!(unregister(id));
assert!(!is_mounted(id));
assert_eq!(mounted_count(), before);
}
#[test]
fn unregister_unknown_id_is_false() {
assert!(!unregister(0xDEAD_BEEF));
}
#[test]
fn mounted_ids_do_not_collide_with_platform_ids() {
let id = register(sample_editor("x")).expect("registry");
assert!(id > 0x5345_4C46_0000_0000, "mounted id {id:#x} must not look platform-issued");
unregister(id);
}
#[test]
fn with_widget_mut_exposes_the_real_widget() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let lines =
with_widget_mut(id, |widget| editor_of(widget).map(|editor| editor.line_count()))
.flatten();
assert_eq!(lines, Some(1));
unregister(id);
}
#[test]
fn with_widget_mut_on_missing_id_is_none() {
assert!(with_widget_mut(0x1234_5678, |_| ()).is_none());
}
#[test]
fn geometry_can_be_read_and_written_through_the_registry() {
let id = register(sample_editor("x")).expect("registry");
assert_eq!(geometry_of(id).map(|r| (r.width, r.height)), Some((320, 200)));
assert!(set_geometry(id, Rect::new(5, 6, 400, 300)));
let geometry = geometry_of(id).expect("mounted");
assert_eq!((geometry.x, geometry.y, geometry.width, geometry.height), (5, 6, 400, 300));
unregister(id);
}
#[test]
fn render_frame_returns_rgba_of_the_requested_size() {
let id = register(sample_editor("fn main() {\n let x = 1;\n}")).expect("registry");
let frame = render_frame(id, Size::new(64, 48), Color::WHITE).expect("frame");
assert_eq!(frame.len(), 64 * 48 * 4);
assert!(frame.chunks_exact(4).any(|px| px[3] != 0), "frame must contain drawn pixels");
unregister(id);
}
#[test]
fn render_frame_rejects_empty_size_and_missing_ids() {
let id = register(sample_editor("x")).expect("registry");
assert!(render_frame(id, Size::new(0, 10), Color::WHITE).is_none());
assert!(render_frame(0x9999, Size::new(10, 10), Color::WHITE).is_none());
unregister(id);
}
#[test]
fn render_frame_at_geometry_uses_widget_geometry() {
let id = register(sample_editor("abc")).expect("registry");
let frame = render_frame_at_geometry(id, Color::WHITE).expect("frame");
assert_eq!(frame.len(), 320 * 200 * 4);
unregister(id);
}
#[test]
fn custom_widgets_report_the_draw_bridge() {
use crate::widget::special_widgets::chip::Chip;
use crate::widget::special_widgets::color_picker::ColorPicker;
use crate::widget::special_widgets::gantt_widget::GanttWidget;
use crate::widget::special_widgets::snackbar::Snackbar;
use crate::widget::special_widgets::terminal_view::TerminalView;
fn assert_mountable(name: &str, widget: &mut dyn Widget) {
assert!(
widget.as_draw_mut().is_some(),
"{name} implements Draw but does not report itself via Widget::as_draw_mut, \
so mounting it in a window would paint nothing"
);
}
assert_mountable("CodeEditor", &mut CodeEditor::new(Rect::new(0, 0, 80, 60)));
assert_mountable("Chip", &mut Chip::new(Rect::new(0, 0, 80, 24)));
assert_mountable("ColorPicker", &mut ColorPicker::new(Rect::new(0, 0, 200, 150)));
assert_mountable("GanttWidget", &mut GanttWidget::new(Rect::new(0, 0, 200, 150)));
assert_mountable("Snackbar", &mut Snackbar::new(Rect::new(0, 0, 200, 40)));
assert_mountable("TerminalView", &mut TerminalView::new(Rect::new(0, 0, 200, 150)));
}
#[test]
fn built_in_controls_claim_the_draw_bridge() {
use crate::widget::base_widgets::button::Button;
use crate::widget::base_widgets::checkbox::CheckBox;
use crate::widget::base_widgets::label::Label;
let mut button = Button::new("ok".to_string(), Rect::new(0, 0, 80, 30));
assert!(
button.as_draw_mut().is_some(),
"Button implements Draw, so mounting it must paint instead of showing a blank surface"
);
let mut checkbox = CheckBox::new(Rect::new(0, 0, 120, 24));
assert!(checkbox.as_draw_mut().is_some(), "CheckBox must be paintable");
let mut label = Label::new("hello".to_string(), Rect::new(0, 0, 80, 20));
assert!(label.as_draw_mut().is_some(), "Label must be paintable");
}
#[test]
fn dispatch_event_reaches_the_widget() {
use crate::event::Event;
let mut editor = CodeEditor::new(Rect::new(0, 0, 320, 200));
editor.set_text("hello");
let id = register(Box::new(editor)).expect("registry");
let delivered =
dispatch_event(id, &Event::MousePress { pos: Point::new(150, 90), button: 1 });
assert!(delivered, "event must reach the mounted widget");
let cursor =
with_widget_mut(id, |widget| editor_of(widget).map(|editor| editor.cursor())).flatten();
assert!(cursor.is_some(), "widget must still be the editor");
unregister(id);
}
#[cfg(full_widgets)]
#[test]
fn a_secondary_press_opens_the_mounted_context_menu() {
let mut menu = crate::Menu::new("Edit", Rect::new(0, 0, 160, 100));
menu.add_action("Copy");
let id = register(Box::new(menu)).expect("registry");
let viewport = Rect::new(0, 0, 500, 400);
let opened = open_context_menu_for_event(
id,
&Event::MousePress {
pos: Point::new(120, 90),
button: crate::event::mouse_button::SECONDARY,
},
viewport,
);
assert!(opened, "a secondary press must open the context menu");
let geometry = geometry_of(id).expect("mounted");
assert_eq!((geometry.x, geometry.y), (120, 90));
assert!(is_mounted(id));
unregister(id);
}
#[cfg(full_widgets)]
#[test]
fn non_secondary_events_do_not_open_the_context_menu() {
let mut menu = crate::Menu::new("Edit", Rect::new(0, 0, 160, 100));
menu.add_action("Copy");
let id = register(Box::new(menu)).expect("registry");
let viewport = Rect::new(0, 0, 500, 400);
assert!(!open_context_menu_for_event(
id,
&Event::MousePress {
pos: Point::new(10, 10),
button: crate::event::mouse_button::PRIMARY
},
viewport,
));
assert!(!open_context_menu_for_event(
id,
&Event::MouseMove { pos: Point::new(10, 10) },
viewport
));
unregister(id);
}
#[cfg(full_widgets)]
#[test]
fn opening_a_context_menu_on_a_non_menu_reports_failure() {
let label = crate::widget::base_widgets::label::Label::new(
"hi".to_string(),
Rect::new(0, 0, 80, 24),
);
let id = register(Box::new(label)).expect("registry");
assert!(!open_context_menu(id, Point::new(5, 5), Rect::new(0, 0, 400, 400)));
assert!(!open_context_menu(9_999, Point::new(5, 5), Rect::new(0, 0, 400, 400)));
unregister(id);
}
fn tree_with_two_children() -> (ObjectId, ObjectId, ObjectId) {
let mut parent =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 300, 300));
let first = register(Box::new(crate::widget::base_widgets::label::Label::new(
"first".to_string(),
Rect::new(0, 0, 100, 50),
)))
.expect("registry");
let second = register(Box::new(crate::widget::base_widgets::label::Label::new(
"second".to_string(),
Rect::new(0, 0, 100, 50),
)))
.expect("registry");
parent.add_child(first);
parent.add_child(second);
let parent = register(Box::new(parent)).expect("registry");
(parent, first, second)
}
#[test]
fn hit_test_descends_to_the_child_under_the_point() {
let (parent, first, second) = tree_with_two_children();
assert!(set_geometry(first, Rect::new(10, 10, 100, 50)));
assert!(set_geometry(second, Rect::new(150, 10, 100, 50)));
assert_eq!(widget_at(parent, Point::new(20, 20)), Some(first));
assert_eq!(widget_at(parent, Point::new(160, 20)), Some(second));
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn hit_test_falls_back_to_the_container() {
let (parent, first, second) = tree_with_two_children();
set_geometry(first, Rect::new(10, 10, 100, 50));
set_geometry(second, Rect::new(150, 10, 100, 50));
assert_eq!(widget_at(parent, Point::new(250, 250)), Some(parent));
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn hit_test_returns_none_outside_the_root() {
let (parent, first, second) = tree_with_two_children();
assert_eq!(widget_at(parent, Point::new(5_000, 5_000)), None);
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn hit_test_on_an_unmounted_root_is_none() {
assert_eq!(widget_at(0xDEAD_BEEF, Point::new(0, 0)), None);
}
#[test]
fn hit_test_prefers_the_topmost_overlapping_child() {
let (parent, first, second) = tree_with_two_children();
set_geometry(first, Rect::new(10, 10, 100, 50));
set_geometry(second, Rect::new(10, 10, 100, 50));
assert_eq!(widget_at(parent, Point::new(20, 20)), Some(second));
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn hit_test_skips_hidden_children() {
let (parent, first, second) = tree_with_two_children();
set_geometry(first, Rect::new(10, 10, 100, 50));
set_geometry(second, Rect::new(10, 10, 100, 50));
with_widget_mut(second, |widget| widget.hide());
assert_eq!(widget_at(parent, Point::new(20, 20)), Some(first));
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn hit_test_finds_a_nested_child_through_two_levels() {
let inner =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(20, 20, 100, 100));
let inner = register(Box::new(inner)).expect("registry");
let leaf = register(Box::new(crate::widget::base_widgets::label::Label::new(
"leaf".to_string(),
Rect::new(30, 30, 40, 20),
)))
.expect("registry");
with_widget_mut(inner, |widget| widget.add_child(leaf));
let outer =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 300, 300));
let outer = register(Box::new(outer)).expect("registry");
with_widget_mut(outer, |widget| widget.add_child(inner));
assert_eq!(widget_at(outer, Point::new(40, 40)), Some(leaf));
assert_eq!(widget_at(outer, Point::new(25, 25)), Some(inner));
assert_eq!(widget_at(outer, Point::new(250, 250)), Some(outer));
unregister(outer);
unregister(inner);
unregister(leaf);
}
#[test]
fn dispatch_pointer_event_reaches_the_widget_under_the_point() {
let mut parent =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 400, 300));
let editor = register(sample_editor("fn main() {}\nlet a = 1;\nlet b = 2;")).expect("r");
set_geometry(editor, Rect::new(100, 100, 320, 200));
parent.add_child(editor);
let parent = register(Box::new(parent)).expect("registry");
let delivered = dispatch_pointer_event(
parent,
&Event::MousePress { pos: Point::new(150, 130), button: 1 },
Point::new(150, 130),
);
assert!(delivered, "the editor under the point must receive the click");
assert_eq!(widget_at(parent, Point::new(380, 20)), Some(parent));
unregister(parent);
unregister(editor);
}
#[test]
fn dispatch_pointer_event_outside_the_root_is_false() {
let (parent, first, second) = tree_with_two_children();
let delivered = dispatch_pointer_event(
parent,
&Event::MousePress { pos: Point::new(9_000, 9_000), button: 1 },
Point::new(9_000, 9_000),
);
assert!(!delivered);
unregister(parent);
unregister(first);
unregister(second);
}
#[test]
fn focus_moves_and_reports_the_new_owner() {
let first = register(sample_editor("a")).expect("registry");
let second = register(sample_editor("b")).expect("registry");
assert!(focus_widget(first));
assert_eq!(focused_widget(), Some(first));
assert!(has_focus(first));
assert!(!has_focus(second));
assert!(focus_widget(second));
assert_eq!(focused_widget(), Some(second));
assert!(!has_focus(first), "the previous owner must lose focus when another takes it");
unregister(first);
unregister(second);
}
#[test]
fn focus_on_the_current_owner_reports_no_move() {
let id = register(sample_editor("a")).expect("registry");
assert!(focus_widget(id));
assert!(!focus_widget(id), "focus did not move, so the call must report false");
unregister(id);
}
#[test]
fn focus_on_an_unmounted_widget_is_refused() {
assert!(!focus_widget(0xDEAD_BEEF));
assert_eq!(focused_widget(), None);
}
#[test]
fn clearing_focus_is_reported_once() {
let id = register(sample_editor("a")).expect("registry");
focus_widget(id);
assert!(clear_focus());
assert_eq!(focused_widget(), None);
assert!(!clear_focus(), "there was no focus to clear the second time");
unregister(id);
}
#[test]
fn unregistering_a_focused_widget_drops_it_from_the_tab_order() {
let first = register(sample_editor("a")).expect("registry");
let second = register(sample_editor("b")).expect("registry");
register_focusable(first);
register_focusable(second);
focus_widget(first);
unregister(first);
assert_eq!(focused_widget(), None, "the removed widget must not stay focused");
assert!(!focusable_widgets().contains(&first));
unregister(second);
}
#[test]
fn tab_order_moves_focus_and_wraps() {
let a = register(sample_editor("a")).expect("registry");
let b = register(sample_editor("b")).expect("registry");
set_focus_order_for_test(&[a, b]);
assert_eq!(focus_next(true), Some(a));
assert_eq!(focused_widget(), Some(a));
assert_eq!(focus_next(true), Some(b));
assert_eq!(focus_next(true), Some(a));
assert_eq!(focus_next(false), Some(b));
assert_eq!(focus_next(false), Some(a));
unregister(a);
unregister(b);
}
#[test]
#[cfg(full_widgets)]
fn platform_pointer_routing_reaches_a_nested_child() {
let mut parent =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 300, 300));
let child = register(Box::new(crate::widget::base_widgets::label::Label::new(
"child".to_string(),
Rect::new(40, 40, 80, 30),
)))
.expect("registry");
parent.add_child(child);
let parent = register(Box::new(parent)).expect("registry");
let backend = crate::platform::platform_facts();
assert!(
backend.route_pointer_event(
parent,
&Event::MousePress { pos: Point::new(50, 50), button: 1 },
Point::new(50, 50),
),
"a click inside the child must be delivered to it"
);
assert!(
backend.route_pointer_event(
parent,
&Event::MousePress { pos: Point::new(250, 250), button: 1 },
Point::new(250, 250),
),
"a click inside the root must still be delivered"
);
assert!(
!backend.route_pointer_event(
parent,
&Event::MousePress { pos: Point::new(9_000, 9_000), button: 1 },
Point::new(9_000, 9_000),
),
"a click outside the root must report that nothing accepted it"
);
unregister(parent);
unregister(child);
}
fn set_focus_order_for_test(ids: &[ObjectId]) {
with_focus_manager(|focus| {
focus.set_focus_order(ids.to_vec());
focus.clear_focus();
})
.expect("ui thread has a focus manager");
}
fn two_siblings() -> (ObjectId, ObjectId, ObjectId) {
let mut parent =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 300, 300));
let left = register(Box::new(crate::widget::base_widgets::label::Label::new(
"left".to_string(),
Rect::new(0, 0, 100, 50),
)))
.expect("registry");
let right = register(Box::new(crate::widget::base_widgets::label::Label::new(
"right".to_string(),
Rect::new(0, 0, 100, 50),
)))
.expect("registry");
set_geometry(left, Rect::new(10, 10, 100, 50));
set_geometry(right, Rect::new(150, 10, 100, 50));
parent.add_child(left);
parent.add_child(right);
let parent = register(Box::new(parent)).expect("registry");
(parent, left, right)
}
#[test]
fn pointer_movement_sets_and_clears_the_hover_target() {
let (parent, left, right) = two_siblings();
clear_hover(Point::new(0, 0));
assert_eq!(hovered_widget(), None);
dispatch_pointer_event(
parent,
&Event::MouseMove { pos: Point::new(20, 20) },
Point::new(20, 20),
);
assert_eq!(hovered_widget(), Some(left), "the pointer is over `left`");
dispatch_pointer_event(
parent,
&Event::MouseMove { pos: Point::new(160, 20) },
Point::new(160, 20),
);
assert_eq!(hovered_widget(), Some(right), "crossing onto `right` must move hover");
clear_hover(Point::new(0, 0));
assert_eq!(hovered_widget(), None);
unregister(parent);
unregister(left);
unregister(right);
}
#[test]
fn entering_a_button_makes_it_report_hover() {
let parent = register(Box::new(crate::widget::container_widgets::groupbox::GroupBox::new(
Rect::new(0, 0, 300, 300),
)))
.expect("registry");
let mut button = crate::widget::base_widgets::button::Button::new(
"ok".to_string(),
Rect::new(20, 20, 80, 30),
);
button.set_geometry(Rect::new(20, 20, 80, 30));
let button_id = register(Box::new(button)).expect("registry");
with_widget_mut(parent, |widget| widget.add_child(button_id));
clear_hover(Point::new(0, 0));
dispatch_pointer_event(
parent,
&Event::MouseMove { pos: Point::new(30, 30) },
Point::new(30, 30),
);
let hovered = with_widget(button_id, |widget| {
(widget as &dyn core::any::Any)
.downcast_ref::<crate::widget::base_widgets::button::Button>()
.map(|b| b.is_hovered())
})
.flatten();
assert_eq!(hovered, Some(true), "MouseEnter must reach the button's hover flag");
clear_hover(Point::new(0, 0));
unregister(parent);
unregister(button_id);
}
#[test]
fn unregistering_a_hovered_widget_clears_the_target() {
let (parent, left, right) = two_siblings();
dispatch_pointer_event(
parent,
&Event::MouseMove { pos: Point::new(20, 20) },
Point::new(20, 20),
);
assert_eq!(hovered_widget(), Some(left));
unregister(left);
assert_eq!(hovered_widget(), None);
unregister(parent);
unregister(right);
}
#[test]
#[cfg(not(alloc_frugal))]
fn moving_focus_notifies_the_ime_bridge() {
let first = register(sample_editor("a")).expect("registry");
let second = register(sample_editor("b")).expect("registry");
clear_focus();
let recorder = RecordingImeBridge::default();
{
use crate::platform::ime::ImeBridge as _;
recorder.focus_in(first);
recorder.focus_out(first);
recorder.focus_in(second);
}
assert_eq!(
recorder.calls(),
vec![("in", first), ("out", first), ("in", second)],
"the IME bridge must be told about every focus change"
);
let backend = crate::platform::platform_facts();
if backend.ime_bridge().is_some() {
assert!(focus_widget(first));
assert_eq!(focused_widget(), Some(first));
assert!(clear_focus());
}
unregister(first);
unregister(second);
}
#[test]
fn row_major_traversal_orders_tab_by_position() {
let a = register(sample_editor("a")).expect("registry");
let b = register(sample_editor("b")).expect("registry");
let c = register(sample_editor("c")).expect("registry");
set_focus_order_for_test(&[c, a, b]);
assert!(set_focusable_position(a, 10, 10));
assert!(set_focusable_position(b, 200, 10));
assert!(set_focusable_position(c, 10, 100));
assert!(set_focus_traversal(crate::event::FocusTraversalStrategy::RowMajor));
assert_eq!(
focusable_widgets(),
vec![a, b, c],
"row-major must visit the top row left-to-right before the row below"
);
assert!(!set_focusable_position(0xDEAD_BEEF, 0, 0));
assert!(set_focus_traversal(crate::event::FocusTraversalStrategy::TabOrder));
unregister(a);
unregister(b);
unregister(c);
}
#[test]
fn column_major_traversal_orders_tab_by_column() {
let a = register(sample_editor("a")).expect("registry");
let b = register(sample_editor("b")).expect("registry");
set_focus_order_for_test(&[b, a]);
assert!(set_focusable_position(a, 10, 300));
assert!(set_focusable_position(b, 500, 10));
assert!(set_focus_traversal(crate::event::FocusTraversalStrategy::ColumnMajor));
assert_eq!(focusable_widgets(), vec![a, b], "column-major must visit left first");
assert!(set_focus_traversal(crate::event::FocusTraversalStrategy::TabOrder));
unregister(a);
unregister(b);
}
#[test]
fn captured_widget_receives_pointer_events_off_itself() {
let (parent, left, right) = two_siblings();
release_pointer_capture();
assert!(capture_pointer(left));
assert_eq!(capturing_widget(), Some(left));
assert!(has_pointer_capture(left));
assert!(
dispatch_pointer_event(
parent,
&Event::MouseMove { pos: Point::new(160, 20) },
Point::new(160, 20),
),
"a captured widget must receive the event even when the pointer left it"
);
assert_eq!(hovered_widget(), Some(right));
assert!(release_pointer_capture());
assert_eq!(capturing_widget(), None);
assert!(!release_pointer_capture(), "a second release has nothing to release");
unregister(parent);
unregister(left);
unregister(right);
}
#[test]
fn capturing_an_unmounted_widget_is_refused() {
release_pointer_capture();
assert!(!capture_pointer(0xDEAD_BEEF));
assert_eq!(capturing_widget(), None);
}
#[test]
fn unregistering_a_captured_widget_releases_capture() {
let (parent, left, right) = two_siblings();
assert!(capture_pointer(left));
unregister(left);
assert_eq!(capturing_widget(), None, "a removed widget must not keep capture");
release_pointer_capture();
unregister(parent);
unregister(right);
}
#[derive(Default)]
struct RecordingImeBridge {
calls: crate::compat::Mutex<Vec<(&'static str, ObjectId)>>,
}
impl RecordingImeBridge {
fn calls(&self) -> Vec<(&'static str, ObjectId)> {
self.calls.lock().unwrap().clone()
}
}
impl crate::platform::ime::ImeBridge for RecordingImeBridge {
fn focus_in(&self, widget_id: ObjectId) {
self.calls.lock().unwrap().push(("in", widget_id));
}
fn focus_out(&self, widget_id: ObjectId) {
self.calls.lock().unwrap().push(("out", widget_id));
}
fn commit_text(&self, _text: &str) {}
fn set_composition(&self, _composition: &crate::platform::ime::ImeComposition) {}
fn set_candidate_window_position(
&self,
_position: crate::platform::ime::ImeCandidatePosition,
) {
}
fn is_active(&self) -> bool {
true
}
}
}