duat_base/widgets/status_line/
mod.rs

1//! A widget that shows general information, usually about a
2//! [`Buffer`]
3//!
4//! The [`StatusLine`] is a very convenient widget when the user
5//! simply wants to show some informatioon. The information, when
6//! relevant, can automatically be tied to the active buffer, saving
7//! some keystrokes for the user's configuration.
8//!
9//! There is also the [`status!`] macro, which is an extremely
10//! convenient way to modify the text of the status line, letting you
11//! place form, in the same way that [`text!`] does, and
12//! automatically recognizing a ton of different types of functions,
13//! that can read from the buffer, from other places, from [data]
14//! types, etc.
15//!
16//! [data]: crate::data
17//! [`Buffer`]: duat_core::buffer::Buffer
18use duat_core::{
19    context::{self, DynBuffer, Handle},
20    data::Pass,
21    text::{AlignRight, Builder, Spacer, Text},
22    ui::{PushSpecs, PushTarget, Side, Widget},
23};
24
25pub use self::state::State;
26use crate::state::{main_txt, mode_txt, name_txt, sels_txt};
27
28mod state;
29#[macro_use]
30mod macros;
31#[doc(inline)]
32pub use crate::__status__ as status;
33
34/// A widget to show information, usually about a [`Buffer`]
35///
36/// This widget is updated whenever any of its parts needs to be
37/// updated, and it also automatically adjusts to where it was pushed.
38/// For example, if you push it to a buffer (via
39/// `hook::add::<Buffer>`, for example), it's information will point
40/// to the [`Buffer`] to which it was pushed. However, if you push it
41/// with [`WindowCreated`], it will always point to the currently
42/// active [`Buffer`]:
43///
44/// ```rust
45/// # duat_core::doc_duat!(duat);
46/// # use duat_base::widgets::status;
47/// setup_duat!(setup);
48/// use duat::prelude::*;
49///
50/// fn setup() {
51///     opts::one_line_footer();
52///     opts::set_status(|pa| status!("{AlignRight}{} {sels_txt} {main_txt}", mode_txt()));
53///
54///     hook::add::<Buffer>(|pa, handle| {
55///         status!("{AlignCenter}{name_txt}")
56///             .above()
57///             .push_on(pa, handle);
58///         Ok(())
59///     });
60/// }
61/// ```
62///
63/// In the code above, I'm modifying the "global" `StatusLine` through
64/// [`opts::set_status`] (this can be done with [hooks] as well, but
65/// this method is added for convenience's sake). This is in
66/// conjunction with [`opts::one_line_footer`], which will place
67/// the [`PromptLine`] and `StatusLine` on the same line.
68///
69/// After that, I'm _also_ pushing a new `StatusLine` above every
70/// opened [`Buffer`], showing that `Buffer`]'s name, centered.
71///
72/// You will usually want to create `StatusLine`s via the
73/// [`status!`] macro, since that is how you can customize it.
74/// Although, if you want the regular status line, you can call
75/// [`StatusLine::builder`]:
76///
77/// ```rust
78/// # duat_core::doc_duat!(duat);
79/// # use duat_base::widgets::StatusLine;
80/// setup_duat!(setup);
81/// use duat::prelude::*;
82///
83/// fn setup() {
84///     hook::add::<Buffer>(|pa, handle| {
85///         StatusLine::builder().above().push_on(pa, handle);
86///         Ok(())
87///     });
88/// }
89/// ```
90///
91/// [`Buffer`]: duat_core::buffer::Buffer
92/// [`WindowCreated`]: duat_core::hook::WindowCreated
93/// [`PromptLine`]: super::PromptLine
94/// [`Notifications`]: super::Notifications
95/// [`FooterWidgets`]: super::FooterWidgets
96/// [`opts::set_status`]: https://docs.rs/duat/latest/duat/opts/fn.set_status.html
97/// [`opts::one_line_footer`]: https://docs.rs/duat/latest/duat/opts/fn.one_line_footer.html
98/// [hooks]: duat_core::hook
99pub struct StatusLine {
100    buffer_handle: BufferHandle,
101    text_fn: TextFn,
102    text: Text,
103    checker: Box<dyn Fn(&Pass) -> bool + Send>,
104}
105
106impl StatusLine {
107    fn new(builder: StatusLineFmt, buffer_handle: BufferHandle) -> Self {
108        let (builder_fn, checker) = if let Some((builder, checker)) = builder.fns {
109            (builder, checker)
110        } else {
111            let mode_txt = mode_txt();
112
113            let opts = match builder.specs.side {
114                Side::Above | Side::Below => {
115                    status!("{mode_txt}{Spacer}{name_txt} {sels_txt} {main_txt}")
116                }
117                Side::Right => {
118                    status!("{AlignRight}{name_txt} {mode_txt} {sels_txt} {main_txt}",)
119                }
120                Side::Left => unreachable!(),
121            };
122
123            opts.fns.unwrap()
124        };
125
126        Self {
127            buffer_handle,
128            text_fn: Box::new(move |pa, fh| {
129                let builder = Text::builder();
130                builder_fn(pa, builder, fh)
131            }),
132            text: Text::new(),
133            checker: Box::new(checker),
134        }
135    }
136
137    /// Replaces this `StatusLine` with a new one
138    pub fn fmt(&mut self, new: StatusLineFmt) {
139        let handle = self.buffer_handle.clone();
140        *self = StatusLine::new(new, handle);
141    }
142
143    /// Returns a [`StatusLineFmt`], which can be used to push
144    /// around `StatusLine`s
145    ///
146    /// The same can be done more conveniently with the [`status!`]
147    /// macro, which is imported by default in the configuration
148    /// crate.
149    pub fn builder() -> StatusLineFmt {
150        StatusLineFmt { fns: None, ..Default::default() }
151    }
152}
153
154impl Widget for StatusLine {
155    fn update(pa: &mut Pass, handle: &Handle<Self>) {
156        if let BufferHandle::Dynamic(dyn_file) = &mut handle.write(pa).buffer_handle {
157            dyn_file.swap_to_current();
158        }
159
160        let sl = handle.read(pa);
161
162        handle.write(pa).text = match &sl.buffer_handle {
163            BufferHandle::Fixed(buffer) => (sl.text_fn)(pa, buffer),
164            BufferHandle::Dynamic(dyn_file) => (sl.text_fn)(pa, dyn_file.handle()),
165        };
166
167        // Do this in case the Buffer is never read during Text construction
168        match &handle.read(pa).buffer_handle {
169            BufferHandle::Fixed(handle) => handle.declare_as_read(),
170            BufferHandle::Dynamic(dyn_file) => dyn_file.declare_as_read(),
171        }
172    }
173
174    fn needs_update(&self, pa: &Pass) -> bool {
175        let buffer_changed = match &self.buffer_handle {
176            BufferHandle::Fixed(handle) => handle.has_changed(pa),
177            BufferHandle::Dynamic(dyn_buf) => dyn_buf.has_changed(pa),
178        };
179        let checkered = (self.checker)(pa);
180
181        buffer_changed || checkered
182    }
183
184    fn text(&self) -> &Text {
185        &self.text
186    }
187
188    fn text_mut(&mut self) -> &mut Text {
189        &mut self.text
190    }
191}
192
193/// A builder for [`StatusLine`]s
194///
195/// This struct is created by the [`status!`] macro, and its purpose
196/// is mainly to allow formatting of the `StatusLine`.
197///
198/// There is also the [`StatusLineFmt::above`] method, which places
199/// the `StatusLine` above, rather than below.
200pub struct StatusLineFmt {
201    fns: Option<(BuilderFn, CheckerFn)>,
202    specs: PushSpecs,
203}
204
205impl StatusLineFmt {
206    /// Push the [`StatusLine`]
207    ///
208    /// If the handle's [`Widget`] is a [`Buffer`], then this
209    /// `StatusLine` will refer to it when printing information about
210    /// `Buffer`s. Otherwise, the `StatusLine` will print information
211    /// about the currently active `Buffer`.
212    ///
213    /// [`Buffer`]: duat_core::buffer::Buffer
214    pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<StatusLine> {
215        let specs = self.specs;
216        let status_line = StatusLine::new(self, match push_target.try_downcast() {
217            Some(handle) => BufferHandle::Fixed(handle),
218            None => BufferHandle::Dynamic(context::dynamic_buffer(pa)),
219        });
220
221        push_target.push_outer(pa, status_line, specs)
222    }
223
224    /// Returns a new `StatusLineFmt`, meant to be called only be the
225    /// [`status!`] macro
226    #[doc(hidden)]
227    pub fn new_with(fns: (BuilderFn, CheckerFn)) -> Self {
228        Self { fns: Some(fns), ..Default::default() }
229    }
230
231    /// Puts the [`StatusLine`] above, as opposed to below
232    pub fn above(self) -> Self {
233        Self {
234            specs: PushSpecs { side: Side::Above, ..self.specs },
235            ..self
236        }
237    }
238
239    /// Puts the [`StatusLine`] below, this is the default
240    pub fn below(self) -> Self {
241        Self {
242            specs: PushSpecs { side: Side::Below, ..self.specs },
243            ..self
244        }
245    }
246
247    /// Puts the [`StatusLine`] on the right
248    pub(crate) fn right(self) -> Self {
249        Self {
250            specs: PushSpecs { side: Side::Right, ..self.specs },
251            ..self
252        }
253    }
254}
255
256impl Default for StatusLineFmt {
257    fn default() -> Self {
258        Self {
259            fns: None,
260            specs: PushSpecs {
261                side: Side::Below,
262                height: Some(1.0),
263                ..Default::default()
264            },
265        }
266    }
267}
268
269type TextFn = Box<dyn Fn(&Pass, &Handle) -> Text + Send>;
270type BuilderFn = Box<dyn Fn(&Pass, Builder, &Handle) -> Text + Send>;
271type CheckerFn = Box<dyn Fn(&Pass) -> bool + Send>;
272
273#[derive(Clone)]
274enum BufferHandle {
275    Fixed(Handle),
276    Dynamic(DynBuffer),
277}