duat_core/ui/window/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 super::{AreaId, GetAreaId, Ui, Widget, WidgetCfg};
14use crate::{
15 context::{self, Handle},
16 data::Pass,
17 file::File,
18 hook::{self, WidgetCreated},
19};
20
21/// A struct for determining what the how the Ui should be constructed
22///
23/// This struct is used from the [`WidgetCreated`] [hook] every time a
24/// new [`Widget`] is created, in order to let you push other
25/// [`Widget`]s around that one. This makes the extension of Duat's
26/// interface quite friendly.
27///
28/// It will also be called once per window, from the [`WindowCreated`]
29/// [hook], letting you place [`Widget`]s "around" the common [`File`]
30/// area, at the edges of the screen.
31///
32/// # Examples
33///
34/// Here's some examples involving one of the main ways this will be
35/// done, in the [`File`] [`Widget`]:
36///
37/// ```rust
38/// # use duat_core::doc_duat as duat;
39/// setup_duat!(setup);
40/// use duat::prelude::*;
41///
42/// fn setup() {
43/// hook::add::<File>(|_, (cfg, builder)| {
44/// builder.push(LineNumbers::cfg());
45/// cfg
46/// });
47/// }
48/// ```
49///
50/// In the example above, I pushed a [`LineNumbers`] widget to the
51/// [`File`]. By default, this widget will go on the left, but you can
52/// change that:
53///
54/// ```rust
55/// # use duat_core::doc_duat as duat;
56/// setup_duat!(setup);
57/// use duat::prelude::*;
58///
59/// fn setup() {
60/// hook::add::<File>(|_, (cfg, builder)| {
61/// let line_numbers_cfg = LineNumbers::cfg().relative().on_the_right();
62/// builder.push(line_numbers_cfg);
63/// cfg
64/// });
65/// }
66/// ```
67///
68/// Note that I also made another change to the widget, it will now
69/// show relative numbers, instead of absolute, like it usually does.
70///
71/// By default, there already exists a [hook group] that adds widgets
72/// to a file, the `"FileWidgets"` group. If you want to get rid of
73/// this group in order to create your own widget layout, you should
74/// use [`hook::remove`]:
75///
76/// ```rust
77/// # use duat_core::doc_duat as duat;
78/// setup_duat!(setup);
79/// use duat::prelude::*;
80///
81/// fn setup() {
82/// hook::remove("FileWidgets");
83/// hook::add::<File>(|_, (cfg, builder)| {
84/// builder.push(LineNumbers::cfg().relative().on_the_right());
85/// // Push a StatusLine to the bottom.
86/// builder.push(StatusLine::cfg());
87/// // Push a PromptLine to the bottom.
88/// builder.push(PromptLine::cfg());
89/// cfg
90/// });
91/// }
92/// ```
93///
94/// [`File`]: crate::file::File
95/// [`LineNumbers`]: https://crates.io/duat-utils/latest/duat_utils/wigets/struct.LineNumbers.html
96/// [hook group]: crate::hook::add_grouped
97/// [`hook::remove`]: crate::hook::remove
98/// [`WindowCreated`]: crate::hook::WindowCreated
99pub struct UiBuilder<U: Ui> {
100 win: usize,
101 widget_id: Option<AreaId>,
102 master_id: Option<AreaId>,
103 main: Option<Handle<dyn Widget<U>, U>>,
104 builder_fn: Option<BuilderFn<U>>,
105}
106
107impl<U: Ui> UiBuilder<U> {
108 /// Returns a new [`UiBuilder`], with no main [`Widget`]
109 ///
110 /// This is used when creating
111 pub(super) fn new_main(win: usize, widget_id: AreaId) -> Self {
112 Self {
113 win,
114 widget_id: Some(widget_id),
115 master_id: None,
116 main: None,
117 builder_fn: None,
118 }
119 }
120
121 /// A [`UiBuilder`] specific to [`Window`] creation, has no
122 /// `widget_id`
123 ///
124 /// [`Window`]: super::Window
125 pub(super) fn new_window(win: usize) -> Self {
126 Self {
127 win,
128 widget_id: None,
129 master_id: None,
130 main: None,
131 builder_fn: None,
132 }
133 }
134
135 /// Returns a new [`UiBuilder`] with a predefined main [`Widget`]
136 fn pushed(
137 win: usize,
138 widget_id: AreaId,
139 master_id: AreaId,
140 main: Option<Handle<dyn Widget<U>, U>>,
141 ) -> Self {
142 Self {
143 win,
144 widget_id: Some(widget_id),
145 master_id: Some(master_id),
146 main,
147 builder_fn: None,
148 }
149 }
150
151 /// Pushes a widget to the main area of the [`UiBuilder`]
152 ///
153 /// If this [`Widget`] is being pushed to a [`File`]'s group,
154 /// then this [`Widget`] will be included in that [`File`]'s
155 /// group. This means that, if that [`File`] is moved around
156 /// or deleted, this [`Widget`] (and all others in its group)
157 /// will follow suit.
158 ///
159 /// When you push a [`Widget`], it is placed on an edge of the
160 /// area, and a new parent area may be created to hold both
161 /// widgets. If created, that new area will be used for pushing
162 /// widgets in the future.
163 ///
164 /// This means that, if you push widget *A* to the left, then you
165 /// push widget *B* to the bottom, you will get this layout:
166 ///
167 /// ```text
168 /// ╭───┬──────────╮
169 /// │ │ │
170 /// │ A │ File │
171 /// │ │ │
172 /// ├───┴──────────┤
173 /// │ B │
174 /// ╰──────────────╯
175 /// ```
176 ///
177 /// Here's an example of such a layout:
178 ///
179 /// ```rust
180 /// # use duat_core::doc_duat as duat;
181 /// setup_duat!(setup);
182 /// use duat::prelude::*;
183 ///
184 /// fn setup() {
185 /// hook::remove("FileWidgets");
186 /// hook::add::<File>(|_, (cfg, builder)| {
187 /// builder.push(LineNumbers::cfg().rel_abs());
188 /// builder.push(status!("{name_txt} {selections_txt} {main_txt}"));
189 /// cfg
190 /// });
191 /// }
192 /// ```
193 ///
194 /// In this case, each file will have [`LineNumbers`] with
195 /// relative/absolute numbering, and a [`StatusLine`] showing
196 /// the file's name, how many selections are in it, and its main
197 /// selection.
198 ///
199 /// [`File`]: crate::file::File
200 /// [`LineNumbers`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbers.html
201 /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
202 /// [`WindowCreated`]: crate::hook::WindowCreated
203 pub fn push<Cfg: WidgetAlias<U, impl BuilderDummy>>(&mut self, widget: Cfg) -> AreaId {
204 widget.push_alias(&mut RawUiBuilder(self))
205 }
206
207 /// Pushes a widget to a specific [`AreaId`]
208 ///
209 /// This method can be used to get some more advanced layouts,
210 /// where you have multiple widgets parallel to each other, yet on
211 /// the same edge.
212 ///
213 /// One of the main ways in which this is used is when using a
214 /// hidden [`Notifications`] widget alongside the [`PromptLine`]
215 /// widget.
216 ///
217 /// ```rust
218 /// # use duat_core::doc_duat as duat;
219 /// setup_duat!(setup);
220 /// use duat::prelude::*;
221 ///
222 /// fn setup() {
223 /// hook::remove("FileWidgets");
224 /// hook::add::<File>(|_, (cfg, builder)| {
225 /// builder.push(LineNumbers::cfg());
226 ///
227 /// let status_cfg = status!("{name_txt} {selections_txt} {main_txt}");
228 /// let child = builder.push(status_cfg);
229 /// let prompt_cfg = PromptLine::cfg().left_ratioed(3, 5);
230 /// let child = builder.push_to(child, prompt_cfg);
231 /// builder.push_to(child, Notifications::cfg());
232 /// cfg
233 /// });
234 /// }
235 /// ```
236 ///
237 /// Pushing directly to the [`PromptLine`]'s [`U::Area`] means
238 /// that they'll share a parent that holds only them. This is
239 /// actually done in a more convenient way through the
240 /// [`FooterWidgets`] widget group from [`duat-utils`]
241 ///
242 /// [`File`]: crate::file::File
243 /// [`Notifications`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.Notifications.html
244 /// [`PromptLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.PromptLine.html
245 /// [`U::Area`]: Ui::Area
246 /// [hook group]: crate::hook::add_grouped
247 /// [`FooterWidgets`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.FooterWidgets.html
248 /// [`duat-utils`]: https://docs.rs/duat-utils/latest/duat_utils
249 pub fn push_to<Cfg: WidgetCfg<U>>(&mut self, area: AreaId, cfg: Cfg) -> AreaId {
250 RawUiBuilder(self).push_to(area, cfg)
251 }
252
253 /// The [`Handle<File>`] of this [`UiBuilder`], if there is one
254 ///
255 /// This will be [`Some`] when this [`Widget`] is being pushed
256 /// around a [`File`], and [`None`] otherwise.
257 ///
258 /// [`File`]: crate::file::File
259 pub fn file(&self) -> Option<Handle<File<U>, U>> {
260 self.main.as_ref().and_then(Handle::try_downcast)
261 }
262
263 /// Runs the post [`Widget`] construction [hook]
264 ///
265 /// This does NOT push the widget into the [`Window`], as that is
266 /// supposed to have happened already.
267 ///
268 /// [`Window`]: super::Window
269 pub(super) fn finish_around_widget(
270 self,
271 pa: &mut Pass,
272 parent_id: Option<AreaId>,
273 handle: Handle<dyn Widget<U>, U>,
274 ) -> AreaId {
275 let do_cluster = self.main.is_some();
276
277 let master_id = parent_id
278 .filter(|_| do_cluster)
279 .or(self.master_id)
280 .or(self.main.as_ref().map(|h| h.area_id()))
281 .unwrap_or(handle.area_id());
282
283 if let Some(builder_fn) = self.builder_fn {
284 builder_fn(pa, master_id, self.main.or(Some(handle)))
285 } else {
286 master_id
287 }
288 }
289
290 /// Finishes building around a [`Window`]'s central [`File`]
291 /// region
292 ///
293 /// [`Window`]: super::Window
294 pub(super) fn finish_around_window(self, pa: &mut Pass, window_id: AreaId) -> AreaId {
295 if let Some(builder_fn) = self.builder_fn {
296 builder_fn(pa, window_id, None)
297 } else {
298 window_id
299 }
300 }
301
302 ////////// Querying functions
303
304 /// The [`AreaId`] for the [`Widget`] being built
305 pub fn widget_id(&self) -> Option<AreaId> {
306 self.widget_id
307 }
308}
309
310/// A struct over the regular [`UiBuilder`], which grants direct
311/// access to ui control primitives
312pub struct RawUiBuilder<'a, U: Ui>(&'a mut UiBuilder<U>);
313
314impl<U: Ui> RawUiBuilder<'_, U> {
315 /// Raw pushing of [`Widget`]s by [`WidgetAlias`]es
316 pub fn push<Cfg: WidgetCfg<U>>(&mut self, cfg: Cfg) -> AreaId {
317 let prev_build_fn = self.0.builder_fn.take();
318 let widget_id = AreaId::new();
319
320 let win = self.0.win;
321 self.0.builder_fn = Some(Box::new(move |pa, mut master_id, main| {
322 let do_cluster = main.is_some();
323 let on_file = main
324 .as_ref()
325 .is_some_and(|w| w.widget().data_is::<File<U>>());
326
327 if let Some(prev) = prev_build_fn {
328 master_id = prev(pa, master_id, main.clone());
329 }
330
331 let (cfg, builder) = {
332 let builder = UiBuilder::pushed(win, widget_id, master_id, main.clone());
333 let wc = WidgetCreated::<Cfg::Widget, U>((Some(cfg), builder));
334 let (cfg, builder) = hook::trigger(pa, wc).0;
335 (cfg.unwrap(), builder)
336 };
337
338 let (widget, specs) = cfg.build(pa, BuildInfo { main, pushed_to: None });
339
340 let (node, parent_id) = context::windows().push(
341 pa,
342 win,
343 (widget, specs),
344 (master_id, widget_id),
345 (do_cluster, on_file),
346 );
347
348 if let Some(main) = builder.main.as_ref() {
349 main.related().write(pa).push(node.handle().clone())
350 }
351
352 if let Some(builder_fn) = builder.builder_fn {
353 builder_fn(pa, widget_id, builder.main.or(Some(node.handle().clone())));
354 }
355
356 parent_id.unwrap_or(master_id)
357 }));
358
359 widget_id
360 }
361
362 /// Same as [`UiBuilder::push_to`]
363 pub fn push_to<Cfg: WidgetCfg<U>>(&mut self, to_id: AreaId, cfg: Cfg) -> AreaId {
364 let prev_build_fn = self.0.builder_fn.take();
365 let widget_id = AreaId::new();
366
367 let win = self.0.win;
368 self.0.builder_fn = Some(Box::new(move |pa, mut master_id, main| {
369 let do_cluster = main.is_some();
370 let on_file = main
371 .as_ref()
372 .is_some_and(|h| h.widget().data_is::<File<U>>());
373
374 if let Some(prev) = prev_build_fn {
375 master_id = prev(pa, master_id, main.clone());
376 }
377
378 let (cfg, builder) = {
379 let builder = UiBuilder::pushed(win, widget_id, master_id, main);
380 let wc = WidgetCreated::<Cfg::Widget, U>((Some(cfg), builder));
381 let (cfg, builder) = hook::trigger(pa, wc).0;
382 (cfg.unwrap(), builder)
383 };
384
385 let (widget, specs) = cfg.build(pa, BuildInfo {
386 main: builder.main.clone(),
387 pushed_to: None,
388 });
389
390 let (node, parent_id) = context::windows().push(
391 pa,
392 win,
393 (widget, specs),
394 (to_id, widget_id),
395 (do_cluster, on_file),
396 );
397
398 if let Some(main) = builder.main.as_ref() {
399 main.related().write(pa).push(node.handle().clone())
400 }
401
402 if let Some(builder_fn) = builder.builder_fn {
403 builder_fn(pa, widget_id, builder.main.or(Some(node.handle().clone())));
404 }
405
406 parent_id.unwrap_or(master_id)
407 }));
408
409 widget_id
410 }
411}
412
413/// An alias to allow generic, yet consistent utilization of
414/// [`UiBuilder`]s
415///
416/// This trait lets you do any available actions with [`UiBuilder`]s,
417/// like pushing multiple [`Widget`]s, for example.
418///
419/// The reason for using this, rather than have your type just take a
420/// [`UiBuilder`] parameter in its creation function, is mostly for
421/// consistency's sake, since [`Ui`] building is done by pushing
422/// [`Widget`]s into the builder, this lets you push multiple
423/// [`Widget`]s, without breaking that same consistency.
424pub trait WidgetAlias<U: Ui, D: BuilderDummy = WidgetCfgDummy> {
425 /// "Pushes [`Widget`]s to a [`UiBuilder`], in a specific
426 /// [`U::Area`]
427 ///
428 /// [`U::Area`]: Ui::Area
429 fn push_alias(self, builder: &mut RawUiBuilder<U>) -> AreaId;
430}
431
432impl<Cfg: WidgetCfg<U>, U: Ui> WidgetAlias<U> for Cfg {
433 fn push_alias(self, builder: &mut RawUiBuilder<U>) -> AreaId {
434 builder.push(self)
435 }
436}
437
438/// A dummy trait, meant for specialization
439pub trait BuilderDummy {}
440
441#[doc(hidden)]
442pub struct WidgetCfgDummy;
443
444impl BuilderDummy for WidgetCfgDummy {}
445
446/// Information about the construction of the [`Ui`]
447pub struct BuildInfo<U: Ui> {
448 main: Option<Handle<dyn Widget<U>, U>>,
449 pushed_to: Option<Handle<dyn Widget<U>, U>>,
450}
451
452impl<U: Ui> BuildInfo<U> {
453 pub(super) fn for_main() -> Self {
454 Self { main: None, pushed_to: None }
455 }
456
457 /// Returns the [`File`]'s [`Handle`], if this [`Widget`] was
458 /// pushed to one
459 pub fn file(&self) -> Option<Handle<File<U>, U>> {
460 self.main.as_ref().and_then(Handle::try_downcast)
461 }
462
463 /// The [`Handle`] of the main [`Widget`] from this group
464 ///
465 /// Will return [`None`] when the [`Widget`] being built _is_ the
466 /// main [`Widget`], which could happen if it was a spawned
467 /// floating [`Widget`], for example.
468 ///
469 /// This differs from [`pushed_to`], since this will return the
470 /// [`Widget`] at the "top level of construction". For example, if
471 /// a [`File`] is added, a [`VertRule`] is pushed, and a
472 /// [`LineNumbers`] is pushed to the [`VertRule`], when calling
473 /// [`LineNumbersOptions::build`], [`BuildInfo::main`] will
474 /// return the [`File`], while [`BuildInfo::pushed_to`] will
475 /// return the [`VertRule`].
476 ///
477 /// [`pushed_to`]: Self::pushed_to
478 /// [`VertRule`]: https://docs.rs/duat/latest/duat/prelude/struct.VertRule.html
479 /// [`LineNumbers`]: https://docs.rs/duat/latest/duat/prelude/struct.LineNumbers.html
480 /// [`LineNumbersOptions::build`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbersOptions.html
481 pub fn main(&self) -> Option<Handle<dyn Widget<U>, U>> {
482 self.main.clone()
483 }
484
485 /// The [`Handle`] of the [`Widget`] this one was pushed to
486 ///
487 /// Will return [`None`] when this [`Widget`] wasn't pushed onto
488 /// any other, which could happen if it was a spawned floating
489 /// [`Widget`], for example.
490 ///
491 /// This differs from [`main`], since that one will return the
492 /// [`Widget`] at the "top level of construction". For example, if
493 /// a [`File`] is added, a [`VertRule`] is pushed, and a
494 /// [`LineNumbers`] is pushed to the [`VertRule`], when calling
495 /// [`LineNumbersOptions::build`], [`BuildInfo::main`] will
496 /// return the [`File`], while [`BuildInfo::pushed_to`] will
497 /// return the [`VertRule`].
498 ///
499 /// [`main`]: Self::main
500 /// [`VertRule`]: https://docs.rs/duat/latest/duat/prelude/struct.VertRule.html
501 /// [`LineNumbers`]: https://docs.rs/duat/latest/duat/prelude/struct.LineNumbers.html
502 /// [`LineNumbersOptions::build`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbersOptions.html
503 pub fn pushed_to(&self) -> Option<Handle<dyn Widget<U>, U>> {
504 self.pushed_to.clone()
505 }
506}
507
508type BuilderFn<U> = Box<dyn FnOnce(&mut Pass, AreaId, Option<Handle<dyn Widget<U>, U>>) -> AreaId>;