duat_core/ui/
mod.rs

1//! [Ui] structs and functions
2//!
3//! Although there is only a terminal [Ui] implemented at the
4//! moment, Duat is supposed to be Ui agnostic, and I plan to create a
5//! GUI app (probably in `gpui` or something), and a web app as well,
6//! which is honestly more of an excuse for me to become more well
7//! versed on javascript.
8//!
9//! Each [Ui] is essentially a screen separated by a bunch of
10//! [`Ui::Area`]s. This happens by splitting a main `Ui::Area`
11//! continuously, by pushing [`Widget`]s on other `Widget`s. When a
12//! `Widget` is pushed to another, the area of the prior `Widget`
13//! is split in half, with [`PushSpecs`] defining information about
14//! the new area.
15//!
16//! Additionally, [`Widget`]s may be spawned via various methods, such
17//! as [on `Handle`]s, [on `Text`], or even [around the `Window`]
18//!
19//! Duat also supports multiple [`Window`]s in a [`Windows`] struct,
20//! each of which is defined by a main [`Ui::Area`] that was split
21//! many times over. This `Windows` struct is accessible in
22//! [`context::windows`], and you are free to inspect and mutate
23//! whatever state is in there.
24//!
25//! The [Ui] also supports the concept of "clustering", that is,
26//! when you push a [`Widget`] to a [`Buffer`], it gets "clustered" to
27//! that `Buffer`. This means a few things. For one, if you close that
28//! `Buffer`, all of its clustered `Widget`s will also close. If
29//! you swap two `Buffer`s, what you will actually swap is the
30//! [`Ui::Area`] that contains the `Buffer` and all of its clustered
31//! `Widget`.
32//!
33//! Additionally, on the terminal [Ui], clustering is used to
34//! determine where to draw borders between [`Ui::Area`]s, and it
35//! should be used like that in other [Ui] implementations as well.
36//!
37//! [`hook`]: crate::hook
38//! [`Buffer`]: crate::buffer::Buffer
39//! [`WidgetCreated`]: crate::hook::WidgetCreated
40//! [Ui]: traits::RawUi
41//! [`Ui::Area`]: traits::RawUi::Area
42//! [on `Handle`]: Handle::spawn_widget
43//! [on `Text`]: crate::text::SpawnTag
44//! [`context::windows`]: crate::context::windows
45use std::fmt::Debug;
46
47pub(crate) use self::widget::Node;
48pub use self::{
49    type_erased::{Area, PrintInfo, RwArea, Ui},
50    widget::Widget,
51    window::{Window, Windows},
52};
53use crate::{context::Handle, data::Pass};
54
55pub mod layout;
56pub mod traits;
57mod type_erased;
58mod widget;
59mod window;
60
61/// A coordinate on screen
62///
63/// An integer value should represent the size of a monospaced font
64/// cell. So, for example, in a terminal, x should represent the top
65/// left corner of a column, and y represents the top left corner of a
66/// row.
67///
68/// For non terminal GUIs, an integer should have the same
69/// representation, but fractional values should be permitted as well.
70#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
71pub struct Coord {
72    /// The x value of this coordinate. In a terminal cell, it would
73    /// be the top left corner.
74    pub x: f32,
75    /// The y value of this coordinate. In a terminal cell, it would
76    /// be the top left corner.
77    pub y: f32,
78}
79
80/// A dimension on screen, can either be horizontal or vertical
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Axis {
83    /// The horizontal axis
84    Horizontal,
85    /// The vertical axis
86    Vertical,
87}
88
89impl Axis {
90    /// The [`Axis`] perpendicular to this one
91    pub fn perp(&self) -> Self {
92        match self {
93            Axis::Horizontal => Axis::Vertical,
94            Axis::Vertical => Axis::Horizontal,
95        }
96    }
97
98    /// Returns `true` if the axis is [`Horizontal`].
99    ///
100    /// [`Horizontal`]: Axis::Horizontal
101    #[must_use]
102    pub fn is_hor(&self) -> bool {
103        matches!(self, Self::Horizontal)
104    }
105
106    /// Returns `true` if the axis is [`Vertical`].
107    ///
108    /// [`Vertical`]: Axis::Vertical
109    #[must_use]
110    pub fn is_ver(&self) -> bool {
111        matches!(self, Self::Vertical)
112    }
113}
114
115impl From<PushSpecs> for Axis {
116    fn from(value: PushSpecs) -> Self {
117        match value.side {
118            Side::Above | Side::Below => Axis::Vertical,
119            _ => Axis::Horizontal,
120        }
121    }
122}
123
124/// Information on how a [`Widget`] should be pushed onto another
125///
126/// This information is composed of four parts:
127///
128/// * A side to push;
129/// * An optional width;
130/// * An optional height;
131/// * Wether to hide it by default;
132/// * wether to cluster the [`Widget`]
133///
134/// Constraints are demands that must be met by the widget's
135/// [`Area`], on a best effort basis.
136///
137/// So, for example, if the [`PushSpecs`] are:
138///
139/// ```rust
140/// # duat_core::doc_duat!(duat);
141/// use duat::prelude::*;
142/// let specs = ui::PushSpecs {
143///     side: ui::Side::Left,
144///     width: Some(3.0),
145///     height: None,
146///     hidden: false,
147///     cluster: true,
148/// };
149/// ```
150///
151/// Then the widget should be pushed to the left, with a width of 3,
152/// an unspecified height, _not_ hidden by default and clustered if
153/// possible. Note that you can shorten the definition above:
154///
155/// ```rust
156/// # duat_core::doc_duat!(duat);
157/// use duat::prelude::*;
158/// let specs = ui::PushSpecs {
159///     side: ui::Side::Left,
160///     width: Some(3.0),
161///     ..Default::default()
162/// };
163/// ```
164///
165/// Since the remaining values are the default.
166#[derive(Clone, Copy, Debug)]
167pub struct PushSpecs {
168    /// Which [`Side`] to push the [`Widget`] to
169    pub side: Side,
170    /// A width (in character cells) for this `Widget`
171    ///
172    /// Note that this may be ignored if it is not possible to
173    /// create an area big (or small) enough.
174    pub width: Option<f32>,
175    /// A height (in lines) for this `Widget`
176    ///
177    /// Note that this may be ignored if it is not possible to
178    /// create an area big (or small) enough.
179    pub height: Option<f32>,
180    /// Hide this `Widget` by default
181    ///
182    /// You can call [`Area::hide`] or [`Area::reveal`] to toggle
183    /// this property.
184    pub hidden: bool,
185    /// Cluster this `Widget` when pushing
186    ///
187    /// This makes it so, if the main `Widget` is moved or deleted,
188    /// then this one will follow. Useful for things like
189    /// [`LineNumbers`], since they should follow their [`Buffer`]
190    /// around.
191    ///
192    /// [`LineNumbers`]: https://docs.rs/duat/latest/duat/widgets/struct.LineNumbers.html
193    /// [`Buffer`]: crate::buffer::Buffer
194    pub cluster: bool,
195}
196
197impl Default for PushSpecs {
198    fn default() -> Self {
199        Self {
200            side: Side::Right,
201            width: None,
202            height: None,
203            hidden: false,
204            cluster: true,
205        }
206    }
207}
208
209impl PushSpecs {
210    /// The [`Axis`] where it will be pushed
211    ///
212    /// - left/right: [`Axis::Horizontal`]
213    /// - above/below: [`Axis::Vertical`]
214    pub const fn axis(&self) -> Axis {
215        match self.side {
216            Side::Above | Side::Below => Axis::Vertical,
217            Side::Right | Side::Left => Axis::Horizontal,
218        }
219    }
220
221    /// Wether this "comes earlier" on the screen
222    ///
223    /// This returns true if `self.side() == Side::Left || self.side()
224    /// == Side::Above`, since that is considered "earlier" on
225    /// screens.
226    pub const fn comes_earlier(&self) -> bool {
227        matches!(self.side, Side::Left | Side::Above)
228    }
229
230    /// The constraints on a given [`Axis`]
231    pub fn len_on(&self, axis: Axis) -> Option<f32> {
232        match axis {
233            Axis::Horizontal => self.width,
234            Axis::Vertical => self.height,
235        }
236    }
237}
238
239/// Information about how a [`Widget`] should be spawned dynamically
240///
241/// Dynamically spawned `Widget`s are those that are spawned on
242/// [`Handle`]s or [`Text`]. They are called dynamic because their
243/// spawning location can change automatically, either by the widget
244/// they are spawned on resizing, or the `Text` changing, etc.
245///
246/// This is in contrast with [`StaticSpawnSpecs`], which are not
247/// spawned on a `Handle` or `Text`, and are instead placed in a
248/// [`Coord`] on screen.
249///
250/// [`Handle`]: Handle::push_outer_widget
251/// [`Text`]: crate::text::SpawnTag
252#[derive(Default, Debug, Clone, Copy)]
253pub struct DynSpawnSpecs {
254    /// The orientation to place this [`Widget`] in
255    ///
256    /// May receive some reworks in the future.
257    pub orientation: Orientation,
258    /// A width (in character cells) for this `Widget`
259    ///
260    /// Note that this may be ignored if it is not possible to
261    /// create an area big (or small) enough.
262    pub width: Option<f32>,
263    /// A height (in lines) for this `Widget`
264    ///
265    /// Note that this may be ignored if it is not possible to
266    /// create an area big (or small) enough.
267    pub height: Option<f32>,
268    /// Hide this `Widget` by default
269    ///
270    /// You can call [`Area::hide`] or [`Area::reveal`] to toggle
271    /// this property.
272    pub hidden: bool,
273}
274
275impl DynSpawnSpecs {
276    /// The constraints on a given [`Axis`]
277    pub fn len_on(&self, axis: Axis) -> Option<f32> {
278        match axis {
279            Axis::Horizontal => self.width,
280            Axis::Vertical => self.height,
281        }
282    }
283}
284
285/// Information about how a [`Widget`] should be spawned statically
286///
287/// Statically spawned `Widget`s are those that are placed in a
288/// [`Coord`] on screen via [`Window::spawn`] and don't change
289/// location.
290///
291/// This is in contrast with [`DynSpawnSpecs`], which are allowed to
292/// be moved automatically, due to being spawned on [`Handle`]s or
293/// [`Text`], which are allowed to change.
294///
295/// [`Text`]: crate::text::Text
296#[derive(Debug, Clone, Copy, PartialEq)]
297pub struct StaticSpawnSpecs {
298    /// The top left corner where the [`Widget`] will be spawned
299    pub top_left: Coord,
300    /// The desired width for the [`Widget`]
301    pub width: f32,
302    /// The desired height for the [`Widget`]
303    pub height: f32,
304    /// Hide this [`Widget`] by default
305    ///
306    /// You can call [`Area::hide`] or [`Area::reveal`] to toggle
307    /// this property.
308    pub hidden: bool,
309}
310
311impl StaticSpawnSpecs {
312    /// The constraints on a given [`Axis`]
313    pub fn len_on(&self, axis: Axis) -> f32 {
314        match axis {
315            Axis::Horizontal => self.width,
316            Axis::Vertical => self.height,
317        }
318    }
319}
320
321/// A direction, where a [`Widget`] will be placed in relation to
322/// another.
323#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
324pub enum Side {
325    /// Put the [`Widget`] above another
326    Above,
327    /// Put the [`Widget`] on the right
328    #[default]
329    Right,
330    /// Put the [`Widget`] on the left
331    Below,
332    /// Put the [`Widget`] below another
333    Left,
334}
335
336impl Side {
337    /// Which [`Axis`] this [`Side`] belongs to
338    pub fn axis(&self) -> Axis {
339        match self {
340            Side::Above | Side::Below => Axis::Vertical,
341            Side::Right | Side::Left => Axis::Horizontal,
342        }
343    }
344}
345
346/// Where to place a spawned [`Widget`]
347///
348/// The `Orientation` has 3 components of positioning, which follow
349/// priorities in order to relocate the `Widget` in case there isn't
350/// enough space. Respectively, they are the following:
351///
352/// - An axis to align the `Widget`.
353/// - How to align said `Widget` on said axis.
354/// - Which side of the parent should be prioritized.
355///
356/// For example, [`Orientation::HorTopLeft`] means: Spawn this
357/// `Widget` horizontally, trying to align its top edge with the top
358/// edge of the parent, prioritizing the left side. Visually speaking,
359/// it will try to spawn a `Widget` like this:
360///
361/// ```text
362/// ╭─────────┬────────╮
363/// │         │ Parent │
364/// │ Spawned ├────────╯
365/// │         │
366/// ╰─────────╯
367/// ```
368///
369/// Notice that their tops are aligned, the edges connect on the
370/// horizontal axis, and it is on the left side. However, if there is
371/// not enough space, (e.g. the parent is very close to the bottom
372/// left edge of the screen) it might try to spawn it like this:
373///
374/// ```text
375/// ╭─────────╮                                 ╭─────────╮
376/// │         ├────────╮                        │         │
377/// │ Spawned │ Parent │, or even like ╭────────┤ Spawned │
378/// │         ├────────╯               │ Parent │         │
379/// ╰─────────╯                        ╰────────┴─────────╯
380/// ```
381///
382/// This prioritization gives more flexibility to the spawning of
383/// `Widget`s, which usually follows patterns of where to spawn and
384/// how to place things, mostly to prevent obscuring information. The
385/// most notable example of this are completion lists. For obvious
386/// reasons, those should only be placed above or below (`Ver`),
387/// alignment should try to be on the left edge (`VerLeft`), and
388/// ideally below the cursor ([`Orientation::VerLeftBelow`]).
389/// Likewise, these completion lists are sometimes accompanied by
390/// description panels, which should ideally follow a
391/// [`HorCenterRight`] or [`HorBottomRight`] orientation.
392///
393/// [`HorCenterRight`]: Orientation::HorCenterRight
394/// [`HorBottomRight`]: Orientation::HorBottomRight
395#[derive(Default, Debug, Clone, Copy)]
396pub enum Orientation {
397    /// Place the [`Widget`] vertically, prioritizing the left edge
398    /// above
399    VerLeftAbove,
400    /// Place the [`Widget`] vertically, prioritizing centering above
401    VerCenterAbove,
402    /// Place the [`Widget`] vertically, prioritizing the right edge
403    /// above
404    VerRightAbove,
405    /// Place the [`Widget`] vertically, prioritizing the left edge
406    /// below
407    #[default]
408    VerLeftBelow,
409    /// Place the [`Widget`] vertically, prioritizing centering below
410    VerCenterBelow,
411    /// Place the [`Widget`] vertically, prioritizing the right edge
412    /// below
413    VerRightBelow,
414    /// Place the [`Widget`] horizontally, prioritizing the top edge
415    /// on the left
416    HorTopLeft,
417    /// Place the [`Widget`] horizontally, prioritizing centering
418    /// on the left
419    HorCenterLeft,
420    /// Place the [`Widget`] horizontally, prioritizing the right edge
421    /// on the left
422    HorBottomLeft,
423    /// Place the [`Widget`] horizontally, prioritizing the top edge
424    /// on the right
425    HorTopRight,
426    /// Place the [`Widget`] horizontally, prioritizing centering
427    /// on the right
428    HorCenterRight,
429    /// Place the [`Widget`] horizontally, prioritizing the bottom
430    /// edge on the right
431    HorBottomRight,
432}
433
434impl Orientation {
435    /// The [`Axis`] to which this `Orientation` pushes
436    pub fn axis(&self) -> Axis {
437        match self {
438            Orientation::VerLeftAbove
439            | Orientation::VerCenterAbove
440            | Orientation::VerRightAbove
441            | Orientation::VerLeftBelow
442            | Orientation::VerCenterBelow
443            | Orientation::VerRightBelow => Axis::Vertical,
444            Orientation::HorTopLeft
445            | Orientation::HorCenterLeft
446            | Orientation::HorBottomLeft
447            | Orientation::HorTopRight
448            | Orientation::HorCenterRight
449            | Orientation::HorBottomRight => Axis::Horizontal,
450        }
451    }
452
453    /// Wether this should prefer being pushed before (left or above)
454    pub fn prefers_before(&self) -> bool {
455        match self {
456            Orientation::VerLeftAbove
457            | Orientation::VerCenterAbove
458            | Orientation::VerRightAbove
459            | Orientation::HorTopLeft
460            | Orientation::HorCenterLeft
461            | Orientation::HorBottomLeft => true,
462            Orientation::VerLeftBelow
463            | Orientation::VerCenterBelow
464            | Orientation::VerRightBelow
465            | Orientation::HorTopRight
466            | Orientation::HorCenterRight
467            | Orientation::HorBottomRight => false,
468        }
469    }
470}
471
472/// A struct representing a "visual position" on the screen
473///
474/// This position differs from a [`VPoint`] in the sense that it
475/// represents three properties of a printed character:
476///
477/// - The x position in which it was printed;
478/// - The amount of horizontal space it occupies;
479/// - Wether this character is the first on the line (i.e. it wraps)
480///
481/// [`VPoint`]: crate::mode::VPoint
482#[derive(Debug, Clone, Copy)]
483pub struct Caret {
484    /// The horizontal position in which a character was printed
485    pub x: u32,
486    /// The horizontal space it occupied
487    pub len: u32,
488    /// Wether it is the first character in the line
489    pub wrap: bool,
490}
491
492impl Caret {
493    /// Returns a new [`Caret`]
494    #[inline(always)]
495    pub fn new(x: u32, len: u32, wrap: bool) -> Self {
496        Self { x, len, wrap }
497    }
498}
499
500/// A target for pushing [`Widget`]s to
501///
502/// This can either be a [`Handle`], which will push around a `Widget`
503/// or a [`Window`], which will push around the window.
504///
505/// This trait is useful if you wish to let your [`Widget`] both be
506/// pushed around other `Widget`s and also around the window with the
507/// [`Window`]. One example of this is the [`StatusLine`] widget,
508/// which behaves differently depending on if it was pushed to a
509/// [`Handle<Buffer>`].
510///
511/// [`StatusLine`]: https://docs.rs/duat/duat/latest/widgets/struct.StatusLine.html
512pub trait PushTarget {
513    /// Pushes a [`Widget`] around `self`
514    ///
515    /// If `self` is a [`Handle`], this will push around the
516    /// `Handle`'s own [`Area`]. If this is a [`Window`],
517    /// this will push around the master `Area` of the central
518    /// region of buffers.
519    ///
520    /// This `Widget` will be placed internally, i.e., around the
521    /// [`Area`] of `self`. This is in contrast to
522    /// [`Handle::push_outer_widget`], which will push around the
523    /// "cluster master" of `self`.
524    ///
525    /// A cluster master is the collection of every `Widget` that was
526    /// pushed around a central one with [`PushSpecs::cluster`] set to
527    /// `true`.
528    ///
529    /// Both of these functions behave identically in the situation
530    /// where no other [`Widget`]s were pushed around `self`.
531    ///
532    /// However, if, for example, a `Widget` was previously pushed
533    /// below `self`, when pushing to the left, the following would
534    /// happen:
535    ///
536    /// ```text
537    /// ╭────────────────╮    ╭─────┬──────────╮
538    /// │                │    │     │          │
539    /// │      self      │    │ new │   self   │
540    /// │                │ -> │     │          │
541    /// ├────────────────┤    ├─────┴──────────┤
542    /// │      old       │    │      old       │
543    /// ╰────────────────╯    ╰────────────────╯
544    /// ```
545    ///
546    /// While in [`Handle::push_outer_widget`], this happens instead:
547    ///
548    /// ```text
549    /// ╭────────────────╮    ╭─────┬──────────╮
550    /// │                │    │     │          │
551    /// │      self      │    │     │   self   │
552    /// │                │ -> │ new │          │
553    /// ├────────────────┤    │     ├──────────┤
554    /// │      old       │    │     │   old    │
555    /// ╰────────────────╯    ╰─────┴──────────╯
556    /// ```
557    ///
558    /// Note that `new` was pushed _around_ other clustered widgets in
559    /// the second case, not just around `self`.
560    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW>;
561
562    /// Pushes a [`Widget`] around the "master region" of `self`
563    ///
564    /// If `self` is a [`Handle`], this will push its "cluster
565    /// master". If this is a [`Window`], this will push the
566    /// `Widget` to the edges of the window.
567    ///
568    /// A cluster master is the collection of every `Widget` that was
569    /// pushed around a central one with [`PushSpecs::cluster`] set to
570    /// `true`.
571    ///
572    /// This [`Widget`] will be placed externally, i.e., around every
573    /// other `Widget` that was pushed around `self`. This is in
574    /// contrast to [`Handle::push_inner_widget`], which will push
575    /// only around `self`.
576    ///
577    /// Both of these functions behave identically in the situation
578    /// where no other [`Widget`]s were pushed around `self`.
579    ///
580    /// However, if, for example, a `Widget` was previously pushed
581    /// to the left of `self`, when pushing to the left again, the
582    /// following would happen:
583    ///
584    /// ```text
585    /// ╭──────┬──────────╮    ╭─────┬─────┬──────╮
586    /// │      │          │    │     │     │      │
587    /// │      │          │    │     │     │      │
588    /// │  old │   self   │ -> │ new │ old │ self │
589    /// │      │          │    │     │     │      │
590    /// │      │          │    │     │     │      │
591    /// ╰──────┴──────────╯    ╰─────┴─────┴──────╯
592    /// ```
593    ///
594    /// While in [`Handle::push_inner_widget`], this happens instead:
595    ///
596    /// ```text
597    /// ╭──────┬──────────╮    ╭─────┬─────┬──────╮
598    /// │      │          │    │     │     │      │
599    /// │      │          │    │     │     │      │
600    /// │  old │   self   │ -> │ old │ new │ self │
601    /// │      │          │    │     │     │      │
602    /// │      │          │    │     │     │      │
603    /// ╰──────┴──────────╯    ╰─────┴─────┴──────╯
604    /// ```
605    ///
606    /// Note that `new` was pushed _around_ other clustered widgets in
607    /// the first case, not just around `self`.
608    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW>;
609
610    /// Tries to downcast to a [`Handle`] of some `W`
611    fn try_downcast<W: Widget>(&self) -> Option<Handle<W>>;
612}
613
614impl<W: Widget + ?Sized> PushTarget for Handle<W> {
615    #[doc(hidden)]
616    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
617        self.push_inner_widget(pa, widget, specs)
618    }
619
620    #[doc(hidden)]
621    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
622        self.push_outer_widget(pa, widget, specs)
623    }
624
625    fn try_downcast<DW: Widget>(&self) -> Option<Handle<DW>> {
626        self.try_downcast()
627    }
628}
629
630impl PushTarget for Window {
631    #[doc(hidden)]
632    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
633        Window::push_inner(self, pa, widget, specs)
634    }
635
636    #[doc(hidden)]
637    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
638        Window::push_outer(self, pa, widget, specs)
639    }
640
641    fn try_downcast<W: Widget>(&self) -> Option<Handle<W>> {
642        None
643    }
644}