egui/crates/egui/src/memory.rs
Matt Campbell 0336816faf
Fix keyboard support in DragValue (#2342)
* Enable incrementing and decrementing `DragValue` with the keyboard

* As soon as a DragValue is focused, render it in edit mode

* Simpler, more reliable approach to managing the drag value's edit string

* Add changelog entry

* Update doc comment

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Add comment explaining why we don't listen for left/right arrow

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-11-29 15:27:14 +01:00

603 lines
21 KiB
Rust

use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
// ----------------------------------------------------------------------------
/// The data that egui persists between frames.
///
/// This includes window positions and sizes,
/// how far the user has scrolled in a [`ScrollArea`](crate::ScrollArea) etc.
///
/// If you want this to persist when closing your app you should serialize [`Memory`] and store it.
/// For this you need to enable the `persistence`.
///
/// If you want to store data for your widgets, you should look at [`Memory::data`]
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Memory {
pub options: Options,
/// This map stores some superficial state for all widgets with custom [`Id`]s.
///
/// This includes storing if a [`crate::CollapsingHeader`] is open, how far scrolled a
/// [`crate::ScrollArea`] is, where the cursor in a [`crate::TextEdit`] is, etc.
///
/// This is NOT meant to store any important data. Store that in your own structures!
///
/// Each read clones the data, so keep your values cheap to clone.
/// If you want to store a lot of data you should wrap it in `Arc<Mutex<…>>` so it is cheap to clone.
///
/// This will be saved between different program runs if you use the `persistence` feature.
///
/// To store a state common for all your widgets (a singleton), use [`Id::null`] as the key.
pub data: crate::util::IdTypeMap,
// ------------------------------------------
/// Can be used to cache computations from one frame to another.
///
/// This is for saving CPU when you have something that may take 1-100ms to compute.
/// Things that are very slow (>100ms) should instead be done async (i.e. in another thread)
/// so as not to lock the UI thread.
///
/// ```
/// use egui::util::cache::{ComputerMut, FrameCache};
///
/// #[derive(Default)]
/// struct CharCounter {}
/// impl ComputerMut<&str, usize> for CharCounter {
/// fn compute(&mut self, s: &str) -> usize {
/// s.chars().count() // you probably want to cache something more expensive than this
/// }
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut ctx = egui::Context::default();
/// let mut memory = ctx.memory();
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ```
#[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage,
// ------------------------------------------
/// new scale that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_pixels_per_point: Option<f32>,
/// new fonts that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_font_definitions: Option<epaint::text::FontDefinitions>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) interaction: Interaction,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) window_interaction: Option<window::WindowInteraction>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) drag_value: crate::widgets::drag_value::MonoState,
pub(crate) areas: Areas,
/// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu etc.
#[cfg_attr(feature = "persistence", serde(skip))]
popup: Option<Id>,
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
}
// ----------------------------------------------------------------------------
/// Some global options that you can read and write.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Options {
/// The default style for new [`Ui`](crate::Ui):s.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) style: std::sync::Arc<Style>,
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
/// This does not at all change the behavior of egui,
/// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
pub screen_reader: bool,
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
///
/// Only the fonts in [`Style::text_styles`] will be pre-cached.
///
/// This can lead to fewer texture operations, but may use up the texture atlas quicker
/// if you are changing [`Style::text_styles`], of have a lot of text styles.
pub preload_font_glyphs: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
style: Default::default(),
tessellation_options: Default::default(),
screen_reader: false,
preload_font_glyphs: true,
}
}
}
// ----------------------------------------------------------------------------
/// Say there is a button in a scroll area.
/// If the user clicks the button, the button should click.
/// If the user drags the button we should scroll the scroll area.
/// So what we do is that when the mouse is pressed we register both the button
/// and the scroll area (as `click_id`/`drag_id`).
/// If the user releases the button without moving the mouse we register it as a click on `click_id`.
/// If the cursor moves too much we clear the `click_id` and start passing move events to `drag_id`.
#[derive(Clone, Debug, Default)]
pub(crate) struct Interaction {
/// A widget interested in clicks that has a mouse press on it.
pub click_id: Option<Id>,
/// A widget interested in drags that has a mouse press on it.
pub drag_id: Option<Id>,
pub focus: Focus,
/// HACK: windows have low priority on dragging.
/// This is so that if you drag a slider in a window,
/// the slider will steal the drag away from the window.
/// This is needed because we do window interaction first (to prevent frame delay),
/// and then do content layout.
pub drag_is_window: bool,
/// Any interest in catching clicks this frame?
/// Cleared to false at start of each frame.
pub click_interest: bool,
/// Any interest in catching clicks this frame?
/// Cleared to false at start of each frame.
pub drag_interest: bool,
}
/// Keeps tracks of what widget has keyboard focus
#[derive(Clone, Debug, Default)]
pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field).
id: Option<Id>,
/// What had keyboard focus previous frame?
id_previous_frame: Option<Id>,
/// Give focus to this widget next frame
id_next_frame: Option<Id>,
/// If set, the next widget that is interested in focus will automatically get it.
/// Probably because the user pressed Tab.
give_to_next: bool,
/// The last widget interested in focus.
last_interested: Option<Id>,
/// 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 at the beginning of the frame, set to `false` when "used".
pressed_shift_tab: bool,
}
impl Interaction {
/// Are we currently clicking or dragging an egui widget?
pub fn is_using_pointer(&self) -> bool {
self.click_id.is_some() || self.drag_id.is_some()
}
fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
) {
self.click_interest = false;
self.drag_interest = false;
if !prev_input.pointer.could_any_button_be_click() {
self.click_id = None;
}
if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
// pointer button was not down last frame
self.click_id = None;
self.drag_id = None;
}
self.focus.begin_frame(new_input);
}
}
impl Focus {
/// Which widget currently has keyboard focus?
pub fn focused(&self) -> Option<Id> {
self.id
}
fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
self.id_previous_frame = self.id;
if let Some(id) = self.id_next_frame.take() {
self.id = Some(id);
}
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;
}
if let crate::Event::Key {
key: crate::Key::Tab,
pressed: true,
modifiers,
} = event
{
if !self.is_focus_locked {
if modifiers.shift {
self.pressed_shift_tab = true;
} else {
self.pressed_tab = true;
}
}
}
}
}
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
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);
if !recently_gained_focus && !used_ids.contains_key(&id) {
// Dead-mans-switch: the widget with focus has disappeared!
self.id = None;
}
}
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.id_previous_frame == Some(id)
}
fn interested_in_focus(&mut self, id: Id) {
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 {
self.id = None;
self.give_to_next = true;
self.pressed_tab = false;
} else if self.pressed_shift_tab && !self.is_focus_locked {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.pressed_shift_tab = false;
}
} else if self.pressed_tab && 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.last_interested = Some(id);
}
}
impl Memory {
pub(crate) fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
) {
self.interaction.begin_frame(prev_input, new_input);
if !prev_input.pointer.any_down() {
self.window_interaction = None;
}
}
pub(crate) fn end_frame(&mut self, input: &InputState, used_ids: &IdMap<Rect>) {
self.caches.update();
self.areas.end_frame();
self.interaction.focus.end_frame(used_ids);
self.drag_value.end_frame(input);
}
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
self.areas.layer_id_at(pos, resize_interact_radius_side)
}
/// An iterator over all layers. Back-to-front. Top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas.order().iter().copied()
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.interaction.focus.id_previous_frame == Some(id)
}
/// True if the given widget had keyboard focus last frame, but not this one.
pub(crate) fn lost_focus(&self, id: Id) -> bool {
self.had_focus_last_frame(id) && !self.has_focus(id)
}
/// True if the given widget has keyboard focus this frame, but didn't last frame.
pub(crate) fn gained_focus(&self, id: Id) -> bool {
!self.had_focus_last_frame(id) && self.has_focus(id)
}
/// Does this widget have keyboard focus?
///
/// This function does not consider whether the UI as a whole (e.g. window)
/// has the keyboard focus. That makes this function suitable for deciding
/// widget state that should not be disrupted if the user moves away
/// from the window and back.
#[inline(always)]
pub fn has_focus(&self, id: Id) -> bool {
self.interaction.focus.id == Some(id)
}
/// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> {
self.interaction.focus.id
}
/// Prevent keyboard focus from moving away from this widget even if users presses the tab key.
/// You must first give focus to the widget before calling this.
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked = lock_focus;
}
}
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&mut self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {
false
}
}
/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.id = Some(id);
self.interaction.focus.is_focus_locked = false;
}
/// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`].
#[inline(always)]
pub fn surrender_focus(&mut self, id: Id) {
if self.interaction.focus.id == Some(id) {
self.interaction.focus.id = None;
self.interaction.focus.is_focus_locked = false;
}
}
/// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab.
/// This is normally done automatically when handling interactions,
/// but it is sometimes useful to pre-register interest in focus,
/// e.g. before deciding which type of underlying widget to use,
/// as in the [`crate::DragValue`] widget, so a widget can be focused
/// and rendered correctly in a single frame.
#[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) {
self.interaction.focus.interested_in_focus(id);
}
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
self.interaction.focus.id = None;
}
#[inline(always)]
pub fn is_anything_being_dragged(&self) -> bool {
self.interaction.drag_id.is_some()
}
#[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction.drag_id == Some(id)
}
#[inline(always)]
pub fn set_dragged_id(&mut self, id: Id) {
self.interaction.drag_id = Some(id);
}
/// Forget window positions, sizes etc.
/// Can be used to auto-layout windows.
pub fn reset_areas(&mut self) {
self.areas = Default::default();
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be be open at a time.
impl Memory {
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popup == Some(popup_id) || self.everything_is_visible()
}
pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some(popup_id);
}
pub fn close_popup(&mut self) {
self.popup = None;
}
pub fn toggle_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.close_popup();
} else {
self.open_popup(popup_id);
}
}
/// If true, all windows, menus, tooltips etc are to be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
///
/// Experimental feature!
#[inline(always)]
pub fn everything_is_visible(&self) -> bool {
self.everything_is_visible
}
/// If true, all windows, menus, tooltips etc are to be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
///
/// Experimental feature!
pub fn set_everything_is_visible(&mut self, value: bool) {
self.everything_is_visible = value;
}
}
// ----------------------------------------------------------------------------
/// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s.
/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`](crate::Order).
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Areas {
areas: IdMap<area::State>,
/// Back-to-front. Top is last.
order: Vec<LayerId>,
visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,
/// When an area want to be on top, it is put in here.
/// At the end of the frame, this is used to reorder the layers.
/// This means if several layers want to be on top, they will keep their relative order.
/// So if you close three windows and then reopen them all in one frame,
/// they will all be sent to the top, but keep their previous internal order.
wants_to_be_on_top: ahash::HashSet<LayerId>,
}
impl Areas {
pub(crate) fn count(&self) -> usize {
self.areas.len()
}
pub(crate) fn get(&self, id: Id) -> Option<&area::State> {
self.areas.get(&id)
}
/// Back-to-front. Top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
}
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::State) {
self.visible_current_frame.insert(layer_id);
self.areas.insert(layer_id.id, state);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
}
}
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
for layer in self.order.iter().rev() {
if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) {
let mut rect = state.rect();
if state.interactable {
// Allow us to resize by dragging just outside the window:
rect = rect.expand(resize_interact_radius_side);
if rect.contains(pos) {
return Some(*layer);
}
}
}
}
}
None
}
pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id)
}
pub fn is_visible(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame
.iter()
.cloned()
.chain(self.visible_current_frame.iter().cloned())
.collect()
}
pub(crate) fn visible_windows(&self) -> Vec<&area::State> {
self.visible_layer_ids()
.iter()
.filter(|layer| layer.order == crate::Order::Middle)
.filter_map(|layer| self.get(layer.id))
.collect()
}
pub fn move_to_top(&mut self, layer_id: LayerId) {
self.visible_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
}
}
pub(crate) fn end_frame(&mut self) {
let Self {
visible_last_frame,
visible_current_frame,
order,
wants_to_be_on_top,
..
} = self;
std::mem::swap(visible_last_frame, visible_current_frame);
visible_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
wants_to_be_on_top.clear();
}
}
// ----------------------------------------------------------------------------
#[test]
fn memory_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Memory>();
}