Button/input outline, Input select all

This commit is contained in:
Jonathan Johnson 2023-11-10 19:29:24 -08:00
parent d844a44b33
commit 972a1c1c13
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 169 additions and 47 deletions

View file

@ -7,10 +7,10 @@ use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
}; };
use kludgine::app::winit::window; use kludgine::app::winit::window;
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{IntoSigned, Point, Rect, Size}; use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::Kludgine; use kludgine::{Color, Kludgine};
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground}; 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<Unit>(&mut self, color: Color, options: StrokeOptions<Unit>)
where
Unit: ScreenScale<Px = Px, Lp = Lp>,
{
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. /// Renders the default focus ring for this widget.
/// ///
/// To ensure the correct color is used, include [`HighlightColor`] in the /// 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; return;
} }
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1))); let color = styles.get(&HighlightColor, self);
let focus_ring = Shape::stroked_rect( self.stroke_outline::<Lp>(color, StrokeOptions::default());
visible_rect,
styles.get(&HighlightColor, self),
StrokeOptions::default(),
);
self.gfx
.draw_shape(&focus_ring, Point::default(), None, None);
} }
/// Renders the default focus ring for this widget. /// Renders the default focus ring for this widget.

View file

@ -394,3 +394,39 @@ impl ComponentDefinition for WidgetBackground {
Color::CLEAR_WHITE 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::<Global>("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::<Global>("disabled_outline_color"))
}
}
impl ComponentDefinition for DisabledOutlineColor {
type ComponentType = Color;
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color {
context.theme().surface.outline_variant
}
}

View file

