duat_utils/widgets/status_line/
mod.rs

1//! A widget that shows general information, usually about a [`File`]
2//!
3//! The [`StatusLine`] is a very convenient widget when the user
4//! simply wants to show some informatioon. The information, when
5//! relevant, can automatically be tied to the active file, saving
6//! some keystrokes for the user's configuration.
7//!
8//! There is also the [`status!`] macro, which is an extremely
9//! convenient way to modify the text of the status line, letting you
10//! place form, in the same way that [`text!`] does, and
11//! automatically recognizing a ton of different types of functions,
12//! that can read from the file, from other places, from [data] types,
13//! etc.
14//!
15//! [data]: crate::data
16mod state;
17
18use std::{cell::RefCell, rc::Rc};
19
20use duat_core::{prelude::*, text::Builder, ui::Side};
21
22pub use self::{macros::status, state::State};
23use crate::state::{file_fmt, main_fmt, mode_fmt, mode_name, sels_fmt};
24
25/// A widget to show information, usually about a [`File`]
26///
27/// This widget is updated whenever any of its parts needs to be
28/// updated, and it also automatically adjusts to where it was pushed.
29/// For example, if you push it with [`OnFileOpen`], it's information
30/// will point to the [`File`] to which it was pushed. However, if you
31/// push it with [`OnWindowOpen`], it will always point to the
32/// currently active [`File`]:
33///
34/// ```rust
35/// use duat_core::{
36///     hook::{OnFileOpen, OnWindowOpen},
37///     prelude::*,
38/// };
39/// use duat_utils::{state::*, widgets::*};
40///
41/// fn setup_generic_over_ui<U: Ui>() {
42///     hook::remove("FileWidgets");
43///     hook::add::<OnFileOpen<U>, U>(|pa, builder| {
44///         builder.push(pa, LineNumbers::cfg());
45///         builder.push(pa, status!("{file_fmt}").above());
46///     });
47///
48///     hook::remove("WindowWidgets");
49///     hook::add::<OnWindowOpen<U>, U>(|pa, builder| {
50///         let footer = FooterWidgets::new(status!("{mode_fmt} {sels_fmt} {main_fmt}"));
51///         builder.push(pa, footer);
52///     });
53/// }
54/// ```
55///
56/// In the above example, each file would have a status line with the
57/// name of the file, and by pushing [`FooterWidgets`], you will push
58/// a [`StatusLine`], [`PromptLine`] and [`Notifications`] combo to
59/// each window. This [`StatusLine`] will point to the currently
60/// active [`File`], instead of a specific one.
61///
62/// You will usually want to create [`StatusLine`]s via the
63/// [`status!`] macro, since that is how you can customize it.
64/// Although, if you want the regular status line, you can just:
65///
66/// ```rust
67/// use duat_core::{hook::OnFileOpen, prelude::*};
68/// use duat_utils::widgets::*;
69///
70/// fn setup_generic_over_ui<U: Ui>() {
71///     hook::remove("FileWidgets");
72///     hook::add::<OnFileOpen<U>, U>(|pa, builder| {
73///         builder.push(pa, LineNumbers::cfg());
74///         builder.push(pa, StatusLine::cfg().above());
75///     });
76/// }
77/// ```
78///
79/// [`File`]: duat_core::file::File
80/// [`OnFileOpen`]: duat_core::hook::OnFileOpen
81/// [`OnWindowOpen`]: duat_core::hook::OnWindowOpen
82/// [`PromptLine`]: super::PromptLine
83/// [`Notifications`]: super::Notifications
84/// [`FooterWidgets`]: super::FooterWidgets
85pub struct StatusLine<U: Ui> {
86    handle: FileHandle<U>,
87    text_fn: TextFn<U>,
88    text: Text,
89    checker: Box<dyn Fn() -> bool>,
90}
91
92impl<U: Ui> Widget<U> for StatusLine<U> {
93    type Cfg = StatusLineCfg<U>;
94
95    fn update(pa: &mut Pass, handle: Handle<Self, U>) {
96        let text = handle.read(pa, |wid, _| wid.text_fn.borrow_mut()(pa, &wid.handle));
97        handle.widget().replace_text(pa, text);
98    }
99
100    fn needs_update(&self) -> bool {
101        self.handle.has_changed() || (self.checker)()
102    }
103
104    fn cfg() -> Self::Cfg {
105        macros::status!("{file_fmt} {mode_fmt} {sels_fmt} {}", main_fmt)
106    }
107
108    fn text(&self) -> &Text {
109        &self.text
110    }
111
112    fn text_mut(&mut self) -> &mut Text {
113        &mut self.text
114    }
115
116    fn once() -> Result<(), Text> {
117        form::set_weak("file", Form::yellow().italic());
118        form::set_weak("selections", Form::dark_blue());
119        form::set_weak("coord", Form::dark_yellow());
120        form::set_weak("separator", Form::cyan());
121        form::set_weak("mode", Form::green());
122        Ok(())
123    }
124}
125
126/// The [`WidgetCfg`] for a [`StatusLine`]
127pub struct StatusLineCfg<U: Ui> {
128    builder: Option<BuilderFn<U>>,
129    checker: Option<Box<dyn Fn() -> bool>>,
130    specs: PushSpecs,
131}
132
133impl<U: Ui> StatusLineCfg<U> {
134    #[doc(hidden)]
135    pub fn new_with(
136        (builder, checker): (BuilderFn<U>, Box<dyn Fn() -> bool>),
137        specs: PushSpecs,
138    ) -> Self {
139        Self {
140            builder: Some(builder),
141            checker: Some(checker),
142            specs,
143        }
144    }
145
146    /// Replaces the previous formatting with a new one
147    pub fn replace(self, new: Self) -> Self {
148        Self { specs: self.specs, ..new }
149    }
150
151    /// Puts the [`StatusLine`] above, as opposed to below
152    pub fn above(self) -> Self {
153        Self {
154            specs: PushSpecs::above().with_ver_len(1.0),
155            ..self
156        }
157    }
158
159    /// Puts the [`StatusLine`] below, this is the default
160    pub fn below(self) -> Self {
161        Self {
162            specs: PushSpecs::below().with_ver_len(1.0),
163            ..self
164        }
165    }
166
167    /// Puts the [`StatusLine`] on the right, instead of below
168    ///
169    /// use this if you want a single line [`StatusLine`],
170    /// [`PromptLine`]/[`Notifications`] combo.
171    ///
172    /// [`PromptLine`]: super::PromptLine
173    /// [`Notifications`]: super::Notifications
174    pub fn right_ratioed(self, den: u16, div: u16) -> Self {
175        Self {
176            specs: self.specs.to_right().with_hor_ratio(den, div),
177            ..self
178        }
179    }
180
181    /// The [`PushSpecs`] in use
182    pub fn specs(&self) -> PushSpecs {
183        self.specs
184    }
185}
186
187impl<U: Ui> WidgetCfg<U> for StatusLineCfg<U> {
188    type Widget = StatusLine<U>;
189
190    fn build(self, pa: &mut Pass, handle: Option<FileHandle<U>>) -> (Self::Widget, PushSpecs) {
191        let handle = match handle {
192            Some(handle) => handle,
193            None => context::dyn_file(pa).unwrap(),
194        };
195
196        let checker = match self.checker {
197            Some(checker) => checker,
198            // mode checker because mode_name is used in the default
199            None => Box::new(crate::state::mode_name().checker()),
200        };
201
202        let text_fn: TextFn<U> = match self.builder {
203            Some(mut text_fn) => Rc::new(RefCell::new(
204                for<'a, 'b> move |pa: &'a Pass, handle: &'b FileHandle<U>| -> Text {
205                    text_fn(pa, Text::builder(), handle)
206                },
207            )),
208            None => {
209                let cfg = match self.specs.side() {
210                    Side::Above | Side::Below => {
211                        let mode_upper_fmt = mode_name().map(|mode| {
212                            let mode = match mode.split_once('<') {
213                                Some((mode, _)) => mode,
214                                None => mode,
215                            };
216                            txt!("[mode]{}", mode.to_uppercase()).build()
217                        });
218                        macros::status!("{mode_upper_fmt}{Spacer}{file_fmt} {sels_fmt} {main_fmt}")
219                    }
220                    Side::Right => {
221                        macros::status!("{AlignRight}{file_fmt} {mode_fmt} {sels_fmt} {main_fmt}")
222                    }
223                    Side::Left => unreachable!(),
224                };
225
226                let mut text_fn = cfg.builder.unwrap();
227                Rc::new(RefCell::new(
228                    for<'a, 'b> move |pa: &'a Pass, handle: &'b FileHandle<U>| -> Text {
229                        text_fn(pa, Text::builder(), handle)
230                    },
231                ))
232            }
233        };
234
235        let widget = StatusLine {
236            handle,
237            text_fn,
238            text: Text::default(),
239            checker: Box::new(checker),
240        };
241        (widget, self.specs)
242    }
243}
244
245impl<U: Ui> Default for StatusLineCfg<U> {
246    fn default() -> Self {
247        StatusLine::cfg()
248    }
249}
250
251mod macros {
252    /// The macro that creates a [`StatusLine`]
253    ///
254    /// This macro works like the [`txt!`] macro, in  that [`Form`]s
255    /// are pushed with `[{FormName}]`. However, [`txt!`]  is
256    /// evaluated immediately, while [`status!`] is evaluated when
257    /// updates occur.
258    ///
259    /// The macro will mostly read from the [`File`] widget and its
260    /// related structs. In order to do that, it will accept functions
261    /// as arguments. These functions take the following
262    /// parameters:
263    ///
264    /// * The [`&File`] widget;
265    /// * The [`&Selections`] of the [`File`]
266    /// * A specific [`&impl Widget`], which is glued to the [`File`];
267    ///
268    /// Here's some examples:
269    ///
270    /// ```rust
271    /// use duat_core::{hook::OnWindowOpen, prelude::*};
272    /// use duat_utils::widgets::status;
273    ///
274    /// fn name_but_funky<U: Ui>(file: &File<U>) -> String {
275    ///     let mut name = String::new();
276    ///
277    ///     for byte in unsafe { name.as_bytes_mut().iter_mut().step_by(2) } {
278    ///         *byte = byte.to_ascii_uppercase();
279    ///     }
280    ///
281    ///     name
282    /// }
283    ///
284    /// fn powerline_main_fmt<U: Ui>(file: &File<U>, area: &U::Area) -> Text {
285    ///     let selections = file.selections();
286    ///     let cfg = file.print_cfg();
287    ///     let v_caret = selections
288    ///         .get_main()
289    ///         .unwrap()
290    ///         .v_caret(file.text(), area, cfg);
291    ///
292    ///     txt!(
293    ///         "[separator][coord]{}[separator][coord]{}[separator][coord]{}",
294    ///         v_caret.visual_col(),
295    ///         v_caret.line(),
296    ///         file.len_lines()
297    ///     )
298    ///     .build()
299    /// }
300    ///
301    /// fn setup_generic_over_ui<U: Ui>() {
302    ///     hook::add::<OnWindowOpen<U>, U>(|pa, builder| {
303    ///         builder.push(pa, status!("[file]{name_but_funky}[] {powerline_main_fmt}"));
304    ///     });
305    /// }
306    /// ```
307    ///
308    /// Now, there are other types of arguments that you can also
309    /// pass. They update differently from the previous ones. The
310    /// previous arguments update when the [`File`] updates. The
311    /// following types of arguments update independently or not
312    /// at all:
313    ///
314    /// - A [`Text`] argument can include [`Form`]s and buttons;
315    /// - Any [`impl Display`], such as numbers, strings, chars, etc;
316    /// - [`RwData`] or [`DataMap`]s of the previous two types. These
317    ///   will update whenever the data inside is changed;
318    /// - An [`(FnMut() -> Text | impl Display, FnMut() -> bool)`]
319    ///   tuple. The first function returns what will be shown, while
320    ///   the second function tells it to update;
321    ///
322    /// Here's an examples:
323    ///
324    /// ```rust
325    /// use std::sync::atomic::{AtomicUsize, Ordering};
326    ///
327    /// use duat_core::{data::RwData, hook::OnWindowOpen, prelude::*};
328    /// use duat_utils::widgets::status;
329    ///
330    /// # fn test<U: Ui>() {
331    /// let changing_text = RwData::new(txt!("Prev text").build());
332    ///
333    /// fn counter(pa: &Pass) -> usize {
334    ///     static COUNT: AtomicUsize = AtomicUsize::new(0);
335    ///     COUNT.fetch_add(1, Ordering::Relaxed)
336    /// }
337    ///
338    /// hook::add::<OnWindowOpen<U>, U>({
339    ///     let changing_text = changing_text.clone();
340    ///     move |pa, builder| {
341    ///         let changing_text = changing_text.clone();
342    ///         let checker = changing_text.checker();
343    ///
344    ///         let text = txt!("Static text").build();
345    ///
346    ///         builder.push(
347    ///             pa,
348    ///             status!("{changing_text} [counter]{}[] {text}", (counter, checker)),
349    ///         );
350    ///     }
351    /// });
352    /// # }
353    /// ```
354    ///
355    /// In the above example, I added some dynamic [`Text`], through
356    /// the usage of an [`RwData<Text>`], I added some static
357    /// [`Text`], some [`Form`]s (`"counter"` and `"default"`) and
358    /// even a counter, which will update whenever `changing_text`
359    /// is altered.
360    ///
361    /// [`StatusLine`]: super::StatusLine
362    /// [`txt!`]: duat_core::text::txt
363    /// [`File`]: duat_core::file::File
364    /// [`&File`]: duat_core::file::File
365    /// [`&Selections`]: duat_core::mode::Selections
366    /// [`&impl Widget`]: duat_core::ui::Widget
367    /// [`impl Display`]: std::fmt::Display
368    /// [`Text`]: duat_core::text::Text
369    /// [`RwData`]: duat_core::data::RwData
370    /// [`DataMap`]: duat_core::data::DataMap
371    /// [`FnMut() -> Arg`]: FnMut
372    /// [`(FnMut() -> Text | impl Display, FnMut() -> bool)`]: FnMut
373    /// [`RwData<Text>`]: duat_core::data::RwData
374    /// [`Form`]: duat_core::form::Form
375    pub macro status($($parts:tt)*) {{
376        #[allow(unused_imports)]
377        use $crate::{
378            private_exports::{
379                duat_core::{context::FileHandle, data::Pass, text::Builder, ui::PushSpecs},
380                format_like, parse_form, parse_status_part, parse_str
381            },
382            widgets::StatusLineCfg,
383        };
384
385        let text_fn= |_: &Pass, _: &mut Builder, _: &FileHandle<_>| {};
386        let checker = || false;
387
388        let (mut text_fn, checker) = format_like!(
389            parse_str,
390            [('{', parse_status_part, false), ('[', parse_form, true)],
391            (text_fn, checker),
392            $($parts)*
393        );
394
395        StatusLineCfg::new_with(
396            {
397                (
398                    Box::new(move |pa: &Pass, mut builder: Builder, handle: &FileHandle<_>| {
399                        text_fn(pa, &mut builder, &handle);
400                        builder.build()
401                    }),
402                    Box::new(checker),
403                )
404            },
405            PushSpecs::below().with_ver_len(1.0),
406        )
407    }}
408}
409
410type TextFn<U> = Rc<RefCell<dyn FnMut(&Pass, &FileHandle<U>) -> Text>>;
411type BuilderFn<U> = Box<dyn FnMut(&Pass, Builder, &FileHandle<U>) -> Text>;