diff --git a/.typos.toml b/.typos.toml index 97c54d657..8f2a34b6e 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,6 +4,7 @@ [default.extend-words] ime = "ime" # Input Method Editor +abou = "abou" # part of @AmmarAbouZor username nknown = "nknown" # part of @55nknown username isse = "isse" # part of @IsseW username tye = "tye" # part of @tye-exe username diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bae7a734..e7c9601d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Fix `ScrollArea::scroll_to_*` calls when `stick_to_bottom` is Active [#8033](https://github.com/emilk/egui/pull/8033) by [@AmmarAbouZor](https://github.com/AmmarAbouZor) + + ## 0.34.2 - 2026-05-04 ### ⭐ Added * Add regression test for O(n²) word boundary scan [#8077](https://github.com/emilk/egui/pull/8077) by [@hallyhaa](https://github.com/hallyhaa) diff --git a/Cargo.lock b/Cargo.lock index 94bfe892a..76ed5b660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,7 +1197,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "cint", @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit", "ahash", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1287,7 +1287,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit_winit", "arboard", @@ -1310,7 +1310,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit", "accesskit_consumer", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.34.2" +version = "0.34.3" dependencies = [ "criterion", "document-features", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "document-features", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "document-features", @@ -1393,7 +1393,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.34.2" +version = "0.34.3" dependencies = [ "dify", "document-features", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.34.2" +version = "0.34.3" dependencies = [ "egui", "egui_extras", @@ -1443,7 +1443,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "document-features", @@ -1541,7 +1541,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1570,7 +1570,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.34.2" +version = "0.34.3" [[package]] name = "equivalent" @@ -3545,7 +3545,7 @@ dependencies = [ [[package]] name = "popups" -version = "0.34.2" +version = "0.34.3" dependencies = [ "eframe", "env_logger", @@ -5893,7 +5893,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.34.2" +version = "0.34.3" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 3710f8cb3..1eb1d7737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.92" -version = "0.34.2" +version = "0.34.3" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.34.2", path = "crates/emath", default-features = false } -ecolor = { version = "0.34.2", path = "crates/ecolor", default-features = false } -epaint = { version = "0.34.2", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.34.2", path = "crates/epaint_default_fonts" } -egui = { version = "0.34.2", path = "crates/egui", default-features = false } -egui-winit = { version = "0.34.2", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.34.2", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.34.2", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.34.2", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.34.2", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.34.2", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.34.2", path = "crates/eframe", default-features = false } +emath = { version = "0.34.3", path = "crates/emath", default-features = false } +ecolor = { version = "0.34.3", path = "crates/ecolor", default-features = false } +epaint = { version = "0.34.3", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.34.3", path = "crates/epaint_default_fonts" } +egui = { version = "0.34.3", path = "crates/egui", default-features = false } +egui-winit = { version = "0.34.3", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.34.3", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.34.3", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.34.3", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.34.3", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.34.3", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.34.3", path = "crates/eframe", default-features = false } accesskit = "0.24.0" accesskit_consumer = "0.35.0" diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 1e10de83b..e441bf982 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 9071754f2..faea2d407 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,11 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Default `app_id` to `app_name` on native [#8172](https://github.com/emilk/egui/pull/8172) by [@grtlr](https://github.com/grtlr) +* Add winit window access to `eframe::Frame` and `CreationContext` [#8205](https://github.com/emilk/egui/pull/8205) by [@emilk](https://github.com/emilk) + + ## 0.34.2 - 2026-05-04 * Document glow-only fields in `NativeOptions` [#8104](https://github.com/emilk/egui/pull/8104) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 2eda803fc..f36b8f539 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Fix random hangs by improving `wgpu::Surface` lifecycle handling [#8171](https://github.com/emilk/egui/pull/8171) by [@grtlr](https://github.com/grtlr) + + ## 0.34.2 - 2026-05-04 * Update to wgpu 29.0.1 [#8073](https://github.com/emilk/egui/pull/8073) by [@emilk](https://github.com/emilk) * Warn if using a software rasterizer [#8101](https://github.com/emilk/egui/pull/8101) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 15c7c2d52..58cc9be9e 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 009dc8486..c31fc1373 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1600,6 +1600,22 @@ fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option { KeyCode::F34 => Key::F34, KeyCode::F35 => Key::F35, + // Modifier keys — egui now surfaces them as distinct physical + // variants so games / capture UIs can bind them independently. + // The collapsed `Modifiers.shift/ctrl/alt/command` booleans still + // track just the "any side is pressed" state for shortcut matching. + KeyCode::ShiftLeft => Key::ShiftLeft, + KeyCode::ShiftRight => Key::ShiftRight, + KeyCode::ControlLeft => Key::ControlLeft, + KeyCode::ControlRight => Key::ControlRight, + KeyCode::AltLeft => Key::AltLeft, + KeyCode::AltRight => Key::AltRight, + KeyCode::SuperLeft => Key::SuperLeft, + KeyCode::SuperRight => Key::SuperRight, + + // ISO 102nd key — `<>|` on French AZERTY, `\|` on UK QWERTY. + KeyCode::IntlBackslash => Key::IntlBackslash, + _ => { return None; } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index f95a5c2bd..0fa4bba0c 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -753,7 +753,12 @@ impl ScrollArea { ctx.animate_bool_responsive(id.with("v"), show_bars[1]), ); - let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width(); + let scroll_style = ui.spacing().scroll; + let current_bar_use = if scroll_style.floating { + show_bars.to_vec2().yx() * scroll_style.allocated_width() + } else { + show_bars_factor.yx() * scroll_style.allocated_width() + }; let available_outer = ui.available_rect_before_wrap(); @@ -1042,17 +1047,13 @@ impl ScrollArea { .inner; let (content_size, state) = prepared.end(ui); - let output = ScrollAreaOutput { + ScrollAreaOutput { inner, id, state, content_size, inner_rect, - }; - - paint_fade_areas(ui, &output); - - output + } } } @@ -1191,9 +1192,15 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); + let limit_rect = if ui.spacing().scroll.floating { + outer_rect + } else { + inner_rect + }; + let content_is_too_large = Vec2b::new( - direction_enabled[0] && inner_rect.width() < content_size.x, - direction_enabled[1] && inner_rect.height() < content_size.y, + direction_enabled[0] && (limit_rect.width().ceil() < content_size.x), + direction_enabled[1] && (limit_rect.height().ceil() < content_size.y), ); let max_offset = content_size - inner_rect.size(); @@ -1260,6 +1267,11 @@ impl Prepared { let scroll_style = ui.spacing().scroll; + // Reserve the scroll area before painting fades, because fade painting uses ui.min_rect(). + ui.advance_cursor_after_rect(outer_rect); + + paint_fade_areas_impl(ui, inner_rect, content_size, state.offset); + // Paint the bars: let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect); for d in 0..2 { @@ -1508,8 +1520,6 @@ impl Prepared { } } - ui.advance_cursor_after_rect(outer_rect); - if show_scroll_this_frame != state.show_scroll { ui.request_repaint(); } @@ -1551,7 +1561,7 @@ impl Prepared { /// Paint fade-out gradients at the top and/or bottom of a scroll area to /// indicate that more content is available beyond the visible region. -fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { +fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset: Vec2) { let crate::style::ScrollFadeStyle { strength, size: fade_size, @@ -1563,11 +1573,9 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { let bg = ui.stack().bg_color(); - let offset = scroll_output.state.offset; - let overflow = scroll_output.content_size - scroll_output.inner_rect.size(); + let overflow = content_size - inner_rect.size(); - let paint_rect = scroll_output - .inner_rect + let paint_rect = inner_rect .intersect(ui.min_rect()) .expand(ui.visuals().clip_rect_margin); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cd9cc896b..57e285575 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1307,6 +1307,52 @@ impl Context { .map(|widget_rect| self.get_response(widget_rect)) } + /// Rectangles that could receive pointer input in the last completed pass. + /// + /// This exposes the same widget rectangles egui uses for hit-testing, after + /// filtering out disabled widgets, non-interactive widgets, and layers that + /// are currently blocked from interaction. The returned rectangles are in + /// global viewport coordinates, with layer transforms applied. + /// + /// This is meant for integrations that must declare platform input regions + /// before pointer events can be delivered to egui, such as transparent or + /// click-through overlays. + #[must_use] + pub fn interactive_rects_last_pass(&self) -> Vec { + self.read(|ctx| { + let Some(viewport) = ctx.viewports.get(&ctx.viewport_id()) else { + return Vec::new(); + }; + + let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); + layers.sort_by(|&a, &b| ctx.memory.areas().compare_order(a, b)); + + let mut rects = Vec::new(); + for layer_id in layers { + if !ctx.memory.allows_interaction(layer_id) { + continue; + } + + let to_global = ctx.memory.to_global.get(&layer_id).copied(); + for widget in viewport.prev_pass.widgets.get_layer(layer_id) { + if !widget.enabled || !widget.sense.interactive() { + continue; + } + + let rect = if let Some(to_global) = to_global { + to_global * widget.interact_rect + } else { + widget.interact_rect + }; + if rect.is_positive() && rect.is_finite() { + rects.push(rect); + } + } + } + rects + }) + } + /// Do all interaction for an existing widget, without (re-)registering it. pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response { use response::Flags; diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs deleted file mode 100644 index 0b03d02a9..000000000 --- a/crates/egui/src/data/input.rs +++ /dev/null @@ -1,1384 +0,0 @@ -//! The input needed by egui. - -use epaint::{ColorImage, MarginF32}; - -use crate::{ - Key, OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, - emath::{Pos2, Rect, Vec2}, -}; - -/// What the integrations provides to egui at the start of each frame. -/// -/// Set the values that make sense, leave the rest at their `Default::default()`. -/// -/// You can check if `egui` is using the inputs using -/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`]. -/// -/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. -/// -/// Ii "points" can be calculated from native physical pixels -/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`; -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct RawInput { - /// The id of the active viewport. - pub viewport_id: ViewportId, - - /// Information about all egui viewports. - pub viewports: ViewportIdMap, - - /// The insets used to only render content in a mobile safe area - /// - /// `None` will be treated as "same as last frame" - pub safe_area_insets: Option, - - /// Position and size of the area that egui should use, in points. - /// Usually you would set this to - /// - /// `Some(Rect::from_min_size(Default::default(), screen_size_in_points))`. - /// - /// but you could also constrain egui to some smaller portion of your window if you like. - /// - /// `None` will be treated as "same as last frame", with the default being a very big area. - pub screen_rect: Option, - - /// Maximum size of one side of the font texture. - /// - /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. - /// - /// The default is a very small (but very portable) 2048. - pub max_texture_side: Option, - - /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. - /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). - pub time: Option, - - /// Should be set to the expected time between frames when painting at vsync speeds. - /// The default for this is 1/60. - /// Can safely be left at its default value. - pub predicted_dt: f32, - - /// Which modifier keys are down at the start of the frame? - pub modifiers: Modifiers, - - /// In-order events received this frame. - /// - /// There is currently no way to know if egui handles a particular event, - /// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`] - /// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`]. - pub events: Vec, - - /// Dragged files hovering over egui. - pub hovered_files: Vec, - - /// Dragged files dropped into egui. - /// - /// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has - /// been disabled in [`crate::viewport::ViewportBuilder`]. - pub dropped_files: Vec, - - /// The native window has the keyboard focus (i.e. is receiving key presses). - /// - /// False when the user alt-tab away from the application, for instance. - pub focused: bool, - - /// Does the OS use dark or light mode? - /// - /// `None` means "don't know". - pub system_theme: Option, -} - -impl Default for RawInput { - fn default() -> Self { - Self { - viewport_id: ViewportId::ROOT, - viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), - screen_rect: None, - max_texture_side: None, - time: None, - predicted_dt: 1.0 / 60.0, - modifiers: Modifiers::default(), - events: vec![], - hovered_files: Default::default(), - dropped_files: Default::default(), - focused: true, // integrations opt into global focus tracking - system_theme: None, - safe_area_insets: Default::default(), - } - } -} - -impl RawInput { - /// Info about the active viewport - #[inline] - pub fn viewport(&self) -> &ViewportInfo { - self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") - } - - /// Helper: move volatile (deltas and events), clone the rest. - /// - /// * [`Self::hovered_files`] is cloned. - /// * [`Self::dropped_files`] is moved. - pub fn take(&mut self) -> Self { - Self { - viewport_id: self.viewport_id, - viewports: self - .viewports - .iter_mut() - .map(|(id, info)| (*id, info.take())) - .collect(), - screen_rect: self.screen_rect.take(), - safe_area_insets: self.safe_area_insets.take(), - max_texture_side: self.max_texture_side.take(), - time: self.time, - predicted_dt: self.predicted_dt, - modifiers: self.modifiers, - events: std::mem::take(&mut self.events), - hovered_files: self.hovered_files.clone(), - dropped_files: std::mem::take(&mut self.dropped_files), - focused: self.focused, - system_theme: self.system_theme, - } - } - - /// Add on new input. - pub fn append(&mut self, newer: Self) { - let Self { - viewport_id: viewport_ids, - viewports, - screen_rect, - max_texture_side, - time, - predicted_dt, - modifiers, - mut events, - mut hovered_files, - mut dropped_files, - focused, - system_theme, - safe_area_insets: safe_area, - } = newer; - - self.viewport_id = viewport_ids; - self.viewports = viewports; - self.screen_rect = screen_rect.or(self.screen_rect); - self.max_texture_side = max_texture_side.or(self.max_texture_side); - self.time = time; // use latest time - self.predicted_dt = predicted_dt; // use latest dt - self.modifiers = modifiers; // use latest - self.events.append(&mut events); - self.hovered_files.append(&mut hovered_files); - self.dropped_files.append(&mut dropped_files); - self.focused = focused; - self.system_theme = system_theme; - self.safe_area_insets = safe_area; - } -} - -/// An input event from the backend into egui, about a specific [viewport](crate::viewport). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ViewportEvent { - /// The user clicked the close-button on the window, or similar. - /// - /// If this is the root viewport, the application will exit - /// after this frame unless you send a - /// [`crate::ViewportCommand::CancelClose`] command. - /// - /// If this is not the root viewport, - /// it is up to the user to hide this viewport the next frame. - /// - /// This even will wake up both the child and parent viewport. - Close, -} - -/// Information about the current viewport, given as input each frame. -/// -/// `None` means "unknown". -/// -/// All units are in ui "points", which can be calculated from native physical pixels -/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; -#[derive(Clone, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ViewportInfo { - /// Parent viewport, if known. - pub parent: Option, - - /// Name of the viewport, if known. - pub title: Option, - - pub events: Vec, - - /// The OS native pixels-per-point. - /// - /// This should always be set, if known. - /// - /// On web this takes browser scaling into account, - /// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. - pub native_pixels_per_point: Option, - - /// Current monitor size in egui points. - pub monitor_size: Option, - - /// The inner rectangle of the native window, in monitor space and ui points scale. - /// - /// This is the content rectangle of the viewport. - /// - /// **`eframe` notes**: - /// - /// On Android / Wayland, this will always be `None` since getting the - /// position of the window is not possible. - pub inner_rect: Option, - - /// The outer rectangle of the native window, in monitor space and ui points scale. - /// - /// This is the content rectangle plus decoration chrome. - /// - /// **`eframe` notes**: - /// - /// On Android / Wayland, this will always be `None` since getting the - /// position of the window is not possible. - pub outer_rect: Option, - - /// Are we minimized? - pub minimized: Option, - - /// Are we maximized? - pub maximized: Option, - - /// Are we in fullscreen mode? - pub fullscreen: Option, - - /// Is the window focused and able to receive input? - /// - /// This should be the same as [`RawInput::focused`]. - pub focused: Option, - - /// Is the window fully occluded (completely covered) by another window? - /// - /// Not all platforms support this. - /// On platforms that don't, this will be `None` or `Some(false)`. - pub occluded: Option, -} - -impl ViewportInfo { - /// Is the window considered visible for rendering purposes? - /// - /// A window is not visible if it is minimized or occluded. - /// When not visible, the UI is not painted and rendering is skipped, - /// but application logic may still be executed by some integrations. - pub fn visible(&self) -> Option { - match (self.minimized, self.occluded) { - (Some(true), _) | (_, Some(true)) => Some(false), - (Some(false), Some(false)) => Some(true), - (_, None) | (None, _) => None, - } - } - - /// This viewport has been told to close. - /// - /// If this is the root viewport, the application will exit - /// after this frame unless you send a - /// [`crate::ViewportCommand::CancelClose`] command. - /// - /// If this is not the root viewport, - /// it is up to the user to hide this viewport the next frame. - pub fn close_requested(&self) -> bool { - self.events.contains(&ViewportEvent::Close) - } - - /// Helper: move [`Self::events`], clone the other fields. - pub fn take(&mut self) -> Self { - Self { - parent: self.parent, - title: self.title.clone(), - events: std::mem::take(&mut self.events), - native_pixels_per_point: self.native_pixels_per_point, - monitor_size: self.monitor_size, - inner_rect: self.inner_rect, - outer_rect: self.outer_rect, - minimized: self.minimized, - maximized: self.maximized, - fullscreen: self.fullscreen, - focused: self.focused, - occluded: self.occluded, - } - } - - pub fn ui(&self, ui: &mut crate::Ui) { - let Self { - parent, - title, - events, - native_pixels_per_point, - monitor_size, - inner_rect, - outer_rect, - minimized, - maximized, - fullscreen, - focused, - occluded, - } = self; - - crate::Grid::new("viewport_info").show(ui, |ui| { - ui.label("Parent:"); - ui.label(opt_as_str(parent)); - ui.end_row(); - - ui.label("Title:"); - ui.label(opt_as_str(title)); - ui.end_row(); - - ui.label("Events:"); - ui.label(format!("{events:?}")); - ui.end_row(); - - ui.label("Native pixels-per-point:"); - ui.label(opt_as_str(native_pixels_per_point)); - ui.end_row(); - - ui.label("Monitor size:"); - ui.label(opt_as_str(monitor_size)); - ui.end_row(); - - ui.label("Inner rect:"); - ui.label(opt_rect_as_string(inner_rect)); - ui.end_row(); - - ui.label("Outer rect:"); - ui.label(opt_rect_as_string(outer_rect)); - ui.end_row(); - - ui.label("Minimized:"); - ui.label(opt_as_str(minimized)); - ui.end_row(); - - ui.label("Maximized:"); - ui.label(opt_as_str(maximized)); - ui.end_row(); - - ui.label("Fullscreen:"); - ui.label(opt_as_str(fullscreen)); - ui.end_row(); - - ui.label("Focused:"); - ui.label(opt_as_str(focused)); - ui.end_row(); - - ui.label("Occluded:"); - ui.label(opt_as_str(occluded)); - ui.end_row(); - - let visible = self.visible(); - - ui.label("Visible:"); - ui.label(opt_as_str(&visible)); - ui.end_row(); - - #[expect(clippy::ref_option)] - fn opt_rect_as_string(v: &Option) -> String { - v.as_ref().map_or(String::new(), |r| { - format!("Pos: {:?}, size: {:?}", r.min, r.size()) - }) - } - - #[expect(clippy::ref_option)] - fn opt_as_str(v: &Option) -> String { - v.as_ref().map_or(String::new(), |v| format!("{v:?}")) - } - }); - } -} - -/// A file about to be dropped into egui. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct HoveredFile { - /// Set by the `egui-winit` backend. - pub path: Option, - - /// With the `eframe` web backend, this is set to the mime-type of the file (if available). - pub mime: String, -} - -/// A file dropped into egui. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct DroppedFile { - /// Set by the `egui-winit` backend. - pub path: Option, - - /// Name of the file. Set by the `eframe` web backend. - pub name: String, - - /// With the `eframe` web backend, this is set to the mime-type of the file (if available). - pub mime: String, - - /// Set by the `eframe` web backend. - pub last_modified: Option, - - /// Set by the `eframe` web backend. - pub bytes: Option>, -} - -/// An input event generated by the integration. -/// -/// This only covers events that egui cares about. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum Event { - /// The integration detected a "copy" event (e.g. Cmd+C). - Copy, - - /// The integration detected a "cut" event (e.g. Cmd+X). - Cut, - - /// The integration detected a "paste" event (e.g. Cmd+V). - Paste(String), - - /// Text input, e.g. via keyboard. - /// - /// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]). - Text(String), - - /// A key was pressed or released. - /// - /// ## Note for integration authors - /// - /// Key events that has been processed by IMEs should not be sent to `egui`. - Key { - /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak - /// keyboard layout, it will be taken into account. - /// - /// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters), - /// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of - /// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`). - key: Key, - - /// The physical key, corresponding to the actual position on the keyboard. - /// - /// This ignores keymaps, so it is not recommended to use this. - /// The only thing it makes sense for is things like games, - /// where e.g. the physical location of WSAD on QWERTY should always map to movement, - /// even if the user is using Dvorak or AZERTY. - /// - /// `eframe` does not (yet) implement this on web. - physical_key: Option, - - /// Was it pressed or released? - pressed: bool, - - /// If this is a `pressed` event, is it a key-repeat? - /// - /// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats. - /// Sometimes you will want to ignore such events, and this lets you do that. - /// - /// egui will automatically detect such repeat events and mark them as such here. - /// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`). - repeat: bool, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The mouse or touch moved to a new place. - PointerMoved(Pos2), - - /// The mouse moved, the units are unspecified. - /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. - /// `PointerMoved` and `MouseMoved` can be sent at the same time. - /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. - MouseMoved(Vec2), - - /// A mouse button was pressed or released (or a touch started or stopped). - PointerButton { - /// Where is the pointer? - pos: Pos2, - - /// What mouse button? For touches, use [`PointerButton::Primary`]. - button: PointerButton, - - /// Was it the button/touch pressed this frame, or released? - pressed: bool, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The mouse left the screen, or the last/primary touch input disappeared. - /// - /// This means there is no longer a cursor on the screen for hovering etc. - /// - /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. - PointerGone, - - /// Zoom scale factor this frame (e.g. from a pinch gesture). - /// - /// * `zoom = 1`: no change. - /// * `zoom < 1`: pinch together - /// * `zoom > 1`: pinch spread - /// - /// Note that egui also implement zooming by holding `Ctrl` and scrolling the mouse wheel, - /// so integration need NOT emit this `Zoom` event in those cases, just [`Self::MouseWheel`]. - /// - /// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame. - Zoom(f32), - - /// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture). - Rotate(f32), - - /// IME Event - Ime(ImeEvent), - - /// On touch screens, report this *in addition to* - /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] - Touch { - /// Hashed device identifier (if available; may be zero). - /// Can be used to separate touches from different devices. - device_id: TouchDeviceId, - - /// Unique identifier of a finger/pen. Value is stable from touch down - /// to lift-up - id: TouchId, - - /// One of: start move end cancel. - phase: TouchPhase, - - /// Position of the touch (or where the touch was last detected) - pos: Pos2, - - /// Describes how hard the touch device was pressed. May always be `None` if the platform does - /// not support pressure sensitivity. - /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). - force: Option, - }, - - /// A raw mouse wheel event as sent by the backend. - /// - /// Used for scrolling. - MouseWheel { - /// The unit of `delta`: points, lines, or pages. - unit: MouseWheelUnit, - - /// The direction of the vector indicates how to move the _content_ that is being viewed. - /// So if you get positive values, the content being viewed should move to the right and down, - /// revealing new things to the left and up. - /// - /// A positive X-value indicates the content is being moved right, - /// as when swiping right on a touch-screen or track-pad with natural scrolling. - /// - /// A positive Y-value indicates the content is being moved down, - /// as when swiping down on a touch-screen or track-pad with natural scrolling. - delta: Vec2, - - /// The phase of the scroll, useful for trackpads. - /// - /// If unknown set this to [`TouchPhase::Move`]. - phase: TouchPhase, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The native window gained or lost focused (e.g. the user clicked alt-tab). - WindowFocused(bool), - - /// An assistive technology (e.g. screen reader) requested an action. - AccessKitActionRequest(accesskit::ActionRequest), - - /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. - Screenshot { - viewport_id: crate::ViewportId, - - /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. - user_data: crate::UserData, - - image: std::sync::Arc, - }, -} - -/// IME event. -/// -/// See -#[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ImeEvent { - /// Notifies when the IME was enabled. - #[deprecated = "No longer used by egui"] - Enabled, - - /// A new IME candidate is being suggested. - /// - /// An empty preedit string indicates that the IME has been dismissed, while - /// a non-empty preedit string indicates that the IME is active. - Preedit { - text: String, - active_range_chars: Option>, - }, - - /// IME composition ended with this final result. - /// - /// The IME is considered dismissed after this event. - Commit(String), - - /// Notifies when the IME was disabled. - #[deprecated = "No longer used by egui"] - Disabled, -} - -/// Mouse button (or similar for touch input) -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum PointerButton { - /// The primary mouse button is usually the left one. - Primary = 0, - - /// The secondary mouse button is usually the right one, - /// and most often used for context menus or other optional things. - Secondary = 1, - - /// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel). - Middle = 2, - - /// The first extra mouse button on some mice. In web typically corresponds to the Browser back button. - Extra1 = 3, - - /// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button. - Extra2 = 4, -} - -/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`]. -pub const NUM_POINTER_BUTTONS: usize = 5; - -/// State of the modifier keys. These must be fed to egui. -/// -/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. -/// -/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function -/// -/// ```rust -/// # let ctx = egui::Context::default(); -/// let modifiers = ctx.input(|i| i.modifiers); -/// ``` -/// -/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers -/// as on mac that is how you type special characters, -/// so those key presses are usually not reported to egui. -#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Modifiers { - /// Either of the alt keys are down (option ⌥ on Mac). - pub alt: bool, - - /// Either of the control keys are down. - /// When checking for keyboard shortcuts, consider using [`Self::command`] instead. - pub ctrl: bool, - - /// Either of the shift keys are down. - pub shift: bool, - - /// The Mac ⌘ Command key. Should always be set to `false` on other platforms. - pub mac_cmd: bool, - - /// On Windows and Linux, set this to the same value as `ctrl`. - /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). - /// This is so that egui can, for instance, select all text by checking for `command + A` - /// and it will work on both Mac and Windows. - pub command: bool, -} - -impl std::fmt::Debug for Modifiers { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_none() { - return write!(f, "Modifiers::NONE"); - } - - let Self { - alt, - ctrl, - shift, - mac_cmd, - command, - } = *self; - - let mut debug = f.debug_struct("Modifiers"); - if alt { - debug.field("alt", &true); - } - if ctrl { - debug.field("ctrl", &true); - } - if shift { - debug.field("shift", &true); - } - if mac_cmd { - debug.field("mac_cmd", &true); - } - if command { - debug.field("command", &true); - } - debug.finish() - } -} - -impl Modifiers { - pub const NONE: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: false, - command: false, - }; - - pub const ALT: Self = Self { - alt: true, - ctrl: false, - shift: false, - mac_cmd: false, - command: false, - }; - pub const CTRL: Self = Self { - alt: false, - ctrl: true, - shift: false, - mac_cmd: false, - command: false, - }; - pub const SHIFT: Self = Self { - alt: false, - ctrl: false, - shift: true, - mac_cmd: false, - command: false, - }; - - /// The Mac ⌘ Command key - pub const MAC_CMD: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: true, - command: false, - }; - - /// On Mac: ⌘ Command key, elsewhere: Ctrl key - pub const COMMAND: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: false, - command: true, - }; - - /// ``` - /// # use egui::Modifiers; - /// assert_eq!( - /// Modifiers::CTRL | Modifiers::ALT, - /// Modifiers { ctrl: true, alt: true, ..Default::default() } - /// ); - /// assert_eq!( - /// Modifiers::ALT.plus(Modifiers::CTRL), - /// Modifiers::CTRL.plus(Modifiers::ALT), - /// ); - /// assert_eq!( - /// Modifiers::CTRL | Modifiers::ALT, - /// Modifiers::CTRL.plus(Modifiers::ALT), - /// ); - /// ``` - #[inline] - pub const fn plus(self, rhs: Self) -> Self { - Self { - alt: self.alt | rhs.alt, - ctrl: self.ctrl | rhs.ctrl, - shift: self.shift | rhs.shift, - mac_cmd: self.mac_cmd | rhs.mac_cmd, - command: self.command | rhs.command, - } - } - - #[inline] - pub fn is_none(&self) -> bool { - self == &Self::default() - } - - #[inline] - pub fn any(&self) -> bool { - !self.is_none() - } - - #[inline] - pub fn all(&self) -> bool { - self.alt && self.ctrl && self.shift && self.command - } - - /// Is shift the only pressed button? - #[inline] - pub fn shift_only(&self) -> bool { - self.shift && !(self.alt || self.command) - } - - /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. - #[inline] - pub fn command_only(&self) -> bool { - !self.alt && !self.shift && self.command - } - - /// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset - /// of the pressed key (`self`). - /// - /// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not. - /// - /// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts. - /// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`, - /// but a Swedish keyboard has dedicated `+` key. - /// So if you want to make a [`KeyboardShortcut`] looking for `Cmd` + `+`, it makes sense - /// to ignore the shift key. - /// Similarly, the `Alt` key is sometimes used to type special characters. - /// - /// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys - /// to be pressed, then they must be pressed. - /// - /// # Example: - /// ``` - /// # use egui::Modifiers; - /// # let pressed_modifiers = Modifiers::default(); - /// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) { - /// // Alt and Shift are pressed, but not ctrl/command - /// } - /// ``` - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); - /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); - /// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD)); - /// ``` - pub fn matches_logically(&self, pattern: Self) -> bool { - if pattern.alt && !self.alt { - return false; - } - if pattern.shift && !self.shift { - return false; - } - - self.cmd_ctrl_matches(pattern) - } - - /// Check for equality but with proper handling of [`Self::command`]. - /// - /// `self` here are the currently pressed modifiers, - /// and the argument the pattern we are testing for. - /// - /// Note that this will require the `shift` and `alt` keys match, even though - /// these modifiers are sometimes required to produce some logical keys. - /// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`, - /// but on a Swedish keyboard you can press the dedicated `+` key. - /// Therefore, you often want to use [`Self::matches_logically`] instead. - /// - /// # Example: - /// ``` - /// # use egui::Modifiers; - /// # let pressed_modifiers = Modifiers::default(); - /// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) { - /// // Alt and Shift are pressed, and nothing else - /// } - /// ``` - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); - /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); - /// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD)); - /// ``` - pub fn matches_exact(&self, pattern: Self) -> bool { - // alt and shift must always match the pattern: - if pattern.alt != self.alt || pattern.shift != self.shift { - return false; - } - - self.cmd_ctrl_matches(pattern) - } - - /// Check if any of the modifiers match exactly. - /// - /// Returns true if the same modifier is pressed in `self` as in `pattern`, - /// for at least one modifier. - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL)); - /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL)); - /// ``` - pub fn matches_any(&self, pattern: Self) -> bool { - if self.alt && pattern.alt { - return true; - } - if self.shift && pattern.shift { - return true; - } - if self.ctrl && pattern.ctrl { - return true; - } - if self.mac_cmd && pattern.mac_cmd { - return true; - } - if (self.mac_cmd || self.command || self.ctrl) && pattern.command { - return true; - } - false - } - - /// Checks only cmd/ctrl, not alt/shift. - /// - /// `self` here are the currently pressed modifiers, - /// and the argument the pattern we are testing for. - /// - /// This takes care to properly handle the difference between - /// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`]. - pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool { - if pattern.mac_cmd { - // Mac-specific match: - if !self.mac_cmd { - return false; - } - if pattern.ctrl != self.ctrl { - return false; - } - return true; - } - - if !pattern.ctrl && !pattern.command { - // the pattern explicitly doesn't want any ctrl/command: - return !self.ctrl && !self.command; - } - - // if the pattern is looking for command, then `ctrl` may or may not be set depending on platform. - // if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform. - - if pattern.ctrl && !self.ctrl { - return false; - } - if pattern.command && !self.command { - return false; - } - - true - } - - /// Whether another set of modifiers is contained in this set of modifiers with proper handling of [`Self::command`]. - /// - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::default().contains(Modifiers::default())); - /// assert!(Modifiers::CTRL.contains(Modifiers::default())); - /// assert!(Modifiers::CTRL.contains(Modifiers::CTRL)); - /// assert!(Modifiers::CTRL.contains(Modifiers::COMMAND)); - /// assert!(Modifiers::MAC_CMD.contains(Modifiers::COMMAND)); - /// assert!(Modifiers::COMMAND.contains(Modifiers::MAC_CMD)); - /// assert!(Modifiers::COMMAND.contains(Modifiers::CTRL)); - /// assert!(!(Modifiers::ALT | Modifiers::CTRL).contains(Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT)); - /// ``` - pub fn contains(&self, query: Self) -> bool { - if query == Self::default() { - return true; - } - - let Self { - alt, - ctrl, - shift, - mac_cmd, - command, - } = *self; - - if alt && query.alt { - return self.contains(Self { - alt: false, - ..query - }); - } - if shift && query.shift { - return self.contains(Self { - shift: false, - ..query - }); - } - - if (ctrl || command) && (query.ctrl || query.command) { - return self.contains(Self { - command: false, - ctrl: false, - ..query - }); - } - if (mac_cmd || command) && (query.mac_cmd || query.command) { - return self.contains(Self { - mac_cmd: false, - command: false, - ..query - }); - } - - false - } -} - -impl std::ops::BitOr for Modifiers { - type Output = Self; - - #[inline] - fn bitor(self, rhs: Self) -> Self { - self.plus(rhs) - } -} - -impl std::ops::BitOrAssign for Modifiers { - #[inline] - fn bitor_assign(&mut self, rhs: Self) { - *self = *self | rhs; - } -} - -impl Modifiers { - pub fn ui(&self, ui: &mut crate::Ui) { - ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac())); - } -} - -// ---------------------------------------------------------------------------- - -/// Names of different modifier keys. -/// -/// Used to name modifiers. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ModifierNames<'a> { - pub is_short: bool, - - pub alt: &'a str, - pub ctrl: &'a str, - pub shift: &'a str, - pub mac_cmd: &'a str, - pub mac_alt: &'a str, - - /// What goes between the names - pub concat: &'a str, -} - -impl ModifierNames<'static> { - /// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font. - pub const SYMBOLS: Self = Self { - is_short: true, - alt: "⌥", - ctrl: "⌃", - shift: "⇧", - mac_cmd: "⌘", - mac_alt: "⌥", - concat: "", - }; - - /// Alt, Ctrl, Shift, Cmd - pub const NAMES: Self = Self { - is_short: false, - alt: "Alt", - ctrl: "Ctrl", - shift: "Shift", - mac_cmd: "Cmd", - mac_alt: "Option", - concat: "+", - }; -} - -impl ModifierNames<'_> { - pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { - let mut s = String::new(); - - let mut append_if = |modifier_is_active, modifier_name| { - if modifier_is_active { - if !s.is_empty() { - s += self.concat; - } - s += modifier_name; - } - }; - - if is_mac { - append_if(modifiers.ctrl, self.ctrl); - append_if(modifiers.shift, self.shift); - append_if(modifiers.alt, self.mac_alt); - append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd); - } else { - append_if(modifiers.ctrl || modifiers.command, self.ctrl); - append_if(modifiers.alt, self.alt); - append_if(modifiers.shift, self.shift); - } - - s - } -} - -// ---------------------------------------------------------------------------- - -/// A keyboard shortcut, e.g. `Ctrl+Alt+W`. -/// -/// Can be used with [`crate::InputState::consume_shortcut`] -/// and [`crate::Context::format_shortcut`]. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct KeyboardShortcut { - pub modifiers: Modifiers, - - pub logical_key: Key, -} - -impl KeyboardShortcut { - pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self { - Self { - modifiers, - logical_key, - } - } - - pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { - let mut s = names.format(&self.modifiers, is_mac); - if !s.is_empty() { - s += names.concat; - } - if names.is_short { - s += self.logical_key.symbol_or_name(); - } else { - s += self.logical_key.name(); - } - s - } -} - -#[test] -fn format_kb_shortcut() { - let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F); - assert_eq!( - cmd_shift_f.format(&ModifierNames::NAMES, false), - "Ctrl+Shift+F" - ); - assert_eq!( - cmd_shift_f.format(&ModifierNames::NAMES, true), - "Shift+Cmd+F" - ); - assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F"); - assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F"); -} - -// ---------------------------------------------------------------------------- - -impl RawInput { - pub fn ui(&self, ui: &mut crate::Ui) { - let Self { - viewport_id, - viewports, - screen_rect, - max_texture_side, - time, - predicted_dt, - modifiers, - events, - hovered_files, - dropped_files, - focused, - system_theme, - safe_area_insets: safe_area, - } = self; - - ui.label(format!("Active viewport: {viewport_id:?}")); - let ordered_viewports = viewports - .iter() - .map(|(id, value)| (*id, value)) - .collect::>(); - for (id, viewport) in ordered_viewports { - ui.group(|ui| { - ui.label(format!("Viewport {id:?}")); - ui.push_id(id, |ui| { - viewport.ui(ui); - }); - }); - } - ui.label(format!("screen_rect: {screen_rect:?} points")); - - ui.label(format!("max_texture_side: {max_texture_side:?}")); - if let Some(time) = time { - ui.label(format!("time: {time:.3} s")); - } else { - ui.label("time: None"); - } - ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); - ui.label(format!("modifiers: {modifiers:#?}")); - ui.label(format!("hovered_files: {}", hovered_files.len())); - ui.label(format!("dropped_files: {}", dropped_files.len())); - ui.label(format!("focused: {focused}")); - ui.label(format!("system_theme: {system_theme:?}")); - ui.label(format!("safe_area: {safe_area:?}")); - ui.scope(|ui| { - ui.set_min_height(150.0); - ui.label(format!("events: {events:#?}")) - .on_hover_text("key presses etc"); - }); - } -} - -/// this is a `u64` as values of this kind can always be obtained by hashing -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct TouchDeviceId(pub u64); - -/// Unique identification of a touch occurrence (finger or pen or …). -/// A Touch ID is valid until the finger is lifted. -/// A new ID is used for the next touch. -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct TouchId(pub u64); - -/// In what phase a touch event is in. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum TouchPhase { - /// User just placed a touch point on the touch surface - Start, - - /// User moves a touch point along the surface. This event is also sent when - /// any attributes (position, force, …) of the touch point change. - Move, - - /// User lifted the finger or pen from the surface, or slid off the edge of - /// the surface - End, - - /// Touch operation has been disrupted by something (various reasons are possible, - /// maybe a pop-up alert or any other kind of interruption which may not have - /// been intended by the user) - Cancel, -} - -/// The unit associated with the numeric value of a mouse wheel event -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum MouseWheelUnit { - /// Number of ui points (logical pixels) - Point, - - /// Number of lines - Line, - - /// Number of pages - Page, -} - -impl From for TouchId { - fn from(id: u64) -> Self { - Self(id) - } -} - -impl From for TouchId { - fn from(id: i32) -> Self { - Self(id as u64) - } -} - -impl From for TouchId { - fn from(id: u32) -> Self { - Self(id as u64) - } -} - -// ---------------------------------------------------------------------------- - -// TODO(emilk): generalize this to a proper event filter. -/// Controls which events that a focused widget will have exclusive access to. -/// -/// Currently this only controls a few special keyboard events, -/// but in the future this `struct` should be extended into a full callback thing. -/// -/// Any events not covered by the filter are given to the widget, but are not exclusive. -#[derive(Clone, Copy, Debug)] -pub struct EventFilter { - /// If `true`, pressing tab will act on the widget, - /// and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub tab: bool, - - /// If `true`, pressing horizontal arrows will act on the - /// widget, and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub horizontal_arrows: bool, - - /// If `true`, pressing vertical arrows will act on the - /// widget, and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub vertical_arrows: bool, - - /// If `true`, pressing escape will act on the widget, - /// and NOT surrender focus from the focused widget. - /// - /// Default: `false` - pub escape: bool, -} - -#[expect(clippy::derivable_impls)] // let's be explicit -impl Default for EventFilter { - fn default() -> Self { - Self { - tab: false, - horizontal_arrows: false, - vertical_arrows: false, - escape: false, - } - } -} - -impl EventFilter { - pub fn matches(&self, event: &Event) -> bool { - if let Event::Key { key, .. } = event { - match key { - crate::Key::Tab => self.tab, - crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows, - crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows, - crate::Key::Escape => self.escape, - _ => true, - } - } else { - true - } - } -} - -/// The 'safe area' insets of the screen -/// -/// This represents the area taken up by the status bar, navigation controls, notches, -/// or any other items that obscure parts of the screen. -#[derive(Debug, PartialEq, Copy, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct SafeAreaInsets(pub MarginF32); - -impl std::ops::Sub for Rect { - type Output = Self; - - fn sub(self, rhs: SafeAreaInsets) -> Self::Output { - self - rhs.0 - } -} diff --git a/crates/egui/src/data/input/dropped_file.rs b/crates/egui/src/data/input/dropped_file.rs new file mode 100644 index 000000000..39faceba8 --- /dev/null +++ b/crates/egui/src/data/input/dropped_file.rs @@ -0,0 +1,19 @@ +/// A file dropped into egui. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct DroppedFile { + /// Set by the `egui-winit` backend. + pub path: Option, + + /// Name of the file. Set by the `eframe` web backend. + pub name: String, + + /// With the `eframe` web backend, this is set to the mime-type of the file (if available). + pub mime: String, + + /// Set by the `eframe` web backend. + pub last_modified: Option, + + /// Set by the `eframe` web backend. + pub bytes: Option>, +} diff --git a/crates/egui/src/data/input/event.rs b/crates/egui/src/data/input/event.rs new file mode 100644 index 000000000..117a200b6 --- /dev/null +++ b/crates/egui/src/data/input/event.rs @@ -0,0 +1,186 @@ +use epaint::ColorImage; + +use crate::{ + Key, + emath::{Pos2, Vec2}, +}; + +use super::{ + ImeEvent, Modifiers, MouseWheelUnit, PointerButton, TouchDeviceId, TouchId, TouchPhase, +}; + +/// An input event generated by the integration. +/// +/// This only covers events that egui cares about. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum Event { + /// The integration detected a "copy" event (e.g. Cmd+C). + Copy, + + /// The integration detected a "cut" event (e.g. Cmd+X). + Cut, + + /// The integration detected a "paste" event (e.g. Cmd+V). + Paste(String), + + /// Text input, e.g. via keyboard. + /// + /// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]). + Text(String), + + /// A key was pressed or released. + /// + /// ## Note for integration authors + /// + /// Key events that has been processed by IMEs should not be sent to `egui`. + Key { + /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak + /// keyboard layout, it will be taken into account. + /// + /// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters), + /// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of + /// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`). + key: Key, + + /// The physical key, corresponding to the actual position on the keyboard. + /// + /// This ignores keymaps, so it is not recommended to use this. + /// The only thing it makes sense for is things like games, + /// where e.g. the physical location of WSAD on QWERTY should always map to movement, + /// even if the user is using Dvorak or AZERTY. + /// + /// `eframe` does not (yet) implement this on web. + physical_key: Option, + + /// Was it pressed or released? + pressed: bool, + + /// If this is a `pressed` event, is it a key-repeat? + /// + /// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats. + /// Sometimes you will want to ignore such events, and this lets you do that. + /// + /// egui will automatically detect such repeat events and mark them as such here. + /// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`). + repeat: bool, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The mouse or touch moved to a new place. + PointerMoved(Pos2), + + /// The mouse moved, the units are unspecified. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// `PointerMoved` and `MouseMoved` can be sent at the same time. + /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. + MouseMoved(Vec2), + + /// A mouse button was pressed or released (or a touch started or stopped). + PointerButton { + /// Where is the pointer? + pos: Pos2, + + /// What mouse button? For touches, use [`PointerButton::Primary`]. + button: PointerButton, + + /// Was it the button/touch pressed this frame, or released? + pressed: bool, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The mouse left the screen, or the last/primary touch input disappeared. + /// + /// This means there is no longer a cursor on the screen for hovering etc. + /// + /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. + PointerGone, + + /// Zoom scale factor this frame (e.g. from a pinch gesture). + /// + /// * `zoom = 1`: no change. + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + /// + /// Note that egui also implement zooming by holding `Ctrl` and scrolling the mouse wheel, + /// so integration need NOT emit this `Zoom` event in those cases, just [`Self::MouseWheel`]. + /// + /// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame. + Zoom(f32), + + /// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture). + Rotate(f32), + + /// IME Event + Ime(ImeEvent), + + /// On touch screens, report this *in addition to* + /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] + Touch { + /// Hashed device identifier (if available; may be zero). + /// Can be used to separate touches from different devices. + device_id: TouchDeviceId, + + /// Unique identifier of a finger/pen. Value is stable from touch down + /// to lift-up + id: TouchId, + + /// One of: start move end cancel. + phase: TouchPhase, + + /// Position of the touch (or where the touch was last detected) + pos: Pos2, + + /// Describes how hard the touch device was pressed. May always be `None` if the platform does + /// not support pressure sensitivity. + /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). + force: Option, + }, + + /// A raw mouse wheel event as sent by the backend. + /// + /// Used for scrolling. + MouseWheel { + /// The unit of `delta`: points, lines, or pages. + unit: MouseWheelUnit, + + /// The direction of the vector indicates how to move the _content_ that is being viewed. + /// So if you get positive values, the content being viewed should move to the right and down, + /// revealing new things to the left and up. + /// + /// A positive X-value indicates the content is being moved right, + /// as when swiping right on a touch-screen or track-pad with natural scrolling. + /// + /// A positive Y-value indicates the content is being moved down, + /// as when swiping down on a touch-screen or track-pad with natural scrolling. + delta: Vec2, + + /// The phase of the scroll, useful for trackpads. + /// + /// If unknown set this to [`TouchPhase::Move`]. + phase: TouchPhase, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The native window gained or lost focused (e.g. the user clicked alt-tab). + WindowFocused(bool), + + /// An assistive technology (e.g. screen reader) requested an action. + AccessKitActionRequest(accesskit::ActionRequest), + + /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. + Screenshot { + viewport_id: crate::ViewportId, + + /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. + user_data: crate::UserData, + + image: std::sync::Arc, + }, +} diff --git a/crates/egui/src/data/input/event_filter.rs b/crates/egui/src/data/input/event_filter.rs new file mode 100644 index 000000000..6f0a81316 --- /dev/null +++ b/crates/egui/src/data/input/event_filter.rs @@ -0,0 +1,63 @@ +use super::Event; + +// TODO(emilk): generalize this to a proper event filter. +/// Controls which events that a focused widget will have exclusive access to. +/// +/// Currently this only controls a few special keyboard events, +/// but in the future this `struct` should be extended into a full callback thing. +/// +/// Any events not covered by the filter are given to the widget, but are not exclusive. +#[derive(Clone, Copy, Debug)] +pub struct EventFilter { + /// If `true`, pressing tab will act on the widget, + /// and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub tab: bool, + + /// If `true`, pressing horizontal arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub horizontal_arrows: bool, + + /// If `true`, pressing vertical arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub vertical_arrows: bool, + + /// If `true`, pressing escape will act on the widget, + /// and NOT surrender focus from the focused widget. + /// + /// Default: `false` + pub escape: bool, +} + +#[expect(clippy::derivable_impls)] // let's be explicit +impl Default for EventFilter { + fn default() -> Self { + Self { + tab: false, + horizontal_arrows: false, + vertical_arrows: false, + escape: false, + } + } +} + +impl EventFilter { + pub fn matches(&self, event: &Event) -> bool { + if let Event::Key { key, .. } = event { + match key { + crate::Key::Tab => self.tab, + crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows, + crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows, + crate::Key::Escape => self.escape, + _ => true, + } + } else { + true + } + } +} diff --git a/crates/egui/src/data/input/hovered_file.rs b/crates/egui/src/data/input/hovered_file.rs new file mode 100644 index 000000000..0c558bf79 --- /dev/null +++ b/crates/egui/src/data/input/hovered_file.rs @@ -0,0 +1,10 @@ +/// A file about to be dropped into egui. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct HoveredFile { + /// Set by the `egui-winit` backend. + pub path: Option, + + /// With the `eframe` web backend, this is set to the mime-type of the file (if available). + pub mime: String, +} diff --git a/crates/egui/src/data/input/ime_event.rs b/crates/egui/src/data/input/ime_event.rs new file mode 100644 index 000000000..de12a920f --- /dev/null +++ b/crates/egui/src/data/input/ime_event.rs @@ -0,0 +1,28 @@ +/// IME event. +/// +/// See +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ImeEvent { + /// Notifies when the IME was enabled. + #[deprecated = "No longer used by egui"] + Enabled, + + /// A new IME candidate is being suggested. + /// + /// An empty preedit string indicates that the IME has been dismissed, while + /// a non-empty preedit string indicates that the IME is active. + Preedit { + text: String, + active_range_chars: Option>, + }, + + /// IME composition ended with this final result. + /// + /// The IME is considered dismissed after this event. + Commit(String), + + /// Notifies when the IME was disabled. + #[deprecated = "No longer used by egui"] + Disabled, +} diff --git a/crates/egui/src/data/input/keyboard_shortcut.rs b/crates/egui/src/data/input/keyboard_shortcut.rs new file mode 100644 index 000000000..3998c5c45 --- /dev/null +++ b/crates/egui/src/data/input/keyboard_shortcut.rs @@ -0,0 +1,52 @@ +use crate::Key; + +use super::{ModifierNames, Modifiers}; + +/// A keyboard shortcut, e.g. `Ctrl+Alt+W`. +/// +/// Can be used with [`crate::InputState::consume_shortcut`] +/// and [`crate::Context::format_shortcut`]. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct KeyboardShortcut { + pub modifiers: Modifiers, + + pub logical_key: Key, +} + +impl KeyboardShortcut { + pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self { + Self { + modifiers, + logical_key, + } + } + + pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { + let mut s = names.format(&self.modifiers, is_mac); + if !s.is_empty() { + s += names.concat; + } + if names.is_short { + s += self.logical_key.symbol_or_name(); + } else { + s += self.logical_key.name(); + } + s + } +} + +#[test] +fn format_kb_shortcut() { + let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F); + assert_eq!( + cmd_shift_f.format(&ModifierNames::NAMES, false), + "Ctrl+Shift+F" + ); + assert_eq!( + cmd_shift_f.format(&ModifierNames::NAMES, true), + "Shift+Cmd+F" + ); + assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F"); + assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F"); +} diff --git a/crates/egui/src/data/input/mod.rs b/crates/egui/src/data/input/mod.rs new file mode 100644 index 000000000..b4e739471 --- /dev/null +++ b/crates/egui/src/data/input/mod.rs @@ -0,0 +1,33 @@ +//! The input needed by egui. + +mod dropped_file; +mod event; +mod event_filter; +mod hovered_file; +mod ime_event; +mod keyboard_shortcut; +mod modifier_names; +mod modifiers; +mod mouse_wheel_unit; +mod pointer_button; +mod raw_input; +mod safe_area_insets; +mod touch; +mod viewport_info; + +pub use self::{ + dropped_file::DroppedFile, + event::Event, + event_filter::EventFilter, + hovered_file::HoveredFile, + ime_event::ImeEvent, + keyboard_shortcut::KeyboardShortcut, + modifier_names::ModifierNames, + modifiers::Modifiers, + mouse_wheel_unit::MouseWheelUnit, + pointer_button::{NUM_POINTER_BUTTONS, PointerButton}, + raw_input::RawInput, + safe_area_insets::SafeAreaInsets, + touch::{TouchDeviceId, TouchId, TouchPhase}, + viewport_info::{ViewportEvent, ViewportInfo}, +}; diff --git a/crates/egui/src/data/input/modifier_names.rs b/crates/egui/src/data/input/modifier_names.rs new file mode 100644 index 000000000..585802530 --- /dev/null +++ b/crates/egui/src/data/input/modifier_names.rs @@ -0,0 +1,70 @@ +use super::Modifiers; + +/// Names of different modifier keys. +/// +/// Used to name modifiers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ModifierNames<'a> { + pub is_short: bool, + + pub alt: &'a str, + pub ctrl: &'a str, + pub shift: &'a str, + pub mac_cmd: &'a str, + pub mac_alt: &'a str, + + /// What goes between the names + pub concat: &'a str, +} + +impl ModifierNames<'static> { + /// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font. + pub const SYMBOLS: Self = Self { + is_short: true, + alt: "⌥", + ctrl: "⌃", + shift: "⇧", + mac_cmd: "⌘", + mac_alt: "⌥", + concat: "", + }; + + /// Alt, Ctrl, Shift, Cmd + pub const NAMES: Self = Self { + is_short: false, + alt: "Alt", + ctrl: "Ctrl", + shift: "Shift", + mac_cmd: "Cmd", + mac_alt: "Option", + concat: "+", + }; +} + +impl ModifierNames<'_> { + pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { + let mut s = String::new(); + + let mut append_if = |modifier_is_active, modifier_name| { + if modifier_is_active { + if !s.is_empty() { + s += self.concat; + } + s += modifier_name; + } + }; + + if is_mac { + append_if(modifiers.ctrl, self.ctrl); + append_if(modifiers.shift, self.shift); + append_if(modifiers.alt, self.mac_alt); + append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd); + } else { + append_if(modifiers.ctrl || modifiers.command, self.ctrl); + append_if(modifiers.alt, self.alt); + append_if(modifiers.shift, self.shift); + } + + s + } +} diff --git a/crates/egui/src/data/input/modifiers.rs b/crates/egui/src/data/input/modifiers.rs new file mode 100644 index 000000000..2478ea343 --- /dev/null +++ b/crates/egui/src/data/input/modifiers.rs @@ -0,0 +1,410 @@ +use super::ModifierNames; + +/// State of the modifier keys. These must be fed to egui. +/// +/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. +/// +/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let modifiers = ctx.input(|i| i.modifiers); +/// ``` +/// +/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers +/// as on mac that is how you type special characters, +/// so those key presses are usually not reported to egui. +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Modifiers { + /// Either of the alt keys are down (option ⌥ on Mac). + pub alt: bool, + + /// Either of the control keys are down. + /// When checking for keyboard shortcuts, consider using [`Self::command`] instead. + pub ctrl: bool, + + /// Either of the shift keys are down. + pub shift: bool, + + /// The Mac ⌘ Command key. Should always be set to `false` on other platforms. + pub mac_cmd: bool, + + /// On Windows and Linux, set this to the same value as `ctrl`. + /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). + /// This is so that egui can, for instance, select all text by checking for `command + A` + /// and it will work on both Mac and Windows. + pub command: bool, +} + +impl std::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_none() { + return write!(f, "Modifiers::NONE"); + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + let mut debug = f.debug_struct("Modifiers"); + if alt { + debug.field("alt", &true); + } + if ctrl { + debug.field("ctrl", &true); + } + if shift { + debug.field("shift", &true); + } + if mac_cmd { + debug.field("mac_cmd", &true); + } + if command { + debug.field("command", &true); + } + debug.finish() + } +} + +impl Modifiers { + pub const NONE: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: false, + command: false, + }; + + pub const ALT: Self = Self { + alt: true, + ctrl: false, + shift: false, + mac_cmd: false, + command: false, + }; + pub const CTRL: Self = Self { + alt: false, + ctrl: true, + shift: false, + mac_cmd: false, + command: false, + }; + pub const SHIFT: Self = Self { + alt: false, + ctrl: false, + shift: true, + mac_cmd: false, + command: false, + }; + + /// The Mac ⌘ Command key + pub const MAC_CMD: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: true, + command: false, + }; + + /// On Mac: ⌘ Command key, elsewhere: Ctrl key + pub const COMMAND: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: false, + command: true, + }; + + /// ``` + /// # use egui::Modifiers; + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers { ctrl: true, alt: true, ..Default::default() } + /// ); + /// assert_eq!( + /// Modifiers::ALT.plus(Modifiers::CTRL), + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// ``` + #[inline] + pub const fn plus(self, rhs: Self) -> Self { + Self { + alt: self.alt | rhs.alt, + ctrl: self.ctrl | rhs.ctrl, + shift: self.shift | rhs.shift, + mac_cmd: self.mac_cmd | rhs.mac_cmd, + command: self.command | rhs.command, + } + } + + #[inline] + pub fn is_none(&self) -> bool { + self == &Self::default() + } + + #[inline] + pub fn any(&self) -> bool { + !self.is_none() + } + + #[inline] + pub fn all(&self) -> bool { + self.alt && self.ctrl && self.shift && self.command + } + + /// Is shift the only pressed button? + #[inline] + pub fn shift_only(&self) -> bool { + self.shift && !(self.alt || self.command) + } + + /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. + #[inline] + pub fn command_only(&self) -> bool { + !self.alt && !self.shift && self.command + } + + /// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset + /// of the pressed key (`self`). + /// + /// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not. + /// + /// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts. + /// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but a Swedish keyboard has dedicated `+` key. + /// So if you want to make a [`KeyboardShortcut`](crate::KeyboardShortcut) looking for `Cmd` + `+`, it makes sense + /// to ignore the shift key. + /// Similarly, the `Alt` key is sometimes used to type special characters. + /// + /// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys + /// to be pressed, then they must be pressed. + /// + /// # Example: + /// ``` + /// # use egui::Modifiers; + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) { + /// // Alt and Shift are pressed, but not ctrl/command + /// } + /// ``` + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD)); + /// ``` + pub fn matches_logically(&self, pattern: Self) -> bool { + if pattern.alt && !self.alt { + return false; + } + if pattern.shift && !self.shift { + return false; + } + + self.cmd_ctrl_matches(pattern) + } + + /// Check for equality but with proper handling of [`Self::command`]. + /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// Note that this will require the `shift` and `alt` keys match, even though + /// these modifiers are sometimes required to produce some logical keys. + /// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but on a Swedish keyboard you can press the dedicated `+` key. + /// Therefore, you often want to use [`Self::matches_logically`] instead. + /// + /// # Example: + /// ``` + /// # use egui::Modifiers; + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) { + /// // Alt and Shift are pressed, and nothing else + /// } + /// ``` + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); + /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); + /// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD)); + /// ``` + pub fn matches_exact(&self, pattern: Self) -> bool { + // alt and shift must always match the pattern: + if pattern.alt != self.alt || pattern.shift != self.shift { + return false; + } + + self.cmd_ctrl_matches(pattern) + } + + /// Check if any of the modifiers match exactly. + /// + /// Returns true if the same modifier is pressed in `self` as in `pattern`, + /// for at least one modifier. + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL)); + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL)); + /// ``` + pub fn matches_any(&self, pattern: Self) -> bool { + if self.alt && pattern.alt { + return true; + } + if self.shift && pattern.shift { + return true; + } + if self.ctrl && pattern.ctrl { + return true; + } + if self.mac_cmd && pattern.mac_cmd { + return true; + } + if (self.mac_cmd || self.command || self.ctrl) && pattern.command { + return true; + } + false + } + + /// Checks only cmd/ctrl, not alt/shift. + /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// This takes care to properly handle the difference between + /// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`]. + pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool { + if pattern.mac_cmd { + // Mac-specific match: + if !self.mac_cmd { + return false; + } + if pattern.ctrl != self.ctrl { + return false; + } + return true; + } + + if !pattern.ctrl && !pattern.command { + // the pattern explicitly doesn't want any ctrl/command: + return !self.ctrl && !self.command; + } + + // if the pattern is looking for command, then `ctrl` may or may not be set depending on platform. + // if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform. + + if pattern.ctrl && !self.ctrl { + return false; + } + if pattern.command && !self.command { + return false; + } + + true + } + + /// Whether another set of modifiers is contained in this set of modifiers with proper handling of [`Self::command`]. + /// + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::default().contains(Modifiers::default())); + /// assert!(Modifiers::CTRL.contains(Modifiers::default())); + /// assert!(Modifiers::CTRL.contains(Modifiers::CTRL)); + /// assert!(Modifiers::CTRL.contains(Modifiers::COMMAND)); + /// assert!(Modifiers::MAC_CMD.contains(Modifiers::COMMAND)); + /// assert!(Modifiers::COMMAND.contains(Modifiers::MAC_CMD)); + /// assert!(Modifiers::COMMAND.contains(Modifiers::CTRL)); + /// assert!(!(Modifiers::ALT | Modifiers::CTRL).contains(Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT)); + /// ``` + pub fn contains(&self, query: Self) -> bool { + if query == Self::default() { + return true; + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + if alt && query.alt { + return self.contains(Self { + alt: false, + ..query + }); + } + if shift && query.shift { + return self.contains(Self { + shift: false, + ..query + }); + } + + if (ctrl || command) && (query.ctrl || query.command) { + return self.contains(Self { + command: false, + ctrl: false, + ..query + }); + } + if (mac_cmd || command) && (query.mac_cmd || query.command) { + return self.contains(Self { + mac_cmd: false, + command: false, + ..query + }); + } + + false + } +} + +impl std::ops::BitOr for Modifiers { + type Output = Self; + + #[inline] + fn bitor(self, rhs: Self) -> Self { + self.plus(rhs) + } +} + +impl std::ops::BitOrAssign for Modifiers { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +impl Modifiers { + pub fn ui(&self, ui: &mut crate::Ui) { + ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac())); + } +} diff --git a/crates/egui/src/data/input/mouse_wheel_unit.rs b/crates/egui/src/data/input/mouse_wheel_unit.rs new file mode 100644 index 000000000..d543d01a8 --- /dev/null +++ b/crates/egui/src/data/input/mouse_wheel_unit.rs @@ -0,0 +1,13 @@ +/// The unit associated with the numeric value of a mouse wheel event +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum MouseWheelUnit { + /// Number of ui points (logical pixels) + Point, + + /// Number of lines + Line, + + /// Number of pages + Page, +} diff --git a/crates/egui/src/data/input/pointer_button.rs b/crates/egui/src/data/input/pointer_button.rs new file mode 100644 index 000000000..cfab22a09 --- /dev/null +++ b/crates/egui/src/data/input/pointer_button.rs @@ -0,0 +1,23 @@ +/// Mouse button (or similar for touch input) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum PointerButton { + /// The primary mouse button is usually the left one. + Primary = 0, + + /// The secondary mouse button is usually the right one, + /// and most often used for context menus or other optional things. + Secondary = 1, + + /// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel). + Middle = 2, + + /// The first extra mouse button on some mice. In web typically corresponds to the Browser back button. + Extra1 = 3, + + /// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button. + Extra2 = 4, +} + +/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`]. +pub const NUM_POINTER_BUTTONS: usize = 5; diff --git a/crates/egui/src/data/input/raw_input.rs b/crates/egui/src/data/input/raw_input.rs new file mode 100644 index 000000000..2ba2caada --- /dev/null +++ b/crates/egui/src/data/input/raw_input.rs @@ -0,0 +1,225 @@ +use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect}; + +use super::{DroppedFile, Event, HoveredFile, Modifiers, SafeAreaInsets, ViewportInfo}; + +/// What the integrations provides to egui at the start of each frame. +/// +/// Set the values that make sense, leave the rest at their `Default::default()`. +/// +/// You can check if `egui` is using the inputs using +/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`]. +/// +/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. +/// +/// Ii "points" can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`; +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RawInput { + /// The id of the active viewport. + pub viewport_id: ViewportId, + + /// Information about all egui viewports. + pub viewports: ViewportIdMap, + + /// The insets used to only render content in a mobile safe area + /// + /// `None` will be treated as "same as last frame" + pub safe_area_insets: Option, + + /// Position and size of the area that egui should use, in points. + /// Usually you would set this to + /// + /// `Some(Rect::from_min_size(Default::default(), screen_size_in_points))`. + /// + /// but you could also constrain egui to some smaller portion of your window if you like. + /// + /// `None` will be treated as "same as last frame", with the default being a very big area. + pub screen_rect: Option, + + /// Maximum size of one side of the font texture. + /// + /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. + /// + /// The default is a very small (but very portable) 2048. + pub max_texture_side: Option, + + /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. + /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). + pub time: Option, + + /// Should be set to the expected time between frames when painting at vsync speeds. + /// The default for this is 1/60. + /// Can safely be left at its default value. + pub predicted_dt: f32, + + /// Which modifier keys are down at the start of the frame? + pub modifiers: Modifiers, + + /// In-order events received this frame. + /// + /// There is currently no way to know if egui handles a particular event, + /// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`] + /// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`]. + pub events: Vec, + + /// Dragged files hovering over egui. + pub hovered_files: Vec, + + /// Dragged files dropped into egui. + /// + /// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has + /// been disabled in [`crate::viewport::ViewportBuilder`]. + pub dropped_files: Vec, + + /// The native window has the keyboard focus (i.e. is receiving key presses). + /// + /// False when the user alt-tab away from the application, for instance. + pub focused: bool, + + /// Does the OS use dark or light mode? + /// + /// `None` means "don't know". + pub system_theme: Option, +} + +impl Default for RawInput { + fn default() -> Self { + Self { + viewport_id: ViewportId::ROOT, + viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), + screen_rect: None, + max_texture_side: None, + time: None, + predicted_dt: 1.0 / 60.0, + modifiers: Modifiers::default(), + events: vec![], + hovered_files: Default::default(), + dropped_files: Default::default(), + focused: true, // integrations opt into global focus tracking + system_theme: None, + safe_area_insets: Default::default(), + } + } +} + +impl RawInput { + /// Info about the active viewport + #[inline] + pub fn viewport(&self) -> &ViewportInfo { + self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") + } + + /// Helper: move volatile (deltas and events), clone the rest. + /// + /// * [`Self::hovered_files`] is cloned. + /// * [`Self::dropped_files`] is moved. + pub fn take(&mut self) -> Self { + Self { + viewport_id: self.viewport_id, + viewports: self + .viewports + .iter_mut() + .map(|(id, info)| (*id, info.take())) + .collect(), + screen_rect: self.screen_rect.take(), + safe_area_insets: self.safe_area_insets.take(), + max_texture_side: self.max_texture_side.take(), + time: self.time, + predicted_dt: self.predicted_dt, + modifiers: self.modifiers, + events: std::mem::take(&mut self.events), + hovered_files: self.hovered_files.clone(), + dropped_files: std::mem::take(&mut self.dropped_files), + focused: self.focused, + system_theme: self.system_theme, + } + } + + /// Add on new input. + pub fn append(&mut self, newer: Self) { + let Self { + viewport_id: viewport_ids, + viewports, + screen_rect, + max_texture_side, + time, + predicted_dt, + modifiers, + mut events, + mut hovered_files, + mut dropped_files, + focused, + system_theme, + safe_area_insets: safe_area, + } = newer; + + self.viewport_id = viewport_ids; + self.viewports = viewports; + self.screen_rect = screen_rect.or(self.screen_rect); + self.max_texture_side = max_texture_side.or(self.max_texture_side); + self.time = time; // use latest time + self.predicted_dt = predicted_dt; // use latest dt + self.modifiers = modifiers; // use latest + self.events.append(&mut events); + self.hovered_files.append(&mut hovered_files); + self.dropped_files.append(&mut dropped_files); + self.focused = focused; + self.system_theme = system_theme; + self.safe_area_insets = safe_area; + } +} + +impl RawInput { + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + viewport_id, + viewports, + screen_rect, + max_texture_side, + time, + predicted_dt, + modifiers, + events, + hovered_files, + dropped_files, + focused, + system_theme, + safe_area_insets: safe_area, + } = self; + + ui.label(format!("Active viewport: {viewport_id:?}")); + let ordered_viewports = viewports + .iter() + .map(|(id, value)| (*id, value)) + .collect::>(); + for (id, viewport) in ordered_viewports { + ui.group(|ui| { + ui.label(format!("Viewport {id:?}")); + ui.push_id(id, |ui| { + viewport.ui(ui); + }); + }); + } + ui.label(format!("screen_rect: {screen_rect:?} points")); + + ui.label(format!("max_texture_side: {max_texture_side:?}")); + if let Some(time) = time { + ui.label(format!("time: {time:.3} s")); + } else { + ui.label("time: None"); + } + ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); + ui.label(format!("modifiers: {modifiers:#?}")); + ui.label(format!("hovered_files: {}", hovered_files.len())); + ui.label(format!("dropped_files: {}", dropped_files.len())); + ui.label(format!("focused: {focused}")); + ui.label(format!("system_theme: {system_theme:?}")); + ui.label(format!("safe_area: {safe_area:?}")); + ui.scope(|ui| { + ui.set_min_height(150.0); + ui.label(format!("events: {events:#?}")) + .on_hover_text("key presses etc"); + }); + } +} diff --git a/crates/egui/src/data/input/safe_area_insets.rs b/crates/egui/src/data/input/safe_area_insets.rs new file mode 100644 index 000000000..914d227c2 --- /dev/null +++ b/crates/egui/src/data/input/safe_area_insets.rs @@ -0,0 +1,19 @@ +use epaint::MarginF32; + +use crate::emath::Rect; + +/// The 'safe area' insets of the screen +/// +/// This represents the area taken up by the status bar, navigation controls, notches, +/// or any other items that obscure parts of the screen. +#[derive(Debug, PartialEq, Copy, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct SafeAreaInsets(pub MarginF32); + +impl std::ops::Sub for Rect { + type Output = Self; + + fn sub(self, rhs: SafeAreaInsets) -> Self::Output { + self - rhs.0 + } +} diff --git a/crates/egui/src/data/input/touch.rs b/crates/egui/src/data/input/touch.rs new file mode 100644 index 000000000..e269e437c --- /dev/null +++ b/crates/egui/src/data/input/touch.rs @@ -0,0 +1,50 @@ +/// this is a `u64` as values of this kind can always be obtained by hashing +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct TouchDeviceId(pub u64); + +/// Unique identification of a touch occurrence (finger or pen or …). +/// A Touch ID is valid until the finger is lifted. +/// A new ID is used for the next touch. +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct TouchId(pub u64); + +/// In what phase a touch event is in. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum TouchPhase { + /// User just placed a touch point on the touch surface + Start, + + /// User moves a touch point along the surface. This event is also sent when + /// any attributes (position, force, …) of the touch point change. + Move, + + /// User lifted the finger or pen from the surface, or slid off the edge of + /// the surface + End, + + /// Touch operation has been disrupted by something (various reasons are possible, + /// maybe a pop-up alert or any other kind of interruption which may not have + /// been intended by the user) + Cancel, +} + +impl From for TouchId { + fn from(id: u64) -> Self { + Self(id) + } +} + +impl From for TouchId { + fn from(id: i32) -> Self { + Self(id as u64) + } +} + +impl From for TouchId { + fn from(id: u32) -> Self { + Self(id as u64) + } +} diff --git a/crates/egui/src/data/input/viewport_info.rs b/crates/egui/src/data/input/viewport_info.rs new file mode 100644 index 000000000..774ca1e6e --- /dev/null +++ b/crates/egui/src/data/input/viewport_info.rs @@ -0,0 +1,217 @@ +use crate::emath::{Rect, Vec2}; + +/// An input event from the backend into egui, about a specific [viewport](crate::viewport). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ViewportEvent { + /// The user clicked the close-button on the window, or similar. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + /// + /// This even will wake up both the child and parent viewport. + Close, +} + +/// Information about the current viewport, given as input each frame. +/// +/// `None` means "unknown". +/// +/// All units are in ui "points", which can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportInfo { + /// Parent viewport, if known. + pub parent: Option, + + /// Name of the viewport, if known. + pub title: Option, + + pub events: Vec, + + /// The OS native pixels-per-point. + /// + /// This should always be set, if known. + /// + /// On web this takes browser scaling into account, + /// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. + pub native_pixels_per_point: Option, + + /// Current monitor size in egui points. + pub monitor_size: Option, + + /// The inner rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle of the viewport. + /// + /// **`eframe` notes**: + /// + /// On Android / Wayland, this will always be `None` since getting the + /// position of the window is not possible. + pub inner_rect: Option, + + /// The outer rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle plus decoration chrome. + /// + /// **`eframe` notes**: + /// + /// On Android / Wayland, this will always be `None` since getting the + /// position of the window is not possible. + pub outer_rect: Option, + + /// Are we minimized? + pub minimized: Option, + + /// Are we maximized? + pub maximized: Option, + + /// Are we in fullscreen mode? + pub fullscreen: Option, + + /// Is the window focused and able to receive input? + /// + /// This should be the same as [`RawInput::focused`](crate::RawInput::focused). + pub focused: Option, + + /// Is the window fully occluded (completely covered) by another window? + /// + /// Not all platforms support this. + /// On platforms that don't, this will be `None` or `Some(false)`. + pub occluded: Option, +} + +impl ViewportInfo { + /// Is the window considered visible for rendering purposes? + /// + /// A window is not visible if it is minimized or occluded. + /// When not visible, the UI is not painted and rendering is skipped, + /// but application logic may still be executed by some integrations. + pub fn visible(&self) -> Option { + match (self.minimized, self.occluded) { + (Some(true), _) | (_, Some(true)) => Some(false), + (Some(false), Some(false)) => Some(true), + (_, None) | (None, _) => None, + } + } + + /// This viewport has been told to close. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + pub fn close_requested(&self) -> bool { + self.events.contains(&ViewportEvent::Close) + } + + /// Helper: move [`Self::events`], clone the other fields. + pub fn take(&mut self) -> Self { + Self { + parent: self.parent, + title: self.title.clone(), + events: std::mem::take(&mut self.events), + native_pixels_per_point: self.native_pixels_per_point, + monitor_size: self.monitor_size, + inner_rect: self.inner_rect, + outer_rect: self.outer_rect, + minimized: self.minimized, + maximized: self.maximized, + fullscreen: self.fullscreen, + focused: self.focused, + occluded: self.occluded, + } + } + + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + parent, + title, + events, + native_pixels_per_point, + monitor_size, + inner_rect, + outer_rect, + minimized, + maximized, + fullscreen, + focused, + occluded, + } = self; + + crate::Grid::new("viewport_info").show(ui, |ui| { + ui.label("Parent:"); + ui.label(opt_as_str(parent)); + ui.end_row(); + + ui.label("Title:"); + ui.label(opt_as_str(title)); + ui.end_row(); + + ui.label("Events:"); + ui.label(format!("{events:?}")); + ui.end_row(); + + ui.label("Native pixels-per-point:"); + ui.label(opt_as_str(native_pixels_per_point)); + ui.end_row(); + + ui.label("Monitor size:"); + ui.label(opt_as_str(monitor_size)); + ui.end_row(); + + ui.label("Inner rect:"); + ui.label(opt_rect_as_string(inner_rect)); + ui.end_row(); + + ui.label("Outer rect:"); + ui.label(opt_rect_as_string(outer_rect)); + ui.end_row(); + + ui.label("Minimized:"); + ui.label(opt_as_str(minimized)); + ui.end_row(); + + ui.label("Maximized:"); + ui.label(opt_as_str(maximized)); + ui.end_row(); + + ui.label("Fullscreen:"); + ui.label(opt_as_str(fullscreen)); + ui.end_row(); + + ui.label("Focused:"); + ui.label(opt_as_str(focused)); + ui.end_row(); + + ui.label("Occluded:"); + ui.label(opt_as_str(occluded)); + ui.end_row(); + + let visible = self.visible(); + + ui.label("Visible:"); + ui.label(opt_as_str(&visible)); + ui.end_row(); + + #[expect(clippy::ref_option)] + fn opt_rect_as_string(v: &Option) -> String { + v.as_ref().map_or(String::new(), |r| { + format!("Pos: {:?}, size: {:?}", r.min, r.size()) + }) + } + + #[expect(clippy::ref_option)] + fn opt_as_str(v: &Option) -> String { + v.as_ref().map_or(String::new(), |v| format!("{v:?}")) + } + }); + } +} diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 2a0f33fc3..edcc58f42 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -188,6 +188,38 @@ pub enum Key { /// Android sends this key on Back button press. /// Does not work on Web. BrowserBack, + + // ---------------------------------------------- + // Modifier keys (exposed as distinct left/right variants so that + // games and input-capture UIs can bind them independently). egui's + // `Modifiers` struct still collapses both sides for the common case + // (e.g. "Ctrl+C"); these variants are emitted only as physical + // `Event::Key` presses. + /// Left Shift key. + ShiftLeft, + /// Right Shift key. + ShiftRight, + /// Left Control key. + ControlLeft, + /// Right Control key. + ControlRight, + /// Left Alt / Option key. + AltLeft, + /// Right Alt / AltGr / Option key. + AltRight, + /// Left Super / Meta / Command / Windows key. + SuperLeft, + /// Right Super / Meta / Command / Windows key. + SuperRight, + + // ---------------------------------------------- + // International keys — physical positions that only exist on + // non-US keyboards. + /// ISO 102nd key: physically located between the left Shift and Z + /// on ISO layouts. On French AZERTY it produces `<>|`; on UK + /// QWERTY a secondary `\` / `|`. Missing from US ANSI keyboards. + IntlBackslash, + // When adding keys, remember to also update: // * crates/egui-winit/src/lib.rs // * Key::ALL @@ -314,6 +346,17 @@ impl Key { Self::F35, // Navigation keys: Self::BrowserBack, + // Modifier keys (physical L/R): + Self::ShiftLeft, + Self::ShiftRight, + Self::ControlLeft, + Self::ControlRight, + Self::AltLeft, + Self::AltRight, + Self::SuperLeft, + Self::SuperRight, + // International keys: + Self::IntlBackslash, ]; /// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc. @@ -444,6 +487,17 @@ impl Key { "BrowserBack" => Self::BrowserBack, + "ShiftLeft" => Self::ShiftLeft, + "ShiftRight" => Self::ShiftRight, + "ControlLeft" => Self::ControlLeft, + "ControlRight" => Self::ControlRight, + "AltLeft" => Self::AltLeft, + "AltRight" => Self::AltRight, + "SuperLeft" => Self::SuperLeft, + "SuperRight" => Self::SuperRight, + + "IntlBackslash" => Self::IntlBackslash, + _ => return None, }) } @@ -599,6 +653,17 @@ impl Key { Self::F35 => "F35", Self::BrowserBack => "BrowserBack", + + Self::ShiftLeft => "ShiftLeft", + Self::ShiftRight => "ShiftRight", + Self::ControlLeft => "ControlLeft", + Self::ControlRight => "ControlRight", + Self::AltLeft => "AltLeft", + Self::AltRight => "AltRight", + Self::SuperLeft => "SuperLeft", + Self::SuperRight => "SuperRight", + + Self::IntlBackslash => "IntlBackslash", } } } @@ -607,7 +672,7 @@ impl Key { fn test_key_from_name() { assert_eq!( Key::ALL.len(), - Key::BrowserBack as usize + 1, + Key::IntlBackslash as usize + 1, "Some keys are missing in Key::ALL" ); diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c0e21fbe2..e7bb4b2e2 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -65,7 +65,12 @@ impl Id { /// Generate a new root [`Id`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsId) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source)); + + #[cfg(debug_assertions)] + id_source::insert_root(id, &source); + + id } /// Generate a child [`Id`] by salting the parent [`Id`] with the given argument. @@ -73,8 +78,13 @@ impl Id { use std::hash::{BuildHasher as _, Hasher as _}; let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); hasher.write_u64(self.value()); - hasher.write_u64(IdSalt::new(salt).value()); - Self::from_hash(hasher.finish()) + hasher.write_u64(IdSalt::new(&salt).value()); + let id = Self::from_hash(hasher.finish()); + + #[cfg(debug_assertions)] + id_source::insert_child(id, self, &salt); + + id } /// Short and readable summary @@ -116,10 +126,19 @@ impl Id { impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:04X}", self.value() as u16) + if *self == Self::NULL { + return write!(f, "Id::NULL"); + } + #[cfg(debug_assertions)] + if let Some(source) = id_source::get(*self) { + return f.write_str(&source); + } + write!(f, "id_{:04X}", self.value() as u16) } } +// ---------------------------------------------------------------------------- + /// Convenience impl From<&'static str> for Id { #[inline] @@ -135,12 +154,6 @@ impl From for Id { } } -#[test] -fn id_size() { - assert_eq!(std::mem::size_of::(), 8); - assert_eq!(std::mem::size_of::>(), 8); -} - // ---------------------------------------------------------------------------- /// `IdSet` is a `HashSet` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. @@ -148,3 +161,108 @@ pub type IdSet = nohash_hasher::IntSet; /// `IdMap` is a `HashMap` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. pub type IdMap = nohash_hasher::IntMap; + +// ---------------------------------------------------------------------------- + +/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`]. +/// +/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`, +/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc. +#[cfg(debug_assertions)] +mod id_source { + use super::{AsId, AsIdSalt, Id, IdMap}; + use epaint::mutex::RwLock; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn insert_root(id: Id, source: &impl AsId) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl. + let formatted = format!("Id::new({source:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Look up parent's repr and drop the read guard before formatting, + // since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl. + let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned(); + let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}")); + let formatted = format!("{parent_repr}.with({salt:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn get(id: Id) -> Option { + SOURCE_MAP.read().get(&id).cloned() + } +} + +#[test] +fn id_size() { + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::>(), 8); +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod debug_format_tests { + use crate::IdSalt; + + use super::Id; + + #[test] + fn root_string() { + let id = Id::new("foo"); + assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#); + } + + #[test] + fn root_integer() { + let id = Id::new(42_i32); + assert_eq!(format!("{id:?}"), "Id::new(42)"); + } + + #[test] + fn root_id_salt() { + let id = Id::new(IdSalt::new("foo")); + assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#); + } + + #[test] + fn with_one_child() { + let id = Id::new("parent").with("child"); + assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#); + } + + #[test] + fn with_chain() { + let id = Id::new("a").with("b").with("c").with(7_i32); + assert_eq!( + format!("{id:?}"), + r#"Id::new("a").with("b").with("c").with(7)"# + ); + } + + #[test] + fn nested_id_as_source() { + let inner = Id::new("foo"); + let outer = Id::new(inner); + assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#); + } + + #[test] + fn null_prints_as_null() { + assert_eq!(format!("{:?}", Id::NULL), "Id::NULL"); + } + + #[test] + fn null_as_parent() { + let id = Id::NULL.with("foo"); + assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#); + } +} diff --git a/crates/egui/src/id_salt.rs b/crates/egui/src/id_salt.rs index 0912dcbd3..486dda239 100644 --- a/crates/egui/src/id_salt.rs +++ b/crates/egui/src/id_salt.rs @@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {} impl IdSalt { /// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsIdSalt) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source)); + + #[cfg(debug_assertions)] + id_salt_source::maybe_insert(id_salt, &source); + + id_salt } /// Create a new root [`IdSalt`] from a high-entropy hash. @@ -54,6 +59,78 @@ impl IdSalt { impl std::fmt::Debug for IdSalt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + if let Some(source) = id_salt_source::get(*self) { + return write!(f, "IdSalt::new({source})"); + } write!(f, "salt_{:04X}", self.value() as u16) } } + +/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`]. +/// +/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as +/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as +/// `IdSalt::new(IdSalt::new("foo"))`, etc. +#[cfg(debug_assertions)] +mod id_salt_source { + use super::{AsIdSalt, IdSalt}; + use epaint::mutex::RwLock; + use nohash_hasher::IntMap; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) { + if !SOURCE_MAP.read().contains_key(&id_salt) { + let formatted = format!("{source:?}"); + SOURCE_MAP.write().insert(id_salt, formatted); + } + } + + pub(super) fn get(id_salt: IdSalt) -> Option { + SOURCE_MAP.read().get(&id_salt).cloned() + } +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod tests { + use super::IdSalt; + + #[test] + fn debug_format_string_source() { + let salt = IdSalt::new("foo"); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#); + } + + #[test] + fn debug_format_integer_source() { + let salt = IdSalt::new(42_i32); + assert_eq!(format!("{salt:?}"), "IdSalt::new(42)"); + } + + #[test] + fn debug_format_nested_salt() { + let inner = IdSalt::new("foo"); + let outer = IdSalt::new(inner); + assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#); + } + + #[test] + fn debug_format_triple_nested_salt() { + let a = IdSalt::new("foo"); + let b = IdSalt::new(a); + let c = IdSalt::new(b); + assert_eq!( + format!("{c:?}"), + r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"# + ); + } + + #[test] + fn debug_format_tuple_source() { + let salt = IdSalt::new(("foo", 7_i32)); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#); + } +} diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index b65dfdffa..43694453c 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -490,6 +490,13 @@ pub(crate) struct Focus { /// The ID of a widget that had keyboard focus during the previous frame. id_previous_frame: Option, + /// The ID of a widget that had keyboard focus *two* frames ago. + /// + /// Kept so `Response::lost_focus` can still fire after a mid-frame + /// focus transition (e.g. clicking a `TextEdit` that was added to + /// the UI later than the currently focused one). + id_two_frames_ago: Option, + /// The ID of a widget to give the focus to in the next frame. id_next_frame: Option, @@ -545,6 +552,7 @@ impl Focus { } fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) { + self.id_two_frames_ago = self.id_previous_frame; self.id_previous_frame = self.focused(); if let Some(id) = self.id_next_frame.take() { self.focused_widget = Some(FocusWidget::new(id)); @@ -831,10 +839,21 @@ impl Memory { self.focus().and_then(|f| f.id_previous_frame) == Some(id) } - /// Check if the layer lost focus last frame. - /// returns `true` if the layer lost focus last frame, but not this one. + /// Check if the widget lost keyboard focus. + /// + /// Returns `true` when `id` was the focused widget at the start + /// of this frame *or* the start of the previous frame — but is + /// not focused now. The two-frame window matters when focus + /// transfers mid-frame: the previously-focused widget has + /// usually already been rendered by the time another widget + /// claims focus, so the loss signal can only reach it on its + /// next render pass. pub(crate) fn lost_focus(&self, id: Id) -> bool { - self.had_focus_last_frame(id) && !self.has_focus(id) + let had_recent_focus = self + .focus() + .map(|f| f.id_previous_frame == Some(id) || f.id_two_frames_ago == Some(id)) + .unwrap_or(false); + had_recent_focus && !self.has_focus(id) } /// Check if the layer gained focus this frame. @@ -1368,6 +1387,54 @@ fn memory_impl_send_sync() { assert_send_sync::(); } +// Regression test for https://github.com/emilk/egui/issues/2142. +#[test] +fn lost_focus_fires_after_mid_frame_focus_transfer() { + use crate::data::input::RawInput; + let a = Id::new("A"); + let b = Id::new("B"); + let mut focus = Focus::default(); + let raw = RawInput::default(); + + fn lost_focus_check(focus: &Focus, id: Id) -> bool { + let was_focused = + focus.id_previous_frame == Some(id) || focus.id_two_frames_ago == Some(id); + was_focused && focus.focused() != Some(id) + } + + // Frame N-1 + { + focus.begin_pass(&raw); + focus.focused_widget = Some(FocusWidget::new(a)); + } + + // Frame N: `A` is focused at start; user clicks `B` mid-frame + { + focus.begin_pass(&raw); + assert_eq!(focus.id_previous_frame, Some(a)); + assert!(!lost_focus_check(&focus, a)); + focus.focused_widget = Some(FocusWidget::new(b)); + } + + // Frame N+1: `A` deferred lost_focus signal must fire + { + focus.begin_pass(&raw); + assert_eq!(focus.id_two_frames_ago, Some(a)); + assert_eq!(focus.id_previous_frame, Some(b)); + assert!(lost_focus_check(&focus, a), "`A` lost_focus must fire"); + assert!(!lost_focus_check(&focus, b)); + } + + // Frame N+2 + { + focus.begin_pass(&raw); + assert!( + !lost_focus_check(&focus, a), + "A's lost_focus must stop firing once the two-frame window passes", + ); + } +} + #[test] fn order_map_total_ordering() { let mut layers = [ diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 0900d0d06..67ed692a2 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -117,6 +117,8 @@ impl UiBuilder { /// Make the new `Ui` disabled, i.e. grayed-out and non-interactive. /// /// Note that if the parent `Ui` is disabled, the child will always be disabled. + /// + /// See also [`crate::Ui::add_enabled`], [`crate::Ui::add_enabled_ui`] and [`crate::Ui::is_enabled`]. #[inline] pub fn disabled(mut self) -> Self { self.disabled = true; diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index e4dc95335..a85c2a7b4 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -85,7 +85,29 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) { } } -fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { +/// Show a color with background checkers to demonstrate transparency (if any). +fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response { + let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover()); + if ui.is_rect_visible(rect) { + show_srgba_unmultiplied_at(ui.painter(), srgba, rect); + } + response +} + +/// Show a color with background checkers to demonstrate transparency (if any). +fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) { + if a == 255 { + painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b)); + } else { + background_checkers(painter, rect); + let left = Rect::from_min_max(rect.left_top(), rect.center_bottom()); + let right = Rect::from_min_max(rect.center_top(), rect.right_bottom()); + painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a)); + painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b)); + } +} + +fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response { let size = ui.spacing().interact_size; let (rect, response) = ui.allocate_exact_size(size, Sense::click()); response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton)); @@ -99,7 +121,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { let rect = rect.expand(visuals.expansion); let stroke_width = 1.0; - show_color_at(ui.painter(), color, rect.shrink(stroke_width)); + show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width)); let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding ui.painter().rect_stroke( @@ -314,7 +336,12 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) { } let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y); - show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color"); + show_srgba_unmultiplied( + ui, + Hsva::from(*hsvag).to_srgba_unmultiplied(), + current_color_size, + ) + .on_hover_text("Selected color"); if alpha == Alpha::BlendOrAdditive { let a = &mut hsvag.a; @@ -491,7 +518,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response { let popup_id = ui.auto_id_with("popup"); let open = Popup::is_id_open(ui.ctx(), popup_id); - let mut button_response = color_button(ui, (*hsva).into(), open); + let mut button_response = color_button(ui, hsva.to_srgba_unmultiplied(), open); if ui.style().explanation_tooltips { button_response = button_response.on_hover_text("Click to edit color"); } diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index c47f260e9..80725c619 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390 -size 98934 +oid sha256:1cf4c34af7b69cd8220b11ff7e355ddf8d7b52a43a60a1748abf9cc1d5c7da9b +size 98869 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index f26458ea5..53575912a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1 -size 114038 +oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b +size 114174 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 09763f516..f2494709e 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7 -size 45061 +oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23 +size 45044 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index 85a4fa0da..e88d67b6b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3 -size 118907 +oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6 +size 118889 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index ec2750465..44c159f4f 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8 -size 35960 +oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c +size 35943 diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index f2685a11b..45c1b9f56 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 9147b3c0d..6fb5e014e 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -634,7 +634,11 @@ impl TableState { InitialColumnSize::Automatic(_) => Size::exact(*prev_width), InitialColumnSize::Remainder => Size::remainder(), } - .at_least(column.width_range.min.max(*max_used)) + .at_least(if column.clip { + column.width_range.min + } else { + column.width_range.min.max(*max_used) + }) .at_most(column.width_range.max) }; sizing.add(size); diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 32ed885cd..83cf74472 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index bc7f428ba..1c69ee38b 100644 --- a/crates/emath/CHANGELOG.md +++ b/crates/emath/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index e6ccb10a1..7d0781d52 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 * Fix text layout bugs in wrapped texts [#8137](https://github.com/emilk/egui/pull/8137) by [@lucasmerlin](https://github.com/lucasmerlin) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index ee3a39e22..35afd930b 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new