@ -4,16 +4,18 @@ use std::panic::UnwindSafe;
use std::time::Duration; use std::time::Duration;
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; 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::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
use kludgine::shapes::StrokeOptions;
use kludgine::text::Text; use kludgine::text::Text;
use kludgine::Color; 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::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::names::Name; use crate::names::Name;
use crate::styles::components::{ 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::styles::{ColorExt, ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
use crate::utils::ModifiersExt; use crate::utils::ModifiersExt;
@ -31,8 +33,8 @@ pub struct Button {
pub enabled: Value<bool>, pub enabled: Value<bool>,
currently_enabled: bool, currently_enabled: bool,
buttons_pressed: usize, buttons_pressed: usize,
background_color: Option<Dynamic<Color>>, colors: Option<Dynamic<Colors>>,
text_color: Option<Dynamic<Color>>,
color_animation: AnimationHandle, color_animation: AnimationHandle,
} }
@ -45,8 +47,7 @@ impl Button {
enabled: Value::Constant(true), enabled: Value::Constant(true),
currently_enabled: true, currently_enabled: true,
buttons_pressed: 0, buttons_pressed: 0,
background_color: None, colors: None,
text_color: None,
color_animation: AnimationHandle::default(), color_animation: AnimationHandle::default(),
} }
} }
@ -88,18 +89,23 @@ impl Button {
&Easing, &Easing,
&TextColor, &TextColor,
&SurfaceColor, &SurfaceColor,
&OutlineColor,
&DisabledOutlineColor,
]); ]);
let text_color = styles.get(&TextColor, context); let text_color = styles.get(&TextColor, context);
let surface_color = styles.get(&SurfaceColor, 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(&ButtonDisabledBackground, context),
styles.get(&DisabledOutlineColor, context),
text_color, text_color,
surface_color, surface_color,
) )
} else if context.is_default() { } else if context.is_default() {
// TODO this probably should be de-prioritized if ButtonBackground is explicitly set. // TODO this probably should be de-prioritized if ButtonBackground is explicitly set.
( (
context.theme().primary.color,
context.theme().primary.color, context.theme().primary.color,
context.theme().primary.on_color, context.theme().primary.on_color,
context.theme().primary.color, context.theme().primary.color,
@ -107,56 +113,77 @@ impl Button {
} else if context.active() { } else if context.active() {
( (
styles.get(&ButtonActiveBackground, context), styles.get(&ButtonActiveBackground, context),
outline_color,
text_color, text_color,
surface_color, surface_color,
) )
} else if context.hovered() { } else if context.hovered() {
( (
styles.get(&ButtonHoverBackground, context), styles.get(&ButtonHoverBackground, context),
outline_color,
text_color, text_color,
surface_color, surface_color,
) )
} else { } else {
( (
styles.get(&ButtonBackground, context), styles.get(&ButtonBackground, context),
outline_color,
text_color, text_color,
surface_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) { let new_colors = Colors {
(false, Some(bg), Some(text)) => { background,
self.color_animation = ( text,
bg.transition_to(background_color), outline,
text.transition_to(text_color), };
)
match (immediate, &self.colors) {
(false, Some(colors)) => {
self.color_animation = colors
.transition_to(new_colors)
.over(Duration::from_millis(150)) .over(Duration::from_millis(150))
.with_easing(styles.get(&Easing, context)) .with_easing(styles.get(&Easing, context))
.spawn(); .spawn();
} }
(true, Some(bg), Some(text)) => { (true, Some(colors)) => {
bg.update(background_color); colors.update(new_colors);
text.update(text_color);
self.color_animation.clear(); self.color_animation.clear();
} }
_ => { _ => {
self.background_color = Some(Dynamic::new(background_color)); self.colors = Some(Dynamic::new(new_colors));
self.text_color = Some(Dynamic::new(text_color));
} }
} }
} }
fn current_colors(&mut self, context: &WidgetContext<'_, '_>) -> (Color, Color) { fn current_colors(&mut self, context: &WidgetContext<'_, '_>) -> Colors {
if self.background_color.is_none() { if self.colors.is_none() {
self.update_colors(context, false); self.update_colors(context, false);
} }
let background_color = self.background_color.as_ref().expect("always initialized"); let colors = self.colors.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(colors);
context.redraw_when_changed(background_color); colors.get()
(background_color.get(), text_color.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.label.redraw_when_changed(context);
self.enabled.redraw_when_changed(context); self.enabled.redraw_when_changed(context);
let (background_color, text_color) = self.current_colors(context); let colors = self.current_colors(context);
context.gfx.fill(background_color); context.gfx.fill(colors.background);
if context.focused() { if context.focused() {
context.draw_focus_ring(); context.draw_focus_ring();
} else {
context.stroke_outline::<Lp>(colors.outline, StrokeOptions::default());
} }
self.label.map(|label| { self.label.map(|label| {
context.gfx.draw_text( context.gfx.draw_text(
Text::new(label, text_color) Text::new(label, colors.text)
.origin(kludgine::text::TextOrigin::Center) .origin(kludgine::text::TextOrigin::Center)
.wrap_at(size.width), .wrap_at(size.width),
center, center,

View file

@ -5,17 +5,19 @@ use std::time::Duration;
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent}; use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
use kludgine::app::winit::keyboard::Key; use kludgine::app::winit::keyboard::Key;
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping}; use kludgine::cosmic_text::{
use kludgine::figures::units::{Px, UPx}; Action, Affinity, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping,
};
use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{ use kludgine::figures::{
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
}; };
use kludgine::shapes::Shape; use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::text::TextOrigin; use kludgine::text::TextOrigin;
use kludgine::{Color, Kludgine}; use kludgine::{Color, Kludgine};
use crate::context::{EventContext, LayoutContext, WidgetContext}; 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::styles::Styles;
use crate::utils::ModifiersExt; use crate::utils::ModifiersExt;
use crate::value::{Generation, IntoValue, Value}; use crate::value::{Generation, IntoValue, Value};
@ -32,6 +34,8 @@ pub struct Input {
on_key: Option<Callback<KeyEvent, EventHandling>>, on_key: Option<Callback<KeyEvent, EventHandling>>,
editor: Option<LiveEditor>, editor: Option<LiveEditor>,
cursor_state: CursorState, cursor_state: CursorState,
needs_to_select_all: bool,
mouse_buttons_down: usize,
} }
impl Input { impl Input {
@ -47,6 +51,8 @@ impl Input {
editor: None, editor: None,
cursor_state: CursorState::default(), cursor_state: CursorState::default(),
on_key: None, on_key: None,
mouse_buttons_down: 0,
needs_to_select_all: true,
} }
} }
@ -100,6 +106,22 @@ impl Input {
fn styles(context: &WidgetContext<'_, '_>) -> Styles { fn styles(context: &WidgetContext<'_, '_>) -> Styles {
context.query_styles(&[&TextColor, &TextSize, &LineHeight]) 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 { impl Default for Input {
@ -132,7 +154,9 @@ impl Widget for Input {
_button: kludgine::app::winit::event::MouseButton, _button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_, '_>, context: &mut EventContext<'_, '_>,
) -> EventHandling { ) -> EventHandling {
self.mouse_buttons_down += 1;
context.focus(); context.focus();
self.needs_to_select_all = false;
let styles = context.query_styles(&[&TextColor]); let styles = context.query_styles(&[&TextColor]);
self.editor_mut(context.kludgine, &styles, &context.widget) self.editor_mut(context.kludgine, &styles, &context.widget)
.action( .action(
@ -166,12 +190,22 @@ impl Widget for Input {
context.set_needs_redraw(); context.set_needs_redraw();
} }
fn mouse_up(
&mut self,
_location: Option<Point<Px>>,
_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)] #[allow(clippy::too_many_lines)]
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
self.cursor_state.update(context.elapsed()); self.cursor_state.update(context.elapsed());
let cursor_state = self.cursor_state; let cursor_state = self.cursor_state;
let size = context.gfx.size(); 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 highlight = styles.get(&HighlightColor, context);
let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget); let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget);
let cursor = editor.cursor(); let cursor = editor.cursor();
@ -311,6 +345,9 @@ impl Widget for Input {
context.redraw_when_changed(context.window().focused()); context.redraw_when_changed(context.window().focused());
} }
} }
} else {
let outline_color = styles.get(&OutlineColor, context);
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
} }
let text_color = styles.get(&TextColor, context); let text_color = styles.get(&TextColor, context);
@ -330,6 +367,10 @@ impl Widget for Input {
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> { ) -> Size<UPx> {
let styles = context.query_styles(&[&TextColor]); 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 editor = self.editor_mut(&mut context.graphics.gfx, &styles, &context.graphics.widget);
let buffer = editor.buffer_mut(); let buffer = editor.buffer_mut();
buffer.set_size( buffer.set_size(
@ -362,7 +403,7 @@ impl Widget for Input {
// "Keyboard input: {:?}. {:?}, {:?}", // "Keyboard input: {:?}. {:?}, {:?}",
// input.logical_key, input.text, input.physical_key // 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), _) => { (ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
editor.action( editor.action(
context.kludgine.font_system(), context.kludgine.font_system(),
@ -400,6 +441,12 @@ impl Widget for Input {
); );
(false, HANDLED) (false, HANDLED)
} }
(state, _, Some("a")) if context.modifiers().primary() => {
if state.is_pressed() {
self.select_all();
}
(false, HANDLED)
}
(state, _, Some(text)) (state, _, Some(text))
if !context.modifiers().primary() if !context.modifiers().primary()
&& text != "\t" // tab && text != "\t" // tab
@ -408,7 +455,7 @@ impl Widget for Input {
=> =>
{ {
if state.is_pressed() { if state.is_pressed() {
editor.insert_string(&text, None); editor.insert_string(text, None);
} }
(state.is_pressed(), HANDLED) (state.is_pressed(), HANDLED)
} }
@ -456,6 +503,10 @@ impl Widget for Input {
} }
fn focus(&mut self, context: &mut EventContext<'_, '_>) { 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_ime_allowed(true);
context.set_needs_redraw(); context.set_needs_redraw();
} }