diff --git a/CHANGELOG.md b/CHANGELOG.md index 03412458..156d88ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)). * Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)). * You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)). +* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)). ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f42690ee..99cb9f05 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -621,6 +621,8 @@ impl Context { ) -> Response { let hovered = hovered && enabled; // can't even hover disabled widgets + let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id)); + let mut response = Response { ctx: self.clone(), layer_id, @@ -629,6 +631,7 @@ impl Context { sense, enabled, hovered, + highlighted, clicked: Default::default(), double_clicked: Default::default(), triple_clicked: Default::default(), @@ -1284,6 +1287,15 @@ impl Context { pub fn wants_keyboard_input(&self) -> bool { self.memory(|m| m.interaction.focus.focused().is_some()) } + + /// Highlight this widget, to make it look like it is hovered, even if it isn't. + /// + /// The highlight takes on frame to take effect if you call this after the widget has been fully rendered. + /// + /// See also [`Response::highlight`]. + pub fn highlight_widget(&self, id: Id) { + self.frame_state_mut(|fs| fs.highlight_next_frame.insert(id)); + } } // Ergonomic methods to forward some calls often used in 'if let' without holding the borrow diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 73dcfd4c..715e8262 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -1,6 +1,6 @@ use std::ops::RangeInclusive; -use crate::*; +use crate::{id::IdSet, *}; #[derive(Clone, Copy, Debug)] pub(crate) struct TooltipFrameState { @@ -51,6 +51,12 @@ pub(crate) struct FrameState { #[cfg(feature = "accesskit")] pub(crate) accesskit_state: Option, + + /// Highlight these widgets this next frame. Read from this. + pub(crate) highlight_this_frame: IdSet, + + /// Highlight these widgets the next frame. Write to this. + pub(crate) highlight_next_frame: IdSet, } impl Default for FrameState { @@ -65,6 +71,8 @@ impl Default for FrameState { scroll_target: [None, None], #[cfg(feature = "accesskit")] accesskit_state: None, + highlight_this_frame: Default::default(), + highlight_next_frame: Default::default(), } } } @@ -81,6 +89,8 @@ impl FrameState { scroll_target, #[cfg(feature = "accesskit")] accesskit_state, + highlight_this_frame, + highlight_next_frame, } = self; used_ids.clear(); @@ -90,10 +100,13 @@ impl FrameState { *tooltip_state = None; *scroll_delta = input.scroll_delta; *scroll_target = [None, None]; + #[cfg(feature = "accesskit")] { *accesskit_state = None; } + + *highlight_this_frame = std::mem::take(highlight_next_frame); } /// How much space is still available after panels has been added. diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index fee5e259..b0fe41fd 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -168,5 +168,8 @@ impl std::hash::BuildHasher for BuilIdHasher { } } +/// `IdSet` is a `HashSet` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. +pub type IdSet = std::collections::HashSet; + /// `IdMap` is a `HashMap` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. pub type IdMap = std::collections::HashMap; diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 7c0a743b..b3d79906 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -13,6 +13,7 @@ use crate::{ /// /// Whenever something gets added to a [`Ui`], a [`Response`] object is returned. /// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts. +// TODO(emilk): we should be using bit sets instead of so many bools #[derive(Clone)] pub struct Response { // CONTEXT: @@ -42,6 +43,10 @@ pub struct Response { #[doc(hidden)] pub hovered: bool, + /// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`]. + #[doc(hidden)] + pub highlighted: bool, + /// The pointer clicked this thing this frame. #[doc(hidden)] pub clicked: [bool; NUM_POINTER_BUTTONS], @@ -90,6 +95,7 @@ impl std::fmt::Debug for Response { sense, enabled, hovered, + highlighted, clicked, double_clicked, triple_clicked, @@ -106,6 +112,7 @@ impl std::fmt::Debug for Response { .field("sense", sense) .field("enabled", enabled) .field("hovered", hovered) + .field("highlighted", highlighted) .field("clicked", clicked) .field("double_clicked", double_clicked) .field("triple_clicked", triple_clicked) @@ -213,6 +220,12 @@ impl Response { self.hovered } + /// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`]. + #[doc(hidden)] + pub fn highlighted(&self) -> bool { + self.highlighted + } + /// This widget has the keyboard focus (i.e. is receiving key presses). /// /// This function only returns true if the UI as a whole (e.g. window) @@ -454,6 +467,17 @@ impl Response { }) } + /// Highlight this widget, to make it look like it is hovered, even if it isn't. + /// + /// The highlight takes on frame to take effect if you call this after the widget has been fully rendered. + /// + /// See also [`Context::highlight_widget`]. + pub fn highlight(mut self) -> Self { + self.ctx.highlight_widget(self.id); + self.highlighted = true; + self + } + /// Show this text when hovering if the widget is disabled. pub fn on_disabled_hover_text(self, text: impl Into) -> Self { self.on_disabled_hover_ui(|ui| { @@ -688,6 +712,7 @@ impl Response { sense: self.sense.union(other.sense), enabled: self.enabled || other.enabled, hovered: self.hovered || other.hovered, + highlighted: self.highlighted || other.highlighted, clicked: [ self.clicked[0] || other.clicked[0], self.clicked[1] || other.clicked[1], diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 08334cc3..2c1175ee 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -576,7 +576,9 @@ pub struct Widgets { /// The style of an interactive widget, such as a button, at rest. pub inactive: WidgetVisuals, - /// The style of an interactive widget while you hover it. + /// The style of an interactive widget while you hover it, or when it is highlighted. + /// + /// See [`Response::hovered`], [`Response::highlighted`] and [`Response::highlight`]. pub hovered: WidgetVisuals, /// The style of an interactive widget as you are clicking or dragging it. @@ -592,7 +594,7 @@ impl Widgets { &self.noninteractive } else if response.is_pointer_button_down_on() || response.has_focus() { &self.active - } else if response.hovered() { + } else if response.hovered() | response.highlighted() { &self.hovered } else { &self.inactive diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 03eb2a13..3e387fca 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -173,7 +173,7 @@ impl Widget for Label { if ui.is_rect_visible(response.rect) { let response_color = ui.style().interact(&response).text_color(); - let underline = if response.has_focus() { + let underline = if response.has_focus() || response.highlighted() { Stroke::new(1.0, response_color) } else { Stroke::NONE diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index ca9cc3be..31a28ae8 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -90,6 +90,7 @@ impl Default for Tests { fn default() -> Self { Self::from_demos(vec![ Box::new(super::tests::CursorTest::default()), + Box::new(super::highlighting::Highlighting::default()), Box::new(super::tests::IdTest::default()), Box::new(super::tests::InputTest::default()), Box::new(super::layout_test::LayoutTest::default()), diff --git a/crates/egui_demo_lib/src/demo/highlighting.rs b/crates/egui_demo_lib/src/demo/highlighting.rs new file mode 100644 index 00000000..24cf21c5 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/highlighting.rs @@ -0,0 +1,37 @@ +#[derive(Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct Highlighting {} + +impl super::Demo for Highlighting { + fn name(&self) -> &'static str { + "Highlighting" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .default_width(320.0) + .open(open) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for Highlighting { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.label("This demo demonstrates highlighting a widget."); + ui.add_space(4.0); + let label_response = ui.label("Hover me to highlight the button!"); + ui.add_space(4.0); + let mut button_response = ui.button("Hover the button to highlight the label!"); + + if label_response.hovered() { + button_response = button_response.highlight(); + } + if button_response.hovered() { + label_response.highlight(); + } + } +} diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 53893567..dddf215d 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -12,6 +12,7 @@ pub mod dancing_strings; pub mod demo_app_windows; pub mod drag_and_drop; pub mod font_book; +pub mod highlighting; pub mod layout_test; pub mod misc_demo_window; pub mod multi_touch;