duat_core/widgets/
mod.rs

1//! APIs for the construction of widgets, and a few common ones.
2//!
3//! This module has the [`Widget`] trait, which is a region on the
4//! window containing a [`Text`], and may be modified by user mode
5//! (but not necessarily).
6//!
7//! With the exception of the [`File`], these widgets will show up in
8//! one of three contexts:
9//!
10//! - Being pushed to a [`File`] via the hook [`OnFileOpen`];
11//! - Being pushed to the outer edges via [`OnWindowOpen`];
12//! - Being pushed to popup widgets via `OnPopupOpen` (TODO);
13//!
14//! These widgets can be pushed to all 4 sides of other widgets,
15//! through the use of [`PushSpecs`]. When pushing widgets, you can
16//! also include [`Constraint`] in order to get a specific size on the
17//! screen for the widget.
18//!
19//! ```rust
20//! # use duat_core::ui::PushSpecs;
21//! let specs = PushSpecs::left().with_hor_min(10.0).with_ver_len(2.0);
22//! ```
23//!
24//! When pushing a widget with these `specs` to another widget, Duat
25//! will put it on the left, and _try_ to give it a minimum width of
26//! `10.0`, and a height of `2.0`.
27//!
28//! The module also provides 4 native widgets, [`File`] and
29//! [`PromptLine`], which can receive user mode, and [`StatusLine`]
30//! and [`LineNumbers`] which are not supposed to.
31//!
32//! These 4 widgets are supposed to be universal, not needing a
33//! specific [`Ui`] implementation to work. In contrast, you can
34//! create widgets for specific [`Ui`]s. As an example, the
35//! [`duat-term`] crate, which is a terminal [`Ui`] implementation for
36//! Duat, defines the [`VertRule`] widget, which is a separator that
37//! only makes sense in the context of a terminal.
38//!
39//! This module also describes a [`WidgetCfg`], which is used in
40//! widget construction.
41//!
42//! [`duat-term`]: https://docs.rs/duat-term/latest/duat_term/
43//! [`VertRule`]: https://docs.rs/duat-term/latest/duat_term/struct.VertRule.html
44//! [`OnFileOpen`]: crate::hooks::OnFileOpen
45//! [`OnWindowOpen`]: crate::hooks::OnWindowOpen
46//! [`Constraint`]: crate::ui::Constraint
47use std::sync::{
48    Arc,
49    atomic::{AtomicBool, Ordering},
50};
51
52pub use self::{
53    prompt_line::{PromptLine, PromptLineCfg},
54    file::{File, FileCfg, PathKind},
55    line_numbers::{LineNum, LineNumbers, LineNumbersOptions},
56    notifier::{Notifier, NotificationsCfg},
57    status_line::{State, StatusLine, StatusLineCfg, status},
58};
59use crate::{
60    cfg::PrintCfg,
61    context::FileParts,
62    data::{ReadDataGuard, RwData},
63    form,
64    hooks::{self, FocusedOn, UnfocusedFrom},
65    mode::Cursors,
66    text::Text,
67    ui::{Area, PushSpecs, Ui},
68};
69
70mod prompt_line;
71mod file;
72mod line_numbers;
73mod notifier;
74mod status_line;
75
76/// An area where [`Text`] will be printed to the screen
77///
78/// Most widgets are supposed to be passive widgets, that simply show
79/// information about the current state of Duat. If you want to see
80/// how to create a widget that takes in mode, see [`Mode`]
81///
82/// In order to show that information, widgets make use of [`Text`],
83/// which can show stylized text, buttons, and all sorts of other
84/// stuff.
85///
86/// For a demonstration on how to create a widget, I will create a
87/// widget that shows the uptime, in seconds, for Duat.
88///
89/// ```rust
90/// # use duat_core::text::Text;
91/// struct UpTime(Text);
92/// ```
93///
94/// In order to be a proper widget, it must have a [`Text`] to
95/// display. Next, I must implement [`Widget`]:
96///
97/// ```rust
98/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
99/// # use duat_core::{
100/// #     hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui},
101/// #     widgets::{Widget, WidgetCfg},
102/// # };
103/// # struct UpTime(Text);
104/// # struct UpTimeCfg<U>(PhantomData<U>);
105/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
106/// #     type Widget = UpTime;
107/// #     fn build(self,_: bool) -> (Self::Widget, impl Fn() -> bool + 'static, PushSpecs) {
108/// #         (UpTime(Text::new()), || false, PushSpecs::below())
109/// #     }
110/// # }
111/// impl<U: Ui> Widget<U> for UpTime {
112///     type Cfg = UpTimeCfg<U>;
113///
114///     fn cfg() -> Self::Cfg {
115///         UpTimeCfg(PhantomData)
116///     }
117///     // ...
118/// #     fn text(&self) -> &Text {
119/// #         &self.0
120/// #     }
121/// #     fn text_mut(&mut self) -> &mut Text {
122/// #         &mut self.0
123/// #     }
124/// #     fn once() -> Result<(), Text> {
125/// #         Ok(())
126/// #     }
127/// }
128/// ```
129///
130/// Note the [`Cfg`](Widget::Cfg) type, and the [`cfg`] method.
131/// These exist to give the user the ability to modify the widgets
132/// before they are pushed. The `Cfg` type, which implements
133/// [`WidgetCfg`] is the thing that will actually construct the
134/// widget. Let's look at `UpTimeCfg`:
135///
136/// ```rust
137/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
138/// # use duat_core::{
139/// #     hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
140/// # };
141/// # struct UpTime(Text);
142/// struct UpTimeCfg<U>(PhantomData<U>);
143///
144/// impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
145///     type Widget = UpTime;
146///
147///     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
148///         let widget = UpTime(Text::new());
149///         let checker = periodic_checker(Duration::new(1, 0));
150///         let specs = PushSpecs::below().with_ver_len(1.0);
151///
152///         (widget, checker, specs)
153///     }
154/// }
155/// # impl<U: Ui> Widget<U> for UpTime {
156/// #     type Cfg = UpTimeCfg<U>;
157/// #     fn cfg() -> Self::Cfg {
158/// #         UpTimeCfg(PhantomData)
159/// #     }
160/// #     fn text(&self) -> &Text {
161/// #         &self.0
162/// #     }
163/// #     fn text_mut(&mut self) -> &mut Text{
164/// #         &mut self.0
165/// #     }
166/// #     fn once() -> Result<(), Text> {
167/// #         Ok(())
168/// #     }
169/// # }
170/// ```
171///
172/// The [`build`] method should return 3 objects:
173///
174/// * The widget itself.
175/// * A checker function that tells Duat when to update the widget.
176/// * [How] to push the widget into the [`File`]/window.
177///
178/// In this case, [`periodic_checker`] returns a function that returns
179/// `true` every `duration` that passes.
180///
181/// Also, note that `UpTimeCfg` includes a [`PhantomData<U>`]. This is
182/// done so that the end user does not need to specify a [`Ui`] when
183/// using [`WidgetCfg`]s.
184///
185/// Now, there are some other methods from [`Widget`] that need
186/// to be implemented for this to work. First of all, there needs to
187/// be a starting [`Instant`] to compare with the current moment in
188/// time.
189///
190/// The best time to do something like this is after Duat is done with
191/// initial setup. This happens when the [`ConfigLoaded`] hook is
192/// triggered.
193///
194/// ```rust
195/// # use std::{sync::OnceLock, time::Instant};
196/// # use duat_core::{hooks::{self, ConfigLoaded}, ui::Ui};
197/// # fn test<U: Ui>() {
198/// static START_TIME: OnceLock<Instant> = OnceLock::new();
199/// hooks::add::<ConfigLoaded>(|_| START_TIME.set(Instant::now()).unwrap());
200/// # }
201/// ```
202///
203/// I could put this code inside the [`cfg`] method, however, by
204/// doing so, it will be called every time this widget is added to the
205/// ui.
206///
207/// Instead, I'll put it in [`Widget::once`]. This function is
208/// only triggered once, no matter how many times the widget is added
209/// to the ui:
210///
211/// ```rust
212/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
213/// # use duat_core::{
214/// #     form::{self, Form}, hooks::{self, ConfigLoaded}, periodic_checker,
215/// #     text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
216/// # };
217/// # struct UpTime(Text);
218/// # struct UpTimeCfg<U>(PhantomData<U>);
219/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
220/// #     type Widget = UpTime;
221/// #     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
222/// #         (UpTime(Text::new()), || false, PushSpecs::below())
223/// #     }
224/// # }
225/// static START_TIME: OnceLock<Instant> = OnceLock::new();
226///
227/// impl<U: Ui> Widget<U> for UpTime {
228/// #     type Cfg = UpTimeCfg<U>;
229/// #     fn cfg() -> Self::Cfg {
230/// #         UpTimeCfg(PhantomData)
231/// #     }
232/// #     fn text(&self) -> &Text {
233/// #         &self.0
234/// #     }
235/// #     fn text_mut(&mut self) -> &mut Text {
236/// #         &mut self.0
237/// #     }
238///     // ...
239///     fn once() -> Result<(), Text> {
240///         hooks::add::<ConfigLoaded>(|_| {
241///             START_TIME.set(Instant::now()).unwrap()
242///         });
243///         form::set_weak("UpTime", Form::cyan());
244///         Ok(())
245///     }
246/// }
247/// ```
248///
249/// I also added the `"UpTime"` [`Form`], which will be used by the
250/// widget when it is updated. When adding form, you should use the
251/// [`form::set_weak*`] functions, in order to not interfere with
252/// the configuration crate.
253///
254/// Next, I need to implement the [`update`] method, which will simply
255/// format the [`Text`] into a readable format:
256///
257/// ```rust
258/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
259/// # use duat_core::{
260/// #     hooks, periodic_checker, text::{Text, text}, ui::{PushSpecs, Ui},
261/// #     widgets::{Widget, WidgetCfg},
262/// # };
263/// # struct UpTime(Text);
264/// # struct UpTimeCfg<U>(PhantomData<U>);
265/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
266/// #     type Widget = UpTime;
267/// #     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
268/// #         (UpTime(Text::new()), || false, PushSpecs::below())
269/// #     }
270/// # }
271/// # static START_TIME: OnceLock<Instant> = OnceLock::new();
272/// impl<U: Ui> Widget<U> for UpTime {
273/// #     type Cfg = UpTimeCfg<U>;
274/// #     fn cfg() -> Self::Cfg {
275/// #         UpTimeCfg(PhantomData)
276/// #     }
277/// #     fn text(&self) -> &Text {
278/// #         &self.0
279/// #     }
280/// #     fn text_mut(&mut self) -> &mut Text {
281/// #         &mut self.0
282/// #     }
283///     // ...
284///     fn update(&mut self, _area: &U::Area) {
285///         let Some(start) = START_TIME.get() else {
286///             return;
287///         };
288///         let duration = start.elapsed();
289///         let mins = duration.as_secs() / 60;
290///         let secs = duration.as_secs() % 60;
291///         self.0 = text!([UpTime] mins "m " secs "s");
292///     }
293///     // ...
294///     fn once() -> Result<(), Text> {
295///         Ok(())
296///     }
297/// }
298/// ```
299///
300/// [`Mode`]: crate::mode::Mode
301/// [`cfg`]: Widget::cfg
302/// [`build`]: WidgetCfg::build
303/// [How]: PushSpecs
304/// [`periodic_checker`]: crate::periodic_checker
305/// [`PhantomData<U>`]: std::marker::PhantomData
306/// [`Instant`]: std::time::Instant
307/// [`ConfigLoaded`]: crate::hooks::ConfigLoaded
308/// [`update`]: Widget::update
309/// [`Form`]: crate::form::Form
310/// [`form::set_weak*`]: crate::form::set_weak
311/// [`text!`]: crate::text::text
312pub trait Widget<U: Ui>: Send + 'static {
313    /// The configuration type
314    type Cfg: WidgetCfg<U, Widget = Self>
315    where
316        Self: Sized;
317
318    /// Returns a [`WidgetCfg`], for use in layout construction
319    ///
320    /// This function exists primarily so the [`WidgetCfg`]s
321    /// themselves don't need to be in scope. You will want to use
322    /// these in [hooks] like [`OnFileOpen`]:
323    ///
324    /// ```rust
325    /// # use duat_core::{
326    /// #     hooks::{self, OnFileOpen}, ui::{FileBuilder, Ui},
327    /// #     widgets::{File, LineNumbers, Widget},
328    /// # };
329    /// # fn test<U: Ui>() {
330    /// hooks::remove("FileWidgets");
331    /// hooks::add::<OnFileOpen<U>>(|builder| {
332    ///     // Screw it, LineNumbers on both sides.
333    ///     builder.push(LineNumbers::cfg());
334    ///     builder.push(LineNumbers::cfg().on_the_right().align_right());
335    /// });
336    /// # }
337    /// ```
338    ///
339    /// [`OnFileOpen`]: crate::hooks::OnFileOpen
340    fn cfg() -> Self::Cfg
341    where
342        Self: Sized;
343
344    /// Updates the widget, allowing the modification of its [`Area`]
345    ///
346    /// There are a few contexts in which this function is triggered:
347    ///
348    /// * A key was sent to the widget, if it is an [`Widget`]
349    /// * It was modified externally by something like [`IncSearch`]
350    /// * The window was resized, so all widgets must be reprinted
351    ///
352    /// In this function, the text should be updated to match its new
353    /// conditions, or request changes to its [`Area`]. As an example,
354    /// the [`LineNumbers`] widget asks for [more or less width],
355    /// depending on the number of lines in the file, in order
356    /// to show an appropriate number of digits.
357    ///
358    /// [`Session`]: crate::session::Session
359    /// [more or less width]: Area::constrain_hor
360    /// [`IncSearch`]: crate::mode::IncSearch
361    fn update(&mut self, _area: &U::Area) {}
362
363    /// The text that this widget prints out
364    fn text(&self) -> &Text;
365
366    /// A mutable reference to the [`Text`] that is printed
367    fn text_mut(&mut self) -> &mut Text;
368
369    /// The [`Cursors`] that are used on the [`Text`], if they exist
370    fn cursors(&self) -> Option<&Cursors> {
371        self.text().cursors()
372    }
373
374    /// A mutable reference to the [`Cursors`], if they exist
375    fn cursors_mut(&mut self) -> Option<&mut Cursors> {
376        self.text_mut().cursors_mut()
377    }
378
379    /// Actions to do whenever this [`Widget`] is focused.
380    #[allow(unused)]
381    fn on_focus(&mut self, area: &U::Area) {}
382
383    /// Actions to do whenever this [`Widget`] is unfocused.
384    #[allow(unused)]
385    fn on_unfocus(&mut self, area: &U::Area) {}
386
387    /// The [configuration] for how to print [`Text`]
388    ///
389    /// The default configuration, used when `print_cfg` is not
390    /// implemented,can be found at [`PrintCfg::new`].
391    ///
392    /// [configuration]: PrintCfg
393    fn print_cfg(&self) -> PrintCfg {
394        PrintCfg::new()
395    }
396
397    /// Prints the widget
398    ///
399    /// Very rarely shouuld you actually implement this method, one
400    /// example of where this is actually implemented is in
401    /// [`File::print`], where [`Area::print_with`] is called in order
402    /// to simultaneously update the list of lines numbers, for
403    /// widgets like [`LineNumbers`] to read.
404    fn print(&mut self, area: &U::Area) {
405        // crate::log_file!("printing {}", crate::duat_name::<Self>());
406        let cfg = self.print_cfg();
407        area.print(self.text_mut(), cfg, form::painter::<Self>())
408    }
409
410    /// Actions taken when this widget opens for the first time
411    ///
412    /// Examples of things that should go in here are [`form`]
413    /// functions, [hooks], [commands] you want executed only once
414    ///
415    /// [commands]: crate::cmd
416    fn once() -> Result<(), Text>
417    where
418        Self: Sized;
419}
420
421/// A configuration struct for a [`Widget`]
422///
423/// This configuration is used to make adjustments on how a widget
424/// will be added to a file or a window. These adjustments are
425/// primarily configurations for the widget itself, and to what
426/// direction it will be pushed:
427///
428/// ```rust
429/// # use duat_core::{
430/// #     hooks::{self, OnFileOpen},
431/// #     ui::Ui,
432/// #     widgets::{LineNumbers, Widget},
433/// # };
434/// # fn test<U: Ui>() {
435/// hooks::add::<OnFileOpen<U>>(|builder| {
436///     // Change pushing direction to the right.
437///     let cfg = LineNumbers::cfg().on_the_right();
438///     // Changes to where the numbers will be placed.
439///     let cfg = cfg.align_right().align_main_left();
440///
441///     builder.push(cfg);
442/// });
443/// # }
444/// ```
445pub trait WidgetCfg<U>: Sized
446where
447    U: Ui,
448{
449    type Widget: Widget<U>;
450
451    fn build(self, on_file: bool) -> (Self::Widget, impl CheckerFn, PushSpecs);
452}
453
454// Elements related to the [`Widget`]s
455#[derive(Clone)]
456pub struct Node<U: Ui> {
457    widget: RwData<dyn Widget<U>>,
458    area: U::Area,
459
460    checker: Arc<dyn Fn() -> bool + Send + Sync>,
461    busy_updating: Arc<AtomicBool>,
462
463    related_widgets: Related<U>,
464    on_focus: fn(&Node<U>),
465    on_unfocus: fn(&Node<U>),
466}
467
468impl<U: Ui> Node<U> {
469    pub fn new<W: Widget<U>>(
470        widget: RwData<dyn Widget<U>>,
471        area: U::Area,
472        checker: impl CheckerFn,
473    ) -> Self {
474        fn related_widgets<U: Ui>(
475            widget: &RwData<dyn Widget<U>>,
476            area: &U::Area,
477        ) -> Option<RwData<Vec<Node<U>>>> {
478            widget.write_as::<File>().map(|mut file| {
479                let cfg = file.print_cfg();
480                file.text_mut().add_cursors(area, cfg);
481                RwData::default()
482            })
483        }
484        let related_widgets = related_widgets::<U>(&widget, &area);
485
486        Self {
487            widget,
488            area,
489
490            checker: Arc::new(checker),
491            busy_updating: Arc::new(AtomicBool::new(false)),
492
493            related_widgets,
494            on_focus: Self::on_focus_fn::<W>,
495            on_unfocus: Self::on_unfocus_fn::<W>,
496        }
497    }
498
499    pub fn widget(&self) -> &RwData<dyn Widget<U>> {
500        &self.widget
501    }
502
503    /// Returns the downcast ref of this [`Widget`].
504    pub fn try_downcast<W>(&self) -> Option<RwData<W>> {
505        self.widget.try_downcast()
506    }
507
508    pub fn data_is<W: 'static>(&self) -> bool {
509        self.widget.data_is::<W>()
510    }
511
512    pub fn update_and_print(&self) {
513        self.busy_updating.store(true, Ordering::Release);
514
515        if let Some(nodes) = &self.related_widgets {
516            for node in &*nodes.read() {
517                if node.needs_update() {
518                    node.update_and_print();
519                }
520            }
521        }
522
523        let mut widget = self.widget.raw_write();
524
525        let cfg = widget.print_cfg();
526        widget.text_mut().remove_cursors(&self.area, cfg);
527
528        widget.update(&self.area);
529
530        widget.text_mut().add_cursors(&self.area, cfg);
531        if let Some(main) = widget.cursors().and_then(Cursors::get_main) {
532            self.area
533                .scroll_around_point(widget.text(), main.caret(), widget.print_cfg());
534        }
535
536        widget.print(&self.area);
537        drop(widget);
538
539        self.busy_updating.store(false, Ordering::Release);
540    }
541
542    pub fn read_as<W: 'static>(&self) -> Option<ReadDataGuard<'_, W>> {
543        self.widget.read_as()
544    }
545
546    pub fn ptr_eq<W: ?Sized>(&self, other: &RwData<W>) -> bool {
547        self.widget.ptr_eq(other)
548    }
549
550    pub fn needs_update(&self) -> bool {
551        if !self.busy_updating.load(Ordering::Acquire) {
552            self.area.has_changed() || (self.checker)()
553        } else {
554            false
555        }
556    }
557
558    pub(crate) fn update(&self) {
559        self.widget.raw_write().update(&self.area)
560    }
561
562    pub(crate) fn parts(&self) -> (&RwData<dyn Widget<U>>, &<U as Ui>::Area, &Related<U>) {
563        (&self.widget, &self.area, &self.related_widgets)
564    }
565
566    pub(crate) fn as_file(&self) -> Option<FileParts<U>> {
567        self.widget.try_downcast().map(|file| {
568            (
569                file,
570                self.area.clone(),
571                self.related_widgets.clone().unwrap(),
572            )
573        })
574    }
575
576    pub(crate) fn on_focus(&self) {
577        self.area.set_as_active();
578        (self.on_focus)(self)
579    }
580
581    pub(crate) fn on_unfocus(&self) {
582        (self.on_unfocus)(self)
583    }
584
585    pub(crate) fn raw_inspect<B>(&self, f: impl FnOnce(&dyn Widget<U>) -> B) -> B {
586        let widget = self.widget.raw_read();
587        f(&*widget)
588    }
589
590    pub(crate) fn area(&self) -> &U::Area {
591        &self.area
592    }
593
594    pub(crate) fn related_widgets(&self) -> Option<&RwData<Vec<Node<U>>>> {
595        self.related_widgets.as_ref()
596    }
597
598    fn on_focus_fn<W: Widget<U>>(&self) {
599        self.area.set_as_active();
600        let widget = self.widget.try_downcast().unwrap();
601        hooks::trigger::<FocusedOn<W, U>>((widget, self.area.clone()));
602    }
603
604    fn on_unfocus_fn<W: Widget<U>>(&self) {
605        let widget = self.widget.try_downcast().unwrap();
606        hooks::trigger::<UnfocusedFrom<W, U>>((widget, self.area.clone()));
607    }
608}
609
610impl<U: Ui> PartialEq for Node<U> {
611    fn eq(&self, other: &Self) -> bool {
612        self.widget.ptr_eq(&other.widget)
613    }
614}
615
616impl<U: Ui> Eq for Node<U> {}
617
618pub trait CheckerFn = Fn() -> bool + Send + Sync + 'static;
619
620pub type Related<U> = Option<RwData<Vec<Node<U>>>>;