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,
};
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<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.
///
/// 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::<Lp>(color, StrokeOptions::default());
}
/// Renders the default focus ring for this widget.

View file

@ -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::<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 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<bool>,
currently_enabled: bool,
buttons_pressed: usize,
background_color: Option<Dynamic<Color>>,
text_color: Option<Dynamic<Color>>,
colors: Option<Dynamic<Colors>>,
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::<Lp>(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,

View file

@ -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<Callback<KeyEvent, EventHandling>>,
editor: Option<LiveEditor>,
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<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)]
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::<Lp>(outline_color, StrokeOptions::default());
}
let text_color = styles.get(&TextColor, context);
@ -330,6 +367,10 @@ impl Widget for Input {
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
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();
}