Skip to main content

ez_tui/core/
view.rs

1use crate::inputs::legend::Legend;
2use crate::types::args::EzArgs;
3use crate::types::cpt_ids::{EzCptId, EzCptIds};
4use crate::types::event::EzMsg;
5use crate::types::focus_stack::{FocusChangeInfo, FocusStack};
6use crate::types::state::EzState;
7use crate::{
8    AppStateCpt, AttrValue, Attribute, BoxedComponent, CustomLayout, Error, EzEvent, Legends,
9    LogsViewer, Matcher, Props, RenderIndicatorCpt, Result, State, Theme, TickIndicatorCpt,
10};
11use crossterm::event::{KeyCode, KeyModifiers};
12use displaydoc::Display;
13use hashlink::LinkedHashMap;
14use ratatui::Frame;
15use ratatui::buffer::Buffer;
16use ratatui::layout::Rect;
17use std::convert::Into;
18use std::fmt::Debug;
19use std::sync::LazyLock;
20use tracing::{debug, info, warn};
21
22static GLOBAL: LazyLock<Legend> = LazyLock::new(|| {
23    Legend::new(
24        "Global".into(),
25        vec![
26            Matcher::new(vec![KeyCode::Char('Q').into()], "Quit".to_string()),
27            Matcher::new(vec![KeyCode::Tab.into()], "Next focus".to_string()),
28            Matcher::new(
29                vec![(KeyCode::Tab, KeyModifiers::SHIFT).into()],
30                "Previous focus".to_string(),
31            ),
32        ],
33    )
34});
35/// A type to describe a "reusable" component.
36///
37/// For really specific component, just implement [`Component`] and [`MockComponent`] directly.
38///
39/// To actually reuse a [`MockComponent`], you still need to create a struct implementing [`Component`], initialize it with a field holding the mock to reuse and then either:
40/// * implement a dummy version [`MockComponent`] for your struct that will just call the mock's methods
41/// * use the [`MockComponent`] derive macro to generate the implementation for you. (as long as the field is named `component`).
42pub trait MockComponent {
43    /// Draw the component in the given area of the given buffer.
44    fn draw(&mut self, area: Rect, buf: &mut Buffer, theme: &Theme);
45}
46/// Similar to [`MockComponent`], but for the component's attributes.
47pub trait MockProps {
48    /// Get a reference to the component's attribute
49    fn props(&self) -> Option<&Props> {
50        None
51    }
52
53    /// Get a mutable reference to the component's attribute
54    fn props_mut(&mut self) -> Option<&mut Props> {
55        None
56    }
57}
58/// A type to describe a concrete component.
59/// In your app, every component should have it's own struct implementing this trait.
60// FEAT: this component VS mock component is copied from tui-realm, I don't know if I want to keep that or not
61pub trait Component<CID, CA, CS, CM>: MockComponent + MockProps
62where
63    CID: EzCptIds,
64    CA: EzArgs,
65    CS: EzState,
66    CM: EzMsg,
67{
68    /// Method to implement to handle events.
69    /// The component will receive any event if it is focused; but only events it is subscribed to otherwise.
70    #[allow(unused_variables)]
71    fn on_event(
72        &mut self,
73        _event: EzEvent<CID, CM>,
74        state: &mut State<CS>,
75    ) -> Vec<EzEvent<CID, CM>> {
76        vec![]
77    }
78
79    /// Method called on first tick only.
80    #[allow(unused_variables)]
81    fn init(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
82        vec![]
83    }
84
85    /// Method called on every tick. You should implement it only if you need to save a reference to the state or if your business logic needs to execute some code on a regular basis.
86    #[allow(unused_variables)]
87    fn tick(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
88        vec![]
89    }
90
91    /// Whether the component is focusable or not.
92    fn focusable(&self) -> bool {
93        false
94    }
95    /// Get the component's hotkeys legend.
96    fn legend(&self) -> Option<Legend> {
97        None
98    }
99
100    /// Returns true if the focus is handled internally.
101    /// Let's say this component has two inner focusable area:
102    ///  * on first call to `capture_focus` with `forward = true`, it's your responsibility to focus the first area and return false.
103    /// * on second call same; focus the second area and return false.
104    /// * on third call, unfocus all area and return **false** so the view can cycle to the next component.
105    #[allow(unused_variables)]
106    fn capture_focus(&mut self, forward: bool) -> bool {
107        false
108    }
109}
110
111/// The errors that could happen when using the [`View`]
112#[derive(Debug, Display, Eq, PartialEq, Clone, PartialOrd)]
113pub enum ViewError {
114    /// component already mounted
115    ComponentAlreadyMounted,
116    /// component '{0}' not found
117    ComponentNotFound(String),
118    /// there's no component to blur
119    NoComponentToBlur,
120    /// you have not set any layout
121    NoLayout,
122    /// layout '{0}' has {1} areas but {2} items.
123    MismatchConstraintsAndCpts(String, usize, usize),
124}
125
126/// The view is the struct that holds all your components. You'll only have one view in your app but as many components as you want.
127/// Those components will be orchestrated by the view. This includes handling their layout, the focused one and forward event to them if needed.
128///
129/// Every component must implement two traits: [`Component`] and [`MockComponent`].
130/// [`Component`]; its sole responsibility is to **alter its state based on the events it receives.**
131/// [`MockComponent`]; it is responsible for rendering the component and holding its state.
132//FEAT: when unmounting a component, we should probably keep it in components map. We should also keep track of two sets of
133// mounted and unmounted components to not render the unmounted ones. On remount/unmount switch the id from list This will make [`SubClause`] more relevant
134#[allow(missing_debug_implementations)]
135pub struct View<CID, CA, CS, CM>
136where
137    CID: EzCptIds,
138    CA: EzArgs,
139    CS: EzState,
140    CM: EzMsg,
141{
142    theme: Theme,
143    layout: Option<CustomLayout<EzCptId<CID>>>,
144    components: LinkedHashMap<EzCptId<CID>, BoxedComponent<CID, CA, CS, CM>>,
145    focus_stack: FocusStack<CID>,
146    dynamic_legend: Option<Legend>,
147}
148
149impl<CID, CA, CS, CM> Default for View<CID, CA, CS, CM>
150where
151    CID: EzCptIds,
152    CA: EzArgs,
153    CS: EzState,
154    CM: EzMsg,
155{
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161impl<CID, CA, CS, CM> View<CID, CA, CS, CM>
162where
163    CID: EzCptIds,
164    CA: EzArgs,
165    CS: EzState,
166    CM: EzMsg,
167{
168    /// Create a new view with the given [`AppArguments`]
169    #[must_use]
170    pub fn new() -> Self {
171        Self {
172            theme: Theme::default(),
173            layout: None,
174            components: LinkedHashMap::new(),
175            focus_stack: FocusStack::new(),
176            dynamic_legend: None,
177        }
178    }
179}
180
181// Public interface
182impl<CID, CA, CS, CM> View<CID, CA, CS, CM>
183where
184    CID: EzCptIds,
185    CA: EzArgs,
186    CS: EzState,
187    CM: EzMsg,
188{
189    #[must_use]
190    /// Set the thme to be used by the view
191    pub fn set_theme(mut self, theme: Theme) -> Self {
192        self.theme = theme;
193        self
194    }
195
196    /// Get the current focus
197    pub fn focused(&mut self) -> Option<&EzCptId<CID>> {
198        self.focus_stack.current_focus()
199    }
200
201    /// Try to focus a component
202    ///
203    /// # Errors
204    /// * [`ViewError::ComponentNotFound`] if the component is not mounted
205    pub fn focus(&mut self, id: &EzCptId<CID>, forward: bool) -> Result<bool> {
206        if let Some(cpt) = self.components.get(id) {
207            if cpt.focusable() {
208                self.set_legend(cpt.legend());
209                let focus_state = self.focus_stack.focus(id);
210                self.handle_change_of_focus(focus_state)?;
211                debug!("Focused {:?}", id);
212
213                if let Some(new_cpt) = self.components.get_mut(id) {
214                    new_cpt.capture_focus(forward);
215                }
216
217                return Ok(true);
218            }
219        }
220        Ok(false)
221    }
222    /// Set the focus to a component
223    ///
224    /// # Errors
225    /// * [`ViewError::ComponentNotFound`] if the component to be focused is not mounted
226    pub fn cycle_focus(&mut self) -> Result<()> {
227        // If the current component can capture focus, do it
228        if let Some(id) = self.focus_stack.current_focus().cloned()
229            && let Some(cpt) = self.components.get_mut(&id)
230            && cpt.capture_focus(true)
231        {
232            return Ok(());
233        }
234
235        // Otherwise, cycle through the components in the default FocusChain
236        let current = self.focus_stack.current_focus().cloned();
237        let ids: Vec<EzCptId<CID>> = self.components.keys().cloned().collect();
238
239        // If empty, just blur
240        if ids.is_empty() {
241            return self.blur();
242        }
243
244        // Find the index of the current item
245        let start_index = current
246            .as_ref()
247            .and_then(|c| ids.iter().position(|x| x == c))
248            .map_or(0, |i| (i + 1) % ids.len()); // If no current, start at beginning
249
250        // Loop through at most N items (one full cycle)
251        for offset in 0..ids.len() {
252            let iddddddddd = (start_index + offset) % ids.len();
253            let cid = &ids[iddddddddd];
254            if self.focus(cid, true)? {
255                return Ok(());
256            }
257        }
258
259        // No focus succeeded, fallback to blur
260        self.blur()
261    }
262
263    /// Set the focus to a component
264    ///
265    /// # Errors
266    /// * [`ViewError::ComponentNotFound`] if the initial component is not mounted
267    pub fn initial_focus(mut self, id: &EzCptId<CID>) -> Result<Self> {
268        self.focus(id, true)?;
269        Ok(self)
270    }
271
272    /// Set an initial dynamic legend to be used by the view.
273    /// It can later be updated by sending the [`EzEvent::UpdateLegend`] message to the view.
274    ///
275    /// # Errors
276    /// * [`ViewError::ComponentNotFound`] if the component is not mounted
277    #[must_use]
278    pub fn initial_legend(mut self, matchers: &[Matcher]) -> Self {
279        self.dynamic_legend = Some(Legend::new("Client".into(), matchers.into()));
280        self.build_legends();
281        self
282    }
283
284    /// Remove current focus and focus the last focused component
285    ///
286    /// # Errors
287    /// * [`ViewError::ComponentNotFound`] if the component to be focused is not mounted
288    pub fn blur(&mut self) -> Result<()> {
289        if let Some(id) = self.focus_stack.current_focus().cloned()
290            && let Some(cpt) = self.components.get_mut(&id)
291            && cpt.capture_focus(false)
292        {
293            return Ok(());
294        }
295        let focus_state = self.focus_stack.blur();
296        self.handle_change_of_focus(focus_state)
297    }
298    /// Mount a new component to the view
299    ///
300    /// # Errors
301    /// * [`ViewError::ComponentAlreadyMounted`] if the component is already mounted
302    #[must_use]
303    pub fn mount(mut self, id: CID, component: BoxedComponent<CID, CA, CS, CM>) -> Self {
304        self.mount_internal(id.into(), component);
305        self
306    }
307
308    /// Unmount a component from the view
309    ///
310    /// # Errors
311    /// * [`ViewError::ComponentNotFound`] if the component is not mounted
312    pub fn umount(mut self, id: &CID) -> Result<Self> {
313        self.umount_internal(&id.clone().into())?;
314        Ok(self)
315    }
316
317    /// Check if the component is mounted
318    pub fn mounted(&self, id: &CID) -> bool {
319        self.mounted_internal(&id.clone().into())
320    }
321    /// Set the [`CustomLayout`] to be used by the view
322    ///
323    /// **Notes:** This should be called **after** you call [`Self::initial_legend`]
324    #[must_use]
325    pub fn layout(mut self, layout: CustomLayout<EzCptId<CID>>) -> View<CID, CA, CS, CM> {
326        self.layout = Some(layout);
327        self.mount_internal_dependents();
328        self
329    }
330
331    /// Query a component for an attribute
332    ///
333    /// # Errors
334    /// * [`ViewError::ComponentNotFound`] if the component is not mounted
335    pub fn query_cpt(&self, id: &EzCptId<CID>, query: Attribute) -> Result<Option<AttrValue>> {
336        match self.components.get(id) {
337            None => Err(ViewError::ComponentNotFound(format!("{id:?}")).into()),
338            Some(c) => {
339                if let Some(props) = c.props() {
340                    Ok(props.get(query))
341                } else {
342                    Ok(None)
343                }
344            }
345        }
346    }
347
348    /// Set an attribute to a component
349    ///
350    /// # Errors
351    /// * [`ViewError::ComponentNotFound`] if the component is not mounted
352    pub fn set_cpt_attr(
353        &mut self,
354        id: &EzCptId<CID>,
355        attr: Attribute,
356        value: AttrValue,
357    ) -> Result<()> {
358        if let Some(c) = self.components.get_mut(id) {
359            if let Some(props) = c.props_mut() {
360                props.set(attr, value);
361            }
362            Ok(())
363        } else {
364            Err(ViewError::ComponentNotFound(format!("{id:?}")).into())
365        }
366    }
367}
368
369// Library interface
370impl<CID, CA, CS, CM> View<CID, CA, CS, CM>
371where
372    CID: EzCptIds,
373    CA: EzArgs,
374    CS: EzState,
375    CM: EzMsg,
376{
377    /// Draw the view in its current state
378    pub(crate) fn draw(&mut self, frame: &mut Frame) -> Result<()> {
379        if let Some(layout) = self.layout.clone() {
380            let mut callback = |(cid, area, buf): (EzCptId<CID>, Rect, &mut Buffer)| {
381                self.draw_component(&cid, area, buf);
382            };
383            layout.draw(frame.area(), frame.buffer_mut(), &mut callback);
384            Ok(())
385        } else {
386            Err(ViewError::NoLayout.into())
387        }
388    }
389
390    pub(crate) fn init_cpts(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
391        self.components
392            .iter_mut()
393            .flat_map(|(_, cpt)| cpt.init(state))
394            .collect()
395    }
396
397    /// Forward the tick calls to all components
398    pub(crate) fn tick_components(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
399        self.components
400            .iter_mut()
401            .flat_map(|(_, cpt)| cpt.tick(state))
402            .collect()
403    }
404
405    /// Forward an event to a component
406    pub(crate) fn forward_event_to_component(
407        &mut self,
408        id: &EzCptId<CID>,
409        event: EzEvent<CID, CM>,
410        state: &mut State<CS>,
411    ) -> Vec<EzEvent<CID, CM>> {
412        match self.components.get_mut(id) {
413            None => {
414                warn!("Forwarding to missing component {:?}", id);
415                vec![]
416            }
417            Some(c) => c.on_event(event, state),
418        }
419    }
420
421    /// Forward an event to the component with focus
422    pub(crate) fn forward_event_to_focused_component(
423        &mut self,
424        event: EzEvent<CID, CM>,
425        state: &mut State<CS>,
426    ) -> Vec<EzEvent<CID, CM>> {
427        if let Some(id) = self.focus_stack.current_focus().cloned() {
428            self.forward_event_to_component(&id, event, state)
429        } else {
430            vec![]
431        }
432    }
433
434    pub(crate) fn mount_internal(
435        &mut self,
436        ez_id: EzCptId<CID>,
437        component: BoxedComponent<CID, CA, CS, CM>,
438    ) {
439        if !self.mounted_internal(&ez_id) {
440            self.components.insert(ez_id, component);
441        }
442    }
443
444    pub(crate) fn umount_internal(&mut self, ez_id: &EzCptId<CID>) -> Result<()> {
445        if !self.mounted_internal(ez_id) {
446            return Err(ViewError::ComponentNotFound(format!("{ez_id:?}")).into());
447        }
448        let focus_changed = self.focus_stack.forget(ez_id);
449        self.handle_change_of_focus(focus_changed)?;
450        self.components.remove(ez_id);
451        Ok(())
452    }
453
454    pub(crate) fn mounted_internal(&self, ez_id: &EzCptId<CID>) -> bool {
455        self.components.contains_key(ez_id)
456    }
457
458    pub(crate) fn mount_internal_dependents(&mut self) {
459        if let Some(layout) = self.layout.clone() {
460            if layout.contains(&EzCptId::Legends) {
461                info!("Mounting GlobalLegend component");
462                self.build_legends();
463            }
464
465            if layout.contains(&EzCptId::TickIndicator) {
466                info!("Mounting TickIndicator component");
467                self.mount_internal(
468                    EzCptId::TickIndicator,
469                    Box::new(TickIndicatorCpt::with_palettes(
470                        &self.theme.main_palette,
471                        &self.theme.accent_palette,
472                    )),
473                );
474            } else {
475                self.umount_internal(&EzCptId::TickIndicator).ok();
476            }
477
478            if layout.contains(&EzCptId::FrameIndicator) {
479                info!("Mounting FrameIndicator component");
480                self.mount_internal(
481                    EzCptId::FrameIndicator,
482                    Box::new(RenderIndicatorCpt::with_palettes(
483                        &self.theme.accent_palette,
484                        &self.theme.main_palette,
485                    )),
486                );
487            } else {
488                self.umount_internal(&EzCptId::FrameIndicator).ok();
489            }
490
491            if layout.contains(&EzCptId::StateDebugger) {
492                info!("Mounting StateDebugger component");
493                self.mount_internal(EzCptId::StateDebugger, Box::new(AppStateCpt::default()));
494            } else {
495                self.umount_internal(&EzCptId::StateDebugger).ok();
496            }
497
498            if layout.contains(&EzCptId::LogsViewer) {
499                info!("Mounting LogsViewer component");
500                self.mount_internal(EzCptId::LogsViewer, Box::new(LogsViewer::new()));
501            } else {
502                self.umount_internal(&EzCptId::LogsViewer).ok();
503            }
504        }
505    }
506}
507
508// Private functions
509impl<CID, CA, CS, CM> View<CID, CA, CS, CM>
510where
511    CID: EzCptIds,
512    CA: EzArgs,
513    CS: EzState,
514    CM: EzMsg,
515{
516    fn draw_component(&mut self, cid: &EzCptId<CID>, area: Rect, buf: &mut Buffer) {
517        if let Some(cpt) = self.components.get_mut(cid) {
518            cpt.draw(area, buf, &self.theme);
519        }
520    }
521
522    fn handle_change_of_focus(&mut self, infos: FocusChangeInfo<CID>) -> Result<()> {
523        match infos {
524            FocusChangeInfo::Unchanged => {}
525            FocusChangeInfo::Changed { old, new } => {
526                if let Some(old_id) = old {
527                    if let Some(c) = self.components.get_mut(&old_id) {
528                        if let Some(props) = c.props_mut() {
529                            props.set(Attribute::Focus, AttrValue::Flag(false));
530                        }
531                    } else {
532                        return Err(ViewError::ComponentNotFound(format!("{old_id:?}")).into());
533                    }
534                }
535
536                if let Some(new_id) = new {
537                    if let Some(c) = self.components.get_mut(&new_id) {
538                        if let Some(props) = c.props_mut() {
539                            props.set(Attribute::Focus, AttrValue::Flag(true));
540                        }
541                    } else {
542                        return Err(ViewError::ComponentNotFound(format!("{new_id:?}")).into());
543                    }
544                }
545            }
546        }
547        Ok(())
548    }
549    pub(crate) fn set_legend(&mut self, legend: Option<Legend>) {
550        self.dynamic_legend = legend;
551        self.build_legends();
552    }
553
554    fn build_legends(&mut self) {
555        let global = (*GLOBAL).clone();
556        let legends = match self.dynamic_legend.clone() {
557            None => vec![global],
558            Some(dynamic) => vec![global, dynamic],
559        };
560
561        self.umount_internal(&EzCptId::Legends).ok();
562        self.mount_internal(EzCptId::Legends, Box::new(Legends::new(legends)));
563    }
564}
565
566impl From<ViewError> for Error {
567    fn from(value: ViewError) -> Self {
568        Error::View(value)
569    }
570}