clankerdiff_ratatui/markdown_review/
component.rs1use 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}