Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_tui/src/lib_tui/draw/tbox.rs

15.8 KiB, 54 runs

created by r1870400018:1187, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::lib_tui::{
2 draw::{
3 canvas::{
4 Canvas,
5 CanvasConfig,
6 },
7 scrollbars::{
8 ScrollBar,
9 ScrollBars,
10 ScrollBarsConfig,
11 TextViewDim,
12 },
13 },
14 render::{
15 CursorStyle,
16 Drawable,
17 Drawer,
18 Renderer,
19 Sink,
20 When,
21 },
22 style::Style,
23 text::{
24 nav::PositionCursor,
25 typ::{
26 HighlightType,
27 TextType,
28 TextViewType,
29 },
30 view::TextView,
31 },
32};
33
34use oxedyne_fe2o3_core::prelude::*;
35use oxedyne_fe2o3_geom::{
36 dim::{
37 Coord,
38 Dim,
39 },
40 rect::{
41 AbsRect,
42 AbsSize,
43 },
44};
45use oxedyne_fe2o3_text::{
46 access::AccessibleText,
47 lines::TextLines,
48};
49
50
51#[derive(Clone, Debug, Default)]
52pub struct TextBoxConfig {
53 pub cursor_style: Option<CursorStyle>,
54 pub cursor_position: PositionCursor,
55 pub scrollbars: Option<ScrollBarsConfig>,
56 pub empty_line: Option<(Style, String)>,
57 pub highlight_styles: Vec<Style>,
58}
59
60impl TextBoxConfig {
61 pub fn cursor_is_visible(&self) -> bool {
62 self.cursor_style.is_some()
63 }
64}
65
66/// The text box adopts the tab canvas, or if there are no tabs, the window canvas.
67#[derive(Clone, Debug, Default)]
68pub struct TextBoxState {
69 pub canvas: CanvasConfig,
70}
71
72#[derive(Clone, Debug, Default)]
73pub struct TextBox {
74 pub cfg: TextBoxConfig,
75 pub state: TextBoxState,
76 pub tview: TextView,
77 pub view: AbsRect, // View in terminal.
78}
79
80impl Drawable for TextBox {
81 fn render<S: Sink, R: Renderer<S>>(
82 &mut self,
83 drawer: &mut Drawer<S, R>,
84 when: When,
85 )
86 -> Outcome<()>
87 {
88 // Account for scrollbars and render them if necessary.
89 if let Some(cfg) = &self.cfg.scrollbars {
90 let (mut scrollbars, thickness) = self.scrollbars(
91 cfg.clone(),
92 self.view,
93 );
94 res!(scrollbars.render(drawer, When::Later));
95 self.view.size -= thickness;
96 }
97
98 res!(self.tview.update_view(
99 &self.view,
100 &self.cfg.cursor_position,
101 ));
102
103 // Full redraw needed, paint the canvas.
104 let mut canvas = Canvas::new(
105 self.state.canvas.clone(),
106 self.view,
107 );
108 res!(canvas.render(drawer, When::Later));
109
110 res!(self.state.canvas.style.render(drawer, When::Later));
111
112 //// We want to borrow the TextLines but then need mutable access to the TextBox to render.
113 //// The Rust borrow rules during compilation don't allow a mutable borrow while an immutable
114 //// one exists on all or part of TextBox. So we use the Indiana Jones trick of taking it
115 //// out and replacing it during runtime once we have finished, as a method of interior
116 //// mutability.
117 //// Credit:
118 //// https://rust-unofficial.github.io/too-many-lists/first-push.html?highlight=indiana#push
119 ////
120 let atext = std::mem::replace(&mut self.tview.atext, AccessibleText::default());
121
122 // Access the text, and render it.
123 match atext {
124 AccessibleText::ThreadShared(ref locked) => {
125 let text_lines = lock_write!(locked);
126 res!(self.render_text_view(drawer, &text_lines));
127 }
128 AccessibleText::Shared(ref locked) => {
129 let text_lines = lock_write!(locked);
130 res!(self.render_text_view(drawer, &text_lines));
131 }
132 }
133
134 // Replace the text back into the TextBox.
135 self.tview.atext = atext;
136
137 res!(drawer.rend.reset_style(When::Later));
138
139 if when == When::Now {
140 res!(drawer.rend.flush());
141 }
142 Ok(())
143 }
144}
145
146impl TextBox {
147
148 pub fn new(
149 cfg: TextBoxConfig,
150 tview: TextView,
151 )
152 -> Outcome<Self>
153 {
154 if cfg.highlight_styles.len() == 0 {
155 return Err(err!(
156 "TextBoxConfig highlight_styles vector is empty.";
157 Input, Invalid, Missing));
158 }
159 Ok(Self {
160 cfg,
161 tview,
162 ..Default::default()
163 })
164 }
165
166 pub fn is_editable(&self) -> bool {
167 match &self.tview.vtyp {
168 TextViewType::Editable(..) => true,
169 _ => false,
170 }
171 }
172
173 /// Pan the text view up, if possible. This is infallible with the use of saturating
174 /// arithmetic.
175 pub fn pan_up(&mut self, by: Dim) {
176 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
177 let rel_cursor = *cursor - self.tview.text_view.top_left;
178 self.tview.text_view.dec_y(by);
179 *cursor = self.tview.text_view.top_left + rel_cursor;
180 }
181 }
182
183 /// Pan the text view down, if possible. This is infallible with the use of saturating
184 /// arithmetic.
185 pub fn pan_down(&mut self, by: Dim) {
186 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
187 let dy = by.min(self.tview.extent.y
188 + 1
189 - self.tview.text_view.top_left.y
190 - self.tview.text_view.size.y
191 );
192 let rel_cursor = *cursor - self.tview.text_view.top_left;
193 self.tview.text_view.inc_y(dy);
194 *cursor = self.tview.text_view.top_left + rel_cursor;
195 }
196 }
197
198 /// Pan the text view left, if possible. This is infallible with the use of saturating
199 /// arithmetic.
200 pub fn pan_left(&mut self, by: Dim) {
201 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
202 let rel_cursor = *cursor - self.tview.text_view.top_left;
203 self.tview.text_view.dec_x(by);
204 *cursor = self.tview.text_view.top_left + rel_cursor;
205 }
206 }
207
208 /// Pan the text view right, if possible. This is infallible with the use of saturating
209 /// arithmetic.
210 pub fn pan_right(&mut self, by: Dim) {
211 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
212 let dx = by.min(self.tview.extent.x
213 + 1
214 - self.tview.text_view.top_left.x
215 - self.tview.text_view.size.x
216 );
217 let rel_cursor = *cursor - self.tview.text_view.top_left;
218 self.tview.text_view.inc_x(dx);
219 *cursor = self.tview.text_view.top_left + rel_cursor;
220 }
221 }
222
223 /// Move the cursor up one line, if possible. This is infallible with the use of saturating
224 /// arithmetic.
225 pub fn cursor_up(&mut self) -> bool {
226 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
227 if self.cfg.cursor_is_visible() && cursor.y > Dim(0) {
228 // Order important here, need to test whether cursor is at top of text view before
229 // moving it.
230 let needs_pan = cursor.y == self.tview.text_view.top();
231 if needs_pan {
232 self.tview.text_view.dec_y(Dim(1));
233 }
234 cursor.dec_y(Dim(1));
235 return needs_pan;
236 }
237 }
238 false
239 }
240
241 /// Move the cursor down one line, if possible. This is infallible with the use of saturating
242 /// arithmetic.
243 pub fn cursor_down(&mut self) -> bool {
244 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
245 if self.cfg.cursor_is_visible() && cursor.y < self.tview.extent.y {
246 cursor.inc_y(Dim(1));
247 let needs_pan = cursor.y == self.tview.text_view.bottom();
248 if needs_pan {
249 self.tview.text_view.inc_y(Dim(1));
250 }
251 return needs_pan;
252 }
253 }
254 false
255 }
256
257 /// Move the cursor left one character, if possible. This is method is infallible.
258 pub fn cursor_left(&mut self) -> bool {
259 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
260 if self.cfg.cursor_is_visible() && cursor.x > Dim(0) {
261 // Order important here, need to test whether cursor is at the left of the text view
262 // before moving it.
263 let needs_pan = cursor.x == self.tview.text_view.left();
264 if needs_pan {
265 self.tview.text_view.dec_x(Dim(1));
266 }
267 cursor.dec_x(Dim(1));
268 return needs_pan;
269 }
270 }
271 false
272 }
273
274 /// Move the cursor right one character, if possible. This is method is infallible.
275 pub fn cursor_right(&mut self) -> bool {
276 if let Some(cursor) = self.tview.vtyp.get_cursor_mut() {
277 if self.cfg.cursor_is_visible() && cursor.x < self.tview.extent.x {
278 cursor.inc_x(Dim(1));
279 let needs_pan = cursor.x == self.tview.text_view.right();
280 if needs_pan {
281 self.tview.text_view.inc_x(Dim(1));
282 }
283 return needs_pan;
284 }
285 }
286 false
287 }
288
289 /// Returns the `ScrollBars` and scrollbar widths.
290 pub fn scrollbars(
291 &self,
292 cfg: ScrollBarsConfig,
293 term_view: AbsRect,
294 )
295 -> (ScrollBars, AbsSize)
296 {
297 let (x, y, mut w, mut h) = term_view.tup();
298 let (start_x, start_y) = self.tview.text_view.top_left.tup();
299 let (extent_x, extent_y) = self.tview.extent.tup();
300 if cfg.always {
301 w = w - 1;
302 h = h - 1;
303 (
304 ScrollBars {
305 cfg,
306 x: Some(ScrollBar {
307 top_left: Coord::new((x, y + h)),
308 scroll_len: w,
309 tview: TextViewDim::new(
310 start_x,
311 w,
312 extent_x - start_x - w,
313 ),
314 }),
315 y: Some(ScrollBar {
316 top_left: Coord::new((x + w, y)),
317 scroll_len: h,
318 tview: TextViewDim::new(
319 start_y,
320 h,
321 extent_y - start_y - h,
322 ),
323 }),
324 },
325 AbsSize::from((Dim(1), Dim(1))),
326 )
327 } else {
328 let mut x_scrollbar = false;
329 let mut y_scrollbar = false;
330 let (wext, hext) = self.tview.extent.tup();
331 if wext > w {
332 h = h - 1;
333 x_scrollbar = true;
334 }
335 if hext > h {
336 w = w - 1;
337 y_scrollbar = true;
338 }
339 (
340 ScrollBars {
341 cfg,
342 x: if x_scrollbar {
343 Some(ScrollBar {
344 top_left: Coord::new((x, y + h + 1)),
345 scroll_len: w,
346 tview: TextViewDim::new(
347 start_x,
348 w,
349 extent_x - start_x - w,
350 ),
351 })
352 } else {
353 None
354 },
355 y: if y_scrollbar {
356 Some(ScrollBar {
357 top_left: Coord::new((x + w + 1, y)),
358 scroll_len: h,
359 tview: TextViewDim::new(
360 start_y,
361 h,
362 extent_y - start_y - h,
363 ),
364 })
365 } else {
366 None
367 },
368 },
369 AbsSize::from((
370 Dim(if y_scrollbar { 1 } else { 0 }),
371 Dim(if x_scrollbar { 1 } else { 0 }),
372 )),
373 )
374 }
375 }
376
377 /// Get the rectangle defining the line focus, if it is visible in the text view.
378 pub fn get_highlight(
379 &self,
380 text_lines: &TextLines<TextType, HighlightType>,
381 )
382 -> Outcome<Option<(Style, AbsRect)>>
383 {
384 if let Some(highlighter) = text_lines.get_highlighter() {
385 if let Some(highlighted) = highlighter.get_highlighted() {
386 let line_range = highlighted.get_range();
387 match self.tview.text_view.clip(line_range.to_abs_rect()) {
388 Some(mut abs_rect) => {
389 abs_rect.top_left -= self.tview.text_view.top_left;
390 let style_index = try_rem!(
391 highlighted.get_level() as usize,
392 self.cfg.highlight_styles.len(),
393 );
394 return Ok(Some(
395 (
396 self.cfg.highlight_styles[style_index].clone(),
397 abs_rect,
398 )
399 ));
400 }
401 None => {}
402 }
403 }
404 }
405 Ok(None)
406 }
407
408 pub fn render_text_view<S: Sink, R: Renderer<S>>(
409 &mut self,
410 drawer: &mut Drawer<S, R>,
411 text_lines: &TextLines<TextType, HighlightType>,
412 )
413 -> Outcome<()>
414 {
415 // Take a snapshot of the text lines within the text view.
416 let lines = TextView::extract_view(&text_lines.lines, &self.tview.text_view);
417 // Identify whether the line focus range is within view, and if so adjust its
418 // location to be relative to the view coordinates (where top left is (0,0).
419 let mut highlight_opt = res!(self.get_highlight(&text_lines));
420 let (x, y, _w, h) = self.tview.term_view.tup();
421
422 for j in 0..h.as_index() {
423 res!(drawer.rend.set_cursor(Coord::new((x, y + j)), When::Later));
424 if let Some((style, empty_line_marker)) = &mut self.cfg.empty_line {
425 if j < lines.len() {
426 res!(self.render_text_line(
427 drawer,
428 lines[j],
429 j,
430 &mut highlight_opt,
431 ));
432 } else {
433 // Print empty line marker.
434 res!(style.render(drawer, When::Later));
435 res!(drawer.rend.print(&empty_line_marker, When::Later));
436 }
437 } else {
438 // Print without empty line markers.
439 if j < lines.len() {
440 res!(self.render_text_line(
441 drawer,
442 lines[j],
443 j,
444 &mut highlight_opt,
445 ));
446 }
447 }
448 }
449 Ok(())
450 }
451
452 pub fn render_text_line<S: Sink, R: Renderer<S>>(
453 &mut self,
454 drawer: &mut Drawer<S, R>,
455 line: &str,
456 j: usize,
457 highlight_opt: &mut Option<(Style, AbsRect)>,
458 )
459 -> Outcome<()>
460 {
461 if let Some((style, abs_rect)) = highlight_opt {
462 let (xf, yf, wf, _hf) = abs_rect.tup();
463 if j == yf {
464 // Print with highlighting.
465 let (pre, rest) = line.split_at(xf.as_index());
466 let (focus, post) = rest.split_at(wf.as_index());
467 res!(drawer.rend.print(pre, When::Later));
468 res!(style.render(drawer, When::Later));
469 res!(drawer.rend.print(focus, When::Later));
470 res!(self.state.canvas.style.render(drawer, When::Later));
471 res!(drawer.rend.print(post, When::Later));
472 return Ok(());
473 }
474 }
475 // Print as normal without any highlighting.
476 res!(self.state.canvas.style.render(drawer, When::Later));
477 res!(drawer.rend.print(line, When::Later));
478 Ok(())
479 }
480}
481