Skip to main content

clankerdiff_ratatui/markdown_review/
component.rs

1use super::{MarkdownReviewEvent, MarkdownReviewState, MarkdownReviewWidget};
2use crate::{
3    InputOutcome, KeyBinding, KeyEvent, MarkdownReviewCommand, NavigationPane, ReviewInput,
4    ReviewOptions, ThemeChoice, default_markdown_keybindings, keybindings::markdown_command_label,
5};
6use clankerdiff_core::ReviewCapabilities;
7use clankerdiff_markdown::{MarkdownDocument, MarkdownReviewError};
8use clankerdiff_theme::ReviewTheme;
9#[cfg(feature = "crossterm-backend")]
10use crossterm::event::Event;
11use ratatui::{Frame, layout::Rect};
12use std::sync::Arc;
13
14#[derive(Debug)]
15pub struct MarkdownReview {
16    state: MarkdownReviewState,
17    widget: MarkdownReviewWidget,
18}
19
20impl MarkdownReview {
21    #[must_use]
22    pub fn builder() -> MarkdownReviewBuilder {
23        MarkdownReviewBuilder::default()
24    }
25
26    pub fn render(&mut self, frame: &mut Frame<'_>, area: Rect) {
27        frame.render_stateful_widget(self.widget.clone(), area, &mut self.state);
28        if let Some(position) = self.state.cursor_position() {
29            frame.set_cursor_position(position);
30        }
31    }
32
33    pub fn handle_input(
34        &mut self,
35        input: impl Into<ReviewInput>,
36    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
37        self.state.handle_input(input.into())
38    }
39
40    pub fn handle_command(
41        &mut self,
42        command: impl Into<MarkdownReviewCommand>,
43    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
44        self.state.handle_command(command)
45    }
46
47    #[cfg(feature = "crossterm-backend")]
48    pub fn handle_crossterm_event(
49        &mut self,
50        event: &Event,
51    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
52        super::handle_crossterm_event(&mut self.state, event.clone())
53    }
54
55    #[must_use]
56    pub const fn state(&self) -> &MarkdownReviewState {
57        &self.state
58    }
59
60    pub fn state_mut(&mut self) -> &mut MarkdownReviewState {
61        self.state.mark_dirty();
62        &mut self.state
63    }
64
65    #[must_use]
66    pub const fn is_dirty(&self) -> bool {
67        self.state.is_dirty()
68    }
69
70    pub fn mark_dirty(&mut self) {
71        self.state.mark_dirty();
72    }
73
74    pub fn set_document(&mut self, document: impl Into<Arc<MarkdownDocument>>) {
75        self.state.set_document(document.into());
76    }
77
78    pub fn set_theme(&mut self, theme: ReviewTheme) {
79        self.state.set_theme(theme);
80    }
81}
82
83#[derive(Debug)]
84pub struct MarkdownReviewBuilder<T = ()> {
85    document: T,
86    theme: ReviewTheme,
87    theme_choices: Arc<[ThemeChoice]>,
88    options: ReviewOptions,
89    capabilities: ReviewCapabilities,
90    keybindings: Vec<KeyBinding<MarkdownReviewCommand>>,
91    widget: MarkdownReviewWidget,
92}
93
94impl Default for MarkdownReviewBuilder {
95    fn default() -> Self {
96        Self {
97            document: (),
98            theme: ReviewTheme::default(),
99            theme_choices: Arc::from([]),
100            options: ReviewOptions::default(),
101            capabilities: ReviewCapabilities::default(),
102            keybindings: default_markdown_keybindings(),
103            widget: MarkdownReviewWidget::new(),
104        }
105    }
106}
107
108impl<T> MarkdownReviewBuilder<T> {
109    #[must_use]
110    pub fn markdown(self, source: impl AsRef<str>) -> MarkdownReviewBuilder<Arc<MarkdownDocument>> {
111        self.document(MarkdownDocument::parse(source.as_ref()))
112    }
113
114    #[must_use]
115    pub fn document(
116        self,
117        document: impl Into<Arc<MarkdownDocument>>,
118    ) -> MarkdownReviewBuilder<Arc<MarkdownDocument>> {
119        MarkdownReviewBuilder {
120            document: document.into(),
121            theme: self.theme,
122            theme_choices: self.theme_choices,
123            options: self.options,
124            capabilities: self.capabilities,
125            keybindings: self.keybindings,
126            widget: self.widget,
127        }
128    }
129
130    #[must_use]
131    pub fn embedded(self) -> Self {
132        self.borders(false)
133            .footer(false)
134            .navigation(NavigationPane::Hidden)
135    }
136
137    #[must_use]
138    pub fn theme(mut self, theme: ReviewTheme) -> Self {
139        self.theme = theme;
140        self
141    }
142
143    #[must_use]
144    pub fn theme_choices(mut self, themes: impl Into<Arc<[ThemeChoice]>>) -> Self {
145        self.theme_choices = themes.into();
146        self
147    }
148
149    #[must_use]
150    pub fn options(mut self, options: ReviewOptions) -> Self {
151        self.options = options;
152        self
153    }
154
155    #[must_use]
156    pub fn footer(mut self, visible: bool) -> Self {
157        self.options.footer = visible;
158        self
159    }
160
161    #[must_use]
162    pub fn navigation(mut self, navigation: NavigationPane) -> Self {
163        self.options.navigation = navigation;
164        self
165    }
166
167    #[must_use]
168    pub fn borders(mut self, visible: bool) -> Self {
169        self.widget = self.widget.borders(visible);
170        self
171    }
172
173    #[must_use]
174    pub fn title(mut self, title: impl Into<String>) -> Self {
175        self.widget = self.widget.title(title);
176        self
177    }
178
179    #[must_use]
180    pub fn capabilities(mut self, capabilities: ReviewCapabilities) -> Self {
181        self.capabilities = capabilities;
182        self
183    }
184
185    #[must_use]
186    pub fn bind(self, key: impl Into<KeyEvent>, command: impl Into<MarkdownReviewCommand>) -> Self {
187        let command = command.into();
188        self.binding(KeyBinding::new(
189            key.into(),
190            command,
191            markdown_command_label(command),
192        ))
193    }
194
195    #[must_use]
196    pub fn binding(mut self, binding: KeyBinding<MarkdownReviewCommand>) -> Self {
197        self.keybindings.push(binding);
198        self
199    }
200
201    #[must_use]
202    pub fn keybindings(
203        mut self,
204        bindings: impl Into<Vec<KeyBinding<MarkdownReviewCommand>>>,
205    ) -> Self {
206        self.keybindings = bindings.into();
207        self
208    }
209}
210
211impl MarkdownReviewBuilder<Arc<MarkdownDocument>> {
212    #[must_use]
213    pub fn build(self) -> MarkdownReview {
214        let mut state = MarkdownReviewState::with_theme(self.document, self.theme);
215        state.set_options(self.options);
216        state.set_capabilities(self.capabilities);
217        state.set_keybindings(self.keybindings);
218        state.set_theme_choices(self.theme_choices);
219        MarkdownReview {
220            state,
221            widget: self.widget,
222        }
223    }
224}