Skip to main content

rat_focus/
builder.rs

1use crate::core::{Container, FocusCore};
2use crate::{Focus, FocusFlag, HasFocus, Navigation};
3use fxhash::FxBuildHasher;
4use ratatui_core::layout::Rect;
5use std::cell::Cell;
6use std::collections::HashSet;
7use std::ops::Range;
8
9macro_rules! focus_debug {
10    ($core:expr, $($arg:tt)+) => {
11        if $core.log.get() {
12            log::log!(log::Level::Debug, $($arg)+);
13        }
14    }
15}
16
17macro_rules! focus_fail {
18    ($core:expr, $($arg:tt)+) => {
19        if $core.log.get() {
20            log::log!(log::Level::Debug, $($arg)+);
21        }
22        if $core.insta_panic.get() {
23            panic!($($arg)+)
24        }
25    }
26}
27
28/// Builder for the Focus.
29#[derive(Debug, Default)]
30pub struct FocusBuilder {
31    last: FocusCore,
32
33    log: Cell<bool>,
34    insta_panic: Cell<bool>,
35
36    // base z value.
37    // starting a container adds the z-value of the container
38    // to the z_base. closing the container subtracts from the
39    // z_base. any widgets added in between have a z-value
40    // of z_base + widget z-value.
41    //
42    // this enables clean stacking of containers/widgets.
43    z_base: u16,
44
45    // new core
46    focus_ids: HashSet<usize, FxBuildHasher>,
47    focus_flags: Vec<FocusFlag>,
48    duplicate: Vec<bool>,
49    areas: Vec<(Rect, u16)>,
50    navigable: Vec<Navigation>,
51    container_ids: HashSet<usize, FxBuildHasher>,
52    containers: Vec<(Container, Range<usize>)>,
53}
54
55impl FocusBuilder {
56    /// Create a new FocusBuilder.
57    ///
58    /// This can take the previous Focus and ensures that
59    /// widgets that are no longer part of the focus list
60    /// have their focus-flag cleared.
61    ///
62    /// It will also recycle the storage of the old Focus.
63    pub fn new(last: Option<Focus>) -> FocusBuilder {
64        if let Some(mut last) = last {
65            // clear any data but retain the allocation.
66            last.last.clear();
67
68            Self {
69                last: last.core,
70                log: Default::default(),
71                insta_panic: Default::default(),
72                z_base: 0,
73                focus_ids: last.last.focus_ids,
74                focus_flags: last.last.focus_flags,
75                duplicate: last.last.duplicate,
76                areas: last.last.areas,
77                navigable: last.last.navigable,
78                container_ids: last.last.container_ids,
79                containers: last.last.containers,
80            }
81        } else {
82            Self {
83                last: FocusCore::default(),
84                log: Default::default(),
85                insta_panic: Default::default(),
86                z_base: Default::default(),
87                focus_ids: Default::default(),
88                focus_flags: Default::default(),
89                duplicate: Default::default(),
90                areas: Default::default(),
91                navigable: Default::default(),
92                container_ids: Default::default(),
93                containers: Default::default(),
94            }
95        }
96    }
97
98    /// The same as build_for but with logs enabled.
99    pub fn log_build_for(container: &dyn HasFocus) -> Focus {
100        let mut b = FocusBuilder::new(None);
101        b.enable_log();
102        b.widget(container);
103        b.build()
104    }
105
106    /// Shortcut for building the focus for a container
107    /// that implements [HasFocus].
108    ///
109    /// This creates a fresh Focus.
110    ///
111    /// __See__
112    ///
113    /// Use [rebuild_for](FocusBuilder::rebuild_for) if you want
114    /// to ensure that widgets that are no longer in the widget
115    /// structure have their focus flag reset properly. If you
116    /// don't have some logic to conditionally add widgets to
117    /// the focus, this function is probably fine.
118    pub fn build_for(container: &dyn HasFocus) -> Focus {
119        let mut b = FocusBuilder::new(None);
120        b.widget(container);
121        b.build()
122    }
123
124    /// The same as rebuild_for but with logs enabled.
125    pub fn log_rebuild_for(container: &dyn HasFocus, old: Option<Focus>) -> Focus {
126        let mut b = FocusBuilder::new(old);
127        b.enable_log();
128        b.widget(container);
129        b.build()
130    }
131
132    /// Shortcut function for building the focus for a container
133    /// that implements [HasFocus]
134    ///
135    /// This takes the old Focus and reuses most of its allocations.
136    /// It also ensures that any widgets no longer in the widget structure
137    /// have their focus-flags reset.
138    pub fn rebuild_for(container: &dyn HasFocus, old: Option<Focus>) -> Focus {
139        let mut b = FocusBuilder::new(old);
140        b.widget(container);
141        b.build()
142    }
143
144    /// Do some logging of the build.
145    pub fn enable_log(&self) {
146        self.log.set(true);
147    }
148
149    /// Do some logging of the build.
150    pub fn disable_log(&self) {
151        self.log.set(false);
152    }
153
154    /// Enable insta-panic for certain failures.
155    pub fn enable_panic(&self) {
156        self.insta_panic.set(true);
157    }
158
159    /// Disable insta-panic for certain failures.
160    pub fn disable_panic(&self) {
161        self.insta_panic.set(false);
162    }
163
164    /// Add a widget by calling its `build` function.
165    pub fn widget(&mut self, widget: &dyn HasFocus) -> &mut Self {
166        widget.build(self);
167        self
168    }
169
170    /// Add a widget by calling its build function.
171    ///
172    /// This tries to override the default navigation
173    /// for the given widget. This will fail if the
174    /// widget is a container. It may also fail
175    /// for other reasons. Depends on the widget.
176    ///
177    /// Enable log to check.
178    #[allow(clippy::collapsible_else_if)]
179    pub fn widget_navigate(&mut self, widget: &dyn HasFocus, navigation: Navigation) -> &mut Self {
180        widget.build_nav(navigation, self);
181
182        let widget_flag = widget.focus();
183        // override navigation for the widget
184        if let Some(idx) = self.focus_flags.iter().position(|v| *v == widget_flag) {
185            focus_debug!(
186                self,
187                "override navigation for {:?} with {:?}",
188                widget_flag,
189                navigation
190            );
191
192            self.navigable[idx] = navigation;
193        } else {
194            if self.container_ids.contains(&widget_flag.widget_id()) {
195                focus_fail!(
196                    self,
197                    "FAIL to override navigation for {:?}. This is a container.",
198                    widget_flag,
199                );
200            } else {
201                focus_fail!(
202                    self,
203                    "FAIL to override navigation for {:?}. Widget doesn't use this focus-flag",
204                    widget_flag,
205                );
206            }
207        }
208
209        self
210    }
211
212    /// Add a bunch of widgets.
213    #[inline]
214    pub fn widgets<const N: usize>(&mut self, widgets: [&dyn HasFocus; N]) -> &mut Self {
215        for widget in widgets {
216            widget.build(self);
217        }
218        self
219    }
220
221    /// Start a container widget. Must be matched with
222    /// the equivalent [end](Self::end). Uses focus(), area() and
223    /// z_area() of the given container. navigable() is
224    /// currently not used, just leave it at the default.
225    ///
226    /// __Attention__
227    ///
228    /// Use the returned value when calling [end](Self::end).
229    ///
230    /// __Panic__
231    ///
232    /// Panics if the same container-flag is added twice.
233    #[must_use]
234    pub fn start(&mut self, container: &dyn HasFocus) -> FocusFlag {
235        self.start_with_flags(container.focus(), container.area(), container.area_z())
236    }
237
238    /// End a container widget.
239    pub fn end(&mut self, tag: FocusFlag) {
240        focus_debug!(self, "end container {:?}", tag);
241        assert!(self.container_ids.contains(&tag.widget_id()));
242
243        for (c, r) in self.containers.iter_mut().rev() {
244            if c.container_flag != tag {
245                if !c.complete {
246                    panic!("FocusBuilder: Unclosed container {:?}", c.container_flag);
247                }
248            } else {
249                r.end = self.focus_flags.len();
250                c.complete = true;
251
252                focus_debug!(self, "container range {:?}", r);
253
254                self.z_base -= c.delta_z;
255
256                break;
257            }
258        }
259    }
260
261    /// Directly add the given widget's flags. Doesn't call
262    /// build() instead it uses focus(), etc. and appends a single widget.
263    ///
264    /// This is intended to be used when __implementing__
265    /// HasFocus::build() for a widget.
266    ///
267    /// In all other situations it's better to use [widget](FocusBuilder::widget)
268    /// or [widget_navigate](FocusBuilder::widget_navigate)
269    ///
270    /// __Panic__
271    ///
272    /// Panics if the same focus-flag is added twice.
273    pub fn leaf_widget(&mut self, widget: &dyn HasFocus) -> &mut Self {
274        self.leaf_with_flags(
275            widget.focus(),
276            widget.area(),
277            widget.area_z(),
278            widget.navigable(),
279        );
280        self
281    }
282
283    #[deprecated(since = "2.0.2", note = "use leaf_with_flags instead")]
284    pub fn widget_with_flags(
285        &mut self,
286        focus: FocusFlag,
287        area: Rect,
288        area_z: u16,
289        navigable: Navigation,
290    ) {
291        self.leaf_with_flags(focus, area, area_z, navigable)
292    }
293
294    /// Manually add a widgets flags.
295    ///
296    /// This is intended to be used when __implementing__
297    /// HasFocus::build() for a widget.
298    ///
299    /// In all other situations it's better to use [widget](FocusBuilder::widget)
300    /// or [widget_navigate](FocusBuilder::widget_navigate)
301    ///
302    /// __Panic__
303    ///
304    /// Panics if the same focus-flag is added twice.
305    /// Except it is allowable to add the flag a second time with
306    /// Navigation::Mouse or Navigation::None
307    pub fn leaf_with_flags(
308        &mut self,
309        focus: FocusFlag,
310        area: Rect,
311        area_z: u16,
312        navigable: Navigation,
313    ) {
314        let duplicate = self.focus_ids.contains(&focus.widget_id());
315
316        // there can be a second entry for the same focus-flag
317        // if it is only for mouse interactions.
318        if duplicate {
319            if !matches!(navigable, Navigation::Mouse | Navigation::None) {
320                focus_debug!(self, "{:#?}", self);
321                panic!(
322                    "Duplicate flag is only allowed if the second call uses Navigation::Mouse|Navigation::None. Was {:?}.",
323                    focus
324                )
325            }
326        }
327
328        focus_debug!(self, "widget {:?}", focus);
329
330        self.focus_ids.insert(focus.widget_id());
331        self.focus_flags.push(focus);
332        self.duplicate.push(duplicate);
333        self.areas.push((area, self.z_base + area_z));
334        self.navigable.push(navigable);
335    }
336
337    /// Start a container widget.
338    ///
339    /// Returns the FocusFlag of the container. This flag must
340    /// be used to close the container with [end](Self::end).
341    ///
342    /// __Panic__
343    ///
344    /// Panics if the same container-flag is added twice.
345    #[must_use]
346    pub fn start_with_flags(
347        &mut self,
348        container_flag: FocusFlag,
349        area: Rect,
350        area_z: u16,
351    ) -> FocusFlag {
352        focus_debug!(self, "start container {:?}", container_flag);
353
354        // no duplicates allowed for containers.
355        assert!(!self.container_ids.contains(&container_flag.widget_id()));
356
357        self.z_base += area_z;
358
359        let len = self.focus_flags.len();
360        self.container_ids.insert(container_flag.widget_id());
361        self.containers.push((
362            Container {
363                container_flag: container_flag.clone(),
364                area: (area, self.z_base),
365                delta_z: area_z,
366                complete: false,
367            },
368            len..len,
369        ));
370
371        container_flag
372    }
373
374    /// Build the Focus.
375    ///
376    /// If the previous Focus is known, this will also
377    /// reset the FocusFlag for any widget no longer part of
378    /// the Focus.
379    pub fn build(mut self) -> Focus {
380        // cleanup outcasts.
381        for v in &self.last.focus_flags {
382            if !self.focus_ids.contains(&v.widget_id()) {
383                v.clear();
384            }
385        }
386        for (v, _) in &self.last.containers {
387            let have_container = self
388                .containers
389                .iter()
390                .any(|(c, _)| v.container_flag == c.container_flag);
391            if !have_container {
392                v.container_flag.clear();
393            }
394        }
395        self.last.clear();
396
397        // check new tree.
398        for (c, _) in self.containers.iter_mut().rev() {
399            if !c.complete {
400                panic!("FocusBuilder: Unclosed container {:?}", c.container_flag);
401            }
402        }
403
404        let log = self.last.log.get();
405        let insta_panic = self.last.insta_panic.get();
406
407        Focus {
408            last: self.last,
409            core: FocusCore {
410                log: Cell::new(log),
411                insta_panic: Cell::new(insta_panic),
412                focus_ids: self.focus_ids,
413                focus_flags: self.focus_flags,
414                duplicate: self.duplicate,
415                areas: self.areas,
416                navigable: self.navigable,
417                container_ids: self.container_ids,
418                containers: self.containers,
419            },
420        }
421    }
422}