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