mirror of
https://github.com/danbulant/cushy
synced 2026-06-21 07:32:08 +00:00
Button/input outline, Input select all
This commit is contained in:
parent
d844a44b33
commit
972a1c1c13
4 changed files with 169 additions and 47 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue