multiverse/widgets/room_view/details/
mod.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
2use ratatui::{prelude::*, widgets::*};
3use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
4use style::palette::tailwind;
5
6use self::{events::EventsView, linked_chunk::LinkedChunkView, read_receipts::ReadReceipts};
7use super::DetailsState;
8use crate::widgets::recovery::ShouldExit;
9
10mod events;
11mod linked_chunk;
12mod read_receipts;
13
14#[derive(Clone, Copy, Default, Display, FromRepr, EnumIter)]
15enum SelectedTab {
16    /// Show the raw event sources of the timeline.
17    #[default]
18    Events,
19
20    /// Show details about read receipts of the room.
21    ReadReceipts,
22
23    /// Show the linked chunks that are used to display the timeline.
24    LinkedChunks,
25}
26
27impl SelectedTab {
28    /// Get the previous tab, if there is no previous tab return the current
29    /// tab.
30    fn previous(self) -> Self {
31        let current_index: usize = self as usize;
32        let previous_index = current_index.saturating_sub(1);
33        Self::from_repr(previous_index).unwrap_or(self)
34    }
35
36    /// Get the next tab, if there is no next tab return the current tab.
37    fn next(self) -> Self {
38        let current_index = self as usize;
39        let next_index = current_index.saturating_add(1);
40        Self::from_repr(next_index).unwrap_or(self)
41    }
42
43    /// Cycle to the next tab, if we're at the last tab we return the first and
44    /// default tab.
45    fn cycle_next(self) -> Self {
46        let current_index = self as usize;
47        let next_index = current_index.saturating_add(1);
48        Self::from_repr(next_index).unwrap_or_default()
49    }
50
51    /// Cycle to the previous tab, if we're at the first tab we return the last
52    /// tab.
53    fn cycle_prev(self) -> Self {
54        let current_index = self as usize;
55
56        if current_index == 0 {
57            Self::iter().next_back().expect("We should always have a last element in our enum")
58        } else {
59            let previous_index = current_index.saturating_sub(1);
60            Self::from_repr(previous_index).unwrap_or(self)
61        }
62    }
63
64    /// Return tab's name as a styled `Line`
65    fn title(self) -> Line<'static> {
66        format!("  {self}  ").fg(tailwind::SLATE.c200).bg(self.palette().c900).into()
67    }
68
69    const fn palette(&self) -> tailwind::Palette {
70        match self {
71            Self::Events => tailwind::BLUE,
72            Self::ReadReceipts => tailwind::EMERALD,
73            Self::LinkedChunks => tailwind::INDIGO,
74        }
75    }
76}
77
78impl<'a> StatefulWidget for &'a SelectedTab {
79    type State = DetailsState<'a>;
80
81    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
82    where
83        Self: Sized,
84    {
85        match self {
86            SelectedTab::Events => {
87                EventsView::new(state.selected_room).render(area, buf);
88            }
89            SelectedTab::ReadReceipts => {
90                ReadReceipts::new(state).render(area, buf);
91            }
92            SelectedTab::LinkedChunks => {
93                LinkedChunkView::new(state.selected_room).render(area, buf)
94            }
95        }
96    }
97}
98
99#[derive(Default)]
100pub struct RoomDetails {
101    selected_tab: SelectedTab,
102}
103
104impl RoomDetails {
105    /// Create a new [`RoomDetails`] struct with the [`SelectedTab::Events`] as
106    /// the selected tab.
107    pub fn with_events_as_selected() -> Self {
108        Self { selected_tab: SelectedTab::Events }
109    }
110
111    /// Create a new [`RoomDetails`] struct with the
112    /// [`SelectedTab::ReadReceipts`] as the selected tab.
113    pub fn with_receipts_as_selected() -> Self {
114        Self { selected_tab: SelectedTab::ReadReceipts }
115    }
116
117    /// Create a new [`RoomDetails`] struct with the
118    /// [`SelectedTab::LinkedChunks`] as the selected tab.
119    pub fn with_chunks_as_selected() -> Self {
120        Self { selected_tab: SelectedTab::LinkedChunks }
121    }
122
123    pub fn handle_key_press(&mut self, event: KeyEvent) -> ShouldExit {
124        use KeyCode::*;
125
126        if event.kind != KeyEventKind::Press {
127            return ShouldExit::No;
128        }
129
130        match event.code {
131            Char('l') | Right => {
132                self.next_tab();
133                ShouldExit::No
134            }
135
136            Tab => {
137                self.cycle_next_tab();
138                ShouldExit::No
139            }
140
141            BackTab => {
142                self.cycle_prev_tab();
143                ShouldExit::No
144            }
145
146            Char('h') | Left => {
147                self.previous_tab();
148                ShouldExit::No
149            }
150
151            Char('q') | Esc => ShouldExit::Yes,
152
153            _ => ShouldExit::No,
154        }
155    }
156
157    fn cycle_next_tab(&mut self) {
158        self.selected_tab = self.selected_tab.cycle_next();
159    }
160
161    fn cycle_prev_tab(&mut self) {
162        self.selected_tab = self.selected_tab.cycle_prev();
163    }
164
165    fn next_tab(&mut self) {
166        self.selected_tab = self.selected_tab.next();
167    }
168
169    fn previous_tab(&mut self) {
170        self.selected_tab = self.selected_tab.previous();
171    }
172}
173
174impl<'a> StatefulWidget for &'a mut RoomDetails {
175    type State = DetailsState<'a>;
176
177    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
178    where
179        Self: Sized,
180    {
181        use Constraint::{Length, Min};
182        let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
183        let [header_area, inner_area, footer_area] = vertical.areas(area);
184
185        let horizontal = Layout::horizontal([Min(0), Length(20)]);
186        let [tab_title_area, title_area] = horizontal.areas(header_area);
187
188        "Room details".bold().render(title_area, buf);
189
190        Block::bordered()
191            .border_set(symbols::border::PROPORTIONAL_TALL)
192            .padding(Padding::horizontal(1))
193            .border_style(tailwind::BLUE.c700)
194            .render(inner_area, buf);
195
196        let titles = SelectedTab::iter().map(SelectedTab::title);
197        let highlight_style = (Color::default(), self.selected_tab.palette().c700);
198        let selected_tab_index = self.selected_tab as usize;
199
200        let tabs_area = inner_area.inner(Margin::new(1, 1));
201
202        Tabs::new(titles)
203            .highlight_style(highlight_style)
204            .select(selected_tab_index)
205            .padding("", "")
206            .divider(" ")
207            .render(tab_title_area, buf);
208
209        self.selected_tab.render(tabs_area, buf, state);
210
211        Line::raw("◄ ► to change tab | Press q to exit the details screen")
212            .centered()
213            .render(footer_area, buf);
214    }
215}