on 2158366 help sign in
helix/helix-view/src/graphics.rs
530 lines 17 kB history raw
1use bitflags::bitflags;
2use std::{
3 cmp::{max, min},
4 str::FromStr,
5};
7#[derive(Debug, Clone, Copy, PartialEq)]
8/// UNSTABLE
9pub enum CursorKind {
10 /// █
11 Block,
12 /// |
13 Bar,
14 /// _
15 Underline,
16 /// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
17 Hidden,
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct Margin {
22 pub vertical: u16,
23 pub horizontal: u16,
26/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
27/// area they are supposed to render to.
28#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
29pub struct Rect {
30 pub x: u16,
31 pub y: u16,
32 pub width: u16,
33 pub height: u16,
36impl Default for Rect {
37 fn default() -> Rect {
38 Rect {
39 x: 0,
40 y: 0,
41 width: 0,
42 height: 0,
43 }
44 }
47impl Rect {
48 /// Creates a new rect, with width and height limited to keep the area under max u16.
49 /// If clipped, aspect ratio will be preserved.
50 pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
51 let max_area = u16::max_value();
52 let (clipped_width, clipped_height) =
53 if u32::from(width) * u32::from(height) > u32::from(max_area) {
54 let aspect_ratio = f64::from(width) / f64::from(height);
55 let max_area_f = f64::from(max_area);
56 let height_f = (max_area_f / aspect_ratio).sqrt();
57 let width_f = height_f * aspect_ratio;
58 (width_f as u16, height_f as u16)
59 } else {
60 (width, height)
61 };
62 Rect {
63 x,
64 y,
65 width: clipped_width,
66 height: clipped_height,
67 }
68 }
70 #[inline]
71 pub fn area(self) -> u16 {
72 self.width * self.height
73 }
75 #[inline]
76 pub fn left(self) -> u16 {
77 self.x
78 }
80 #[inline]
81 pub fn right(self) -> u16 {
82 self.x.saturating_add(self.width)
83 }
85 #[inline]
86 pub fn top(self) -> u16 {
87 self.y
88 }
90 #[inline]
91 pub fn bottom(self) -> u16 {
92 self.y.saturating_add(self.height)
93 }
95 pub fn inner(self, margin: &Margin) -> Rect {
96 if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
97 Rect::default()
98 } else {
99 Rect {
100 x: self.x + margin.horizontal,
101 y: self.y + margin.vertical,
102 width: self.width - 2 * margin.horizontal,
103 height: self.height - 2 * margin.vertical,
104 }
105 }
106 }
108 /// Calculate the union between two [`Rect`]s.
109 pub fn union(self, other: Rect) -> Rect {
110 // Example:
111 //
112 // If `Rect` A is positioned at `(0, 0)` with a width and height of `5`,
113 // and `Rect` B is positioned at `(5, 0)` with a width and height of `2`,
114 // then this is the resulting union:
115 //
116 // x1 = min(0, 5) => x1 = 0
117 // y1 = min(0, 0) => y1 = 0
118 // x2 = max(0 + 5, 5 + 2) => x2 = 7
119 // y2 = max(0 + 5, 0 + 2) => y2 = 5
120 let x1 = min(self.x, other.x);
121 let y1 = min(self.y, other.y);
122 let x2 = max(self.x + self.width, other.x + other.width);
123 let y2 = max(self.y + self.height, other.y + other.height);
124 Rect {
125 x: x1,
126 y: y1,
127 width: x2 - x1,
128 height: y2 - y1,
129 }
130 }
132 /// Calculate the intersection between two [`Rect`]s.
133 pub fn intersection(self, other: Rect) -> Rect {
134 // Example:
135 //
136 // If `Rect` A is positioned at `(0, 0)` with a width and height of `5`,
137 // and `Rect` B is positioned at `(5, 0)` with a width and height of `2`,
138 // then this is the resulting intersection:
139 //
140 // x1 = max(0, 5) => x1 = 5
141 // y1 = max(0, 0) => y1 = 0
142 // x2 = min(0 + 5, 5 + 2) => x2 = 5
143 // y2 = min(0 + 5, 0 + 2) => y2 = 2
144 let x1 = max(self.x, other.x);
145 let y1 = max(self.y, other.y);
146 let x2 = min(self.x + self.width, other.x + other.width);
147 let y2 = min(self.y + self.height, other.y + other.height);
148 Rect {
149 x: x1,
150 y: y1,
151 width: x2 - x1,
152 height: y2 - y1,
153 }
154 }
156 pub fn intersects(self, other: Rect) -> bool {
157 self.x < other.x + other.width
158 && self.x + self.width > other.x
159 && self.y < other.y + other.height
160 && self.y + self.height > other.y
161 }
164#[derive(Debug, Clone, Copy, PartialEq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub enum Color {
167 Reset,
168 Black,
169 Red,
170 Green,
171 Yellow,
172 Blue,
173 Magenta,
174 Cyan,
175 Gray,
176 DarkGray,
177 LightRed,
178 LightGreen,
179 LightYellow,
180 LightBlue,
181 LightMagenta,
182 LightCyan,
183 White,
184 Rgb(u8, u8, u8),
185 Indexed(u8),
188#[cfg(feature = "term")]
189impl From<Color> for crossterm::style::Color {
190 fn from(color: Color) -> Self {
191 use crossterm::style::Color as CColor;
193 match color {
194 Color::Reset => CColor::Reset,
195 Color::Black => CColor::Black,
196 Color::Red => CColor::DarkRed,
197 Color::Green => CColor::DarkGreen,
198 Color::Yellow => CColor::DarkYellow,
199 Color::Blue => CColor::DarkBlue,
200 Color::Magenta => CColor::DarkMagenta,
201 Color::Cyan => CColor::DarkCyan,
202 Color::Gray => CColor::Grey,
203 Color::DarkGray => CColor::DarkGrey,
204 Color::LightRed => CColor::Red,
205 Color::LightGreen => CColor::Green,
206 Color::LightBlue => CColor::Blue,
207 Color::LightYellow => CColor::Yellow,
208 Color::LightMagenta => CColor::Magenta,
209 Color::LightCyan => CColor::Cyan,
210 Color::White => CColor::White,
211 Color::Indexed(i) => CColor::AnsiValue(i),
212 Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
213 }
214 }
217bitflags! {
218 /// Modifier changes the way a piece of text is displayed.
219 ///
220 /// They are bitflags so they can easily be composed.
221 ///
222 /// ## Examples
223 ///
224 /// ```rust
225 /// # use helix_view::graphics::Modifier;
226 ///
227 /// let m = Modifier::BOLD | Modifier::ITALIC;
228 /// ```
229 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
230 pub struct Modifier: u16 {
231 const BOLD = 0b0000_0000_0001;
232 const DIM = 0b0000_0000_0010;
233 const ITALIC = 0b0000_0000_0100;
234 const UNDERLINED = 0b0000_0000_1000;
235 const SLOW_BLINK = 0b0000_0001_0000;
236 const RAPID_BLINK = 0b0000_0010_0000;
237 const REVERSED = 0b0000_0100_0000;
238 const HIDDEN = 0b0000_1000_0000;
239 const CROSSED_OUT = 0b0001_0000_0000;
240 }
243impl FromStr for Modifier {
244 type Err = &'static str;
246 fn from_str(modifier: &str) -> Result<Self, Self::Err> {
247 match modifier {
248 "bold" => Ok(Self::BOLD),
249 "dim" => Ok(Self::DIM),
250 "italic" => Ok(Self::ITALIC),
251 "underlined" => Ok(Self::UNDERLINED),
252 "slow_blink" => Ok(Self::SLOW_BLINK),
253 "rapid_blink" => Ok(Self::RAPID_BLINK),
254 "reversed" => Ok(Self::REVERSED),
255 "hidden" => Ok(Self::HIDDEN),
256 "crossed_out" => Ok(Self::CROSSED_OUT),
257 _ => Err("Invalid modifier"),
258 }
259 }
262/// Style let you control the main characteristics of the displayed elements.
263///
264/// ```rust
265/// # use helix_view::graphics::{Color, Modifier, Style};
266/// Style::default()
267/// .fg(Color::Black)
268/// .bg(Color::Green)
269/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
270/// ```
271///
272/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
273/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
274/// just S3.
275///
276/// ```rust
277/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
278/// # use helix_tui::buffer::Buffer;
279/// let styles = [
280/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
281/// Style::default().bg(Color::Red),
282/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
283/// ];
284/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
285/// for style in &styles {
286/// buffer.get_mut(0, 0).set_style(*style);
287/// }
288/// assert_eq!(
289/// Style {
290/// fg: Some(Color::Yellow),
291/// bg: Some(Color::Red),
292/// add_modifier: Modifier::BOLD,
293/// sub_modifier: Modifier::empty(),
294/// },
295/// buffer.get(0, 0).style(),
296/// );
297/// ```
298///
299/// The default implementation returns a `Style` that does not modify anything. If you wish to
300/// reset all properties until that point use [`Style::reset`].
301///
302/// ```
303/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
304/// # use helix_tui::buffer::Buffer;
305/// let styles = [
306/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
307/// Style::reset().fg(Color::Yellow),
308/// ];
309/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
310/// for style in &styles {
311/// buffer.get_mut(0, 0).set_style(*style);
312/// }
313/// assert_eq!(
314/// Style {
315/// fg: Some(Color::Yellow),
316/// bg: Some(Color::Reset),
317/// add_modifier: Modifier::empty(),
318/// sub_modifier: Modifier::empty(),
319/// },
320/// buffer.get(0, 0).style(),
321/// );
322/// ```
323#[derive(Debug, Clone, Copy, PartialEq)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub struct Style {
326 pub fg: Option<Color>,
327 pub bg: Option<Color>,
328 pub add_modifier: Modifier,
329 pub sub_modifier: Modifier,
332impl Default for Style {
333 fn default() -> Style {
334 Style {
335 fg: None,
336 bg: None,
337 add_modifier: Modifier::empty(),
338 sub_modifier: Modifier::empty(),
339 }
340 }
343impl Style {
344 /// Returns a `Style` resetting all properties.
345 pub fn reset() -> Style {
346 Style {
347 fg: Some(Color::Reset),
348 bg: Some(Color::Reset),
349 add_modifier: Modifier::empty(),
350 sub_modifier: Modifier::all(),
351 }
352 }
354 /// Changes the foreground color.
355 ///
356 /// ## Examples
357 ///
358 /// ```rust
359 /// # use helix_view::graphics::{Color, Style};
360 /// let style = Style::default().fg(Color::Blue);
361 /// let diff = Style::default().fg(Color::Red);
362 /// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
363 /// ```
364 pub fn fg(mut self, color: Color) -> Style {
365 self.fg = Some(color);
366 self
367 }
369 /// Changes the background color.
370 ///
371 /// ## Examples
372 ///
373 /// ```rust
374 /// # use helix_view::graphics::{Color, Style};
375 /// let style = Style::default().bg(Color::Blue);
376 /// let diff = Style::default().bg(Color::Red);
377 /// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
378 /// ```
379 pub fn bg(mut self, color: Color) -> Style {
380 self.bg = Some(color);
381 self
382 }
384 /// Changes the text emphasis.
385 ///
386 /// When applied, it adds the given modifier to the `Style` modifiers.
387 ///
388 /// ## Examples
389 ///
390 /// ```rust
391 /// # use helix_view::graphics::{Color, Modifier, Style};
392 /// let style = Style::default().add_modifier(Modifier::BOLD);
393 /// let diff = Style::default().add_modifier(Modifier::ITALIC);
394 /// let patched = style.patch(diff);
395 /// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
396 /// assert_eq!(patched.sub_modifier, Modifier::empty());
397 /// ```
398 pub fn add_modifier(mut self, modifier: Modifier) -> Style {
399 self.sub_modifier.remove(modifier);
400 self.add_modifier.insert(modifier);
401 self
402 }
404 /// Changes the text emphasis.
405 ///
406 /// When applied, it removes the given modifier from the `Style` modifiers.
407 ///
408 /// ## Examples
409 ///
410 /// ```rust
411 /// # use helix_view::graphics::{Color, Modifier, Style};
412 /// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
413 /// let diff = Style::default().remove_modifier(Modifier::ITALIC);
414 /// let patched = style.patch(diff);
415 /// assert_eq!(patched.add_modifier, Modifier::BOLD);
416 /// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
417 /// ```
418 pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
419 self.add_modifier.remove(modifier);
420 self.sub_modifier.insert(modifier);
421 self
422 }
424 /// Results in a combined style that is equivalent to applying the two individual styles to
425 /// a style one after the other.
426 ///
427 /// ## Examples
428 /// ```
429 /// # use helix_view::graphics::{Color, Modifier, Style};
430 /// let style_1 = Style::default().fg(Color::Yellow);
431 /// let style_2 = Style::default().bg(Color::Red);
432 /// let combined = style_1.patch(style_2);
433 /// assert_eq!(
434 /// Style::default().patch(style_1).patch(style_2),
435 /// Style::default().patch(combined));
436 /// ```
437 pub fn patch(mut self, other: Style) -> Style {
438 self.fg = other.fg.or(self.fg);
439 self.bg = other.bg.or(self.bg);
441 self.add_modifier.remove(other.sub_modifier);
442 self.add_modifier.insert(other.add_modifier);
443 self.sub_modifier.remove(other.add_modifier);
444 self.sub_modifier.insert(other.sub_modifier);
446 self
447 }
450#[cfg(test)]
451mod tests {
452 use super::*;
454 #[test]
455 fn test_rect_size_truncation() {
456 for width in 256u16..300u16 {
457 for height in 256u16..300u16 {
458 let rect = Rect::new(0, 0, width, height);
459 rect.area(); // Should not panic.
460 assert!(rect.width < width || rect.height < height);
461 // The target dimensions are rounded down so the math will not be too precise
462 // but let's make sure the ratios don't diverge crazily.
463 assert!(
464 (f64::from(rect.width) / f64::from(rect.height)
465 - f64::from(width) / f64::from(height))
466 .abs()
467 < 1.0
468 )
469 }
470 }
472 // One dimension below 255, one above. Area above max u16.
473 let width = 900;
474 let height = 100;
475 let rect = Rect::new(0, 0, width, height);
476 assert_ne!(rect.width, 900);
477 assert_ne!(rect.height, 100);
478 assert!(rect.width < width || rect.height < height);
479 }
481 #[test]
482 fn test_rect_size_preservation() {
483 for width in 0..256u16 {
484 for height in 0..256u16 {
485 let rect = Rect::new(0, 0, width, height);
486 rect.area(); // Should not panic.
487 assert_eq!(rect.width, width);
488 assert_eq!(rect.height, height);
489 }
490 }
492 // One dimension below 255, one above. Area below max u16.
493 let rect = Rect::new(0, 0, 300, 100);
494 assert_eq!(rect.width, 300);
495 assert_eq!(rect.height, 100);
496 }
498 fn styles() -> Vec<Style> {
499 vec![
500 Style::default(),
501 Style::default().fg(Color::Yellow),
502 Style::default().bg(Color::Yellow),
503 Style::default().add_modifier(Modifier::BOLD),
504 Style::default().remove_modifier(Modifier::BOLD),
505 Style::default().add_modifier(Modifier::ITALIC),
506 Style::default().remove_modifier(Modifier::ITALIC),
507 Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
508 Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
509 ]
510 }
512 #[test]
513 fn combined_patch_gives_same_result_as_individual_patch() {
514 let styles = styles();
515 for &a in &styles {
516 for &b in &styles {
517 for &c in &styles {
518 for &d in &styles {
519 let combined = a.patch(b.patch(c.patch(d)));
521 assert_eq!(
522 Style::default().patch(a).patch(b).patch(c).patch(d),
523 Style::default().patch(combined)
524 );
525 }
526 }
527 }
528 }
529 }