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}