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::widgets::File
13use std::sync::LazyLock;
14
15use super::{Area, Ui};
16use crate::{
17    context::{self},
18    data::RwData,
19    duat_name,
20    widgets::{Node, Widget, WidgetCfg},
21};
22
23/// A constructor helper for [`File`] initiations
24///
25/// This helper is used primarily to push widgets around the file in
26/// question, and is only obtainable in a [`OnFileOpen`] hook:
27///
28/// ```rust
29/// # use duat_core::{
30/// #     hooks::{self, OnFileOpen},
31/// #     ui::{FileBuilder, Ui},
32/// #     widgets::{LineNumbers, Widget},
33/// # };
34/// # fn test<U: Ui>() {
35/// hooks::add::<OnFileOpen<U>>(|builder: &mut FileBuilder<U>| {
36///     builder.push(LineNumbers::cfg());
37/// });
38/// # }
39/// ```
40///
41/// In the example above, I pushed a [`LineNumbers`] widget to the
42/// [`File`]. By default, this widget will go on the left, but you can
43/// change that:
44///
45/// ```rust
46/// # use duat_core::{
47/// #     hooks::{self, OnFileOpen},
48/// #     ui::{FileBuilder, Ui},
49/// #     widgets::{LineNumbers, Widget},
50/// # };
51/// # fn test<U: Ui>() {
52/// hooks::add::<OnFileOpen<U>>(|builder: &mut FileBuilder<U>| {
53///     let line_numbers_cfg = LineNumbers::cfg().relative().on_the_right();
54///     builder.push(line_numbers_cfg);
55/// });
56/// # }
57/// ```
58///
59/// Note that I also made another change to the widget, it will now
60/// show [relative] numbers, instead of [absolute], like it usually
61/// does.
62///
63/// By default, there already exists a [hook group] that adds widgets
64/// to a file, the `"FileWidgets"` group. If you want to get rid of
65/// this group in order to create your own widget layout, you should
66/// use [`hooks::remove`]:
67///
68/// ```rust
69/// # use duat_core::{
70/// #     hooks::{self, OnFileOpen},
71/// #     ui::{FileBuilder, Ui},
72/// #     widgets::{CmdLine, LineNumbers, Widget, StatusLine},
73/// # };
74/// # fn test<U: Ui>() {
75/// hooks::remove("FileWidgets");
76/// hooks::add::<OnFileOpen<U>>(|builder: &mut FileBuilder<U>| {
77///     let line_numbers_cfg = LineNumbers::cfg().relative().on_the_right();
78///     builder.push(line_numbers_cfg);
79///     // Push a StatusLine to the bottom.
80///     builder.push(StatusLine::cfg());
81///     // Push a CmdLine to the bottom.
82///     builder.push(CmdLine::cfg());
83/// });
84/// # }
85/// ```
86///
87/// [`File`]: crate::widgets::File
88/// [`OnFileOpen`]: crate::hooks::OnFileOpen
89/// [`LineNumbers`]: crate::widgets::LineNumbers
90/// [relative]: crate::widgets::LineNumbersCfg::relative
91/// [absolute]: crate::widgets::LineNumbersCfg::absolute
92/// [hook group]: crate::hooks::add_grouped
93/// [`hooks::remove`]: crate::hooks::remove
94pub struct FileBuilder<U>
95where
96    U: Ui,
97{
98    window_i: usize,
99    node: Node<U>,
100    area: U::Area,
101    prev: Option<Node<U>>,
102}
103
104impl<U> FileBuilder<U>
105where
106    U: Ui,
107{
108    /// Creates a new [`FileBuilder`].
109    pub(crate) fn new(node: Node<U>, window_i: usize) -> Self {
110        let (_, prev) = context::set_cur(node.as_file(), node.clone()).unzip();
111        let area = node.area().clone();
112
113        Self { window_i, node, area, prev }
114    }
115
116    /// Pushes a widget to the main area of this [`File`]
117    ///
118    /// This widget will be a satellite of the file. This means that,
119    /// if the file is destroyed, the widget will be destroyed as
120    /// well, if it is moved, the widget will be moved with it, etc.
121    ///
122    /// When you push a widget, it is placed on the edge of the file.
123    /// The next widget is placed on the edge of the area containing
124    /// the file and the previous widget, and so on.
125    ///
126    /// This means that, if you push widget *A* to the left of the
127    /// file, then you push widget *B* to the bottom of the window,
128    /// you will get this layout:
129    ///
130    /// ```text
131    /// ╭───┬──────────╮
132    /// │   │          │
133    /// │ A │   File   │
134    /// │   │          │
135    /// ├───┴──────────┤
136    /// │      B       │
137    /// ╰──────────────╯
138    /// ```
139    ///
140    /// Here's an example of such a layout:
141    ///
142    /// ```rust
143    /// # use duat_core::{
144    /// #     hooks::{self, OnFileOpen},
145    /// #     ui::{FileBuilder, Ui},
146    /// #     widgets::{File, LineNumbers, Widget, common::selections_fmt, status},
147    /// # };
148    /// # fn test<U: Ui>() {
149    /// hooks::remove("FileWidgets");
150    /// hooks::add::<OnFileOpen<U>>(|builder: &mut FileBuilder<U>| {
151    ///     let line_numbers_cfg = LineNumbers::cfg().rel_abs();
152    ///     builder.push(line_numbers_cfg);
153    ///
154    ///     let status_line_cfg = status!({ File::name } " " selections_fmt);
155    ///     builder.push(status_line_cfg);
156    /// });
157    /// # }
158    /// ```
159    ///
160    /// In this case, each file will have [`LineNumbers`] with
161    /// [`relative/absolute`] numbering, and a [`StatusLine`] showing
162    /// the file's name and how many selections are in it.
163    ///
164    /// [`File`]: crate::widgets::File
165    /// [`LineNumbers`]: crate::widgets::LineNumbers
166    /// [`relative/absolute`]: crate::widgets::LineNumbersCfg::rel_abs
167    /// [`StatusLine`]: crate::widgets::StatusLine
168    pub fn push<W: Widget<U>>(
169        &mut self,
170        cfg: impl WidgetCfg<U, Widget = W>,
171    ) -> (U::Area, Option<U::Area>) {
172        run_once::<W, U>();
173        let (widget, checker, specs) = cfg.build(true);
174
175        let mut windows = context::windows().write();
176        let window = &mut windows[self.window_i];
177
178        let (child, parent) = {
179            let (node, parent) = window.push(widget, &self.area, checker, specs, true);
180
181            if let Some(related) = self.node.related_widgets() {
182                related.write().push(node.clone())
183            }
184
185            if let Some(parent) = &parent {
186                if parent.is_master_of(&window.files_area) {
187                    window.files_area = parent.clone();
188                }
189            }
190
191            (node.area().clone(), parent)
192        };
193
194        if let Some(parent) = &parent {
195            self.area = parent.clone();
196        }
197
198        (child, parent)
199    }
200
201    /// Pushes a widget to a specific area around a [`File`]
202    ///
203    /// This method can be used to get some more advanced layouts,
204    /// where you have multiple widgets parallel to each other, yet on
205    /// the same edge.
206    ///
207    /// One example of where you might want to do this is if you want
208    /// to have multiple [`StatusLine`]s in a single [`File`], each
209    /// showing their own distinct information:
210    ///
211    /// ```rust
212    /// # fn mode_fmt(file: &File) -> Text {
213    /// #     todo!();
214    /// # }
215    /// # use duat_core::{
216    /// #     hooks::{self, OnFileOpen},
217    /// #     text::Text,
218    /// #     ui::{FileBuilder, Ui},
219    /// #     widgets::{
220    /// #         CmdLine, File, LineNumbers, Widget,
221    /// #         common::{selections_fmt, main_fmt}, status
222    /// #     },
223    /// # };
224    /// # fn test<U: Ui>() {
225    /// hooks::remove("FileWidgets");
226    /// hooks::add::<OnFileOpen<U>>(|builder: &mut FileBuilder<U>| {
227    ///     builder.push(LineNumbers::cfg());
228    ///     
229    ///     let right_status = status!(
230    ///         { File::name } " " selections_fmt " " main_fmt
231    ///     );
232    ///     let (right_status_area, _) = builder.push(right_status);
233    ///
234    ///     let left_status = status!(mode_fmt).push_left();
235    ///     builder.push_to(left_status, right_status_area);
236    ///
237    ///     builder.push(CmdLine::cfg());
238    /// });
239    /// # }
240    /// ```
241    ///
242    /// [`File`]: crate::widgets::File
243    /// [`StatusLine`]: crate::widgets::StatusLine
244    pub fn push_to<W: Widget<U>>(
245        &self,
246        cfg: impl WidgetCfg<U, Widget = W>,
247        area: U::Area,
248    ) -> (U::Area, Option<U::Area>) {
249        run_once::<W, U>();
250        let (widget, checker, specs) = cfg.build(true);
251
252        let mut windows = context::windows().write();
253        let window = &mut windows[self.window_i];
254
255        let (node, parent) = window.push(widget, &area, checker, specs, true);
256        if let Some(related) = self.node.related_widgets() {
257            related.write().push(node.clone())
258        }
259        (node.area().clone(), parent)
260    }
261}
262
263impl<U: Ui> Drop for FileBuilder<U> {
264    fn drop(&mut self) {
265        if let Some(prev) = self.prev.take() {
266            context::set_cur(prev.as_file(), prev);
267        }
268    }
269}
270
271/// A constructor helper for window initiations
272///
273/// This helper is used primarily to push widgets around the window,
274/// surrounding the "main area" which contains all [`File`]s and
275/// widgets added via the [`OnFileOpen`] hook.
276///
277/// It is used whenever the [`OnWindowOpen`] hook is triggered, which
278/// happens whenever a new window is opened:
279///
280/// ```rust
281/// # use duat_core::{
282/// #     hooks::{self, OnWindowOpen},
283/// #     ui::{Ui, WindowBuilder},
284/// #     widgets::{CmdLine, Widget, StatusLine},
285/// # };
286/// # fn test<U: Ui>() {
287/// hooks::add::<OnWindowOpen<U>>(|builder: &mut WindowBuilder<U>| {
288///     // Push a StatusLine to the bottom.
289///     builder.push(StatusLine::cfg());
290///     // Push a CmdLine to the bottom.
291///     builder.push(CmdLine::cfg());
292/// });
293/// # }
294/// ```
295///
296/// Contrast this with the example in the [`FileBuilder`] docs, where
297/// a similar hook is added, but with [`OnFileOpen`] instead of
298/// [`OnWindowOpen`]. In that scenario, the widgets are added to each
299/// file that is opened, while in this one, only one instance of these
300/// widgets will be created per window.
301///
302/// The existence of these two hooks lets the user make some more
303/// advanced choices on the layout:
304///
305/// ```rust
306/// # use duat_core::{
307/// #     hooks::{self, OnFileOpen, OnWindowOpen},
308/// #     ui::{WindowBuilder, Ui},
309/// #     widgets::{CmdLine, LineNumbers, Widget, StatusLine},
310/// # };
311/// # fn test<U: Ui>() {
312/// hooks::remove("FileWidgets");
313/// hooks::add::<OnFileOpen<U>>(|builder| {
314///     builder.push(LineNumbers::cfg());
315///     builder.push(StatusLine::cfg());
316/// });
317///
318/// hooks::remove("WindowWidgets");
319/// hooks::add::<OnWindowOpen<U>>(|builder| {
320///     builder.push(CmdLine::cfg());
321/// });
322/// # }
323/// ```
324///
325/// In this case, each file gets a [`StatusLine`], and the window will
326/// get one [`CmdLine`], after all, what is the point of having
327/// more than one command line?
328///
329/// You can go further with this idea, like a status line on each
330/// file, that shows different information from the status line for
331/// the whole window, and so on and so forth.
332///
333/// [`File`]: crate::widgets::File
334/// [`OnFileOpen`]: crate::hooks::OnFileOpen
335/// [`OnWindowOpen`]: crate::hooks::OnWindowOpen
336/// [`StatusLine`]: crate::widgets::StatusLine
337/// [`CmdLine`]: crate::widgets::CmdLine
338pub struct WindowBuilder<U: Ui> {
339    window_i: usize,
340    area: U::Area,
341}
342
343impl<U: Ui> WindowBuilder<U> {
344    /// Creates a new [`WindowBuilder`].
345    pub(crate) fn new(window_i: usize) -> Self {
346        let windows = context::windows::<U>().read();
347        let area = windows[window_i].files_area.clone();
348        Self { window_i, area }
349    }
350
351    /// Pushes a [widget] to an edge of the window, given a [cfg]
352    ///
353    /// This widget will be pushed to the "main" area, i.e., the area
354    /// that contains all other widgets. After that, the widget's
355    /// parent will become the main area, which can be pushed onto
356    /// again.
357    ///
358    /// This means that, if you push widget *A* to the right of the
359    /// window, then you push widget *B* to the bottom of the window,
360    /// and then you push widget *C* to the left of the window,you
361    /// will end up with something like this:
362    ///
363    /// ```text
364    /// ╭───┬───────────┬───╮
365    /// │   │           │   │
366    /// │   │ main area │ A │
367    /// │ C │           │   │
368    /// │   ├───────────┴───┤
369    /// │   │       B       │
370    /// ╰───┴───────────────╯
371    /// ```
372    ///
373    /// This method returns the [`Area`] created for this widget, as
374    /// well as an [`Area`] for the parent of the two widgets, if a
375    /// new one was created.
376    ///
377    /// [widget]: Widget
378    /// [cfg]: WidgetCfg
379    pub fn push<W: Widget<U>>(
380        &mut self,
381        cfg: impl WidgetCfg<U, Widget = W>,
382    ) -> (U::Area, Option<U::Area>) {
383        run_once::<W, U>();
384        let (widget, checker, specs) = cfg.build(false);
385
386        let mut windows = context::windows().write();
387        let window = &mut windows[self.window_i];
388
389        let (child, parent) = window.push(widget, &self.area, checker, specs, false);
390
391        if let Some(parent) = &parent {
392            self.area = parent.clone();
393        }
394
395        (child.area().clone(), parent)
396    }
397
398    /// Pushes a widget to a specific area
399    ///
400    /// Unlike [`push`], this method will push the widget to an area
401    /// that is not the main area. This can be used to create more
402    /// intricate layouts.
403    ///
404    /// For example, let's say I push a [`StatusLine`] below the main
405    /// area, and then I push a [`CmdLine`] on the left of the
406    /// status's area:
407    ///
408    /// ```rust
409    /// # use duat_core::{
410    /// #     ui::{Ui, WindowBuilder},
411    /// #     widgets::{CmdLine, Widget, StatusLine},
412    /// # };
413    /// # fn test<U: Ui>(builder: &mut WindowBuilder<U>) {
414    /// // StatusLine goes below by default
415    /// let (status_area, _) = builder.push(StatusLine::cfg());
416    /// let cmd_line_cfg = CmdLine::cfg().left_ratioed(3, 5);
417    /// builder.push_to(cmd_line_cfg, status_area);
418    /// # }
419    /// ```
420    ///
421    /// The following would happen:
422    ///
423    /// ```text
424    /// ╭────────────────────────────────────╮
425    /// │                                    │
426    /// │              main area             │
427    /// │                                    │
428    /// ├─────────────┬──────────────────────┤
429    /// │ CmdLine │      StatusLine      │
430    /// ╰─────────────┴──────────────────────╯
431    /// ```
432    ///
433    /// This is the layout that Kakoune uses by default, and the
434    /// default for Duat as well.
435    ///
436    /// [`push`]: Self::push
437    /// [`StatusLine`]: crate::widgets::StatusLine
438    /// [`CmdLine`]: crate::widgets::CmdLine
439    pub fn push_to<W: Widget<U>>(
440        &self,
441        cfg: impl WidgetCfg<U, Widget = W>,
442        area: U::Area,
443    ) -> (U::Area, Option<U::Area>) {
444        run_once::<W, U>();
445        let (widget, checker, specs) = cfg.build(false);
446
447        let mut windows = context::windows().write();
448        let window = &mut windows[self.window_i];
449
450        let (node, parent) = window.push(widget, &area, checker, specs, true);
451
452        (node.area().clone(), parent)
453    }
454}
455
456/// Runs the [`once`] function of widgets.
457///
458/// [`once`]: Widget::once
459fn run_once<W: Widget<U>, U: Ui>() {
460    static ONCE_LIST: LazyLock<RwData<Vec<&'static str>>> =
461        LazyLock::new(|| RwData::new(Vec::new()));
462
463    let mut once_list = ONCE_LIST.write();
464    if !once_list.contains(&duat_name::<W>()) {
465        W::once().unwrap();
466        once_list.push(duat_name::<W>());
467    }
468}