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 dimension on screen, can either be horizontal or vertical
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Axis {
64    /// The horizontal axis
65    Horizontal,
66    /// The vertical axis
67    Vertical,
68}
69
70impl Axis {
71    /// The [`Axis`] perpendicular to this one
72    pub fn perp(&self) -> Self {
73        match self {
74            Axis::Horizontal => Axis::Vertical,
75            Axis::Vertical => Axis::Horizontal,
76        }
77    }
78
79    /// Returns `true` if the axis is [`Horizontal`].
80    ///
81    /// [`Horizontal`]: Axis::Horizontal
82    #[must_use]
83    pub fn is_hor(&self) -> bool {
84        matches!(self, Self::Horizontal)
85    }
86
87    /// Returns `true` if the axis is [`Vertical`].
88    ///
89    /// [`Vertical`]: Axis::Vertical
90    #[must_use]
91    pub fn is_ver(&self) -> bool {
92        matches!(self, Self::Vertical)
93    }
94}
95
96impl From<PushSpecs> for Axis {
97    fn from(value: PushSpecs) -> Self {
98        match value.side {
99            Side::Above | Side::Below => Axis::Vertical,
100            _ => Axis::Horizontal,
101        }
102    }
103}
104
105/// Information on how a [`Widget`] should be pushed onto another
106///
107/// This information is composed of four parts:
108///
109/// * A side to push;
110/// * An optional width;
111/// * An optional height;
112/// * Wether to hide it by default;
113/// * wether to cluster the [`Widget`]
114///
115/// Constraints are demands that must be met by the widget's
116/// [`Area`], on a best effort basis.
117///
118/// So, for example, if the [`PushSpecs`] are:
119///
120/// ```rust
121/// # duat_core::doc_duat!(duat);
122/// use duat::prelude::*;
123/// let specs = ui::PushSpecs {
124///     side: ui::Side::Left,
125///     width: Some(3.0),
126///     height: None,
127///     hidden: false,
128///     cluster: true,
129/// };
130/// ```
131///
132/// Then the widget should be pushed to the left, with a width of 3,
133/// an unspecified height, _not_ hidden by default and clustered if
134/// possible. Note that, with `#[feature(default_field_values)]`, the
135/// same can be accomplished by the following:
136///
137/// ```rust
138/// #![feature(default_field_values)]
139/// # duat_core::doc_duat!(duat);
140/// use duat::prelude::*;
141/// let specs = ui::PushSpecs {
142///     side: ui::Side::Left,
143///     width: Some(3.0),
144///     ..
145/// };
146/// ```
147///
148/// Since the remaining values are the default.
149#[derive(Clone, Copy, Debug, Default)]
150pub struct PushSpecs {
151    /// Which [`Side`] to push the [`Widget`] to
152    pub side: Side = Side::Below,
153    /// A width (in character cells) for this `Widget`
154    ///
155    /// Note that this may be ignored if it is not possible to
156    /// create an area big (or small) enough.
157    pub width: Option<f32> = None,
158    /// A height (in lines) for this `Widget`
159    ///
160    /// Note that this may be ignored if it is not possible to
161    /// create an area big (or small) enough.
162    pub height: Option<f32> = None,
163    /// Hide this `Widget` by default
164    ///
165    /// You can call [`Area::hide`] or [`Area::reveal`] to toggle
166    /// this property.
167    pub hidden: bool = false,
168    /// Cluster this `Widget` when pushing
169    ///
170    /// This makes it so, if the main `Widget` is moved or deleted,
171    /// then this one will follow. Useful for things like
172    /// [`LineNumbers`], since they should follow their [`Buffer`] around.
173    ///
174    /// [`LineNumbers`]: https://docs.rs/duat/latest/duat/widgets/struct.LineNumbers.html
175    /// [`Buffer`]: crate::buffer::Buffer
176    pub cluster: bool = true,
177}
178
179impl PushSpecs {
180    /// The [`Axis`] where it will be pushed
181    ///
182    /// - left/right: [`Axis::Horizontal`]
183    /// - above/below: [`Axis::Vertical`]
184    pub const fn axis(&self) -> Axis {
185        match self.side {
186            Side::Above | Side::Below => Axis::Vertical,
187            Side::Right | Side::Left => Axis::Horizontal,
188        }
189    }
190
191    /// Wether this "comes earlier" on the screen
192    ///
193    /// This returns true if `self.side() == Side::Left || self.side()
194    /// == Side::Above`, since that is considered "earlier" on
195    /// screens.
196    pub const fn comes_earlier(&self) -> bool {
197        matches!(self.side, Side::Left | Side::Above)
198    }
199
200    /// The constraints on a given [`Axis`]
201    pub fn len_on(&self, axis: Axis) -> Option<f32> {
202        match axis {
203            Axis::Horizontal => self.width,
204            Axis::Vertical => self.height,
205        }
206    }
207}
208
209/// A direction, where a [`Widget`] will be placed in relation to
210/// another.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum Side {
213    /// Put the [`Widget`] above another
214    Above,
215    /// Put the [`Widget`] on the right
216    Right,
217    /// Put the [`Widget`] on the left
218    Below,
219    /// Put the [`Widget`] below another
220    Left,
221}
222
223impl Side {
224    /// Which [`Axis`] this [`Side`] belongs to
225    pub fn axis(&self) -> Axis {
226        match self {
227            Side::Above | Side::Below => Axis::Vertical,
228            Side::Right | Side::Left => Axis::Horizontal,
229        }
230    }
231}
232
233/// Much like [`PushSpecs`], but for floating [`Widget`]s
234#[derive(Default, Debug, Clone, Copy)]
235pub struct SpawnSpecs {
236    /// The orientation to place this [`Widget`] in
237    ///
238    /// May receive some reworks in the future.
239    pub orientation: Orientation = Orientation::VerLeftBelow,
240    /// A width (in character cells) for this `Widget`
241    ///
242    /// Note that this may be ignored if it is not possible to
243    /// create an area big (or small) enough.
244    pub width: Option<f32> = None,
245    /// A height (in lines) for this `Widget`
246    ///
247    /// Note that this may be ignored if it is not possible to
248    /// create an area big (or small) enough.
249    pub height: Option<f32> = None,
250    /// Hide this `Widget` by default
251    ///
252    /// You can call [`Area::hide`] or [`Area::reveal`] to toggle
253    /// this property.
254    pub hidden: bool = false,
255}
256
257impl SpawnSpecs {
258    /// The constraints on a given [`Axis`]
259    pub fn len_on(&self, axis: Axis) -> Option<f32> {
260        match axis {
261            Axis::Horizontal => self.width,
262            Axis::Vertical => self.height,
263        }
264    }
265}
266
267/// Where to place a spawned [`Widget`]
268///
269/// The `Orientation` has 3 components of positioning, which follow
270/// priorities in order to relocate the `Widget` in case there isn't
271/// enough space. Respectively, they are the following:
272///
273/// - An axis to align the `Widget`.
274/// - How to align said `Widget` on said axis.
275/// - Which side of the parent should be prioritized.
276///
277/// For example, [`Orientation::HorTopLeft`] means: Spawn this
278/// `Widget` horizontally, trying to align its top edge with the top
279/// edge of the parent, prioritizing the left side. Visually speaking,
280/// it will try to spawn a `Widget` like this:
281///
282/// ```text
283/// ╭─────────┬────────╮
284/// │         │ Parent │
285/// │ Spawned ├────────╯
286/// │         │
287/// ╰─────────╯
288/// ```
289///
290/// Notice that their tops are aligned, the edges connect on the
291/// horizontal axis, and it is on the left side. However, if there is
292/// not enough space, (e.g. the parent is very close to the bottom
293/// left edge of the screen) it might try to spawn it like this:
294///
295/// ```text
296/// ╭─────────╮                                 ╭─────────╮
297/// │         ├────────╮                        │         │
298/// │ Spawned │ Parent │, or even like ╭────────┤ Spawned │
299/// │         ├────────╯               │ Parent │         │
300/// ╰─────────╯                        ╰────────┴─────────╯
301/// ```
302///
303/// This prioritization gives more flexibility to the spawning of
304/// `Widget`s, which usually follows patterns of where to spawn and
305/// how to place things, mostly to prevent obscuring information. The
306/// most notable example of this are completion lists. For obvious
307/// reasons, those should only be placed above or below (`Ver`),
308/// alignment should try to be on the left edge (`VerLeft`), and
309/// ideally below the cursor ([`Orientation::VerLeftBelow`]).
310/// Likewise, these completion lists are sometimes accompanied by
311/// description panels, which should ideally follow a
312/// [`HorCenterRight`] or [`HorBottomRight`] orientation.
313///
314/// [`HorCenterRight`]: Orientation::HorCenterRight
315/// [`HorBottomRight`]: Orientation::HorBottomRight
316#[derive(Clone, Copy, Debug)]
317pub enum Orientation {
318    /// Place the [`Widget`] vertically, prioritizing the left edge
319    /// above
320    VerLeftAbove,
321    /// Place the [`Widget`] vertically, prioritizing centering above
322    VerCenterAbove,
323    /// Place the [`Widget`] vertically, prioritizing the right edge
324    /// above
325    VerRightAbove,
326    /// Place the [`Widget`] vertically, prioritizing the left edge
327    /// below
328    VerLeftBelow,
329    /// Place the [`Widget`] vertically, prioritizing centering below
330    VerCenterBelow,
331    /// Place the [`Widget`] vertically, prioritizing the right edge
332    /// below
333    VerRightBelow,
334    /// Place the [`Widget`] horizontally, prioritizing the top edge
335    /// on the left
336    HorTopLeft,
337    /// Place the [`Widget`] horizontally, prioritizing centering
338    /// on the left
339    HorCenterLeft,
340    /// Place the [`Widget`] horizontally, prioritizing the right edge
341    /// on the left
342    HorBottomLeft,
343    /// Place the [`Widget`] horizontally, prioritizing the top edge
344    /// on the right
345    HorTopRight,
346    /// Place the [`Widget`] horizontally, prioritizing centering
347    /// on the right
348    HorCenterRight,
349    /// Place the [`Widget`] horizontally, prioritizing the bottom
350    /// edge on the right
351    HorBottomRight,
352}
353
354impl Orientation {
355    /// The [`Axis`] to which this `Orientation` pushes
356    pub fn axis(&self) -> Axis {
357        match self {
358            Orientation::VerLeftAbove
359            | Orientation::VerCenterAbove
360            | Orientation::VerRightAbove
361            | Orientation::VerLeftBelow
362            | Orientation::VerCenterBelow
363            | Orientation::VerRightBelow => Axis::Vertical,
364            Orientation::HorTopLeft
365            | Orientation::HorCenterLeft
366            | Orientation::HorBottomLeft
367            | Orientation::HorTopRight
368            | Orientation::HorCenterRight
369            | Orientation::HorBottomRight => Axis::Horizontal,
370        }
371    }
372
373    /// Wether this should prefer being pushed before (left or above)
374    pub fn prefers_before(&self) -> bool {
375        match self {
376            Orientation::VerLeftAbove
377            | Orientation::VerCenterAbove
378            | Orientation::VerRightAbove
379            | Orientation::HorTopLeft
380            | Orientation::HorCenterLeft
381            | Orientation::HorBottomLeft => true,
382            Orientation::VerLeftBelow
383            | Orientation::VerCenterBelow
384            | Orientation::VerRightBelow
385            | Orientation::HorTopRight
386            | Orientation::HorCenterRight
387            | Orientation::HorBottomRight => false,
388        }
389    }
390}
391
392/// A struct representing a "visual position" on the screen
393///
394/// This position differs from a [`VPoint`] in the sense that it
395/// represents three properties of a printed character:
396///
397/// - The x position in which it was printed;
398/// - The amount of horizontal space it occupies;
399/// - Wether this character is the first on the line (i.e. it wraps)
400///
401/// [`VPoint`]: crate::mode::VPoint
402#[derive(Debug, Clone, Copy)]
403pub struct Caret {
404    /// The horizontal position in which a character was printed
405    pub x: u32,
406    /// The horizontal space it occupied
407    pub len: u32,
408    /// Wether it is the first character in the line
409    pub wrap: bool,
410}
411
412impl Caret {
413    /// Returns a new [`Caret`]
414    #[inline(always)]
415    pub fn new(x: u32, len: u32, wrap: bool) -> Self {
416        Self { x, len, wrap }
417    }
418}
419
420/// A target for pushing [`Widget`]s to
421///
422/// This can either be a [`Handle`], which will push around a `Widget`
423/// or a [`Window`], which will push around the window.
424///
425/// This trait is useful if you wish to let your [`Widget`] both be
426/// pushed around other `Widget`s and also around the window with the
427/// [`Window`]. One example of this is the [`StatusLine`] widget,
428/// which behaves differently depending on if it was pushed to a
429/// [`Handle<Buffer>`].
430///
431/// [`StatusLine`]: https://docs.rs/duat/duat/latest/widgets/struct.StatusLine.html
432pub trait PushTarget {
433    /// Pushes a [`Widget`] around `self`
434    ///
435    /// If `self` is a [`Handle`], this will push around the
436    /// `Handle`'s own [`Area`]. If this is a [`Window`],
437    /// this will push around the master `Area` of the central
438    /// region of buffers.
439    ///
440    /// This `Widget` will be placed internally, i.e., around the
441    /// [`Area`] of `self`. This is in contrast to
442    /// [`Handle::push_outer_widget`], which will push around the
443    /// "cluster master" of `self`.
444    ///
445    /// A cluster master is the collection of every `Widget` that was
446    /// pushed around a central one with [`PushSpecs::cluster`] set to
447    /// `true`.
448    ///
449    /// Both of these functions behave identically in the situation
450    /// where no other [`Widget`]s were pushed around `self`.
451    ///
452    /// However, if, for example, a `Widget` was previously pushed
453    /// below `self`, when pushing to the left, the following would
454    /// happen:
455    ///
456    /// ```text
457    /// ╭────────────────╮    ╭─────┬──────────╮
458    /// │                │    │     │          │
459    /// │      self      │    │ new │   self   │
460    /// │                │ -> │     │          │
461    /// ├────────────────┤    ├─────┴──────────┤
462    /// │      old       │    │      old       │
463    /// ╰────────────────╯    ╰────────────────╯
464    /// ```
465    ///
466    /// While in [`Handle::push_outer_widget`], this happens instead:
467    ///
468    /// ```text
469    /// ╭────────────────╮    ╭─────┬──────────╮
470    /// │                │    │     │          │
471    /// │      self      │    │     │   self   │
472    /// │                │ -> │ new │          │
473    /// ├────────────────┤    │     ├──────────┤
474    /// │      old       │    │     │   old    │
475    /// ╰────────────────╯    ╰─────┴──────────╯
476    /// ```
477    ///
478    /// Note that `new` was pushed _around_ other clustered widgets in
479    /// the second case, not just around `self`.
480    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW>;
481
482    /// Pushes a [`Widget`] around the "master region" of `self`
483    ///
484    /// If `self` is a [`Handle`], this will push its "cluster
485    /// master". If this is a [`Window`], this will push the
486    /// `Widget` to the edges of the window.
487    ///
488    /// A cluster master is the collection of every `Widget` that was
489    /// pushed around a central one with [`PushSpecs::cluster`] set to
490    /// `true`.
491    ///
492    /// This [`Widget`] will be placed externally, i.e., around every
493    /// other `Widget` that was pushed around `self`. This is in
494    /// contrast to [`Handle::push_inner_widget`], which will push
495    /// only around `self`.
496    ///
497    /// Both of these functions behave identically in the situation
498    /// where no other [`Widget`]s were pushed around `self`.
499    ///
500    /// However, if, for example, a `Widget` was previously pushed
501    /// to the left of `self`, when pushing to the left again, the
502    /// following would happen:
503    ///
504    /// ```text
505    /// ╭──────┬──────────╮    ╭─────┬─────┬──────╮
506    /// │      │          │    │     │     │      │
507    /// │      │          │    │     │     │      │
508    /// │  old │   self   │ -> │ new │ old │ self │
509    /// │      │          │    │     │     │      │
510    /// │      │          │    │     │     │      │
511    /// ╰──────┴──────────╯    ╰─────┴─────┴──────╯
512    /// ```
513    ///
514    /// While in [`Handle::push_inner_widget`], this happens instead:
515    ///
516    /// ```text
517    /// ╭──────┬──────────╮    ╭─────┬─────┬──────╮
518    /// │      │          │    │     │     │      │
519    /// │      │          │    │     │     │      │
520    /// │  old │   self   │ -> │ old │ new │ self │
521    /// │      │          │    │     │     │      │
522    /// │      │          │    │     │     │      │
523    /// ╰──────┴──────────╯    ╰─────┴─────┴──────╯
524    /// ```
525    ///
526    /// Note that `new` was pushed _around_ other clustered widgets in
527    /// the first case, not just around `self`.
528    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW>;
529
530    /// Tries to downcast to a [`Handle`] of some `W`
531    fn try_downcast<W: Widget>(&self) -> Option<Handle<W>>;
532}
533
534impl<W: Widget + ?Sized> PushTarget for Handle<W> {
535    #[doc(hidden)]
536    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
537        self.push_inner_widget(pa, widget, specs)
538    }
539
540    #[doc(hidden)]
541    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
542        self.push_outer_widget(pa, widget, specs)
543    }
544
545    fn try_downcast<DW: Widget>(&self) -> Option<Handle<DW>> {
546        self.try_downcast()
547    }
548}
549
550impl PushTarget for Window {
551    #[doc(hidden)]
552    fn push_inner<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
553        Window::push_inner(self, pa, widget, specs)
554    }
555
556    #[doc(hidden)]
557    fn push_outer<PW: Widget>(&self, pa: &mut Pass, widget: PW, specs: PushSpecs) -> Handle<PW> {
558        Window::push_outer(self, pa, widget, specs)
559    }
560
561    fn try_downcast<W: Widget>(&self) -> Option<Handle<W>> {
562        None
563    }
564}