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});
35pub trait MockComponent {
43 fn draw(&mut self, area: Rect, buf: &mut Buffer, theme: &Theme);
45}
46pub trait MockProps {
48 fn props(&self) -> Option<&Props> {
50 None
51 }
52
53 fn props_mut(&mut self) -> Option<&mut Props> {
55 None
56 }
57}
58pub trait Component<CID, CA, CS, CM>: MockComponent + MockProps
62where
63 CID: EzCptIds,
64 CA: EzArgs,
65 CS: EzState,
66 CM: EzMsg,
67{
68 #[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 #[allow(unused_variables)]
81 fn init(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
82 vec![]
83 }
84
85 #[allow(unused_variables)]
87 fn tick(&mut self, state: &mut State<CS>) -> Vec<EzEvent<CID, CM>> {
88 vec![]
89 }
90
91 fn focusable(&self) -> bool {
93 false
94 }
95 fn legend(&self) -> Option<Legend> {
97 None
98 }
99
100 #[allow(unused_variables)]
106 fn capture_focus(&mut self, forward: bool) -> bool {
107 false
108 }
109}
110
111#[derive(Debug, Display, Eq, PartialEq, Clone, PartialOrd)]
113pub enum ViewError {
114 ComponentAlreadyMounted,
116 ComponentNotFound(String),
118 NoComponentToBlur,
120 NoLayout,
122 MismatchConstraintsAndCpts(String, usize, usize),
124}
125
126#[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 #[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
181impl<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 pub fn set_theme(mut self, theme: Theme) -> Self {
192 self.theme = theme;
193 self
194 }
195
196 pub fn focused(&mut self) -> Option<&EzCptId<CID>> {
198 self.focus_stack.current_focus()
199 }
200
201 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 pub fn cycle_focus(&mut self) -> Result<()> {
227 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 let current = self.focus_stack.current_focus().cloned();
237 let ids: Vec<EzCptId<CID>> = self.components.keys().cloned().collect();
238
239 if ids.is_empty() {
241 return self.blur();
242 }
243
244 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()); 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 self.blur()
261 }
262
263 pub fn initial_focus(mut self, id: &EzCptId<CID>) -> Result<Self> {
268 self.focus(id, true)?;
269 Ok(self)
270 }
271
272 #[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 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 #[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 pub fn umount(mut self, id: &CID) -> Result<Self> {
313 self.umount_internal(&id.clone().into())?;
314 Ok(self)
315 }
316
317 pub fn mounted(&self, id: &CID) -> bool {
319 self.mounted_internal(&id.clone().into())
320 }
321 #[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 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 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
369impl<CID, CA, CS, CM> View<CID, CA, CS, CM>
371where
372 CID: EzCptIds,
373 CA: EzArgs,
374 CS: EzState,
375 CM: EzMsg,
376{
377 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 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 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 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
508impl<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}