use std::sync::Arc;
use lieui_geom::{Color, Rect, Size};
use lieui_layout::{ComputedLayout, FlexStyle};
use lieui_text::TextEngine;
use crate::event::{HandlerSlot, PointerId};
use crate::style::{ImageStyle, PaintStyle, TextStyle};
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct NodeId(u64);
impl NodeId {
pub const NULL: NodeId = NodeId(u64::MAX);
pub const fn new(index: u32, generation: u32) -> Self {
Self(((generation as u64) << 32) | index as u64)
}
pub const fn null() -> Self {
Self::NULL
}
pub const fn index(self) -> u32 {
self.0 as u32
}
pub const fn generation(self) -> u32 {
(self.0 >> 32) as u32
}
pub const fn to_u64(self) -> u64 {
self.0
}
pub const fn is_null(self) -> bool {
self.0 == u64::MAX
}
}
impl Default for NodeId {
fn default() -> Self {
Self::NULL
}
}
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct Flags(u16);
impl Flags {
pub const EMPTY: Self = Self(0);
pub const MEASURE_DIRTY: Self = Self(1);
pub const ARRANGE_DIRTY: Self = Self(1 << 1);
pub const PAINT_DIRTY: Self = Self(1 << 2);
pub const ATTACHED: Self = Self(1 << 3);
pub const fn empty() -> Self {
Self(0)
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
}
impl std::ops::BitOr for Flags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for Flags {
fn bitor_assign(&mut self, rhs: Self) {
self.insert(rhs);
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum Visibility {
#[default]
Visible,
Hidden,
Collapsed,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum FocusState {
#[default]
Unfocused,
Pointer,
Keyboard,
Programmatic,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct InteractionState {
pub pointer_over: bool,
pub pressed: bool,
pub focused: bool,
pub enabled: bool,
}
impl Default for InteractionState {
fn default() -> Self {
Self {
pointer_over: false,
pressed: false,
focused: false,
enabled: true,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Transform {
pub translate: (f32, f32),
pub scale: (f32, f32),
pub rotation_deg: f32,
pub origin: (f32, f32),
}
impl Default for Transform {
fn default() -> Self {
Self {
translate: (0.0, 0.0),
scale: (1.0, 1.0),
rotation_deg: 0.0,
origin: (0.5, 0.5),
}
}
}
impl Transform {
pub fn is_identity(&self) -> bool {
self.translate == (0.0, 0.0)
&& self.scale == (1.0, 1.0)
&& self.rotation_deg == 0.0
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ImageData {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum Axis {
X,
#[default]
Y,
Both,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum KindTag {
Box,
Text,
Image,
Button,
Checkbox,
Slider,
Progress,
Input,
Switch,
Radio,
Custom,
}
#[derive(Default, Clone)]
pub struct Bindings {
pub checked: Option<crate::reactive::Signal<bool>>,
pub value: Option<crate::reactive::Signal<f32>>,
pub text: Option<crate::reactive::Signal<String>>,
}
impl Bindings {
pub fn any(&self) -> bool {
self.checked.is_some() || self.value.is_some() || self.text.is_some()
}
}
impl std::fmt::Debug for Bindings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bindings")
.field("checked", &self.checked.is_some())
.field("value", &self.value.is_some())
.field("text", &self.text.is_some())
.finish()
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum KindDesc {
Box,
Text(String),
Image(Arc<ImageData>),
Button { label: String },
Checkbox { checked: bool },
Slider { value: f32, min: f32, max: f32 },
Progress { value: f32 },
Input { text: String, placeholder: String },
Switch { on: bool },
Radio { selected: bool, value: String },
Custom(crate::custom::CustomCell),
}
impl KindDesc {
pub fn tag(&self) -> KindTag {
match self {
KindDesc::Box => KindTag::Box,
KindDesc::Text(_) => KindTag::Text,
KindDesc::Image(_) => KindTag::Image,
KindDesc::Button { .. } => KindTag::Button,
KindDesc::Checkbox { .. } => KindTag::Checkbox,
KindDesc::Slider { .. } => KindTag::Slider,
KindDesc::Progress { .. } => KindTag::Progress,
KindDesc::Input { .. } => KindTag::Input,
KindDesc::Switch { .. } => KindTag::Switch,
KindDesc::Radio { .. } => KindTag::Radio,
KindDesc::Custom(_) => KindTag::Custom,
}
}
pub fn apply_to(&self, kind: &mut Kind) -> bool {
match (self, kind) {
(KindDesc::Box, Kind::Box) => false,
(KindDesc::Text(new), Kind::Text(old)) => {
if old == new {
false
} else {
old.clone_from(new);
true
}
}
(KindDesc::Image(new), Kind::Image(old)) => {
if Arc::ptr_eq(old, new) {
false
} else {
*old = Arc::clone(new);
true
}
}
(KindDesc::Button { label }, Kind::Button { label: old }) => {
if old == label {
false
} else {
old.clone_from(label);
true
}
}
(KindDesc::Checkbox { checked }, Kind::Checkbox { checked: old }) => {
let changed = *old != *checked;
*old = *checked;
changed
}
(
KindDesc::Slider { value, min, max },
Kind::Slider {
value: old,
min: old_min,
max: old_max,
..
},
) => {
let changed = old.to_bits() != value.to_bits()
|| old_min.to_bits() != min.to_bits()
|| old_max.to_bits() != max.to_bits();
*old = *value;
*old_min = *min;
*old_max = *max;
changed
}
(KindDesc::Progress { value }, Kind::Progress { value: old }) => {
let changed = old.to_bits() != value.to_bits();
*old = *value;
changed
}
(
KindDesc::Input { text, placeholder },
Kind::Input {
text: old,
placeholder: old_ph,
caret,
anchor,
preedit,
..
},
) => {
let mut changed = false;
if old != text && preedit.is_empty() {
old.clone_from(text);
*caret = old.len();
*anchor = *caret;
changed = true;
}
if old_ph != placeholder {
old_ph.clone_from(placeholder);
changed = true;
}
changed
}
(KindDesc::Custom(new), Kind::Custom(old)) => {
if old.ptr_eq(new) {
false
} else {
*old = new.clone();
true
}
}
(KindDesc::Switch { on }, Kind::Switch { on: old }) => {
let changed = *old != *on;
*old = *on;
changed
}
(
KindDesc::Radio { selected, value },
Kind::Radio {
selected: old_sel,
value: old_val,
},
) => {
let changed = *old_sel != *selected || old_val != value;
*old_sel = *selected;
old_val.clone_from(value);
changed
}
_ => true,
}
}
}
#[derive(Clone, Debug)]
pub enum Kind {
Box,
Text(String),
Image(Arc<ImageData>),
Button {
label: String,
},
Checkbox {
checked: bool,
},
Slider {
value: f32,
min: f32,
max: f32,
dragging: bool,
},
Progress {
value: f32,
},
Input {
text: String,
placeholder: String,
caret: usize,
anchor: usize,
preedit: String,
scroll: f32,
},
Switch { on: bool },
Radio { selected: bool, value: String },
Custom(crate::custom::CustomCell),
}
impl Kind {
pub fn tag(&self) -> KindTag {
match self {
Kind::Box => KindTag::Box,
Kind::Text(_) => KindTag::Text,
Kind::Image(_) => KindTag::Image,
Kind::Button { .. } => KindTag::Button,
Kind::Checkbox { .. } => KindTag::Checkbox,
Kind::Slider { .. } => KindTag::Slider,
Kind::Progress { .. } => KindTag::Progress,
Kind::Input { .. } => KindTag::Input,
Kind::Switch { .. } => KindTag::Switch,
Kind::Radio { .. } => KindTag::Radio,
Kind::Custom(_) => KindTag::Custom,
}
}
pub fn from_desc(desc: &KindDesc) -> Self {
match desc {
KindDesc::Box => Kind::Box,
KindDesc::Text(s) => Kind::Text(s.clone()),
KindDesc::Image(d) => Kind::Image(Arc::clone(d)),
KindDesc::Button { label } => Kind::Button {
label: label.clone(),
},
KindDesc::Checkbox { checked } => Kind::Checkbox { checked: *checked },
KindDesc::Slider { value, min, max } => Kind::Slider {
value: *value,
min: *min,
max: *max,
dragging: false,
},
KindDesc::Progress { value } => Kind::Progress { value: *value },
KindDesc::Input { text, placeholder } => Kind::Input {
text: text.clone(),
placeholder: placeholder.clone(),
caret: text.len(),
anchor: text.len(),
preedit: String::new(),
scroll: 0.0,
},
KindDesc::Custom(cell) => Kind::Custom(cell.clone()),
KindDesc::Switch { on } => Kind::Switch { on: *on },
KindDesc::Radio { selected, value } => Kind::Radio {
selected: *selected,
value: value.clone(),
},
}
}
}
pub fn prev_char_boundary(s: &str, i: usize) -> usize {
let i = i.min(s.len());
s[..i].char_indices().next_back().map(|(k, _)| k).unwrap_or(0)
}
pub fn next_char_boundary(s: &str, i: usize) -> usize {
let i = i.min(s.len());
s[i..]
.chars()
.next()
.map(|c| i + c.len_utf8())
.unwrap_or(s.len())
}
pub fn clamp_to_char_boundary(s: &str, i: usize) -> usize {
let mut i = i.min(s.len());
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Key {
Str(String),
I64(i64),
U64(u64),
}
impl From<&str> for Key {
fn from(v: &str) -> Self {
Key::Str(v.to_string())
}
}
impl From<String> for Key {
fn from(v: String) -> Self {
Key::Str(v)
}
}
impl From<i64> for Key {
fn from(v: i64) -> Self {
Key::I64(v)
}
}
impl From<u64> for Key {
fn from(v: u64) -> Self {
Key::U64(v)
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub enum Layer {
Content,
Overlay,
Popup,
Tooltip,
Modal,
DragPreview,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Placement {
Below,
Above,
RightOf,
LeftOf,
ScreenCenter,
Fixed { x: f32, y: f32 },
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum FocusPolicy {
Transparent,
Dismissable,
BlockBelow,
}
#[derive(Clone, PartialEq, Debug)]
pub enum AnchorTarget {
Key(Key),
Node(NodeId),
}
#[derive(Clone, PartialEq, Debug)]
pub struct Anchor {
pub target: AnchorTarget,
pub placement: Placement,
}
#[derive(Clone, PartialEq, Debug)]
pub struct LayerOpts {
pub backdrop: Option<Color>,
pub blocks_below: bool,
pub dismiss_on_outside_click: bool,
pub hit_test_visible: bool,
pub anchor: Option<Anchor>,
pub focus: FocusPolicy,
}
impl LayerOpts {
pub fn for_layer(layer: Layer) -> Self {
match layer {
Layer::Content => Self {
backdrop: None,
blocks_below: false,
dismiss_on_outside_click: false,
hit_test_visible: true,
anchor: None,
focus: FocusPolicy::Transparent,
},
Layer::Overlay => Self {
backdrop: None,
blocks_below: false,
dismiss_on_outside_click: false,
hit_test_visible: false,
anchor: None,
focus: FocusPolicy::Transparent,
},
Layer::Popup | Layer::Tooltip => Self {
backdrop: None,
blocks_below: false,
dismiss_on_outside_click: true,
hit_test_visible: true,
anchor: None,
focus: FocusPolicy::Dismissable,
},
Layer::Modal => Self {
backdrop: Some(Color::rgba(0, 0, 0, 80)),
blocks_below: true,
dismiss_on_outside_click: false,
hit_test_visible: true,
anchor: None,
focus: FocusPolicy::BlockBelow,
},
Layer::DragPreview => Self {
backdrop: None,
blocks_below: false,
dismiss_on_outside_click: false,
hit_test_visible: false,
anchor: None,
focus: FocusPolicy::Transparent,
},
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct RootId(pub u32);
#[derive(Clone, Debug)]
pub struct Root {
pub id: RootId,
pub node: NodeId,
pub layer: Layer,
pub owner: Option<RootId>,
pub opts: LayerOpts,
pub framework: bool,
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct ScrollDrag {
pub pointer: PointerId,
pub vertical: bool,
pub grab: f32,
}
pub struct Node {
pub parent: Option<NodeId>,
pub children: Vec<NodeId>,
pub kind: Kind,
pub key: Option<Key>,
pub tooltip: Option<String>,
pub text_wrap: Option<f32>,
pub layout: FlexStyle,
pub paint: PaintStyle,
pub text: TextStyle,
pub image: ImageStyle,
pub visibility: Visibility,
pub hit_test_visible: bool,
pub clip: Option<Rect>,
pub transform: Transform,
pub tab_stop: bool,
pub tab_index: i32,
pub layout_rounding: bool,
pub allow_drop: bool,
pub can_drag: bool,
pub interaction: InteractionState,
pub focus_state: FocusState,
pub scroll_offset: (f32, f32),
pub content_size: Size,
pub scroll_drag: Option<ScrollDrag>,
pub needs_scroll_event: bool,
pub flags: Flags,
pub desired: Size,
pub computed: ComputedLayout,
pub handlers: Vec<HandlerSlot>,
pub bindings: Bindings,
}
impl Node {
fn new(kind: Kind, key: Option<Key>) -> Self {
Self {
parent: None,
children: Vec::new(),
kind,
key,
tooltip: None,
text_wrap: None,
layout: FlexStyle::default(),
paint: PaintStyle::default(),
text: TextStyle::default(),
image: ImageStyle::default(),
visibility: Visibility::Visible,
hit_test_visible: true,
clip: None,
transform: Transform::default(),
tab_stop: false,
tab_index: 0,
layout_rounding: false,
allow_drop: false,
can_drag: false,
interaction: InteractionState::default(),
focus_state: FocusState::default(),
scroll_offset: (0.0, 0.0),
content_size: Size::zero(),
scroll_drag: None,
needs_scroll_event: false,
flags: Flags::MEASURE_DIRTY | Flags::ARRANGE_DIRTY | Flags::PAINT_DIRTY,
desired: Size::zero(),
computed: ComputedLayout::default(),
handlers: Vec::new(),
bindings: Bindings::default(),
}
}
pub fn rect(&self) -> Rect {
Rect::new(
self.computed.x,
self.computed.y,
self.computed.width,
self.computed.height,
)
}
pub fn paint_bounds(&self) -> Rect {
let r = self.rect();
if matches!(self.kind, Kind::Text(_)) {
let w = self.desired.width.min(r.width).max(0.0);
let h = self.desired.height.min(r.height).max(0.0);
if w > 0.0 && h > 0.0 && (w < r.width - 0.5 || h < r.height - 0.5) {
let x = match self.text.spec.text_align {
lieui_text::TextAlign::Center => r.x + (r.width - w) * 0.5,
lieui_text::TextAlign::End => r.right() - w,
_ => r.x,
};
return Rect::new(x, r.y, w, h);
}
}
r
}
}
#[derive(Default)]
pub struct Track {
nodes: Vec<Option<Node>>,
gens: Vec<u32>,
free: Vec<u32>,
roots: Vec<Root>,
next_root: u32,
pub hover: Option<NodeId>,
pub hover_path: Vec<NodeId>,
pub pressed: Option<NodeId>,
pub pressed_path: Vec<NodeId>,
pub focused: Option<NodeId>,
pub captures: Vec<(PointerId, NodeId)>,
pub blink_on: bool,
pub damage: Vec<Rect>,
pub damage_all: bool,
}
impl Track {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.nodes.iter().filter(|n| n.is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn contains(&self, id: NodeId) -> bool {
self.get(id).is_some()
}
pub fn get(&self, id: NodeId) -> Option<&Node> {
let i = id.index() as usize;
match self.nodes.get(i) {
Some(Some(n)) if self.gens[i] == id.generation() => Some(n),
_ => None,
}
}
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
let i = id.index() as usize;
match self.nodes.get_mut(i) {
Some(Some(n)) if self.gens[i] == id.generation() => Some(n),
_ => None,
}
}
pub fn create(&mut self, kind: Kind, key: Option<Key>) -> NodeId {
if let Kind::Custom(cell) = &kind
&& let Some(mut inst) = cell.try_borrow_mut()
{
inst.on_attached();
}
let node = Node::new(kind, key);
self.alloc(node)
}
fn alloc(&mut self, node: Node) -> NodeId {
if let Some(idx) = self.free.pop() {
let i = idx as usize;
self.nodes[i] = Some(node);
NodeId::new(idx, self.gens[i])
} else {
let idx = self.nodes.len() as u32;
self.nodes.push(Some(node));
self.gens.push(0);
NodeId::new(idx, 0)
}
}
fn free_slot(&mut self, id: NodeId) {
let i = id.index() as usize;
if i >= self.nodes.len() || self.nodes[i].is_none() {
return;
}
self.nodes[i] = None;
self.gens[i] = self.gens[i].wrapping_add(1);
self.free.push(id.index());
}
pub fn children(&self, id: NodeId) -> &[NodeId] {
self.get(id).map(|n| n.children.as_slice()).unwrap_or(&[])
}
pub fn parent_of(&self, id: NodeId) -> Option<NodeId> {
self.get(id).and_then(|n| n.parent)
}
pub fn ancestors(&self, id: NodeId) -> Ancestors<'_> {
Ancestors {
track: self,
cursor: self.parent_of(id),
}
}
pub fn descendants(&self, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
self.collect_subtree(id, &mut out);
out
}
fn collect_subtree(&self, id: NodeId, out: &mut Vec<NodeId>) {
out.push(id);
if let Some(n) = self.get(id) {
for c in &n.children {
self.collect_subtree(*c, out);
}
}
}
pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
self.insert_child(parent, usize::MAX, child);
}
pub fn insert_child(&mut self, parent: NodeId, pos: usize, child: NodeId) {
self.detach(child);
if let Some(c) = self.get_mut(child) {
c.parent = Some(parent);
c.flags.insert(Flags::ATTACHED);
}
let added = if let Some(p) = self.get_mut(parent) {
let pos = pos.min(p.children.len());
p.children.insert(pos, child);
true
} else {
false
};
debug_assert!(added, "insert_child: 父节点不存在");
self.mark_layout_dirty(parent);
}
pub fn detach(&mut self, child: NodeId) {
let Some(parent) = self.parent_of(child) else {
return;
};
if let Some(p) = self.get_mut(parent) {
p.children.retain(|c| *c != child);
}
if let Some(c) = self.get_mut(child) {
c.parent = None;
c.flags.remove(Flags::ATTACHED);
}
}
pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) {
if let Some(p) = self.get_mut(parent) {
debug_assert_eq!(p.children.len(), children.len(), "set_children 必须给全量顺序");
p.children.clear();
p.children.extend_from_slice(children);
}
self.mark_layout_dirty(parent);
}
pub fn destroy(&mut self, id: NodeId) -> usize {
let ids = self.descendants(id);
let parent = self.parent_of(id);
self.detach(id);
for i in &ids {
if let Some(n) = self.get(*i)
&& let Kind::Custom(cell) = &n.kind
&& let Some(mut inst) = cell.try_borrow_mut()
{
inst.on_detached();
}
}
if let Some(p) = parent {
self.mark_layout_dirty(p);
}
for rid in ids.iter().rev() {
self.free_slot(*rid);
}
self.roots.retain(|r| !ids.contains(&r.node));
ids.len()
}
pub fn roots(&self) -> &[Root] {
&self.roots
}
pub fn root(&self, id: RootId) -> Option<&Root> {
self.roots.iter().find(|r| r.id == id)
}
pub fn root_mut(&mut self, id: RootId) -> Option<&mut Root> {
self.roots.iter_mut().find(|r| r.id == id)
}
pub fn content_root(&self) -> Option<&Root> {
self.roots.iter().find(|r| r.layer == Layer::Content)
}
pub fn roots_of(&self, layer: Layer) -> impl Iterator<Item = &Root> {
self.roots.iter().filter(move |r| r.layer == layer)
}
pub fn add_root(&mut self, layer: Layer, owner: Option<RootId>, node: NodeId) -> RootId {
let id = RootId(self.next_root);
self.next_root += 1;
let opts = LayerOpts::for_layer(layer);
self.roots.push(Root {
id,
node,
layer,
owner,
opts,
framework: false,
});
if let Some(n) = self.get_mut(node) {
n.flags.insert(Flags::ATTACHED);
}
self.mark_layout_dirty(node);
self.damage_whole_window();
id
}
pub fn add_framework_root(&mut self, layer: Layer, owner: Option<RootId>, node: NodeId) -> RootId {
let rid = self.add_root(layer, owner, node);
if let Some(r) = self.root_mut(rid) {
r.framework = true;
}
rid
}
pub fn remove_root(&mut self, id: RootId) -> usize {
let Some(root) = self.root(id).cloned() else {
return 0;
};
let descendants: Vec<RootId> = self
.roots
.iter()
.filter(|r| r.owner == Some(id))
.map(|r| r.id)
.collect();
let mut removed = 1;
for d in descendants {
removed += self.remove_root(d);
}
self.roots.retain(|r| r.id != id);
self.destroy(root.node);
self.damage_whole_window();
removed
}
pub fn replace_child_at(&mut self, parent: NodeId, index: usize, new: NodeId) {
if let Some(p) = self.get_mut(parent)
&& index < p.children.len()
{
p.children[index] = new;
}
if let Some(n) = self.get_mut(new) {
n.parent = Some(parent);
n.flags.insert(Flags::ATTACHED);
}
self.mark_layout_dirty(parent);
}
pub fn state(&self, id: NodeId) -> InteractionState {
self.get(id).map(|n| n.interaction).unwrap_or_default()
}
pub fn set_pointer_over(&mut self, id: NodeId, v: bool) {
let mut repaint = false;
if let Some(n) = self.get_mut(id)
&& n.interaction.pointer_over != v
{
n.interaction.pointer_over = v;
repaint = n.paint.is_interactive() || n.text.is_interactive();
}
if repaint {
self.mark_paint_dirty(id);
}
}
pub fn set_pressed(&mut self, id: NodeId, v: bool) {
let mut repaint = false;
if let Some(n) = self.get_mut(id)
&& n.interaction.pressed != v
{
n.interaction.pressed = v;
repaint = n.paint.pressed_background.is_some() || n.text.pressed_color.is_some();
}
if repaint {
self.mark_paint_dirty(id);
}
}
pub fn set_focused(&mut self, id: NodeId, state: FocusState) {
if let Some(n) = self.get_mut(id)
&& n.focus_state != state
{
n.focus_state = state;
n.interaction.focused = state != FocusState::Unfocused;
self.mark_paint_dirty(id);
}
}
pub fn scroll_offset(&self, id: NodeId) -> (f32, f32) {
self.get(id).map(|n| n.scroll_offset).unwrap_or((0.0, 0.0))
}
pub fn set_scroll_offset(&mut self, id: NodeId, v: (f32, f32)) -> bool {
if let Some(n) = self.get_mut(id)
&& n.scroll_offset != v
{
n.scroll_offset = v;
n.needs_scroll_event = true;
self.mark_layout_dirty(id);
return true;
}
false
}
pub fn take_scroll_changes(&mut self) -> Vec<NodeId> {
let mut out = Vec::new();
for (i, slot) in self.nodes.iter_mut().enumerate() {
if let Some(n) = slot
&& n.needs_scroll_event
{
n.needs_scroll_event = false;
out.push(NodeId::new(i as u32, self.gens[i]));
}
}
out
}
pub fn find_by_key(&self, key: &Key) -> Option<NodeId> {
self.node_ids()
.find(|id| self.get(*id).and_then(|n| n.key.as_ref()) == Some(key))
}
pub fn slider_drag_to(&mut self, id: NodeId, x: f32) -> bool {
let rect = self.get(id).map(|n| n.rect()).unwrap_or_default();
let (changed, binding) = {
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Slider {
value, min, max, ..
} = &mut n.kind
else {
return false;
};
let t = if rect.width > 0.0 {
((x - rect.x) / rect.width).clamp(0.0, 1.0)
} else {
0.0
};
let v = *min + t * (*max - *min);
let changed = if v.to_bits() != value.to_bits() {
*value = v;
Some(v)
} else {
None
};
(changed, n.bindings.value.clone())
};
if let Some(v) = changed {
self.mark_paint_dirty(id);
if let Some(sig) = binding {
sig.set(v);
}
}
changed.is_some()
}
pub fn toggle_checked(&mut self, id: NodeId) -> Option<bool> {
let (next, binding) = {
let n = self.get_mut(id)?;
let Kind::Checkbox { checked } = &mut n.kind else {
return None;
};
*checked = !*checked;
(*checked, n.bindings.checked.clone())
};
self.mark_paint_dirty(id);
if let Some(sig) = binding {
sig.set(next);
}
Some(next)
}
pub fn toggle_switch(&mut self, id: NodeId) -> Option<bool> {
let (next, binding) = {
let n = self.get_mut(id)?;
let Kind::Switch { on } = &mut n.kind else {
return None;
};
*on = !*on;
(*on, n.bindings.checked.clone())
};
self.mark_paint_dirty(id);
if let Some(sig) = binding {
sig.set(next);
}
Some(next)
}
pub fn select_radio(&mut self, id: NodeId) -> Option<String> {
let (value, binding) = {
let n = self.get(id)?;
let Kind::Radio { value, .. } = &n.kind else {
return None;
};
(value.clone(), n.bindings.text.clone())
};
if let Some(sig) = binding {
sig.set(value.clone());
}
Some(value)
}
pub fn input_selection(&self, id: NodeId) -> Option<(usize, usize)> {
let n = self.get(id)?;
let Kind::Input {
caret, anchor, text, ..
} = &n.kind
else {
return None;
};
if caret == anchor {
return None;
}
let (a, b) = (*caret, *anchor);
Some((a.min(b).min(text.len()), a.max(b).min(text.len())))
}
pub fn input_selected_text(&self, id: NodeId) -> Option<String> {
let (a, b) = self.input_selection(id)?;
let n = self.get(id)?;
let Kind::Input { text, .. } = &n.kind else {
return None;
};
Some(text[a..b].to_string())
}
pub fn input_insert(&mut self, id: NodeId, s: &str) -> bool {
if s.is_empty() {
return false;
}
let sel = self.input_selection(id);
let binding = self.input_binding(id);
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Input {
text,
caret,
anchor,
preedit,
..
} = &mut n.kind
else {
return false;
};
if !preedit.is_empty() {
preedit.clear();
}
let (a, b) = match sel {
Some((a, b)) => (a, b),
None => (*caret, *caret),
};
let (a, b) = (clamp_to_char_boundary(text, a), clamp_to_char_boundary(text, b));
text.replace_range(a..b, s);
let pos = a + s.len();
*caret = pos;
*anchor = pos;
let new = text.clone();
self.input_finish(id, true, binding, new);
self.input_ensure_caret_visible(id);
true
}
pub fn input_backspace(&mut self, id: NodeId) -> bool {
self.input_delete_range(id, true)
}
pub fn input_delete(&mut self, id: NodeId) -> bool {
self.input_delete_range(id, false)
}
fn input_delete_range(&mut self, id: NodeId, backward: bool) -> bool {
let sel = self.input_selection(id);
let binding = self.input_binding(id);
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Input {
text,
caret,
anchor,
..
} = &mut n.kind
else {
return false;
};
let (a, b) = match sel {
Some((a, b)) => (a, b),
None => {
if backward {
let c = clamp_to_char_boundary(text, *caret);
(prev_char_boundary(text, c), c)
} else {
let c = clamp_to_char_boundary(text, *caret);
(c, next_char_boundary(text, c))
}
}
};
if a == b {
return false;
}
text.replace_range(a..b, "");
*caret = a;
*anchor = a;
let new = text.clone();
self.input_finish(id, true, binding, new);
self.input_ensure_caret_visible(id);
true
}
pub fn input_move_caret(&mut self, id: NodeId, delta: isize, extend: bool) -> bool {
let cur = match self.get(id).map(|n| &n.kind) {
Some(Kind::Input { caret, .. }) => *caret,
_ => return false,
};
let mut pos = cur;
if let Some(Kind::Input { text, .. }) = self.get(id).map(|n| &n.kind) {
if delta < 0 {
for _ in 0..delta.unsigned_abs() {
pos = prev_char_boundary(text, pos);
}
} else {
for _ in 0..delta as usize {
pos = next_char_boundary(text, pos);
}
}
}
self.input_set_caret(id, pos, extend)
}
pub fn input_set_caret(&mut self, id: NodeId, pos: usize, extend: bool) -> bool {
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Input {
text,
caret,
anchor,
..
} = &mut n.kind
else {
return false;
};
let pos = clamp_to_char_boundary(text, pos);
let changed = *caret != pos || (!extend && *anchor != pos);
*caret = pos;
if !extend {
*anchor = pos;
}
if changed {
self.mark_paint_dirty(id);
self.input_ensure_caret_visible(id);
}
changed
}
pub fn input_select_all(&mut self, id: NodeId) -> bool {
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Input {
text,
caret,
anchor,
..
} = &mut n.kind
else {
return false;
};
let len = text.len();
let changed = *caret != len || *anchor != 0;
*caret = len;
*anchor = 0;
if changed {
self.mark_paint_dirty(id);
}
changed
}
pub fn input_set_preedit(&mut self, id: NodeId, text: String) -> bool {
let Some(n) = self.get_mut(id) else {
return false;
};
let Kind::Input { preedit, .. } = &mut n.kind else {
return false;
};
if *preedit == text {
return false;
}
*preedit = text;
self.mark_paint_dirty(id);
true
}
pub fn input_preedit(&self, id: NodeId) -> Option<&str> {
match self.get(id).map(|n| &n.kind) {
Some(Kind::Input { preedit, .. }) => Some(preedit.as_str()),
_ => None,
}
}
pub fn input_is_active(&self, id: NodeId) -> bool {
self.focused == Some(id)
&& self
.get(id)
.map(|n| n.interaction.enabled && matches!(n.kind, Kind::Input { .. }))
.unwrap_or(false)
}
fn input_binding(&self, id: NodeId) -> Option<crate::reactive::Signal<String>> {
self.get(id).and_then(|n| n.bindings.text.clone())
}
fn input_finish(
&mut self,
id: NodeId,
layout_may_change: bool,
binding: Option<crate::reactive::Signal<String>>,
text: String,
) {
if layout_may_change {
self.mark_layout_dirty(id);
}
self.mark_paint_dirty(id);
if let Some(sig) = binding {
sig.set(text);
}
}
fn input_ensure_caret_visible(&mut self, id: NodeId) {
let (rect, pad_l, pad_r, caret_px, old_scroll) = {
let Some(n) = self.get(id) else {
return;
};
let Kind::Input {
text,
caret,
scroll,
..
} = &n.kind
else {
return;
};
let prefix = clamp_to_char_boundary(text, *caret);
let caret_px = TextEngine::measure_text(&text[..prefix], &n.text.spec).0 as f32;
(
n.rect(),
n.layout.padding[lieui_layout::CSSDirection::Left as usize],
n.layout.padding[lieui_layout::CSSDirection::Right as usize],
caret_px,
*scroll,
)
};
let inner = (rect.width - pad_l - pad_r).max(0.0);
let new_scroll = old_scroll.clamp((caret_px - inner).max(0.0), caret_px.max(0.0));
if (new_scroll - old_scroll).abs() > 0.01
&& let Some(n) = self.get_mut(id)
&& let Kind::Input { scroll, .. } = &mut n.kind
{
*scroll = new_scroll;
self.mark_paint_dirty(id);
}
}
pub fn set_dragging(&mut self, id: NodeId, v: bool) -> bool {
let changed = match self.get_mut(id) {
Some(n) => match &mut n.kind {
Kind::Slider { dragging, .. } => {
let c = *dragging != v;
*dragging = v;
c
}
_ => false,
},
None => false,
};
if changed {
self.mark_paint_dirty(id);
}
changed
}
pub fn capture_pointer(&mut self, pointer: PointerId, id: NodeId) {
self.captures.retain(|(p, _)| *p != pointer);
self.captures.push((pointer, id));
}
pub fn release_pointer(&mut self, pointer: PointerId) -> Option<NodeId> {
let prev = self
.captures
.iter()
.find(|(p, _)| *p == pointer)
.map(|(_, id)| *id);
self.captures.retain(|(p, _)| *p != pointer);
prev
}
pub fn captured_by(&self, pointer: PointerId) -> Option<NodeId> {
self.captures
.iter()
.find(|(p, _)| *p == pointer)
.map(|(_, id)| *id)
}
pub fn mark_flags(&mut self, id: NodeId, f: Flags) {
if let Some(n) = self.get_mut(id) {
n.flags.insert(f);
}
}
pub fn add_builtin_handler(
&mut self,
id: NodeId,
kind: crate::event::EventKind,
f: impl Fn(&mut crate::event::Ctx) + 'static,
) {
if let Some(n) = self.get_mut(id) {
n.handlers.push(HandlerSlot {
kind,
handler: std::rc::Rc::new(f),
handled_events_too: true,
});
}
}
pub fn has_layout_dirty(&self) -> bool {
self.nodes.iter().flatten().any(|n| {
n.flags.contains(Flags::MEASURE_DIRTY) || n.flags.contains(Flags::ARRANGE_DIRTY)
})
}
pub fn mark_paint_dirty(&mut self, id: NodeId) {
self.mark_flags(id, Flags::PAINT_DIRTY);
if let Some(r) = self.damage_bounds(id) {
self.damage_rect(r);
}
}
pub fn damage_bounds(&self, id: NodeId) -> Option<Rect> {
let mut acc = crate::transform::Affine::IDENTITY;
let mut cur = Some(id);
while let Some(c) = cur {
let n = self.get(c)?;
acc = acc.then(n.transform.matrix(n.rect()));
cur = n.parent;
}
let r = acc.bounding_box(self.get(id)?.paint_bounds());
if r.width > 0.0 && r.height > 0.0 { Some(r) } else { None }
}
pub fn mark_layout_dirty(&mut self, id: NodeId) {
let mut cur = Some(id);
while let Some(c) = cur {
let parent = {
let Some(n) = self.get_mut(c) else { break };
n.flags.insert(Flags::MEASURE_DIRTY | Flags::ARRANGE_DIRTY);
if size_stable(&n.layout) {
break;
}
n.parent
};
cur = parent;
}
self.mark_flags(id, Flags::PAINT_DIRTY);
}
pub fn mark_flow_dirty(&mut self, id: NodeId) {
self.mark_layout_dirty(id);
if let Some(p) = self.parent_of(id) {
self.mark_layout_dirty(p);
}
}
pub fn mark_all_layout_dirty(&mut self) {
for n in self.nodes.iter_mut().flatten() {
n.flags
.insert(Flags::MEASURE_DIRTY | Flags::ARRANGE_DIRTY | Flags::PAINT_DIRTY);
}
self.damage_whole_window();
}
pub fn clear_layout_flags(&mut self) {
for n in self.nodes.iter_mut().flatten() {
n.flags
.remove(Flags::MEASURE_DIRTY | Flags::ARRANGE_DIRTY);
}
}
pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
self.nodes
.iter()
.enumerate()
.filter_map(move |(i, n)| n.as_ref().map(|_| NodeId::new(i as u32, self.gens[i])))
}
pub fn layout_boundaries(&self) -> Vec<NodeId> {
let dirty = |id: NodeId| {
self.get(id)
.map(|n| n.flags.contains(Flags::ARRANGE_DIRTY))
.unwrap_or(false)
};
self.node_ids()
.filter(|id| {
dirty(*id) && self.parent_of(*id).map(|p| !dirty(p)).unwrap_or(true)
})
.collect()
}
pub fn damage_rect(&mut self, r: Rect) {
if r.width > 0.0 && r.height > 0.0 {
self.damage.push(r);
}
}
pub fn damage_whole_window(&mut self) {
self.damage_all = true;
}
pub fn take_damage(&mut self) -> (Vec<Rect>, bool) {
(std::mem::take(&mut self.damage), std::mem::take(&mut self.damage_all))
}
}
impl std::fmt::Debug for Track {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Track")
.field("nodes", &self.len())
.field("roots", &self.roots)
.finish()
}
}
fn size_stable(style: &FlexStyle) -> bool {
lieui_layout::is_defined(style.dim[0]) && lieui_layout::is_defined(style.dim[1])
}
pub struct Ancestors<'a> {
track: &'a Track,
cursor: Option<NodeId>,
}
impl Iterator for Ancestors<'_> {
type Item = NodeId;
fn next(&mut self) -> Option<Self::Item> {
let cur = self.cursor?;
self.cursor = self.track.parent_of(cur);
Some(cur)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slider(value: f32) -> Kind {
Kind::Slider {
value,
min: 0.0,
max: 1.0,
dragging: false,
}
}
#[test]
fn alloc_free_reuses_index_but_bumps_generation() {
let mut t = Track::new();
let a = t.create(Kind::Box, None);
assert_eq!(a.index(), 0);
assert_eq!(a.generation(), 0);
assert_eq!(t.destroy(a), 1);
assert!(!t.contains(a), "旧句柄必须失效");
let b = t.create(Kind::Box, None);
assert_eq!(b.index(), 0, "index 复用");
assert_ne!(b.generation(), a.generation(), "generation 必须变");
}
#[test]
fn destroy_subtree_frees_all_and_detaches() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
let mid = t.create(Kind::Box, None);
let leaf = t.create(Kind::Text("x".into()), None);
t.append_child(root, mid);
t.append_child(mid, leaf);
assert_eq!(t.len(), 3);
assert_eq!(t.destroy(mid), 2);
assert_eq!(t.len(), 1);
assert!(t.children(root).is_empty());
assert!(!t.contains(leaf));
}
#[test]
fn descendants_and_ancestors() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
let a = t.create(Kind::Box, None);
let b = t.create(Kind::Text("b".into()), None);
t.append_child(root, a);
t.append_child(a, b);
assert_eq!(t.descendants(root), vec![root, a, b]);
assert_eq!(t.ancestors(b).collect::<Vec<_>>(), vec![a, root]);
}
#[test]
fn kind_desc_applies_without_touching_state() {
let mut t = Track::new();
let id = t.create(slider(0.5), None);
t.get_mut(id).unwrap().kind = Kind::Slider {
value: 0.5,
min: 0.0,
max: 1.0,
dragging: true,
};
let changed = t
.get_mut(id)
.map(|n| KindDesc::Slider { value: 0.8, min: 0.0, max: 1.0 }.apply_to(&mut n.kind))
.unwrap();
assert!(changed);
match &t.get(id).unwrap().kind {
Kind::Slider { value, dragging, .. } => {
assert_eq!(*value, 0.8);
assert!(*dragging, "state 组必须保留");
}
_ => panic!(),
}
let changed_again = t
.get_mut(id)
.map(|n| KindDesc::Slider { value: 0.8, min: 0.0, max: 1.0 }.apply_to(&mut n.kind))
.unwrap();
assert!(!changed_again);
}
#[test]
fn kind_desc_tag_mismatch_reports_changed() {
let mut t = Track::new();
let id = t.create(Kind::Text("a".into()), None);
let mut kind = Kind::Text("a".into());
assert_eq!(KindDesc::Text("a".into()).tag(), kind.tag());
assert!(KindDesc::Box.apply_to(&mut kind), "标签不同应报 changed");
let _ = t.destroy(id);
}
#[test]
fn roots_are_nested_and_removal_cascades() {
let mut t = Track::new();
let content = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, content);
let modal_node = t.create(Kind::Box, None);
let modal = t.add_root(Layer::Modal, None, modal_node);
let popup_node = t.create(Kind::Box, None);
let popup = t.add_root(Layer::Popup, Some(modal), popup_node);
assert_eq!(t.roots_of(Layer::Popup).count(), 1);
assert_eq!(t.root(popup).unwrap().owner, Some(modal));
assert_eq!(t.remove_root(modal), 2);
assert!(t.root(popup).is_none());
assert!(!t.contains(popup_node));
assert!(t.root(modal).is_none());
assert!(t.content_root().is_some());
}
#[test]
fn layer_defaults_follow_the_layer() {
assert_eq!(LayerOpts::for_layer(Layer::Overlay).hit_test_visible, false);
assert_eq!(LayerOpts::for_layer(Layer::Modal).blocks_below, true);
assert!(LayerOpts::for_layer(Layer::Modal).backdrop.is_some());
assert!(LayerOpts::for_layer(Layer::Popup).dismiss_on_outside_click);
assert!(!LayerOpts::for_layer(Layer::DragPreview).hit_test_visible);
}
#[test]
fn paint_dirty_registers_node_rect_as_damage() {
let mut t = Track::new();
let id = t.create(Kind::Box, None);
t.get_mut(id).unwrap().computed = ComputedLayout {
x: 10.0,
y: 20.0,
width: 30.0,
height: 40.0,
overflow_scroll: false,
};
t.mark_paint_dirty(id);
let (rects, all) = t.take_damage();
assert!(!all);
assert_eq!(rects, vec![Rect::new(10.0, 20.0, 30.0, 40.0)]);
assert!(t.get(id).unwrap().flags.contains(Flags::PAINT_DIRTY));
let fresh = t.create(Kind::Box, None);
t.mark_paint_dirty(fresh);
let (rects, _) = t.take_damage();
assert!(rects.is_empty());
}
#[test]
fn layout_dirty_bubbles_to_ancestors() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
let mid = t.create(Kind::Box, None);
let leaf = t.create(Kind::Text("x".into()), None);
t.append_child(root, mid);
t.append_child(mid, leaf);
t.get_mut(root).unwrap().flags = Flags::empty();
t.get_mut(mid).unwrap().flags = Flags::empty();
t.get_mut(leaf).unwrap().flags = Flags::empty();
t.mark_layout_dirty(leaf);
for id in [leaf, mid, root] {
assert!(
t.get(id).unwrap().flags.contains(Flags::MEASURE_DIRTY),
"ancestor {id:?} 应被冒泡置脏"
);
}
}
#[test]
fn interaction_state_transitions_mark_paint_dirty() {
let mut t = Track::new();
let id = t.create(Kind::Box, None);
let _ = t.take_damage();
t.set_pointer_over(id, true);
t.set_pressed(id, true);
t.set_focused(id, FocusState::Keyboard);
let st = t.state(id);
assert!(st.pointer_over && st.pressed && st.focused);
assert_eq!(t.get(id).unwrap().focus_state, FocusState::Keyboard);
t.get_mut(id).unwrap().flags.remove(Flags::PAINT_DIRTY);
t.set_pointer_over(id, true);
assert!(!t.get(id).unwrap().flags.contains(Flags::PAINT_DIRTY));
}
#[test]
fn pointer_capture_is_per_pointer() {
let mut t = Track::new();
let a = t.create(Kind::Box, None);
let b = t.create(Kind::Box, None);
let mouse = PointerId(0);
let touch = PointerId(1);
t.capture_pointer(mouse, a);
t.capture_pointer(touch, b);
assert_eq!(t.captured_by(mouse), Some(a));
assert_eq!(t.captured_by(touch), Some(b));
t.capture_pointer(mouse, b);
assert_eq!(t.captured_by(mouse), Some(b));
assert_eq!(t.captures.len(), 2);
assert_eq!(t.release_pointer(mouse), Some(b));
assert_eq!(t.captured_by(mouse), None);
assert_eq!(t.captured_by(touch), Some(b));
}
#[test]
fn scroll_offset_change_marks_layout_and_paint() {
let mut t = Track::new();
let id = t.create(Kind::Box, None);
t.get_mut(id).unwrap().flags = Flags::empty();
assert!(t.set_scroll_offset(id, (0.0, 24.0)));
assert!(!t.set_scroll_offset(id, (0.0, 24.0)), "同值不算变化");
let f = t.get(id).unwrap().flags;
assert!(f.contains(Flags::PAINT_DIRTY));
assert!(f.contains(Flags::MEASURE_DIRTY), "滚动触发边界重排");
}
#[test]
fn set_children_reorders_without_destroying() {
let mut t = Track::new();
let p = t.create(Kind::Box, None);
let a = t.create(Kind::Text("a".into()), None);
let b = t.create(Kind::Text("b".into()), None);
t.append_child(p, a);
t.append_child(p, b);
t.set_children(p, &[b, a]);
assert_eq!(t.children(p), &[b, a]);
assert_eq!(t.parent_of(a), Some(p));
}
#[test]
fn image_desc_uses_arc_identity() {
let data = Arc::new(ImageData {
width: 1,
height: 1,
rgba: vec![0, 0, 0, 255],
});
let mut kind = Kind::Image(Arc::clone(&data));
assert!(!KindDesc::Image(Arc::clone(&data)).apply_to(&mut kind));
}
}