mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 23:13:13 -04:00
Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Box<dyn epi::Storage>> {
|
||||
#[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
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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<PathBuf>) -> Self {
|
||||
fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> 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<Self> {
|
||||
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<Self> {
|
||||
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 {:?}: {}",
|
||||
|
||||
@@ -815,7 +815,7 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
|
||||
VirtualKeyCode::Escape => 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<egui:
|
||||
VirtualKeyCode::PageUp => 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,
|
||||
|
||||
@@ -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<Rect>,
|
||||
}
|
||||
|
||||
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<Rect>) {
|
||||
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<Rect>) -> Option<Id> {
|
||||
// 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 {
|
||||
|
||||
@@ -126,6 +126,7 @@ struct PlotMemory {
|
||||
last_click_pos_for_zoom: Option<Pos2>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl PlotMemory {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
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<Self> {
|
||||
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.
|
||||
|
||||
@@ -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<Rangef> for RangeInclusive<f32> {
|
||||
|
||||
Reference in New Issue
Block a user