multiverse/widgets/room_view/details/
mod.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
2use matrix_sdk_ui::room_list_service::Room;
3use ratatui::{prelude::*, widgets::*};
4use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
5use style::palette::tailwind;
6
7use self::{events::EventsView, linked_chunk::LinkedChunkView, read_receipts::ReadReceipts};
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 = Option<&'a Room>;
80
81    fn render(self, area: Rect, buf: &mut Buffer, room: &mut Self::State)
82    where
83        Self: Sized,
84    {
85        match self {
86            SelectedTab::Events => {
87                EventsView::new(room.as_deref()).render(area, buf);
88            }
89            SelectedTab::ReadReceipts => {
90                ReadReceipts::new(room.as_deref()).render(area, buf);
91            }
92            SelectedTab::LinkedChunks => LinkedChunkView::new(room.as_deref()).render(area, buf),
93        }
94    }
95}
96
97#[derive(Default)]
98pub struct RoomDetails {
99    selected_tab: SelectedTab,
100}
101
102impl RoomDetails {
103    /// Create a new [`RoomDetails`] struct with the [`SelectedTab::Events`] as
104    /// the selected tab.
105    pub fn with_events_as_selected() -> Self {
106        Self { selected_tab: SelectedTab::Events }
107    }
108
109    /// Create a new [`RoomDetails`] struct with the
110    /// [`SelectedTab::ReadReceipts`] as the selected tab.
111    pub fn with_receipts_as_selected() -> Self {
112        Self { selected_tab: SelectedTab::ReadReceipts }
113    }
114
115    /// Create a new [`RoomDetails`] struct with the
116    /// [`SelectedTab::LinkedChunks`] as the selected tab.
117    pub fn with_chunks_as_selected() -> Self {
118        Self { selected_tab: SelectedTab::LinkedChunks }
119    }
120
121    pub fn handle_key_press(&mut self, event: KeyEvent) -> ShouldExit {
122        use KeyCode::*;
123
124        if event.kind != KeyEventKind::Press {
125            return ShouldExit::No;
126        }
127
128        match event.code {
129            Char('l') | Right => {
130                self.next_tab();
131                ShouldExit::No
132            }
133
134            Tab => {
135                self.cycle_next_tab();
136                ShouldExit::No
137            }
138
139            BackTab => {
140                self.cycle_prev_tab();
141                ShouldExit::No
142            }
143
144            Char('h') | Left => {
145                self.previous_tab();
146                ShouldExit::No
147            }
148
149            Char('q') | Esc => ShouldExit::Yes,
150
151            _ => ShouldExit::No,
152        }
153    }
154
155    fn cycle_next_tab(&mut self) {
156        self.selected_tab = self.selected_tab.cycle_next();
157    }
158
159    fn cycle_prev_tab(&mut self) {
160        self.selected_tab = self.selected_tab.cycle_prev();
161    }
162
163    fn next_tab(&mut self) {
164        self.selected_tab = self.selected_tab.next();
165    }
166
167    fn previous_tab(&mut self) {
168        self.selected_tab = self.selected_tab.previous();
169    }
170}
171
172impl<'a> StatefulWidget for &'a mut RoomDetails {
173    type State = Option<&'a Room>;
174
175    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
176    where
177        Self: Sized,
178    {
179        use Constraint::{Length, Min};
180        let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
181        let [header_area, inner_area, footer_area] = vertical.areas(area);
182
183        let horizontal = Layout::horizontal([Min(0), Length(20)]);
184        let [tab_title_area, title_area] = horizontal.areas(header_area);
185
186        "Room details".bold().render(title_area, buf);
187
188        Block::bordered()
189            .border_set(symbols::border::PROPORTIONAL_TALL)
190            .padding(Padding::horizontal(1))
191            .border_style(tailwind::BLUE.c700)
192            .render(inner_area, buf);
193
194        let titles = SelectedTab::iter().map(SelectedTab::title);
195        let highlight_style = (Color::default(), self.selected_tab.palette().c700);
196        let selected_tab_index = self.selected_tab as usize;
197
198        let tabs_area = inner_area.inner(Margin::new(1, 1));
199
200        Tabs::new(titles)
201            .highlight_style(highlight_style)
202            .select(selected_tab_index)
203            .padding("", "")
204            .divider(" ")
205            .render(tab_title_area, buf);
206
207        self.selected_tab.render(tabs_area, buf, state);
208
209        Line::raw("◄ ► to change tab | Press q to exit the details screen")
210            .centered()
211            .render(footer_area, buf);
212    }
213}