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