#![doc = include_str!("../README.md")]
pub mod accessibility;
pub mod animation;
pub mod colors;
pub mod command;
pub mod debugger;
pub mod effects;
pub mod image;
pub mod layer;
pub use mtk_layout as layout;
pub(crate) mod node;
pub mod render;
pub mod style;
pub mod text;
pub mod ui;
pub mod windowing;
use crate::ui::morph::{MorphId, MorphInfo, MorphPair, MorphTransition};
use ::winit::keyboard::ModifiersState;
use ::winit::window::Window;
pub use mtk_macro::Lens;
pub use crate::accessibility::{AccessibleInfo, AccessibleView, AccessibleViewExt};
pub use crate::colors::Color;
pub use crate::command::{Command, IntoCommand};
pub use crate::debugger::{LayoutSnapshot, NodeDebugInfo, SourceLocation};
pub use crate::effects::{Border, BoxShadow, Effects, Radius};
pub use crate::image::{ImageCache, ImageData, ObjectFit, SvgData, SvgStyle};
pub use crate::layer::*;
pub use crate::layout::{LayoutEngine, NodeId};
pub use crate::node::Node;
pub use crate::render::RenderCommand;
pub use crate::style::*;
pub use crate::text::*;
pub use crate::ui::KineticTracker;
pub use crate::ui::widgets::canvas::{
CanvasData, CanvasEventDetails, CanvasPainterKind, PaintContext, PixelPainter, WgpuPainter,
};
pub use crate::ui::widgets::{
CheckboxStyle, RadioGroup, RadioStyle, ScrollAxis, ScrollOffset, SwitchStyle, TooltipAnimation,
TooltipPlacement,
};
pub use crate::ui::{
BoxedElement, BoxedView, BoxedViewExt, DragContext, DragPhase, Either, Focusable, FocusableExt,
KeyEvent, KeyEventContext, Keyed, KeyedViewSequence, Motion, MotionOffset, PageTransition,
Presence, PresenceViewExt, Router, ScrollContext, ScrollHandler, ScrollSource,
ThumbScrollContext, ThumbScrollHandler, Transition, TransitionOrder, ViewEitherExt,
ViewEventExt, ViewStyleExt, boxed, either, keyed, keyed_sequence, presence, router,
};
pub use accesskit::{Action, ActionData, ActionRequest, Role, TreeUpdate};
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClipboardData {
Text(String),
}
pub struct Context {
pub layout: LayoutEngine,
pub text_sizing_func: Option<
Box<
dyn Fn(
&mut Context,
Node,
&str,
Option<&dyn std::any::Any>,
f32,
f32,
) -> TextComputedOutput,
>,
>,
pub effects: HashMap<Node, Effects>,
pub dirty_effects: HashSet<Node>,
pub accessibility_nodes: HashMap<Node, AccessibleInfo>,
pub dirty_accessibility: HashSet<Node>,
pub scrollbars: HashMap<Node, ScrollbarStyle>,
pub text_context: SharedTextContext,
pub focused_node: Option<Node>,
pub focusable_nodes: Vec<Node>,
pub window: Option<Arc<dyn Window>>,
pub modifiers: ModifiersState,
pub ensure_visible_requests: HashMap<Node, crate::style::Rect>,
pub clipboard: Arc<Mutex<Option<arboard::Clipboard>>>,
pub canvases: RefCell<HashMap<Node, CanvasData>>,
pub images: RefCell<HashMap<Node, (ImageData, ObjectFit)>>,
pub svgs: RefCell<HashMap<Node, (SvgData, ObjectFit)>>,
pub dt: f32,
pub node_sources: HashMap<Node, SourceLocation>,
pub highlight_node: Option<Node>,
pub scale_factor: f32,
pub captured_pointer: Option<PointerCapture>,
pub morph_registry: HashMap<Node, MorphInfo>,
pub morph_nodes_by_id: HashMap<MorphId, Vec<Node>>,
pub morph_suppressed_nodes: HashSet<Node>,
pub base_layer: InternalLayer,
pub intermediate_layers: Vec<UserLayer>,
pub overlay_layer: InternalLayer,
pub modal_layer: InternalLayer,
pub active_layer: ActiveLayerId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CursorGrabPolicy {
#[default]
Normal,
Locked,
}
#[derive(Clone, Debug)]
pub struct PointerCapture {
pub node: Node,
pub policy: CursorGrabPolicy,
pub initial_pos: (f32, f32),
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
pub fn new() -> Self {
Self {
layout: LayoutEngine::new(),
text_sizing_func: None,
effects: HashMap::new(),
dirty_effects: HashSet::new(),
accessibility_nodes: HashMap::new(),
dirty_accessibility: HashSet::new(),
scrollbars: HashMap::new(),
text_context: Arc::new(Mutex::new(TextContext::new())),
focused_node: None,
focusable_nodes: Vec::new(),
window: None,
modifiers: ModifiersState::default(),
ensure_visible_requests: HashMap::new(),
clipboard: Arc::new(Mutex::new(None)),
canvases: RefCell::new(HashMap::new()),
images: RefCell::new(HashMap::new()),
svgs: RefCell::new(HashMap::new()),
dt: 0.016,
node_sources: HashMap::new(),
highlight_node: None,
scale_factor: 1.0,
captured_pointer: None,
morph_registry: HashMap::new(),
morph_nodes_by_id: HashMap::new(),
morph_suppressed_nodes: HashSet::new(),
base_layer: InternalLayer::new(true),
intermediate_layers: Vec::new(),
overlay_layer: InternalLayer::new(false),
modal_layer: InternalLayer::new(false),
active_layer: ActiveLayerId::Base,
}
}
pub fn capture_pointer(&mut self, node: Node, policy: CursorGrabPolicy) {
if policy == CursorGrabPolicy::Locked {
if let Some(window) = &self.window {
let _ = window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
.or_else(|_| window.set_cursor_grab(winit::window::CursorGrabMode::Confined));
window.set_cursor_visible(false);
}
}
self.captured_pointer = Some(PointerCapture {
node,
policy,
initial_pos: (0.0, 0.0),
});
}
pub fn release_pointer(&mut self) {
if let Some(capture) = self.captured_pointer.take() {
if capture.policy == CursorGrabPolicy::Locked {
if let Some(window) = &self.window {
let _ = window.set_cursor_grab(winit::window::CursorGrabMode::None);
window.set_cursor_visible(true);
}
}
}
}
pub fn is_pointer_captured(&self, node: Node) -> bool {
self.captured_pointer
.as_ref()
.is_some_and(|c| c.node == node)
}
pub fn has_pointer_capture(&self) -> bool {
self.captured_pointer.is_some()
}
pub fn captured_node(&self) -> Option<Node> {
self.captured_pointer.as_ref().map(|c| c.node)
}
pub fn layout(&self) -> &LayoutEngine {
&self.layout
}
pub fn layout_mut(&mut self) -> &mut LayoutEngine {
&mut self.layout
}
pub fn text_context(&self) -> SharedTextContext {
self.text_context.clone()
}
pub fn active_layer(&self) -> ActiveLayerId {
if self.modal_layer.state.visible {
ActiveLayerId::Modal
} else if let Some(last_inter) = self
.intermediate_layers
.iter()
.rfind(|l| l.state.visible && l.blocking)
{
ActiveLayerId::Intermediate(last_inter.id)
} else if self.overlay_layer.state.visible {
ActiveLayerId::Overlay
} else {
ActiveLayerId::Base
}
}
pub fn set_active_layer(&mut self, layer: ActiveLayerId) {
self.active_layer = layer;
}
pub fn clear_layer_focus(&mut self, layer: ActiveLayerId) {
let root = match layer {
ActiveLayerId::Base => self.base_layer.state.root_node,
ActiveLayerId::Modal => self.modal_layer.state.root_node,
ActiveLayerId::Overlay => self.overlay_layer.state.root_node,
ActiveLayerId::Intermediate(id) => self
.intermediate_layers
.iter()
.find(|l| l.id == id)
.and_then(|l| l.state.root_node),
};
if let (Some(root_node), Some(focused)) = (root, self.focused_node) {
if focused.is_descendant_of(self, root_node) {
self.clear_focus();
}
}
}
pub fn dt(&self) -> f32 {
self.dt
}
pub fn request_focus(&mut self, node: Node) {
self.focused_node = Some(node);
self.dirty_accessibility.insert(node);
self.request_frame();
}
pub fn clear_focus(&mut self) {
if let Some(focused) = self.focused_node {
self.dirty_accessibility.insert(focused);
}
self.focused_node = None;
self.request_frame();
}
pub fn blur(&mut self) {
self.clear_focus();
}
pub fn check_click_focus_blur(&mut self, hit_nodes: &[Node]) -> Option<Node> {
if let Some(focused) = self.focused_node {
let clicked_same = hit_nodes.iter().any(|n| n.is_descendant_of(self, focused));
if !clicked_same {
self.clear_focus();
return Some(focused);
}
}
None
}
pub fn request_ensure_visible(&mut self, node: Node, rect: crate::style::Rect) {
self.ensure_visible_requests.insert(node, rect);
self.request_frame();
}
pub fn register_focusable(&mut self, node: Node) {
if !self.focusable_nodes.contains(&node) {
self.focusable_nodes.push(node);
}
}
pub fn unregister_focusable(&mut self, node: Node) {
self.focusable_nodes.retain(|n| n != &node);
if self.focused_node == Some(node) {
self.clear_focus();
}
}
pub fn active_focusable_nodes(&self) -> Vec<Node> {
let active = self.active_layer();
match active {
ActiveLayerId::Modal => {
if let Some(modal_root) = self.modal_layer.state.root_node {
self.focusable_nodes
.iter()
.copied()
.filter(|n| n.is_descendant_of(self, modal_root))
.collect()
} else {
Vec::new()
}
}
ActiveLayerId::Intermediate(id) => {
if let Some(inter_root) = self
.intermediate_layers
.iter()
.find(|l| l.id == id)
.and_then(|l| l.state.root_node)
{
self.focusable_nodes
.iter()
.copied()
.filter(|n| n.is_descendant_of(self, inter_root))
.collect()
} else {
Vec::new()
}
}
ActiveLayerId::Overlay => {
if let Some(overlay_root) = self.overlay_layer.state.root_node {
self.focusable_nodes
.iter()
.copied()
.filter(|n| n.is_descendant_of(self, overlay_root))
.collect()
} else {
Vec::new()
}
}
ActiveLayerId::Base => {
let mut layer_roots = Vec::new();
if let Some(r) = self.modal_layer.state.root_node {
layer_roots.push(r);
}
for l in &self.intermediate_layers {
if let Some(r) = l.state.root_node {
layer_roots.push(r);
}
}
if let Some(r) = self.overlay_layer.state.root_node {
layer_roots.push(r);
}
self.focusable_nodes
.iter()
.copied()
.filter(|n| !layer_roots.iter().any(|r| n.is_descendant_of(self, *r)))
.collect()
}
}
}
pub fn focus_next(&mut self) {
let active_nodes = self.active_focusable_nodes();
if active_nodes.is_empty() {
self.clear_focus();
return;
}
if let Some(focused) = self.focused_node {
if let Some(idx) = active_nodes.iter().position(|n| *n == focused) {
let next_idx = (idx + 1) % active_nodes.len();
self.request_focus(active_nodes[next_idx]);
} else {
self.request_focus(active_nodes[0]);
}
} else {
self.request_focus(active_nodes[0]);
}
}
pub fn focus_prev(&mut self) {
let active_nodes = self.active_focusable_nodes();
if active_nodes.is_empty() {
self.clear_focus();
return;
}
if let Some(focused) = self.focused_node {
if let Some(idx) = active_nodes.iter().position(|n| *n == focused) {
let prev_idx = if idx == 0 {
active_nodes.len() - 1
} else {
idx - 1
};
self.request_focus(active_nodes[prev_idx]);
} else {
self.request_focus(active_nodes[active_nodes.len() - 1]);
}
} else {
self.request_focus(active_nodes[active_nodes.len() - 1]);
}
}
pub fn focused_node(&self) -> Option<Node> {
self.focused_node
}
pub fn modifiers(&self) -> winit::keyboard::ModifiersState {
self.modifiers
}
pub fn clipboard_copy(&self, data: ClipboardData) {
if let Ok(mut guard) = self.clipboard.lock() {
if guard.is_none() {
*guard = arboard::Clipboard::new().ok();
}
if let Some(cb) = guard.as_mut() {
match data {
ClipboardData::Text(text) => {
let _ = cb.set_text(text);
}
}
} else if let Ok(mut new_cb) = arboard::Clipboard::new() {
match data {
ClipboardData::Text(text) => {
let _ = new_cb.set_text(text);
}
}
*guard = Some(new_cb);
}
}
}
pub fn clipboard_get(&self) -> Option<ClipboardData> {
if let Ok(mut guard) = self.clipboard.lock() {
if guard.is_none() {
*guard = arboard::Clipboard::new().ok();
}
if let Some(cb) = guard.as_mut() {
if let Ok(text) = cb.get_text() {
return Some(ClipboardData::Text(text));
}
}
if let Ok(mut new_cb) = arboard::Clipboard::new() {
if let Ok(text) = new_cb.get_text() {
let result = Some(ClipboardData::Text(text));
*guard = Some(new_cb);
return result;
}
*guard = Some(new_cb);
}
}
None
}
pub fn create_node(&mut self) -> Node {
Node(self.layout.create_node())
}
pub fn window(&self) -> Option<Arc<dyn winit::window::Window>> {
self.window.clone()
}
pub fn request_frame(&self) {
if let Some(window) = self.window.as_ref() {
window.request_redraw();
}
}
pub fn destroy_node(&mut self, node: Node) {
let accessibility_nodes = &mut self.accessibility_nodes;
let dirty_accessibility = &mut self.dirty_accessibility;
let effects = &mut self.effects;
let dirty_effects = &mut self.dirty_effects;
let canvases = &self.canvases;
let images = &self.images;
let svgs = &self.svgs;
let scrollbars = &mut self.scrollbars;
let ensure_visible_requests = &mut self.ensure_visible_requests;
let node_sources = &mut self.node_sources;
let focused_node = &mut self.focused_node;
let focusable_nodes = &mut self.focusable_nodes;
let highlight_node = &mut self.highlight_node;
let captured_pointer = &mut self.captured_pointer;
let morph_registry = &mut self.morph_registry;
let morph_nodes_by_id = &mut self.morph_nodes_by_id;
let morph_suppressed_nodes = &mut self.morph_suppressed_nodes;
self.layout.destroy_node_with(node.0, |destroyed_id| {
let n = Node(destroyed_id);
accessibility_nodes.remove(&n);
dirty_accessibility.remove(&n);
effects.remove(&n);
dirty_effects.remove(&n);
canvases.borrow_mut().remove(&n);
images.borrow_mut().remove(&n);
svgs.borrow_mut().remove(&n);
scrollbars.remove(&n);
ensure_visible_requests.remove(&n);
node_sources.remove(&n);
morph_suppressed_nodes.remove(&n);
if let Some(info) = morph_registry.remove(&n) {
if let Some(list) = morph_nodes_by_id.get_mut(&info.id) {
list.retain(|x| *x != n);
if list.is_empty() {
morph_nodes_by_id.remove(&info.id);
}
}
}
if *focused_node == Some(n) {
*focused_node = None;
}
if *highlight_node == Some(n) {
*highlight_node = None;
}
if captured_pointer.as_ref().map_or(false, |c| c.node == n) {
*captured_pointer = None;
}
focusable_nodes.retain(|&x| x != n);
});
}
pub fn set_accessible(&mut self, node: Node, info: AccessibleInfo) {
self.dirty_accessibility.insert(node);
self.accessibility_nodes.insert(node, info);
}
pub fn get_accessible(&self, node: Node) -> Option<&AccessibleInfo> {
self.accessibility_nodes.get(&node)
}
pub fn remove_accessible(&mut self, node: Node) -> Option<AccessibleInfo> {
self.dirty_accessibility.insert(node);
self.accessibility_nodes.remove(&node)
}
pub fn build_accesskit_tree_update(&mut self, is_full: bool) -> accesskit::TreeUpdate {
crate::accessibility::build_tree_update(self, is_full)
}
pub fn root_attach(&mut self, node: Node) {
self.base_layer.state.root_node = Some(node);
self.layout.root_attach(node.0);
}
pub fn root_drop(&mut self) {
self.base_layer.state.root_node = None;
self.layout.root_drop();
}
pub fn compute_layout(&mut self, viewport_width: f32, viewport_height: f32) {
let mut layout = std::mem::take(&mut self.layout);
let mut sizing_func = self.text_sizing_func.take();
let text_context = self.text_context.clone();
layout.compute_layout(
viewport_width,
viewport_height,
|node_id, text, userdata, avail_w, avail_h| {
if let Some(func) = sizing_func.as_mut() {
let out = func(self, Node(node_id), text, userdata, avail_w, avail_h);
crate::layout::TextMetrics {
width: out.computed_width,
height: out.computed_height,
baseline_offset: out.baseline_offset,
}
} else {
let default_style = TextStyle::default();
let (style, spans) = if let Some(info) =
userdata.and_then(|u| u.downcast_ref::<crate::TextRenderInfo>())
{
(&info.style, &info.spans[..])
} else if let Some(style) = userdata.and_then(|u| u.downcast_ref::<TextStyle>())
{
(style, &[][..])
} else {
(&default_style, &[][..])
};
let out = crate::text::measure_text(
text,
style,
avail_w,
avail_h,
&text_context,
spans,
);
crate::layout::TextMetrics {
width: out.computed_width,
height: out.computed_height,
baseline_offset: out.baseline_offset,
}
}
},
);
self.layout = layout;
self.text_sizing_func = sizing_func;
self.clamp_scroll_offsets(viewport_width, viewport_height);
}
fn clamp_scroll_offsets(&mut self, viewport_width: f32, viewport_height: f32) {
if self.layout.layout_order.is_empty() {
return;
}
let nodes: Vec<Node> = self.layout.layout_order.iter().map(|&n| Node(n)).collect();
let mut any_clamped = false;
for node in nodes {
if !node.is_valid() {
continue;
}
if let Some(constraints) = node.get_constraints(self) {
let is_scrollable_y = constraints.overflow == crate::Overflow::Scroll
|| constraints.overflow == crate::Overflow::Auto;
let is_scrollable_x = constraints.overflow == crate::Overflow::Scroll
|| constraints.overflow == crate::Overflow::Auto
|| constraints.overflow == crate::Overflow::Hidden;
if is_scrollable_y || is_scrollable_x {
if let Some(computed) = node.get_computed(self) {
let content_h = node.compute_content_height(self);
let max_scroll_y = (content_h - computed.h).max(0.0);
let content_w = computed.content_w.max(computed.w);
let max_scroll_x = (content_w - computed.w).max(0.0);
let mut new_y = constraints.scroll.y;
let mut new_x = constraints.scroll.x;
if is_scrollable_y && new_y > max_scroll_y {
new_y = max_scroll_y;
}
if is_scrollable_x && new_x > max_scroll_x {
new_x = max_scroll_x;
}
if (new_y - constraints.scroll.y).abs() > 0.001
|| (new_x - constraints.scroll.x).abs() > 0.001
{
node.update_constraints(self, |c| {
c.scroll.y = new_y;
c.scroll.x = new_x;
});
any_clamped = true;
}
}
}
}
}
if any_clamped {
let mut layout = std::mem::take(&mut self.layout);
let mut sizing_func = self.text_sizing_func.take();
let text_context = self.text_context.clone();
layout.compute_layout(
viewport_width,
viewport_height,
|node_id, text, userdata, avail_w, avail_h| {
if let Some(func) = sizing_func.as_mut() {
let out = func(self, Node(node_id), text, userdata, avail_w, avail_h);
crate::layout::TextMetrics {
width: out.computed_width,
height: out.computed_height,
baseline_offset: out.baseline_offset,
}
} else {
let default_style = TextStyle::default();
let (style, spans) = if let Some(info) =
userdata.and_then(|u| u.downcast_ref::<crate::TextRenderInfo>())
{
(&info.style, &info.spans[..])
} else if let Some(style) =
userdata.and_then(|u| u.downcast_ref::<TextStyle>())
{
(style, &[][..])
} else {
(&default_style, &[][..])
};
let out = crate::text::measure_text(
text,
style,
avail_w,
avail_h,
&text_context,
spans,
);
crate::layout::TextMetrics {
width: out.computed_width,
height: out.computed_height,
baseline_offset: out.baseline_offset,
}
}
},
);
self.layout = layout;
self.text_sizing_func = sizing_func;
}
}
pub fn build_render_list(&mut self, viewport: Rect) {
self.layout.build_render_list(viewport);
}
pub fn render_list(&self) -> impl Iterator<Item = RenderCommand<'_>> {
self.layout
.render_list
.iter()
.map(|cmd| RenderCommand { cmd })
}
pub fn set_text_sizing_func<F>(&mut self, func: F)
where
F: Fn(&mut Context, Node, &str, Option<&dyn std::any::Any>, f32, f32) -> TextComputedOutput
+ 'static,
{
self.text_sizing_func = Some(Box::new(func));
}
pub fn register_fonts(&mut self, font_data: Vec<u8>) {
self.text_context.lock().unwrap().register_fonts(font_data);
}
pub fn register_font_file(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
self.text_context.lock().unwrap().register_font_file(path)
}
pub fn pick(&mut self, x: f32, y: f32) -> Vec<Node> {
self.layout.pick(x, y).iter().map(|&n| Node(n)).collect()
}
pub fn set_node_source(&mut self, node: Node, loc: SourceLocation) {
self.node_sources.insert(node, loc);
}
pub fn get_node_source(&self, node: Node) -> Option<SourceLocation> {
self.node_sources.get(&node).copied()
}
pub fn register_morph_node(&mut self, node: Node, id: MorphId, transition: MorphTransition) {
let info = MorphInfo {
node,
id: id.clone(),
transition,
};
self.morph_registry.insert(node, info);
self.morph_nodes_by_id.entry(id).or_default().push(node);
}
pub fn unregister_morph_node(&mut self, node: Node) {
if let Some(info) = self.morph_registry.remove(&node) {
if let Some(list) = self.morph_nodes_by_id.get_mut(&info.id) {
list.retain(|n| *n != node);
if list.is_empty() {
self.morph_nodes_by_id.remove(&info.id);
}
}
}
}
pub fn find_morph_pairs(&self, out_root: Node, cur_root: Node) -> Vec<MorphPair> {
let mut pairs = Vec::new();
for (id, nodes) in &self.morph_nodes_by_id {
let mut source = None;
let mut target = None;
for &node in nodes {
if node.is_descendant_of(self, out_root) || node == out_root {
if let Some(info) = self.morph_registry.get(&node) {
source = Some(info.clone());
}
} else if node.is_descendant_of(self, cur_root) || node == cur_root {
if let Some(info) = self.morph_registry.get(&node) {
target = Some(info.clone());
}
}
}
if let (Some(source), Some(target)) = (source, target) {
pairs.push(MorphPair {
id: id.clone(),
source,
target,
});
}
}
pairs
}
pub fn root_node(&self) -> Option<Node> {
self.layout
.root
.map(Node)
.or(self.base_layer.state.root_node)
}
pub fn count_nodes(&self) -> usize {
let mut count = 0;
if let Some(root) = self.root_node() {
count = 1 + self.count_children_recursive(root);
}
count
}
fn count_children_recursive(&self, node: Node) -> usize {
let children = node.children(self);
let mut count = children.len();
for child in children {
count += self.count_children_recursive(child);
}
count
}
pub fn build_debug_snapshot(
&self,
viewport_w: f32,
viewport_h: f32,
hovered_node: Option<Node>,
) -> LayoutSnapshot {
let root = self.root_node().and_then(|r| self.build_node_debug_info(r));
let total_nodes = self.count_nodes();
LayoutSnapshot {
root,
total_nodes,
viewport_w,
viewport_h,
hovered_node: hovered_node.map(|n| n.id()),
}
}
fn build_node_debug_info(&self, node: Node) -> Option<NodeDebugInfo> {
let computed = node.get_computed(self)?;
let constraints = node.get_constraints(self);
let source = self.get_node_source(node);
let text = node.get_text(self).map(|s| s.to_string());
let name = if let Some(src) = source {
src.type_name.to_string()
} else if text.is_some() {
"Text".to_string()
} else {
format!("Node(#{})", node.id())
};
let metrics = if let Some(cons) = constraints {
crate::debugger::NodeBoxMetrics {
x: computed.x,
y: computed.y,
w: computed.w,
h: computed.h,
content_w: computed.content_w,
content_h: computed.content_h,
pad_top: cons.padding.top,
pad_bottom: cons.padding.bottom,
pad_left: cons.padding.left,
pad_right: cons.padding.right,
border_top: cons.border.top,
border_bottom: cons.border.bottom,
border_left: cons.border.left,
border_right: cons.border.right,
flex_direction: format!("{:?}", cons.flex_direction),
flex_grow: cons.flex_grow,
flex_shrink: cons.flex_shrink,
}
} else {
crate::debugger::NodeBoxMetrics {
x: computed.x,
y: computed.y,
w: computed.w,
h: computed.h,
content_w: computed.content_w,
content_h: computed.content_h,
..Default::default()
}
};
let children = node
.children(self)
.into_iter()
.filter_map(|c| self.build_node_debug_info(c))
.collect();
Some(NodeDebugInfo {
id: node.id(),
name,
source,
metrics,
text,
children,
})
}
}
pub mod text_property {
pub use parley::layout::Alignment;
pub use parley::style::*;
}
pub mod winit {
pub use winit::*;
}
pub mod wgpu {
pub use wgpu::*;
}
pub mod accesskit {
pub use accesskit::*;
}
pub mod bytemuck {
pub use bytemuck::*;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effects::Effects;
use crate::image::{ImageData, ObjectFit};
use crate::style::ScrollbarStyle;
#[test]
fn test_destroy_node_recursive_cleanup() {
let mut ctx = Context::new();
let parent = ctx.create_node();
let child1 = ctx.create_node();
let child2 = ctx.create_node();
let grand_child = ctx.create_node();
parent.append(&mut ctx, child1);
parent.append(&mut ctx, child2);
child1.append(&mut ctx, grand_child);
child1.set_effects(&mut ctx, Effects::default());
grand_child.set_scrollbar_style(&mut ctx, ScrollbarStyle::default());
ctx.set_accessible(child2, AccessibleInfo::new(accesskit::Role::Button));
ctx.images.borrow_mut().insert(
grand_child,
(
ImageData {
id: 42,
width: 100,
height: 100,
pixels: std::sync::Arc::from([]),
},
ObjectFit::Cover,
),
);
ctx.focused_node = Some(child2);
assert_eq!(ctx.focused_node(), Some(child2));
assert!(ctx.effects.contains_key(&child1));
assert!(ctx.scrollbars.contains_key(&grand_child));
assert!(ctx.accessibility_nodes.contains_key(&child2));
assert!(ctx.images.borrow().contains_key(&grand_child));
ctx.destroy_node(parent);
assert!(!ctx.effects.contains_key(&child1));
assert!(!ctx.scrollbars.contains_key(&grand_child));
assert!(!ctx.accessibility_nodes.contains_key(&child2));
assert!(!ctx.images.borrow().contains_key(&grand_child));
assert_eq!(ctx.focused_node(), None);
assert!(!ctx.layout.is_valid(parent.0));
assert!(!ctx.layout.is_valid(child1.0));
assert!(!ctx.layout.is_valid(child2.0));
assert!(!ctx.layout.is_valid(grand_child.0));
}
}