From e14706ec7d8c0fe83aa02d82f11f6414b8d2fe52 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Wed, 4 Oct 2023 10:49:20 -0400 Subject: [PATCH] Add EditableText family of palette styles --- cursive-core/src/theme/palette.rs | 39 +++++++- cursive-core/src/views/edit_view.rs | 150 +++++++++++++++------------- cursive-core/src/views/text_area.rs | 83 +++++++-------- cursive/examples/theme_manual.rs | 17 +++- 4 files changed, 175 insertions(+), 114 deletions(-) diff --git a/cursive-core/src/theme/palette.rs b/cursive-core/src/theme/palette.rs index da72bae3..37d1b8d9 100644 --- a/cursive-core/src/theme/palette.rs +++ b/cursive-core/src/theme/palette.rs @@ -107,6 +107,12 @@ fn default_styles() -> EnumMap { color: ColorStyle::highlight_inactive().invert(), effects: enumset::enum_set!(Effect::Reverse), }, + EditableText => Style { + color: ColorStyle::secondary(), + effects: enumset::enum_set!(Effect::Reverse), + }, + EditableTextCursor => ColorStyle::secondary().into(), + EditableTextInactive => ColorStyle::secondary().into(), } } @@ -360,6 +366,24 @@ pub enum PaletteColor { } /// Style entry in a palette. +/// +/// This represents a color "role". The palette will resolve this to a `Style`. +/// +/// For example, `PaletteStyle::Highlight` should be used when drawing highlighted text. +/// In the default palette, it will resolve to a `Style` made of: +/// * The `Reverse` effect (front and background will be swapped). +/// * A front color of `PaletteColor::Highlight` (but with the reverse effect, +/// it will become the background color). +/// * A back color of `PaletteColor::HighlightText` (will become the front color). +/// +/// From there, the `PaletteColor::Highlight` and `PaletteColor::HighlightText` will be resolved to +/// concrete colors (or possibly to `InheritParent`, which will inherit the previous concrete +/// color). +/// +/// To override the look of highlighted text, you can either: +/// * Change the palette entries for `PaletteColor::Highlight`/`PaletteColor::HighlightText`. +/// * Change the palette entry for `PaletteStyle::Highlight`, possibly using different palette +/// colors instead (or directly specifying a concrete color there). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Enum)] pub enum PaletteStyle { /// Style used for regular text. @@ -380,8 +404,17 @@ pub enum PaletteStyle { Highlight, /// Style used for inactive highlighted text. HighlightInactive, - /// Style used to draw the shadows. + + /// Style used to draw the drop shadows (1-cell border to the bottom/right + /// of views). Shadow, + + /// Style used for editable text (TextArea, EditView). + EditableText, + /// Style used for the selected character in editable text. + EditableTextCursor, + /// Style used for editable text when inactive. + EditableTextInactive, } impl PaletteStyle { @@ -415,6 +448,7 @@ impl FromStr for PaletteStyle { use PaletteStyle::*; Ok(match s { + // TODO: make a macro for this? "Background" | "background" => Background, "Shadow" | "shadow" => Shadow, "View" | "view" => View, @@ -425,6 +459,9 @@ impl FromStr for PaletteStyle { "TitleSecondary" | "title_secondary" => TitleSecondary, "Highlight" | "highlight" => Highlight, "HighlightInactive" | "highlight_inactive" => HighlightInactive, + "EditableText" | "editable_text" => EditableText, + "EditableTextCursor" | "editable_text_cursor" => EditableTextCursor, + "EditableTextInactive" | "editable_text_inactive" => EditableTextInactive, _ => return Err(NoSuchColor), }) } diff --git a/cursive-core/src/views/edit_view.rs b/cursive-core/src/views/edit_view.rs index da2b18bd..35d15906 100644 --- a/cursive-core/src/views/edit_view.rs +++ b/cursive-core/src/views/edit_view.rs @@ -105,7 +105,9 @@ pub struct EditView { enabled: bool, - style: StyleType, + regular_style: StyleType, + inactive_style: StyleType, + cursor_style: StyleType, } new_default!(EditView); @@ -126,7 +128,9 @@ impl EditView { secret: false, filler: "_".to_string(), enabled: true, - style: PaletteStyle::Secondary.into(), + regular_style: PaletteStyle::EditableText.into(), + inactive_style: PaletteStyle::EditableTextInactive.into(), + cursor_style: PaletteStyle::EditableTextCursor.into(), } } @@ -192,7 +196,9 @@ impl EditView { /// /// Defaults to `ColorStyle::Secondary`. pub fn set_style>(&mut self, style: S) { - self.style = style.into(); + let style = style.into(); + self.regular_style = style; + // TODO: Also update cursor and inactive styles? } /// Sets the style used for this view. @@ -503,6 +509,11 @@ impl EditView { /// Best used for single character replacement. fn make_small_stars(length: usize) -> &'static str { // TODO: be able to use any character as hidden mode? + assert!( + length <= 4, + "Can only generate stars for one grapheme at a time." + ); + &"****"[..length] } @@ -514,80 +525,81 @@ impl View for EditView { self.last_length, printer.size.x ); + let (style, cursor_style) = if self.enabled && printer.enabled { + (self.regular_style, self.cursor_style) + } else { + (self.inactive_style, self.inactive_style) + }; + let width = self.content.width(); - printer.with_style(self.style, |printer| { - let effect = if self.enabled && printer.enabled { - Effect::Reverse + printer.with_style(style, |printer| { + if width < self.last_length { + // No problem, everything fits. + assert!(printer.size.x >= width); + if self.secret { + printer.print_hline((0, 0), width, "*"); + } else { + printer.print((0, 0), &self.content); + } + let filler_len = (printer.size.x - width) / self.filler.width(); + printer.print_hline((width, 0), filler_len, self.filler.as_str()); } else { - Effect::Simple - }; - printer.with_effect(effect, |printer| { + let content = &self.content[self.offset..]; + let display_bytes = content + .graphemes(true) + .scan(0, |w, g| { + *w += g.width(); + if *w > self.last_length { + None + } else { + Some(g) + } + }) + .map(str::len) + .sum(); + + let content = &content[..display_bytes]; + let width = content.width(); + + if self.secret { + printer.print_hline((0, 0), width, "*"); + } else { + printer.print((0, 0), content); + } + if width < self.last_length { - // No problem, everything fits. - assert!(printer.size.x >= width); - if self.secret { - printer.print_hline((0, 0), width, "*"); - } else { - printer.print((0, 0), &self.content); - } - let filler_len = (printer.size.x - width) / self.filler.width(); + let filler_len = (self.last_length - width) / self.filler.width(); printer.print_hline((width, 0), filler_len, self.filler.as_str()); - } else { - let content = &self.content[self.offset..]; - let display_bytes = content - .graphemes(true) - .scan(0, |w, g| { - *w += g.width(); - if *w > self.last_length { - None - } else { - Some(g) - } - }) - .map(str::len) - .sum(); - - let content = &content[..display_bytes]; - let width = content.width(); - - if self.secret { - printer.print_hline((0, 0), width, "*"); - } else { - printer.print((0, 0), content); - } - - if width < self.last_length { - let filler_len = (self.last_length - width) / self.filler.width(); - printer.print_hline((width, 0), filler_len, self.filler.as_str()); - } } - }); + } + }); - // Now print cursor - if printer.focused { - let c: &str = if self.cursor == self.content.len() { - &self.filler + // Now print cursor + if printer.focused { + let c: &str = if self.cursor == self.content.len() { + &self.filler + } else { + // Get the char from the string... Is it so hard? + let selected = self.content[self.cursor..] + .graphemes(true) + .next() + .unwrap_or_else(|| { + panic!( + "Found no char at cursor {} in {}", + self.cursor, &self.content + ) + }); + if self.secret { + make_small_stars(selected.width()) } else { - // Get the char from the string... Is it so hard? - let selected = self.content[self.cursor..] - .graphemes(true) - .next() - .unwrap_or_else(|| { - panic!( - "Found no char at cursor {} in {}", - self.cursor, &self.content - ) - }); - if self.secret { - make_small_stars(selected.width()) - } else { - selected - } - }; - let offset = self.content[self.offset..self.cursor].width(); + selected + } + }; + let offset = self.content[self.offset..self.cursor].width(); + printer.with_style(cursor_style, |printer| { printer.print((offset, 0), c); - } - }); + }); + } } fn layout(&mut self, size: Vec2) { diff --git a/cursive-core/src/views/text_area.rs b/cursive-core/src/views/text_area.rs index 9b101954..605d9193 100644 --- a/cursive-core/src/views/text_area.rs +++ b/cursive-core/src/views/text_area.rs @@ -487,49 +487,52 @@ impl View for TextArea { } fn draw(&self, printer: &Printer) { - printer.with_style(PaletteStyle::Secondary, |printer| { - let effect = if self.enabled && printer.enabled { - Effect::Reverse - } else { - Effect::Simple - }; - - let w = if self.scrollbase.scrollable() { - printer.size.x.saturating_sub(1) - } else { - printer.size.x - }; - printer.with_effect(effect, |printer| { - for y in 0..printer.size.y { - printer.print_hline((0, y), w, " "); - } - }); + let (style, cursor_style) = if self.enabled && printer.enabled { + (PaletteStyle::EditableText, PaletteStyle::EditableTextCursor) + } else { + ( + PaletteStyle::EditableTextInactive, + PaletteStyle::EditableTextInactive, + ) + }; - debug!("Content: `{}`", &self.content); - self.scrollbase.draw(printer, |printer, i| { - debug!("Drawing row {}", i); - let row = &self.rows[i]; - debug!("row: {:?}", row); - let text = &self.content[row.start..row.end]; - debug!("row text: `{}`", text); - printer.with_effect(effect, |printer| { - printer.print((0, 0), text); - }); + let w = if self.scrollbase.scrollable() { + printer.size.x.saturating_sub(1) + } else { + printer.size.x + }; + printer.with_style(style, |printer| { + for y in 0..printer.size.y { + printer.print_hline((0, y), w, " "); + } + }); - if printer.focused && i == self.selected_row() { - let cursor_offset = self.cursor - row.start; - let c = if cursor_offset == text.len() { - "_" - } else { - text[cursor_offset..] - .graphemes(true) - .next() - .expect("Found no char!") - }; - let offset = text[..cursor_offset].width(); - printer.print((offset, 0), c); - } + debug!("Content: `{}`", &self.content); + self.scrollbase.draw(printer, |printer, i| { + debug!("Drawing row {}", i); + let row = &self.rows[i]; + debug!("row: {:?}", row); + let text = &self.content[row.start..row.end]; + debug!("row text: `{}`", text); + printer.with_style(style, |printer| { + printer.print((0, 0), text); }); + + if printer.focused && i == self.selected_row() { + let cursor_offset = self.cursor - row.start; + let c = if cursor_offset == text.len() { + "_" + } else { + text[cursor_offset..] + .graphemes(true) + .next() + .expect("Found no char!") + }; + let offset = text[..cursor_offset].width(); + printer.with_style(cursor_style, |printer| { + printer.print((offset, 0), c); + }); + } }); } diff --git a/cursive/examples/theme_manual.rs b/cursive/examples/theme_manual.rs index 5321c411..52475545 100644 --- a/cursive/examples/theme_manual.rs +++ b/cursive/examples/theme_manual.rs @@ -32,26 +32,35 @@ fn main() { use cursive::theme::PaletteStyle::*; use cursive::theme::Style; palette[Highlight] = Style::from(Blue.light()).combine(Bold); + palette[EditableTextCursor] = Style::secondary().combine(Reverse).combine(Underline) } }), }); let layout = LinearLayout::vertical() .child(TextView::new("This is a dynamic theme example!")) - .child(EditView::new().content("Woo! colors!")); + .child(EditView::new().content("Woo! colors!").filler(" ")); siv.add_layer( Dialog::around(layout) .title("Theme example") .button("Change", |s| { + use cursive::theme::BaseColor::*; + use cursive::theme::Color::TerminalDefault; + use cursive::theme::PaletteColor::*; // Change _something_ when the button is pressed. let mut theme = s.current_theme().clone(); theme.shadow = !theme.shadow; theme.borders = match theme.borders { - BorderStyle::Simple => BorderStyle::Outset, - BorderStyle::Outset => BorderStyle::None, - BorderStyle::None => BorderStyle::Simple, + BorderStyle::None => { + theme.palette[View] = TerminalDefault; + BorderStyle::Simple + } + _ => { + theme.palette[View] = Black.light(); + BorderStyle::None + } }; s.set_theme(theme);