diff --git a/examples/cursor-icon.rs b/examples/cursor-icon.rs new file mode 100644 index 0000000..c55b9ae --- /dev/null +++ b/examples/cursor-icon.rs @@ -0,0 +1,25 @@ +use gooey::value::Dynamic; +use gooey::widget::MakeWidget; +use gooey::widgets::input::InputValue; +use gooey::widgets::Custom; +use gooey::Run; +use kludgine::app::winit::window::CursorIcon; + +fn main() -> gooey::Result { + Custom::new( + "Try hovering the mouse cursor around this window" + .and( + Dynamic::new(String::from("Input fields show the text selection cursor")) + .into_input(), + ) + .into_rows() + .pad() + .centered(), + ) + .on_hover(|_location, _context| Some(CursorIcon::Help)) + .on_hit_test(|_location, _context| true) + .contain() + .centered() + .expand() + .run() +} diff --git a/examples/focus.rs b/examples/focus.rs index df7df49..de9e0af 100644 --- a/examples/focus.rs +++ b/examples/focus.rs @@ -17,7 +17,7 @@ fn main() -> gooey::Result { .and("Allow Custom Widget to Lose Focus".into_checkbox(allow_blur.clone())) .and( Custom::empty() - .on_accept_focus(|_| true) + .on_accept_focus(|context| context.enabled()) .on_redraw(|context| { context.fill(context.theme().secondary.color); if context.focused() { diff --git a/examples/slider.rs b/examples/slider.rs index 03dc203..8ce52df 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -1,6 +1,7 @@ use gooey::animation::{LinearInterpolate, PercentBetween}; use gooey::value::{Dynamic, ForEach}; use gooey::widget::MakeWidget; +use gooey::widgets::checkbox::Checkable; use gooey::widgets::input::InputValue; use gooey::widgets::slider::Slidable; use gooey::Run; @@ -8,10 +9,14 @@ use kludgine::figures::units::Lp; use kludgine::figures::Ranged; fn main() -> gooey::Result { + let enabled = Dynamic::new(true); u8_slider() .and(u8_range_slider()) .and(enum_slider()) .into_rows() + .with_enabled(enabled.clone()) + .and(enabled.into_checkbox("Enabled")) + .into_rows() .expand_horizontally() .contain() .width(..Lp::points(800)) diff --git a/src/context.rs b/src/context.rs index c480c5f..04a0189 100644 --- a/src/context.rs +++ b/src/context.rs @@ -8,6 +8,7 @@ use kempt::Set; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size, Zero}; use kludgine::shapes::{Shape, StrokeOptions}; @@ -163,10 +164,17 @@ impl<'context, 'window> EventContext<'context, 'window> { let mut context = self.for_other(&unhovered); unhovered.lock().as_widget().unhover(&mut context); } - for hover in changes.hovered { + + let mut cursor = None; + for hover in changes.hovered.into_iter().rev() { let mut context = self.for_other(&hover); - hover.lock().as_widget().hover(location, &mut context); + let widget_cursor = hover.lock().as_widget().hover(location, &mut context); + + if cursor.is_none() { + cursor = widget_cursor; + } } + self.winit().set_cursor_icon(cursor.unwrap_or_default()); } pub(crate) fn clear_hover(&mut self) { @@ -177,6 +185,8 @@ impl<'context, 'window> EventContext<'context, 'window> { let mut old_hover_context = self.for_other(&old_hover); old_hover.lock().as_widget().unhover(&mut old_hover_context); } + + self.winit().set_cursor_icon(CursorIcon::Default); } fn apply_pending_activation(&mut self) { @@ -227,14 +237,10 @@ impl<'context, 'window> EventContext<'context, 'window> { fn apply_pending_focus(&mut self) { let mut focus_changes = 0; while focus_changes < Self::MAX_PENDING_CHANGE_CYCLES { - let focus = match self + let focus = self .pending_state .focus - .and_then(|w| self.current_node.tree.widget(w)) - { - Some(focus) => self.for_other(&focus).enabled().then_some(focus), - None => None, - }; + .and_then(|w| self.current_node.tree.widget(w)); if self.current_node.tree.focused_widget() == focus.as_ref().map(|w| w.node_id) { break; } @@ -242,8 +248,7 @@ impl<'context, 'window> EventContext<'context, 'window> { self.pending_state.focus = focus.and_then(|mut focus| loop { let mut focus_context = self.for_other(&focus); - let accept_focus = focus_context.enabled() - && focus.lock().as_widget().accept_focus(&mut focus_context); + let accept_focus = focus.lock().as_widget().accept_focus(&mut focus_context); drop(focus_context); if accept_focus { @@ -402,8 +407,7 @@ impl<'context, 'window> EventContext<'context, 'window> { } let mut child_context = self.for_other(&child); - let accept_focus = child_context.enabled() - && child.lock().as_widget().accept_focus(&mut child_context); + let accept_focus = child.lock().as_widget().accept_focus(&mut child_context); drop(child_context); if accept_focus { return Some(child.id()); diff --git a/src/styles/components.rs b/src/styles/components.rs index 13d6968..c13db05 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -129,6 +129,8 @@ define_components! { WidgetBackground(Color, "widget_backgrond_color", Color::CLEAR_WHITE) /// A [`Color`] to be used to accent a widget. WidgetAccentColor(Color, "widget_accent_color", .primary.color) + /// A [`Color`] to be used to accent a disabled widget. + DisabledWidgetAccentColor(Color, "disabled_widget_accent_color", .primary.color_dim) /// A [`Color`] to be used as an outline color. OutlineColor(Color, "outline_color", .surface.outline) /// A [`Color`] to be used as an outline color. diff --git a/src/widget.rs b/src/widget.rs index 4575dd5..43ff166 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -12,6 +12,7 @@ use alot::LotId; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::Color; @@ -68,7 +69,13 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { /// The widget is currently has a cursor hovering it at `location`. #[allow(unused_variables)] - fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) {} + fn hover( + &mut self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + None + } /// The widget is no longer being hovered. #[allow(unused_variables)] @@ -333,7 +340,13 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// The widget is currently has a cursor hovering it at `location`. #[allow(unused_variables)] - fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) {} + fn hover( + &mut self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + None + } /// The widget is no longer being hovered. #[allow(unused_variables)] @@ -500,8 +513,12 @@ where T::hit_test(self, location, context) } - fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) { - T::hover(self, location, context); + fn hover( + &mut self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + T::hover(self, location, context) } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 609883c..4f79c45 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -3,6 +3,7 @@ use std::panic::UnwindSafe; use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::{Shape, StrokeOptions}; @@ -383,7 +384,7 @@ impl Widget for Button { } fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { - context.get(&AutoFocusableControls).is_all() + context.enabled() && context.get(&AutoFocusableControls).is_all() } fn mouse_down( @@ -492,8 +493,18 @@ impl Widget for Button { self.update_colors(context, false); } - fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { + fn hover( + &mut self, + _location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { self.update_colors(context, false); + + if context.enabled() { + Some(CursorIcon::Pointer) + } else { + Some(CursorIcon::NotAllowed) + } } fn focus(&mut self, context: &mut EventContext<'_, '_>) { diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs index 3107f52..1f7b365 100644 --- a/src/widgets/custom.rs +++ b/src/widgets/custom.rs @@ -4,6 +4,7 @@ use std::panic::UnwindSafe; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::Px; use kludgine::figures::{Point, Size}; use kludgine::Color; @@ -36,7 +37,7 @@ pub struct Custom { adjust_child: Option>, position_child: Option>, hit_test: Option, bool>>>, - hover: Option>>>, + hover: Option, Option>>>, mouse_down: Option, DeviceId, MouseButton, EventHandling>>>, mouse_drag: Option, DeviceId, MouseButton>>>, @@ -366,7 +367,10 @@ impl Custom { Hover: Send + UnwindSafe + 'static - + for<'context, 'window> FnMut(Point, &mut EventContext<'context, 'window>), + + for<'context, 'window> FnMut( + Point, + &mut EventContext<'context, 'window>, + ) -> Option, { self.hover = Some(Box::new(hover)); self @@ -563,10 +567,13 @@ impl WrapperWidget for Custom { } } - fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) { - if let Some(hover) = &mut self.hover { - hover.invoke(location, context); - } + fn hover( + &mut self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + let hover = self.hover.as_mut()?; + hover.invoke(location, context) } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { diff --git a/src/widgets/input.rs b/src/widgets/input.rs index e230468..7c63bf4 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -12,7 +12,7 @@ use std::time::Duration; use intentional::Cast; use kludgine::app::winit::event::{ElementState, Ime, KeyEvent}; use kludgine::app::winit::keyboard::{Key, NamedKey}; -use kludgine::app::winit::window::ImePurpose; +use kludgine::app::winit::window::{CursorIcon, ImePurpose}; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, @@ -163,7 +163,11 @@ where }); } - fn forward_delete(&mut self) { + fn forward_delete(&mut self, context: &mut EventContext<'_, '_>) { + if !context.enabled() { + return; + } + let (cursor, selection) = self.selected_range(); if let Some(selection) = selection { self.replace_range(cursor, selection, ""); @@ -193,7 +197,11 @@ where }); } - fn delete(&mut self) { + fn delete(&mut self, context: &mut EventContext<'_, '_>) { + if !context.enabled() { + return; + } + let (cursor, selection) = self.selected_range(); if let Some(selection) = selection { self.replace_range(cursor, selection, ""); @@ -395,7 +403,11 @@ where }); } - fn replace_selection(&mut self, new_text: &str) { + fn replace_selection(&mut self, new_text: &str, context: &mut EventContext<'_, '_>) { + if !context.enabled() { + return; + } + let selected_range = self.selected_range(); match selected_range { (start, Some(end)) => { @@ -415,12 +427,16 @@ where } fn paste_from_clipboard(&mut self, context: &mut EventContext<'_, '_>) -> bool { + if !context.enabled() { + return false; + } + match context .clipboard_guard() .map(|mut clipboard| clipboard.get_text()) { Some(Ok(text)) => { - self.replace_selection(&text); + self.replace_selection(&text, context); true } None | Some(Err(arboard::Error::ConversionFailure)) => false, @@ -435,8 +451,8 @@ where match (input.state, input.logical_key, input.text.as_deref()) { (ElementState::Pressed, Key::Named(key @ (NamedKey::Backspace| NamedKey::Delete)), _) => { match key { - NamedKey::Backspace => self.delete(), - NamedKey::Delete => self.forward_delete(), + NamedKey::Backspace => self.delete(context), + NamedKey::Delete => self.forward_delete(context), _ => unreachable!("previously matched"), } @@ -505,7 +521,7 @@ where => { if state.is_pressed() { - self.replace_selection(text); + self.replace_selection(text, context); } HANDLED } @@ -914,6 +930,14 @@ where HANDLED } + fn hover( + &mut self, + _location: Point, + _context: &mut EventContext<'_, '_>, + ) -> Option { + Some(CursorIcon::Text) + } + fn mouse_drag( &mut self, location: Point, @@ -963,6 +987,7 @@ where } else { ImePurpose::Normal }); + if let Some(selection) = cache.selection { let (start, end) = if selection < cache.cursor { (selection, cache.cursor) @@ -1098,7 +1123,7 @@ where tracing::warn!("TODO: preview IME input {text}, cursor: {cursor:?}"); } Ime::Commit(text) => { - self.replace_selection(&text); + self.replace_selection(&text, context); context.set_needs_redraw(); } } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 622d9ff..08f1ed3 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -3,6 +3,7 @@ use std::time::Duration; use intentional::Cast; use kludgine::app::winit::event::{DeviceId, MouseScrollDelta, TouchPhase}; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, @@ -107,8 +108,14 @@ impl Widget for Scroll { true } - fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { + fn hover( + &mut self, + _location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { self.show_scrollbars(context); + + None } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index d198ed1..fd5cedc 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -7,6 +7,7 @@ use std::panic::UnwindSafe; use intentional::{Assert, Cast as _}; use kludgine::app::winit::event::{DeviceId, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::keyboard::{Key, NamedKey}; +use kludgine::app::winit::window::CursorIcon; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ FloatConversion, IntoSigned, Point, Ranged, Rect, Round, ScreenScale, Size, @@ -17,7 +18,8 @@ use kludgine::{Color, DrawableExt, Origin}; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::{ - AutoFocusableControls, OpaqueWidgetColor, OutlineColor, WidgetAccentColor, + AutoFocusableControls, DisabledWidgetAccentColor, OpaqueWidgetColor, OutlineColor, + WidgetAccentColor, }; use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder}; use crate::value::{Dynamic, IntoDynamic, IntoValue, Value}; @@ -420,9 +422,19 @@ where T: SliderValue, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - let track_color = context.get(&TrackColor); - let inactive_track_color = context.get(&InactiveTrackColor); - let knob_color = context.get(&KnobColor); + let (track_color, inactive_track_color, knob_color) = if context.enabled() { + ( + context.get(&TrackColor), + context.get(&InactiveTrackColor), + context.get(&KnobColor), + ) + } else { + ( + context.get(&DisabledTrackColor), + context.get(&DisabledInactiveTrackColor), + context.get(&DisabledKnobColor), + ) + }; let knob_size = self.knob_size.into_signed(); let mut track_size = context.get(&TrackSize).into_px(context.gfx.scale()); if knob_size > 0 { @@ -548,8 +560,29 @@ where self.interactive } + fn hover( + &mut self, + _location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + (self.interactive && self.knob_visible).then_some({ + if context.enabled() { + if self.mouse_buttons_down > 0 { + CursorIcon::Grabbing + } else { + CursorIcon::Grab + } + } else { + CursorIcon::NotAllowed + } + }) + } + fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { - self.interactive && self.knob_visible && context.get(&AutoFocusableControls).is_all() + context.enabled() + && self.interactive + && self.knob_visible + && context.get(&AutoFocusableControls).is_all() } fn focus(&mut self, context: &mut EventContext<'_, '_>) { @@ -606,13 +639,15 @@ where return IGNORED; }; - let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) { - (None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus), - (None, None) => None, - }; - self.update_from_click(location, previous_focus); + if context.enabled() { + let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) { + (None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus), + (None, None) => None, + }; + self.update_from_click(location, previous_focus); + context.focus(); + } self.mouse_buttons_down += 1; - context.focus(); HANDLED } @@ -621,9 +656,11 @@ where location: Point, _device_id: DeviceId, _button: MouseButton, - _context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_, '_>, ) { - self.update_from_click(location, None); + if context.enabled() { + self.update_from_click(location, None); + } } fn mouse_up( @@ -666,24 +703,26 @@ where _device_id: DeviceId, delta: MouseScrollDelta, _phase: TouchPhase, - _context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_, '_>, ) -> EventHandling { let true = self.interactive else { return IGNORED; }; - let factor: f32 = match delta { - MouseScrollDelta::LineDelta(_, y) => y, - MouseScrollDelta::PixelDelta(pt) => pt.y.cast(), - }; + if context.enabled() { + let factor: f32 = match delta { + MouseScrollDelta::LineDelta(_, y) => y, + MouseScrollDelta::PixelDelta(pt) => pt.y.cast(), + }; - let (forwards, factor) = if factor.is_sign_negative() { - (false, -factor) - } else { - (true, factor) - }; + let (forwards, factor) = if factor.is_sign_negative() { + (false, -factor) + } else { + (true, factor) + }; - self.step(forwards, factor); + self.step(forwards, factor); + } // @ecton: Unlike scroll alreas cascasing, I feel like scrolling while // using a mouse wheel as an input is annoying. @@ -726,10 +765,16 @@ define_components! { MinimumSliderSize(Dimension, "minimum_size", |context| context.get(&KnobSize) * 2) /// The color of the draggable portion of the knob. KnobColor(Color, "knob_color", @WidgetAccentColor) + /// The color of the draggable portion of the knob. + DisabledKnobColor(Color, "disabled_knob_color", @DisabledWidgetAccentColor) /// The color of the track that the knob rests on. TrackColor(Color,"track_color", |context| context.get(&KnobColor)) - /// The color of the track that the knob rests on. + /// The color of the track that the knob rests on when the widget is disabled. + DisabledTrackColor(Color,"track_color", |context| context.get(&DisabledKnobColor)) + /// The color of the track that the knob rests. InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor)) + /// The color of the track that the knob rests. + DisabledInactiveTrackColor(Color, "disabled_inactive_track_color", |context| context.get(&OpaqueWidgetColor)) } }