From 972a1c1c1390a4695d05a4251e765bfee651e833 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Fri, 10 Nov 2023 19:29:24 -0800 Subject: [PATCH] Button/input outline, Input select all --- src/context.rs | 28 ++++++++----- src/styles/components.rs | 36 +++++++++++++++++ src/widgets/button.rs | 87 ++++++++++++++++++++++++++-------------- src/widgets/input.rs | 65 ++++++++++++++++++++++++++---- 4 files changed, 169 insertions(+), 47 deletions(-) diff --git a/src/context.rs b/src/context.rs index 018a6dc..ca50068 100644 --- a/src/context.rs +++ b/src/context.rs @@ -7,10 +7,10 @@ use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::window; -use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{IntoSigned, Point, Rect, Size}; +use kludgine::figures::units::{Lp, Px, UPx}; +use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::{Shape, StrokeOptions}; -use kludgine::Kludgine; +use kludgine::{Color, Kludgine}; use crate::graphics::Graphics; use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground}; @@ -450,6 +450,18 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } } + /// Strokes an outline around this widget's contents. + pub fn stroke_outline(&mut self, color: Color, options: StrokeOptions) + where + Unit: ScreenScale, + { + let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1))); + let focus_ring = + Shape::stroked_rect(visible_rect, color, options.into_px(self.gfx.scale())); + self.gfx + .draw_shape(&focus_ring, Point::default(), None, None); + } + /// Renders the default focus ring for this widget. /// /// To ensure the correct color is used, include [`HighlightColor`] in the @@ -460,14 +472,8 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' return; } - let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1))); - let focus_ring = Shape::stroked_rect( - visible_rect, - styles.get(&HighlightColor, self), - StrokeOptions::default(), - ); - self.gfx - .draw_shape(&focus_ring, Point::default(), None, None); + let color = styles.get(&HighlightColor, self); + self.stroke_outline::(color, StrokeOptions::default()); } /// Renders the default focus ring for this widget. diff --git a/src/styles/components.rs b/src/styles/components.rs index aa8078e..0baa24c 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -394,3 +394,39 @@ impl ComponentDefinition for WidgetBackground { Color::CLEAR_WHITE } } + +/// A [`Color`] to be used as an outline color. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct OutlineColor; + +impl NamedComponent for OutlineColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("outline_color")) + } +} + +impl ComponentDefinition for OutlineColor { + type ComponentType = Color; + + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.outline + } +} + +/// A [`Color`] to be used as an outline color. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct DisabledOutlineColor; + +impl NamedComponent for DisabledOutlineColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("disabled_outline_color")) + } +} + +impl ComponentDefinition for DisabledOutlineColor { + type ComponentType = Color; + + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.outline_variant + } +} diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 1893f7e..ad8323d 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -4,16 +4,18 @@ use std::panic::UnwindSafe; use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; -use kludgine::figures::units::{Px, UPx}; +use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::shapes::StrokeOptions; use kludgine::text::Text; use kludgine::Color; -use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; +use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn}; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::names::Name; use crate::styles::components::{ - AutoFocusableControls, Easing, IntrinsicPadding, SurfaceColor, TextColor, + AutoFocusableControls, DisabledOutlineColor, Easing, IntrinsicPadding, OutlineColor, + SurfaceColor, TextColor, }; use crate::styles::{ColorExt, ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::utils::ModifiersExt; @@ -31,8 +33,8 @@ pub struct Button { pub enabled: Value, currently_enabled: bool, buttons_pressed: usize, - background_color: Option>, - text_color: Option>, + colors: Option>, + color_animation: AnimationHandle, } @@ -45,8 +47,7 @@ impl Button { enabled: Value::Constant(true), currently_enabled: true, buttons_pressed: 0, - background_color: None, - text_color: None, + colors: None, color_animation: AnimationHandle::default(), } } @@ -88,18 +89,23 @@ impl Button { &Easing, &TextColor, &SurfaceColor, + &OutlineColor, + &DisabledOutlineColor, ]); let text_color = styles.get(&TextColor, context); let surface_color = styles.get(&SurfaceColor, context); - let (background_color, text_color, surface_color) = if !self.enabled.get() { + let outline_color = styles.get(&OutlineColor, context); + let (background, outline, text_color, surface_color) = if !self.enabled.get() { ( styles.get(&ButtonDisabledBackground, context), + styles.get(&DisabledOutlineColor, context), text_color, surface_color, ) } else if context.is_default() { // TODO this probably should be de-prioritized if ButtonBackground is explicitly set. ( + context.theme().primary.color, context.theme().primary.color, context.theme().primary.on_color, context.theme().primary.color, @@ -107,56 +113,77 @@ impl Button { } else if context.active() { ( styles.get(&ButtonActiveBackground, context), + outline_color, text_color, surface_color, ) } else if context.hovered() { ( styles.get(&ButtonHoverBackground, context), + outline_color, text_color, surface_color, ) } else { ( styles.get(&ButtonBackground, context), + outline_color, text_color, surface_color, ) }; - let text_color = background_color.most_contrasting(&[text_color, surface_color]); + let text = background.most_contrasting(&[text_color, surface_color]); - match (immediate, &self.background_color, &self.text_color) { - (false, Some(bg), Some(text)) => { - self.color_animation = ( - bg.transition_to(background_color), - text.transition_to(text_color), - ) + let new_colors = Colors { + background, + text, + outline, + }; + + match (immediate, &self.colors) { + (false, Some(colors)) => { + self.color_animation = colors + .transition_to(new_colors) .over(Duration::from_millis(150)) .with_easing(styles.get(&Easing, context)) .spawn(); } - (true, Some(bg), Some(text)) => { - bg.update(background_color); - text.update(text_color); + (true, Some(colors)) => { + colors.update(new_colors); self.color_animation.clear(); } _ => { - self.background_color = Some(Dynamic::new(background_color)); - self.text_color = Some(Dynamic::new(text_color)); + self.colors = Some(Dynamic::new(new_colors)); } } } - fn current_colors(&mut self, context: &WidgetContext<'_, '_>) -> (Color, Color) { - if self.background_color.is_none() { + fn current_colors(&mut self, context: &WidgetContext<'_, '_>) -> Colors { + if self.colors.is_none() { self.update_colors(context, false); } - let background_color = self.background_color.as_ref().expect("always initialized"); - let text_color = self.text_color.as_ref().expect("always initialized"); // TODO combine these into a single option - context.redraw_when_changed(background_color); - (background_color.get(), text_color.get()) + let colors = self.colors.as_ref().expect("always initialized"); + context.redraw_when_changed(colors); + colors.get() + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +struct Colors { + background: Color, + text: Color, + outline: Color, +} + +impl LinearInterpolate for Colors { + fn lerp(&self, target: &Self, percent: f32) -> Self { + Self { + background: self.background.lerp(&target.background, percent), + text: self.text.lerp(&target.text, percent), + outline: self.outline.lerp(&target.outline, percent), + } } } @@ -175,16 +202,18 @@ impl Widget for Button { self.label.redraw_when_changed(context); self.enabled.redraw_when_changed(context); - let (background_color, text_color) = self.current_colors(context); - context.gfx.fill(background_color); + let colors = self.current_colors(context); + context.gfx.fill(colors.background); if context.focused() { context.draw_focus_ring(); + } else { + context.stroke_outline::(colors.outline, StrokeOptions::default()); } self.label.map(|label| { context.gfx.draw_text( - Text::new(label, text_color) + Text::new(label, colors.text) .origin(kludgine::text::TextOrigin::Center) .wrap_at(size.width), center, diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 900987f..44482fd 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -5,17 +5,19 @@ use std::time::Duration; use kludgine::app::winit::event::{ElementState, Ime, KeyEvent}; use kludgine::app::winit::keyboard::Key; -use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping}; -use kludgine::figures::units::{Px, UPx}; +use kludgine::cosmic_text::{ + Action, Affinity, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping, +}; +use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, }; -use kludgine::shapes::Shape; +use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::text::TextOrigin; use kludgine::{Color, Kludgine}; use crate::context::{EventContext, LayoutContext, WidgetContext}; -use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}; +use crate::styles::components::{HighlightColor, LineHeight, OutlineColor, TextColor, TextSize}; use crate::styles::Styles; use crate::utils::ModifiersExt; use crate::value::{Generation, IntoValue, Value}; @@ -32,6 +34,8 @@ pub struct Input { on_key: Option>, editor: Option, cursor_state: CursorState, + needs_to_select_all: bool, + mouse_buttons_down: usize, } impl Input { @@ -47,6 +51,8 @@ impl Input { editor: None, cursor_state: CursorState::default(), on_key: None, + mouse_buttons_down: 0, + needs_to_select_all: true, } } @@ -100,6 +106,22 @@ impl Input { fn styles(context: &WidgetContext<'_, '_>) -> Styles { context.query_styles(&[&TextColor, &TextSize, &LineHeight]) } + + fn select_all(&mut self) { + let Some(editor) = self.editor.as_mut().map(|editor| &mut editor.editor) else { + return; + }; + if !editor.buffer().lines.is_empty() { + let line = editor.buffer().lines.len() - 1; + let end = Cursor::new_with_affinity( + line, + editor.buffer().lines[line].text().len(), + Affinity::After, + ); + editor.set_cursor(end); + editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before))); + } + } } impl Default for Input { @@ -132,7 +154,9 @@ impl Widget for Input { _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_, '_>, ) -> EventHandling { + self.mouse_buttons_down += 1; context.focus(); + self.needs_to_select_all = false; let styles = context.query_styles(&[&TextColor]); self.editor_mut(context.kludgine, &styles, &context.widget) .action( @@ -166,12 +190,22 @@ impl Widget for Input { context.set_needs_redraw(); } + fn mouse_up( + &mut self, + _location: Option>, + _device_id: kludgine::app::winit::event::DeviceId, + _button: kludgine::app::winit::event::MouseButton, + _context: &mut EventContext<'_, '_>, + ) { + self.mouse_buttons_down -= 1; + } + #[allow(clippy::too_many_lines)] fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { self.cursor_state.update(context.elapsed()); let cursor_state = self.cursor_state; let size = context.gfx.size(); - let styles = context.query_styles(&[&TextColor, &HighlightColor]); + let styles = context.query_styles(&[&TextColor, &HighlightColor, &OutlineColor]); let highlight = styles.get(&HighlightColor, context); let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget); let cursor = editor.cursor(); @@ -311,6 +345,9 @@ impl Widget for Input { context.redraw_when_changed(context.window().focused()); } } + } else { + let outline_color = styles.get(&OutlineColor, context); + context.stroke_outline::(outline_color, StrokeOptions::default()); } let text_color = styles.get(&TextColor, context); @@ -330,6 +367,10 @@ impl Widget for Input { context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let styles = context.query_styles(&[&TextColor]); + if self.needs_to_select_all { + self.needs_to_select_all = false; + self.select_all(); + } let editor = self.editor_mut(&mut context.graphics.gfx, &styles, &context.graphics.widget); let buffer = editor.buffer_mut(); buffer.set_size( @@ -362,7 +403,7 @@ impl Widget for Input { // "Keyboard input: {:?}. {:?}, {:?}", // input.logical_key, input.text, input.physical_key // ); - let (text_changed, handled) = match (input.state, input.logical_key, input.text) { + let (text_changed, handled) = match (input.state, input.logical_key, input.text.as_deref()) { (ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => { editor.action( context.kludgine.font_system(), @@ -400,6 +441,12 @@ impl Widget for Input { ); (false, HANDLED) } + (state, _, Some("a")) if context.modifiers().primary() => { + if state.is_pressed() { + self.select_all(); + } + (false, HANDLED) + } (state, _, Some(text)) if !context.modifiers().primary() && text != "\t" // tab @@ -408,7 +455,7 @@ impl Widget for Input { => { if state.is_pressed() { - editor.insert_string(&text, None); + editor.insert_string(text, None); } (state.is_pressed(), HANDLED) } @@ -456,6 +503,10 @@ impl Widget for Input { } fn focus(&mut self, context: &mut EventContext<'_, '_>) { + if self.mouse_buttons_down == 0 { + self.needs_to_select_all = true; + } + context.set_ime_allowed(true); context.set_needs_redraw(); }