1use rustc_hash::FxHashMap;
2use std::any::TypeId;
3use std::cell::{Cell, RefCell};
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use crate::app::context::SurfaceMode;
8use crate::app::input::command_registry::CommandEntry;
9use crate::app::input::command_registry::CommandRegistry;
10use crate::callback::{CancellationToken, CommandLink, CommandTx, Dispatcher, Link, ScopeId};
11use crate::core::context_value::ContextValue;
12use crate::core::element::{Element, Key};
13use crate::core::event::KeyEvent;
14use crate::core::node::{NodeId, NodeKind, NodeTree};
15use crate::core::runtime_env::{
16 DevToolsRequest, MemoDependency, MemoDependencySnapshot, RuntimeEnv, TranscriptEntry,
17};
18use crate::style::{HostTerminalColors, Rect, RichText, Theme, ThemeExtension};
19
20#[non_exhaustive]
25pub struct Command {
26 action: Box<dyn CommandAction>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum TaskPolicy {
32 QueueAll,
34 DropIfRunning,
36 LatestOnly,
38}
39
40mod task_policy;
41
42use task_policy::Task;
43
44#[cfg(not(target_arch = "wasm32"))]
45mod executor_native;
46#[cfg(target_arch = "wasm32")]
47mod executor_wasm;
48
49#[cfg(not(target_arch = "wasm32"))]
50use executor_native::TaskExecutor;
51#[cfg(target_arch = "wasm32")]
52use executor_wasm::TaskExecutor;
53
54impl std::fmt::Debug for Command {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("Command").finish_non_exhaustive()
57 }
58}
59
60impl Command {
61 pub fn new(action: impl FnOnce() + 'static) -> Self {
63 Self {
64 action: Box::new(RunAction(Some(action))),
65 }
66 }
67
68 pub fn spawn<Msg, F>(f: F) -> Self
70 where
71 Msg: Send + 'static,
72 F: FnOnce(CommandLink<Msg>) + Send + 'static,
73 {
74 Self {
75 action: Box::new(SpawnAction::<Msg, F> {
76 f: Some(f),
77 _marker: PhantomData,
78 }),
79 }
80 }
81
82 pub fn spawn_keyed<Msg, F>(key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Self
84 where
85 Msg: Send + 'static,
86 F: FnOnce(CommandLink<Msg>) + Send + 'static,
87 {
88 Self {
89 action: Box::new(SpawnKeyedAction::<Msg, F> {
90 key: key.into(),
91 policy,
92 f: Some(f),
93 _marker: PhantomData,
94 }),
95 }
96 }
97
98 pub(crate) fn run(self, runtime: CommandRuntime) {
99 self.action.run(runtime);
100 }
101}
102
103pub(crate) struct CommandRuntime {
104 pub(crate) scope: ScopeId,
105 pub(crate) tx: CommandTx,
106}
107
108trait CommandAction {
109 fn run(self: Box<Self>, runtime: CommandRuntime);
110}
111
112struct RunAction<F>(Option<F>);
113
114impl<F> CommandAction for RunAction<F>
115where
116 F: FnOnce() + 'static,
117{
118 fn run(mut self: Box<Self>, _runtime: CommandRuntime) {
119 if let Some(f) = self.0.take() {
120 f();
121 }
122 }
123}
124
125struct SpawnAction<Msg, F> {
126 f: Option<F>,
127 _marker: PhantomData<fn(Msg)>,
128}
129
130struct SpawnKeyedAction<Msg, F> {
131 key: Arc<str>,
132 policy: TaskPolicy,
133 f: Option<F>,
134 _marker: PhantomData<fn(Msg)>,
135}
136
137impl<Msg, F> CommandAction for SpawnAction<Msg, F>
138where
139 Msg: Send + 'static,
140 F: FnOnce(CommandLink<Msg>) + Send + 'static,
141{
142 fn run(mut self: Box<Self>, runtime: CommandRuntime) {
143 let Some(f) = self.f.take() else {
144 return;
145 };
146
147 let token = CancellationToken::default();
148 let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
149 TaskExecutor::global().execute(Task::with_token(move || f(link), token));
150 }
151}
152
153impl<Msg, F> CommandAction for SpawnKeyedAction<Msg, F>
154where
155 Msg: Send + 'static,
156 F: FnOnce(CommandLink<Msg>) + Send + 'static,
157{
158 fn run(mut self: Box<Self>, runtime: CommandRuntime) {
159 let Some(f) = self.f.take() else {
160 return;
161 };
162
163 let key = Arc::clone(&self.key);
164 let policy = self.policy;
165 let token = CancellationToken::default();
166 let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
167 TaskExecutor::global().execute_keyed(key, policy, Task::with_token(move || f(link), token));
168 }
169}
170
171impl<Msg: 'static> Link<Msg> {
172 pub fn command<F>(&self, f: F) -> Command
174 where
175 Msg: Send + 'static,
176 F: FnOnce(CommandLink<Msg>) + Send + 'static,
177 {
178 Command::spawn::<Msg, F>(f)
179 }
180
181 pub fn command_keyed<F>(&self, key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Command
183 where
184 Msg: Send + 'static,
185 F: FnOnce(CommandLink<Msg>) + Send + 'static,
186 {
187 Command::spawn_keyed::<Msg, F>(key, policy, f)
188 }
189}
190
191#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
195pub(crate) enum UpdateLevel {
196 #[default]
197 None,
198 Paint,
199 Layout,
200 Full,
201}
202
203pub struct Update {
205 pub dirty: bool,
207 pub(crate) level: UpdateLevel,
209 pub command: Option<Command>,
211}
212
213impl Update {
214 pub fn paint() -> Self {
216 Self {
217 dirty: true,
218 level: UpdateLevel::Paint,
219 command: None,
220 }
221 }
222
223 pub fn layout() -> Self {
225 Self {
226 dirty: true,
227 level: UpdateLevel::Layout,
228 command: None,
229 }
230 }
231
232 pub fn full() -> Self {
234 Self {
235 dirty: true,
236 level: UpdateLevel::Full,
237 command: None,
238 }
239 }
240
241 pub fn command_only(command: Command) -> Self {
243 Self {
244 dirty: false,
245 level: UpdateLevel::None,
246 command: Some(command),
247 }
248 }
249
250 pub fn none() -> Self {
252 Self {
253 dirty: false,
254 level: UpdateLevel::None,
255 command: None,
256 }
257 }
258
259 pub fn with_command(command: impl Into<Option<Command>>) -> Self {
261 match command.into() {
262 Some(cmd) => Self {
263 dirty: true,
264 level: UpdateLevel::Full,
265 command: Some(cmd),
266 },
267 None => Self::full(),
268 }
269 }
270
271 pub(crate) fn level(&self) -> UpdateLevel {
272 self.level
273 }
274}
275
276pub struct KeyUpdate {
280 pub handled: bool,
282 pub update: Update,
284}
285
286impl KeyUpdate {
287 pub fn handled(update: Update) -> Self {
289 Self {
290 handled: true,
291 update,
292 }
293 }
294
295 pub fn unhandled(update: Update) -> Self {
297 Self {
298 handled: false,
299 update,
300 }
301 }
302}
303
304pub trait Component: Sized + 'static {
309 type Message: 'static;
311
312 type Properties: Clone + PartialEq + 'static;
314
315 type State: 'static;
317
318 fn create_state(&self, props: &Self::Properties) -> Self::State;
320
321 fn memo_key(&self, _props: &Self::Properties, _ctx: &Context<Self>) -> Option<u64> {
327 None
328 }
329
330 fn init(&mut self, _ctx: &mut Context<Self>) -> Option<Command> {
334 None
335 }
336
337 fn view(&self, ctx: &Context<Self>) -> Element;
339
340 fn on_key(&mut self, _key: KeyEvent, _ctx: &mut Context<Self>) -> KeyUpdate {
345 KeyUpdate::unhandled(Update::none())
346 }
347
348 fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update;
352
353 fn on_props_changed(
357 &mut self,
358 _old_props: &Self::Properties,
359 _ctx: &mut Context<Self>,
360 ) -> Update {
361 Update::none()
362 }
363
364 fn unmount(&mut self, _ctx: &mut Context<Self>) {}
366}
367
368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub enum Breakpoint {
371 Small,
373 Medium,
375 Large,
377}
378
379#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
381pub struct ScrollbarVisibility {
382 pub v: bool,
384 pub h: bool,
386}
387
388#[derive(Default)]
390struct NodeChainState {
391 current_node: Cell<Option<NodeId>>,
392 chain_scopes: RefCell<Vec<ScopeId>>,
393 chain_keys: RefCell<Vec<Key>>,
394 generation: Cell<u64>,
395}
396
397impl NodeChainState {
398 fn node_id(&self) -> Option<NodeId> {
399 self.current_node.get()
400 }
401
402 fn has_within_scope(&self, scope: ScopeId) -> bool {
403 self.chain_scopes.borrow().contains(&scope)
404 }
405
406 fn has_within_key(&self, key: &Key) -> bool {
407 self.chain_keys
408 .borrow()
409 .iter()
410 .any(|candidate| candidate == key)
411 }
412
413 fn push_scope_if_missing(scopes: &mut Vec<ScopeId>, scope: ScopeId) {
414 if !scopes.contains(&scope) {
415 scopes.push(scope);
416 }
417 }
418
419 fn push_key_if_missing(keys: &mut Vec<Key>, key: &Key) {
420 if !keys.iter().any(|candidate| candidate == key) {
421 keys.push(key.clone());
422 }
423 }
424
425 fn replace_snapshot(&self, node: Option<NodeId>, scopes: Vec<ScopeId>, keys: Vec<Key>) {
426 let mut scope_ref = self.chain_scopes.borrow_mut();
427 let mut key_ref = self.chain_keys.borrow_mut();
428 let changed = self.current_node.get() != node || *scope_ref != scopes || *key_ref != keys;
429
430 self.current_node.set(node);
431 *scope_ref = scopes;
432 *key_ref = keys;
433
434 if changed {
435 self.generation
436 .set(self.generation.get().wrapping_add(1).max(1));
437 }
438 }
439
440 fn update_chain(&self, tree: &NodeTree, node: Option<NodeId>) {
442 let mut scopes = Vec::new();
443 let mut keys = Vec::new();
444
445 if let Some(mut cur) = node {
446 Self::push_scope_if_missing(&mut scopes, ScopeId(1));
447
448 loop {
449 if !tree.is_valid(cur) {
450 break;
451 }
452
453 let node_ref = tree.node(cur);
454
455 if let Some(k) = &node_ref.key {
456 Self::push_key_if_missing(&mut keys, k);
457 }
458
459 if let NodeKind::Group(group) = &node_ref.kind {
460 Self::push_scope_if_missing(&mut scopes, group.scope);
461 }
462
463 let Some(parent) = node_ref.parent else {
464 break;
465 };
466 cur = parent;
467 }
468 }
469
470 self.replace_snapshot(node, scopes, keys);
471 }
472
473 fn generation(&self) -> u64 {
474 self.generation.get()
475 }
476}
477
478#[derive(Default)]
480pub(crate) struct FocusContext {
481 inner: NodeChainState,
482}
483
484impl FocusContext {
485 pub(crate) fn update_from_tree(
486 &self,
487 tree: &NodeTree,
488 focused: Option<NodeId>,
489 focused_key: Option<&Key>,
490 ) {
491 let mut cur = focused.filter(|id| tree.is_valid(*id));
492
493 if cur.is_none()
494 && let Some(key) = focused_key
495 {
496 if let Some(id) = tree
497 .iter()
498 .find(|n| n.key.as_ref() == Some(key))
499 .map(|n| n.id)
500 {
501 cur = Some(id);
502 } else {
503 self.inner
504 .replace_snapshot(None, Vec::new(), vec![key.clone()]);
505 return;
506 }
507 }
508
509 self.inner.update_chain(tree, cur);
510 }
511
512 pub(crate) fn focused_node_id(&self) -> Option<NodeId> {
513 self.inner.node_id()
514 }
515
516 pub(crate) fn has_focus_within_scope(&self, scope: ScopeId) -> bool {
517 self.inner.has_within_scope(scope)
518 }
519
520 pub(crate) fn has_focus_within_key(&self, key: &Key) -> bool {
521 self.inner.has_within_key(key)
522 }
523
524 pub(crate) fn generation(&self) -> u64 {
525 self.inner.generation()
526 }
527}
528
529#[derive(Default)]
531pub(crate) struct HoverContext {
532 inner: NodeChainState,
533}
534
535impl HoverContext {
536 pub(crate) fn update_from_tree(&self, tree: &NodeTree, hovered: Option<NodeId>) {
537 let cur = hovered.filter(|id| tree.is_valid(*id));
538 self.inner.update_chain(tree, cur);
539 }
540
541 pub(crate) fn hovered_node_id(&self) -> Option<NodeId> {
542 self.inner.node_id()
543 }
544
545 pub(crate) fn has_hover_within_scope(&self, scope: ScopeId) -> bool {
546 self.inner.has_within_scope(scope)
547 }
548
549 pub(crate) fn has_hover_within_key(&self, key: &Key) -> bool {
550 self.inner.has_within_key(key)
551 }
552
553 pub(crate) fn generation(&self) -> u64 {
554 self.inner.generation()
555 }
556}
557
558#[derive(Default)]
560pub(crate) struct ScrollContext {
561 by_key: RefCell<FxHashMap<Key, ScrollbarVisibility>>,
562 text_area_metrics_by_key: RefCell<FxHashMap<Key, crate::widgets::TextAreaMetrics>>,
563 generation: Cell<u64>,
564}
565
566impl ScrollContext {
567 pub(crate) fn update_from_tree(&self, tree: &NodeTree) {
568 let mut map = self.by_key.borrow_mut();
569 let prev = std::mem::take(&mut *map);
570 let mut metrics_map = self.text_area_metrics_by_key.borrow_mut();
571 let prev_metrics = std::mem::take(&mut *metrics_map);
572
573 for node in tree.iter_with_overlays() {
574 if let (Some(key), NodeKind::TextArea(text_area)) = (&node.key, &node.kind) {
575 let metrics = text_area.metrics(node.rect);
576 map.insert(key.clone(), metrics.scrollbars);
577 metrics_map.insert(key.clone(), metrics);
578 }
579 }
580
581 if *map != prev || *metrics_map != prev_metrics {
582 self.generation
583 .set(self.generation.get().wrapping_add(1).max(1));
584 }
585 }
586
587 pub(crate) fn get(&self, key: &Key) -> Option<ScrollbarVisibility> {
588 self.by_key.borrow().get(key).copied()
589 }
590
591 pub(crate) fn text_area_metrics(&self, key: &Key) -> Option<crate::widgets::TextAreaMetrics> {
592 self.text_area_metrics_by_key.borrow().get(key).cloned()
593 }
594
595 pub(crate) fn generation(&self) -> u64 {
596 self.generation.get()
597 }
598}
599
600pub struct Context<C: Component> {
602 pub state: C::State,
604
605 pub props: C::Properties,
607
608 viewport: Rect,
609 link: Link<C::Message>,
610 env: RuntimeEnv,
611 scope: ScopeId,
612}
613
614impl<C: Component> Context<C> {
615 pub(crate) fn new(
616 component: &C,
617 scope: ScopeId,
618 dispatcher: Dispatcher,
619 props: C::Properties,
620 env: RuntimeEnv,
621 viewport: Rect,
622 ) -> Self {
623 let state = component.create_state(&props);
624 Self {
625 state,
626 props,
627 viewport,
628 link: Link::new(scope, dispatcher),
629 env,
630 scope,
631 }
632 }
633
634 pub fn link(&self) -> &Link<C::Message> {
636 &self.link
637 }
638
639 pub fn toast(&self) -> crate::overlay::ToastHandle {
641 crate::overlay::ToastHandle::new(self.env.overlay_manager.clone())
642 }
643
644 pub fn clipboard(&self) -> crate::clipboard::ClipboardHandle {
646 crate::clipboard::ClipboardHandle::new(
647 self.env.clipboard.clone(),
648 self.env.clipboard_config.clone(),
649 )
650 }
651
652 pub fn command_registry(&self) -> CommandRegistry {
654 self.env.command_registry.clone()
655 }
656
657 pub fn register_command(&self, entry: CommandEntry) {
659 self.env
660 .command_registry
661 .register_for_scope(self.scope, entry);
662 }
663
664 pub fn viewport(&self) -> Rect {
666 self.env.note_memo_dependency(MemoDependency::Viewport);
667 self.viewport
668 }
669
670 pub fn theme(&self) -> Theme {
672 self.env.note_memo_dependency(MemoDependency::Theme);
673 self.env.active_theme.borrow().clone()
674 }
675
676 pub fn theme_extension<T>(&self) -> Option<T>
678 where
679 T: ThemeExtension,
680 {
681 self.env.note_memo_dependency(MemoDependency::Theme);
682 self.env.active_theme.borrow().extension_cloned::<T>()
683 }
684
685 pub fn use_context<T>(&self) -> Option<T>
687 where
688 T: ContextValue,
689 {
690 self.env
691 .note_memo_dependency(MemoDependency::Context(TypeId::of::<T>()));
692 self.env
693 .contexts
694 .borrow()
695 .get(&TypeId::of::<T>())
696 .and_then(|value| value.as_ref().downcast_ref::<T>())
697 .cloned()
698 }
699
700 pub fn context<T>(&self) -> Option<T>
702 where
703 T: ContextValue,
704 {
705 self.use_context::<T>()
706 }
707
708 pub fn is_inline(&self) -> bool {
710 self.env.surface_mode.is_inline()
711 }
712
713 pub fn surface_mode(&self) -> SurfaceMode {
715 self.env.surface_mode
716 }
717
718 pub fn effect_phase(&self) -> u64 {
723 self.env.effect_phase.get()
724 }
725
726 pub fn host_terminal_colors(&self) -> Option<HostTerminalColors> {
733 self.env.host_terminal_colors()
734 }
735
736 pub fn host_terminal_color_generation(&self) -> u64 {
742 self.env.host_terminal_color_generation()
743 }
744
745 pub fn request_host_terminal_color_refresh(&self) {
755 self.env.request_host_terminal_color_refresh();
756 }
757
758 pub fn mouse_capture_enabled(&self) -> bool {
760 self.env.note_memo_dependency(MemoDependency::MouseCapture);
761 self.env.mouse_capture.get()
762 }
763
764 pub fn set_mouse_capture(&self, enabled: bool) {
766 if self.env.mouse_capture.get() != enabled {
767 self.env.mouse_capture.set(enabled);
768 self.env.mouse_capture_generation.set(
769 self.env
770 .mouse_capture_generation
771 .get()
772 .wrapping_add(1)
773 .max(1),
774 );
775 }
776 }
777
778 pub fn toggle_mouse_capture(&self) -> bool {
780 let next = !self.env.mouse_capture.get();
781 self.set_mouse_capture(next);
782 next
783 }
784
785 pub fn append_transcript_lines<I, L>(&mut self, lines: I)
789 where
790 I: IntoIterator<Item = L>,
791 L: Into<RichText>,
792 {
793 if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
794 return;
795 }
796
797 let lines: Vec<RichText> = lines.into_iter().map(Into::into).collect();
798 if lines.is_empty() {
799 return;
800 }
801
802 self.env
803 .transcript_history
804 .borrow_mut()
805 .push(TranscriptEntry::Lines(lines.clone()));
806 self.env
807 .pending_transcript_entries
808 .borrow_mut()
809 .push_back(TranscriptEntry::Lines(lines));
810 }
811
812 pub fn append_transcript_element(&mut self, element: impl Into<Element>) {
817 if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
818 return;
819 }
820
821 let element = element.into();
822 if element.contains_unexpanded_component() {
823 crate::debug::internal_log!(
824 "[tui-lipan] append_transcript_element ignored an element containing Component nodes"
825 );
826 return;
827 }
828
829 self.env
830 .transcript_history
831 .borrow_mut()
832 .push(TranscriptEntry::Element(Box::new(element.clone())));
833 self.env
834 .pending_transcript_entries
835 .borrow_mut()
836 .push_back(TranscriptEntry::Element(Box::new(element)));
837 }
838
839 pub fn has_focus_within(&self) -> bool {
842 self.env.note_memo_dependency(MemoDependency::Focus);
843 self.env.focus.has_focus_within_scope(self.scope)
844 }
845
846 pub fn has_focus_within_key(&self, key: impl Into<Key>) -> bool {
849 let key = key.into();
850 self.env.note_memo_dependency(MemoDependency::Focus);
851 self.env.focus.has_focus_within_key(&key)
852 }
853
854 pub fn text_area_scrollbars(&self, key: impl Into<Key>) -> ScrollbarVisibility {
859 let key = key.into();
860 self.env.note_memo_dependency(MemoDependency::Scroll);
861 self.text_area_metrics(key.clone())
862 .map(|metrics| metrics.scrollbars)
863 .or_else(|| self.env.scroll.get(&key))
864 .unwrap_or_default()
865 }
866
867 pub fn text_area_metrics(
869 &self,
870 key: impl Into<Key>,
871 ) -> Option<crate::widgets::TextAreaMetrics> {
872 let key = key.into();
873 self.env.note_memo_dependency(MemoDependency::Scroll);
874 self.env.scroll.text_area_metrics(&key)
875 }
876
877 pub fn has_hover_within(&self) -> bool {
880 self.env.note_memo_dependency(MemoDependency::Hover);
881 self.env.hover.has_hover_within_scope(self.scope)
882 }
883
884 pub fn has_hover_within_key(&self, key: impl Into<Key>) -> bool {
887 let key = key.into();
888 self.env.note_memo_dependency(MemoDependency::Hover);
889 self.env.hover.has_hover_within_key(&key)
890 }
891
892 pub fn focused_node_id(&self) -> Option<NodeId> {
894 self.env.note_memo_dependency(MemoDependency::Focus);
895 self.env.focus.focused_node_id()
896 }
897
898 pub fn hovered_node_id(&self) -> Option<NodeId> {
900 self.env.note_memo_dependency(MemoDependency::Hover);
901 self.env.hover.hovered_node_id()
902 }
903
904 pub fn transition<T>(
929 &self,
930 key: impl Into<Key>,
931 target: T,
932 config: crate::animation::TransitionConfig,
933 ) -> T
934 where
935 T: crate::animation::Lerp + PartialEq + 'static,
936 {
937 let key = key.into();
938 self.env.note_memo_dependency(MemoDependency::Transition);
939 self.env.animations.transition(key, target, config)
940 }
941
942 pub fn breakpoint(&self, medium: u16, large: u16) -> Breakpoint {
949 let (medium, large) = if medium <= large {
950 (medium, large)
951 } else {
952 (large, medium)
953 };
954
955 self.env.note_memo_dependency(MemoDependency::Viewport);
956 let w = self.viewport.w;
957 if w < medium {
958 Breakpoint::Small
959 } else if w < large {
960 Breakpoint::Medium
961 } else {
962 Breakpoint::Large
963 }
964 }
965
966 pub(crate) fn env(&self) -> &RuntimeEnv {
967 &self.env
968 }
969
970 pub(crate) fn set_viewport(&mut self, viewport: Rect) {
971 self.viewport = viewport;
972 }
973
974 pub(crate) fn set_active_theme(&mut self, theme: Theme) {
975 let mut active_theme = self.env.active_theme.borrow_mut();
976 if *active_theme != theme {
977 *active_theme = theme;
978 self.env.active_theme_generation.set(
979 self.env
980 .active_theme_generation
981 .get()
982 .wrapping_add(1)
983 .max(1),
984 );
985 }
986 }
987
988 pub(crate) fn set_contexts(
989 &mut self,
990 contexts: rustc_hash::FxHashMap<TypeId, std::sync::Arc<dyn std::any::Any>>,
991 generations: rustc_hash::FxHashMap<TypeId, u64>,
992 ) {
993 *self.env.contexts.borrow_mut() = contexts;
994 *self.env.context_generations.borrow_mut() = generations;
995 }
996
997 pub(crate) fn memo_key(&self, component: &C) -> Option<u64> {
998 component.memo_key(&self.props, self)
999 }
1000
1001 pub(crate) fn begin_memo_dependency_capture(&self) {
1002 self.env.begin_memo_dependency_capture();
1003 }
1004
1005 pub(crate) fn finish_memo_dependency_capture(&self) -> MemoDependencySnapshot {
1006 self.env.finish_memo_dependency_capture(self.viewport)
1007 }
1008
1009 pub(crate) fn memo_dependencies_match(&self, snapshot: &MemoDependencySnapshot) -> bool {
1010 snapshot.matches(&self.env, self.viewport)
1011 }
1012
1013 pub fn quit(&mut self) {
1015 self.env.quit.set(true);
1016 }
1017
1018 pub fn request_focus(&mut self, key: impl Into<Key>) {
1022 *self.env.focus_request.borrow_mut() = Some(key.into());
1023 }
1024
1025 pub fn request_full_repaint(&self) {
1030 self.env.full_repaint.set(true);
1031 }
1032
1033 pub fn show_devtools(&self) {
1035 *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Show);
1036 }
1037
1038 pub fn hide_devtools(&self) {
1040 *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Hide);
1041 }
1042
1043 pub fn toggle_devtools(&self) {
1045 *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Toggle);
1046 }
1047
1048 pub(crate) fn take_focus_request(&self) -> Option<Key> {
1049 self.env.focus_request.borrow_mut().take()
1050 }
1051
1052 pub(crate) fn take_full_repaint_request(&self) -> bool {
1053 self.env.full_repaint.replace(false)
1054 }
1055
1056 pub(crate) fn take_devtools_request(&self) -> Option<DevToolsRequest> {
1057 self.env.devtools_request.borrow_mut().take()
1058 }
1059
1060 pub fn request_ui_snapshot_to(&self, path: impl AsRef<std::path::Path>) {
1069 let path = path.as_ref().to_path_buf();
1070 let extension = path.extension();
1071 let format = if extension.is_some_and(|ext| ext == "json" || ext == "JSON") {
1072 #[cfg(feature = "ui-snapshot-json")]
1073 {
1074 crate::ui_snapshot::UiSnapshotFileFormat::Json
1075 }
1076 #[cfg(not(feature = "ui-snapshot-json"))]
1077 {
1078 crate::ui_snapshot::UiSnapshotFileFormat::Markdown
1079 }
1080 } else if extension.is_some_and(|ext| ext == "png" || ext == "PNG") {
1081 #[cfg(feature = "ui-snapshot-png")]
1082 {
1083 crate::ui_snapshot::UiSnapshotFileFormat::Png
1084 }
1085 #[cfg(not(feature = "ui-snapshot-png"))]
1086 {
1087 crate::ui_snapshot::UiSnapshotFileFormat::Markdown
1088 }
1089 } else {
1090 crate::ui_snapshot::UiSnapshotFileFormat::Markdown
1091 };
1092 *self.env.ui_snapshot_request.borrow_mut() =
1093 Some(crate::ui_snapshot::UiSnapshotRequest::Write { path, format });
1094 self.request_full_repaint();
1095 }
1096
1097 pub fn request_ui_snapshot_to_slot(&self, slot: &crate::ui_snapshot::UiSnapshotSlot) {
1102 *self.env.ui_snapshot_request.borrow_mut() = Some(
1103 crate::ui_snapshot::UiSnapshotRequest::Deliver(slot.shared()),
1104 );
1105 self.request_full_repaint();
1106 }
1107
1108 pub(crate) fn take_ui_snapshot_request(&self) -> Option<crate::ui_snapshot::UiSnapshotRequest> {
1109 self.env.ui_snapshot_request.borrow_mut().take()
1110 }
1111
1112 pub(crate) fn should_quit(&self) -> bool {
1113 self.env.quit.get()
1114 }
1115}
1116
1117#[cfg(test)]
1118mod tests;