diff --git a/crates/eframe/src/epi/mod.rs b/crates/eframe/src/epi/mod.rs index 524ae7306..2933f1fba 100644 --- a/crates/eframe/src/epi/mod.rs +++ b/crates/eframe/src/epi/mod.rs @@ -140,13 +140,8 @@ pub trait App { /// Only called when the "persistence" feature is enabled. /// /// On web the state is stored to "Local Storage". - /// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: - /// * Linux: `/home/UserName/.local/share/APP_ID` - /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` - /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` /// - /// where `APP_ID` is determined by either [`NativeOptions::app_id`] or - /// the title argument to [`crate::run_native`]. + /// On native the path is picked using [`crate::storage_dir`]. fn save(&mut self, _storage: &mut dyn Storage) {} /// Called when the user attempts to close the desktop window and/or quit the application. @@ -423,13 +418,10 @@ pub struct NativeOptions { /// The application id, used for determining the folder to persist the app to. /// - /// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: - /// * Linux: `/home/UserName/.local/share/APP_ID` - /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` - /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` + /// On native the path is picked using [`crate::storage_dir`]. /// /// If you don't set [`Self::app_id`], the title argument to [`crate::run_native`] - /// will be used instead. + /// will be used as app id instead. /// /// ### On Wayland /// On Wayland this sets the Application ID for the window. diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 44e7bdb3c..5bf0a57fc 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -159,6 +159,11 @@ pub use web::{WebLogger, WebRunner}; #[cfg(any(feature = "glow", feature = "wgpu"))] mod native; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +#[cfg(feature = "persistence")] +pub use native::file_storage::storage_dir; + /// This is how you start a native (desktop) app. /// /// The first argument is name of your app, used for the title bar of the native window diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 2cc3ac1ea..93189da4c 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -305,7 +305,7 @@ pub fn handle_app_output( /// For loading/saving app state and/or egui memory to disk. pub fn create_storage(_app_name: &str) -> Option> { #[cfg(feature = "persistence")] - if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) { + if let Some(storage) = super::file_storage::FileStorage::from_app_id(_app_name) { return Some(Box::new(storage)); } None diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 2f316a8c9..eae742cff 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -3,6 +3,20 @@ use std::{ path::{Path, PathBuf}, }; +/// The folder where `eframe` will store its state. +/// +/// The given `app_id` is either [`crate::NativeOptions::app_id`] or +/// the title argument to [`crate::run_native`]. +/// +/// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: +/// * Linux: `/home/UserName/.local/share/APP_ID` +/// * macOS: `/Users/UserName/Library/Application Support/APP_ID` +/// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` +pub fn storage_dir(app_id: &str) -> Option { + directories_next::ProjectDirs::from("", "", app_id) + .map(|proj_dirs| proj_dirs.data_dir().to_path_buf()) +} + // ---------------------------------------------------------------------------- /// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk. @@ -24,7 +38,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. - pub fn from_ron_filepath(ron_filepath: impl Into) -> Self { + fn from_ron_filepath(ron_filepath: impl Into) -> Self { let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -36,9 +50,8 @@ impl FileStorage { } /// Find a good place to put the files that the OS likes. - pub fn from_app_name(app_name: &str) -> Option { - if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) { - let data_dir = proj_dirs.data_dir().to_path_buf(); + pub fn from_app_id(app_id: &str) -> Option { + if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( "Saving disabled: Failed to create app path at {:?}: {}", diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index f324bae82..6fc6bdf8e 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -815,7 +815,7 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option Key::Escape, VirtualKeyCode::Tab => Key::Tab, VirtualKeyCode::Back => Key::Backspace, - VirtualKeyCode::Return => Key::Enter, + VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => Key::Enter, VirtualKeyCode::Space => Key::Space, VirtualKeyCode::Insert => Key::Insert, @@ -825,10 +825,12 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option Key::PageUp, VirtualKeyCode::PageDown => Key::PageDown, - VirtualKeyCode::Minus => Key::Minus, + VirtualKeyCode::Minus | VirtualKeyCode::NumpadSubtract => Key::Minus, // Using Mac the key with the Plus sign on it is reported as the Equals key // (with both English and Swedish keyboard). - VirtualKeyCode::Equals => Key::PlusEquals, + VirtualKeyCode::Equals | VirtualKeyCode::Plus | VirtualKeyCode::NumpadAdd => { + Key::PlusEquals + } VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0, VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1, diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index e2ea2f630..73b54efba 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,6 +1,6 @@ -use ahash::HashMap; - use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId}; +use ahash::HashMap; +use epaint::{emath::Rangef, vec2, Vec2}; // ---------------------------------------------------------------------------- @@ -108,6 +108,44 @@ pub struct Memory { everything_is_visible: bool, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum FocusDirection { + /// Select the widget closest above the current focused widget. + Up, + + /// Select the widget to the right of the current focused widget. + Right, + + /// Select the widget below the current focused widget. + Down, + + /// Select the widget to the left of the the current focused widget. + Left, + + /// Select the previous widget that had focus. + Previous, + + /// Select the next widget that wants focus. + Next, + + /// Don't change focus. + #[default] + None, +} + +impl FocusDirection { + fn is_cardinal(&self) -> bool { + match self { + FocusDirection::Up + | FocusDirection::Right + | FocusDirection::Down + | FocusDirection::Left => true, + + FocusDirection::Previous | FocusDirection::Next | FocusDirection::None => false, + } + } +} + // ---------------------------------------------------------------------------- /// Some global options that you can read and write. @@ -219,11 +257,11 @@ pub(crate) struct Focus { /// If `true`, pressing tab will NOT move focus away from the current widget. is_focus_locked: bool, - /// Set at the beginning of the frame, set to `false` when "used". - pressed_tab: bool, + /// Set when looking for widget with navigational keys like arrows, tab, shift+tab + focus_direction: FocusDirection, - /// Set at the beginning of the frame, set to `false` when "used". - pressed_shift_tab: bool, + /// A cache of widget ids that are interested in focus with their corresponding rectangles. + focus_widgets_cache: IdMap, } impl Interaction { @@ -271,36 +309,40 @@ impl Focus { self.id_requested_by_accesskit = None; } - self.pressed_tab = false; - self.pressed_shift_tab = false; - for event in &new_input.events { - if matches!( - event, - crate::Event::Key { - key: crate::Key::Escape, - pressed: true, - modifiers: _, - .. - } - ) { - self.id = None; - self.is_focus_locked = false; - break; - } + self.focus_direction = FocusDirection::None; + for event in &new_input.events { if let crate::Event::Key { - key: crate::Key::Tab, + key, pressed: true, modifiers, .. } = event { - if !self.is_focus_locked { - if modifiers.shift { - self.pressed_shift_tab = true; - } else { - self.pressed_tab = true; + if let Some(cardinality) = match key { + crate::Key::ArrowUp => Some(FocusDirection::Up), + crate::Key::ArrowRight => Some(FocusDirection::Right), + crate::Key::ArrowDown => Some(FocusDirection::Down), + crate::Key::ArrowLeft => Some(FocusDirection::Left), + crate::Key::Tab => { + if !self.is_focus_locked { + if modifiers.shift { + Some(FocusDirection::Previous) + } else { + Some(FocusDirection::Next) + } + } else { + None + } } + crate::Key::Escape => { + self.id = None; + self.is_focus_locked = false; + Some(FocusDirection::None) + } + _ => None, + } { + self.focus_direction = cardinality; } } @@ -319,6 +361,12 @@ impl Focus { } pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { + if self.focus_direction.is_cardinal() { + if let Some(found_widget) = self.find_widget_in_direction(used_ids) { + self.id = Some(found_widget); + } + } + if let Some(id) = self.id { // Allow calling `request_focus` one frame and not using it until next frame let recently_gained_focus = self.id_previous_frame != Some(id); @@ -341,31 +389,119 @@ impl Focus { self.id = Some(id); self.id_requested_by_accesskit = None; self.give_to_next = false; - self.pressed_tab = false; - self.pressed_shift_tab = false; + self.reset_focus(); } } + // The rect is updated at the end of the frame. + self.focus_widgets_cache + .entry(id) + .or_insert(Rect::EVERYTHING); + if self.give_to_next && !self.had_focus_last_frame(id) { self.id = Some(id); self.give_to_next = false; } else if self.id == Some(id) { - if self.pressed_tab && !self.is_focus_locked { + if self.focus_direction == FocusDirection::Next && !self.is_focus_locked { self.id = None; self.give_to_next = true; - self.pressed_tab = false; - } else if self.pressed_shift_tab && !self.is_focus_locked { + self.reset_focus(); + } else if self.focus_direction == FocusDirection::Previous && !self.is_focus_locked { self.id_next_frame = self.last_interested; // frame-delay so gained_focus works - self.pressed_shift_tab = false; + self.reset_focus(); } - } else if self.pressed_tab && self.id.is_none() && !self.give_to_next { + } else if self.focus_direction == FocusDirection::Next + && self.id.is_none() + && !self.give_to_next + { // nothing has focus and the user pressed tab - give focus to the first widgets that wants it: self.id = Some(id); - self.pressed_tab = false; + self.reset_focus(); } self.last_interested = Some(id); } + + fn reset_focus(&mut self) { + self.focus_direction = FocusDirection::None; + } + + fn find_widget_in_direction(&mut self, new_rects: &IdMap) -> Option { + // NOTE: `new_rects` here include some widgets _not_ interested in focus. + + /// * negative if `a` is left of `b` + /// * positive if `a` is right of `b` + /// * zero if the ranges overlap significantly + fn range_diff(a: Rangef, b: Rangef) -> f32 { + let has_significant_overlap = a.intersection(b).span() >= 0.5 * b.span().min(a.span()); + if has_significant_overlap { + 0.0 + } else { + a.center() - b.center() + } + } + + let Some(focus_id) = self.id else { + return None; + }; + + // In what direction we are looking for the next widget. + let search_direction = match self.focus_direction { + FocusDirection::Up => Vec2::UP, + FocusDirection::Right => Vec2::RIGHT, + FocusDirection::Down => Vec2::DOWN, + FocusDirection::Left => Vec2::LEFT, + _ => { + return None; + } + }; + + // Update cache with new rects + self.focus_widgets_cache.retain(|id, old_rect| { + if let Some(new_rect) = new_rects.get(id) { + *old_rect = *new_rect; + true // Keep the item + } else { + false // Remove the item + } + }); + + let current_rect = *self.focus_widgets_cache.get(&focus_id).unwrap(); + + let mut best_score = std::f32::INFINITY; + let mut best_id = None; + + for (candidate_id, candidate_rect) in &self.focus_widgets_cache { + if Some(*candidate_id) == self.id { + continue; + } + + // There is a lot of room for improvement here. + let to_candidate = vec2( + range_diff(candidate_rect.x_range(), current_rect.x_range()), + range_diff(candidate_rect.y_range(), current_rect.y_range()), + ); + + let acos_angle = to_candidate.normalized().dot(search_direction); + + // Only interested in widgets that fall in a 90° cone (±45°) + // of the search direction. + let is_in_search_cone = 0.5_f32.sqrt() <= acos_angle; + if is_in_search_cone { + let distance = to_candidate.length(); + + // There is a lot of room for improvement here. + let score = distance / (acos_angle * acos_angle); + + if score < best_score { + best_score = score; + best_id = Some(*candidate_id); + } + } + } + + best_id + } } impl Memory { diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 08a6ebd15..835a75d19 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -126,6 +126,7 @@ struct PlotMemory { last_click_pos_for_zoom: Option, } +#[cfg(feature = "serde")] impl PlotMemory { pub fn load(ctx: &Context, id: Id) -> Option { ctx.data_mut(|d| d.get_persisted(id)) @@ -136,6 +137,17 @@ impl PlotMemory { } } +#[cfg(not(feature = "serde"))] +impl PlotMemory { + pub fn load(ctx: &Context, id: Id) -> Option { + ctx.data_mut(|d| d.get_temp(id)) + } + + pub fn store(self, ctx: &Context, id: Id) { + ctx.data_mut(|d| d.insert_temp(id, self)); + } +} + // ---------------------------------------------------------------------------- /// Indicates a vertical or horizontal cursor line in plot coordinates. diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index 11459763d..b7b975c45 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -49,6 +49,12 @@ impl Rangef { self.max - self.min } + /// The center of the range + #[inline] + pub fn center(self) -> f32 { + 0.5 * (self.min + self.max) + } + #[inline] #[must_use] pub fn contains(self, x: f32) -> bool { @@ -90,6 +96,25 @@ impl Rangef { max: self.max + amnt, } } + + /// The overlap of two ranges, i.e. the range that is contained by both. + /// + /// If the ranges do not overlap, returns a range with `span() < 0.0`. + /// + /// ``` + /// # use emath::Rangef; + /// assert_eq!(Rangef::new(0.0, 10.0).intersection(Rangef::new(5.0, 15.0)), Rangef::new(5.0, 10.0)); + /// assert_eq!(Rangef::new(0.0, 10.0).intersection(Rangef::new(10.0, 20.0)), Rangef::new(10.0, 10.0)); + /// assert!(Rangef::new(0.0, 10.0).intersection(Rangef::new(20.0, 30.0)).span() < 0.0); + /// ``` + #[inline] + #[must_use] + pub fn intersection(self, other: Self) -> Self { + Self { + min: self.min.max(other.min), + max: self.max.min(other.max), + } + } } impl From for RangeInclusive {