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