duat_core/ui/
builder.rs

1//! Builder helpers for Duat
2//!
3//! These builders are used primarily to push widgets to either a
4//! [`File`] or a window. They offer a convenient way to make massive
5//! changes to the layout, in a very intuitive "inner-outer" order,
6//! where widgets get pushed to a "main area" which holds all of the
7//! widgets that were added to that helper.
8//!
9//! This pushing to an unnamed, but known area makes the syntax for
10//! layout modification fairly minimal, with minimal boilerplate.
11//!
12//! [`File`]: crate::file::File
13use std::sync::Mutex;
14
15use super::{RawArea, Ui};
16use crate::{
17    context::{self, FileHandle},
18    data::Pass,
19    duat_name,
20    file::{File, ReaderCfg},
21    hook::{self, WidgetCreated},
22    ui::{Node, Widget, WidgetCfg},
23};
24
25/// A trait used to make [`Ui`] building generic
26///
27/// Its main implementors are [`FileBuilder`] and [`WindowBuilder`].
28pub trait UiBuilder<U: Ui>: Sized {
29    /// Pushes a [`Widget`] to this [`UiBuilder`], on its main
30    /// [`U::Area`]
31    ///
32    /// [`U::Area`]: Ui::Area
33    fn push_cfg<W: WidgetCfg<U>>(&mut self, pa: &mut Pass, cfg: W) -> (U::Area, Option<U::Area>);
34
35    /// Pushes a [`Widget`] to this [`UiBuilder`], on a given
36    /// [`U::Area`]
37    ///
38    /// [`U::Area`]: Ui::Area
39    fn push_cfg_to<W: WidgetCfg<U>>(
40        &mut self,
41        pa: &mut Pass,
42        area: U::Area,
43        cfg: W,
44    ) -> (U::Area, Option<U::Area>);
45}
46
47impl<U: Ui> UiBuilder<U> for FileBuilder<U> {
48    fn push_cfg<W: WidgetCfg<U>>(&mut self, pa: &mut Pass, cfg: W) -> (U::Area, Option<U::Area>) {
49        run_once::<W::Widget, U>();
50
51        let cfg = {
52            let wc = WidgetCreated::<W::Widget, U>((Some(cfg), Some(self.handle.clone())));
53            hook::trigger(pa, wc).0.0.unwrap()
54        };
55
56        let (widget, specs) = cfg.build(pa, Some(self.handle.clone()));
57
58        let mut windows = context::windows().borrow_mut();
59        let window = &mut windows[self.window_i];
60
61        let (node, parent) = window.push(pa, widget, &self.area, specs, true, true);
62
63        self.get_areas(pa, window, node, parent)
64    }
65
66    fn push_cfg_to<W: WidgetCfg<U>>(
67        &mut self,
68        pa: &mut Pass,
69        area: U::Area,
70        cfg: W,
71    ) -> (U::Area, Option<U::Area>) {
72        run_once::<W::Widget, U>();
73
74        let cfg = {
75            let wc = WidgetCreated::<W::Widget, U>((Some(cfg), Some(self.handle.clone())));
76            hook::trigger(pa, wc).0.0.unwrap()
77        };
78
79        let (widget, specs) = cfg.build(pa, Some(self.handle.clone()));
80
81        let mut windows = context::windows().borrow_mut();
82        let window = &mut windows[self.window_i];
83
84        let (node, parent) = window.push(pa, widget, &area, specs, true, true);
85
86        self.get_areas(pa, window, node, parent)
87    }
88}
89
90/// A constructor helper for [`File`] initiations
91///
92/// This helper is used primarily to push widgets around the file in
93/// question, and is only obtainable in a [`OnFileOpen`] hook:
94///
95/// ```rust
96/// # use duat_core::doc_duat as duat;
97/// setup_duat!(setup);
98/// use duat::prelude::*;
99///
100/// fn setup() {
101///     hook::add::<OnFileOpen>(|pa, builder| {
102///         builder.push(pa, LineNumbers::cfg());
103///     });
104/// }
105/// ```
106///
107/// In the example above, I pushed a [`LineNumbers`] widget to the
108/// [`File`]. By default, this widget will go on the left, but you can
109/// change that:
110///
111/// ```rust
112/// # use duat_core::doc_duat as duat;
113/// setup_duat!(setup);
114/// use duat::prelude::*;
115///
116/// fn setup() {
117///     hook::add::<OnFileOpen>(|pa, builder| {
118///         let line_numbers_cfg = LineNumbers::cfg().relative().on_the_right();
119///         builder.push(pa, line_numbers_cfg);
120///     });
121/// }
122/// ```
123///
124/// Note that I also made another change to the widget, it will now
125/// show relative numbers, instead of absolute, like it usually does.
126///
127/// By default, there already exists a [hook group] that adds widgets
128/// to a file, the `"FileWidgets"` group. If you want to get rid of
129/// this group in order to create your own widget layout, you should
130/// use [`hook::remove`]:
131///
132/// ```rust
133/// # use duat_core::doc_duat as duat;
134/// setup_duat!(setup);
135/// use duat::prelude::*;
136///
137/// fn setup() {
138///     hook::remove("FileWidgets");
139///     hook::add::<OnFileOpen>(|pa, builder| {
140///         let line_numbers_cfg = LineNumbers::cfg().relative().on_the_right();
141///         builder.push(pa, line_numbers_cfg);
142///         // Push a StatusLine to the bottom.
143///         builder.push(pa, StatusLine::cfg());
144///         // Push a PromptLine to the bottom.
145///         builder.push(pa, PromptLine::cfg());
146///     });
147/// }
148/// ```
149///
150/// [`File`]: crate::file::File
151/// [`OnFileOpen`]: crate::hook::OnFileOpen
152/// [`LineNumbers`]: https://crates.io/duat-utils/latest/duat_utils/wigets/struct.LineNumbers.html
153/// [hook group]: crate::hook::add_grouped
154/// [`hook::remove`]: crate::hook::remove
155pub struct FileBuilder<U: Ui> {
156    window_i: usize,
157    handle: FileHandle<U>,
158    area: U::Area,
159}
160
161impl<U: Ui> FileBuilder<U> {
162    /// Creates a new [`FileBuilder`].
163    pub(crate) fn new(pa: &mut Pass, node: Node<U>, window_i: usize) -> Self {
164        let name = node.read_as(pa, |f: &File<U>| f.name()).unwrap();
165        let handle = context::file_named(pa, name).unwrap();
166        let area = node.area().clone();
167
168        Self { window_i, handle, area }
169    }
170
171    /// Pushes a widget to the main area of this [`File`]
172    ///
173    /// This widget will be a satellite of the file. This means that,
174    /// if the file is destroyed, the widget will be destroyed as
175    /// well, if it is moved, the widget will be moved with it, etc.
176    ///
177    /// When you push a widget, it is placed on the edge of the file.
178    /// The next widget is placed on the edge of the area containing
179    /// the file and the previous widget, and so on.
180    ///
181    /// This means that, if you push widget *A* to the left of the
182    /// file, then you push widget *B* to the bottom of the window,
183    /// you will get this layout:
184    ///
185    /// ```text
186    /// ╭───┬──────────╮
187    /// │   │          │
188    /// │ A │   File   │
189    /// │   │          │
190    /// ├───┴──────────┤
191    /// │      B       │
192    /// ╰──────────────╯
193    /// ```
194    ///
195    /// Here's an example of such a layout:
196    ///
197    /// ```rust
198    /// # use duat_core::doc_duat as duat;
199    /// setup_duat!(setup);
200    /// use duat::prelude::*;
201    ///
202    /// fn setup() {
203    ///     hook::remove("FileWidgets");
204    ///     hook::add::<OnFileOpen>(|pa, builder| {
205    ///         let line_numbers_cfg = LineNumbers::cfg().rel_abs();
206    ///         builder.push(pa, line_numbers_cfg);
207    ///
208    ///         let status_line_cfg = status!("{file_fmt} {selections_fmt} {main_fmt}");
209    ///         builder.push(pa, status_line_cfg);
210    ///     });
211    /// }
212    /// ```
213    ///
214    /// In this case, each file will have [`LineNumbers`] with
215    /// relative/absolute numbering, and a [`StatusLine`] showing
216    /// the file's name and how many selections are in it.
217    ///
218    /// [`File`]: crate::file::File
219    /// [`LineNumbers`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbers.html
220    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
221    #[inline(never)]
222    pub fn push<W: WidgetAlias<U, impl BuilderDummy>>(
223        &mut self,
224        pa: &mut Pass,
225        widget: W,
226    ) -> (U::Area, Option<U::Area>) {
227        widget.push(pa, self)
228    }
229
230    /// Pushes a widget to a specific area around a [`File`]
231    ///
232    /// This method can be used to get some more advanced layouts,
233    /// where you have multiple widgets parallel to each other, yet on
234    /// the same edge.
235    ///
236    /// One of the main ways in which this is used is when using a
237    /// hidden [`Notifications`] widget alongside the [`PromptLine`]
238    /// widget.
239    ///
240    /// ```rust
241    /// # use duat_core::doc_duat as duat;
242    /// setup_duat!(setup);
243    /// use duat::prelude::*;
244    ///
245    /// fn setup() {
246    ///     hook::remove("FileWidgets");
247    ///     hook::add::<OnFileOpen>(|pa, builder| {
248    ///         builder.push(pa, LineNumbers::cfg());
249    ///
250    ///         let status_cfg = status!("{file_fmt} {selections_fmt} {main_fmt}");
251    ///         let (child, _) = builder.push(pa, status_cfg);
252    ///         let prompt_cfg = PromptLine::cfg().left_ratioed(3, 5);
253    ///         let (child, _) = builder.push_to(pa, child, prompt_cfg);
254    ///         builder.push_to(pa, child, Notifications::cfg());
255    ///     });
256    /// }
257    /// ```
258    ///
259    /// Pushing directly to the [`PromptLine`]'s [`U::Area`] means
260    /// that they'll share a parent that holds only them.
261    ///
262    /// [`File`]: crate::file::File
263    /// [`Notifications`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.Notifications.html
264    /// [`PromptLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.PromptLine.html
265    /// [`U::Area`]: Ui::Area
266    /// [hook group]: crate::hook::add_grouped
267    #[inline(never)]
268    pub fn push_to<W: WidgetCfg<U>>(
269        &mut self,
270        pa: &mut Pass,
271        area: U::Area,
272        cfg: W,
273    ) -> (U::Area, Option<U::Area>) {
274        self.push_cfg_to(pa, area, cfg)
275    }
276
277    /// Adds a [`Reader`] to this [`File`]
278    ///
279    /// [`Reader`]s read changes to [`Text`] and can act accordingly
280    /// by adding or removing [`Tag`]s from it. They can also be
281    /// accessed via a public API, in order to be used for other
282    /// things, like the treesitter [`Reader`], which, internally,
283    /// creates the syntax tree and does syntax highlighting, but
284    /// externally it can also be used for indentation of [`Text`] by
285    /// [`Mode`]s
286    ///
287    /// [`Tag`]: crate::text::Tag
288    /// [`Text`]: crate::text::Text
289    /// [`Reader`]: crate::file::Reader
290    /// [`Mode`]: crate::mode::Mode
291    pub fn add_reader(&mut self, pa: &mut Pass, reader_cfg: impl ReaderCfg<U>) {
292        self.handle.write(pa, |file, _| {
293            // SAFETY: Because this function takes in a &mut Pass, it is safe
294            // to create new ones inside.
295            let mut pa = unsafe { Pass::new() };
296            file.add_reader(&mut pa, reader_cfg)
297        })
298    }
299
300    /// The [`File`] that this hook is being applied to
301    pub fn read<Ret>(&mut self, pa: &Pass, f: impl FnOnce(&File<U>, &U::Area) -> Ret) -> Ret {
302        self.handle.read(pa, f)
303    }
304
305    /// Mutable reference to the [`File`] that this hooks is being
306    /// applied to
307    pub fn write<Ret>(
308        &mut self,
309        pa: &mut Pass,
310        f: impl FnOnce(&mut File<U>, &U::Area) -> Ret,
311    ) -> Ret {
312        self.handle.write(pa, f)
313    }
314
315    fn get_areas(
316        &mut self,
317        pa: &mut Pass,
318        window: &mut super::Window<U>,
319        node: Node<U>,
320        parent: Option<U::Area>,
321    ) -> (U::Area, Option<U::Area>) {
322        self.handle
323            .write_related_widgets(pa, |related| related.push(node.clone()));
324
325        if let Some(parent) = &parent {
326            if parent.is_master_of(&window.files_area) {
327                window.files_area = parent.clone();
328            }
329            self.area = parent.clone();
330        }
331
332        (node.area().clone(), parent)
333    }
334}
335
336/// A constructor helper for window initiations
337///
338/// This helper is used primarily to push widgets around the window,
339/// surrounding the "main area" which contains all [`File`]s and
340/// widgets added via the [`OnFileOpen`] hook.
341///
342/// It is used whenever the [`OnWindowOpen`] hook is triggered, which
343/// happens whenever a new window is opened:
344///
345/// ```rust
346/// # use duat_core::doc_duat as duat;
347/// setup_duat!(setup);
348/// use duat::prelude::*;
349///
350/// fn setup() {
351///     hook::add::<OnWindowOpen>(|pa, builder| {
352///         // Push a StatusLine to the bottom.
353///         builder.push(pa, StatusLine::cfg());
354///         // Push a PromptLine to the bottom.
355///         builder.push(pa, PromptLine::cfg());
356///     });
357/// }
358/// ```
359///
360/// Contrast this with the example in the [`FileBuilder`] docs, where
361/// a similar hook is added, but with [`OnFileOpen`] instead of
362/// [`OnWindowOpen`]. In that scenario, the widgets are added to each
363/// file that is opened, while in this one, only one instance of these
364/// widgets will be created per window.
365///
366/// The existence of these two hooks lets the user make some more
367/// advanced choices on the layout:
368///
369/// ```rust
370/// # use duat_core::doc_duat as duat;
371/// setup_duat!(setup);
372/// use duat::prelude::*;
373///
374/// fn setup() {
375///     hook::remove("FileWidgets");
376///     hook::add::<OnFileOpen>(|pa, builder| {
377///         builder.push(pa, LineNumbers::cfg());
378///         builder.push(pa, StatusLine::cfg());
379///     });
380///
381///     hook::remove("WindowWidgets");
382///     hook::add::<OnWindowOpen>(|pa, builder| {
383///         builder.push(pa, PromptLine::cfg());
384///     });
385/// }
386/// ```
387///
388/// In this case, each file gets a [`StatusLine`], and the window will
389/// get one [`PromptLine`], after all, what is the point of having
390/// more than one command line?
391///
392/// You can go further with this idea, like a status line on each
393/// file, that shows different information from the status line for
394/// the whole window, and so on and so forth.
395///
396/// [`File`]: crate::file::File
397/// [`OnFileOpen`]: crate::hook::OnFileOpen
398/// [`OnWindowOpen`]: crate::hook::OnWindowOpen
399/// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
400/// [`PromptLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.PromptLine.html
401pub struct WindowBuilder<U: Ui> {
402    window_i: usize,
403    area: U::Area,
404}
405
406impl<U: Ui> UiBuilder<U> for WindowBuilder<U> {
407    fn push_cfg<W: WidgetCfg<U>>(&mut self, pa: &mut Pass, cfg: W) -> (U::Area, Option<U::Area>) {
408        run_once::<W::Widget, U>();
409
410        let cfg = {
411            let wc = WidgetCreated::<W::Widget, U>((Some(cfg), None));
412            hook::trigger(pa, wc).0.0.unwrap()
413        };
414
415        let (widget, specs) = cfg.build(pa, None);
416
417        let mut windows = context::windows().borrow_mut();
418        let window = &mut windows[self.window_i];
419
420        let (child, parent) = window.push(pa, widget, &self.area, specs, false, false);
421
422        if let Some(parent) = &parent {
423            self.area = parent.clone();
424        }
425
426        (child.area().clone(), parent)
427    }
428
429    fn push_cfg_to<W: WidgetCfg<U>>(
430        &mut self,
431        pa: &mut Pass,
432        area: U::Area,
433        cfg: W,
434    ) -> (U::Area, Option<U::Area>) {
435        run_once::<W::Widget, U>();
436        let (widget, specs) = cfg.build(pa, None);
437
438        let mut windows = context::windows().borrow_mut();
439        let window = &mut windows[self.window_i];
440
441        let (node, parent) = window.push(pa, widget, &area, specs, true, false);
442
443        if area == self.area
444            && let Some(parent) = &parent
445        {
446            self.area = parent.clone();
447        }
448
449        (node.area().clone(), parent)
450    }
451}
452
453impl<U: Ui> WindowBuilder<U> {
454    /// Creates a new [`WindowBuilder`].
455    pub(crate) fn new(window_i: usize) -> Self {
456        let windows = context::windows::<U>().borrow();
457        let area = windows[window_i].files_area.clone();
458        Self { window_i, area }
459    }
460
461    /// Pushes a [widget] to an edge of the window, given a [cfg]
462    ///
463    /// This widget will be pushed to the "main" area, i.e., the area
464    /// that contains all other widgets. After that, the widget's
465    /// parent will become the main area, which can be pushed onto
466    /// again.
467    ///
468    /// This means that, if you push widget *A* to the right of the
469    /// window, then you push widget *B* to the bottom of the window,
470    /// and then you push widget *C* to the left of the window,you
471    /// will end up with something like this:
472    ///
473    /// ```text
474    /// ╭───┬───────────┬───╮
475    /// │   │           │   │
476    /// │   │ main area │ A │
477    /// │ C │           │   │
478    /// │   ├───────────┴───┤
479    /// │   │       B       │
480    /// ╰───┴───────────────╯
481    /// ```
482    ///
483    /// This method returns the [`Area`] created for this widget, as
484    /// well as an [`Area`] for the parent of the two widgets, if a
485    /// new one was created.
486    ///
487    /// [widget]: Widget
488    /// [cfg]: WidgetCfg
489    /// [`Area`]: crate::ui::Ui::Area
490    #[inline(never)]
491    pub fn push<W: WidgetAlias<U, impl BuilderDummy>>(
492        &mut self,
493        pa: &mut Pass,
494        widget: W,
495    ) -> (U::Area, Option<U::Area>) {
496        widget.push(pa, self)
497    }
498
499    /// Pushes a widget to a specific area
500    ///
501    /// Unlike [`push`], this method will push the widget to an area
502    /// that is not the main area. This can be used to create more
503    /// intricate layouts.
504    ///
505    /// For example, let's say I push a [`StatusLine`] below the main
506    /// area, and then I push a [`PromptLine`] on the left of the
507    /// status's area:
508    ///
509    /// ```rust
510    /// # use duat_core::doc_duat as duat;
511    /// setup_duat!(setup);
512    /// use duat::prelude::*;
513    ///
514    /// fn setup() {
515    ///     hook::add::<OnWindowOpen>(|pa, builder| {
516    ///         // StatusLine goes below by default
517    ///         let (status_area, _) = builder.push(pa, StatusLine::cfg());
518    ///         let prompt_cfg = PromptLine::cfg().left_ratioed(3, 5);
519    ///         builder.push_to(pa, status_area, prompt_cfg);
520    ///     });
521    /// }
522    /// ```
523    ///
524    /// The following would happen:
525    ///
526    /// ```text
527    /// ╭────────────────────────────────────╮
528    /// │                                    │
529    /// │              main area             │
530    /// │                                    │
531    /// ├─────────────┬──────────────────────┤
532    /// │ PromptLine  │      StatusLine      │
533    /// ╰─────────────┴──────────────────────╯
534    /// ```
535    ///
536    /// This is the layout that Kakoune uses by default, and is also
537    /// avaliable as commented option in the default config.
538    ///
539    /// [`push`]: Self::push
540    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
541    /// [`PromptLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.PromptLine.html
542    #[inline(never)]
543    pub fn push_to<W: WidgetCfg<U>>(
544        &mut self,
545        pa: &mut Pass,
546        area: U::Area,
547        cfg: W,
548    ) -> (U::Area, Option<U::Area>) {
549        self.push_cfg_to(pa, area, cfg)
550    }
551}
552
553/// An alias to allow generic, yet consistent utilization of
554/// [`UiBuilder`]s
555///
556/// This trait lets you do any available actions with [`UiBuilder`]s,
557/// like pushing multiple [`Widget`]s, for example.
558///
559/// The reason for using this, rather than have your type just take a
560/// [`UiBuilder`] parameter in its creation function, is mostly for
561/// consistency's sake, since [`Ui`] building is done by pushing
562/// [`Widget`]s into the builder, this lets you push multiple
563/// [`Widget`]s, without breaking that same consistency.
564pub trait WidgetAlias<U: Ui, D: BuilderDummy = WidgetCfgDummy> {
565    /// "Pushes [`Widget`]s to a [`UiBuilder`], in a specific
566    /// [`U::Area`]
567    ///
568    /// [`U::Area`]: Ui::Area
569    fn push(self, pa: &mut Pass, builder: &mut impl UiBuilder<U>) -> (U::Area, Option<U::Area>);
570}
571
572impl<W: WidgetCfg<U>, U: Ui> WidgetAlias<U> for W {
573    fn push(self, pa: &mut Pass, builder: &mut impl UiBuilder<U>) -> (U::Area, Option<U::Area>) {
574        builder.push_cfg(pa, self)
575    }
576}
577
578/// A dummy trait, meant for specialization
579pub trait BuilderDummy {}
580
581pub struct WidgetCfgDummy;
582
583impl BuilderDummy for WidgetCfgDummy {}
584
585/// Runs the [`once`] function of widgets.
586///
587/// [`once`]: Widget::once
588fn run_once<W: Widget<U>, U: Ui>() {
589    static ONCE_LIST: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
590
591    let mut once_list = ONCE_LIST.lock().unwrap();
592    if !once_list.contains(&duat_name::<W>()) {
593        W::once().unwrap();
594        once_list.push(duat_name::<W>());
595    }
596}