use std::rc::Rc;
use lieui_geom::{Color, Rect};
use lieui_layout::{CSSDirection, Dimension, FlexAlign, FlexDirection, FlexStyle};
use crate::event::{EventKind, HandlerSlot};
use crate::reactive::Signal;
use crate::style::{ImageStyle, PaintStyle, ShadowSpec, TextStyle};
use crate::theme::Theme;
use crate::track::{
Anchor, Bindings, ImageData, Key, KindDesc, Layer, LayerOpts, Placement, Transform, Visibility,
};
pub(crate) struct DescNode {
pub kind: KindDesc,
pub key: Option<Key>,
pub tooltip: Option<String>,
pub layout: FlexStyle,
pub paint: PaintStyle,
pub text: TextStyle,
pub image: ImageStyle,
pub visibility: Visibility,
pub hit_test_visible: bool,
pub enabled: bool,
pub clip: Option<Rect>,
pub transform: Transform,
pub tab_stop: bool,
pub tab_index: i32,
pub handlers: Vec<HandlerSlot>,
pub bindings: Bindings,
pub children: Vec<u32>,
pub child_keys: Vec<Option<Key>>,
}
impl DescNode {
fn new(kind: KindDesc) -> Self {
Self {
kind,
key: None,
tooltip: None,
layout: FlexStyle::default(),
paint: PaintStyle::default(),
text: TextStyle::default(),
image: ImageStyle::default(),
visibility: Visibility::Visible,
hit_test_visible: true,
enabled: true,
clip: None,
transform: Transform::default(),
tab_stop: false,
tab_index: 0,
handlers: Vec::new(),
bindings: Bindings::default(),
children: Vec::new(),
child_keys: Vec::new(),
}
}
}
pub(crate) struct DescRoot {
pub node: u32,
pub layer: Layer,
pub owner: Option<u32>,
pub opts: LayerOpts,
}
#[derive(Default)]
pub struct ViewBuf {
pub(crate) nodes: Vec<DescNode>,
pub(crate) roots: Vec<DescRoot>,
stack: Vec<u32>,
root_stack: Vec<u32>,
in_keyed_item: bool,
theme: Theme,
}
impl ViewBuf {
pub fn new() -> Self {
Self::default()
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn on(&mut self, kind: EventKind, f: impl Fn(&mut crate::event::Ctx) + 'static) {
let d = self.cur();
d.on(kind, f);
}
pub(crate) fn set_theme(&mut self, t: Theme) {
self.theme = t;
}
pub fn begin(&mut self) {
self.nodes.clear();
self.roots.clear();
self.stack.clear();
self.root_stack.clear();
self.in_keyed_item = false;
}
pub(crate) fn node(&self, idx: u32) -> &DescNode {
&self.nodes[idx as usize]
}
fn node_mut(&mut self, idx: u32) -> &mut DescNode {
&mut self.nodes[idx as usize]
}
fn alloc(&mut self, kind: KindDesc) -> u32 {
self.nodes.push(DescNode::new(kind));
(self.nodes.len() - 1) as u32
}
fn push_desc(&mut self, kind: KindDesc) -> u32 {
let idx = self.alloc(kind);
match self.stack.last().copied() {
Some(parent) => {
debug_assert!(
!self.node(parent).child_keys.iter().any(|k| k.is_some()) || self.in_keyed_item,
"该容器已被 keyed_list 接管,不能在它里面直接再加子节点"
);
let n = self.node_mut(parent);
n.children.push(idx);
if !n.child_keys.is_empty() {
n.child_keys.push(None);
}
}
None => {
debug_assert!(
self.roots.iter().all(|r| r.layer != Layer::Content),
"内容根只能声明一次:顶层第二个声明请改用层(modal/overlay/popup_at/…)"
);
let rid = self.roots.len() as u32;
self.roots.push(DescRoot {
node: idx,
layer: Layer::Content,
owner: None,
opts: LayerOpts::for_layer(Layer::Content),
});
self.root_stack.push(rid);
}
}
idx
}
fn cur(&mut self) -> DescRef<'_> {
match self.stack.last().copied() {
Some(idx) => DescRef { v: self, idx },
None => panic!("样式糖必须在容器闭包内使用(v.column(|c| c.gap(8.0)))"),
}
}
pub fn column(&mut self, f: impl FnOnce(&mut Self)) {
let idx = self.push_desc(KindDesc::Box);
self.node_mut(idx).layout.flex_direction = FlexDirection::Column;
self.with_container(idx, f);
}
pub fn row(&mut self, f: impl FnOnce(&mut Self)) {
let idx = self.push_desc(KindDesc::Box);
self.node_mut(idx).layout.flex_direction = FlexDirection::Row;
self.with_container(idx, f);
}
pub fn container(&mut self, f: impl FnOnce(&mut Self)) {
let idx = self.push_desc(KindDesc::Box);
self.node_mut(idx).layout.flex_direction = FlexDirection::Column;
self.with_container(idx, f);
}
pub fn scroll(&mut self, f: impl FnOnce(&mut Self)) {
let idx = self.push_desc(KindDesc::Box);
{
let n = self.node_mut(idx);
n.layout.flex_direction = FlexDirection::Column;
n.layout.overflow_scroll = true;
n.layout.show_scrollbar = true;
n.paint.clip_content = true;
}
self.with_container(idx, f);
}
pub fn spacer(&mut self) {
let idx = self.push_desc(KindDesc::Box);
self.node_mut(idx).layout.flex_grow = 1.0;
}
fn with_container(&mut self, idx: u32, f: impl FnOnce(&mut Self)) {
self.stack.push(idx);
f(self);
self.stack.pop();
}
pub fn text(&mut self, s: impl Into<String>) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Text(s.into()));
self.node_mut(idx).text.color = self.theme().text;
DescRef { v: self, idx }
}
pub fn button(&mut self, label: &str) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Button {
label: label.to_string(),
});
self.bake_button_style(idx);
DescRef { v: self, idx }
}
pub fn icon(&mut self, name: impl AsRef<str>) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Text(
crate::icon::icon_char(name.as_ref()).to_string(),
));
{
let t = *self.theme();
let n = self.node_mut(idx);
n.text.color = t.text;
n.text.spec = crate::icon::icon_spec(20.0);
}
DescRef { v: self, idx }
}
pub fn icon_button(&mut self, name: impl AsRef<str>) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Button {
label: crate::icon::icon_char(name.as_ref()).to_string(),
});
self.bake_button_style(idx);
self.node_mut(idx).text.spec = crate::icon::icon_spec(20.0);
DescRef { v: self, idx }
}
fn bake_button_style(&mut self, idx: u32) {
let t = *self.theme();
let n = self.node_mut(idx);
n.paint = PaintStyle::new()
.background(t.control)
.hover_background(t.control_hover)
.pressed_background(t.control_pressed)
.radius(t.control_radius);
n.text.color = t.text;
n.layout = n
.layout
.clone()
.padding_left(10.0)
.padding_right(10.0)
.padding_top(6.0)
.padding_bottom(6.0);
}
pub fn checkbox(&mut self, checked: bool) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Checkbox { checked });
{
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(18.0).height(18.0);
}
DescRef { v: self, idx }
}
pub fn slider(&mut self, value: f32) -> DescRef<'_> {
self.slider_range(value, 0.0, 1.0)
}
pub fn slider_range(&mut self, value: f32, min: f32, max: f32) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Slider { value, min, max });
{
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(140.0).height(20.0);
}
DescRef { v: self, idx }
}
pub fn slider_bound(&mut self, sig: &Signal<f32>, min: f32, max: f32) -> DescRef<'_> {
let value = sig.get().clamp(min, max);
let idx = self.push_desc(KindDesc::Slider { value, min, max });
{
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(140.0).height(20.0);
n.bindings.value = Some(sig.clone());
}
DescRef { v: self, idx }
}
pub fn input(&mut self, value: &str) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Input {
text: value.to_string(),
placeholder: String::new(),
});
self.style_input(idx);
DescRef { v: self, idx }
}
pub fn input_bound(&mut self, sig: &Signal<String>) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Input {
text: sig.get(),
placeholder: String::new(),
});
self.style_input(idx);
self.node_mut(idx).bindings.text = Some(sig.clone());
DescRef { v: self, idx }
}
fn style_input(&mut self, idx: u32) {
let t = *self.theme();
let n = self.node_mut(idx);
n.layout = n
.layout
.clone()
.width(200.0)
.height(28.0)
.padding_left(8.0)
.padding_right(8.0)
.padding_top(4.0)
.padding_bottom(4.0);
n.paint = PaintStyle::new()
.background(t.input_background)
.border(1.0, t.control_border)
.radius(t.control_radius);
n.text.color = t.text;
n.tab_stop = true;
n.paint.clip_content = true;
}
pub fn custom(&mut self, cell: &crate::custom::CustomCell) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Custom(cell.clone()));
DescRef { v: self, idx }
}
pub fn switch(&mut self, on: bool) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Switch { on });
self.style_switch(idx);
DescRef { v: self, idx }
}
pub fn switch_bound(&mut self, sig: &Signal<bool>) -> DescRef<'_> {
let on = sig.get();
let idx = self.push_desc(KindDesc::Switch { on });
self.style_switch(idx);
self.node_mut(idx).bindings.checked = Some(sig.clone());
DescRef { v: self, idx }
}
pub fn radio(&mut self, selected: bool) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Radio {
selected,
value: String::new(),
});
self.style_radio(idx);
DescRef { v: self, idx }
}
pub fn radio_bound(&mut self, sig: &Signal<String>, value: &str) -> DescRef<'_> {
let selected = sig.get() == value;
let idx = self.push_desc(KindDesc::Radio {
selected,
value: value.to_string(),
});
self.style_radio(idx);
self.node_mut(idx).bindings.text = Some(sig.clone());
DescRef { v: self, idx }
}
fn style_switch(&mut self, idx: u32) {
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(40.0).height(20.0);
n.tab_stop = true;
}
fn style_radio(&mut self, idx: u32) {
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(18.0).height(18.0);
n.tab_stop = true;
}
pub fn checkbox_bound(&mut self, sig: &Signal<bool>) -> DescRef<'_> {
let checked = sig.get();
let idx = self.push_desc(KindDesc::Checkbox { checked });
{
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(18.0).height(18.0);
n.bindings.checked = Some(sig.clone());
}
DescRef { v: self, idx }
}
pub fn progress(&mut self, value: f32) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Progress { value });
{
let n = self.node_mut(idx);
n.layout = n.layout.clone().width(120.0).height(8.0);
}
DescRef { v: self, idx }
}
pub fn image(&mut self, data: std::sync::Arc<ImageData>) -> DescRef<'_> {
let idx = self.push_desc(KindDesc::Image(data));
DescRef { v: self, idx }
}
pub fn keyed_list<I, K, F>(&mut self, items: I, key_fn: fn(&I::Item) -> K, mut item: F)
where
I: IntoIterator,
K: Into<Key>,
F: FnMut(&mut Self, &I::Item),
{
let Some(parent) = self.stack.last().copied() else {
panic!("keyed_list 必须在容器闭包内使用");
};
{
let n = self.node_mut(parent);
debug_assert!(
n.children.is_empty(),
"keyed_list 会接管容器的子节点,请让该容器只放列表"
);
n.children.clear();
n.child_keys.clear();
}
for it in items {
let key: Key = key_fn(&it).into();
let before = self.node(parent).children.len();
self.in_keyed_item = true;
item(self, &it);
self.in_keyed_item = false;
let after = self.node(parent).children.len();
debug_assert_eq!(after, before + 1, "keyed_list 的 item 闭包必须恰好产生一个子节点");
let child = self.node(parent).children[after - 1];
self.node_mut(child).key = Some(key.clone());
let n = self.node_mut(parent);
if n.child_keys.len() < n.children.len() {
n.child_keys.resize(n.children.len(), None);
}
n.child_keys[after - 1] = Some(key);
}
}
pub fn modal(&mut self, f: impl FnOnce(&mut Self)) {
self.layer(Layer::Modal, None, |_| {}, f);
}
pub fn overlay(&mut self, f: impl FnOnce(&mut Self)) {
self.layer(Layer::Overlay, None, |_| {}, f);
}
pub fn popup_at(
&mut self,
anchor_key: impl Into<Key>,
placement: Placement,
f: impl FnOnce(&mut Self),
) {
let key: Key = anchor_key.into();
self.layer(
Layer::Popup,
None,
|opts| {
opts.anchor = Some(Anchor {
target: crate::track::AnchorTarget::Key(key),
placement,
});
},
f,
);
}
pub fn tooltip_at(
&mut self,
anchor_key: impl Into<Key>,
placement: Placement,
f: impl FnOnce(&mut Self),
) {
let key: Key = anchor_key.into();
self.layer(
Layer::Tooltip,
None,
|opts| {
opts.anchor = Some(Anchor {
target: crate::track::AnchorTarget::Key(key),
placement,
});
},
f,
);
}
pub fn drag_preview(&mut self, f: impl FnOnce(&mut Self)) {
self.layer(Layer::DragPreview, None, |_| {}, f);
}
fn layer(
&mut self,
layer: Layer,
owner: Option<u32>,
configure: impl FnOnce(&mut LayerOpts),
f: impl FnOnce(&mut Self),
) {
let owner = owner.or_else(|| self.root_stack.last().copied());
let node = self.alloc(KindDesc::Box);
let rid = self.roots.len() as u32;
let mut opts = LayerOpts::for_layer(layer);
configure(&mut opts);
if matches!(layer, Layer::Popup | Layer::Tooltip) {
let t = *self.theme();
let n = self.node_mut(node);
n.paint = PaintStyle::new()
.background(t.input_background)
.border(1.0, t.control_border)
.radius(6.0)
.shadow(ShadowSpec::new(0.0, 4.0, 12.0, 2.0, Color::rgba(0, 0, 0, 60)));
n.layout = n
.layout
.clone()
.padding_left(6.0)
.padding_right(6.0)
.padding_top(6.0)
.padding_bottom(6.0);
}
self.roots.push(DescRoot {
node,
layer,
owner,
opts,
});
self.root_stack.push(rid);
self.stack.push(node);
f(self);
self.stack.pop();
self.root_stack.pop();
}
pub fn layer_opts(&mut self, f: impl FnOnce(&mut LayerOpts)) {
let Some(rid) = self.root_stack.last().copied() else {
panic!("layer_opts 只能在层闭包内使用");
};
let mut opts = self.roots[rid as usize].opts.clone();
f(&mut opts);
self.roots[rid as usize].opts = opts;
}
pub fn gap(&mut self, g: f32) {
self.cur().gap(g);
}
pub fn layout(&mut self, f: impl FnOnce(&mut FlexStyle)) {
self.cur().layout(f);
}
pub fn padding(&mut self, p: f32) {
self.cur().padding(p);
}
pub fn padding_y(&mut self, p: f32) {
self.cur().padding_y(p);
}
pub fn width(&mut self, w: f32) {
self.cur().width(w);
}
pub fn height(&mut self, h: f32) {
self.cur().height(h);
}
pub fn expand(&mut self, v: bool) {
self.cur().expand(v);
}
pub fn center(&mut self) {
self.cur().center();
}
pub fn align_items(&mut self, a: FlexAlign) {
self.cur().align_items(a);
}
pub fn justify_content(&mut self, a: FlexAlign) {
self.cur().justify_content(a);
}
pub fn background(&mut self, c: Color) {
self.cur().background(c);
}
pub fn radius(&mut self, r: f32) {
self.cur().radius(r);
}
pub fn border(&mut self, w: f32, c: Color) {
self.cur().border(w, c);
}
pub fn clip(&mut self, v: bool) {
self.cur().clip(v);
}
pub fn key(&mut self, k: impl Into<Key>) {
self.cur().key(k);
}
}
pub struct DescRef<'a> {
v: &'a mut ViewBuf,
idx: u32,
}
impl<'a> DescRef<'a> {
fn n(&mut self) -> &mut DescNode {
self.v.node_mut(self.idx)
}
pub fn layout(mut self, f: impl FnOnce(&mut FlexStyle)) -> Self {
f(&mut self.n().layout);
self
}
pub fn paint(mut self, f: impl FnOnce(&mut PaintStyle)) -> Self {
f(&mut self.n().paint);
self
}
pub fn text_style(mut self, f: impl FnOnce(&mut TextStyle)) -> Self {
f(&mut self.n().text);
self
}
pub fn gap(mut self, g: f32) -> Self {
self.n().layout.item_space = g;
self
}
pub fn padding(mut self, p: f32) -> Self {
let l = &mut self.n().layout;
for i in 0..4 {
l.padding[i] = p;
}
self
}
pub fn padding_y(mut self, p: f32) -> Self {
let l = &mut self.n().layout;
l.padding[CSSDirection::Top as usize] = p;
l.padding[CSSDirection::Bottom as usize] = p;
self
}
pub fn width(mut self, w: f32) -> Self {
self.n().layout.dim[Dimension::Width as usize] = w;
self
}
pub fn height(mut self, h: f32) -> Self {
self.n().layout.dim[Dimension::Height as usize] = h;
self
}
pub fn expand(mut self, v: bool) -> Self {
self.n().layout.flex_grow = if v { 1.0 } else { 0.0 };
self
}
pub fn center(mut self) -> Self {
let l = &mut self.n().layout;
l.justify_content = FlexAlign::Center;
l.align_items = FlexAlign::Center;
self
}
pub fn align_items(mut self, a: FlexAlign) -> Self {
self.n().layout.align_items = a;
self
}
pub fn justify_content(mut self, a: FlexAlign) -> Self {
self.n().layout.justify_content = a;
self
}
pub fn background(mut self, c: Color) -> Self {
self.n().paint.background_color = Some(c);
self
}
pub fn hover_background(mut self, c: Color) -> Self {
self.n().paint.hover_background = Some(c);
self
}
pub fn pressed_background(mut self, c: Color) -> Self {
self.n().paint.pressed_background = Some(c);
self
}
pub fn radius(mut self, r: f32) -> Self {
self.n().paint.border_radius = r;
self
}
pub fn border(mut self, w: f32, c: Color) -> Self {
let p = &mut self.n().paint;
p.border_width = w;
p.border_color = Some(c);
self
}
pub fn placeholder(mut self, s: &str) -> Self {
if let KindDesc::Input { placeholder, .. } = &mut self.n().kind {
*placeholder = s.to_string();
}
self
}
pub fn shadow(mut self, s: ShadowSpec) -> Self {
self.n().paint.shadow = Some(s);
self
}
pub fn clip(mut self, v: bool) -> Self {
self.n().paint.clip_content = v;
self
}
pub fn font_size(mut self, s: f64) -> Self {
self.n().text.spec.font_size = s;
self
}
pub fn color(mut self, c: Color) -> Self {
self.n().text.color = c;
self
}
pub fn hover_color(mut self, c: Color) -> Self {
self.n().text.hover_color = Some(c);
self
}
pub fn pressed_color(mut self, c: Color) -> Self {
self.n().text.pressed_color = Some(c);
self
}
pub fn font_family(mut self, f: impl Into<String>) -> Self {
self.n().text.spec.font_family = f.into();
self
}
pub fn text_align(mut self, a: lieui_text::TextAlign) -> Self {
self.n().text.spec.text_align = a;
self
}
pub fn wrap(mut self, v: bool) -> Self {
self.n().text.spec.wrap = v;
self
}
pub fn optical_align(mut self, v: bool) -> Self {
self.n().text.spec.optical_align = v;
self
}
pub fn visible(mut self, v: bool) -> Self {
self.n().visibility = if v {
Visibility::Visible
} else {
Visibility::Collapsed
};
self
}
pub fn hidden(mut self) -> Self {
self.n().visibility = Visibility::Hidden;
self
}
pub fn collapsed(mut self) -> Self {
self.n().visibility = Visibility::Collapsed;
self
}
pub fn hit_test_visible(mut self, v: bool) -> Self {
self.n().hit_test_visible = v;
self
}
pub fn enabled(mut self, v: bool) -> Self {
self.n().enabled = v;
self
}
pub fn tooltip(mut self, s: impl Into<String>) -> Self {
self.n().tooltip = Some(s.into());
self
}
pub fn opacity(mut self, o: f32) -> Self {
self.n().paint.opacity = o.clamp(0.0, 1.0);
self
}
pub fn rotate(mut self, deg: f32) -> Self {
self.n().transform.rotation_deg = deg;
self
}
pub fn translate(mut self, x: f32, y: f32) -> Self {
self.n().transform.translate = (x, y);
self
}
pub fn clip_rect(mut self, r: Rect) -> Self {
self.n().clip = Some(r);
self
}
pub fn tab_stop(mut self, v: bool) -> Self {
self.n().tab_stop = v;
self
}
pub fn tab_index(mut self, i: i32) -> Self {
self.n().tab_index = i;
self
}
pub fn key(mut self, k: impl Into<Key>) -> Self {
self.n().key = Some(k.into());
self
}
pub fn on(mut self, kind: EventKind, f: impl Fn(&mut crate::event::Ctx) + 'static) -> Self {
self.n().handlers.push(HandlerSlot {
kind,
handler: Rc::new(f),
handled_events_too: false,
});
self
}
pub fn on_always(
mut self,
kind: EventKind,
f: impl Fn(&mut crate::event::Ctx) + 'static,
) -> Self {
self.n().handlers.push(HandlerSlot {
kind,
handler: Rc::new(f),
handled_events_too: true,
});
self
}
pub fn on_tap(self, f: impl Fn() + 'static) -> Self {
self.on(EventKind::Tapped, move |_| f())
}
pub fn on_tap_with(self, f: impl Fn(&mut crate::event::Ctx) + 'static) -> Self {
self.on(EventKind::Tapped, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn icon_creates_icon_font_text() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| {
c.icon("close");
c.icon("no-such-icon");
});
let root = &v.nodes[v.roots[0].node as usize];
let a = &v.nodes[root.children[0] as usize];
let b = &v.nodes[root.children[1] as usize];
match &a.kind {
KindDesc::Text(s) => assert_eq!(s, "\u{e5cd}", "close = e5cd"),
_ => panic!("icon 应是 Text"),
}
assert_eq!(a.text.spec.font_family.as_str(), crate::icon::ICON_FONT_FAMILY);
assert!(!a.text.spec.wrap, "图标不换行");
assert_eq!(a.text.spec.font_size, 20.0);
match &b.kind {
KindDesc::Text(s) => assert_eq!(s.as_str(), "□"),
_ => panic!(),
}
}
#[test]
fn icon_button_is_a_button_with_icon_glyph() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| {
c.icon_button("settings");
});
let root = &v.nodes[v.roots[0].node as usize];
let a = &v.nodes[root.children[0] as usize];
assert_eq!(
a.kind.tag(),
crate::track::KindTag::Button,
"icon_button 走按钮交互"
);
match &a.kind {
KindDesc::Button { label } => assert_eq!(label, "\u{e8b8}"),
_ => panic!(),
}
assert_eq!(a.text.spec.font_family, crate::icon::ICON_FONT_FAMILY);
}
#[test]
fn builds_a_nested_tree_with_content_root() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| {
c.gap(12.0);
c.text("title").font_size(48.0);
c.row(|r| {
r.gap(4.0);
r.button("a");
r.button("b");
});
});
assert_eq!(v.roots.len(), 1);
assert_eq!(v.roots[0].layer, Layer::Content);
let root = &v.nodes[v.roots[0].node as usize];
assert_eq!(root.children.len(), 2);
assert_eq!(root.layout.item_space, 12.0);
assert!(matches!(root.kind, KindDesc::Box));
assert_eq!(root.children[0], 1);
let title = &v.nodes[1];
assert!(matches!(title.kind, KindDesc::Text(_)));
assert_eq!(title.text.spec.font_size, 48.0);
let row = &v.nodes[root.children[1] as usize];
assert_eq!(row.children.len(), 2);
assert_eq!(v.nodes[row.children[0] as usize].kind.tag(), crate::track::KindTag::Button);
assert_eq!(v.nodes.len(), 5);
}
#[test]
fn begin_resets_for_reuse() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| { c.text("x"); });
let cap = v.nodes.capacity();
v.begin();
assert!(v.nodes.is_empty());
assert!(v.roots.is_empty());
v.column(|c| { c.text("y"); });
assert_eq!(v.nodes.len(), 2, "复用后不残留");
assert!(v.nodes.capacity() >= cap);
}
#[test]
fn layers_are_nested_and_owned() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| { c.text("body"); });
v.overlay(|o| { o.text("WATERMARK"); });
v.modal(|m| {
m.text("确认?");
m.popup_at("more", Placement::RightOf, |p| { p.text("子菜单"); });
});
let layers: Vec<Layer> = v.roots.iter().map(|r| r.layer).collect();
assert_eq!(
layers,
vec![Layer::Content, Layer::Overlay, Layer::Modal, Layer::Popup]
);
let content = 0;
let modal = 2;
let popup: usize = 3;
assert_eq!(v.roots[content].owner, None);
assert_eq!(v.roots[1].owner, Some(content as u32));
assert_eq!(v.roots[modal].owner, Some(content as u32));
assert_eq!(v.roots[popup].owner, Some(modal as u32));
let mo = &v.roots[modal].opts;
assert!(mo.backdrop.is_some());
assert!(mo.blocks_below);
assert!(mo.anchor.is_none());
assert!(!v.roots[1].opts.hit_test_visible);
assert_eq!(
v.roots[popup]
.opts
.anchor
.as_ref()
.map(|a| match &a.target {
crate::track::AnchorTarget::Key(k) => k.clone(),
_ => panic!("popup 锚点应为 key"),
}),
Some(Key::Str("more".into()))
);
}
#[test]
fn keyed_list_records_keys_in_order() {
let mut v = ViewBuf::new();
v.begin();
let items = vec![10u64, 20, 30];
v.column(|c| {
c.keyed_list(items.iter().copied(), |id| *id, |v, id| {
v.text(format!("item {id}"));
});
});
let root = &v.nodes[v.roots[0].node as usize];
assert_eq!(root.children.len(), 3);
assert_eq!(
root.child_keys,
vec![
Some(Key::U64(10)),
Some(Key::U64(20)),
Some(Key::U64(30))
]
);
let first = &v.nodes[root.children[0] as usize];
match &first.kind {
KindDesc::Text(s) => assert_eq!(s, "item 10"),
_ => panic!(),
}
}
#[test]
fn handlers_are_recorded_per_event_kind() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| {
c.button("+1").on_tap(|| {});
c.button("ctx").on_tap_with(|_cx| {});
});
let root = &v.nodes[v.roots[0].node as usize];
let a = &v.nodes[root.children[0] as usize];
let b = &v.nodes[root.children[1] as usize];
assert_eq!(a.handlers.len(), 1);
assert_eq!(a.handlers[0].kind, EventKind::Tapped);
assert_eq!(b.handlers[0].kind, EventKind::Tapped);
assert!(!a.handlers[0].handled_events_too);
}
#[test]
fn spacer_and_scroll_configuration() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| {
c.spacer();
c.scroll(|s| s.expand(true));
});
let root = &v.nodes[v.roots[0].node as usize];
assert_eq!(v.nodes[root.children[0] as usize].layout.flex_grow, 1.0);
let sc = &v.nodes[root.children[1] as usize];
assert!(sc.layout.overflow_scroll);
assert!(sc.paint.clip_content);
assert_eq!(sc.layout.flex_grow, 1.0);
}
#[test]
#[should_panic(expected = "内容根只能声明一次")]
fn second_content_root_panics() {
let mut v = ViewBuf::new();
v.begin();
v.column(|c| { c.text("a"); });
v.column(|c| { c.text("b"); }); }
}