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