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}