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}