1use std::{
5 any::Any,
6 f32::consts::{PI, TAU},
7 fmt,
8 sync::{
9 Arc,
10 atomic::{AtomicU32, Ordering},
11 },
12 time::Duration,
13};
14
15use omp_core::Str;
16use smallvec::SmallVec;
17use xutf::Text as _;
18
19use crate::{
20 anim::{self, Easing, Lerp, Tween},
21 components::Markdown,
22 context::UiContext,
23 frame::{Color, Frame, Gradient, Rect, Style},
24 input::{Key, Mouse, UiEvent},
25 markup::{Align, Dim},
26 props::{Prop, PropValue, Props},
27};
28
29pub type Slot = u32;
31
32static NEXT_SLOT: AtomicU32 = AtomicU32::new(1);
33
34pub fn next_slot() -> Slot {
37 NEXT_SLOT.fetch_add(1, Ordering::Relaxed)
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum Flow {
43 Skip,
45 Consumed,
47 Event(UiEvent),
49}
50
51pub struct ResizeTail<'a> {
54 pub children: &'a mut [Cached],
56 pub gap: u16,
58}
59
60pub trait Component: Any {
62 fn props(&self) -> &Props;
64 fn props_mut(&mut self) -> &mut Props;
66 fn slot(&self) -> Slot;
68 fn kind(&self) -> &'static str {
72 std::any::type_name::<Self>()
73 }
74 fn children(&self) -> &[Cached] {
76 &[]
77 }
78 fn children_mut(&mut self) -> &mut [Cached] {
80 &mut []
81 }
82 fn measure(&mut self, ctx: &UiContext) -> (u16, u16);
84 fn height(&mut self, ctx: &UiContext, width: u16) -> u16;
86 fn place(&mut self, ctx: &UiContext, content: Rect) {
88 let _ = (ctx, content);
89 }
90 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect);
92 fn paints_border(&self) -> bool {
95 true
96 }
97 fn paints_background(&self) -> bool {
101 true
102 }
103 fn gradient_bounds(&self, content: Rect) -> Option<Rect> {
109 let _ = content;
110 None
111 }
112 fn resize_tail(&mut self) -> Option<ResizeTail<'_>> {
124 None
125 }
126 fn validation_error(&self) -> Option<String> {
130 None
131 }
132 fn stretch_in_row(&self) -> bool {
134 false
135 }
136 fn focusable(&self) -> bool {
140 self.props().flag(Prop::Focus)
141 }
142 fn enter(&mut self, forward: bool) {
144 let _ = forward;
145 }
146 fn ring(&self, out: &mut Vec<Slot>) {
148 if self.focusable() {
149 out.push(self.slot());
150 }
151 for child in self.children().iter().filter(|child| child.visible) {
152 child.comp.ring(out);
153 }
154 }
155 fn key(&mut self, ec: &mut EventCtx<'_>, key: Key) -> Flow {
157 let _ = (ec, key);
158 Flow::Skip
159 }
160 fn mouse(
162 &mut self,
163 ec: &mut EventCtx<'_>,
164 tag: HitTag,
165 at: (u16, u16),
166 rect: Rect,
167 mouse: Mouse,
168 ) -> Flow {
169 let _ = (ec, tag, at, rect, mouse);
170 Flow::Skip
171 }
172 fn paste(&mut self, ec: &mut EventCtx<'_>, text: &str) -> Flow {
176 let _ = (ec, text);
177 Flow::Skip
178 }
179 fn paste_raw(&mut self, ec: &mut EventCtx<'_>, text: &str) -> Flow {
184 self.paste(ec, text)
185 }
186 fn value(&self, out: &mut serde_json::Map<String, serde_json::Value>) {
188 let _ = out;
189 }
190 fn set_text(&mut self, ctx: &UiContext, text: Str) -> bool {
192 let _ = (ctx, text);
193 false
194 }
195}
196impl dyn Component {
197 pub(crate) fn is<T: Component>(&self) -> bool {
198 (self as &dyn Any).is::<T>()
199 }
200
201 #[cfg(test)]
202 pub(crate) fn downcast_ref<T: Component>(&self) -> Option<&T> {
203 (self as &dyn Any).downcast_ref()
204 }
205
206 pub(crate) fn downcast_mut<T: Component>(&mut self) -> Option<&mut T> {
207 (self as &mut dyn Any).downcast_mut()
208 }
209}
210
211#[derive(Clone, Copy, PartialEq, Eq)]
219pub struct MemoKey {
220 version: u64,
221 width_epoch: u64,
222 revision: u64,
223}
224
225impl MemoKey {
226 pub(crate) fn new(version: u64, ctx: &UiContext) -> Self {
228 Self { version, width_epoch: crate::rich::width_config_epoch(), revision: ctx.revision }
229 }
230}
231
232pub struct Cached {
235 comp: Box<dyn Component>,
236 pub rect: Rect,
238 pub visible: bool,
240 version: u64,
241 measured: Option<(MemoKey, (u16, u16))>,
242 laid: Option<(MemoKey, u16, u16)>,
243 anim: Option<Box<AnimState>>,
244}
245
246impl Cached {
247 pub fn new(comp: Box<dyn Component>) -> Self {
249 Self {
250 comp,
251 rect: Rect::new(0, 0, 0, 0),
252 visible: true,
253 version: 0,
254 measured: None,
255 laid: None,
256 anim: None,
257 }
258 }
259
260 pub fn measure(&mut self, ctx: &UiContext) -> (u16, u16) {
266 let key = MemoKey::new(self.version, ctx);
267 if let Some((cached, measured)) = self.measured
268 && cached == key
269 {
270 return measured;
271 }
272 let (mut min, mut nat) = self.comp.measure(ctx);
273 let extra = horizontal_inset(self.comp.props(), self.comp.paints_border()).saturating_mul(2);
274 min = min.saturating_add(extra);
275 nat = nat.saturating_add(extra).max(min);
276 let measured = (min, nat);
277 self.measured = Some((key, measured));
278 measured
279 }
280
281 pub fn height(&mut self, ctx: &UiContext, width: u16) -> u16 {
283 let key = MemoKey::new(self.version, ctx);
284 if let Some((cached, laid_width, height)) = self.laid
285 && cached == key
286 && laid_width == width
287 {
288 return height;
289 }
290 let fixed = self.sampled_h(ctx);
291 let paints_border = self.comp.paints_border();
292 let x_inset = horizontal_inset(self.comp.props(), paints_border);
293 let y_inset = vertical_inset(self.comp.props(), paints_border);
294 let height = if let Some(fixed) = fixed {
295 fixed
296 } else {
297 let content_width = width.saturating_sub(x_inset.saturating_mul(2));
298 let minimum = self
299 .measure(ctx)
300 .0
301 .saturating_sub(x_inset.saturating_mul(2));
302 self
303 .comp
304 .height(ctx, content_width.max(minimum).max(1))
305 .saturating_add(y_inset.saturating_mul(2))
306 };
307 let height = height.saturating_add(self.comp.props().lift());
309 if self.size_settled(ctx.now) {
310 self.laid = Some((key, width, height));
311 }
312 height
313 }
314
315 pub fn place(&mut self, ctx: &UiContext, rect: Rect) {
320 self.rect = rect;
321 let props = self.comp.props();
322 let chrome = lifted_rect(rect, props.lift(), 0);
323 let content = content_rect(chrome, props, self.comp.paints_border());
324 self.comp.place(ctx, content);
325 }
326
327 pub fn paint(&mut self, pc: &mut PaintCtx<'_>) {
339 let own = self.comp.slot();
340 let decorated = self.comp.props().hover_decorated();
341 let pointer_hovered = decorated && pc.hover.is_some_and(|(slot, _)| self.contains_slot(slot));
342 let hovered = pointer_hovered || (decorated && pc.keyboard && pc.focus == Some(own));
343 let mut glow = if pointer_hovered {
344 self.border_glow(pc)
345 } else if hovered {
346 self.focus_glow(pc)
347 } else {
348 None
349 };
350 let hover_swap = if hovered && glow.is_none() {
351 self.swap_hover_chrome()
352 } else {
353 None
354 };
355 let anim = self.begin_paint(pc.ctx, pc.now);
356 let chrome_anim = anim
357 .as_ref()
358 .map_or_else(ChromeAnim::default, |paint| paint.chrome);
359 let rect = self.rect;
360 if decorated {
361 pc.hits.push(Hit { rect, slot: own, tag: HitTag::Zone });
364 }
365 let lift = self.comp.props().lift();
366 let (risen, rise) = if lift == 0 {
367 (0, f32::from(u8::from(hovered)))
368 } else {
369 self.lift_rise(pc, hovered, lift)
370 };
371 let chrome = lifted_rect(rect, lift, risen);
372 if let Some(glow) = glow.as_mut()
373 && glow.focus
374 {
375 glow.strength = self.focus_bloom(pc);
379 glow.pointer =
380 (chrome.x.saturating_add(chrome.width / 2), chrome.y.saturating_add(chrome.height / 2));
381 } else {
382 if let Some(glow) = glow.as_mut() {
383 glow.strength = rise;
385 }
386 if let Some(state) = self.anim.as_deref_mut() {
387 state.bloom = None;
390 }
391 }
392 let paints_border = self.comp.paints_border();
393 if lift > 0 {
394 self
396 .comp
397 .place(pc.ctx, content_rect(chrome, self.comp.props(), paints_border));
398 }
399 let props = self.comp.props();
400 if props.border().is_some() && paints_border {
401 paint_border(pc, chrome, props, chrome_anim, glow);
402 }
403 let content = content_rect(chrome, props, paints_border);
404 let outer_clip = pc.clip;
409 if props.h().is_some() {
410 pc.clip = pc.clip.min(content.y.saturating_add(content.height));
411 }
412 self.comp.paint(pc, content);
413 pc.clip = outer_clip;
414 paint_gradients(
415 pc,
416 chrome,
417 self.comp.gradient_bounds(content),
418 self.comp.props(),
419 paints_border,
420 self.comp.paints_background(),
421 chrome_anim,
422 );
423 if risen > 0 {
424 paint_lift_shadow(pc, chrome, rect);
425 }
426 if glow.is_some() && pointer_hovered {
427 pc.wake(own, pc.now.saturating_add(anim::FRAME));
431 }
432 if let Some(anim) = anim {
433 self.end_paint(pc, anim);
434 }
435 if let Some((prop, displaced)) = hover_swap {
436 match displaced {
437 Some(value) => self.comp.props_mut().set(prop, value),
438 None => self.comp.props_mut().unset(prop),
439 }
440 }
441 }
442
443 pub const fn invalidate(&mut self) {
445 self.version = self.version.wrapping_add(1);
446 self.measured = None;
447 self.laid = None;
448 }
449
450 pub fn update<R>(&mut self, slot: Slot, f: impl FnOnce(&mut Self) -> (R, bool)) -> Option<R> {
453 let mut f = Some(f);
454 self
455 .update_where(&|cached| cached.comp.slot() == slot, &mut f)
456 .map(|(value, _)| value)
457 }
458
459 pub fn update_id<R>(&mut self, id: &str, f: impl FnOnce(&mut Self) -> (R, bool)) -> Option<R> {
462 let mut f = Some(f);
463 self
464 .update_where(
465 &|cached| {
466 cached
467 .comp
468 .props()
469 .id()
470 .is_some_and(|candidate| candidate == id)
471 },
472 &mut f,
473 )
474 .map(|(value, _)| value)
475 }
476
477 fn update_where<R, P, F>(&mut self, predicate: &P, f: &mut Option<F>) -> Option<(R, bool)>
478 where
479 P: Fn(&Self) -> bool,
480 F: FnOnce(&mut Self) -> (R, bool),
481 {
482 if predicate(self) {
483 let (value, dirty) = f.take().expect("update closure reused")(self);
484 if dirty {
485 self.invalidate();
486 }
487 return Some((value, dirty));
488 }
489 let result = self
490 .comp
491 .children_mut()
492 .iter_mut()
493 .find_map(|child| child.update_where(predicate, f));
494 if result.as_ref().is_some_and(|(_, dirty)| *dirty) {
495 self.invalidate();
496 }
497 result
498 }
499
500 pub fn find_slot(&mut self, slot: Slot) -> Option<&mut Self> {
502 if self.comp.slot() == slot {
503 return Some(self);
504 }
505 for child in self.comp.children_mut() {
506 if let Some(found) = child.find_slot(slot) {
507 return Some(found);
508 }
509 }
510 None
511 }
512
513 pub fn comp(&self) -> &dyn Component {
515 self.comp.as_ref()
516 }
517
518 pub fn comp_mut(&mut self) -> &mut dyn Component {
521 self.comp.as_mut()
522 }
523
524 pub(crate) fn into_comp(self) -> Box<dyn Component> {
526 self.comp
527 }
528
529 pub(crate) fn fill_style(&mut self, ctx: &UiContext, now: Duration) -> Style {
532 let paint = self.begin_paint(ctx, now);
533 let style = self.comp.props().style(&ctx.theme);
534 if let Some(paint) = paint {
535 self.restore_props(paint.saved);
536 }
537 style
538 }
539
540 pub(crate) fn w(&mut self, ctx: &UiContext) -> Option<Dim> {
543 let target = self.comp.props().w();
544 let Some((duration, easing)) = self.anim_spec() else {
545 return target;
546 };
547 let state = self.anim.get_or_insert_default();
548 let Some(target) = target else {
549 state.w = None;
550 return None;
551 };
552 let (pct, goal) = match target {
553 Dim::Pct(percent) => (true, u16::from(percent)),
554 Dim::Cells(cells) => (false, cells),
555 };
556 let tween = match &mut state.w {
557 Some((unit, tween)) if *unit == pct => tween,
558 slot => &mut slot.insert((pct, Tween::settled(goal))).1,
559 };
560 tween.retarget(ctx.now, goal, duration, easing);
561 let sampled = tween.sample(ctx.now);
562 Some(if pct {
563 Dim::Pct(sampled.min(100) as u8)
564 } else {
565 Dim::Cells(sampled)
566 })
567 }
568
569 fn sampled_h(&mut self, ctx: &UiContext) -> Option<u16> {
571 let target = self.comp.props().h();
572 let Some((duration, easing)) = self.anim_spec() else {
573 return target;
574 };
575 let state = self.anim.get_or_insert_default();
576 let Some(target) = target else {
577 state.h = None;
578 return None;
579 };
580 let tween = state.h.get_or_insert_with(|| Tween::settled(target));
581 tween.retarget(ctx.now, target, duration, easing);
582 Some(tween.sample(ctx.now))
583 }
584
585 fn size_settled(&self, now: Duration) -> bool {
588 self.anim.as_deref().is_none_or(|state| {
589 state.h.is_none_or(|tween| tween.is_settled(now))
590 && state.w.is_none_or(|(_, tween)| tween.is_settled(now))
591 })
592 }
593
594 fn anim_spec(&self) -> Option<(Duration, Easing)> {
596 let props = self.comp.props();
597 Some((props.anim()?, props.ease()))
598 }
599
600 fn begin_paint(&mut self, ctx: &UiContext, now: Duration) -> Option<PaintAnim> {
604 let props = self.comp.props();
605 let spec = props.anim().map(|duration| (duration, props.ease()));
606 let spin = props.spin();
607 if spec.is_none() && spin.is_none() && self.anim.is_none() {
608 return None;
609 }
610 if spec.is_some() {
611 let _ = self.sampled_h(ctx);
615 let _ = self.w(ctx);
616 }
617 let props = self.comp.props();
618 let mut paint = PaintAnim::default();
619
620 if let Some(period) = spin
623 && (props.gradient_of(Prop::Fg).is_some()
624 || props.gradient_of(Prop::Bg).is_some()
625 || props.gradient_of(Prop::On).is_some()
626 || props.gradient_of(bc_slot(props)).is_some())
627 {
628 let nanos = period.as_nanos().max(1);
629 paint.chrome.angle = ((now.as_nanos() % nanos) * 360 / nanos) as u16;
630 let step = Duration::from_nanos((nanos / 360) as u64).max(anim::FRAME);
631 paint.merge_wake(now.saturating_add(step));
632 }
633
634 if let Some((duration, easing)) = spec {
635 let bg_prop = if props.get(Prop::Bg).is_some() {
636 Prop::Bg
637 } else {
638 Prop::On
639 };
640 let bc_prop = bc_slot(props);
641 let fg_target = color_target(ctx, props, Prop::Fg);
642 let bg_target = color_target(ctx, props, bg_prop);
643 let bc_target = color_target(ctx, props, bc_prop);
644 let state = self.anim.get_or_insert_default();
645 state.fg.retarget(now, fg_target, duration, easing);
646 state.bg.retarget(now, bg_target, duration, easing);
647 state.bc.retarget(now, bc_target, duration, easing);
648 paint.chrome.fg = paint.apply(self.comp.as_mut(), &state.fg, Prop::Fg, now);
649 paint.chrome.bg = paint.apply(self.comp.as_mut(), &state.bg, bg_prop, now);
650 paint.chrome.bc = paint.apply(self.comp.as_mut(), &state.bc, bc_prop, now);
651 for settles in
652 [state.h.map(|tween| tween.settles_at()), state.w.map(|(_, tween)| tween.settles_at())]
653 .into_iter()
654 .flatten()
655 .filter(|&settles| settles > now)
656 {
657 paint.relayout = true;
658 paint.merge_wake(settles.min(now.saturating_add(anim::FRAME)));
659 }
660 } else {
661 self.anim = None;
663 }
664
665 if paint.wake.is_none() {
666 None
667 } else {
668 Some(paint)
669 }
670 }
671
672 fn end_paint(&mut self, pc: &mut PaintCtx<'_>, paint: PaintAnim) {
674 let PaintAnim { saved, wake, relayout, .. } = paint;
675 self.restore_props(saved);
676 if let Some(at) = wake {
677 let slot = self.comp.slot();
678 if relayout {
679 pc.wake_layout(slot, at);
680 } else {
681 pc.wake(slot, at);
682 }
683 }
684 }
685
686 fn restore_props(&mut self, saved: SmallVec<(Prop, PropValue), 3>) {
687 for (prop, value) in saved {
688 self.comp.props_mut().set(prop, value);
689 }
690 }
691
692 pub(crate) fn contains_slot(&self, slot: Slot) -> bool {
694 self.comp.slot() == slot
695 || self
696 .comp
697 .children()
698 .iter()
699 .any(|child| child.contains_slot(slot))
700 }
701
702 fn swap_hover_chrome(&mut self) -> Option<(Prop, Option<PropValue>)> {
707 let props = self.comp.props();
708 let hover = props.get(Prop::Hover).cloned()?;
709 let slot = bc_slot(props);
710 let displaced = props.get(slot).cloned();
711 self.comp.props_mut().set(slot, hover);
712 Some((slot, displaced))
713 }
714
715 fn border_glow(&self, pc: &PaintCtx<'_>) -> Option<BorderGlow> {
718 let pointer = pc.pointer?;
719 let (start, end) = self.hover_ramp(pc)?;
720 Some(BorderGlow { pointer, start, end, strength: 1.0, focus: false })
721 }
722
723 fn focus_glow(&self, pc: &PaintCtx<'_>) -> Option<BorderGlow> {
727 let (start, end) = self.hover_ramp(pc)?;
728 Some(BorderGlow { pointer: (0, 0), start, end, strength: 1.0, focus: true })
729 }
730
731 fn hover_ramp(&self, pc: &PaintCtx<'_>) -> Option<(Color, Color)> {
733 let value = self.comp.props().gradient_of(Prop::Hover)?;
734 let (start, end) = value.split_once("..")?;
735 let resolve = |color: &str| pc.ctx.theme.token(color).or_else(|| Color::parse(color));
736 Some((resolve(start)?, resolve(end)?))
737 }
738
739 fn lift_rise(&mut self, pc: &mut PaintCtx<'_>, hovered: bool, lift: u16) -> (u16, f32) {
746 let target = if hovered { f32::from(lift) } else { 0.0 };
747 let Some((duration, easing)) = self.anim_spec() else {
748 return if hovered { (lift, 1.0) } else { (0, 0.0) };
749 };
750 let (duration, easing) = if pc.keyboard {
751 ((duration / 2).min(KEY_SNAP), Easing::EaseOut)
752 } else {
753 (duration, easing)
754 };
755 let state = self.anim.get_or_insert_default();
756 let tween = state.lift.get_or_insert_with(|| Tween::settled(0.0));
757 tween.retarget(pc.now, target, duration, easing);
758 let sample = tween.sample(pc.now).clamp(0.0, f32::from(lift));
759 if !tween.is_settled(pc.now) {
760 let at = tween.settles_at().min(pc.now.saturating_add(anim::FRAME));
761 pc.wake(self.comp.slot(), at);
762 }
763 (sample.round() as u16, sample / f32::from(lift))
764 }
765
766 fn focus_bloom(&mut self, pc: &mut PaintCtx<'_>) -> f32 {
771 let Some((duration, easing)) = self.anim_spec() else {
772 return 1.0;
773 };
774 let state = self.anim.get_or_insert_default();
775 let tween = state.bloom.get_or_insert_with(|| Tween::settled(0.0));
776 tween.retarget(pc.now, 1.0, duration, easing);
777 let sample = tween.sample(pc.now);
778 if !tween.is_settled(pc.now) {
779 let at = tween.settles_at().min(pc.now.saturating_add(anim::FRAME));
780 pc.wake(self.comp.slot(), at);
781 }
782 sample
783 }
784}
785
786#[derive(Default)]
794struct AnimState {
795 fg: Channel,
796 bg: Channel,
797 bc: Channel,
798 w: Option<(bool, Tween<u16>)>,
801 h: Option<Tween<u16>>,
802 lift: Option<Tween<f32>>,
804 bloom: Option<Tween<f32>>,
806}
807
808#[derive(Clone, Copy, Default)]
813enum Channel {
814 #[default]
816 Empty,
817 Solid(Tween<Color>),
818 Ramp(Tween<(Color, Color)>),
819}
820
821impl Channel {
822 fn retarget(
824 &mut self,
825 now: Duration,
826 target: ChannelTarget,
827 duration: Duration,
828 easing: Easing,
829 ) {
830 match (self, target) {
831 (Self::Solid(tween), ChannelTarget::Solid(color)) => {
832 tween.retarget(now, color, duration, easing);
833 },
834 (Self::Ramp(tween), ChannelTarget::Ramp(start, end)) => {
835 tween.retarget(now, (start, end), duration, easing);
836 },
837 (slot, ChannelTarget::None) => *slot = Self::Empty,
838 (slot, ChannelTarget::Solid(color)) => *slot = Self::Solid(Tween::settled(color)),
839 (slot, ChannelTarget::Ramp(start, end)) => {
840 *slot = Self::Ramp(Tween::settled((start, end)));
841 },
842 }
843 }
844}
845
846#[derive(Clone, Copy)]
848enum ChannelTarget {
849 None,
851 Solid(Color),
852 Ramp(Color, Color),
853}
854
855fn color_target(ctx: &UiContext, props: &Props, prop: Prop) -> ChannelTarget {
858 match props.get(prop) {
859 Some(PropValue::Color(color)) => ChannelTarget::Solid(*color),
860 Some(PropValue::Token(token)) => ctx
861 .theme
862 .token(token)
863 .map_or(ChannelTarget::None, ChannelTarget::Solid),
864 Some(PropValue::Gradient(value)) => {
865 let resolve = |color: &str| ctx.theme.token(color).or_else(|| Color::parse(color));
866 value
867 .split_once("..")
868 .and_then(|(start, end)| Some((resolve(start)?, resolve(end)?)))
869 .map_or(ChannelTarget::None, |(start, end)| ChannelTarget::Ramp(start, end))
870 },
871 _ => ChannelTarget::None,
872 }
873}
874
875#[derive(Clone, Copy, Default)]
877pub struct ChromeAnim {
878 fg: Option<(Color, Color)>,
880 bg: Option<(Color, Color)>,
882 bc: Option<(Color, Color)>,
884 angle: u16,
886}
887
888#[derive(Default)]
891struct PaintAnim {
892 saved: SmallVec<(Prop, PropValue), 3>,
893 chrome: ChromeAnim,
894 wake: Option<Duration>,
895 relayout: bool,
896}
897
898impl PaintAnim {
899 fn apply(
902 &mut self,
903 comp: &mut dyn Component,
904 channel: &Channel,
905 prop: Prop,
906 now: Duration,
907 ) -> Option<(Color, Color)> {
908 match channel {
909 Channel::Empty => None,
910 Channel::Solid(tween) => {
911 if !tween.is_settled(now)
912 && let Some(saved) = comp.props().get(prop).cloned()
913 {
914 self.merge_wake(tween.settles_at().min(now.saturating_add(anim::FRAME)));
915 comp.props_mut().set(prop, tween.sample(now));
916 self.saved.push((prop, saved));
917 }
918 None
919 },
920 Channel::Ramp(tween) => {
921 if tween.is_settled(now) {
922 return None;
923 }
924 self.merge_wake(tween.settles_at().min(now.saturating_add(anim::FRAME)));
925 Some(tween.sample(now))
926 },
927 }
928 }
929
930 fn merge_wake(&mut self, at: Duration) {
931 self.wake = Some(self.wake.map_or(at, |wake| wake.min(at)));
932 }
933}
934
935pub fn horizontal_inset(props: &Props, paints_border: bool) -> u16 {
936 let (_, pad_x) = props.pad();
937 pad_x.saturating_add(u16::from(paints_border && props.border().is_some()))
938}
939
940pub fn vertical_inset(props: &Props, paints_border: bool) -> u16 {
941 let (pad_y, _) = props.pad();
942 pad_y.saturating_add(u16::from(paints_border && props.border().is_some()))
943}
944
945fn content_rect(rect: Rect, props: &Props, paints_border: bool) -> Rect {
946 let x_inset = horizontal_inset(props, paints_border);
947 let y_inset = vertical_inset(props, paints_border);
948 Rect::new(
949 rect.x.saturating_add(x_inset),
950 rect.y.saturating_add(y_inset),
951 rect.width.saturating_sub(x_inset.saturating_mul(2)),
952 rect.height.saturating_sub(y_inset.saturating_mul(2)),
953 )
954}
955
956fn bc_slot(props: &Props) -> Prop {
958 if props.get(Prop::Bc).is_some() {
959 Prop::Bc
960 } else {
961 Prop::Edge
962 }
963}
964
965fn lifted_rect(rect: Rect, lift: u16, risen: u16) -> Rect {
969 let lift = lift.min(rect.height.saturating_sub(1));
970 Rect::new(rect.x, rect.y.saturating_add(lift - risen.min(lift)), rect.width, rect.height - lift)
971}
972
973fn paint_lift_shadow(pc: &mut PaintCtx<'_>, chrome: Rect, rect: Rect) {
977 let y = chrome.y.saturating_add(chrome.height);
978 let Some(glyph) = pc.ctx.charset.shadow() else {
979 return;
980 };
981 if y >= rect.y.saturating_add(rect.height) || y >= pc.clip || rect.width < 3 {
982 return;
983 }
984 let style = Style::new().fg(pc.ctx.theme.shadow);
985 for x in rect.x.saturating_add(1)..rect.x.saturating_add(rect.width - 1) {
986 pc.frame.put(x, y, glyph, style);
987 }
988}
989
990const KEY_SNAP: Duration = Duration::from_millis(120);
993
994#[derive(Clone, Copy)]
999struct BorderGlow {
1000 pointer: (u16, u16),
1001 start: Color,
1002 end: Color,
1003 strength: f32,
1004 focus: bool,
1007}
1008
1009impl BorderGlow {
1010 fn color_at(self, x: u16, y: u16, rect: Rect, base: Color, phase: f32) -> Option<Color> {
1013 let dx = (f32::from(x) - f32::from(self.pointer.0)) * 0.5;
1015 let dy = f32::from(y) - f32::from(self.pointer.1);
1016 let radius = if self.focus {
1017 let corner_x = f32::from(rect.width) * 0.25;
1020 let corner_y = f32::from(rect.height) * 0.5;
1021 corner_x.hypot(corner_y) * self.strength.mul_add(1.6, 0.4)
1022 } else {
1023 (f32::from(rect.width).mul_add(0.5, f32::from(rect.height)) * 0.3).clamp(2.5, 6.0)
1026 * self.strength.mul_add(0.65, 0.35)
1027 };
1028 let amount = self.strength * (-dx.mul_add(dx, dy * dy) / (radius * radius)).exp();
1029 if amount < 0.02 {
1030 return None;
1031 }
1032 let center_x = f32::from(rect.x) + f32::from(rect.width) / 2.0;
1033 let center_y = f32::from(rect.y) + f32::from(rect.height) / 2.0;
1034 let cell = (f32::from(y) - center_y).atan2((f32::from(x) - center_x) * 0.5);
1035 let cursor =
1036 (f32::from(self.pointer.1) - center_y).atan2((f32::from(self.pointer.0) - center_x) * 0.5);
1037 let mut delta = (cell - cursor).rem_euclid(TAU);
1041 if delta > PI {
1042 delta = TAU - delta;
1043 }
1044 let wave = phase.mul_add(0.15, delta / PI);
1045 let wheel = 1.0 - (1.0 - wave.rem_euclid(2.0)).abs();
1046 Some(base.lerp(self.start.lerp(self.end, wheel), amount.min(1.0)))
1047 }
1048}
1049
1050fn glow_cell(frame: &mut Frame, x: u16, y: u16, rect: Rect, glow: BorderGlow, phase: f32) {
1052 frame.recolor_fg(x, y, |base| glow.color_at(x, y, rect, base, phase).unwrap_or(base));
1053}
1054pub fn paint_gradients(
1055 pc: &mut PaintCtx<'_>,
1056 bounds: Rect,
1057 projection: Option<Rect>,
1058 props: &Props,
1059 paints_border: bool,
1060 paints_background: bool,
1061 chrome: ChromeAnim,
1062) {
1063 let angle = (props.angle() + chrome.angle) % 360;
1064 let bottom = bounds.y.saturating_add(bounds.height).min(pc.clip);
1065 let painted = Rect::new(bounds.x, bounds.y, bounds.width, bottom.saturating_sub(bounds.y));
1066 if paints_background {
1067 let background_bounds = if paints_border && props.border().is_some() && !props.bleed() {
1068 Rect::new(
1069 bounds.x.saturating_add(1),
1070 bounds.y.saturating_add(1),
1071 bounds.width.saturating_sub(2),
1072 bounds.height.saturating_sub(2),
1073 )
1074 } else {
1075 bounds
1076 };
1077 let background_bottom = background_bounds
1078 .y
1079 .saturating_add(background_bounds.height)
1080 .min(pc.clip);
1081 let background = Rect::new(
1082 background_bounds.x,
1083 background_bounds.y,
1084 background_bounds.width,
1085 background_bottom.saturating_sub(background_bounds.y),
1086 );
1087 let bg_prop = if props.get(Prop::Bg).is_some() {
1088 Prop::Bg
1089 } else {
1090 Prop::On
1091 };
1092 let gradient = chrome
1093 .bg
1094 .map(|(start, end)| Gradient::new(start, end, angle))
1095 .or_else(|| resolve_gradient(pc.ctx, props, bg_prop, angle));
1096 if let Some(gradient) = gradient {
1097 pc.frame
1098 .underlay_gradient(background, gradient, projection.unwrap_or(background_bounds));
1099 } else {
1100 let bg = props.style(&pc.ctx.theme).background_color();
1101 if bg != Color::Default {
1102 pc.frame.underlay(background, bg);
1103 }
1104 }
1105 }
1106 let gradient = chrome
1107 .fg
1108 .map(|(start, end)| Gradient::new(start, end, angle))
1109 .or_else(|| resolve_gradient(pc.ctx, props, Prop::Fg, angle));
1110 if let Some(gradient) = gradient {
1111 pc.frame
1112 .gradient_foreground(painted, gradient, projection.unwrap_or(bounds));
1113 }
1114}
1115
1116fn resolve_gradient(ctx: &UiContext, props: &Props, prop: Prop, angle: u16) -> Option<Gradient> {
1117 let value = props.gradient_of(prop)?;
1118 let (start, end) = value.split_once("..")?;
1119 let resolve = |color: &str| ctx.theme.token(color).or_else(|| Color::parse(color));
1120 Some(Gradient::new(resolve(start)?, resolve(end)?, angle))
1121}
1122
1123fn assemble_border_line(
1124 line: &mut SmallVec<u8, 256>,
1125 left: char,
1126 horizontal: char,
1127 right: char,
1128 inner: usize,
1129) {
1130 line.clear();
1131 let mut left_bytes = [0; 4];
1132 line.extend_from_slice(left.encode_utf8(&mut left_bytes).as_bytes());
1133 let mut horizontal_bytes = [0; 4];
1134 let horizontal = horizontal.encode_utf8(&mut horizontal_bytes).as_bytes();
1135 for _ in 0..inner {
1136 line.extend_from_slice(horizontal);
1137 }
1138 let mut right_bytes = [0; 4];
1139 line.extend_from_slice(right.encode_utf8(&mut right_bytes).as_bytes());
1140}
1141
1142fn paint_border(
1143 pc: &mut PaintCtx<'_>,
1144 rect: Rect,
1145 props: &Props,
1146 chrome: ChromeAnim,
1147 glow: Option<BorderGlow>,
1148) {
1149 if rect.width < 2 || rect.height < 2 {
1150 return;
1151 }
1152 let border = props.border().unwrap_or_default();
1153 let (tl, tr, bl, br, horizontal, vertical) = pc.ctx.charset.border(border);
1154 let style = props.style(&pc.ctx.theme);
1155 let base = if props.bleed() {
1156 style
1157 } else {
1158 style.bg(Color::Default)
1159 };
1160 let angle = (props.angle() + chrome.angle) % 360;
1161 let ramp = chrome
1162 .bc
1163 .map(|(start, end)| Gradient::new(start, end, angle))
1164 .or_else(|| resolve_gradient(pc.ctx, props, bc_slot(props), angle));
1165 let edge = if ramp.is_some() {
1170 base.fg(Color::Default)
1171 } else if let Some(color) = props.edge(&pc.ctx.theme) {
1172 base.fg(color)
1173 } else if props.get(Prop::Fg).is_some() {
1174 base.dim()
1175 } else {
1176 base.fg(pc.ctx.theme.border)
1177 };
1178 let inner = usize::from(rect.width) - 2;
1179 assemble_border_line(&mut pc.border_scratch, tl, horizontal, tr, inner);
1180 if rect.y < pc.clip {
1181 let top = std::str::from_utf8(&pc.border_scratch)
1182 .expect("border glyph assembly only appends valid UTF-8");
1183 pc.frame.put(rect.x, rect.y, top, edge);
1184 }
1185 let bottom_y = rect.y.saturating_add(rect.height - 1);
1186 if bottom_y < pc.clip {
1187 assemble_border_line(&mut pc.border_scratch, bl, horizontal, br, inner);
1188 let bottom = std::str::from_utf8(&pc.border_scratch)
1189 .expect("border glyph assembly only appends valid UTF-8");
1190 pc.frame.put(rect.x, bottom_y, bottom, edge);
1191 }
1192 let mut vertical_bytes = [0; 4];
1193 let vertical = vertical.encode_utf8(&mut vertical_bytes);
1194 for y in rect.y.saturating_add(1)..bottom_y.min(pc.clip) {
1195 pc.frame.put(rect.x, y, &*vertical, edge);
1196 pc.frame
1197 .put(rect.x.saturating_add(rect.width - 1), y, &*vertical, edge);
1198 }
1199 if let Some(gradient) = ramp {
1200 let side_top = rect.y.saturating_add(1);
1201 let side_rows = bottom_y.min(pc.clip).saturating_sub(side_top);
1202 let strips = [
1203 (rect.y < pc.clip).then(|| Rect::new(rect.x, rect.y, rect.width, 1)),
1204 (bottom_y < pc.clip).then(|| Rect::new(rect.x, bottom_y, rect.width, 1)),
1205 (side_rows > 0).then(|| Rect::new(rect.x, side_top, 1, side_rows)),
1206 (side_rows > 0)
1207 .then(|| Rect::new(rect.x.saturating_add(rect.width - 1), side_top, 1, side_rows)),
1208 ];
1209 for strip in strips.into_iter().flatten() {
1210 pc.frame.gradient_foreground(strip, gradient, rect);
1211 }
1212 }
1213 if let Some(glow) = glow {
1214 let phase = pc.now.as_secs_f32() * 0.5;
1217 let right = rect.x.saturating_add(rect.width - 1);
1218 if rect.y < pc.clip {
1219 for x in rect.x..=right {
1220 glow_cell(pc.frame, x, rect.y, rect, glow, phase);
1221 }
1222 }
1223 if bottom_y < pc.clip {
1224 for x in rect.x..=right {
1225 glow_cell(pc.frame, x, bottom_y, rect, glow, phase);
1226 }
1227 }
1228 for y in rect.y.saturating_add(1)..bottom_y.min(pc.clip) {
1229 glow_cell(pc.frame, rect.x, y, rect, glow, phase);
1230 glow_cell(pc.frame, right, y, rect, glow, phase);
1231 }
1232 }
1233 if rect.y < pc.clip
1234 && let Some(title) = props.title()
1235 {
1236 border_label(pc, rect, rect.y, title, props.title_align(), base, true);
1237 }
1238 if bottom_y < pc.clip
1239 && let Some(footer) = props.footer()
1240 {
1241 border_label(pc, rect, bottom_y, footer, props.footer_align(), base, false);
1242 }
1243}
1244
1245fn border_label(
1253 pc: &mut PaintCtx<'_>,
1254 rect: Rect,
1255 y: u16,
1256 text: &str,
1257 align: Align,
1258 base: Style,
1259 bold: bool,
1260) {
1261 let fit = rect.width.saturating_sub(4);
1263 if fit == 0 {
1264 return;
1265 }
1266 let mut width: u16 = 0;
1269 let mut end = 0usize;
1270 for grapheme in text.graphemes() {
1271 if grapheme == "\n" || grapheme == "\r" {
1272 break;
1273 }
1274 let cells = u16::try_from(grapheme.visible_width()).unwrap_or(u16::MAX);
1275 if width.saturating_add(cells) > fit {
1276 break;
1277 }
1278 width += cells;
1279 end += grapheme.len();
1280 }
1281 if width == 0 {
1282 return;
1283 }
1284 let text = &text[..end];
1285 let total = width + 2;
1286 let x = match align {
1287 Align::Start => rect.x.saturating_add(2),
1288 Align::Center => rect.x.saturating_add(rect.width.saturating_sub(total) / 2),
1289 Align::End => rect
1290 .x
1291 .saturating_add(rect.width.saturating_sub(2).saturating_sub(total)),
1292 }
1293 .clamp(
1294 rect.x.saturating_add(1),
1295 rect
1296 .x
1297 .saturating_add(rect.width.saturating_sub(1).saturating_sub(total)),
1298 );
1299 let end = pc.frame.put(x, y, " ", base);
1300 let end = pc
1301 .frame
1302 .put(end, y, text, if bold { base.bold() } else { base });
1303 pc.frame.put(end, y, " ", base);
1304}
1305
1306pub struct PaintCtx<'a> {
1308 pub frame: &'a mut Frame,
1310 pub clip: u16,
1312 pub ctx: &'a UiContext,
1314 pub hits: &'a mut Vec<Hit>,
1316 pub focus: Option<Slot>,
1318 pub hover: Option<(Slot, HitTag)>,
1320 pub pointer: Option<(u16, u16)>,
1323 pub keyboard: bool,
1327 pub now: Duration,
1329 pub(crate) wakes: &'a mut Vec<Wake>,
1331 border_scratch: SmallVec<u8, 256>,
1333}
1334
1335#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1337pub struct Wake {
1338 pub slot: Slot,
1339 pub at: Duration,
1340 pub layout: bool,
1343}
1344
1345impl<'a> PaintCtx<'a> {
1346 pub(crate) const fn new(
1351 frame: &'a mut Frame,
1352 ctx: &'a UiContext,
1353 hits: &'a mut Vec<Hit>,
1354 wakes: &'a mut Vec<Wake>,
1355 ) -> Self {
1356 let clip = frame.size().height;
1357 Self {
1358 frame,
1359 clip,
1360 ctx,
1361 hits,
1362 focus: None,
1363 hover: None,
1364 pointer: None,
1365 keyboard: false,
1366 now: Duration::ZERO,
1367 border_scratch: SmallVec::new(),
1368 wakes,
1369 }
1370 }
1371
1372 pub(crate) const fn nested<'b>(&'b mut self, frame: &'b mut Frame, clip: u16) -> PaintCtx<'b> {
1376 PaintCtx {
1377 frame,
1378 clip,
1379 ctx: self.ctx,
1380 hits: self.hits,
1381 focus: self.focus,
1382 hover: self.hover,
1383 pointer: self.pointer,
1384 keyboard: self.keyboard,
1385 now: self.now,
1386 border_scratch: SmallVec::new(),
1387 wakes: self.wakes,
1388 }
1389 }
1390
1391 pub fn wake(&mut self, slot: Slot, at: Duration) {
1395 self.request(slot, at, false);
1396 }
1397
1398 pub(crate) fn wake_layout(&mut self, slot: Slot, at: Duration) {
1401 self.request(slot, at, true);
1402 }
1403
1404 fn request(&mut self, slot: Slot, at: Duration, layout: bool) {
1405 match self.wakes.iter_mut().find(|wake| wake.slot == slot) {
1406 Some(wake) => {
1407 wake.at = wake.at.min(at);
1408 wake.layout |= layout;
1409 },
1410 None => self.wakes.push(Wake { slot, at, layout }),
1411 }
1412 }
1413}
1414
1415pub struct EventCtx<'a> {
1417 pub ctx: &'a UiContext,
1419 pub width: u16,
1421 pub view_rows: u16,
1423 pub(crate) layout: bool,
1426}
1427
1428impl<'a> EventCtx<'a> {
1429 pub const fn new(ctx: &'a UiContext, width: u16, view_rows: u16) -> Self {
1431 Self { ctx, width, view_rows, layout: false }
1432 }
1433
1434 pub const fn request_layout(&mut self) {
1440 self.layout = true;
1441 }
1442}
1443
1444pub trait IntoComponent {
1446 fn into_component(self) -> Box<dyn Component>;
1448}
1449
1450impl<T: Component + 'static> IntoComponent for T {
1451 fn into_component(self) -> Box<dyn Component> {
1452 Box::new(self)
1453 }
1454}
1455impl IntoComponent for Box<dyn Component> {
1456 fn into_component(self) -> Box<dyn Component> {
1457 self
1458 }
1459}
1460impl IntoComponent for &str {
1461 fn into_component(self) -> Box<dyn Component> {
1462 Box::new(Markdown::text_of(self))
1463 }
1464}
1465impl IntoComponent for String {
1466 fn into_component(self) -> Box<dyn Component> {
1467 Box::new(Markdown::text_of(self))
1468 }
1469}
1470impl IntoComponent for Str {
1471 fn into_component(self) -> Box<dyn Component> {
1472 Box::new(Markdown::text_of(self))
1473 }
1474}
1475
1476pub trait IntoChildren {
1478 fn extend_children(self, out: &mut Vec<Cached>);
1480}
1481
1482impl<T: IntoComponent> IntoChildren for T {
1483 fn extend_children(self, out: &mut Vec<Cached>) {
1484 out.push(Cached::new(self.into_component()));
1485 }
1486}
1487impl IntoChildren for () {
1488 fn extend_children(self, _out: &mut Vec<Cached>) {}
1489}
1490impl<T: IntoChildren> IntoChildren for Option<T> {
1491 fn extend_children(self, out: &mut Vec<Cached>) {
1492 if let Some(children) = self {
1493 children.extend_children(out);
1494 }
1495 }
1496}
1497impl<T: IntoChildren> IntoChildren for Vec<T> {
1498 fn extend_children(self, out: &mut Vec<Cached>) {
1499 for children in self {
1500 children.extend_children(out);
1501 }
1502 }
1503}
1504impl<T: IntoChildren, const N: usize> IntoChildren for [T; N] {
1505 fn extend_children(self, out: &mut Vec<Cached>) {
1506 for children in self {
1507 children.extend_children(out);
1508 }
1509 }
1510}
1511impl<T: IntoChildren, const N: usize> IntoChildren for SmallVec<T, N> {
1512 fn extend_children(self, out: &mut Vec<Cached>) {
1513 for children in self {
1514 children.extend_children(out);
1515 }
1516 }
1517}
1518impl IntoChildren for Cached {
1519 fn extend_children(self, out: &mut Vec<Cached>) {
1520 out.push(self);
1521 }
1522}
1523
1524pub trait ElementFactory: Send + Sync {
1526 fn build(&self, name: &str, props: Props, children: Vec<Cached>) -> Box<dyn Component>;
1528}
1529
1530impl<F> ElementFactory for F
1531where
1532 F: Fn(&str, Props, Vec<Cached>) -> Box<dyn Component> + Send + Sync,
1533{
1534 fn build(&self, name: &str, props: Props, children: Vec<Cached>) -> Box<dyn Component> {
1535 self(name, props, children)
1536 }
1537}
1538
1539#[derive(Clone, Default)]
1541pub struct Elements(Arc<Vec<(Str, Box<dyn ElementFactory>)>>);
1542
1543impl fmt::Debug for Elements {
1544 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1545 formatter
1546 .debug_struct("Elements")
1547 .field("len", &self.0.len())
1548 .finish()
1549 }
1550}
1551
1552impl Elements {
1553 pub fn builder() -> ElementsBuilder {
1555 ElementsBuilder::default()
1556 }
1557
1558 pub(crate) fn get(&self, name: &str) -> Option<&dyn ElementFactory> {
1559 self
1560 .0
1561 .iter()
1562 .find(|(candidate, _)| candidate == name)
1563 .map(|(_, factory)| factory.as_ref())
1564 }
1565
1566 pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
1567 Arc::ptr_eq(&self.0, &other.0)
1568 }
1569}
1570
1571#[derive(Default)]
1573pub struct ElementsBuilder {
1574 factories: Vec<(Str, Box<dyn ElementFactory>)>,
1575}
1576
1577impl ElementsBuilder {
1578 pub fn with(mut self, name: impl Into<Str>, factory: impl ElementFactory + 'static) -> Self {
1580 let name = name.into();
1581 if let Some((_, stored)) = self
1582 .factories
1583 .iter_mut()
1584 .find(|(candidate, _)| candidate == &name)
1585 {
1586 *stored = Box::new(factory);
1587 } else {
1588 self.factories.push((name, Box::new(factory)));
1589 }
1590 self
1591 }
1592
1593 pub fn build(self) -> Elements {
1595 Elements(Arc::new(self.factories))
1596 }
1597}
1598
1599#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1601pub enum HitTag {
1602 Row(u16),
1604 Sub(u16),
1606 Chip(u16),
1608 Press,
1610 Wheel,
1612 Scrollbar,
1615 Zone,
1617}
1618
1619#[derive(Clone, Copy, Debug)]
1621pub struct Hit {
1622 pub rect: Rect,
1624 pub slot: Slot,
1626 pub tag: HitTag,
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632 use std::{cell::Cell, rc::Rc};
1633
1634 use parking_lot::{Mutex, MutexGuard};
1635
1636 use super::*;
1637
1638 struct Probe {
1639 props: Props,
1640 slot: Slot,
1641 children: Vec<Cached>,
1642 measures: Rc<Cell<u32>>,
1643 }
1644
1645 impl Probe {
1646 fn new(measures: Rc<Cell<u32>>, children: Vec<Cached>) -> Self {
1647 Self { props: Props::new(), slot: next_slot(), children, measures }
1648 }
1649 }
1650
1651 static WIDTH_EPOCH: Mutex<()> = Mutex::new(());
1655
1656 fn width_epoch_guard() -> MutexGuard<'static, ()> {
1657 WIDTH_EPOCH.lock()
1658 }
1659
1660 impl Component for Probe {
1661 fn props(&self) -> &Props {
1662 &self.props
1663 }
1664
1665 fn props_mut(&mut self) -> &mut Props {
1666 &mut self.props
1667 }
1668
1669 fn slot(&self) -> Slot {
1670 self.slot
1671 }
1672
1673 fn children(&self) -> &[Cached] {
1674 &self.children
1675 }
1676
1677 fn children_mut(&mut self) -> &mut [Cached] {
1678 &mut self.children
1679 }
1680
1681 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
1682 self.measures.set(self.measures.get() + 1);
1683 (1, 2)
1684 }
1685
1686 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
1687 1
1688 }
1689
1690 fn paint(&mut self, _pc: &mut PaintCtx<'_>, _rect: Rect) {}
1691 }
1692
1693 #[test]
1694 fn dirty_update_invalidates_only_ancestor_path() {
1695 let _epoch = width_epoch_guard();
1696 let target_count = Rc::new(Cell::new(0));
1697 let sibling_count = Rc::new(Cell::new(0));
1698 let root_count = Rc::new(Cell::new(0));
1699 let target = Cached::new(Box::new(Probe::new(target_count.clone(), Vec::new())));
1700 let target_slot = target.comp().slot();
1701 let sibling = Cached::new(Box::new(Probe::new(sibling_count.clone(), Vec::new())));
1702 let mut root = Cached::new(Box::new(Probe::new(root_count.clone(), vec![target, sibling])));
1703 let ctx = UiContext::default();
1704 root.measure(&ctx);
1705 root.height(&ctx, 8);
1706 for child in root.comp.children_mut() {
1707 child.measure(&ctx);
1708 child.height(&ctx, 8);
1709 }
1710 root.update(target_slot, |_| ((), true)).unwrap();
1711 assert!(root.measured.is_none());
1712 assert!(root.laid.is_none());
1713 let children = root.comp.children();
1714 assert!(children[0].measured.is_none());
1715 assert!(children[0].laid.is_none());
1716 assert!(children[1].measured.is_some());
1717 assert!(children[1].laid.is_some());
1718 root.measure(&ctx);
1719 root.height(&ctx, 8);
1720 root.comp.children_mut()[0].measure(&ctx);
1721 root.comp.children_mut()[0].height(&ctx, 8);
1722 root.comp.children_mut()[1].measure(&ctx);
1723 root.comp.children_mut()[1].height(&ctx, 8);
1724 assert_eq!(root_count.get(), 2);
1725 assert_eq!(target_count.get(), 2);
1726 assert_eq!(sibling_count.get(), 1);
1727
1728 root.update(target_slot, |_| ((), false)).unwrap();
1729 assert!(root.measured.is_some());
1730 assert!(root.laid.is_some());
1731 assert!(root.comp.children()[0].measured.is_some());
1732 assert!(root.comp.children()[0].laid.is_some());
1733 assert!(root.comp.children()[1].measured.is_some());
1734 assert!(root.comp.children()[1].laid.is_some());
1735 }
1736
1737 #[test]
1738 fn into_children_flattens_supported_inputs() {
1739 let mut children = Vec::new();
1740 ().extend_children(&mut children);
1741 Some("one").extend_children(&mut children);
1742 vec!["two", "three"].extend_children(&mut children);
1743 ["four", "five"].extend_children(&mut children);
1744 assert_eq!(children.len(), 5);
1745 }
1746
1747 #[test]
1748 fn elements_builder_resolves_registered_factory() {
1749 let elements = Elements::builder()
1750 .with("card", |_name: &str, _props: Props, _children: Vec<Cached>| {
1751 Box::new(Markdown::text_of("made")) as Box<dyn Component>
1752 })
1753 .build();
1754 let mut built = elements
1755 .get("card")
1756 .unwrap()
1757 .build("card", Props::new(), Vec::new());
1758 assert!(built.measure(&UiContext::default()).1 > 0);
1759 assert!(elements.get("missing").is_none());
1760 }
1761 #[test]
1762 fn width_epoch_invalidates_cached_measurement() {
1763 let _epoch = width_epoch_guard();
1764 let original = crate::rich::jamo_width();
1765 let next = if original == crate::context::JamoWidth::Narrow {
1766 crate::context::JamoWidth::Wide
1767 } else {
1768 crate::context::JamoWidth::Narrow
1769 };
1770 let measures = Rc::new(Cell::new(0));
1771 let mut cached = Cached::new(Box::new(Probe::new(measures.clone(), Vec::new())));
1772 let ctx = UiContext::default();
1773
1774 assert_eq!(cached.measure(&ctx), (1, 2));
1775 assert_eq!(cached.measure(&ctx), (1, 2));
1776 assert_eq!(measures.get(), 1);
1777
1778 assert!(crate::rich::set_jamo_width(next));
1779 assert_eq!(cached.measure(&ctx), (1, 2));
1780 assert_eq!(measures.get(), 2);
1781
1782 crate::rich::set_jamo_width(original);
1783 }
1784}