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(0) };
#[allow(clippy::missing_const_for_thread_local)]
static NEXT_ID_LIMIT: RefCell<ObjectId> = const { RefCell::new(0) };
#[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());
#[allow(clippy::missing_const_for_thread_local)]
static MODAL: RefCell<alloc::vec::Vec<ObjectId>> = const { RefCell::new(alloc::vec::Vec::new()) };
}
const ID_BLOCK: ObjectId = 0x1_0000;
const ID_BASE: ObjectId = 0x5345_4C46_0000_0000;
static ID_HIGH_WATER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(ID_BASE);
fn reserve_id_block() -> Option<ObjectId> {
let start = ID_HIGH_WATER.fetch_add(ID_BLOCK, std::sync::atomic::Ordering::Relaxed);
start.checked_add(ID_BLOCK)?;
NEXT_ID.try_with(|next| *next.borrow_mut() = start).ok()?;
NEXT_ID_LIMIT.try_with(|limit| *limit.borrow_mut() = start.wrapping_add(ID_BLOCK)).ok()?;
Some(start)
}
fn next_widget_id() -> Result<ObjectId, ()> {
NEXT_ID
.try_with(|next| {
let id = *next.borrow();
let limit = NEXT_ID_LIMIT.try_with(|limit| *limit.borrow()).unwrap_or(0);
if id < limit {
*next.borrow_mut() = id.wrapping_add(1);
return Ok(id);
}
let start = reserve_id_block().ok_or(())?;
*next.borrow_mut() = start.wrapping_add(1);
Ok(start)
})
.unwrap_or(Err(()))
}
pub fn register(widget: Box<dyn Widget>) -> Option<ObjectId> {
let id = next_widget_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;
}
if let Some(own_id) = with_widget(id, |widget| widget.base().id()) {
let _ = OWN_IDS.try_with(|map| {
map.borrow_mut().insert(own_id, id);
});
}
let focusable = with_widget(id, |widget| widget.is_focusable()).unwrap_or(false);
if focusable {
register_focusable(id);
}
let _ = enable_damage_tracking_if_useful(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();
}
});
forget_cached_frame(id);
let _ = REPAINT.try_with(|map| map.borrow_mut().remove(&id));
let _ = REPAINT.try_with(|map| map.borrow_mut().remove(&id));
let own_id = MOUNTED
.try_with(|map| map.borrow().get(&id).map(|mounted| mounted.widget.base().id()))
.ok()
.flatten();
if let Some(own_id) = own_id {
let _ = OWN_IDS.try_with(|map| map.borrow_mut().remove(&own_id));
}
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;
}
if modal_blocks(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) && !modal_blocks(captured) {
return dispatch_event(captured, event);
}
let _ = release_pointer_capture();
return false;
}
match target {
Some(target) if !modal_blocks(target) => dispatch_event(target, event),
_ => 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 is_descendant_of(ancestor: ObjectId, descendant: ObjectId) -> bool {
if ancestor == descendant {
return true;
}
let mut cursor = with_widget(descendant, |widget| widget.parent());
while let Some(current) = cursor.flatten() {
if current == ancestor {
return true;
}
cursor = with_widget(current, |widget| widget.parent());
}
false
}
pub fn active_modal() -> Option<ObjectId> {
MODAL.try_with(|stack| stack.borrow().last().copied()).unwrap_or(None)
}
pub fn is_modal_active() -> bool {
MODAL.try_with(|stack| !stack.borrow().is_empty()).unwrap_or(false)
}
pub fn modal_blocks(id: ObjectId) -> bool {
let Some(modal) = active_modal() else {
return false;
};
!is_descendant_of(modal, id)
}
pub fn enter_modal(dialog_id: ObjectId) -> bool {
if !is_mounted(dialog_id) {
return false;
}
MODAL
.try_with(|stack| {
let mut stack = stack.borrow_mut();
if stack.last() == Some(&dialog_id) {
return true;
}
stack.push(dialog_id);
true
})
.unwrap_or(false)
}
pub fn exit_modal(dialog_id: ObjectId) -> bool {
MODAL
.try_with(|stack| {
let mut stack = stack.borrow_mut();
if let Some(pos) = stack.iter().rposition(|&id| id == dialog_id) {
stack.remove(pos);
true
} else {
false
}
})
.unwrap_or(false)
}
pub fn clear_modals() {
let _ = MODAL.try_with(|stack| stack.borrow_mut().clear());
}
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 for_each_mounted_widget<R>(mut f: impl FnMut(ObjectId, &mut dyn Widget) -> R) {
let _ = MOUNTED.try_with(|map| {
for (id, entry) in map.borrow_mut().iter_mut() {
f(*id, entry.widget.as_mut());
}
});
}
pub fn geometry_of(id: ObjectId) -> Option<Rect> {
MOUNTED
.try_with(|map| map.borrow().get(&id).map(|entry| entry.widget.geometry()))
.ok()
.flatten()
}
fn frame_origin(id: ObjectId) -> (i32, i32) {
geometry_of(id).map(|geometry| (geometry.x, geometry.y)).unwrap_or((0, 0))
}
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 {
if modal_blocks(id) {
return false;
}
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);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RepaintMode {
#[default]
Full,
Dirty,
Adaptive,
}
struct RepaintState {
mode: RepaintMode,
tracker: crate::performance::DirtyRegionTracker,
large_damage_run: u32,
}
pub const ADAPTIVE_LARGE_DAMAGE_RUN: u32 = 3;
impl RepaintState {
fn new() -> Self {
Self {
mode: RepaintMode::default(),
tracker: crate::performance::DirtyRegionTracker::new(),
large_damage_run: 0,
}
}
}
pub(crate) fn mark_widget_damage(own_id: ObjectId, rect: Rect) -> bool {
let Some(registry_id) = registry_id_of(own_id) else {
return false;
};
mark_dirty_rect(registry_id, rect)
}
pub fn registry_id_of(own_id: ObjectId) -> Option<ObjectId> {
OWN_IDS.try_with(|map| map.borrow().get(&own_id).copied()).ok().flatten()
}
pub fn set_repaint_mode_adaptive(id: ObjectId) -> bool {
set_repaint_mode(id, RepaintMode::Adaptive)
}
pub fn should_track_damage(id: ObjectId) -> bool {
if !has_ever_requested_redraw(id) {
return false;
}
let Some(geometry) = geometry_of(id) else {
return false;
};
let pixels = u64::from(geometry.width) * u64::from(geometry.height);
if pixels < AUTO_REPAINT_MIN_PIXELS {
return false;
}
let has_children =
with_widget(id, |widget| !widget.base().children().is_empty()).unwrap_or(false);
has_children
}
pub const AUTO_REPAINT_MIN_PIXELS: u64 = 250_000;
fn has_ever_requested_redraw(id: ObjectId) -> bool {
with_widget(id, |widget| widget.has_ever_requested_redraw()).unwrap_or(false)
}
pub fn enable_damage_tracking_if_useful(id: ObjectId) -> bool {
if !should_track_damage(id) {
return false;
}
set_repaint_mode_adaptive(id)
}
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static OWN_IDS: RefCell<HashMap<ObjectId, ObjectId>> = RefCell::new(HashMap::new());
#[allow(clippy::missing_const_for_thread_local)]
static REPAINT: RefCell<HashMap<ObjectId, RepaintState>> = RefCell::new(HashMap::new());
#[allow(clippy::missing_const_for_thread_local)]
static LAST_FRAME: RefCell<HashMap<ObjectId, (Size, Vec<u8>)>> = RefCell::new(HashMap::new());
}
pub fn set_repaint_mode(id: ObjectId, mode: RepaintMode) -> bool {
let mounted = MOUNTED.try_with(|m| m.borrow().contains_key(&id)).unwrap_or(false);
if !mounted {
return false;
}
let _ = REPAINT.try_with(|map| {
let mut map = map.borrow_mut();
let state = map.entry(id).or_insert_with(RepaintState::new);
state.mode = mode;
if mode == RepaintMode::Full {
state.tracker.clear();
}
});
true
}
pub fn repaint_mode(id: ObjectId) -> RepaintMode {
REPAINT
.try_with(|map| map.borrow().get(&id).map(|state| state.mode))
.ok()
.flatten()
.unwrap_or_default()
}
pub fn mark_dirty_rect(id: ObjectId, rect: Rect) -> bool {
let recorded = REPAINT
.try_with(|map| {
let mut map = map.borrow_mut();
let Some(state) = map.get_mut(&id) else {
return false;
};
if state.mode == RepaintMode::Full {
return false;
}
state.tracker.add(rect);
true
})
.ok()
.unwrap_or(false);
if recorded {
if !crate::invalidate_surface_rect(id, rect) {
crate::invalidate_surface(id);
}
}
recorded
}
pub fn dirty_rects(id: ObjectId) -> Vec<Rect> {
let mut tracker = REPAINT
.try_with(|map| {
map.borrow().get(&id).map(|state| {
let mut copy = crate::performance::DirtyRegionTracker::new();
for region in state.tracker.regions() {
copy.add(region.rect);
}
copy
})
})
.ok()
.flatten()
.unwrap_or_default();
tracker.merge();
tracker.regions().iter().map(|region| region.rect).collect()
}
pub fn render_frame_incremental(
id: ObjectId,
size: Size,
clear: crate::core::Color,
previous: Option<&[u8]>,
) -> Option<Vec<u8>> {
if size.width == 0 || size.height == 0 {
return None;
}
let mode = repaint_mode(id);
let expected = size.width as usize * size.height as usize * 4;
let carried = previous.filter(|buffer| buffer.len() == expected);
let use_dirty = mode != RepaintMode::Full && carried.is_some() && !dirty_rects(id).is_empty();
if mode == RepaintMode::Full || carried.is_none() {
if let Some(frame) = render_frame(id, size, clear) {
let _ = REPAINT.try_with(|map| {
if let Some(state) = map.borrow_mut().get_mut(&id) {
state.tracker.clear();
state.large_damage_run = 0;
}
});
return Some(frame);
}
return None;
}
let covered_too_much = {
let frame_area = u64::from(size.width) * u64::from(size.height);
let covered: u64 =
dirty_rects(id).iter().map(|rect| u64::from(rect.width) * u64::from(rect.height)).sum();
frame_area == 0
|| (covered as f32)
>= (frame_area as f32) * crate::performance::render_dirty::FULL_REPAINT_AREA_RATIO
};
if mode == RepaintMode::Adaptive {
let learned = REPAINT
.try_with(|map| {
map.borrow()
.get(&id)
.is_some_and(|state| state.large_damage_run >= ADAPTIVE_LARGE_DAMAGE_RUN)
})
.unwrap_or(false);
if learned && covered_too_much {
if let Some(frame) = render_frame(id, size, clear) {
let _ = REPAINT.try_with(|map| {
if let Some(state) = map.borrow_mut().get_mut(&id) {
state.tracker.clear();
}
});
return Some(frame);
}
return None;
}
}
if !use_dirty {
let _ = REPAINT.try_with(|map| {
if let Some(state) = map.borrow_mut().get_mut(&id) {
state.large_damage_run = 0;
}
});
return Some(carried?.to_vec());
}
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.seed_from(carried?);
let origin = frame_origin(id);
let mut tracker = REPAINT
.try_with(|map| {
map.borrow_mut().get_mut(&id).map(|state| {
if mode == RepaintMode::Adaptive {
state.large_damage_run =
if covered_too_much { state.large_damage_run.saturating_add(1) } else { 0 };
}
let mut taken = crate::performance::DirtyRegionTracker::new();
for region in state.tracker.regions() {
taken.add(region.rect);
}
state.tracker.clear();
taken
})
})
.ok()
.flatten()?;
tracker.merge();
let painted = with_widget_mut(id, |widget| {
let Some(drawable) = widget.as_draw_mut() else {
return false;
};
{
let mut context = RenderContext::new(&mut backend);
context.push_offset(-origin.0, -origin.1);
crate::performance::render_dirty_regions(&mut tracker, &mut context, |ctx| {
drawable.draw(ctx);
});
context.pop_offset();
}
backend.end_frame();
true
})?;
if !painted {
return None;
}
Some(backend.frame_rgba().to_vec())
}
pub fn render_frame_cached(id: ObjectId, size: Size, clear: crate::core::Color) -> Option<Vec<u8>> {
if size.width == 0 || size.height == 0 {
return None;
}
let previous = LAST_FRAME
.try_with(|map| {
map.borrow()
.get(&id)
.filter(|(stored, _)| *stored == size)
.map(|(_, frame)| frame.clone())
})
.ok()
.flatten();
let frame = render_frame_incremental(id, size, clear, previous.as_deref())?;
let _ = LAST_FRAME.try_with(|map| {
map.borrow_mut().insert(id, (size, frame.clone()));
});
Some(frame)
}
pub fn forget_cached_frame(id: ObjectId) {
let _ = LAST_FRAME.try_with(|map| map.borrow_mut().remove(&id));
}
pub fn cached_frame_size(id: ObjectId) -> Option<Size> {
LAST_FRAME.try_with(|map| map.borrow().get(&id).map(|(size, _)| *size)).ok().flatten()
}
pub fn adaptive_large_damage_run(id: ObjectId) -> u32 {
REPAINT
.try_with(|map| map.borrow().get(&id).map(|state| state.large_damage_run))
.ok()
.flatten()
.unwrap_or(0)
}
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 origin = frame_origin(id);
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);
context.push_offset(-origin.0, -origin.1);
drawable.draw(&mut context);
context.pop_offset();
}
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>()
}
#[test]
fn the_auto_decision_refuses_a_small_surface() {
let small = register(Box::new(CodeEditor::new(Rect::new(0, 0, 100, 100)))).expect("r");
let big = register(sample_editor("fn main() {}")).expect("r");
const { assert!(100u64 * 100 < AUTO_REPAINT_MIN_PIXELS) };
const { assert!(320u64 * 200 < AUTO_REPAINT_MIN_PIXELS) };
assert!(!should_track_damage(small), "a 100x100 surface is not worth regioning");
assert!(!should_track_damage(big), "320x200 is below the threshold as well");
let large = register(auto_accepted_surface()).expect("r");
assert!(should_track_damage(large), "800x600 with children must be accepted");
unregister(small);
unregister(big);
unregister(large);
}
#[test]
fn the_auto_decision_refuses_a_control_with_no_children() {
let lone = register(Box::new(CodeEditor::new(Rect::new(0, 0, 800, 600)))).expect("r");
assert!(
with_widget(lone, |widget| widget.base().children().is_empty()).unwrap_or(false),
"the fixture must have no children"
);
assert!(
!should_track_damage(lone),
"a control with no children has one rect, which is the whole surface"
);
unregister(lone);
}
#[test]
fn enable_if_useful_is_a_no_op_where_it_would_not_help() {
let small = register(Box::new(CodeEditor::new(Rect::new(0, 0, 80, 60)))).expect("r");
assert!(!enable_damage_tracking_if_useful(small));
assert_eq!(
repaint_mode(small),
RepaintMode::Full,
"a refused enable must leave the mode alone"
);
assert!(!enable_damage_tracking_if_useful(0xDEAD_BEEF));
unregister(small);
}
#[test]
fn a_widget_enabled_by_the_auto_decision_tracks_damage() {
let id = register(auto_accepted_surface()).expect("r");
assert_eq!(repaint_mode(id), RepaintMode::Adaptive, "register must accept this surface");
with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
assert_eq!(
dirty_rects(id).len(),
1,
"the enabled widget must record damage from request_redraw"
);
unregister(id);
let explicit = register(container_with_child()).expect("r");
assert!(set_geometry(explicit, Rect::new(0, 0, 800, 600)));
assert!(
!enable_damage_tracking_if_useful(explicit),
"a control that never asked to repaint must be refused"
);
with_widget(explicit, |widget| widget.request_redraw()).expect("widget");
assert!(enable_damage_tracking_if_useful(explicit));
assert_eq!(repaint_mode(explicit), RepaintMode::Adaptive);
unregister(explicit);
}
#[test]
fn the_auto_decision_leaves_a_silent_control_alone() {
let silent = register(auto_accepted_surface()).expect("r");
assert_eq!(repaint_mode(silent), RepaintMode::Adaptive, "fixture sanity");
unregister(silent);
let id = register(container_with_child()).expect("r");
assert!(set_geometry(id, Rect::new(0, 0, 800, 600)));
assert!(set_repaint_mode(id, RepaintMode::Full));
assert!(!should_track_damage(id), "a silent control must not be enabled");
with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
assert!(should_track_damage(id), "after asking for a repaint it becomes eligible");
unregister(id);
}
#[test]
fn unregistering_clears_the_auto_decision_state() {
let id = register(auto_accepted_surface()).expect("r");
assert_eq!(repaint_mode(id), RepaintMode::Adaptive);
assert!(should_track_damage(id));
unregister(id);
assert!(!should_track_damage(id), "a dead id must not be judged as trackable");
assert_eq!(repaint_mode(id), RepaintMode::Full, "nor keep a stale mode");
}
#[test]
fn unregister_forgets_the_own_id_mapping() {
let id = register(sample_editor("fn main() {}")).expect("r");
let own = with_widget(id, |widget| widget.base().id()).expect("widget");
assert_eq!(registry_id_of(own), Some(id));
unregister(id);
assert_eq!(registry_id_of(own), None, "the reverse map must not outlive the widget");
}
#[test]
fn diag_container() {
let id = register(auto_accepted_surface()).expect("r");
let geometry = geometry_of(id).expect("geometry");
let kids = with_widget(id, |w| w.base().children().len()).unwrap_or(999);
assert_eq!(geometry.width, 800, "geometry must survive registration");
assert_eq!((geometry.width, geometry.height), (800, 600));
assert_eq!(kids, 1, "children must survive registration");
assert_eq!(repaint_mode(id), RepaintMode::Adaptive);
unregister(id);
}
fn large_damage_run_of(id: ObjectId) -> u32 {
REPAINT
.try_with(|map| map.borrow().get(&id).map(|state| state.large_damage_run))
.ok()
.flatten()
.unwrap_or(0)
}
fn container_with_child() -> Box<dyn Widget> {
let mut group =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 320, 200));
group.base_mut().add_child(7);
Box::new(group)
}
fn auto_accepted_surface() -> Box<dyn Widget> {
let mut group =
crate::widget::container_widgets::groupbox::GroupBox::new(Rect::new(0, 0, 800, 600));
group.base_mut().add_child(7);
group.base_mut().request_redraw();
Box::new(group)
}
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 request_redraw_records_damage_when_adaptive() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode_adaptive(id));
assert!(dirty_rects(id).is_empty(), "nothing has asked to repaint yet");
with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
let damaged = dirty_rects(id);
assert_eq!(damaged.len(), 1, "request_redraw must record exactly one region: {damaged:?}");
assert_eq!(damaged[0], Rect::new(0, 0, 320, 200), "the control's own geometry");
unregister(id);
}
#[test]
fn request_redraw_records_nothing_in_full_mode() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert_eq!(repaint_mode(id), RepaintMode::Full, "Full is the default");
with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
assert!(dirty_rects(id).is_empty(), "Full mode must not accumulate damage");
unregister(id);
}
#[test]
fn request_redraw_on_an_unregistered_widget_is_a_no_op() {
let editor = CodeEditor::new(Rect::new(0, 0, 40, 20));
editor.base().request_redraw();
assert!(dirty_rects(editor.base().id()).is_empty());
}
#[test]
fn a_painted_frame_consumes_the_damage() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode_adaptive(id));
let size = Size::new(320, 200);
let first = render_frame_incremental(id, size, Color::WHITE, None).expect("frame");
assert!(dirty_rects(id).is_empty(), "a full paint resets the damage");
with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
assert_eq!(dirty_rects(id).len(), 1);
let second = render_frame_incremental(id, size, Color::WHITE, Some(&first)).expect("frame");
assert_eq!(second.len(), first.len(), "the frame keeps its size");
assert!(dirty_rects(id).is_empty(), "the incremental paint consumed the damage");
unregister(id);
}
#[test]
fn adaptive_learns_from_full_surface_damage_and_recovers() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode_adaptive(id));
let size = Size::new(320, 200);
let full = Rect::new(0, 0, 320, 200);
let mut frame = render_frame_incremental(id, size, Color::WHITE, None).expect("frame");
for _ in 0..ADAPTIVE_LARGE_DAMAGE_RUN {
assert!(mark_dirty_rect(id, full));
frame = render_frame_incremental(id, size, Color::WHITE, Some(&frame)).expect("frame");
}
assert_eq!(
large_damage_run_of(id),
ADAPTIVE_LARGE_DAMAGE_RUN,
"a run of whole-surface frames must be counted"
);
assert!(mark_dirty_rect(id, Rect::new(0, 0, 4, 4)));
let _ = render_frame_incremental(id, size, Color::WHITE, Some(&frame)).expect("frame");
assert_eq!(large_damage_run_of(id), 0, "small damage must reset the run");
unregister(id);
}
#[test]
fn an_incremental_repaint_matches_a_full_repaint() {
let size = Size::new(320, 200);
let full_id = register(sample_editor("fn main() {}")).expect("registry");
let baseline = render_frame_incremental(full_id, size, Color::WHITE, None).expect("frame");
let mut expected = baseline.clone();
let full_second =
render_frame_incremental(full_id, size, Color::WHITE, Some(&baseline)).expect("frame");
assert_eq!(full_second, baseline, "a full repaint of unchanged state is identical");
expected.clone_from(&full_second);
let dirty_id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode_adaptive(dirty_id));
let first = render_frame_incremental(dirty_id, size, Color::WHITE, None).expect("frame");
assert!(mark_dirty_rect(dirty_id, Rect::new(8, 8, 24, 12)));
let second =
render_frame_incremental(dirty_id, size, Color::WHITE, Some(&first)).expect("frame");
assert_eq!(second.len(), expected.len());
assert_eq!(
second, expected,
"a region-limited repaint must produce the same pixels as a full one"
);
unregister(full_id);
unregister(dirty_id);
}
#[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 repaint_defaults_to_full() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert_eq!(repaint_mode(id), RepaintMode::Full);
unregister(id);
}
#[test]
fn set_repaint_mode_rejects_an_unmounted_id() {
assert!(!set_repaint_mode(0xDEAD_BEEF, RepaintMode::Dirty));
}
#[test]
fn damage_is_recorded_only_when_a_mode_will_read_it() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let rect = Rect::new(10, 10, 40, 20);
assert!(!mark_dirty_rect(id, rect), "Full mode must not accumulate damage");
assert!(dirty_rects(id).is_empty());
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, rect), "Dirty mode must record the damage");
assert_eq!(dirty_rects(id), vec![rect]);
unregister(id);
}
#[test]
fn recording_damage_asks_the_platform_to_repaint() {
use crate::platform::RecordingInvalidations;
let id = register(sample_editor("fn main() {}")).expect("registry");
let log: &'static RecordingInvalidations =
alloc::boxed::Box::leak(alloc::boxed::Box::new(RecordingInvalidations::new()));
assert!(set_repaint_mode(id, RepaintMode::Dirty));
let rect = Rect::new(4, 8, 16, 16);
let recorded =
crate::platform::with_recorded_invalidations(log, || mark_dirty_rect(id, rect));
assert!(recorded, "the damage must still be recorded");
assert_eq!(
log.calls(),
vec![(id, Some(rect))],
"the platform must be told about the damage, narrowed to the rect when it can"
);
assert!(set_repaint_mode(id, RepaintMode::Full));
log.clear();
assert!(!crate::platform::with_recorded_invalidations(log, || {
mark_dirty_rect(id, rect)
}));
assert!(log.calls().is_empty(), "a refused mark must not reach the platform");
unregister(id);
}
#[test]
fn a_backend_that_cannot_narrow_gets_a_whole_surface_repaint() {
use crate::platform::RecordingInvalidations;
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(id, RepaintMode::Dirty));
let log: &'static RecordingInvalidations = alloc::boxed::Box::leak(alloc::boxed::Box::new(
RecordingInvalidations::refusing_narrowing(),
));
let rect = Rect::new(2, 2, 8, 8);
assert!(crate::platform::with_recorded_invalidations(log, || mark_dirty_rect(id, rect)));
assert_eq!(
log.calls(),
vec![(id, None)],
"a backend that refuses narrowing must receive a whole-surface invalidation"
);
unregister(id);
}
#[test]
fn switching_to_full_forgets_accumulated_damage() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, Rect::new(0, 0, 10, 10)));
assert_eq!(dirty_rects(id).len(), 1);
assert!(set_repaint_mode(id, RepaintMode::Full));
assert!(dirty_rects(id).is_empty(), "Full must clear what it will not replay");
unregister(id);
}
#[test]
fn an_undamaged_frame_returns_the_previous_pixels() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let size = Size::new(64, 48);
let first = render_frame(id, size, Color::rgb(10, 20, 30)).expect("first frame");
assert!(set_repaint_mode(id, RepaintMode::Dirty));
let second = render_frame_incremental(id, size, Color::rgb(10, 20, 30), Some(&first))
.expect("second frame");
assert_eq!(second, first, "an undamaged frame must not change any pixel");
unregister(id);
}
#[test]
fn a_missing_previous_frame_forces_a_full_paint() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let size = Size::new(64, 48);
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, Rect::new(0, 0, 8, 8)));
let frame = render_frame_incremental(id, size, Color::rgb(1, 2, 3), None)
.expect("a frame is still produced");
assert_eq!(frame.len(), size.width as usize * size.height as usize * 4);
unregister(id);
}
#[test]
fn the_cached_renderer_remembers_its_frame() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let size = Size::new(64, 48);
assert_eq!(cached_frame_size(id), None, "nothing is cached before a first paint");
let first = render_frame_cached(id, size, Color::rgb(9, 9, 9)).expect("first frame");
assert_eq!(
cached_frame_size(id),
Some(size),
"the frame must be remembered, keyed by the size it was rendered at"
);
assert!(set_repaint_mode(id, RepaintMode::Dirty));
let second = render_frame_cached(id, size, Color::rgb(9, 9, 9)).expect("second frame");
assert_eq!(second, first, "an undamaged repaint must not change a pixel");
unregister(id);
assert_eq!(cached_frame_size(id), None, "unmounting must drop the frame");
}
#[test]
fn a_control_offset_from_the_origin_paints_into_its_own_frame() {
let geometry = Rect::new(12, 42, 200, 160);
let mut chart =
crate::widget::special_widgets::finance::candlestick_chart::CandlestickChart::new(
geometry,
);
let mut series = crate::widget::special_widgets::finance::types::PriceSeries::new();
for index in 0..20 {
let open = 100.0 + index as f64;
series.push(crate::widget::special_widgets::finance::types::Bar::new(
open,
open + 2.0,
open - 2.0,
open + 1.0,
1000.0,
));
}
chart.set_series(series);
let id = register(Box::new(chart)).expect("registry");
let size = Size::new(geometry.width, geometry.height);
let frame = render_frame(id, size, Color::BLACK).expect("frame");
let painted = frame.chunks_exact(4).filter(|pixel| *pixel != [0, 0, 0, 255]).count();
let total = size.width as usize * size.height as usize;
assert!(
painted > total / 2,
"a control offset by ({}, {}) painted only {painted}/{total} pixels",
geometry.x,
geometry.y
);
let mut blank_rows = 0usize;
for row in 0..size.height as usize {
let start = row * size.width as usize * 4;
let blank = frame[start..start + size.width as usize * 4]
.chunks_exact(4)
.all(|pixel| pixel == [0, 0, 0, 255]);
if !blank {
break;
}
blank_rows += 1;
}
assert!(
blank_rows < geometry.y as usize,
"the blank top band ({blank_rows} rows) must be the control's own margin, \
not its absolute y offset ({})",
geometry.y
);
unregister(id);
}
#[test]
fn a_resize_discards_the_cached_frame() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let small = Size::new(32, 24);
let large = Size::new(96, 72);
render_frame_cached(id, small, Color::rgb(1, 1, 1)).expect("small frame");
assert_eq!(cached_frame_size(id), Some(small));
let resized = render_frame_cached(id, large, Color::rgb(1, 1, 1)).expect("large frame");
assert_eq!(
resized.len(),
large.width as usize * large.height as usize * 4,
"a resize must produce a frame of the new size"
);
assert_eq!(
cached_frame_size(id),
Some(large),
"the cache must follow the resize rather than keep the old frame"
);
unregister(id);
}
#[test]
fn unmounting_drops_the_damage_too() {
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, Rect::new(0, 0, 4, 4)));
assert_eq!(dirty_rects(id).len(), 1);
unregister(id);
assert!(dirty_rects(id).is_empty(), "the damage must go with the widget");
}
#[test]
fn adaptive_learns_from_a_run_of_large_damage_but_dirty_does_not() {
let size = Size::new(200, 200);
let whole = Rect::new(0, 0, 200, 200);
let dirty_id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(dirty_id, RepaintMode::Dirty));
let mut frame = render_frame(dirty_id, size, Color::rgb(1, 1, 1)).expect("first frame");
for _ in 0..(ADAPTIVE_LARGE_DAMAGE_RUN + 2) {
assert!(mark_dirty_rect(dirty_id, whole));
frame = render_frame_incremental(dirty_id, size, Color::rgb(1, 1, 1), Some(&frame))
.expect("frame");
}
assert_eq!(
adaptive_large_damage_run(dirty_id),
0,
"Dirty must not learn: the caller already decided, so every frame is measured"
);
unregister(dirty_id);
let adaptive_id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(adaptive_id, RepaintMode::Adaptive));
let mut frame = render_frame(adaptive_id, size, Color::rgb(1, 1, 1)).expect("first frame");
assert_eq!(
adaptive_large_damage_run(adaptive_id),
0,
"a full paint is not evidence either way"
);
for expected in 1..=(ADAPTIVE_LARGE_DAMAGE_RUN as usize) {
assert!(mark_dirty_rect(adaptive_id, whole));
frame = render_frame_incremental(adaptive_id, size, Color::rgb(1, 1, 1), Some(&frame))
.expect("frame");
assert_eq!(
adaptive_large_damage_run(adaptive_id) as usize,
expected,
"each over-threshold frame must advance the run"
);
}
unregister(adaptive_id);
}
#[test]
fn adaptive_recovers_when_the_damage_shrinks() {
let size = Size::new(200, 200);
let id = register(sample_editor("fn main() {}")).expect("registry");
assert!(set_repaint_mode(id, RepaintMode::Adaptive));
let mut frame = render_frame(id, size, Color::rgb(1, 1, 1)).expect("first frame");
for _ in 0..ADAPTIVE_LARGE_DAMAGE_RUN {
assert!(mark_dirty_rect(id, Rect::new(0, 0, 200, 200)));
frame = render_frame_incremental(id, size, Color::rgb(1, 1, 1), Some(&frame))
.expect("frame");
}
assert_eq!(adaptive_large_damage_run(id), ADAPTIVE_LARGE_DAMAGE_RUN);
assert!(mark_dirty_rect(id, Rect::new(0, 0, 4, 4)));
let _settled =
render_frame_incremental(id, size, Color::rgb(1, 1, 1), Some(&frame)).expect("frame");
assert_eq!(
adaptive_large_damage_run(id),
0,
"a small-damage frame must clear the run, or the mode would latch"
);
unregister(id);
}
#[test]
fn a_wrongly_sized_previous_frame_is_ignored() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let size = Size::new(64, 48);
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, Rect::new(0, 0, 8, 8)));
let wrong = vec![0u8; 7];
let frame = render_frame_incremental(id, size, Color::rgb(1, 2, 3), Some(&wrong))
.expect("a frame is still produced");
assert_eq!(
frame.len(),
size.width as usize * size.height as usize * 4,
"a mismatched previous frame must not resize the output"
);
unregister(id);
}
#[test]
fn a_dirty_repaint_preserves_pixels_outside_the_damage() {
let id = register(sample_editor("fn main() {}")).expect("registry");
let size = Size::new(80, 60);
let clear = Color::rgb(200, 30, 40);
let first = render_frame(id, size, clear).expect("first frame");
assert!(set_repaint_mode(id, RepaintMode::Dirty));
assert!(mark_dirty_rect(id, Rect::new(0, 0, 8, 8)));
let second = render_frame_incremental(id, size, Color::rgb(0, 0, 0), Some(&first))
.expect("second frame");
let last_pixel = second.len() - 4;
assert_eq!(
&second[last_pixel..],
&first[last_pixel..],
"the far corner is outside the damage and must keep its pixels"
);
unregister(id);
}
#[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);
}
fn modal_fixture() -> (ObjectId, ObjectId, ObjectId) {
let window = register(Box::new(crate::widget::container_widgets::groupbox::GroupBox::new(
Rect::new(0, 0, 400, 400),
)))
.expect("window");
let modal = register(Box::new(crate::widget::dialog::message_box::MessageBox::new(
Rect::new(50, 50, 200, 100),
)))
.expect("modal");
let outside = register(Box::new(crate::widget::base_widgets::label::Label::new(
"outside".to_string(),
Rect::new(0, 0, 100, 20),
)))
.expect("outside");
with_widget_mut(modal, |w| w.set_parent(Some(window)));
with_widget_mut(outside, |w| w.set_parent(Some(window)));
with_widget_mut(window, |w| {
w.add_child(modal);
w.add_child(outside);
});
(window, modal, outside)
}
#[test]
fn no_modal_blocks_nothing() {
let (window, modal, outside) = modal_fixture();
assert!(!is_modal_active());
assert!(!modal_blocks(window));
assert!(!modal_blocks(modal));
assert!(!modal_blocks(outside));
unregister(window);
unregister(modal);
unregister(outside);
}
#[test]
fn an_active_modal_blocks_input_outside_its_subtree() {
let (window, modal, outside) = modal_fixture();
assert!(enter_modal(modal));
assert!(is_modal_active());
assert_eq!(active_modal(), Some(modal));
assert!(!modal_blocks(modal));
assert!(modal_blocks(window));
assert!(modal_blocks(outside));
let event = crate::event::Event::mouse_press(10, 10, 1);
assert!(!dispatch_event(outside, &event), "a blocked widget must not receive events");
assert!(dispatch_event(modal, &event), "the modal dialog must receive events");
exit_modal(modal);
assert!(!is_modal_active());
assert!(!modal_blocks(outside), "after dismissal nothing is blocked");
unregister(window);
unregister(modal);
unregister(outside);
}
#[test]
fn a_modal_blocks_focus_outside_its_subtree() {
let (window, modal, outside) = modal_fixture();
clear_focus();
enter_modal(modal);
assert!(focus_widget(modal));
assert!(!focus_widget(outside));
assert_eq!(focused_widget(), Some(modal));
exit_modal(modal);
assert!(focus_widget(outside));
assert_eq!(focused_widget(), Some(outside));
clear_focus();
unregister(window);
unregister(modal);
unregister(outside);
}
#[test]
fn an_unmounted_modal_cannot_be_entered() {
clear_modals();
assert!(!enter_modal(0xDEAD_BEEF));
assert!(!is_modal_active());
}
#[test]
fn clear_modals_drops_the_whole_stack() {
let (_, modal, _) = modal_fixture();
assert!(enter_modal(modal));
assert!(is_modal_active());
clear_modals();
assert!(!is_modal_active());
unregister(modal);
}
#[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
}
}
}