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