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>;