duat_core/ui/
mod.rs

1mod builder;
2mod layout;
3
4use std::{
5    fmt::Debug,
6    marker::PhantomData,
7    sync::{Arc, mpsc},
8};
9
10use crossterm::event::KeyEvent;
11use layout::window_files;
12use parking_lot::RwLock;
13use serde::{Deserialize, Serialize};
14
15pub use self::{
16    builder::{FileBuilder, WindowBuilder},
17    layout::{FileId, Layout, MasterOnLeft, WindowFiles},
18};
19use crate::{
20    DuatError,
21    cache::load_cache,
22    cfg::{IterCfg, PrintCfg},
23    data::{RoData, RwData},
24    form::Painter,
25    text::{Item, Iter, Point, RevIter, Text},
26    widgets::{File, Node, Widget},
27};
28
29/// All the methods that a working gui/tui will need to implement, in
30/// order to use Parsec.
31pub trait Ui: Sized + Send + Sync + 'static {
32    type MetaStatics: Default + Send + Sync;
33    type Area: Area<Ui = Self> + Clone + PartialEq + Send + Sync;
34
35    ////////// Functions executed from the outer loop
36
37    /// Functions to trigger when the program begins
38    ///
39    /// These will happen in the main `duat` runner
40    fn open(ms: &'static Self::MetaStatics, duat_tx: Sender, ui_rx: mpsc::Receiver<UiEvent>);
41
42    /// Functions to trigger when the program ends
43    ///
44    /// These will happen in the main `duat` runner
45    fn close(ms: &'static Self::MetaStatics);
46
47    /// Functions to trigger when the program reloads
48    ///
49    /// These will happen inside of the dynamically loaded config
50    /// crate.
51    fn load(ms: &'static Self::MetaStatics);
52
53    ////////// Functions executed from within the configuration loop
54
55    /// Initiates and returns a new "master" [`Area`]
56    ///
57    /// This [`Area`] must not have any parents, and must be placed on
58    /// a new window, that is, a plain region with nothing in it.
59    ///
60    /// [`Area`]: Ui::Area
61    fn new_root(ms: &'static Self::MetaStatics, cache: <Self::Area as Area>::Cache) -> Self::Area;
62
63    /// Switches the currently active window
64    ///
65    /// This will only happen to with window indices that are actual
66    /// windows. If at some point, a window index comes up that is not
67    /// actually a window, that's a bug.
68    fn switch_window(ms: &'static Self::MetaStatics, win: usize);
69
70    /// Flush the layout
71    ///
72    /// When this function is called, it means that Duat has finished
73    /// adding or removing widgets, so the ui should calculate the
74    /// layout.
75    fn flush_layout(ms: &'static Self::MetaStatics);
76
77    /// Unloads the [`Ui`]
78    ///
79    /// Unlike [`Ui::close`], this will happen both when Duat reloads
80    /// the configuration and when it closes the app.
81    ///
82    /// These will happen inside of the dynamically loaded config
83    /// crate.
84    fn unload(ms: &'static Self::MetaStatics);
85
86    /// Removes a window from the [`Ui`]
87    ///
88    /// This should keep the current active window consistent. That
89    /// is, if the current window was ahead of the deleted one, it
90    /// should be shifted back, so that the same window is still
91    /// displayed.
92    fn remove_window(ms: &'static Self::MetaStatics, win: usize);
93}
94
95/// An [`Area`] that supports printing [`Text`]
96///
97/// These represent the entire GUI of Parsec, the only parts of the
98/// screen where text may be printed.
99pub trait Area: Send + Sync + Sized {
100    // This exists solely for automatic type recognition.
101    type Ui: Ui<Area = Self>;
102    type ConstraintChangeErr: std::error::Error + DuatError;
103    type Cache: Default + Serialize + Deserialize<'static> + 'static;
104    type PrintInfo: Default + Clone + Send + Sync;
105
106    ////////// Area modification
107
108    /// Bisects the [`Area`][Ui::Area] with the given index into
109    /// two.
110    ///
111    /// Will return 2 indices, the first one is the index of a new
112    /// area. The second is an index for a newly created parent
113    ///
114    /// As an example, assuming that [`self`] has an index of `0`,
115    /// pushing an area to [`self`] on [`Side::Left`] would create
116    /// 2 new areas:
117    ///
118    /// ```text
119    /// ╭────────0────────╮     ╭────────1────────╮
120    /// │                 │     │╭──2───╮╭───0───╮│
121    /// │      self       │ --> ││      ││ self  ││
122    /// │                 │     │╰──────╯╰───────╯│
123    /// ╰─────────────────╯     ╰─────────────────╯
124    /// ```
125    ///
126    /// So now, there is a new area `1`, which is the parent of the
127    /// areas `0` and `2`. When a new parent is created, it should be
128    /// returned as the second element in the tuple.
129    ///
130    /// That doesn't always happen though. For example, pushing
131    /// another area to the [`Side::Right`] of `1`, `2`, or `0`,
132    /// in this situation, should not result in the creation of a
133    /// new parent:
134    ///
135    /// ```text
136    /// ╭────────1────────╮     ╭────────1────────╮
137    /// │╭──2───╮╭───0───╮│     │╭─2─╮╭──0──╮╭─3─╮│
138    /// ││      ││ self  ││     ││   ││self ││   ││
139    /// │╰──────╯╰───────╯│     │╰───╯╰─────╯╰───╯│
140    /// ╰─────────────────╯     ╰─────────────────╯
141    /// ```
142    ///
143    /// And so [`Area::bisect`] should return `(3, None)`.
144    fn bisect(
145        &self,
146        specs: PushSpecs,
147        cluster: bool,
148        on_files: bool,
149        cache: Self::Cache,
150        _: DuatPermission,
151    ) -> (Self, Option<Self>);
152
153    /// Deletes this [`Area`], signaling the closing of a [`Widget`]
154    ///
155    /// If the [`Area`]'s parent was also deleted, return it.
156    fn delete(&self, _: DuatPermission) -> Option<Self>;
157
158    /// Swaps this [`Area`] with another one
159    ///
160    /// The swapped [`Area`]s will be cluster masters of the
161    /// respective [`Area`]s. As such, if they belong to the same
162    /// master, nothing happens.
163    fn swap(&self, other: &Self, _: DuatPermission);
164
165    /// Changes the horizontal constraint of the area
166    fn constrain_hor(&self, constraint: Constraint) -> Result<(), Self::ConstraintChangeErr>;
167
168    /// Changes the vertical constraint of the area
169    fn constrain_ver(&self, constraint: Constraint) -> Result<(), Self::ConstraintChangeErr>;
170
171    /// Restores the original constraints of the widget
172    fn restore_constraints(&self) -> Result<(), Self::ConstraintChangeErr>;
173
174    /// Requests that the width be enough to fit a certain piece of
175    /// text.
176    fn request_width_to_fit(&self, text: &str) -> Result<(), Self::ConstraintChangeErr>;
177
178    /// Scrolls the [`Text`] (up or down) until the main cursor is
179    /// within the [`ScrollOff`] range.
180    ///
181    /// [`ScrollOff`]: crate::cfg::ScrollOff
182    fn scroll_around_point(&self, text: &Text, point: Point, cfg: PrintCfg);
183
184    /// Tells the [`Ui`] that this [`Area`] is the one that is
185    /// currently focused.
186    ///
187    /// Should make [`self`] the active [`Area`] while deactivating
188    /// any other active [`Area`].
189    fn set_as_active(&self);
190
191    ////////// Printing
192
193    /// Prints the [`Text`] via an [`Iterator`]
194    fn print(&self, text: &mut Text, cfg: PrintCfg, painter: Painter);
195
196    fn print_with<'a>(
197        &self,
198        text: &mut Text,
199        cfg: PrintCfg,
200        painter: Painter,
201        f: impl FnMut(&Caret, &Item) + 'a,
202    );
203
204    /// Sets a previously acquired [`PrintInfo`] to the area
205    ///
206    /// [`PrintInfo`]: Area::PrintInfo
207    fn set_print_info(&self, info: Self::PrintInfo);
208
209    /// Returns a printing iterator
210    ///
211    /// Given an iterator of [`text::Item`]s, returns an iterator
212    /// which assigns to each of them a [`Caret`]. This struct
213    /// essentially represents where horizontally would this character
214    /// be printed.
215    ///
216    /// If you want a reverse iterator, see [`Area::rev_print_iter`].
217    ///
218    /// [`text::Item`]: Item
219    fn print_iter<'a>(
220        &self,
221        iter: Iter<'a>,
222        cfg: IterCfg,
223    ) -> impl Iterator<Item = (Caret, Item)> + Clone + 'a
224    where
225        Self: Sized;
226
227    fn print_iter_from_top<'a>(
228        &self,
229        text: &'a Text,
230        cfg: IterCfg,
231    ) -> impl Iterator<Item = (Caret, Item)> + Clone + 'a
232    where
233        Self: Sized;
234
235    /// Returns a reversed printing iterator
236    ///
237    /// Given an iterator of [`text::Item`]s, returns a reversed
238    /// iterator which assigns to each of them a [`Caret`]. This
239    /// struct essentially represents where horizontally each
240    /// character would be printed.
241    ///
242    /// If you want a forwards iterator, see [`Area::print_iter`].
243    ///
244    /// [`text::Item`]: Item
245    fn rev_print_iter<'a>(
246        &self,
247        iter: RevIter<'a>,
248        cfg: IterCfg,
249    ) -> impl Iterator<Item = (Caret, Item)> + Clone + 'a;
250
251    ////////// Queries
252
253    /// Whether or not [`self`] has changed
254    ///
255    /// This would mean anything relevant that wouldn't be determined
256    /// by [`PrintInfo`], this is most likely going to be the bounding
257    /// box, but it may be something else.
258    ///
259    /// [`PrintInfo`]: Area::PrintInfo
260    fn has_changed(&self) -> bool;
261
262    /// Whether or not [`self`] is the "master" of `other`
263    ///
264    /// This can only happen if, by following [`self`]'s children, you
265    /// would eventually reach `other`.
266    fn is_master_of(&self, other: &Self) -> bool;
267
268    /// Returns the clustered master of [`self`], if there is one
269    ///
270    /// If [`self`] belongs to a clustered group, return the most
271    /// senior member of said cluster, which must hold all other
272    /// members of the cluster.
273    fn get_cluster_master(&self) -> Option<Self>;
274
275    /// Returns the statics from `self`
276    fn cache(&self) -> Option<Self::Cache>;
277
278    /// Gets the width of the area
279    fn width(&self) -> u32;
280
281    /// Gets the height of the area
282    fn height(&self) -> u32;
283
284    /// The first point that should be printed
285    fn first_points(&self, text: &Text, cfg: PrintCfg) -> (Point, Option<Point>);
286
287    /// The last point that should be printed
288    fn last_points(&self, text: &Text, cfg: PrintCfg) -> (Point, Option<Point>);
289
290    /// The current printing information of the area
291    fn print_info(&self) -> Self::PrintInfo;
292
293    /// Returns `true` if this is the currently active [`Area`]
294    ///
295    /// Only one [`Area`] should be active at any given moment.
296    fn is_active(&self) -> bool;
297}
298
299/// A container for a master [`Area`] in Parsec
300pub struct Window<U: Ui> {
301    nodes: Vec<Node<U>>,
302    files_area: U::Area,
303    layout: Box<dyn Layout<U>>,
304}
305
306impl<U: Ui> Window<U> {
307    /// Returns a new instance of [`Window`]
308    pub(crate) fn new<W: Widget<U>>(
309        ms: &'static U::MetaStatics,
310        widget: W,
311        checker: impl Fn() -> bool + Send + Sync + 'static,
312        layout: Box<dyn Layout<U>>,
313    ) -> (Self, Node<U>) {
314        let widget = RwData::<dyn Widget<U>>::new_unsized::<W>(Arc::new(RwLock::new(widget)));
315
316        let cache = if let Some(path) = widget.inspect_as(|file: &File| file.path())
317            && let Some(cache) = load_cache::<<U::Area as Area>::Cache>(path)
318        {
319            cache
320        } else {
321            <U::Area as Area>::Cache::default()
322        };
323
324        let area = U::new_root(ms, cache);
325
326        let node = Node::new::<W>(widget, area.clone(), checker);
327        node.update();
328
329        let window = Self {
330            nodes: vec![node.clone()],
331            files_area: area.clone(),
332            layout,
333        };
334
335        (window, node)
336    }
337
338    pub(crate) fn from_raw(
339        files_area: U::Area,
340        nodes: Vec<Node<U>>,
341        layout: Box<dyn Layout<U>>,
342    ) -> Self {
343        let files_area = files_area.get_cluster_master().unwrap_or(files_area);
344        Self { nodes, files_area, layout }
345    }
346
347    /// Pushes a [`Widget`] onto an existing one
348    pub(crate) fn push<W: Widget<U>>(
349        &mut self,
350        widget: W,
351        area: &U::Area,
352        checker: impl Fn() -> bool + 'static + Send + Sync,
353        specs: PushSpecs,
354        cluster: bool,
355    ) -> (Node<U>, Option<U::Area>) {
356        let widget = RwData::<dyn Widget<U>>::new_unsized::<W>(Arc::new(RwLock::new(widget)));
357
358        let cache = if let Some(path) = widget.inspect_as::<File, String>(|file| file.path())
359            && let Some(cache) = load_cache::<<U::Area as Area>::Cache>(path)
360        {
361            cache
362        } else {
363            <U::Area as Area>::Cache::default()
364        };
365
366        let on_files = self.files_area.is_master_of(area);
367        let (child, parent) = area.bisect(specs, cluster, on_files, cache, DuatPermission::new());
368
369        self.nodes.push(Node::new::<W>(widget, child, checker));
370        (self.nodes.last().unwrap().clone(), parent)
371    }
372
373    /// Pushes a [`File`] to the file's parent
374    ///
375    /// This function will push to the edge of `self.files_parent`
376    /// This is an area, usually in the center, that contains all
377    /// [`File`]s, and their associated [`Widget`]s,
378    /// with others being at the perifery of this area.
379    pub(crate) fn push_file(
380        &mut self,
381        mut file: File,
382        checker: impl Fn() -> bool + 'static + Send + Sync,
383    ) -> crate::Result<(Node<U>, Option<U::Area>), ()> {
384        let window_files = window_files(&self.nodes);
385        file.layout_ordering = window_files.len();
386        let (id, specs) = self.layout.new_file(&file, window_files)?;
387
388        let (child, parent) = self.push(file, &id.0, checker, specs, false);
389
390        if let Some(parent) = &parent
391            && id.0 == self.files_area
392        {
393            self.files_area = parent.clone();
394        }
395
396        Ok((child, parent))
397    }
398
399    /// Removes all [`Node`]s whose [`Area`]s where deleted
400    pub(crate) fn remove_file(&mut self, name: &str) {
401        let Some(node) = self
402            .nodes
403            .extract_if(.., |node| {
404                node.as_file()
405                    .is_some_and(|(f, ..)| f.read().name() == name)
406            })
407            .next()
408        else {
409            return;
410        };
411
412        // If this is the case, this means there is only one File left in this
413        // Window, so the files_area should be the cluster master of that
414        // File.
415        if let Some(parent) = node.area().delete(DuatPermission::new())
416            && parent == self.files_area
417        {
418            let files = self.file_nodes();
419            let (only_file, _) = files.first().unwrap();
420            self.files_area = only_file
421                .area()
422                .get_cluster_master()
423                .unwrap_or(only_file.area().clone())
424        }
425
426        if let Some(related) = node.related_widgets() {
427            let nodes = related.read();
428            for node in self.nodes.extract_if(.., |node| nodes.contains(node)) {
429                node.area().delete(DuatPermission::new());
430            }
431        }
432    }
433
434    /// Takes all [`Node`]s related to a given [`Node`]
435    pub(crate) fn take_file_and_related_nodes(&mut self, node: &Node<U>) -> Vec<Node<U>> {
436        if let Some(related) = node.related_widgets() {
437            let layout_ordering = node
438                .widget()
439                .inspect_as(|f: &File| f.layout_ordering)
440                .unwrap();
441
442            let nodes = related.read();
443            let nodes = self
444                .nodes
445                .extract_if(.., |n| nodes.contains(n) || n == node)
446                .collect();
447
448            for node in &self.nodes {
449                node.widget().mutate_as(|f: &mut File| {
450                    if f.layout_ordering > layout_ordering {
451                        f.layout_ordering -= 1;
452                    }
453                });
454            }
455
456            nodes
457        } else {
458            Vec::new()
459        }
460    }
461
462    pub(crate) fn insert_file_nodes(&mut self, layout_ordering: usize, nodes: Vec<Node<U>>) {
463        if let Some(i) = self.nodes.iter().position(|node| {
464            node.widget()
465                .inspect_as(|f: &File| f.layout_ordering)
466                .is_some_and(|lo| lo >= layout_ordering)
467        }) {
468            for node in self.nodes[i..].iter() {
469                node.widget()
470                    .mutate_as(|f: &mut File| f.layout_ordering += 1);
471            }
472            self.nodes.splice(i..i, nodes);
473        } else {
474            self.nodes.extend(nodes);
475        }
476    }
477
478    pub fn nodes(&self) -> impl ExactSizeIterator<Item = &Node<U>> + DoubleEndedIterator {
479        self.nodes.iter()
480    }
481
482    /// Returns an [`Iterator`] over the names of [`File`]s
483    /// and their respective [`Widget`] indices
484    ///
485    /// [`Widget`]: crate::widgets::Widget
486    pub fn file_names(&self) -> Vec<String> {
487        window_files(&self.nodes)
488            .into_iter()
489            .map(|f| f.0.widget().inspect_as(|f: &File| f.name()).unwrap())
490            .collect()
491    }
492
493    pub fn file_paths(&self) -> Vec<String> {
494        window_files(&self.nodes)
495            .into_iter()
496            .map(|f| f.0.widget().inspect_as(|f: &File| f.path()).unwrap())
497            .collect()
498    }
499
500    pub fn file_nodes(&self) -> WindowFiles<U> {
501        window_files(&self.nodes)
502    }
503
504    pub fn len_widgets(&self) -> usize {
505        self.nodes.len()
506    }
507}
508
509/// A dimension on screen, can either be horizontal or vertical
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum Axis {
512    Horizontal,
513    Vertical,
514}
515
516impl Axis {
517    pub fn perp(&self) -> Self {
518        match self {
519            Axis::Horizontal => Axis::Vertical,
520            Axis::Vertical => Axis::Horizontal,
521        }
522    }
523}
524
525impl From<PushSpecs> for Axis {
526    fn from(value: PushSpecs) -> Self {
527        match value.side {
528            Side::Above | Side::Below => Axis::Vertical,
529            _ => Axis::Horizontal,
530        }
531    }
532}
533
534#[derive(Debug)]
535pub enum DuatEvent {
536    Key(KeyEvent),
537    Resize,
538    FormChange,
539    MetaMsg(Text),
540    ReloadConfig,
541    OpenFile(String),
542    CloseFile(String),
543    SwapFiles(String, String),
544    OpenWindow(String),
545    SwitchWindow(usize),
546    Quit,
547}
548
549pub struct Sender(&'static mpsc::Sender<DuatEvent>);
550
551impl Sender {
552    pub fn new(sender: &'static mpsc::Sender<DuatEvent>) -> Self {
553        Self(sender)
554    }
555
556    pub fn send_key(&self, key: KeyEvent) -> Result<(), mpsc::SendError<DuatEvent>> {
557        self.0.send(DuatEvent::Key(key))
558    }
559
560    pub fn send_reload_config(&self) -> Result<(), mpsc::SendError<DuatEvent>> {
561        self.0.send(DuatEvent::ReloadConfig)
562    }
563
564    pub fn send_resize(&self) -> Result<(), mpsc::SendError<DuatEvent>> {
565        self.0.send(DuatEvent::Resize)
566    }
567
568    pub(crate) fn send_form_changed(&self) -> Result<(), mpsc::SendError<DuatEvent>> {
569        self.0.send(DuatEvent::FormChange)
570    }
571}
572
573pub enum UiEvent {
574    ResumePrinting,
575    PausePrinting,
576    Quit,
577}
578
579pub struct RoWindow<'a, U: Ui>(&'a Window<U>);
580
581impl<U: Ui> RoWindow<'_, U> {
582    /// Similar to the [`Iterator::fold`] operation, folding each
583    /// [`&File`][File`] by applying an operation,
584    /// returning a final result.
585    ///
586    /// The reason why this is a `fold` operation, and doesn't just
587    /// return an [`Iterator`], is because `f` will act on a
588    /// reference, as to not do unnecessary cloning of the widget's
589    /// inner [`RwData<W>`], and because [`Iterator`]s cannot return
590    /// references to themselves.
591    pub fn fold_files<B>(&self, init: B, mut f: impl FnMut(B, &File) -> B) -> B {
592        self.0.nodes.iter().fold(init, |accum, node| {
593            if let Some(file) = node.try_downcast::<File>() {
594                f(accum, &file.read())
595            } else {
596                accum
597            }
598        })
599    }
600
601    /// Similar to the [`Iterator::fold`] operation, folding each
602    /// [`&dyn Widget<U>`][Widget] by applying an
603    /// operation, returning a final result.
604    ///
605    /// The reason why this is a `fold` operation, and doesn't just
606    /// return an [`Iterator`], is because `f` will act on a
607    /// reference, as to not do unnecessary cloning of the widget's
608    /// inner [`RwData<W>`], and because [`Iterator`]s cannot return
609    /// references to themselves.
610    pub fn fold_widgets<B>(&self, init: B, mut f: impl FnMut(B, &dyn Widget<U>) -> B) -> B {
611        self.0.nodes.iter().fold(init, |accum, node| {
612            let f = &mut f;
613            node.raw_inspect(|widget| f(accum, widget))
614        })
615    }
616}
617
618pub struct RoWindows<U: Ui>(RoData<Vec<Window<U>>>);
619
620impl<U: Ui> RoWindows<U> {
621    pub fn new(windows: RoData<Vec<Window<U>>>) -> Self {
622        RoWindows(windows)
623    }
624
625    pub fn inspect_nth<B>(&self, index: usize, f: impl FnOnce(RoWindow<U>) -> B) -> Option<B> {
626        let windows = self.0.read();
627        windows.get(index).map(|window| f(RoWindow(window)))
628    }
629
630    pub fn try_inspect_nth<B>(&self, index: usize, f: impl FnOnce(RoWindow<U>) -> B) -> Option<B> {
631        self.0
632            .try_read()
633            .and_then(|windows| windows.get(index).map(|window| f(RoWindow(window))))
634    }
635}
636
637/// Information on how a [`Widget`] should be pushed onto another
638///
639/// This information is composed of three parts:
640///
641/// * A side to push;
642/// * A horizontal [`Constraint`];
643/// * A vertical [`Constraint`];
644///
645/// Constraints are demands that must be met by the widget's [`Area`],
646/// on a best effort basis.
647///
648/// So, for example, if the [`PushSpecs`] are:
649///
650/// ```rust
651/// use duat_core::ui::PushSpecs;
652/// let specs = PushSpecs::left().with_hor_len(3.0).with_ver_ratio(2, 3);
653/// ```
654///
655/// Then the widget should be pushed to the left, with a width of 3,
656/// and its height should be equal to two thirds of the area directly
657/// below.
658#[derive(Debug, Clone, Copy)]
659pub struct PushSpecs {
660    side: Side,
661    ver_con: Option<Constraint>,
662    hor_con: Option<Constraint>,
663}
664
665impl PushSpecs {
666    /// Returns a new instance of [`PushSpecs`]
667    pub fn left() -> Self {
668        Self {
669            side: Side::Left,
670            ver_con: None,
671            hor_con: None,
672        }
673    }
674
675    /// Returns a new instance of [`PushSpecs`]
676    pub fn right() -> Self {
677        Self {
678            side: Side::Right,
679            ver_con: None,
680            hor_con: None,
681        }
682    }
683
684    /// Returns a new instance of [`PushSpecs`]
685    pub fn above() -> Self {
686        Self {
687            side: Side::Above,
688            ver_con: None,
689            hor_con: None,
690        }
691    }
692
693    /// Returns a new instance of [`PushSpecs`]
694    pub fn below() -> Self {
695        Self {
696            side: Side::Below,
697            ver_con: None,
698            hor_con: None,
699        }
700    }
701
702    /// Returns a new instance of [`PushSpecs`]
703    pub fn to_left(self) -> Self {
704        Self { side: Side::Left, ..self }
705    }
706
707    /// Returns a new instance of [`PushSpecs`]
708    pub fn to_right(self) -> Self {
709        Self { side: Side::Right, ..self }
710    }
711
712    /// Returns a new instance of [`PushSpecs`]
713    pub fn to_above(self) -> Self {
714        Self { side: Side::Above, ..self }
715    }
716
717    /// Returns a new instance of [`PushSpecs`]
718    pub fn to_below(self) -> Self {
719        Self { side: Side::Below, ..self }
720    }
721
722    pub fn with_ver_len(self, len: f32) -> Self {
723        Self {
724            ver_con: Some(Constraint::Length(len)),
725            ..self
726        }
727    }
728
729    pub fn with_ver_min(self, min: f32) -> Self {
730        Self {
731            ver_con: Some(Constraint::Min(min)),
732            ..self
733        }
734    }
735
736    pub fn with_ver_max(self, max: f32) -> Self {
737        Self {
738            ver_con: Some(Constraint::Max(max)),
739            ..self
740        }
741    }
742
743    pub fn with_ver_ratio(self, den: u16, div: u16) -> Self {
744        Self {
745            ver_con: Some(Constraint::Ratio(den, div)),
746            ..self
747        }
748    }
749
750    pub fn with_hor_len(self, len: f32) -> Self {
751        Self {
752            hor_con: Some(Constraint::Length(len)),
753            ..self
754        }
755    }
756
757    pub fn with_hor_min(self, min: f32) -> Self {
758        Self {
759            hor_con: Some(Constraint::Min(min)),
760            ..self
761        }
762    }
763
764    pub fn with_hor_max(self, max: f32) -> Self {
765        Self {
766            hor_con: Some(Constraint::Max(max)),
767            ..self
768        }
769    }
770
771    pub fn with_hor_ratio(self, den: u16, div: u16) -> Self {
772        Self {
773            hor_con: Some(Constraint::Ratio(den, div)),
774            ..self
775        }
776    }
777
778    pub fn axis(&self) -> Axis {
779        match self.side {
780            Side::Above | Side::Below => Axis::Vertical,
781            Side::Right | Side::Left => Axis::Horizontal,
782        }
783    }
784
785    pub fn comes_earlier(&self) -> bool {
786        matches!(self.side, Side::Left | Side::Above)
787    }
788
789    pub fn ver_constraint(&self) -> Option<Constraint> {
790        self.ver_con
791    }
792
793    pub fn hor_constraint(&self) -> Option<Constraint> {
794        self.hor_con
795    }
796
797    pub fn constraint_on(&self, axis: Axis) -> Option<Constraint> {
798        match axis {
799            Axis::Horizontal => self.hor_con,
800            Axis::Vertical => self.ver_con,
801        }
802    }
803
804    pub fn is_resizable_on(&self, axis: Axis) -> bool {
805        let con = match axis {
806            Axis::Horizontal => self.hor_con,
807            Axis::Vertical => self.ver_con,
808        };
809        matches!(con, Some(Constraint::Min(..) | Constraint::Max(..)) | None)
810    }
811}
812
813#[derive(Debug, Clone, Copy, PartialEq)]
814pub enum Constraint {
815    Ratio(u16, u16),
816    Length(f32),
817    Min(f32),
818    Max(f32),
819}
820
821/// A direction, where a [`Widget`] will be placed in relation to
822/// another.
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
824pub enum Side {
825    Above,
826    Right,
827    Below,
828    Left,
829}
830
831#[derive(Debug, Clone, Copy)]
832pub struct Caret {
833    pub x: u32,
834    pub len: u32,
835    pub wrap: bool,
836}
837
838impl Caret {
839    #[inline(always)]
840    pub fn new(x: u32, len: u32, wrap: bool) -> Self {
841        Self { x, len, wrap }
842    }
843}
844
845/// Only Duat is allowed to alter the layout of [`Widget`]s, so this
846/// struct is used to forbid the end user from doing the same.
847pub struct DuatPermission(PhantomData<()>);
848
849impl DuatPermission {
850    pub(crate) fn new() -> Self {
851        Self(PhantomData)
852    }
853}