diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b33189280..206829e7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Please keep pull requests small and focused. The smaller it is, the more likely Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs! -It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpituity, so we have a high bar on what get merged! +It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged! When reviewing, we look for: * The PR title and description should be helpful @@ -123,7 +123,7 @@ with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards. `egui` uses logical _points_ as its coordinate system. Those related to physical _pixels_ by the `pixels_per_point` scale factor. -For example, a high-dpi screeen can have `pixels_per_point = 2.0`, +For example, a high-dpi screen can have `pixels_per_point = 2.0`, meaning there are two physical screen pixels for each logical point. Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. diff --git a/Cargo.lock b/Cargo.lock index 92948c458..ba8d5a17a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1072,6 +1072,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_keypad" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", +] + [[package]] name = "custom_plot_manipulation" version = "0.1.0" diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 092e332c7..da1aea6ba 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -239,4 +239,8 @@ web-sys = { version = "0.3.58", features = [ # optional web: egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit -wgpu = { workspace = true, optional = true } +wgpu = { workspace = true, optional = true, features = [ + # Let's enable some backends so that users can use `eframe` out-of-the-box + # without having to explicitly opt-in to backends + "webgpu", +] } diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index e26bece3a..16a8a6891 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -196,6 +196,24 @@ pub trait App { fn persist_egui_memory(&self) -> bool { true } + + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// + /// This function provides a way to modify or filter input events before they are processed by egui. + /// + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. + /// + /// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard. + /// + /// # Arguments + /// + /// * `_ctx` - The context of the egui, which provides access to the current state of the egui. + /// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes. + /// + /// # Note + /// + /// This function does not return a value. Any changes to the input should be made directly to `_raw_input`. + fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {} } /// Selects the level of hardware graphics acceleration. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ee54b5212..f27d01120 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -274,6 +274,8 @@ impl EpiIntegration { let close_requested = raw_input.viewport().close_requested(); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index be30d853c..0cb7ec331 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -446,6 +446,33 @@ impl WinitApp for GlowWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport) = glutin + .focused_viewport + .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index e8fe39f90..b3451be9c 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -456,6 +456,33 @@ impl WinitApp for WgpuWinitApp { } } + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + if let Some(viewport) = shared + .focused_viewport + .and_then(|viewport| shared.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, window_id }, diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 13bea2560..71fe04e0c 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -87,7 +87,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa return; } - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; let key = event.key(); @@ -158,7 +158,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa &document, "keyup", |event: web_sys::KeyboardEvent, runner| { - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; if let Some(key) = translate_key(&event.key()) { runner.input.raw.events.push(egui::Event::Key { @@ -301,6 +301,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &canvas, "mousedown", |event: web_sys::MouseEvent, runner: &mut AppRunner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas_id(), &event); let modifiers = runner.input.raw.modifiers; @@ -327,6 +329,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &canvas, "mousemove", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; let pos = pos_from_mouse_event(runner.canvas_id(), &event); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); @@ -336,6 +340,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu )?; runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas_id(), &event); let modifiers = runner.input.raw.modifiers; @@ -474,7 +480,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. + // `modifiers_from_kb_event()`, but we cannot directly use that fn for a [`WheelEvent`]. if event.ctrl_key() || event.meta_key() { let factor = (delta.y / 200.0).exp(); runner.input.raw.events.push(egui::Event::Zoom(factor)); diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index 96cad32e2..223693f42 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -115,7 +115,23 @@ pub fn translate_key(key: &str) -> Option { egui::Key::from_name(key) } -pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { +pub fn modifiers_from_kb_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { + egui::Modifiers { + alt: event.alt_key(), + ctrl: event.ctrl_key(), + shift: event.shift_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + mac_cmd: event.meta_key(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + command: event.ctrl_key() || event.meta_key(), + } +} + +pub fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers { egui::Modifiers { alt: event.alt_key(), ctrl: event.ctrl_key(), diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 15b6663a7..ac29a0b24 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -475,6 +475,13 @@ impl State { } } + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { + x: delta.0 as f32, + y: delta.1 as f32, + })); + } + /// Call this when there is a new [`accesskit::ActionRequest`]. /// /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. @@ -808,12 +815,14 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; + crate::profile_scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } if let Some(ime) = ime { let rect = ime.rect; let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + crate::profile_scope!("set_ime_cursor_area"); window.set_ime_cursor_area( winit::dpi::PhysicalPosition { x: pixels_per_point * rect.min.x, @@ -829,6 +838,7 @@ impl State { #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_ref() { if let Some(update) = accesskit_update { + crate::profile_scope!("accesskit"); accesskit.update_if_active(|| update); } } diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index e93217e58..3c6b711be 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -20,10 +20,6 @@ pub(crate) struct State { /// If false, clicks goes straight through to what is behind us. /// Good for tooltips etc. pub interactable: bool, - - /// When `true`, this `Area` belongs to a resizable window, so it needs to - /// receive mouse input which occurs a short distance beyond its bounding rect. - pub edges_padded_for_resize: bool, } impl State { @@ -52,7 +48,7 @@ impl State { /// /// ``` /// # egui::__run_test_ctx(|ctx| { -/// egui::Area::new("my_area") +/// egui::Area::new(egui::Id::new("my_area")) /// .fixed_pos(egui::pos2(32.0, 32.0)) /// .show(ctx, |ui| { /// ui.label("Floating text!"); @@ -75,13 +71,13 @@ pub struct Area { pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, - edges_padded_for_resize: bool, } impl Area { - pub fn new(id: impl Into) -> Self { + /// The `id` must be globally unique. + pub fn new(id: Id) -> Self { Self { - id: id.into(), + id, movable: true, interactable: true, constrain: false, @@ -92,10 +88,12 @@ impl Area { new_pos: None, pivot: Align2::LEFT_TOP, anchor: None, - edges_padded_for_resize: false, } } + /// Let's you change the `id` that you assigned in [`Self::new`]. + /// + /// The `id` must be globally unique. #[inline] pub fn id(mut self, id: Id) -> Self { self.id = id; @@ -223,14 +221,6 @@ impl Area { Align2::LEFT_TOP } } - - /// When `true`, this `Area` belongs to a resizable window, so it needs to - /// receive mouse input which occurs a short distance beyond its bounding rect. - #[inline] - pub(crate) fn edges_padded_for_resize(mut self, edges_padded_for_resize: bool) -> Self { - self.edges_padded_for_resize = edges_padded_for_resize; - self - } } pub(crate) struct Prepared { @@ -275,7 +265,6 @@ impl Area { anchor, constrain, constrain_rect, - edges_padded_for_resize, } = self; let layer_id = LayerId::new(order, id); @@ -296,11 +285,9 @@ impl Area { pivot, size: Vec2::ZERO, interactable, - edges_padded_for_resize, }); state.pivot_pos = new_pos.unwrap_or(state.pivot_pos); state.interactable = interactable; - state.edges_padded_for_resize = edges_padded_for_resize; if let Some((anchor, offset)) = anchor { let screen = ctx.available_rect(); diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 22286368b..e2654ca5a 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -34,7 +34,7 @@ pub struct Resize { id_source: Option, /// If false, we are no enabled - resizable: bool, + resizable: Vec2b, pub(crate) min_size: Vec2, pub(crate) max_size: Vec2, @@ -49,7 +49,7 @@ impl Default for Resize { Self { id: None, id_source: None, - resizable: true, + resizable: Vec2b::TRUE, min_size: Vec2::splat(16.0), max_size: Vec2::splat(f32::INFINITY), default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area. @@ -152,12 +152,13 @@ impl Resize { /// /// Default is `true`. #[inline] - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; + pub fn resizable(mut self, resizable: impl Into) -> Self { + self.resizable = resizable.into(); self } - pub fn is_resizable(&self) -> bool { + #[inline] + pub fn is_resizable(&self) -> Vec2b { self.resizable } @@ -175,7 +176,7 @@ impl Resize { self.default_size = size; self.min_size = size; self.max_size = size; - self.resizable = false; + self.resizable = Vec2b::FALSE; self } @@ -226,7 +227,7 @@ impl Resize { let mut user_requested_size = state.requested_size.take(); - let corner_id = self.resizable.then(|| id.with("__resize_corner")); + let corner_id = self.resizable.any().then(|| id.with("__resize_corner")); if let Some(corner_id) = corner_id { if let Some(corner_response) = ui.ctx().read_response(corner_id) { @@ -299,18 +300,21 @@ impl Resize { // ------------------------------ - let size = if self.with_stroke || self.resizable { - // We show how large we are, - // so we must follow the contents: + let mut size = state.last_content_size; + for d in 0..2 { + if self.with_stroke || self.resizable[d] { + // We show how large we are, + // so we must follow the contents: - state.desired_size = state.desired_size.max(state.last_content_size); + state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]); - // We are as large as we look - state.desired_size - } else { - // Probably a window. - state.last_content_size - }; + // We are as large as we look + size[d] = state.desired_size[d]; + } else { + // Probably a window. + size[d] = state.last_content_size[d]; + } + } ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); // ------------------------------ @@ -369,19 +373,22 @@ use epaint::Stroke; pub fn paint_resize_corner(ui: &Ui, response: &Response) { let stroke = ui.style().interact(response).fg_stroke; - paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM); + paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM); } pub fn paint_resize_corner_with_style( ui: &Ui, rect: &Rect, - stroke: impl Into, + color: impl Into, corner: Align2, ) { let painter = ui.painter(); let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect)); let mut w = 2.0; - let stroke = stroke.into(); + let stroke = Stroke { + width: 1.0, // Set width to 1.0 to prevent overlapping + color: color.into(), + }; while w <= rect.width() && w <= rect.height() { painter.line_segment( diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index d18cd8028..27128f671 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -2,6 +2,13 @@ use crate::*; +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +struct ScrollTarget { + animation_time_span: (f64, f64), + target_offset: f32, +} + #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -9,6 +16,9 @@ pub struct State { /// Positive offset means scrolling down/right pub offset: Vec2, + /// If set, quickly but smoothly scroll to this target offset. + offset_target: [Option; 2], + /// Were the scroll bars visible last frame? show_scroll: Vec2b, @@ -35,6 +45,7 @@ impl Default for State { fn default() -> Self { Self { offset: Vec2::ZERO, + offset_target: Default::default(), show_scroll: Vec2b::FALSE, content_is_too_large: Vec2b::FALSE, scroll_bar_interaction: Vec2b::FALSE, @@ -559,25 +570,56 @@ impl ScrollArea { state.vel[d] = input.pointer.velocity()[d]; }); state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } else { state.vel[d] = 0.0; } } } else { - // Kinetic scrolling - let stop_speed = 20.0; // Pixels per second. - let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input(|i| i.unstable_dt); + for d in 0..2 { + let dt = ui.input(|i| i.stable_dt).at_most(0.1); - let friction = friction_coeff * dt; - if friction > state.vel.length() || state.vel.length() < stop_speed { - state.vel = Vec2::ZERO; - } else { - state.vel -= friction * state.vel.normalized(); - // Offset has an inverted coordinate system compared to - // the velocity, so we subtract it instead of adding it - state.offset -= state.vel * dt; - ctx.request_repaint(); + if let Some(scroll_target) = state.offset_target[d] { + state.vel[d] = 0.0; + + if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; + } else { + // Move towards target + let t = emath::interpolation_factor( + scroll_target.animation_time_span, + ui.input(|i| i.time), + dt, + emath::ease_in_ease_out, + ); + if t < 1.0 { + state.offset[d] = + emath::lerp(state.offset[d]..=scroll_target.target_offset, t); + ctx.request_repaint(); + } else { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; + } + } + } else { + // Kinetic scrolling + let stop_speed = 20.0; // Pixels per second. + let friction_coeff = 1000.0; // Pixels per second squared. + + let friction = friction_coeff * dt; + if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed { + state.vel[d] = 0.0; + } else { + state.vel[d] -= friction * state.vel[d].signum(); + // Offset has an inverted coordinate system compared to + // the velocity, so we subtract it instead of adding it + state.offset[d] -= state.vel[d] * dt; + ctx.request_repaint(); + } + } } } } @@ -716,11 +758,11 @@ impl Prepared { let scroll_target = content_ui .ctx() .frame_state_mut(|state| state.scroll_target[d].take()); - if let Some((scroll, align)) = scroll_target { + if let Some((target_range, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; - let (start, end) = (scroll.min, scroll.max); + let (start, end) = (target_range.min, target_range.max); let clip_start = clip_rect.min[d]; let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; @@ -729,7 +771,7 @@ impl Prepared { let center_factor = align.to_factor(); let offset = - lerp(scroll, center_factor) - lerp(visible_range, center_factor); + lerp(target_range, center_factor) - lerp(visible_range, center_factor); // Depending on the alignment we need to add or subtract the spacing spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); @@ -745,7 +787,24 @@ impl Prepared { }; if delta != 0.0 { - state.offset[d] += delta; + let target_offset = state.offset[d] + delta; + + if let Some(animation) = &mut state.offset_target[d] { + // For instance: the user is continuously calling `ui.scroll_to_cursor`, + // so we don't want to reset the animation, but perhaps update the target: + animation.target_offset = target_offset; + } else { + // The further we scroll, the more time we take. + // TODO(emilk): let users configure this in `Style`. + let now = ui.input(|i| i.time); + let points_per_second = 1000.0; + let animation_duration = + (delta.abs() / points_per_second).clamp(0.1, 0.3); + state.offset_target[d] = Some(ScrollTarget { + animation_time_span: (now, now + animation_duration as f64), + target_offset, + }); + } ui.ctx().request_repaint(); } } @@ -808,6 +867,7 @@ impl Prepared { }); state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } } } @@ -952,6 +1012,7 @@ impl Prepared { // some manual action taken, scroll not stuck state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } else { state.scroll_start_offset_from_top_left[d] = None; } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 8cf5475eb..698033cf9 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -48,9 +48,7 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(Id::new(title.text())) - .constrain(true) - .edges_padded_for_resize(true); + let area = Area::new(Id::new(title.text())).constrain(true); Self { title, open: None, @@ -119,9 +117,6 @@ impl<'open> Window<'open> { #[inline] pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self { self.resize = mutate(self.resize); - self.area = self - .area - .edges_padded_for_resize(self.resize.is_resizable()); self } @@ -278,7 +273,6 @@ impl<'open> Window<'open> { #[inline] pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); - self.area = self.area.edges_padded_for_resize(false); self } @@ -296,11 +290,15 @@ impl<'open> Window<'open> { /// /// Note that even if you set this to `false` the window may still auto-resize. /// + /// You can set the window to only be resizable in one direction by using + /// e.g. `[true, false]` as the argument, + /// making the window only resizable in the x-direction. + /// /// Default is `true`. #[inline] - pub fn resizable(mut self, resizable: bool) -> Self { + pub fn resizable(mut self, resizable: impl Into) -> Self { + let resizable = resizable.into(); self.resize = self.resize.resizable(resizable); - self.area = self.area.edges_padded_for_resize(resizable); self } @@ -326,7 +324,6 @@ impl<'open> Window<'open> { pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); self.scroll = ScrollArea::neither(); - self.area = self.area.edges_padded_for_resize(false); self } @@ -392,7 +389,12 @@ impl<'open> Window<'open> { let header_color = frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); - let window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + // Keep the original inner margin for later use + let window_margin = window_frame.inner_margin; + let border_padding = window_frame.stroke.width / 2.0; + // Add border padding to the inner margin to prevent it from covering the contents + window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -420,9 +422,10 @@ impl<'open> Window<'open> { // Calculate roughly how much larger the window size is compared to the inner rect let (title_bar_height, title_content_spacing) = if with_title_bar { let style = ctx.style(); - let window_margin = window_frame.inner_margin; let spacing = window_margin.top + window_margin.bottom; let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing; + window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0); + window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0); (height, spacing) } else { (0.0, 0.0) @@ -495,21 +498,33 @@ impl<'open> Window<'open> { .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner(&area_content_ui, &possible, outer_rect, frame_stroke); + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + frame_stroke, + window_frame.rounding, + ); // END FRAME -------------------------------- if let Some(title_bar) = title_bar { - if on_top && area_content_ui.visuals().window_highlight_topmost { - let rect = Rect::from_min_size( - outer_rect.min, - Vec2 { - x: outer_rect.size().x, - y: title_bar_height, - }, - ); + let mut title_rect = Rect::from_min_size( + outer_rect.min + vec2(border_padding, border_padding), + Vec2 { + x: outer_rect.size().x - border_padding * 2.0, + y: title_bar_height, + }, + ); + title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect); + + if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; + + // Eliminate the rounding gap between the title bar and the window frame + round -= border_padding; + if !is_collapsed { round.se = 0.0; round.sw = 0.0; @@ -517,18 +532,18 @@ impl<'open> Window<'open> { area_content_ui.painter().set( *where_to_put_header_background, - RectShape::filled(rect, round, header_color), + RectShape::filled(title_rect, round, header_color), ); }; // Fix title bar separator line position if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height; + response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; } title_bar.ui( &mut area_content_ui, - outer_rect, + title_rect, &content_response, open, &mut collapsing, @@ -558,23 +573,47 @@ fn paint_resize_corner( possible: &PossibleInteractions, outer_rect: Rect, stroke: impl Into, + rounding: impl Into, ) { - let corner = if possible.resize_right && possible.resize_bottom { - Align2::RIGHT_BOTTOM + let stroke = stroke.into(); + let rounding = rounding.into(); + let (corner, radius) = if possible.resize_right && possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) } else if possible.resize_left && possible.resize_bottom { - Align2::LEFT_BOTTOM + (Align2::LEFT_BOTTOM, rounding.sw) } else if possible.resize_left && possible.resize_top { - Align2::LEFT_TOP + (Align2::LEFT_TOP, rounding.nw) } else if possible.resize_right && possible.resize_top { - Align2::RIGHT_TOP + (Align2::RIGHT_TOP, rounding.ne) } else { - return; + // We're not in two directions, but it is still nice to tell the user + // we're resizable by painting the resize corner in the expected place + // (i.e. for windows only resizable in one direction): + if possible.resize_right || possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) + } else if possible.resize_left || possible.resize_bottom { + (Align2::LEFT_BOTTOM, rounding.sw) + } else if possible.resize_left || possible.resize_top { + (Align2::LEFT_TOP, rounding.nw) + } else if possible.resize_right || possible.resize_top { + (Align2::RIGHT_TOP, rounding.ne) + } else { + return; + } }; + // Adjust the corner offset to accommodate the stroke width and window rounding + let offset = if radius <= 2.0 && stroke.width < 2.0 { + 2.0 + } else { + // The corner offset is calculated to make the corner appear to be in the correct position + (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) + * 45.0_f32.to_radians().cos() + }; let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); - let corner_rect = corner_rect.translate(-2.0 * corner.to_sign()); // move away from corner - crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke, corner); + let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner + crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner); } // ---------------------------------------------------------------------------- @@ -592,13 +631,15 @@ struct PossibleInteractions { impl PossibleInteractions { fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self { let movable = area.is_enabled() && area.is_movable(); - let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; + let resizable = resize + .is_resizable() + .and(area.is_enabled() && !is_collapsed); let pivot = area.get_pivot(); Self { - resize_left: resizable && (movable || pivot.x() != Align::LEFT), - resize_right: resizable && (movable || pivot.x() != Align::RIGHT), - resize_top: resizable && (movable || pivot.y() != Align::TOP), - resize_bottom: resizable && (movable || pivot.y() != Align::BOTTOM), + resize_left: resizable.x && (movable || pivot.x() != Align::LEFT), + resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT), + resize_top: resizable.y && (movable || pivot.y() != Align::TOP), + resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM), } } @@ -1036,7 +1077,10 @@ impl TitleBar { let y = content_response.rect.top(); // let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5); let stroke = ui.visuals().widgets.noninteractive.bg_stroke; - ui.painter().hline(outer_rect.x_range(), y, stroke); + // Workaround: To prevent border infringement, + // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + let x_range = outer_rect.x_range().shrink(0.1); + ui.painter().hline(x_range, y, stroke); } // Don't cover the close- and collapse buttons: diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 22741b237..c1dcd5b5b 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -198,100 +198,6 @@ impl ContextImpl { // ---------------------------------------------------------------------------- -/// Used to store each widget's [Id], [Rect] and [Sense] each frame. -/// Used to check for overlaps between widgets when handling events. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WidgetRect { - /// The globally unique widget id. - /// - /// For interactive widgets, this better be globally unique. - /// If not there will be weird bugs, - /// and also big red warning test on the screen in debug builds - /// (see [`Options::warn_on_id_clash`]). - /// - /// You can ensure globally unique ids using [`Ui::push_id`]. - pub id: Id, - - /// What layer the widget is on. - pub layer_id: LayerId, - - /// The full widget rectangle. - pub rect: Rect, - - /// Where the widget is. - /// - /// This is after clipping with the parent ui clip rect. - pub interact_rect: Rect, - - /// How the widget responds to interaction. - pub sense: Sense, - - /// Is the widget enabled? - pub enabled: bool, -} - -/// Stores the positions of all widgets generated during a single egui update/frame. -/// -/// Actually, only those that are on screen. -#[derive(Default, Clone, PartialEq, Eq)] -pub struct WidgetRects { - /// All widgets, in painting order. - pub by_layer: HashMap>, - - /// All widgets - pub by_id: IdMap, -} - -impl WidgetRects { - /// Clear the contents while retaining allocated memory. - pub fn clear(&mut self) { - let Self { by_layer, by_id } = self; - - for rects in by_layer.values_mut() { - rects.clear(); - } - - by_id.clear(); - } - - /// Insert the given widget rect in the given layer. - pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { - if !widget_rect.interact_rect.is_positive() { - return; - } - - let Self { by_layer, by_id } = self; - - let layer_widgets = by_layer.entry(layer_id).or_default(); - - match by_id.entry(widget_rect.id) { - std::collections::hash_map::Entry::Vacant(entry) => { - // A new widget - entry.insert(widget_rect); - layer_widgets.push(widget_rect); - } - std::collections::hash_map::Entry::Occupied(mut entry) => { - // e.g. calling `response.interact(…)` to add more interaction. - let existing = entry.get_mut(); - existing.rect = existing.rect.union(widget_rect.rect); - existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); - existing.sense |= widget_rect.sense; - existing.enabled |= widget_rect.enabled; - - // Find the existing widget in this layer and update it: - for previous in layer_widgets.iter_mut().rev() { - if previous.id == widget_rect.id { - *previous = *existing; - break; - } - } - } - } - } -} - -// ---------------------------------------------------------------------------- - /// State stored per viewport #[derive(Default)] struct ViewportState { @@ -350,7 +256,7 @@ struct ViewportState { #[derive(Clone, Debug)] pub struct RepaintCause { /// What file had the call that requested the repaint? - pub file: String, + pub file: &'static str, /// What line number of the the call that requested the repaint? pub line: u32, @@ -363,7 +269,7 @@ impl RepaintCause { pub fn new() -> Self { let caller = Location::caller(); Self { - file: caller.file().to_owned(), + file: caller.file(), line: caller.line(), } } @@ -546,12 +452,7 @@ impl ContextImpl { .map(|(i, id)| (*id, i)) .collect(); - let mut layers: Vec = viewport - .widgets_prev_frame - .by_layer - .keys() - .copied() - .collect(); + let mut layers: Vec = viewport.widgets_prev_frame.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -595,7 +496,6 @@ impl ContextImpl { pivot: Align2::LEFT_TOP, size: screen_rect.size(), interactable: true, - edges_padded_for_resize: false, }, ); @@ -1118,29 +1018,20 @@ impl Context { /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } + pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { + // Remember this widget + self.write(|ctx| { + let viewport = ctx.viewport(); - if w.interact_rect.is_positive() { - // Remember this widget - self.write(|ctx| { - let viewport = ctx.viewport(); + // We add all widgets here, even non-interactive ones, + // because we need this list not only for checking for blocking widgets, + // but also to know when we have reached the widget we are checking for cover. + viewport.widgets_this_frame.insert(w.layer_id, w); - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.widgets_this_frame.insert(w.layer_id, w); - - if w.sense.focusable { - ctx.memory.interested_in_focus(w.id); - } - }); - } else { - // Don't remember invisible widgets - } + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); + } + }); if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { // Not interested or allowed input: @@ -1175,9 +1066,8 @@ impl Context { let viewport = ctx.viewport(); viewport .widgets_this_frame - .by_id - .get(&id) - .or_else(|| viewport.widgets_prev_frame.by_id.get(&id)) + .get(id) + .or_else(|| viewport.widgets_prev_frame.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1235,7 +1125,8 @@ impl Context { let input = &viewport.input; let memory = &mut ctx.memory; - if sense.click + if enabled + && sense.click && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { @@ -1244,7 +1135,10 @@ impl Context { } #[cfg(feature = "accesskit")] - if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { + if enabled + && sense.click + && input.has_accesskit_action_request(id, accesskit::Action::Default) + { res.clicked[PointerButton::Primary as usize] = true; } @@ -1264,7 +1158,7 @@ impl Context { for pointer_event in &input.pointer.pointer_events { if let PointerEvent::Released { click, button } = pointer_event { - if sense.click && clicked { + if enabled && sense.click && clicked { if let Some(click) = click { res.clicked[*button as usize] = true; res.double_clicked[*button as usize] = click.is_double(); @@ -1916,13 +1810,16 @@ impl Context { #[cfg(debug_assertions)] fn debug_painting(&self) { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { - let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); - painter.debug_rect(widget.interact_rect, color, text); + let rect = widget.interact_rect; + if rect.is_positive() { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(rect, color, text); + } }; let paint_widget_id = |id: Id, text: &str, color: Color32| { if let Some(widget) = - self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned()) + self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned()) { paint_widget(&widget, text, color); } @@ -1931,8 +1828,8 @@ impl Context { if self.style().debug.show_interactive_widgets { // Show all interactive widgets: let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); - for (layer_id, rects) in rects.by_layer { - let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); + for (layer_id, rects) in rects.layers() { + let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { if rect.sense.interactive() { let (color, text) = if rect.sense.click && rect.sense.drag { @@ -2432,9 +2329,7 @@ impl Context { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { - self.memory(|mem| { - mem.layer_id_at(pos, mem.options.style.interaction.resize_grab_radius_side) - }) + self.memory(|mem| mem.layer_id_at(pos)) } /// Moves the given area to the top in its [`Order`]. diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 21fa269db..f61b9312f 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -396,6 +396,12 @@ pub enum Event { /// 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? diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 9960d5159..d1db5fc2a 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -633,6 +633,7 @@ impl WidgetInfo { WidgetType::ColorButton => "color button", WidgetType::ImageButton => "image button", WidgetType::CollapsingHeader => "collapsing header", + WidgetType::ProgressIndicator => "progress indicator", WidgetType::Label | WidgetType::Other => "", }; diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index fe8b085a3..62a762aa5 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -56,8 +56,7 @@ pub fn hit_test( let mut close: Vec = layer_order .iter() .filter(|layer| layer.order.allow_interaction()) - .filter_map(|layer_id| widgets.by_layer.get(layer_id)) - .flatten() + .flat_map(|&layer_id| widgets.get_layer(layer_id)) .filter(|&w| { let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); @@ -79,6 +78,16 @@ pub fn hit_test( let top_layer = closest_hit.layer_id; close.retain(|w| w.layer_id == top_layer); + // If the widget is disabled, treat it as if it isn't sensing anything. + // This simplifies the code in `hit_test_on_close` so it doesn't have to check + // the `enabled` flag everywhere: + for w in &mut close { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } + } + let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); let hits = hit_test_on_close(&close, pos_in_layer); diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 0de78a437..ed29282e9 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -22,6 +22,7 @@ const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings /// You can check if `egui` is using the inputs using /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct InputState { /// The raw input we got this frame from the backend. pub raw: RawInput, @@ -222,7 +223,7 @@ impl InputState { let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta; - let smooth_scroll_delta; + let mut smooth_scroll_delta = Vec2::ZERO; { // Mouse wheels often go very large steps. @@ -232,8 +233,15 @@ impl InputState { let dt = stable_dt.at_most(0.1); let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize - smooth_scroll_delta = t * unprocessed_scroll_delta; - unprocessed_scroll_delta -= smooth_scroll_delta; + for d in 0..2 { + if unprocessed_scroll_delta[d].abs() < 1.0 { + smooth_scroll_delta[d] = unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] = 0.0; + } else { + smooth_scroll_delta[d] = t * unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] -= smooth_scroll_delta[d]; + } + } } let mut modifiers = new.modifiers; @@ -546,6 +554,7 @@ impl InputState { /// A pointer (mouse or touch) click. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct Click { pub pos: Pos2, @@ -567,6 +576,7 @@ impl Click { } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) enum PointerEvent { Moved(Pos2), Pressed { @@ -595,6 +605,7 @@ impl PointerEvent { /// Mouse or touch state. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PointerState { /// Latest known time time: f64, @@ -617,6 +628,11 @@ pub struct PointerState { /// How much the pointer moved compared to last frame, in points. delta: Vec2, + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + motion: Option, + /// Current velocity of pointer. velocity: Vec2, @@ -664,6 +680,7 @@ impl Default for PointerState { latest_pos: None, interact_pos: None, delta: Vec2::ZERO, + motion: None, velocity: Vec2::ZERO, pos_history: History::new(0..1000, 0.1), down: Default::default(), @@ -690,6 +707,9 @@ impl PointerState { let old_pos = self.latest_pos; self.interact_pos = self.latest_pos; + if self.motion.is_some() { + self.motion = Some(Vec2::ZERO); + } for event in &new.events { match event { @@ -775,6 +795,7 @@ impl PointerState { self.latest_pos = None; // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. } + Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, _ => {} } } @@ -819,6 +840,14 @@ impl PointerState { self.delta } + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + #[inline(always)] + pub fn motion(&self) -> Option { + self.motion + } + /// Current velocity of pointer. #[inline(always)] pub fn velocity(&self) -> Vec2 { @@ -1139,6 +1168,7 @@ impl PointerState { latest_pos, interact_pos, delta, + motion, velocity, pos_history: _, down, @@ -1155,6 +1185,7 @@ impl PointerState { ui.label(format!("latest_pos: {latest_pos:?}")); ui.label(format!("interact_pos: {interact_pos:?}")); ui.label(format!("delta: {delta:?}")); + ui.label(format!("motion: {motion:?}")); ui.label(format!( "velocity: [{:3.0} {:3.0}] points/sec", velocity.x, velocity.y diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index a948ab16f..2ee7a91de 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -64,6 +64,7 @@ pub struct MultiTouchInfo { /// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct TouchState { /// Technical identifier of the touch device. This is used to identify relevant touch events /// for this [`TouchState`] instance. @@ -83,6 +84,7 @@ pub(crate) struct TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct GestureState { start_time: f64, start_pointer_pos: Pos2, @@ -93,6 +95,7 @@ struct GestureState { /// Gesture data that can change over time #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct DynGestureState { /// used for proportional zooming avg_distance: f32, @@ -110,6 +113,7 @@ struct DynGestureState { /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct ActiveTouch { /// Current position of this touch, in device coordinates (not necessarily screen position) pos: Pos2, @@ -302,6 +306,7 @@ impl Debug for TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum PinchType { Horizontal, Vertical, diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index f1a3e2a32..20d548870 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -107,13 +107,13 @@ pub(crate) fn interact( crate::profile_function!(); if let Some(id) = interaction.potential_click_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in clicking is gone. interaction.potential_click_id = None; } } if let Some(id) = interaction.potential_drag_id { - if !widgets.by_id.contains_key(&id) { + if !widgets.contains(id) { // The widget we were interested in dragging is gone. // This is fine! This could be drag-and-drop, // and the widget being dragged is now "in the air" and thus @@ -145,7 +145,7 @@ pub(crate) fn interact( if click.is_some() { if let Some(widget) = interaction .potential_click_id - .and_then(|id| widgets.by_id.get(&id)) + .and_then(|id| widgets.get(id)) { clicked = Some(widget.id); } @@ -160,22 +160,21 @@ pub(crate) fn interact( if dragged.is_none() { // Check if we started dragging something new: - if let Some(widget) = interaction - .potential_drag_id - .and_then(|id| widgets.by_id.get(&id)) - { - let is_dragged = if widget.sense.click && widget.sense.drag { - // This widget is sensitive to both clicks and drags. - // When the mouse first is pressed, it could be either, - // so we postpone the decision until we know. - input.pointer.is_decidedly_dragging() - } else { - // This widget is just sensitive to drags, so we can mark it as dragged right away: - widget.sense.drag - }; + if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) { + if widget.enabled { + let is_dragged = if widget.sense.click && widget.sense.drag { + // This widget is sensitive to both clicks and drags. + // When the mouse first is pressed, it could be either, + // so we postpone the decision until we know. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; - if is_dragged { - dragged = Some(widget.id); + if is_dragged { + dragged = Some(widget.id); + } } } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c715b859e..693280453 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -403,6 +403,7 @@ pub mod text_selection; mod ui; pub mod util; pub mod viewport; +mod widget_rect; pub mod widget_text; pub mod widgets; @@ -443,7 +444,7 @@ pub mod text { pub use { containers::*, - context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects}, + context::{Context, RepaintCause, RequestRepaintInfo}, data::{ input::*, output::{ @@ -466,6 +467,7 @@ pub use { text::{Galley, TextFormat}, ui::Ui, viewport::*, + widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, }; @@ -641,6 +643,8 @@ pub enum WidgetType { CollapsingHeader, + ProgressIndicator, + /// If you cannot fit any of the above slots. /// /// If this is something you think should be added, file an issue. diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index fcfe0accb..fb18258eb 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -646,9 +646,8 @@ impl Memory { } /// Top-most layer at the given position. - pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { - self.areas() - .layer_id_at(pos, resize_interact_radius_side, &self.layer_transforms) + pub fn layer_id_at(&self, pos: Pos2) -> Option { + self.areas().layer_id_at(pos, &self.layer_transforms) } /// An iterator over all layers. Back-to-front. Top is last. @@ -921,7 +920,6 @@ impl Areas { pub fn layer_id_at( &self, pos: Pos2, - resize_interact_radius_side: f32, layer_transforms: &HashMap, ) -> Option { for layer in self.order.iter().rev() { @@ -929,11 +927,6 @@ impl Areas { if let Some(state) = self.areas.get(&layer.id) { let mut rect = state.rect(); if state.interactable { - if state.edges_padded_for_resize { - // Allow us to resize by dragging just outside the window: - rect = rect.expand(resize_interact_radius_side); - } - if let Some(transform) = layer_transforms.get(layer) { rect = *transform * rect; } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 846b57383..67689ca9d 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -39,13 +39,14 @@ impl BarState { } /// Show a menu at pointer if primary-clicked response. + /// /// Should be called from [`Context`] on a [`Response`] pub fn bar_menu( &mut self, response: &Response, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option> { - MenuRoot::stationary_click_interaction(response, &mut self.open_menu, response.id); + MenuRoot::stationary_click_interaction(response, &mut self.open_menu); self.open_menu.show(response, add_contents) } @@ -134,7 +135,7 @@ pub(crate) fn submenu_button( /// wrapper for the contents of every menu. pub(crate) fn menu_ui<'c, R>( ctx: &Context, - menu_id: impl Into, + menu_id: Id, menu_state_arc: &Arc>, add_contents: impl FnOnce(&mut Ui) -> R + 'c, ) -> InnerResponse { @@ -144,7 +145,7 @@ pub(crate) fn menu_ui<'c, R>( menu_state.rect.min }; - let area = Area::new(menu_id) + let area = Area::new(menu_id.with("__menu")) .order(Order::Foreground) .fixed_pos(pos) .constrain_to(ctx.screen_rect()) @@ -222,13 +223,20 @@ pub(crate) fn context_menu( let menu_id = Id::new(CONTEXT_MENU_ID_STR); let mut bar_state = BarState::load(&response.ctx, menu_id); - MenuRoot::context_click_interaction(response, &mut bar_state, response.id); + MenuRoot::context_click_interaction(response, &mut bar_state); let inner_response = bar_state.show(response, add_contents); bar_state.store(&response.ctx, menu_id); inner_response } +/// Returns `true` if the context menu is opened for this widget. +pub(crate) fn context_menu_opened(response: &Response) -> bool { + let menu_id = Id::new(CONTEXT_MENU_ID_STR); + let bar_state = BarState::load(&response.ctx, menu_id); + bar_state.is_menu_open(response.id) +} + /// Stores the state for the context menu. #[derive(Clone, Default)] pub(crate) struct MenuRootManager { @@ -237,6 +245,7 @@ pub(crate) struct MenuRootManager { impl MenuRootManager { /// Show a menu at pointer if right-clicked response. + /// /// Should be called from [`Context`] on a [`Response`] pub fn show( &mut self, @@ -308,11 +317,9 @@ impl MenuRoot { /// Interaction with a stationary menu, i.e. fixed in another Ui. /// /// Responds to primary clicks. - fn stationary_interaction( - response: &Response, - root: &mut MenuRootManager, - id: Id, - ) -> MenuResponse { + fn stationary_interaction(response: &Response, root: &mut MenuRootManager) -> MenuResponse { + let id = response.id; + if (response.clicked() && root.is_menu_open(id)) || response.ctx.input(|i| i.key_pressed(Key::Escape)) { @@ -357,8 +364,8 @@ impl MenuRoot { MenuResponse::Stay } - /// Interaction with a context menu (secondary clicks). - fn context_interaction(response: &Response, root: &mut Option, id: Id) -> MenuResponse { + /// Interaction with a context menu (secondary click). + fn context_interaction(response: &Response, root: &mut Option) -> MenuResponse { let response = response.interact(Sense::click()); response.ctx.input(|input| { let pointer = &input.pointer; @@ -371,7 +378,7 @@ impl MenuRoot { } if !in_old_menu { if response.hovered() && response.secondary_clicked() { - return MenuResponse::Create(pos, id); + return MenuResponse::Create(pos, response.id); } else if (response.hovered() && pointer.primary_down()) || destroy { return MenuResponse::Close; } @@ -392,14 +399,14 @@ impl MenuRoot { } /// Respond to secondary (right) clicks. - pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { - let menu_response = Self::context_interaction(response, root, id); + pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) { + let menu_response = Self::context_interaction(response, root); Self::handle_menu_response(root, menu_response); } // Responds to primary clicks. - pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { - let menu_response = Self::stationary_interaction(response, root, id); + pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager) { + let menu_response = Self::stationary_interaction(response, root); Self::handle_menu_response(root, menu_response); } } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 8689ec815..788319dc9 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -152,6 +152,12 @@ impl Painter { pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { self.ctx().round_pos_to_pixels(pos) } + + /// Useful for pixel-perfect rendering. + #[inline] + pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect { + self.ctx().round_rect_to_pixels(rect) + } } /// ## Low level diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 6e791fc57..ddd683c4c 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -42,6 +42,13 @@ pub struct Response { pub interact_rect: Rect, /// The senses (click and/or drag) that the widget was interested in (if any). + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). pub sense: Sense, /// Was the widget enabled? @@ -356,6 +363,20 @@ impl Response { } } + /// If dragged, how far did the mouse move? + /// This will use raw mouse movement if provided by the integration, otherwise will fall back to [`Response::drag_delta`] + /// Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen. + /// This may be useful in certain situations such as draggable values and 3D cameras, where screen position does not matter. + #[inline] + pub fn drag_motion(&self) -> Vec2 { + if self.dragged() { + self.ctx + .input(|i| i.pointer.motion().unwrap_or(i.pointer.delta())) + } else { + Vec2::ZERO + } + } + /// If the user started dragging this widget this frame, store the payload for drag-and-drop. #[doc(alias = "drag and drop")] pub fn dnd_set_drag_payload(&self, payload: Payload) { @@ -520,6 +541,10 @@ impl Response { return true; } + if self.context_menu_opened() { + return false; + } + if self.enabled { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { return false; @@ -658,13 +683,14 @@ impl Response { id: self.id, rect: self.rect, interact_rect: self.interact_rect, - sense, + sense: self.sense | sense, enabled: self.enabled, }) } /// Adjust the scroll position until this UI becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is `None`, it'll scroll enough to bring the UI into view. /// /// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. @@ -764,6 +790,7 @@ impl Response { WidgetType::Slider => Role::Slider, WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, + WidgetType::ProgressIndicator => Role::ProgressIndicator, WidgetType::Other => Role::Unknown, }); if let Some(label) = info.label { @@ -834,6 +861,13 @@ impl Response { menu::context_menu(self, add_contents) } + /// Returns whether a context menu is currently open for this widget. + /// + /// See [`Self::context_menu`]. + pub fn context_menu_opened(&self) -> bool { + menu::context_menu_opened(self) + } + /// Draw a debug rectangle over the response displaying the response's id and whether it is /// enabled and/or hovered. /// diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index e42d1996c..a5b19e14d 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -622,7 +622,7 @@ impl Margin { }; #[inline] - pub fn same(margin: f32) -> Self { + pub const fn same(margin: f32) -> Self { Self { left: margin, right: margin, @@ -633,7 +633,7 @@ impl Margin { /// Margins with the same size on opposing sides #[inline] - pub fn symmetric(x: f32, y: f32) -> Self { + pub const fn symmetric(x: f32, y: f32) -> Self { Self { left: x, right: x, @@ -649,12 +649,12 @@ impl Margin { } #[inline] - pub fn left_top(&self) -> Vec2 { + pub const fn left_top(&self) -> Vec2 { vec2(self.left, self.top) } #[inline] - pub fn right_bottom(&self) -> Vec2 { + pub const fn right_bottom(&self) -> Vec2 { vec2(self.right, self.bottom) } @@ -702,6 +702,116 @@ impl std::ops::Add for Margin { } } +impl std::ops::Add for Margin { + type Output = Self; + + #[inline] + fn add(self, v: f32) -> Self { + Self { + left: self.left + v, + right: self.right + v, + top: self.top + v, + bottom: self.bottom + v, + } + } +} + +impl std::ops::AddAssign for Margin { + #[inline] + fn add_assign(&mut self, v: f32) { + self.left += v; + self.right += v; + self.top += v; + self.bottom += v; + } +} + +impl std::ops::Div for Margin { + type Output = Self; + + #[inline] + fn div(self, v: f32) -> Self { + Self { + left: self.left / v, + right: self.right / v, + top: self.top / v, + bottom: self.bottom / v, + } + } +} + +impl std::ops::DivAssign for Margin { + #[inline] + fn div_assign(&mut self, v: f32) { + self.left /= v; + self.right /= v; + self.top /= v; + self.bottom /= v; + } +} + +impl std::ops::Mul for Margin { + type Output = Self; + + #[inline] + fn mul(self, v: f32) -> Self { + Self { + left: self.left * v, + right: self.right * v, + top: self.top * v, + bottom: self.bottom * v, + } + } +} + +impl std::ops::MulAssign for Margin { + #[inline] + fn mul_assign(&mut self, v: f32) { + self.left *= v; + self.right *= v; + self.top *= v; + self.bottom *= v; + } +} + +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + Self { + left: self.left - other.left, + right: self.right - other.right, + top: self.top - other.top, + bottom: self.bottom - other.bottom, + } + } +} + +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, v: f32) -> Self { + Self { + left: self.left - v, + right: self.right - v, + top: self.top - v, + bottom: self.bottom - v, + } + } +} + +impl std::ops::SubAssign for Margin { + #[inline] + fn sub_assign(&mut self, v: f32) { + self.left -= v; + self.right -= v; + self.top -= v; + self.bottom -= v; + } +} + // ---------------------------------------------------------------------------- /// How and when interaction happens. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 839c4c850..165f7b317 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -75,7 +75,7 @@ impl Ui { /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { let style = ctx.style(); - Ui { + let ui = Ui { id, next_auto_id_source: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), @@ -83,7 +83,20 @@ impl Ui { placer: Placer::new(max_rect, Layout::default()), enabled: true, menu_state: None, - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + ui.ctx().create_widget(WidgetRect { + id: ui.id, + layer_id: ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: ui.enabled, + }); + + ui } /// Create a new [`Ui`] at a specific region. @@ -101,7 +114,7 @@ impl Ui { crate::egui_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); - Ui { + let child_ui = Ui { id: self.id.with(id_source), next_auto_id_source, painter: self.painter.clone(), @@ -109,7 +122,20 @@ impl Ui { placer: Placer::new(max_rect, layout), enabled: self.enabled, menu_state: self.menu_state.clone(), - } + }; + + // Register in the widget stack early, to ensure we are behind all widgets we contain: + let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + child_ui.ctx().create_widget(WidgetRect { + id: child_ui.id, + layer_id: child_ui.layer_id(), + rect: start_rect, + interact_rect: start_rect, + sense: Sense::hover(), + enabled: child_ui.enabled, + }); + + child_ui } // ------------------------------------------------- @@ -668,6 +694,15 @@ impl Ui { self.interact(rect, id, sense) } + /// Interact with the background of this [`Ui`], + /// i.e. behind all the widgets. + /// + /// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`]. + pub fn interact_bg(&self, sense: Sense) -> Response { + // This will update the WidgetRect that was first created in `Ui::new`. + self.interact(self.min_rect(), self.id, sense) + } + /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? /// /// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance, @@ -967,6 +1002,7 @@ impl Ui { /// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is `None`, it'll scroll enough to bring the cursor into view. /// /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`]. [`Ui::scroll_with_delta`].. @@ -993,6 +1029,7 @@ impl Ui { /// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible. /// + /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. /// If `align` is not provided, it'll scroll enough to bring the cursor into view. /// /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. @@ -2188,11 +2225,11 @@ impl Ui { /// /// The given frame is used for its margins, but it color is ignored. #[doc(alias = "drag and drop")] - pub fn dnd_drop_zone( + pub fn dnd_drop_zone( &mut self, frame: Frame, - add_contents: impl FnOnce(&mut Ui), - ) -> (Response, Option>) + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> (InnerResponse, Option>) where Payload: Any + Send + Sync, { @@ -2201,7 +2238,7 @@ impl Ui { DragAndDrop::has_payload_of_type::(self.ctx()); let mut frame = frame.begin(self); - add_contents(&mut frame.content_ui); + let inner = add_contents(&mut frame.content_ui); let response = frame.allocate_space(self); // NOTE: we use `response.contains_pointer` here instead of `hovered`, because @@ -2231,7 +2268,7 @@ impl Ui { let payload = response.dnd_release_payload::(); - (response, payload) + (InnerResponse { inner, response }, payload) } /// Close the menu we are in (including submenus), if any. diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs new file mode 100644 index 000000000..ab95447e1 --- /dev/null +++ b/crates/egui/src/widget_rect.rs @@ -0,0 +1,132 @@ +use ahash::HashMap; + +use crate::*; + +/// Used to store each widget's [Id], [Rect] and [Sense] each frame. +/// +/// Used to check which widget gets input when a user clicks somewhere. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WidgetRect { + /// The globally unique widget id. + /// + /// For interactive widgets, this better be globally unique. + /// If not there will be weird bugs, + /// and also big red warning test on the screen in debug builds + /// (see [`Options::warn_on_id_clash`]). + /// + /// You can ensure globally unique ids using [`Ui::push_id`]. + pub id: Id, + + /// What layer the widget is on. + pub layer_id: LayerId, + + /// The full widget rectangle. + pub rect: Rect, + + /// Where the widget is. + /// + /// This is after clipping with the parent ui clip rect. + pub interact_rect: Rect, + + /// How the widget responds to interaction. + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). + pub sense: Sense, + + /// Is the widget enabled? + pub enabled: bool, +} + +/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. +/// +/// All [`Ui`]s have a [`WidgetRects`], but whether or not their rects are correct +/// depends on if [`Ui::interact_bg`] was ever called. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct WidgetRects { + /// All widgets, in painting order. + by_layer: HashMap>, + + /// All widgets, by id, and their order in their respective layer + by_id: IdMap<(usize, WidgetRect)>, +} + +impl WidgetRects { + /// All known layers with widgets. + pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { + self.by_layer.keys().copied() + } + + pub fn layers(&self) -> impl Iterator + '_ { + self.by_layer + .iter() + .map(|(layer_id, rects)| (layer_id, &rects[..])) + } + + #[inline] + pub fn get(&self, id: Id) -> Option<&WidgetRect> { + self.by_id.get(&id).map(|(_, w)| w) + } + + #[inline] + pub fn contains(&self, id: Id) -> bool { + self.by_id.contains_key(&id) + } + + /// All widgets in this layer, sorted back-to-front. + #[inline] + pub fn get_layer(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.by_layer.get(&layer_id).into_iter().flatten() + } + + /// Clear the contents while retaining allocated memory. + pub fn clear(&mut self) { + let Self { by_layer, by_id } = self; + + for rects in by_layer.values_mut() { + rects.clear(); + } + + by_id.clear(); + } + + /// Insert the given widget rect in the given layer. + pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { + let Self { by_layer, by_id } = self; + + let layer_widgets = by_layer.entry(layer_id).or_default(); + + match by_id.entry(widget_rect.id) { + std::collections::hash_map::Entry::Vacant(entry) => { + // A new widget + let idx_in_layer = layer_widgets.len(); + entry.insert((idx_in_layer, widget_rect)); + layer_widgets.push(widget_rect); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + // This is a known widget, but we might need to update it! + // e.g. calling `response.interact(…)` to add more interaction. + let (idx_in_layer, existing) = entry.get_mut(); + + egui_assert!( + existing.layer_id == widget_rect.layer_id, + "Widget changed layer_id during the frame" + ); + + // Update it: + existing.rect = widget_rect.rect; // last wins + existing.interact_rect = widget_rect.interact_rect; // last wins + existing.sense |= widget_rect.sense; + existing.enabled |= widget_rect.enabled; + + if existing.layer_id == widget_rect.layer_id { + layer_widgets[*idx_in_layer] = *existing; + } + } + } + } +} diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index cd00bd740..99c768bf9 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -113,6 +113,17 @@ impl Widget for ProgressBar { let (outer_rect, response) = ui.allocate_exact_size(vec2(desired_width, height), Sense::hover()); + response.widget_info(|| { + let mut info = if let Some(ProgressBarText::Custom(text)) = &text { + WidgetInfo::labeled(WidgetType::ProgressIndicator, text.text()) + } else { + WidgetInfo::new(WidgetType::ProgressIndicator) + }; + info.value = Some((progress as f64 * 100.0).floor()); + + info + }); + if ui.is_rect_visible(response.rect) { if animate { ui.ctx().request_repaint(); diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index 0327bfab1..f18842844 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,6 +1,6 @@ use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; -use crate::{Response, Sense, Ui, Widget}; +use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType}; /// A spinner widget used to indicate loading. /// @@ -66,6 +66,7 @@ impl Widget for Spinner { .size .unwrap_or_else(|| ui.style().spacing.interact_size.y); let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + response.widget_info(|| WidgetInfo::new(WidgetType::ProgressIndicator)); self.paint_at(ui, rect); response diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 22a5623f0..12077b932 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -940,6 +940,7 @@ fn events( if !text_mark.is_empty() { text.insert_text_at(&mut ccursor, text_mark, char_limit); } + state.ime_cursor_range = cursor_range; Some(CCursorRange::two(start_cursor, ccursor)) } else { None @@ -947,12 +948,16 @@ fn events( } Event::CompositionEnd(prediction) => { - // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, so do not check `state.has_ime = true` in the following statement. + // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, + // so do not check `state.has_ime = true` in the following statement. if prediction != "\n" && prediction != "\r" { state.has_ime = false; - let mut ccursor = text.delete_selected(&cursor_range); - if !prediction.is_empty() { + let mut ccursor; + if !prediction.is_empty() && cursor_range == state.ime_cursor_range { + ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, prediction, char_limit); + } else { + ccursor = cursor_range.primary.ccursor; } Some(CCursorRange::one(ccursor)) } else { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 819688289..8901fd56f 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -44,6 +44,10 @@ pub struct TextEditState { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) has_ime: bool, + // cursor range for IME candidate. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) ime_cursor_range: CursorRange, + // Visual offset when editing singleline text bigger than the width. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) singleline_offset: f32, diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index f28878a1d..e70ce695a 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -89,10 +89,12 @@ pub trait TextBuffer { fn decrease_indentation(&mut self, ccursor: &mut CCursor) { let line_start = find_line_start(self.as_str(), *ccursor); - let remove_len = if self.as_str()[line_start.index..].starts_with('\t') { + let remove_len = if self.as_str().chars().nth(line_start.index) == Some('\t') { Some(1) - } else if self.as_str()[line_start.index..] + } else if self + .as_str() .chars() + .skip(line_start.index) .take(TAB_SIZE) .all(|c| c == ' ') { diff --git a/crates/egui_demo_app/src/apps/custom3d_glow.rs b/crates/egui_demo_app/src/apps/custom3d_glow.rs index 3175cf4fa..7f488e767 100644 --- a/crates/egui_demo_app/src/apps/custom3d_glow.rs +++ b/crates/egui_demo_app/src/apps/custom3d_glow.rs @@ -55,7 +55,7 @@ impl Custom3d { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; // Clone locals so we can move them into the paint callback: let angle = self.angle; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 6b748cb1d..1676a0ba7 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -173,7 +173,7 @@ impl Custom3d { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; ui.painter().add(egui_wgpu::Callback::new_paint_callback( rect, CustomTriangleCallback { angle: self.angle }, diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 56e5c9020..f3143e3c7 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -13,6 +13,7 @@ impl super::Demo for About { .default_width(320.0) .default_height(480.0) .open(open) + .resizable([true, false]) .show(ctx, |ui| { use super::View as _; self.ui(ui); diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index b4adf8fb5..6786e55a1 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -75,6 +75,7 @@ impl super::Demo for CodeExample { .default_size([800.0, 400.0]) .vscroll(false) .hscroll(true) + .resizable([true, false]) .show(ctx, |ui| self.ui(ui)); } } diff --git a/crates/egui_demo_lib/src/demo/drag_and_drop.rs b/crates/egui_demo_lib/src/demo/drag_and_drop.rs index 29bda3350..883095953 100644 --- a/crates/egui_demo_lib/src/demo/drag_and_drop.rs +++ b/crates/egui_demo_lib/src/demo/drag_and_drop.rs @@ -60,7 +60,7 @@ impl super::View for DragAndDropDemo { let frame = Frame::default().inner_margin(4.0); - let (_, dropped_payload) = ui.dnd_drop_zone::(frame, |ui| { + let (_, dropped_payload) = ui.dnd_drop_zone::(frame, |ui| { ui.set_min_size(vec2(64.0, 100.0)); for (row_idx, item) in column.iter().enumerate() { let item_id = Id::new(("my_drag_and_drop_demo", col_idx, row_idx)); diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 6aa42179c..08829d6d5 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -69,30 +69,25 @@ impl super::View for PanZoom { } } - for (id, pos, callback) in [ + for (i, (pos, callback)) in [ ( - "a", egui::Pos2::new(0.0, 0.0), Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) as Box egui::Response>, ), ( - "b", egui::Pos2::new(0.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), ), ( - "c", egui::Pos2::new(120.0, 120.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), ), ( - "d", egui::Pos2::new(120.0, 0.0), Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), ), ( - "e", egui::Pos2::new(60.0, 60.0), Box::new(|ui, state| { use egui::epaint::*; @@ -110,8 +105,11 @@ impl super::View for PanZoom { ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) }), ), - ] { - let id = egui::Area::new(id) + ] + .into_iter() + .enumerate() + { + let id = egui::Area::new(id.with(("subarea", i))) .default_pos(pos) // Need to cover up the pan_zoom demo window, // but may also cover over other windows. diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 30855f77f..0530f2fee 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -252,6 +252,8 @@ impl super::View for ScrollTo { fn ui(&mut self, ui: &mut Ui) { ui.label("This shows how you can scroll to a specific item or pixel offset"); + let num_items = 500; + let mut track_item = false; let mut go_to_scroll_offset = false; let mut scroll_top = false; @@ -260,7 +262,7 @@ impl super::View for ScrollTo { ui.horizontal(|ui| { ui.label("Scroll to a specific item index:"); track_item |= ui - .add(Slider::new(&mut self.track_item, 1..=50).text("Track Item")) + .add(Slider::new(&mut self.track_item, 1..=num_items).text("Track Item")) .dragged(); }); @@ -304,7 +306,7 @@ impl super::View for ScrollTo { ui.scroll_to_cursor(Some(Align::TOP)); } ui.vertical(|ui| { - for item in 1..=50 { + for item in 1..=num_items { if track_item && item == self.track_item { let response = ui.colored_label(Color32::YELLOW, format!("This is item {item}")); diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index ac55a4d01..d68af17db 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -50,7 +50,7 @@ impl super::Demo for WidgetGallery { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { egui::Window::new(self.name()) .open(open) - .resizable(true) + .resizable([true, false]) .default_width(280.0) .show(ctx, |ui| { use super::View as _; diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 0876d9db0..a8ae5998d 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -1,4 +1,4 @@ -use egui::{Pos2, Rect, Response, Sense, Ui}; +use egui::{Id, Pos2, Rect, Response, Sense, Ui}; #[derive(Clone, Copy)] pub(crate) enum CellSize { @@ -113,6 +113,7 @@ impl<'l> StripLayout<'l> { flags: StripLayoutFlags, width: CellSize, height: CellSize, + child_ui_id_source: Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> (Rect, Response) { let max_rect = self.cell_rect(&width, &height); @@ -145,7 +146,9 @@ impl<'l> StripLayout<'l> { ); } - let used_rect = self.cell(flags, max_rect, add_cell_contents); + let child_ui = self.cell(flags, max_rect, child_ui_id_source, add_cell_contents); + + let used_rect = child_ui.min_rect(); self.set_pos(max_rect); @@ -155,7 +158,9 @@ impl<'l> StripLayout<'l> { max_rect.union(used_rect) }; - let response = self.ui.allocate_rect(allocation_rect, self.sense); + self.ui.advance_cursor_after_rect(allocation_rect); + + let response = child_ui.interact(max_rect, child_ui.id(), self.sense); (used_rect, response) } @@ -182,13 +187,17 @@ impl<'l> StripLayout<'l> { self.ui.allocate_rect(rect, Sense::hover()); } + /// Return the Ui to which the contents where added fn cell( &mut self, flags: StripLayoutFlags, rect: Rect, + child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), - ) -> Rect { - let mut child_ui = self.ui.child_ui(rect, self.cell_layout); + ) -> Ui { + let mut child_ui = + self.ui + .child_ui_with_id_source(rect, self.cell_layout, child_ui_id_source); if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); @@ -204,7 +213,7 @@ impl<'l> StripLayout<'l> { add_cell_contents(&mut child_ui); - child_ui.min_rect() + child_ui } /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 10bfec539..afa7d0eef 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -25,6 +25,19 @@ impl FileLoader { const PROTOCOL: &str = "file://"; +/// Remove the leading slash from the path if the target OS is Windows. +/// +/// This is because Windows paths are not supposed to start with a slash. +/// For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path. +#[inline] +fn trim_extra_slash(s: &str) -> &str { + if cfg!(target_os = "windows") { + s.trim_start_matches('/') + } else { + s + } +} + impl BytesLoader for FileLoader { fn id(&self) -> &str { Self::ID @@ -32,12 +45,12 @@ impl BytesLoader for FileLoader { fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { // File loader only supports the `file` protocol. - let Some(path) = uri.strip_prefix(PROTOCOL) else { + let Some(path) = uri.strip_prefix(PROTOCOL).map(trim_extra_slash) else { return Err(LoadError::NotSupported); }; let mut cache = self.cache.lock(); - if let Some(entry) = cache.get(path).cloned() { + if let Some(entry) = cache.get(uri).cloned() { // `path` has either begun loading, is loaded, or has failed to load. match entry { Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { @@ -54,7 +67,7 @@ impl BytesLoader for FileLoader { // Set the file to `pending` until we finish loading it. let path = path.to_owned(); - cache.insert(path.clone(), Poll::Pending); + cache.insert(uri.to_owned(), Poll::Pending); drop(cache); // Spawn a thread to read the file, so that we don't block the render for too long. @@ -63,7 +76,7 @@ impl BytesLoader for FileLoader { .spawn({ let ctx = ctx.clone(); let cache = self.cache.clone(); - let _uri = uri.to_owned(); + let uri = uri.to_owned(); move || { let result = match std::fs::read(&path) { Ok(bytes) => { @@ -82,10 +95,10 @@ impl BytesLoader for FileLoader { } Err(err) => Err(err.to_string()), }; - let prev = cache.lock().insert(path, Poll::Ready(result)); + let prev = cache.lock().insert(uri.clone(), Poll::Ready(result)); assert!(matches!(prev, Some(Poll::Pending))); ctx.request_repaint(); - log::trace!("finished loading {_uri:?}"); + log::trace!("finished loading {uri:?}"); } }) .expect("failed to spawn thread"); diff --git a/crates/egui_extras/src/strip.rs b/crates/egui_extras/src/strip.rs index 67903d1cb..9087f673b 100644 --- a/crates/egui_extras/src/strip.rs +++ b/crates/egui_extras/src/strip.rs @@ -194,7 +194,13 @@ impl<'a, 'b> Strip<'a, 'b> { clip: self.clip, ..Default::default() }; - self.layout.add(flags, width, height, add_contents); + self.layout.add( + flags, + width, + height, + egui::Id::new(self.size_index), + add_contents, + ); } /// Add an empty cell. diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 008041c29..5e9cf80fd 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -1166,7 +1166,13 @@ impl<'a, 'b> TableRow<'a, 'b> { selected: self.selected, }; - let (used_rect, response) = self.layout.add(flags, width, height, add_cell_contents); + let (used_rect, response) = self.layout.add( + flags, + width, + height, + egui::Id::new((self.row_index, col_index)), + add_cell_contents, + ); if let Some(max_w) = self.max_used_widths.get_mut(col_index) { *max_w = max_w.max(used_rect.width()); diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 692399e8b..4238c7b32 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1391,6 +1391,11 @@ pub struct GridMark { pub fn log_grid_spacer(log_base: i64) -> GridSpacer { let log_base = log_base as f64; let step_sizes = move |input: GridInput| -> Vec { + // handle degenerate cases + if input.base_step_size.abs() < f64::EPSILON { + return Vec::new(); + } + // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base let smallest_visible_unit = next_power(input.base_step_size, log_base); @@ -1693,7 +1698,7 @@ impl PreparedPlot { /// assert_eq!(next_power(0.2, 10.0), 1); /// ``` fn next_power(value: f64, base: f64) -> f64 { - assert_ne!(value, 0.0); // can be negative (typical for Y axis) + debug_assert_ne!(value, 0.0); // can be negative (typical for Y axis) base.powi(value.abs().log(base).ceil() as i32) } @@ -1708,7 +1713,7 @@ fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { /// Fill in all values between [min, max] which are a multiple of `step_size` fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { - assert!(max > min); + debug_assert!(max > min); let first = (min / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64; diff --git a/crates/emath/src/history.rs b/crates/emath/src/history.rs index bacd9625f..d85a27a39 100644 --- a/crates/emath/src/history.rs +++ b/crates/emath/src/history.rs @@ -16,6 +16,7 @@ use std::collections::VecDeque; /// or for smoothed velocity (e.g. mouse pointer speed). /// All times are in seconds. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct History { /// In elements, i.e. of `values.len()`. /// The length is initially zero, but once past `min_len` will not shrink below it. diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index d77091667..911fb52a0 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -383,6 +383,52 @@ pub fn exponential_smooth_factor( 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds) } +/// If you have a value animating over time, +/// how much towards its target do you need to move it this frame? +/// +/// You only need to store the start time and target value in order to animate using this function. +/// +/// ``` rs +/// struct Animation { +/// current_value: f32, +/// +/// animation_time_span: (f64, f64), +/// target_value: f32, +/// } +/// +/// impl Animation { +/// fn update(&mut self, now: f64, dt: f32) { +/// let t = interpolation_factor(self.animation_time_span, now, dt, ease_in_ease_out); +/// self.current_value = emath::lerp(self.current_value..=self.target_value, t); +/// } +/// } +/// ``` +pub fn interpolation_factor( + (start_time, end_time): (f64, f64), + current_time: f64, + dt: f32, + easing: impl Fn(f32) -> f32, +) -> f32 { + let animation_duration = (end_time - start_time) as f32; + let prev_time = current_time - dt as f64; + let prev_t = easing((prev_time - start_time) as f32 / animation_duration); + let end_t = easing((current_time - start_time) as f32 / animation_duration); + if end_t < 1.0 { + (end_t - prev_t) / (1.0 - prev_t) + } else { + 1.0 + } +} + +/// Ease in, ease out. +/// +/// `f(0) = 0, f'(0) = 0, f(1) = 1, f'(1) = 0`. +#[inline] +pub fn ease_in_ease_out(t: f32) -> f32 { + let t = t.clamp(0.0, 1.0); + (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0) +} + // ---------------------------------------------------------------------------- /// An assert that is only active when `emath` is compiled with the `extra_asserts` feature diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 2c5ceeedf..0cab00fe6 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -1,5 +1,7 @@ use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use crate::Vec2b; + /// A vector has a direction and length. /// A [`Vec2`] is often used to represent a size. /// @@ -86,6 +88,16 @@ impl From<&Vec2> for (f32, f32) { } } +impl From for Vec2 { + #[inline(always)] + fn from(v: Vec2b) -> Self { + Self { + x: v.x as i32 as f32, + y: v.y as i32 as f32, + } + } +} + // ---------------------------------------------------------------------------- // Mint compatibility and convenience conversions diff --git a/crates/emath/src/vec2b.rs b/crates/emath/src/vec2b.rs index dc5eb0ea5..f241de64e 100644 --- a/crates/emath/src/vec2b.rs +++ b/crates/emath/src/vec2b.rs @@ -19,6 +19,30 @@ impl Vec2b { pub fn any(&self) -> bool { self.x || self.y } + + /// Are both `x` and `y` true? + #[inline] + pub fn all(&self) -> bool { + self.x && self.y + } + + #[inline] + pub fn and(&self, other: impl Into) -> Self { + let other = other.into(); + Self { + x: self.x && other.x, + y: self.y && other.y, + } + } + + #[inline] + pub fn or(&self, other: impl Into) -> Self { + let other = other.into(); + Self { + x: self.x || other.x, + y: self.y || other.y, + } + } } impl From for Vec2b { diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index fa3f1e5e0..454969d75 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -120,6 +120,16 @@ impl ColorImage { Self { size, pixels } } + /// Alternative method to `from_gray`. + /// Create a [`ColorImage`] from iterator over flat opaque gray data. + /// + /// Panics if `size[0] * size[1] != gray_iter.len()`. + pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator) -> Self { + let pixels: Vec<_> = gray_iter.map(Color32::from_gray).collect(); + assert_eq!(size[0] * size[1], pixels.len()); + Self { size, pixels } + } + /// A view of the underlying data as `&[u8]` #[cfg(feature = "bytemuck")] pub fn as_raw(&self) -> &[u8] { diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index ad13037ae..fb6a9a4ab 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -19,8 +19,12 @@ impl Shadow { color: Color32::TRANSPARENT, }; + pub const fn new(extrusion: f32, color: Color32) -> Self { + Self { extrusion, color } + } + /// Tooltips, menus, …, for dark mode. - pub fn small_dark() -> Self { + pub const fn small_dark() -> Self { Self { extrusion: 16.0, color: Color32::from_black_alpha(96), @@ -28,7 +32,7 @@ impl Shadow { } /// Tooltips, menus, …, for light mode. - pub fn small_light() -> Self { + pub const fn small_light() -> Self { Self { extrusion: 16.0, color: Color32::from_black_alpha(20), @@ -36,7 +40,7 @@ impl Shadow { } /// Used for egui windows in dark mode. - pub fn big_dark() -> Self { + pub const fn big_dark() -> Self { Self { extrusion: 32.0, color: Color32::from_black_alpha(96), @@ -44,7 +48,7 @@ impl Shadow { } /// Used for egui windows in light mode. - pub fn big_light() -> Self { + pub const fn big_light() -> Self { Self { extrusion: 32.0, color: Color32::from_black_alpha(16), diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index ffc7b174b..fefae6c93 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -355,6 +355,22 @@ impl Shape { } } + /// Scale the shape by `factor`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn scale(&mut self, factor: f32) { + self.transform(TSTransform::from_scaling(factor)); + } + + /// Move the shape by `delta`, in-place. + /// + /// A wrapper around [`Self::transform`]. + #[inline(always)] + pub fn translate(&mut self, delta: Vec2) { + self.transform(TSTransform::from_translation(delta)); + } + /// Move the shape by this many points, in-place. /// /// If using a [`PaintCallback`], note that only the rect is scaled as opposed @@ -387,6 +403,7 @@ impl Shape { Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; rect_shape.stroke.width *= transform.scaling; + rect_shape.rounding *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -701,7 +718,7 @@ impl Rounding { }; #[inline] - pub fn same(radius: f32) -> Self { + pub const fn same(radius: f32) -> Self { Self { nw: radius, ne: radius, @@ -739,6 +756,130 @@ impl Rounding { } } +impl std::ops::Add for Rounding { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self { + nw: self.nw + rhs.nw, + ne: self.ne + rhs.ne, + sw: self.sw + rhs.sw, + se: self.se + rhs.se, + } + } +} + +impl std::ops::AddAssign for Rounding { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = Self { + nw: self.nw + rhs.nw, + ne: self.ne + rhs.ne, + sw: self.sw + rhs.sw, + se: self.se + rhs.se, + }; + } +} + +impl std::ops::AddAssign for Rounding { + #[inline] + fn add_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw + rhs, + ne: self.ne + rhs, + sw: self.sw + rhs, + se: self.se + rhs, + }; + } +} + +impl std::ops::Sub for Rounding { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self { + nw: self.nw - rhs.nw, + ne: self.ne - rhs.ne, + sw: self.sw - rhs.sw, + se: self.se - rhs.se, + } + } +} + +impl std::ops::SubAssign for Rounding { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = Self { + nw: self.nw - rhs.nw, + ne: self.ne - rhs.ne, + sw: self.sw - rhs.sw, + se: self.se - rhs.se, + }; + } +} + +impl std::ops::SubAssign for Rounding { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw - rhs, + ne: self.ne - rhs, + sw: self.sw - rhs, + se: self.se - rhs, + }; + } +} + +impl std::ops::Div for Rounding { + type Output = Self; + #[inline] + fn div(self, rhs: f32) -> Self { + Self { + nw: self.nw / rhs, + ne: self.ne / rhs, + sw: self.sw / rhs, + se: self.se / rhs, + } + } +} + +impl std::ops::DivAssign for Rounding { + #[inline] + fn div_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw / rhs, + ne: self.ne / rhs, + sw: self.sw / rhs, + se: self.se / rhs, + }; + } +} + +impl std::ops::Mul for Rounding { + type Output = Self; + #[inline] + fn mul(self, rhs: f32) -> Self { + Self { + nw: self.nw * rhs, + ne: self.ne * rhs, + sw: self.sw * rhs, + se: self.se * rhs, + } + } +} + +impl std::ops::MulAssign for Rounding { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + *self = Self { + nw: self.nw * rhs, + ne: self.ne * rhs, + sw: self.sw * rhs, + se: self.se * rhs, + }; + } +} + // ---------------------------------------------------------------------------- /// How to paint some text on screen. diff --git a/examples/custom_3d_glow/src/main.rs b/examples/custom_3d_glow/src/main.rs index 937973978..a1f6fa269 100644 --- a/examples/custom_3d_glow/src/main.rs +++ b/examples/custom_3d_glow/src/main.rs @@ -69,7 +69,7 @@ impl MyApp { let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); - self.angle += response.drag_delta().x * 0.01; + self.angle += response.drag_motion().x * 0.01; // Clone locals so we can move them into the paint callback: let angle = self.angle; diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml new file mode 100644 index 000000000..1557b35c8 --- /dev/null +++ b/examples/custom_keypad/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "custom_keypad" +version = "0.1.0" +authors = ["Varphone Wong "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.72" +publish = false + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/custom_keypad/README.md b/examples/custom_keypad/README.md new file mode 100644 index 000000000..9e5cdf7e8 --- /dev/null +++ b/examples/custom_keypad/README.md @@ -0,0 +1,7 @@ +Example showing how to implements a custom keypad. + +```sh +cargo run -p custom_keypad +``` + +![](screenshot.png) diff --git a/examples/custom_keypad/screenshot.png b/examples/custom_keypad/screenshot.png new file mode 100644 index 000000000..632459e51 Binary files /dev/null and b/examples/custom_keypad/screenshot.png differ diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs new file mode 100644 index 000000000..81800f47d --- /dev/null +++ b/examples/custom_keypad/src/keypad.rs @@ -0,0 +1,255 @@ +use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Transition { + #[default] + None, + CloseOnNextFrame, + CloseImmediately, +} + +#[derive(Clone, Debug)] +struct State { + open: bool, + closable: bool, + close_on_next_frame: bool, + start_pos: egui::Pos2, + focus: Option, + events: Option>, +} + +impl State { + fn new() -> Self { + Self { + open: false, + closable: false, + close_on_next_frame: false, + start_pos: pos2(100.0, 100.0), + focus: None, + events: None, + } + } + + fn queue_char(&mut self, c: char) { + let events = self.events.get_or_insert(vec![]); + if let Some(key) = egui::Key::from_name(&c.to_string()) { + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } + events.push(egui::Event::Text(c.to_string())); + } + + fn queue_key(&mut self, key: egui::Key) { + let events = self.events.get_or_insert(vec![]); + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +/// A simple keypad widget. +pub struct Keypad { + id: egui::Id, +} + +impl Keypad { + pub fn new() -> Self { + Self { + id: egui::Id::new("keypad"), + } + } + + pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + let events = ctx.memory_mut(|m| { + m.data + .get_temp_mut_or_default::(self.id) + .events + .take() + }); + if let Some(mut events) = events { + events.append(&mut raw_input.events); + raw_input.events = events; + } + } + + fn buttons(ui: &mut Ui, state: &mut State) -> Transition { + let mut trans = Transition::None; + ui.vertical(|ui| { + let window_margin = ui.spacing().window_margin; + let size_1x1 = vec2(32.0, 26.0); + let _size_1x2 = vec2(32.0, 52.0 + window_margin.top); + let _size_2x1 = vec2(64.0 + window_margin.left, 26.0); + + ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left); + + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("1")).clicked() { + state.queue_char('1'); + } + if ui.add_sized(size_1x1, Button::new("2")).clicked() { + state.queue_char('2'); + } + if ui.add_sized(size_1x1, Button::new("3")).clicked() { + state.queue_char('3'); + } + if ui.add_sized(size_1x1, Button::new("⏮")).clicked() { + state.queue_key(egui::Key::Home); + } + if ui.add_sized(size_1x1, Button::new("🔙")).clicked() { + state.queue_key(egui::Key::Backspace); + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("4")).clicked() { + state.queue_char('4'); + } + if ui.add_sized(size_1x1, Button::new("5")).clicked() { + state.queue_char('5'); + } + if ui.add_sized(size_1x1, Button::new("6")).clicked() { + state.queue_char('6'); + } + if ui.add_sized(size_1x1, Button::new("⏭")).clicked() { + state.queue_key(egui::Key::End); + } + if ui.add_sized(size_1x1, Button::new("⎆")).clicked() { + state.queue_key(egui::Key::Enter); + trans = Transition::CloseOnNextFrame; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("7")).clicked() { + state.queue_char('7'); + } + if ui.add_sized(size_1x1, Button::new("8")).clicked() { + state.queue_char('8'); + } + if ui.add_sized(size_1x1, Button::new("9")).clicked() { + state.queue_char('9'); + } + if ui.add_sized(size_1x1, Button::new("⏶")).clicked() { + state.queue_key(egui::Key::ArrowUp); + } + if ui.add_sized(size_1x1, Button::new("⌨")).clicked() { + trans = Transition::CloseImmediately; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("0")).clicked() { + state.queue_char('0'); + } + if ui.add_sized(size_1x1, Button::new(".")).clicked() { + state.queue_char('.'); + } + if ui.add_sized(size_1x1, Button::new("⏴")).clicked() { + state.queue_key(egui::Key::ArrowLeft); + } + if ui.add_sized(size_1x1, Button::new("⏷")).clicked() { + state.queue_key(egui::Key::ArrowDown); + } + if ui.add_sized(size_1x1, Button::new("⏵")).clicked() { + state.queue_key(egui::Key::ArrowRight); + } + }); + }); + + trans + } + + pub fn show(&self, ctx: &egui::Context) { + let (focus, mut state) = ctx.memory(|m| { + ( + m.focus(), + m.data.get_temp::(self.id).unwrap_or_default(), + ) + }); + + let mut is_first_show = false; + if ctx.wants_keyboard_input() && state.focus != focus { + let y = ctx.style().spacing.interact_size.y * 1.25; + state.open = true; + state.start_pos = ctx.input(|i| { + i.pointer + .hover_pos() + .map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y)) + }); + state.focus = focus; + is_first_show = true; + } + + if state.close_on_next_frame { + state.open = false; + state.close_on_next_frame = false; + state.focus = None; + } + + let mut open = state.open; + + let win = egui::Window::new("⌨ Keypad"); + let win = if is_first_show { + win.current_pos(state.start_pos) + } else { + win.default_pos(state.start_pos) + }; + let resp = win + .movable(true) + .resizable(false) + .open(&mut open) + .show(ctx, |ui| Self::buttons(ui, &mut state)); + + state.open = open; + + if let Some(resp) = resp { + match resp.inner { + Some(Transition::CloseOnNextFrame) => { + state.close_on_next_frame = true; + } + Some(Transition::CloseImmediately) => { + state.open = false; + state.focus = None; + } + _ => {} + } + if !state.closable && resp.response.hovered() { + state.closable = true; + } + if state.closable && resp.response.clicked_elsewhere() { + state.open = false; + state.closable = false; + state.focus = None; + } + if is_first_show { + ctx.move_to_top(resp.response.layer_id); + } + } + + if let (true, Some(focus)) = (state.open, state.focus) { + ctx.memory_mut(|m| { + m.request_focus(focus); + }); + } + + ctx.memory_mut(|m| m.data.insert_temp(self.id, state)); + } +} + +impl Default for Keypad { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/custom_keypad/src/main.rs b/examples/custom_keypad/src/main.rs new file mode 100644 index 000000000..5cb26240c --- /dev/null +++ b/examples/custom_keypad/src/main.rs @@ -0,0 +1,68 @@ +// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use eframe::egui; + +mod keypad; +use keypad::Keypad; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), + ..Default::default() + }; + eframe::run_native( + "Custom Keypad App", + options, + Box::new(|cc| { + // Use the dark theme + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Box::::default() + }), + ) +} + +struct MyApp { + name: String, + age: u32, + keypad: Keypad, +} + +impl MyApp {} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + keypad: Keypad::new(), + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::Window::new("Custom Keypad") + .default_pos([100.0, 100.0]) + .title_bar(true) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + }); + + self.keypad.show(ctx); + } + + fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + self.keypad.bump_events(ctx, raw_input); + } +}