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}