From 247026149cc438b08e7457f2c56e792a51eeaf09 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 25 Jan 2021 18:50:19 +0100 Subject: [PATCH] Multiple mouse button support (#135) Add support for primary, secondary and middle mouse buttons. Also improve ability to click things in low FPS situations. This introduces a lot of breaking changes: Backends/integrations now pass mouse events via the even stream. Response has an interface of mostly methods instead of public members. input.mouse is now input.pointer and has new interface. * Rename 'mouse' to 'pointer' everywhere (pointer = mouse or touch) * Make Response::clicked and Response::double_clicked into methods * Remove Response::active and add dragged() and interact_pointer_pos() * Support multiple mouse buttons * Make PointerState interface all methods * Make most members of Response private --- CHANGELOG.md | 14 +- README.md | 2 +- eframe/examples/hello_world.rs | 2 +- egui/src/containers/area.rs | 14 +- egui/src/containers/collapsing_header.rs | 2 +- egui/src/containers/combo_box.rs | 4 +- egui/src/containers/popup.rs | 17 +- egui/src/containers/resize.rs | 11 +- egui/src/containers/scroll_area.rs | 44 +- egui/src/containers/window.rs | 58 +-- egui/src/context.rs | 241 ++++----- egui/src/data/input.rs | 47 +- egui/src/input_state.rs | 460 ++++++++++++------ egui/src/introspection.rs | 8 +- egui/src/memory.rs | 13 +- egui/src/menu.rs | 9 +- egui/src/response.rs | 165 +++++-- egui/src/style.rs | 4 +- egui/src/ui.rs | 42 +- egui/src/widgets/button.rs | 2 +- egui/src/widgets/color_picker.rs | 22 +- egui/src/widgets/drag_value.rs | 6 +- egui/src/widgets/hyperlink.rs | 6 +- egui/src/widgets/mod.rs | 7 +- egui/src/widgets/selected_label.rs | 2 +- egui/src/widgets/slider.rs | 26 +- egui/src/widgets/text_edit.rs | 32 +- egui_demo_lib/src/apps/demo/demo_window.rs | 12 +- egui_demo_lib/src/apps/demo/demo_windows.rs | 7 +- egui_demo_lib/src/apps/demo/drag_and_drop.rs | 15 +- egui_demo_lib/src/apps/demo/font_book.rs | 4 +- egui_demo_lib/src/apps/demo/input_test.rs | 52 ++ egui_demo_lib/src/apps/demo/mod.rs | 1 + egui_demo_lib/src/apps/demo/painting.rs | 12 +- egui_demo_lib/src/apps/demo/scrolls.rs | 6 +- egui_demo_lib/src/apps/demo/sliders.rs | 2 +- egui_demo_lib/src/apps/demo/toggle_switch.rs | 4 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 4 +- egui_demo_lib/src/apps/demo/widgets.rs | 4 +- egui_demo_lib/src/apps/http_app.rs | 10 +- egui_demo_lib/src/frame_history.rs | 6 +- egui_demo_lib/src/wrap_app.rs | 12 +- egui_glium/src/lib.rs | 41 +- egui_web/CHANGELOG.md | 8 + egui_web/src/backend.rs | 3 + egui_web/src/lib.rs | 125 ++++- emath/src/vec2.rs | 44 ++ 47 files changed, 1052 insertions(+), 580 deletions(-) create mode 100644 egui_demo_lib/src/apps/demo/input_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8894fe7e..fa351584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added ⭐ -* `egui::popup::popup_below_widget`: show a popup area below another widget +* Add support for secondary and middle mouse buttons. +* `egui::popup::popup_below_widget`: show a popup area below another widget. + +### Changed 🔧 + +* `mouse` has be renamed `pointer` everywhere (to make it clear it includes touches too). +* Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`. +* `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead. +* Backend: pointer (mouse/touch) position and buttons are now passed to egui in the event stream. + +### Fixed 🐛 + +* It is now possible to click widgets even when FPS is very low. ## 0.8.0 - 2021-01-17 - Grid layout & new visual style diff --git a/README.md b/README.md index 6d1ade12..c133cba2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ui.horizontal(|ui| { ui.text_edit_singleline(&mut name); }); ui.add(egui::Slider::u32(&mut age, 0..=120).text("age")); -if ui.button("Click each year").clicked { +if ui.button("Click each year").clicked() { age += 1; } ui.label(format!("Hello '{}', age {}", name, age)); diff --git a/eframe/examples/hello_world.rs b/eframe/examples/hello_world.rs index 218752f6..b7ac1ac6 100644 --- a/eframe/examples/hello_world.rs +++ b/eframe/examples/hello_world.rs @@ -29,7 +29,7 @@ impl epi::App for MyApp { ui.text_edit_singleline(name); }); ui.add(egui::Slider::u32(age, 0..=120).text("age")); - if ui.button("Click each year").clicked { + if ui.button("Click each year").clicked() { *age += 1; } ui.label(format!("Hello '{}', age {}", name, age)); diff --git a/egui/src/containers/area.rs b/egui/src/containers/area.rs index 03a3a229..f4c1f27b 100644 --- a/egui/src/containers/area.rs +++ b/egui/src/containers/area.rs @@ -249,14 +249,14 @@ impl Prepared { sense, ); - if move_response.active && movable { - state.pos += ctx.input().mouse.delta; + if move_response.dragged() && movable { + state.pos += ctx.input().pointer.delta(); } state.pos = ctx.constrain_window_rect(state.rect()).min; - if (move_response.active || move_response.clicked) - || mouse_pressed_on_area(ctx, layer_id) + if (move_response.dragged() || move_response.clicked()) + || pointer_pressed_on_area(ctx, layer_id) || !ctx.memory().areas.visible_last_frame(&layer_id) { ctx.memory().areas.move_to_top(layer_id); @@ -268,9 +268,9 @@ impl Prepared { } } -fn mouse_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { - if let Some(mouse_pos) = ctx.input().mouse.pos { - ctx.input().mouse.pressed && ctx.layer_id_at(mouse_pos) == Some(layer_id) +fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { + if let Some(pointer_pos) = ctx.input().pointer.interact_pos() { + ctx.input().pointer.any_pressed() && ctx.layer_id_at(pointer_pos) == Some(layer_id) } else { false } diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 813ba1fc..555fc385 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -199,7 +199,7 @@ impl CollapsingHeader { ); let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); - if header_response.clicked { + if header_response.clicked() { state.toggle(ui); } diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index a22eaffd..8fd151f7 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -84,7 +84,7 @@ pub fn combo_box( .galley(text_rect.min, galley, text_style, visuals.text_color()); }); - if button_response.clicked { + if button_response.clicked() { ui.memory().toggle_popup(popup_id); } const MAX_COMBO_HEIGHT: f32 = 128.0; @@ -118,7 +118,7 @@ fn button_frame( outer_rect.set_height(outer_rect.height().at_least(interact_size.y)); let mut response = ui.interact(outer_rect, id, sense); - response.active |= button_active; + response.is_pointer_button_down_on |= button_active; let visuals = ui.style().interact(&response); ui.painter().set( diff --git a/egui/src/containers/popup.rs b/egui/src/containers/popup.rs index e2c690b4..d1a51089 100644 --- a/egui/src/containers/popup.rs +++ b/egui/src/containers/popup.rs @@ -2,7 +2,7 @@ use crate::*; -/// Show a tooltip at the current mouse position (if any). +/// Show a tooltip at the current pointer position (if any). /// /// Most of the time it is easier to use [`Response::on_hover_ui`]. /// @@ -10,7 +10,7 @@ use crate::*; /// /// ``` /// # let mut ui = egui::Ui::__test(); -/// if ui.ui_contains_mouse() { +/// if ui.ui_contains_pointer() { /// egui::show_tooltip(ui.ctx(), |ui| { /// ui.label("Helpful text"); /// }); @@ -21,9 +21,9 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { let window_pos = if let Some(tooltip_rect) = tooltip_rect { tooltip_rect.left_bottom() - } else if let Some(mouse_pos) = ctx.input().mouse.pos { + } else if let Some(pointer_pos) = ctx.input().pointer.tooltip_pos() { let expected_size = vec2(ctx.style().spacing.tooltip_width, 32.0); - let position = mouse_pos + vec2(16.0, 16.0); + let position = pointer_pos + vec2(16.0, 16.0); let position = position.min(ctx.input().screen_rect().right_bottom() - expected_size); let position = position.max(ctx.input().screen_rect().left_top()); position @@ -41,7 +41,7 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { ctx.frame_state().tooltip_rect = Some(tooltip_rect.union(response.rect)); } -/// Show some text at the current mouse position (if any). +/// Show some text at the current pointer position (if any). /// /// Most of the time it is easier to use [`Response::on_hover_text`]. /// @@ -49,7 +49,7 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { /// /// ``` /// # let mut ui = egui::Ui::__test(); -/// if ui.ui_contains_mouse() { +/// if ui.ui_contains_pointer() { /// egui::show_tooltip_text(ui.ctx(), "Helpful text"); /// } /// ``` @@ -89,7 +89,7 @@ fn show_tooltip_area( /// # let ui = &mut egui::Ui::__test(); /// let response = ui.button("Open popup"); /// let popup_id = ui.make_persistent_id("my_unique_id"); -/// if response.clicked { +/// if response.clicked() { /// ui.memory().toggle_popup(popup_id); /// } /// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| { @@ -121,7 +121,8 @@ pub fn popup_below_widget( }); }); - if ui.input().key_pressed(Key::Escape) || ui.input().mouse.click && !widget_response.clicked + if ui.input().key_pressed(Key::Escape) + || ui.input().pointer.any_click() && !widget_response.clicked() { ui.memory().close_popup(); } diff --git a/egui/src/containers/resize.rs b/egui/src/containers/resize.rs index bb1f7a1b..5c93f7cd 100644 --- a/egui/src/containers/resize.rs +++ b/egui/src/containers/resize.rs @@ -191,12 +191,11 @@ impl Resize { Rect::from_min_size(position + state.desired_size - corner_size, corner_size); let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag()); - if corner_response.active { - if let Some(mouse_pos) = ui.input().mouse.pos { - user_requested_size = - Some(mouse_pos - position + 0.5 * corner_response.rect.size()); - } + if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + user_requested_size = + Some(pointer_pos - position + 0.5 * corner_response.rect.size()); } + Some(corner_response) } else { None @@ -295,7 +294,7 @@ impl Resize { if let Some(corner_response) = corner_response { paint_resize_corner(ui, &corner_response); - if corner_response.hovered || corner_response.active { + if corner_response.hovered() || corner_response.dragged() { ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe; } } diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 2a0a63db..5ce2583c 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -212,9 +212,9 @@ impl Prepared { let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag()); let input = ui.input(); - if content_response.active { - state.offset.y -= input.mouse.delta.y; - state.vel = input.mouse.velocity; + if content_response.dragged() { + state.offset.y -= input.pointer.delta().y; + state.vel = input.pointer.velocity(); } else { let stop_speed = 20.0; // Pixels per second. let friction_coeff = 1000.0; // Pixels per second squared. @@ -234,7 +234,7 @@ impl Prepared { } let max_offset = content_size.y - inner_rect.height(); - if ui.rect_contains_mouse(outer_rect) { + if ui.rect_contains_pointer(outer_rect) { let mut frame_state = ui.ctx().frame_state(); let scroll_delta = frame_state.scroll_delta; @@ -283,26 +283,24 @@ impl Prepared { let interact_id = id.with("vertical"); let response = ui.interact(outer_scroll_rect, interact_id, Sense::click_and_drag()); - if response.active { - if let Some(mouse_pos) = ui.input().mouse.pos { - let scroll_start_offset_from_top = - state.scroll_start_offset_from_top.get_or_insert_with(|| { - if handle_rect.contains(mouse_pos) { - mouse_pos.y - handle_rect.top() - } else { - let handle_top_pos_at_bottom = bottom - handle_rect.height(); - // Calculate the new handle top position, centering the handle on the mouse. - let new_handle_top_pos = clamp( - mouse_pos.y - handle_rect.height() / 2.0, - top..=handle_top_pos_at_bottom, - ); - mouse_pos.y - new_handle_top_pos - } - }); + if let Some(pointer_pos) = response.interact_pointer_pos() { + let scroll_start_offset_from_top = + state.scroll_start_offset_from_top.get_or_insert_with(|| { + if handle_rect.contains(pointer_pos) { + pointer_pos.y - handle_rect.top() + } else { + let handle_top_pos_at_bottom = bottom - handle_rect.height(); + // Calculate the new handle top position, centering the handle on the mouse. + let new_handle_top_pos = clamp( + pointer_pos.y - handle_rect.height() / 2.0, + top..=handle_top_pos_at_bottom, + ); + pointer_pos.y - new_handle_top_pos + } + }); - let new_handle_top = mouse_pos.y - *scroll_start_offset_from_top; - state.offset.y = remap(new_handle_top, top..=bottom, 0.0..=content_size.y); - } + let new_handle_top = pointer_pos.y - *scroll_start_offset_from_top; + state.offset.y = remap(new_handle_top, top..=bottom, 0.0..=content_size.y); } else { state.scroll_start_offset_from_top = None; } diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 32038c36..e87afbca 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -363,12 +363,14 @@ impl<'open> Window<'open> { ctx.style().visuals.widgets.active, ); } else if let Some(hover_interaction) = hover_interaction { - paint_frame_interaction( - &mut area_content_ui, - outer_rect, - hover_interaction, - ctx.style().visuals.widgets.hovered, - ); + if ctx.input().pointer.has_pointer() { + paint_frame_interaction( + &mut area_content_ui, + outer_rect, + hover_interaction, + ctx.style().visuals.widgets.hovered, + ); + } } } let full_response = area.end(ctx, area_content_ui); @@ -448,24 +450,24 @@ fn interact( fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option { window_interaction.set_cursor(ctx); - let mouse_pos = ctx.input().mouse.pos?; + let pointer_pos = ctx.input().pointer.interact_pos()?; let mut rect = window_interaction.start_rect; // prevent drift if window_interaction.is_resize() { if window_interaction.left { - rect.min.x = ctx.round_to_pixel(mouse_pos.x); + rect.min.x = ctx.round_to_pixel(pointer_pos.x); } else if window_interaction.right { - rect.max.x = ctx.round_to_pixel(mouse_pos.x); + rect.max.x = ctx.round_to_pixel(pointer_pos.x); } if window_interaction.top { - rect.min.y = ctx.round_to_pixel(mouse_pos.y); + rect.min.y = ctx.round_to_pixel(pointer_pos.y); } else if window_interaction.bottom { - rect.max.y = ctx.round_to_pixel(mouse_pos.y); + rect.max.y = ctx.round_to_pixel(pointer_pos.y); } } else { // movement - rect = rect.translate(mouse_pos - ctx.input().mouse.press_origin?); + rect = rect.translate(pointer_pos - ctx.input().pointer.press_origin()?); } Some(rect) @@ -491,7 +493,7 @@ fn window_interaction( if window_interaction.is_none() { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { hover_window_interaction.set_cursor(ctx); - if ctx.input().mouse.pressed { + if ctx.input().pointer.any_pressed() && ctx.input().pointer.any_down() { ctx.memory().interaction.drag_id = Some(id); ctx.memory().interaction.drag_is_window = true; window_interaction = Some(hover_window_interaction); @@ -517,13 +519,13 @@ fn resize_hover( area_layer_id: LayerId, rect: Rect, ) -> Option { - let mouse_pos = ctx.input().mouse.pos?; + let pointer_pos = ctx.input().pointer.interact_pos()?; - if ctx.input().mouse.down && !ctx.input().mouse.pressed { + if ctx.input().pointer.any_down() && !ctx.input().pointer.any_pressed() { return None; // already dragging (something) } - if let Some(top_layer_id) = ctx.layer_id_at(mouse_pos) { + if let Some(top_layer_id) = ctx.layer_id_at(pointer_pos) { if top_layer_id != area_layer_id && top_layer_id.order != Order::Background { return None; // Another window is on top here } @@ -536,33 +538,33 @@ fn resize_hover( let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - if !rect.expand(side_grab_radius).contains(mouse_pos) { + if !rect.expand(side_grab_radius).contains(pointer_pos) { return None; } let (mut left, mut right, mut top, mut bottom) = Default::default(); if possible.resizable { - right = (rect.right() - mouse_pos.x).abs() <= side_grab_radius; - bottom = (rect.bottom() - mouse_pos.y).abs() <= side_grab_radius; + right = (rect.right() - pointer_pos.x).abs() <= side_grab_radius; + bottom = (rect.bottom() - pointer_pos.y).abs() <= side_grab_radius; - if rect.right_bottom().distance(mouse_pos) < corner_grab_radius { + if rect.right_bottom().distance(pointer_pos) < corner_grab_radius { right = true; bottom = true; } if possible.movable { - left = (rect.left() - mouse_pos.x).abs() <= side_grab_radius; - top = (rect.top() - mouse_pos.y).abs() <= side_grab_radius; + left = (rect.left() - pointer_pos.x).abs() <= side_grab_radius; + top = (rect.top() - pointer_pos.y).abs() <= side_grab_radius; - if rect.right_top().distance(mouse_pos) < corner_grab_radius { + if rect.right_top().distance(pointer_pos) < corner_grab_radius { right = true; top = true; } - if rect.left_top().distance(mouse_pos) < corner_grab_radius { + if rect.left_top().distance(pointer_pos) < corner_grab_radius { left = true; top = true; } - if rect.left_bottom().distance(mouse_pos) < corner_grab_radius { + if rect.left_bottom().distance(pointer_pos) < corner_grab_radius { left = true; bottom = true; } @@ -671,7 +673,7 @@ fn show_title_bar( let (_id, rect) = ui.allocate_space(button_size); let collapse_button_response = ui.interact(rect, collapsing_id, Sense::click()); - if collapse_button_response.clicked { + if collapse_button_response.clicked() { collapsing.toggle(ui); } let openness = collapsing.openness(ui.ctx(), collapsing_id); @@ -721,7 +723,7 @@ impl TitleBar { if let Some(open) = open { // Add close button now that we know our full width: - if self.close_button_ui(ui).clicked { + if self.close_button_ui(ui).clicked() { *open = false; } } @@ -752,7 +754,7 @@ impl TitleBar { if ui .interact(self.rect, self.id, Sense::click()) - .double_clicked + .double_clicked() && collapsible { collapsing.toggle(ui); diff --git a/egui/src/context.rs b/egui/src/context.rs index d283074f..5d8d6068 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -8,6 +8,7 @@ use std::sync::{ use crate::{ animation_manager::AnimationManager, data::output::Output, + input_state::*, layers::GraphicLayers, mutex::{Mutex, MutexGuard}, paint::{stats::*, text::Fonts, *}, @@ -190,8 +191,8 @@ impl CtxRef { let show_error = |pos: Pos2, text: String| { let painter = self.debug_painter(); let rect = painter.error(pos, text); - if let Some(mouse_pos) = self.input.mouse.pos { - if rect.contains(mouse_pos) { + if let Some(pointer_pos) = self.input.pointer.tooltip_pos() { + if rect.contains(pointer_pos) { painter.error( rect.left_bottom() + vec2(2.0, 4.0), "ID clashes happens when things like Windows or CollpasingHeaders share names,\n\ @@ -227,7 +228,7 @@ impl CtxRef { sense: Sense, ) -> Response { let interact_rect = rect.expand2((0.5 * item_spacing).min(Vec2::splat(5.0))); // make it easier to click - let hovered = self.rect_contains_mouse(layer_id, clip_rect.intersect(interact_rect)); + let hovered = self.rect_contains_pointer(layer_id, clip_rect.intersect(interact_rect)); self.interact_with_hovered(layer_id, id, rect, sense, hovered) } @@ -241,26 +242,28 @@ impl CtxRef { hovered: bool, ) -> Response { let has_kb_focus = self.memory().has_kb_focus(id); - - // If the the focus is lost after the call to interact, - // this will be `false`, so `TextEdit` also sets this manually. let lost_kb_focus = self.memory().lost_kb_focus(id); + let mut response = Response { + ctx: self.clone(), + layer_id, + id, + rect, + sense, + hovered, + clicked: Default::default(), + double_clicked: Default::default(), + dragged: false, + drag_released: false, + is_pointer_button_down_on: false, + interact_pointer_pos: None, + has_kb_focus, + lost_kb_focus, + }; + if sense == Sense::hover() || !layer_id.allow_interaction() { // Not interested or allowed input: - return Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered, - clicked: false, - double_clicked: false, - active: false, - has_kb_focus, - lost_kb_focus, - }; + return response; } self.register_interaction_id(id, rect.min); @@ -270,102 +273,62 @@ impl CtxRef { memory.interaction.click_interest |= hovered && sense.click; memory.interaction.drag_interest |= hovered && sense.drag; - let active = - memory.interaction.click_id == Some(id) || memory.interaction.drag_id == Some(id); + response.dragged = memory.interaction.drag_id == Some(id); + response.is_pointer_button_down_on = + memory.interaction.click_id == Some(id) || response.dragged; - if self.input.mouse.pressed { - if hovered { - let mut response = Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered: true, - clicked: false, - double_clicked: false, - active: false, - has_kb_focus, - lost_kb_focus, - }; - - if sense.click && memory.interaction.click_id.is_none() { - // start of a click - memory.interaction.click_id = Some(id); - response.active = true; + for pointer_event in &self.input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(pos) => { + if response.is_pointer_button_down_on { + response.interact_pointer_pos = Some(*pos); + } } + PointerEvent::Pressed(pos) => { + if hovered { + if sense.click && memory.interaction.click_id.is_none() { + // potential start of a click + memory.interaction.click_id = Some(id); + response.interact_pointer_pos = Some(*pos); + response.is_pointer_button_down_on = true; + } - if sense.drag - && (memory.interaction.drag_id.is_none() || memory.interaction.drag_is_window) - { - // start of a drag - memory.interaction.drag_id = Some(id); - memory.interaction.drag_is_window = false; - memory.window_interaction = None; // HACK: stop moving windows (if any) - response.active = true; + if sense.drag + && (memory.interaction.drag_id.is_none() + || memory.interaction.drag_is_window) + { + // potential start of a drag + response.interact_pointer_pos = Some(*pos); + memory.interaction.drag_id = Some(id); + memory.interaction.drag_is_window = false; + memory.window_interaction = None; // HACK: stop moving windows (if any) + response.is_pointer_button_down_on = true; + response.dragged = true; + } + } } + PointerEvent::Released(click) => { + response.drag_released = response.dragged; + response.dragged = false; - response - } else { - // miss - Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered, - clicked: false, - double_clicked: false, - active: false, - has_kb_focus, - lost_kb_focus, + if hovered && response.is_pointer_button_down_on { + if let Some(click) = click { + let clicked = hovered && response.is_pointer_button_down_on; + response.interact_pointer_pos = Some(click.pos); + response.clicked[click.button as usize] = clicked; + response.double_clicked[click.button as usize] = + clicked && click.is_double(); + } + } } } - } else if self.input.mouse.released { - let clicked = hovered && active && self.input.mouse.could_be_click; - Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered, - clicked, - double_clicked: clicked && self.input.mouse.double_click, - active, - has_kb_focus, - lost_kb_focus, - } - } else if self.input.mouse.down { - Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered: hovered && active, - clicked: false, - double_clicked: false, - active, - has_kb_focus, - lost_kb_focus, - } - } else { - Response { - ctx: self.clone(), - layer_id, - id, - rect, - sense, - hovered, - clicked: false, - double_clicked: false, - active, - has_kb_focus, - lost_kb_focus, - } } + + if self.input.pointer.any_down() { + response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets + } + + response } pub fn debug_painter(&self) -> Painter { @@ -648,12 +611,12 @@ impl Context { // --------------------------------------------------------------------- - /// Is the mouse over any egui area? - pub fn is_mouse_over_area(&self) -> bool { - if let Some(mouse_pos) = self.input.mouse.pos { - if let Some(layer) = self.layer_id_at(mouse_pos) { + /// Is the pointer (mouse/touch) over any egui area? + pub fn is_pointer_over_area(&self) -> bool { + if let Some(pointer_pos) = self.input.pointer.interact_pos() { + if let Some(layer) = self.layer_id_at(pointer_pos) { if layer.order == Order::Background { - !self.frame_state().unused_rect.contains(mouse_pos) + !self.frame_state().unused_rect.contains(pointer_pos) } else { true } @@ -665,19 +628,29 @@ impl Context { } } - /// True if egui is currently interested in the mouse. - /// Could be the mouse is hovering over a [`Window`] or the user is dragging a widget. - /// If `false`, the mouse is outside of any egui area and so + /// True if egui is currently interested in the pointer (mouse or touch). + /// Could be the pointer is hovering over a [`Window`] or the user is dragging a widget. + /// If `false`, the pointer is outside of any egui area and so /// you may be interested in what it is doing (e.g. controlling your game). /// Returns `false` if a drag started outside of egui and then moved over an egui area. - pub fn wants_mouse_input(&self) -> bool { - self.is_using_mouse() || (self.is_mouse_over_area() && !self.input().mouse.down) + pub fn wants_pointer_input(&self) -> bool { + self.is_using_pointer() || (self.is_pointer_over_area() && !self.input().pointer.any_down()) } - /// Is egui currently using the mouse position (e.g. dragging a slider). - /// NOTE: this will return `false` if the mouse is just hovering over an egui area. + /// Is egui currently using the pointer position (e.g. dragging a slider). + /// NOTE: this will return `false` if the pointer is just hovering over an egui area. + pub fn is_using_pointer(&self) -> bool { + self.memory().interaction.is_using_pointer() + } + + #[deprecated = "Renamed wants_pointer_input"] + pub fn wants_mouse_input(&self) -> bool { + self.wants_pointer_input() + } + + #[deprecated = "Renamed is_using_pointer"] pub fn is_using_mouse(&self) -> bool { - self.memory().interaction.is_using_mouse() + self.is_using_pointer() } /// If `true`, egui is currently listening on text input (e.g. typing text in a [`TextEdit`]). @@ -698,9 +671,9 @@ impl Context { self.memory().layer_id_at(pos, resize_grab_radius_side) } - pub(crate) fn rect_contains_mouse(&self, layer_id: LayerId, rect: Rect) -> bool { - if let Some(mouse_pos) = self.input.mouse.pos { - rect.contains(mouse_pos) && self.layer_id_at(mouse_pos) == Some(layer_id) + pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { + if let Some(pointer_pos) = self.input.pointer.interact_pos() { + rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id) } else { false } @@ -766,10 +739,12 @@ impl Context { pub fn inspection_ui(&self, ui: &mut Ui) { use crate::containers::*; - ui.label(format!("Is using mouse: {}", self.is_using_mouse())) - .on_hover_text("Is egui currently using the mouse actively (e.g. dragging a slider)?"); - ui.label(format!("Wants mouse input: {}", self.wants_mouse_input())) - .on_hover_text("Is egui currently interested in the location of the mouse (either because it is in use, or because it is hovering over a window)."); + ui.label(format!("Is using pointer: {}", self.is_using_pointer())) + .on_hover_text( + "Is egui currently using the pointer actively (e.g. dragging a slider)?", + ); + ui.label(format!("Wants pointer input: {}", self.wants_pointer_input())) + .on_hover_text("Is egui currently interested in the location of the pointer (either because it is in use, or because it is hovering over a window)."); ui.label(format!( "Wants keyboard input: {}", self.wants_keyboard_input() @@ -792,7 +767,7 @@ impl Context { if ui .button("Reset all") .on_hover_text("Reset all egui state") - .clicked + .clicked() { *self.memory() = Default::default(); } @@ -802,7 +777,7 @@ impl Context { "{} areas (window positions)", self.memory().areas.count() )); - if ui.button("Reset").clicked { + if ui.button("Reset").clicked() { self.memory().areas = Default::default(); } }); @@ -835,28 +810,28 @@ impl Context { "{} collapsing headers", self.memory().collapsing_headers.len() )); - if ui.button("Reset").clicked { + if ui.button("Reset").clicked() { self.memory().collapsing_headers = Default::default(); } }); ui.horizontal(|ui| { ui.label(format!("{} menu bars", self.memory().menu_bar.len())); - if ui.button("Reset").clicked { + if ui.button("Reset").clicked() { self.memory().menu_bar = Default::default(); } }); ui.horizontal(|ui| { ui.label(format!("{} scroll areas", self.memory().scroll_areas.len())); - if ui.button("Reset").clicked { + if ui.button("Reset").clicked() { self.memory().scroll_areas = Default::default(); } }); ui.horizontal(|ui| { ui.label(format!("{} resize areas", self.memory().resize.len())); - if ui.button("Reset").clicked { + if ui.button("Reset").clicked() { self.memory().resize = Default::default(); } }); diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index d4a9f177..caa22cc2 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -9,13 +9,6 @@ use crate::math::*; /// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner. #[derive(Clone, Debug)] pub struct RawInput { - /// Is the button currently down? - /// NOTE: egui currently only supports the primary mouse button. - pub mouse_down: bool, - - /// Current position of the mouse in points. - pub mouse_pos: Option, - /// How many points (logical pixels) the user scrolled pub scroll_delta: Vec2, @@ -56,8 +49,6 @@ impl Default for RawInput { fn default() -> Self { #![allow(deprecated)] // for screen_size Self { - mouse_down: false, - mouse_pos: None, scroll_delta: Vec2::zero(), screen_size: Default::default(), screen_rect: None, @@ -75,8 +66,6 @@ impl RawInput { pub fn take(&mut self) -> RawInput { #![allow(deprecated)] // for screen_size RawInput { - mouse_down: self.mouse_down, - mouse_pos: self.mouse_pos, scroll_delta: std::mem::take(&mut self.scroll_delta), screen_size: self.screen_size, screen_rect: self.screen_rect, @@ -107,8 +96,38 @@ pub enum Event { pressed: bool, modifiers: Modifiers, }, + + PointerMoved(Pos2), + PointerButton { + pos: Pos2, + button: PointerButton, + pressed: bool, + /// The state of the modifier keys at the time of the event + modifiers: Modifiers, + }, + /// The mouse left the screen, or the last/primary touch input disappeared. + /// + /// This means there is no longer a cursor on the screen for hovering etc. + /// + /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. + PointerGone, } +/// Mouse button (or similar for touch input) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PointerButton { + /// The primary mouse button is usually the left one. + Primary = 0, + /// The secondary mouse button is usually the right one, + /// and most often used for context menus or other optional things. + Secondary = 1, + /// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel). + Middle = 2, +} + +/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`]. +pub const NUM_POINTER_BUTTONS: usize = 3; + /// State of the modifier keys. These must be fed to egui. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct Modifiers { @@ -208,8 +227,6 @@ impl RawInput { pub fn ui(&self, ui: &mut crate::Ui) { #![allow(deprecated)] // for screen_size let Self { - mouse_down, - mouse_pos, scroll_delta, screen_size: _, screen_rect, @@ -220,10 +237,6 @@ impl RawInput { events, } = self; - // TODO: simpler way to show values, e.g. `ui.value("Mouse Pos:", self.mouse_pos); - // TODO: `ui.style_mut().text_style = TextStyle::Monospace`; - ui.label(format!("mouse_down: {}", mouse_down)); - ui.label(format!("mouse_pos: {:.1?}", mouse_pos)); ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("pixels_per_point: {:?}", pixels_per_point)) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 17e4c6ad..a853df49 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -5,20 +5,21 @@ use crate::data::input::*; pub use crate::data::input::Key; -/// If mouse moves more than this, it is no longer a click (but maybe a drag) +/// If the pointer moves more than this, it is no longer a click (but maybe a drag) const MAX_CLICK_DIST: f32 = 6.0; // TODO: move to settings -/// The new mouse press must come within this many seconds from previous mouse release +/// The new pointer press must come within this many seconds from previous pointer release const MAX_CLICK_DELAY: f64 = 0.3; // TODO: move to settings /// Input state that egui updates each frame. #[derive(Clone, Debug)] pub struct InputState { - /// The raw input we got this frame + /// The raw input we got this frame from the backend. pub raw: RawInput, - pub mouse: CursorState, + /// State of the mouse or touch. + pub pointer: PointerState, - /// How many pixels the user scrolled + /// How many pixels the user scrolled. pub scroll_delta: Vec2, /// Position and size of the egui area. @@ -52,7 +53,7 @@ impl Default for InputState { fn default() -> Self { Self { raw: Default::default(), - mouse: Default::default(), + pointer: Default::default(), scroll_delta: Default::default(), screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), pixels_per_point: 1.0, @@ -66,71 +67,6 @@ impl Default for InputState { } } -/// Mouse (or touch) state. -#[derive(Clone, Debug)] -pub struct CursorState { - /// Is the button currently down? - /// true the frame when it is pressed, - /// false the frame it is released. - pub down: bool, - - /// The mouse went from !down to down - pub pressed: bool, - - /// The mouse went from down to !down - pub released: bool, - - /// If the mouse is down, will it register as a click when released? - /// Set to true on mouse down, set to false when mouse moves too much. - pub could_be_click: bool, - - /// Was there a click? - /// Did a mouse button get released this frame closely after going down? - pub click: bool, - - /// Was there a double-click? - pub double_click: bool, - - /// When did the mouse get click last? - /// Used to check for double-clicks. - pub last_click_time: f64, - - /// Current position of the mouse in points. - /// None for touch screens when finger is not down. - pub pos: Option, - - /// Where did the current click/drag originate? - pub press_origin: Option, - - /// How much the mouse moved compared to last frame, in points. - pub delta: Vec2, - - /// Current velocity of mouse cursor. - pub velocity: Vec2, - - /// Recent movement of the mouse. - /// Used for calculating velocity of mouse pointer. - pos_history: History, -} - -impl Default for CursorState { - fn default() -> Self { - Self { - down: false, - pressed: false, - released: false, - could_be_click: false, - click: false, - double_click: false, - last_click_time: std::f64::NEG_INFINITY, - pos: None, - press_origin: None, - delta: Vec2::zero(), - velocity: Vec2::zero(), - pos_history: History::new(1000, 0.1), - } - } -} impl InputState { #[must_use] pub fn begin_frame(self, new: RawInput) -> InputState { @@ -147,7 +83,7 @@ impl InputState { self.screen_rect } }); - let mouse = self.mouse.begin_frame(time, &new); + let pointer = self.pointer.begin_frame(time, &new); let mut keys_down = self.keys_down; for event in &new.events { if let Event::Key { key, pressed, .. } = event { @@ -159,7 +95,7 @@ impl InputState { } } InputState { - mouse, + pointer, scroll_delta: new.scroll_delta, screen_rect, pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), @@ -178,11 +114,7 @@ impl InputState { } pub fn wants_repaint(&self) -> bool { - self.mouse.pressed - || self.mouse.released - || self.mouse.delta != Vec2::zero() - || self.scroll_delta != Vec2::zero() - || !self.events.is_empty() + self.pointer.wants_repaint() || self.scroll_delta != Vec2::zero() || !self.events.is_empty() } /// Was the given key pressed this frame? @@ -236,85 +168,315 @@ impl InputState { } } -impl CursorState { +// ---------------------------------------------------------------------------- + +/// A pointer (mouse or touch) click. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct Click { + pub pos: Pos2, + pub button: PointerButton, + /// 1 or 2 (double-click) + pub count: u32, + /// Allows you to check for e.g. shift-click + pub modifiers: Modifiers, +} + +impl Click { + pub fn is_double(&self) -> bool { + self.count == 2 + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum PointerEvent { + Moved(Pos2), + Pressed(Pos2), + Released(Option), +} + +impl PointerEvent { + pub fn is_press(&self) -> bool { + matches!(self, PointerEvent::Pressed(_)) + } + pub fn is_release(&self) -> bool { + matches!(self, PointerEvent::Released(_)) + } + pub fn is_click(&self) -> bool { + matches!(self, PointerEvent::Released(Some(_click))) + } +} + +/// Mouse or touch state. +#[derive(Clone, Debug)] +pub struct PointerState { + // Consider a finger tapping a touch screen. + // What position should we report? + // The location of the touch, or `None`, because the finger is gone? + // + // For some cases we want the first: e.g. to check for interaction. + // For showing tooltips, we want the latter (no tooltips, since there are no fingers). + /// Latest reported pointer position. + /// When tapping a touch screen, this will be `None`. + latest_pos: Option, + + /// Latest position of the mouse, but ignoring any [`Event::PointerGone`] + /// if there were interactions this frame. + /// When tapping a touch screen, this will be the location of the touch. + interact_pos: Option, + + /// How much the pointer moved compared to last frame, in points. + delta: Vec2, + + /// Current velocity of pointer. + velocity: Vec2, + + /// Recent movement of the pointer. + /// Used for calculating velocity of pointer. + pos_history: History, + + down: [bool; NUM_POINTER_BUTTONS], + + /// Where did the current click/drag originate? + /// `None` if no mouse button is down. + press_origin: Option, + + /// If the pointer button is down, will it register as a click when released? + /// Set to true on pointer button down, set to false when pointer button moves too much. + could_be_click: bool, + + /// When did the pointer get click last? + /// Used to check for double-clicks. + last_click_time: f64, + + // /// All clicks that occurred this frame + // clicks: Vec, + /// All button events that occurred this frame + pub(crate) pointer_events: Vec, +} + +impl Default for PointerState { + fn default() -> Self { + Self { + latest_pos: None, + interact_pos: None, + delta: Vec2::zero(), + velocity: Vec2::zero(), + pos_history: History::new(1000, 0.1), + down: Default::default(), + press_origin: None, + could_be_click: false, + last_click_time: std::f64::NEG_INFINITY, + pointer_events: vec![], + } + } +} + +impl PointerState { #[must_use] - pub fn begin_frame(mut self, time: f64, new: &RawInput) -> CursorState { - let delta = new - .mouse_pos - .and_then(|new| self.pos.map(|last| new - last)) - .unwrap_or_default(); - let pressed = !self.down && new.mouse_down; + pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> PointerState { + self.pointer_events.clear(); - let released = self.down && !new.mouse_down; - let click = released && self.could_be_click; - let double_click = click && (time - self.last_click_time) < MAX_CLICK_DELAY; - let mut press_origin = self.press_origin; - let mut could_be_click = self.could_be_click; - let mut last_click_time = self.last_click_time; - if click { - last_click_time = time + let old_pos = self.latest_pos; + self.interact_pos = self.latest_pos; + + for event in &new.events { + match event { + Event::PointerMoved(pos) => { + let pos = *pos; + + self.latest_pos = Some(pos); + self.interact_pos = Some(pos); + + if let Some(press_origin) = &mut self.press_origin { + self.could_be_click &= press_origin.distance(pos) < MAX_CLICK_DIST; + } else { + self.could_be_click = false; + } + + self.pointer_events.push(PointerEvent::Moved(pos)); + } + Event::PointerButton { + pos, + button, + pressed, + modifiers, + } => { + let pos = *pos; + let button = *button; + let pressed = *pressed; + let modifiers = *modifiers; + + self.latest_pos = Some(pos); + self.interact_pos = Some(pos); + + if pressed { + // Start of a drag: we want to track the velocity for during the drag + // and ignore any incoming movement + self.pos_history.clear(); + } + + if pressed { + self.press_origin = Some(pos); + self.could_be_click = true; + self.pointer_events.push(PointerEvent::Pressed(pos)); + } else { + let clicked = self.could_be_click; + + let click = if clicked { + let double_click = (time - self.last_click_time) < MAX_CLICK_DELAY; + let count = if double_click { 2 } else { 1 }; + + self.last_click_time = time; + + Some(Click { + pos, + button, + count, + modifiers, + }) + } else { + None + }; + + self.pointer_events.push(PointerEvent::Released(click)); + + self.press_origin = None; + self.could_be_click = false; + } + + self.down[button as usize] = pressed; + } + Event::PointerGone => { + self.latest_pos = None; + // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. + } + _ => {} + } } - if pressed { - press_origin = new.mouse_pos; - could_be_click = true; - } else if !self.down || self.pos.is_none() { - press_origin = None; - } - - if let (Some(press_origin), Some(mouse_pos)) = (new.mouse_pos, press_origin) { - could_be_click &= press_origin.distance(mouse_pos) < MAX_CLICK_DIST; + self.delta = if let (Some(old_pos), Some(new_pos)) = (old_pos, self.latest_pos) { + new_pos - old_pos } else { - could_be_click = false; - } + Vec2::zero() + }; - if pressed { - // Start of a drag: we want to track the velocity for during the drag - // and ignore any incoming movement - self.pos_history.clear(); - } - - if let Some(mouse_pos) = new.mouse_pos { - self.pos_history.add(time, mouse_pos); + if let Some(pos) = self.latest_pos { + self.pos_history.add(time, pos); } else { - // we do not clear the `mouse_tracker` here, because it is exactly when a finger has + // we do not clear the `pos_history` here, because it is exactly when a finger has // released from the touch screen that we may want to assign a velocity to whatever - // the user tried to throw + // the user tried to throw. } self.pos_history.flush(time); - let velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 { + + self.velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 { self.pos_history.velocity().unwrap_or_default() } else { Vec2::default() }; - CursorState { - down: new.mouse_down && new.mouse_pos.is_some(), - pressed, - released, - could_be_click, - click, - double_click, - last_click_time, - pos: new.mouse_pos, - press_origin, - delta, - velocity, - pos_history: self.pos_history, - } + self } + fn wants_repaint(&self) -> bool { + !self.pointer_events.is_empty() || self.delta != Vec2::zero() + } + + /// How much the pointer moved compared to last frame, in points. + pub fn delta(&self) -> Vec2 { + self.delta + } + + /// Current velocity of pointer. + pub fn velocity(&self) -> Vec2 { + self.velocity + } + + /// Where did the current click/drag originate? + /// `None` if no mouse button is down. + pub fn press_origin(&self) -> Option { + self.press_origin + } + + /// Latest reported pointer position. + /// When tapping a touch screen, this will be `None`. + pub(crate) fn latest_pos(&self) -> Option { + self.latest_pos + } + + /// If it is a good idea to show a tooltip, where is pointer? + pub fn tooltip_pos(&self) -> Option { + self.latest_pos + } + + /// If you detect a click or drag and wants to know where it happened, use this. + /// + /// Latest position of the mouse, but ignoring any [`Event::PointerGone`] + /// if there were interactions this frame. + /// When tapping a touch screen, this will be the location of the touch. + pub fn interact_pos(&self) -> Option { + self.interact_pos + } + + /// Do we have a pointer? + /// + /// `false` if the mouse is not over the egui area, or if no touches are down on touch screens. + pub fn has_pointer(&self) -> bool { + self.latest_pos.is_some() + } + + /// Is the pointer currently moving? + /// This is smoothed so a few frames of stillness is required before this returns `true`. pub fn is_moving(&self) -> bool { self.velocity != Vec2::zero() } + + /// Was any pointer button pressed (`!down -> down`) this frame? + /// This can sometimes return `true` even if `any_down() == false` + /// because a press can be shorted than one frame. + pub fn any_pressed(&self) -> bool { + self.pointer_events.iter().any(|event| event.is_press()) + } + + /// Was any pointer button released (`down -> !down`) this frame? + pub fn any_released(&self) -> bool { + self.pointer_events.iter().any(|event| event.is_release()) + } + + /// Is any pointer button currently down? + pub fn any_down(&self) -> bool { + self.down.iter().any(|&down| down) + } + + /// Were there any type of click this frame? + pub fn any_click(&self) -> bool { + self.pointer_events.iter().any(|event| event.is_click()) + } + + // /// Was this button pressed (`!down -> down`) this frame? + // /// This can sometimes return `true` even if `any_down() == false` + // /// because a press can be shorted than one frame. + // pub fn button_pressed(&self, button: PointerButton) -> bool { + // self.pointer_events.iter().any(|event| event.is_press()) + // } + + // /// Was this button released (`down -> !down`) this frame? + // pub fn button_released(&self, button: PointerButton) -> bool { + // self.pointer_events.iter().any(|event| event.is_release()) + // } + + /// Is this button currently down? + pub fn button_down(&self, button: PointerButton) -> bool { + self.down[button as usize] + } } impl InputState { pub fn ui(&self, ui: &mut crate::Ui) { let Self { raw, - mouse, + pointer, scroll_delta, screen_rect, pixels_per_point, @@ -329,10 +491,10 @@ impl InputState { ui.style_mut().body_text_style = crate::paint::TextStyle::Monospace; ui.collapsing("Raw Input", |ui| raw.ui(ui)); - crate::containers::CollapsingHeader::new("🖱 Mouse") + crate::containers::CollapsingHeader::new("🖱 Pointer") .default_open(true) .show(ui, |ui| { - mouse.ui(ui); + pointer.ui(ui); }); ui.label(format!("scroll_delta: {:?} points", scroll_delta)); @@ -354,36 +516,32 @@ impl InputState { } } -impl CursorState { +impl PointerState { pub fn ui(&self, ui: &mut crate::Ui) { let Self { - down, - pressed, - released, - could_be_click, - click, - double_click, - last_click_time, - pos, - press_origin, + latest_pos, + interact_pos, delta, velocity, pos_history: _, + down, + press_origin, + could_be_click, + last_click_time, + pointer_events, } = self; - ui.label(format!("down: {}", down)); - ui.label(format!("pressed: {}", pressed)); - ui.label(format!("released: {}", released)); - ui.label(format!("could_be_click: {}", could_be_click)); - ui.label(format!("click: {}", click)); - ui.label(format!("double_click: {}", double_click)); - ui.label(format!("last_click_time: {:.3}", last_click_time)); - ui.label(format!("pos: {:?}", pos)); - ui.label(format!("press_origin: {:?}", press_origin)); + ui.label(format!("latest_pos: {:?}", latest_pos)); + ui.label(format!("interact_pos: {:?}", interact_pos)); ui.label(format!("delta: {:?}", delta)); ui.label(format!( "velocity: [{:3.0} {:3.0}] points/sec", velocity.x, velocity.y )); + ui.label(format!("down: {:#?}", down)); + ui.label(format!("press_origin: {:?}", press_origin)); + ui.label(format!("could_be_click: {:#?}", could_be_click)); + ui.label(format!("last_click_time: {:#?}", last_click_time)); + ui.label(format!("pointer_events: {:?}", pointer_events)); } } diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index b2698945..dd2fba32 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -29,12 +29,10 @@ impl Widget for &epaint::Texture { let (tex_w, tex_h) = (self.width as f32, self.height as f32); + let pointer_pos = response.interact_pointer_pos(); + response.on_hover_ui(|ui| { - let pos = ui - .input() - .mouse - .pos - .unwrap_or_else(|| ui.min_rect().left_top()); + let pos = pointer_pos.unwrap_or_else(|| ui.min_rect().left_top()); let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w); let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h); diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 82be4289..f030b669 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -125,7 +125,8 @@ pub(crate) struct Interaction { } impl Interaction { - pub fn is_using_mouse(&self) -> bool { + /// Are we currently clicking or dragging an egui widget? + pub fn is_using_pointer(&self) -> bool { self.click_id.is_some() || self.drag_id.is_some() } @@ -138,12 +139,8 @@ impl Interaction { self.click_interest = false; self.drag_interest = false; - if !prev_input.mouse.could_be_click { - self.click_id = None; - } - - if !prev_input.mouse.down || prev_input.mouse.pos.is_none() { - // mouse was not down last frame + if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() { + // pointer button was not down last frame self.click_id = None; self.drag_id = None; } @@ -175,7 +172,7 @@ impl Memory { ) { self.interaction.begin_frame(prev_input, new_input); - if !prev_input.mouse.down { + if !prev_input.pointer.any_down() { self.window_interaction = None; } } diff --git a/egui/src/menu.rs b/egui/src/menu.rs index 5e1a557b..79eebf17 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -7,7 +7,7 @@ //! //! menu::bar(ui, |ui| { //! menu::menu(ui, "File", |ui| { -//! if ui.button("Open").clicked { +//! if ui.button("Open").clicked() { //! // ... //! } //! }); @@ -83,14 +83,14 @@ fn menu_impl<'c>( } let button_response = ui.add(button); - if button_response.clicked { + if button_response.clicked() { // Toggle if bar_state.open_menu == Some(menu_id) { bar_state.open_menu = None; } else { bar_state.open_menu = Some(menu_id); } - } else if button_response.hovered && bar_state.open_menu.is_some() { + } else if button_response.hovered() && bar_state.open_menu.is_some() { bar_state.open_menu = Some(menu_id); } @@ -116,7 +116,8 @@ fn menu_impl<'c>( }); // TODO: this prevents sub-menus in menus. We should fix that. - if ui.input().key_pressed(Key::Escape) || ui.input().mouse.click && !button_response.clicked + if ui.input().key_pressed(Key::Escape) + || ui.input().pointer.any_click() && !button_response.clicked() { bar_state.open_menu = None; } diff --git a/egui/src/response.rs b/egui/src/response.rs index 0fab484b..61bed986 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -1,4 +1,7 @@ -use crate::math::{lerp, Align, Rect}; +use crate::{ + math::{lerp, Align, Pos2, Rect}, + PointerButton, NUM_POINTER_BUTTONS, +}; use crate::{CtxRef, Id, LayerId, Sense, Ui}; // ---------------------------------------------------------------------------- @@ -27,35 +30,34 @@ pub struct Response { pub sense: Sense, // OUT: - /// The mouse is hovering above this. - pub hovered: bool, + /// The pointer is hovering above this widget or the widget was clicked/tapped this frame. + pub(crate) hovered: bool, - /// The mouse clicked this thing this frame. - pub clicked: bool, + /// The pointer clicked this thing this frame. + pub(crate) clicked: [bool; NUM_POINTER_BUTTONS], /// The thing was double-clicked. - pub double_clicked: bool, + pub(crate) double_clicked: [bool; NUM_POINTER_BUTTONS], - /// The mouse is interacting with this thing (e.g. dragging it). - pub active: bool, + /// The widgets is being dragged + pub(crate) dragged: bool, + + /// The widget was being dragged, but now it has been released. + pub(crate) drag_released: bool, + + /// Is the pointer button currently down on this widget? + /// This is true if the pointer is pressing down or dragging a widget + pub(crate) is_pointer_button_down_on: bool, + + /// Where the pointer (mouse/touch) were when when this widget was clicked or dragged. + /// `None` if the widget is not being interacted with. + pub(crate) interact_pointer_pos: Option, /// This widget has the keyboard focus (i.e. is receiving key pressed). - pub has_kb_focus: bool, + pub(crate) has_kb_focus: bool, - /// The widget had keyboard focus and lost it, - /// perhaps because the user pressed enter. - /// If you want to do an action when a user presses enter in a text field, - /// use this. - /// - /// ``` - /// # let mut ui = egui::Ui::__test(); - /// # let mut my_text = String::new(); - /// # fn do_request(_: &str) {} - /// if ui.text_edit_singleline(&mut my_text).lost_kb_focus { - /// do_request(&my_text); - /// } - /// ``` - pub lost_kb_focus: bool, + /// The widget had keyboard focus and lost it. + pub(crate) lost_kb_focus: bool, } impl std::fmt::Debug for Response { @@ -69,7 +71,10 @@ impl std::fmt::Debug for Response { hovered, clicked, double_clicked, - active, + dragged, + drag_released, + is_pointer_button_down_on, + interact_pointer_pos, has_kb_focus, lost_kb_focus, } = self; @@ -81,7 +86,10 @@ impl std::fmt::Debug for Response { .field("hovered", hovered) .field("clicked", clicked) .field("double_clicked", double_clicked) - .field("active", active) + .field("dragged", dragged) + .field("drag_released", drag_released) + .field("is_pointer_button_down_on", is_pointer_button_down_on) + .field("interact_pointer_pos", interact_pointer_pos) .field("has_kb_focus", has_kb_focus) .field("lost_kb_focus", lost_kb_focus) .finish() @@ -89,10 +97,89 @@ impl std::fmt::Debug for Response { } impl Response { + /// Returns true if this widget was clicked this frame by the primary button. + pub fn clicked(&self) -> bool { + self.clicked[PointerButton::Primary as usize] + } + + /// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button). + pub fn secondary_clicked(&self) -> bool { + self.clicked[PointerButton::Secondary as usize] + } + + /// Returns true if this widget was clicked this frame by the middle mouse button. + pub fn middle_clicked(&self) -> bool { + self.clicked[PointerButton::Middle as usize] + } + + /// Returns true if this widget was double-clicked this frame by the primary button. + pub fn double_clicked(&self) -> bool { + self.double_clicked[PointerButton::Primary as usize] + } + + /// The pointer is hovering above this widget or the widget was clicked/tapped this frame. + pub fn hovered(&self) -> bool { + self.hovered + } + + /// The widget had keyboard focus and lost it, + /// perhaps because the user pressed enter. + /// If you want to do an action when a user presses enter in a text field, + /// use this. + /// + /// ``` + /// # let mut ui = egui::Ui::__test(); + /// # let mut my_text = String::new(); + /// # fn do_request(_: &str) {} + /// if ui.text_edit_singleline(&mut my_text).lost_kb_focus() { + /// do_request(&my_text); + /// } + /// ``` + pub fn lost_kb_focus(&self) -> bool { + self.lost_kb_focus + } + + /// The widgets is being dragged. + /// + /// To find out which button(s), query [`PointerState::button_down`] + /// (`ui.input().pointer.button_down(…)`). + pub fn dragged(&self) -> bool { + self.dragged + } + + /// The widget was being dragged, but now it has been released. + pub fn drag_released(&self) -> bool { + self.drag_released + } + + /// Returns true if this widget was clicked this frame by the given button. + pub fn clicked_by(&self, button: PointerButton) -> bool { + self.clicked[button as usize] + } + + /// Returns true if this widget was double-clicked this frame by the given button. + pub fn double_clicked_by(&self, button: PointerButton) -> bool { + self.double_clicked[button as usize] + } + + /// Where the pointer (mouse/touch) were when when this widget was clicked or dragged. + /// `None` if the widget is not being interacted with. + pub fn interact_pointer_pos(&self) -> Option { + self.interact_pointer_pos + } + + /// Is the pointer button currently down on this widget? + /// This is true if the pointer is pressing down or dragging a widget + pub fn is_pointer_button_down_on(&self) -> bool { + self.is_pointer_button_down_on + } + /// Show this UI if the item was hovered (i.e. a tooltip). /// If you call this multiple times the tooltips will stack underneath the previous ones. pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - if self.hovered || self.ctx.memory().everything_is_visible() { + if (self.hovered() && self.ctx.input().pointer.tooltip_pos().is_some()) + || self.ctx.memory().everything_is_visible() + { crate::containers::show_tooltip(&self.ctx, add_contents); } self @@ -116,9 +203,9 @@ impl Response { /// ``` /// # let mut ui = egui::Ui::__test(); /// let response = ui.label("hello"); - /// assert!(!response.clicked); // labels don't sense clicks + /// assert!(!response.clicked()); // labels don't sense clicks /// let response = response.interact(egui::Sense::click()); - /// if response.clicked { /* … */ } + /// if response.clicked() { /* … */ } /// ``` pub fn interact(&self, sense: Sense) -> Self { self.ctx @@ -133,7 +220,7 @@ impl Response { /// egui::ScrollArea::auto_sized().show(ui, |ui| { /// for i in 0..1000 { /// let response = ui.button(format!("Button {}", i)); - /// if response.clicked { + /// if response.clicked() { /// response.scroll_to_me(Align::Center); /// } /// } @@ -161,9 +248,21 @@ impl Response { rect: self.rect.union(other.rect), sense: self.sense.union(other.sense), hovered: self.hovered || other.hovered, - clicked: self.clicked || other.clicked, - double_clicked: self.double_clicked || other.double_clicked, - active: self.active || other.active, + clicked: [ + self.clicked[0] || other.clicked[0], + self.clicked[1] || other.clicked[1], + self.clicked[2] || other.clicked[2], + ], + double_clicked: [ + self.double_clicked[0] || other.double_clicked[0], + self.double_clicked[1] || other.double_clicked[1], + self.double_clicked[2] || other.double_clicked[2], + ], + dragged: self.dragged || other.dragged, + drag_released: self.drag_released || other.drag_released, + is_pointer_button_down_on: self.is_pointer_button_down_on + || other.is_pointer_button_down_on, + interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), has_kb_focus: self.has_kb_focus || other.has_kb_focus, lost_kb_focus: self.lost_kb_focus || other.lost_kb_focus, } @@ -195,7 +294,7 @@ impl std::ops::BitOr for Response { /// let mut response = ui.add(widget_a); /// response |= ui.add(widget_b); /// response |= ui.add(widget_c); -/// if response.active { ui.label("You are interacting with one of the widgets"); } +/// if response.hovered() { ui.label("You hovered at least one of the widgets"); } /// ``` impl std::ops::BitOrAssign for Response { fn bitor_assign(&mut self, rhs: Self) { diff --git a/egui/src/style.rs b/egui/src/style.rs index e1841ebb..3f03a9f3 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -195,11 +195,11 @@ pub struct Widgets { impl Widgets { pub fn style(&self, response: &Response) -> &WidgetVisuals { - if response.active || response.has_kb_focus { + if response.is_pointer_button_down_on() || response.has_kb_focus { &self.active } else if response.sense == crate::Sense::hover() { &self.disabled - } else if response.hovered { + } else if response.hovered() { &self.hovered } else { &self.inactive diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 653cbfad..23961fee 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -363,15 +363,29 @@ impl Ui { ) } - pub fn rect_contains_mouse(&self, rect: Rect) -> bool { + /// 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, + /// if this `Ui` is behind some other window, this will always return `false`. + pub fn rect_contains_pointer(&self, rect: Rect) -> bool { self.ctx() - .rect_contains_mouse(self.layer_id(), self.clip_rect().intersect(rect)) + .rect_contains_pointer(self.layer_id(), self.clip_rect().intersect(rect)) } - /// Is the mouse above this `Ui`? - /// Equivalent to `ui.rect_contains_mouse(ui.min_rect())` + /// Is the pointer (mouse/touch) above this `Ui`? + /// Equivalent to `ui.rect_contains_pointer(ui.min_rect())` + pub fn ui_contains_pointer(&self) -> bool { + self.rect_contains_pointer(self.min_rect()) + } + + #[deprecated = "renamed rect_contains_pointer"] + pub fn rect_contains_mouse(&self, rect: Rect) -> bool { + self.rect_contains_pointer(rect) + } + + #[deprecated = "renamed ui_contains_pointer"] pub fn ui_contains_mouse(&self) -> bool { - self.rect_contains_mouse(self.min_rect()) + self.ui_contains_pointer() } #[deprecated = "Use: interact(rect, id, Sense::hover())"] @@ -379,7 +393,7 @@ impl Ui { self.interact(rect, self.auto_id_with("hover_rect"), Sense::hover()) } - #[deprecated = "Use: rect_contains_mouse()"] + #[deprecated = "Use: rect_contains_pointer()"] pub fn hovered(&self, rect: Rect) -> bool { self.interact(rect, self.id, Sense::hover()).hovered } @@ -410,7 +424,7 @@ impl Ui { /// ``` /// # let mut ui = egui::Ui::__test(); /// let response = ui.allocate_response(egui::vec2(100.0, 200.0), egui::Sense::click()); - /// if response.clicked { /* … */ } + /// if response.clicked() { /* … */ } /// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE)); /// ``` pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response { @@ -572,7 +586,7 @@ impl Ui { /// # use egui::Align; /// # let mut ui = &mut egui::Ui::__test(); /// egui::ScrollArea::auto_sized().show(ui, |ui| { - /// let scroll_bottom = ui.button("Scroll to bottom.").clicked; + /// let scroll_bottom = ui.button("Scroll to bottom.").clicked(); /// for i in 0..1000 { /// ui.label(format!("Item {}", i)); /// } @@ -654,20 +668,20 @@ impl Ui { self.add(TextEdit::multiline(text)) } - /// Usage: `if ui.button("Click me").clicked { ... }` + /// Usage: `if ui.button("Click me").clicked() { ... }` /// /// Shortcut for `add(Button::new(text))` - #[must_use = "You should check if the user clicked this with `if ui.button(...).clicked { ... } "] + #[must_use = "You should check if the user clicked this with `if ui.button(...).clicked() { ... } "] pub fn button(&mut self, text: impl Into) -> Response { self.add(Button::new(text)) } /// A button as small as normal body text. /// - /// Usage: `if ui.small_button("Click me").clicked { ... }` + /// Usage: `if ui.small_button("Click me").clicked() { ... }` /// /// Shortcut for `add(Button::new(text).small())` - #[must_use = "You should check if the user clicked this with `if ui.small_button(...).clicked { ... } "] + #[must_use = "You should check if the user clicked this with `if ui.small_button(...).clicked() { ... } "] pub fn small_button(&mut self, text: impl Into) -> Response { self.add(Button::new(text).small()) } @@ -694,7 +708,7 @@ impl Ui { text: impl Into, ) -> Response { let response = self.radio(*current_value == selected_value, text); - if response.clicked { + if response.clicked() { *current_value = selected_value; } response @@ -716,7 +730,7 @@ impl Ui { text: impl Into, ) -> Response { let response = self.selectable_label(*current_value == selected_value, text); - if response.clicked { + if response.clicked() { *current_value = selected_value; } response diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index ca2dae72..abe6a6be 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -191,7 +191,7 @@ impl<'a> Widget for Checkbox<'a> { desired_size = desired_size.at_least(spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); - if response.clicked { + if response.clicked() { *checked = !*checked; } diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 4917f2ae..cb408e1c 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -94,10 +94,8 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color ); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); - if response.active { - if let Some(mpos) = ui.input().mouse.pos { - *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); - } + if let Some(mpos) = response.interact_pointer_pos() { + *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); } let visuals = ui.style().interact(&response); @@ -151,11 +149,9 @@ fn color_slider_2d( let desired_size = Vec2::splat(ui.style().spacing.slider_width); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); - if response.active { - if let Some(mpos) = ui.input().mouse.pos { - *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); - *y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0); - } + if let Some(mpos) = response.interact_pointer_pos() { + *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); + *y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0); } let visuals = ui.style().interact(&response); @@ -217,7 +213,7 @@ fn color_text_ui(ui: &mut Ui, color: impl Into) { r, g, b, a )); - if ui.button("📋").on_hover_text("Click to copy").clicked { + if ui.button("📋").on_hover_text("Click to copy").clicked() { ui.output().copied_text = format!("rgba({}, {}, {}, {})", r, g, b, a); } }); @@ -323,7 +319,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res let pupup_id = ui.auto_id_with("popup"); let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color"); - if button_response.clicked { + if button_response.clicked() { ui.memory().toggle_popup(pupup_id); } // TODO: make it easier to show a temporary popup that closes when you click outside it @@ -338,8 +334,8 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res }) }); - if !button_response.clicked { - let clicked_outside = ui.input().mouse.click && !area_response.hovered; + if !button_response.clicked() { + let clicked_outside = ui.input().pointer.any_pressed() && !area_response.hovered(); if clicked_outside || ui.input().key_pressed(Key::Escape) { ui.memory().close_popup(); } diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 0eb33730..18b11aa7 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -185,11 +185,11 @@ impl<'a> Widget for DragValue<'a> { value as f32, // Show full precision value on-hover. TODO: figure out f64 vs f32 suffix )); - if response.clicked { + if response.clicked() { ui.memory().request_kb_focus(kb_edit_id); ui.memory().temp_edit_string = None; // Filled in next frame - } else if response.active { - let mdelta = ui.input().mouse.delta; + } else if response.dragged() { + let mdelta = ui.input().pointer.delta(); let delta_points = mdelta.x - mdelta.y; // Increase to the right and up let delta_value = speed * delta_points; if delta_value != 0.0 { diff --git a/egui/src/widgets/hyperlink.rs b/egui/src/widgets/hyperlink.rs index bf04fbc6..88f517d2 100644 --- a/egui/src/widgets/hyperlink.rs +++ b/egui/src/widgets/hyperlink.rs @@ -48,17 +48,17 @@ impl Widget for Hyperlink { let galley = font.layout_multiline(text, ui.available_width()); let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click()); - if response.hovered { + if response.hovered() { ui.ctx().output().cursor_icon = CursorIcon::PointingHand; } - if response.clicked { + if response.clicked() { ui.ctx().output().open_url = Some(url.clone()); } let color = ui.style().visuals.hyperlink_color; let visuals = ui.style().interact(&response); - if response.hovered { + if response.hovered() { // Underline: for line in &galley.rows { let pos = rect.min; diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index ef9c8484..752aa393 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -2,7 +2,7 @@ //! //! Example widget uses: //! * `ui.add(Label::new("Text").text_color(color::red));` -//! * `if ui.add(Button::new("Click me")).clicked { ... }` +//! * `if ui.add(Button::new("Click me")).clicked() { ... }` #![allow(clippy::new_without_default)] @@ -40,7 +40,10 @@ pub trait Widget { /// The button is only enabled if the value does not already have its original value. pub fn reset_button(ui: &mut Ui, value: &mut T) { let def = T::default(); - if ui.add(Button::new("Reset").enabled(*value != def)).clicked { + if ui + .add(Button::new("Reset").enabled(*value != def)) + .clicked() + { *value = def; } } diff --git a/egui/src/widgets/selected_label.rs b/egui/src/widgets/selected_label.rs index fe595b3c..f2c7f081 100644 --- a/egui/src/widgets/selected_label.rs +++ b/egui/src/widgets/selected_label.rs @@ -42,7 +42,7 @@ impl Widget for SelectableLabel { let visuals = ui.style().interact(&response); - if selected || response.hovered { + if selected || response.hovered() { let rect = rect.expand(visuals.expansion); let fill = if selected { ui.style().visuals.selection.bg_fill diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 300f3972..32b02d05 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -252,19 +252,17 @@ impl<'a> Slider<'a> { let rect = &response.rect; let x_range = x_range(rect); - if let Some(mouse_pos) = ui.input().mouse.pos { - if response.active { - let new_value = if self.smart_aim { - let aim_radius = ui.input().aim_radius(); - crate::math::smart_aim::best_in_range_f64( - self.value_from_x(mouse_pos.x - aim_radius, x_range.clone()), - self.value_from_x(mouse_pos.x + aim_radius, x_range.clone()), - ) - } else { - self.value_from_x(mouse_pos.x, x_range.clone()) - }; - self.set_value(new_value); - } + if let Some(pointer_pos) = response.interact_pointer_pos() { + let new_value = if self.smart_aim { + let aim_radius = ui.input().aim_radius(); + crate::math::smart_aim::best_in_range_f64( + self.value_from_x(pointer_pos.x - aim_radius, x_range.clone()), + self.value_from_x(pointer_pos.x + aim_radius, x_range.clone()), + ) + } else { + self.value_from_x(pointer_pos.x, x_range.clone()) + }; + self.set_value(new_value); } // Paint it: @@ -350,7 +348,7 @@ impl<'a> Slider<'a> { self.get_value() as f32 // Show full precision value on-hover. TODO: figure out f64 vs f32 )); // let response = ui.interact(response.rect, kb_edit_id, Sense::click()); - if response.clicked { + if response.clicked() { ui.memory().request_kb_focus(kb_edit_id); ui.memory().temp_edit_string = None; // Filled in next frame } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 83629452..5b548259 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -115,7 +115,7 @@ impl CCursorPair { /// # let mut ui = egui::Ui::__test(); /// # let mut my_string = String::new(); /// let response = ui.add(egui::TextEdit::singleline(&mut my_string)); -/// if response.lost_kb_focus { +/// if response.lost_kb_focus() { /// // use my_string /// } /// ``` @@ -318,45 +318,45 @@ impl<'t> TextEdit<'t> { } if enabled { - if let Some(mouse_pos) = ui.input().mouse.pos { + if let Some(pointer_pos) = ui.input().pointer.interact_pos() { // TODO: triple-click to select whole paragraph // TODO: drag selected text to either move or clone (ctrl on windows, alt on mac) - let cursor_at_mouse = galley.cursor_from_pos(mouse_pos - response.rect.min); + let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - response.rect.min); - if response.hovered && ui.input().mouse.is_moving() { + if response.hovered() && ui.input().pointer.is_moving() { // preview: - paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse); + paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_pointer); } - if response.hovered && response.double_clicked { + if response.hovered() && response.double_clicked() { // Select word: - let center = cursor_at_mouse; + let center = cursor_at_pointer; let ccursorp = select_word_at(text, center.ccursor); state.cursorp = Some(CursorPair { primary: galley.from_ccursor(ccursorp.primary), secondary: galley.from_ccursor(ccursorp.secondary), }); - } else if response.hovered && ui.input().mouse.pressed { + } else if response.hovered() && ui.input().pointer.any_pressed() { ui.memory().request_kb_focus(id); if ui.input().modifiers.shift { if let Some(cursorp) = &mut state.cursorp { - cursorp.primary = cursor_at_mouse; + cursorp.primary = cursor_at_pointer; } else { - state.cursorp = Some(CursorPair::one(cursor_at_mouse)); + state.cursorp = Some(CursorPair::one(cursor_at_pointer)); } } else { - state.cursorp = Some(CursorPair::one(cursor_at_mouse)); + state.cursorp = Some(CursorPair::one(cursor_at_pointer)); } - } else if ui.input().mouse.down && response.active { + } else if ui.input().pointer.any_down() && response.is_pointer_button_down_on() { if let Some(cursorp) = &mut state.cursorp { - cursorp.primary = cursor_at_mouse; + cursorp.primary = cursor_at_pointer; } } } } - if ui.input().mouse.pressed && !response.hovered { + if ui.input().pointer.any_pressed() && !response.hovered() { // User clicked somewhere else ui.memory().surrender_kb_focus(id); } @@ -365,7 +365,7 @@ impl<'t> TextEdit<'t> { ui.memory().surrender_kb_focus(id); } - if response.hovered && enabled { + if response.hovered() && enabled { ui.output().cursor_icon = CursorIcon::Text; } @@ -471,7 +471,7 @@ impl<'t> TextEdit<'t> { modifiers, } => on_key_press(&mut cursorp, text, &galley, *key, modifiers), - Event::Key { .. } => None, + _ => None, }; if let Some(new_ccursorp) = did_mutate_text { diff --git a/egui_demo_lib/src/apps/demo/demo_window.rs b/egui_demo_lib/src/apps/demo/demo_window.rs index 6b131b91..0084231c 100644 --- a/egui_demo_lib/src/apps/demo/demo_window.rs +++ b/egui_demo_lib/src/apps/demo/demo_window.rs @@ -57,7 +57,7 @@ impl DemoWindow { ui.columns(self.num_columns, |cols| { for (i, col) in cols.iter_mut().enumerate() { col.label(format!("Column {} out of {}", i + 1, self.num_columns)); - if i + 1 == self.num_columns && col.button("Delete this").clicked { + if i + 1 == self.num_columns && col.button("Delete this").clicked() { self.num_columns -= 1; } } @@ -287,15 +287,15 @@ impl LayoutDemo { pub fn content_ui(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { - if ui.button("Top-down").clicked { + if ui.button("Top-down").clicked() { *self = Default::default(); } - if ui.button("Top-down, centered and justified").clicked { + if ui.button("Top-down, centered and justified").clicked() { *self = Default::default(); self.cross_align = Align::Center; self.cross_justify = true; } - if ui.button("Horizontal wrapped").clicked { + if ui.button("Horizontal wrapped").clicked() { *self = Default::default(); self.main_dir = Direction::LeftToRight; self.cross_align = Align::Center; @@ -391,7 +391,7 @@ impl Tree { if depth > 0 && ui .add(Button::new("delete").text_color(Color32::RED)) - .clicked + .clicked() { return Action::Delete; } @@ -409,7 +409,7 @@ impl Tree { }) .collect(); - if ui.button("+").clicked { + if ui.button("+").clicked() { self.0.push(Tree::default()); } diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index e615b6d0..fa48595a 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -14,6 +14,7 @@ impl Default for Demos { fn default() -> Self { let demos: Vec> = vec![ Box::new(super::WidgetGallery::default()), + Box::new(super::input_test::InputTest::default()), Box::new(super::FontBook::default()), Box::new(super::Painting::default()), Box::new(super::DancingStrings::default()), @@ -86,7 +87,7 @@ impl DemoWindows { ui.separator(); - if ui.button("Organize windows").clicked { + if ui.button("Organize windows").clicked() { ui.ctx().memory().reset_areas(); } }); @@ -255,13 +256,13 @@ fn show_menu_bar(ui: &mut Ui) { menu::bar(ui, |ui| { menu::menu(ui, "File", |ui| { - if ui.button("Organize windows").clicked { + if ui.button("Organize windows").clicked() { ui.ctx().memory().reset_areas(); } if ui .button("Clear egui memory") .on_hover_text("Forget scroll, collapsing headers etc") - .clicked + .clicked() { *ui.ctx().memory() = Default::default(); } diff --git a/egui_demo_lib/src/apps/demo/drag_and_drop.rs b/egui_demo_lib/src/apps/demo/drag_and_drop.rs index f9e3082c..8d152eb7 100644 --- a/egui_demo_lib/src/apps/demo/drag_and_drop.rs +++ b/egui_demo_lib/src/apps/demo/drag_and_drop.rs @@ -8,7 +8,7 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { // Check for drags: let response = ui.interact(response.rect, id, Sense::drag()); - if response.hovered { + if response.hovered() { ui.output().cursor_icon = CursorIcon::Grab; } } else { @@ -25,8 +25,8 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { // (anything with `Order::Tooltip` always gets an empty `Response`) // So this is fine! - if let Some(mouse_pos) = ui.input().mouse.pos { - let delta = mouse_pos - response.rect.center(); + if let Some(pointer_pos) = ui.input().pointer.interact_pos() { + let delta = pointer_pos - response.rect.center(); ui.ctx().translate_layer(layer_id, delta); } } @@ -49,7 +49,7 @@ pub fn drop_target( let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover()); - let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered { + let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() { ui.style().visuals.widgets.active } else if is_being_dragged && can_accept_what_is_being_dragged { ui.style().visuals.widgets.inactive @@ -126,8 +126,7 @@ impl super::View for DragAndDropDemo { ui.label(item); }); - let this_item_being_dragged = ui.memory().is_being_dragged(item_id); - if this_item_being_dragged { + if ui.memory().is_being_dragged(item_id) { source_col_row = Some((col_idx, row_idx)); } } @@ -135,7 +134,7 @@ impl super::View for DragAndDropDemo { .1; let is_being_dragged = ui.memory().is_anything_being_dragged(); - if is_being_dragged && can_accept_what_is_being_dragged && response.hovered { + if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() { drop_col = Some(col_idx); } } @@ -143,7 +142,7 @@ impl super::View for DragAndDropDemo { if let Some((source_col, source_row)) = source_col_row { if let Some(drop_col) = drop_col { - if ui.input().mouse.released { + if ui.input().pointer.any_released() { // do the drop: let item = self.columns[source_col].remove(source_row); self.columns[drop_col].push(item); diff --git a/egui_demo_lib/src/apps/demo/font_book.rs b/egui_demo_lib/src/apps/demo/font_book.rs index a5d11d68..d2bb3a64 100644 --- a/egui_demo_lib/src/apps/demo/font_book.rs +++ b/egui_demo_lib/src/apps/demo/font_book.rs @@ -31,7 +31,7 @@ impl FontBook { ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32)); }; - if ui.add(button).on_hover_ui(tooltip_ui).clicked { + if ui.add(button).on_hover_ui(tooltip_ui).clicked() { ui.output().copied_text = chr.to_string(); } } @@ -81,7 +81,7 @@ impl super::View for FontBook { ui.label("Filter:"); ui.text_edit_singleline(&mut self.filter); self.filter = self.filter.to_lowercase(); - if ui.button("x").clicked { + if ui.button("x").clicked() { self.filter.clear(); } }); diff --git a/egui_demo_lib/src/apps/demo/input_test.rs b/egui_demo_lib/src/apps/demo/input_test.rs new file mode 100644 index 00000000..effe508a --- /dev/null +++ b/egui_demo_lib/src/apps/demo/input_test.rs @@ -0,0 +1,52 @@ +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(Default)] +pub struct InputTest { + info: String, +} + +impl super::Demo for InputTest { + fn name(&self) -> &str { + "🖱 Input Test" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .show(ctx, |ui| { + use super::View; + self.ui(ui); + }); + } +} + +impl super::View for InputTest { + fn ui(&mut self, ui: &mut egui::Ui) { + let response = ui.add( + egui::Button::new("Click, double-click or drag me with any mouse button") + .sense(egui::Sense::click_and_drag()), + ); + + let mut new_info = String::new(); + for &button in &[ + egui::PointerButton::Primary, + egui::PointerButton::Secondary, + egui::PointerButton::Middle, + ] { + if response.clicked_by(button) { + new_info += &format!("Clicked by {:?}\n", button); + } + if response.double_clicked_by(button) { + new_info += &format!("Double-clicked by {:?}\n", button); + } + if response.dragged() && ui.input().pointer.button_down(button) { + new_info += &format!("Dragged by {:?}\n", button); + } + } + if !new_info.is_empty() { + self.info = new_info; + } + + ui.label(&self.info); + } +} diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 61e3e436..7de770ad 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -12,6 +12,7 @@ mod drag_and_drop; mod font_book; pub mod font_contents_emoji; pub mod font_contents_ubuntu; +pub mod input_test; mod painting; mod scrolls; mod sliders; diff --git a/egui_demo_lib/src/apps/demo/painting.rs b/egui_demo_lib/src/apps/demo/painting.rs index 1782e8cb..24b90d33 100644 --- a/egui_demo_lib/src/apps/demo/painting.rs +++ b/egui_demo_lib/src/apps/demo/painting.rs @@ -21,7 +21,7 @@ impl Painting { ui.horizontal(|ui| { egui::stroke_ui(ui, &mut self.stroke, "Stroke"); ui.separator(); - if ui.button("Clear Painting").clicked { + if ui.button("Clear Painting").clicked() { self.lines.clear(); } }); @@ -38,12 +38,10 @@ impl Painting { let current_line = self.lines.last_mut().unwrap(); - if response.active { - if let Some(mouse_pos) = ui.input().mouse.pos { - let canvas_pos = mouse_pos - rect.min; - if current_line.last() != Some(&canvas_pos) { - current_line.push(canvas_pos); - } + if let Some(pointer_pos) = response.interact_pointer_pos() { + let canvas_pos = pointer_pos - rect.min; + if current_line.last() != Some(&canvas_pos) { + current_line.push(canvas_pos); } } else if !current_line.is_empty() { self.lines.push(vec![]); diff --git a/egui_demo_lib/src/apps/demo/scrolls.rs b/egui_demo_lib/src/apps/demo/scrolls.rs index 49c8f61c..f99f476d 100644 --- a/egui_demo_lib/src/apps/demo/scrolls.rs +++ b/egui_demo_lib/src/apps/demo/scrolls.rs @@ -33,13 +33,13 @@ impl Scrolls { ui.add(Slider::usize(&mut self.track_item, 1..=50).text("Track Item")); }); let (scroll_offset, _) = ui.horizontal(|ui| { - let scroll_offset = ui.small_button("Scroll Offset").clicked; + let scroll_offset = ui.small_button("Scroll Offset").clicked(); ui.add(DragValue::f32(&mut self.offset).speed(1.0).suffix("px")); scroll_offset }); - let scroll_top = ui.button("Scroll to top").clicked; - let scroll_bottom = ui.button("Scroll to bottom").clicked; + let scroll_top = ui.button("Scroll to top").clicked(); + let scroll_bottom = ui.button("Scroll to bottom").clicked(); if scroll_bottom || scroll_top { self.tracking = false; } diff --git a/egui_demo_lib/src/apps/demo/sliders.rs b/egui_demo_lib/src/apps/demo/sliders.rs index 92192deb..9d4bab67 100644 --- a/egui_demo_lib/src/apps/demo/sliders.rs +++ b/egui_demo_lib/src/apps/demo/sliders.rs @@ -73,7 +73,7 @@ impl Sliders { You can always see the full precision value by hovering the value.", ); - if ui.button("Assign PI").clicked { + if ui.button("Assign PI").clicked() { self.value = std::f64::consts::PI; } } diff --git a/egui_demo_lib/src/apps/demo/toggle_switch.rs b/egui_demo_lib/src/apps/demo/toggle_switch.rs index 60087a7f..393cfbe3 100644 --- a/egui_demo_lib/src/apps/demo/toggle_switch.rs +++ b/egui_demo_lib/src/apps/demo/toggle_switch.rs @@ -27,7 +27,7 @@ pub fn toggle(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); // 3. Interact: Time to check for clicks!. - if response.clicked { + if response.clicked() { *on = !*on; } @@ -63,7 +63,7 @@ pub fn toggle(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { fn toggle_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let desired_size = ui.style().spacing.interact_size; let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); - *on ^= response.clicked; // toggle if clicked + *on ^= response.clicked(); // toggle if clicked let how_on = ui.ctx().animate_bool(response.id, *on); let visuals = ui.style().interact(&response); diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index c14dcb01..73400a84 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -115,7 +115,7 @@ impl super::View for WidgetGallery { ui.end_row(); ui.label("Button:"); - if ui.button("Toggle boolean").clicked { + if ui.button("Toggle boolean").clicked() { *boolean = !*boolean; } ui.end_row(); @@ -123,7 +123,7 @@ impl super::View for WidgetGallery { ui.label("ImageButton:"); if ui .add(egui::ImageButton::new(egui::TextureId::Egui, [24.0, 16.0])) - .clicked + .clicked() { *boolean = !*boolean; } diff --git a/egui_demo_lib/src/apps/demo/widgets.rs b/egui_demo_lib/src/apps/demo/widgets.rs index b5026e9b..663be8c0 100644 --- a/egui_demo_lib/src/apps/demo/widgets.rs +++ b/egui_demo_lib/src/apps/demo/widgets.rs @@ -97,7 +97,7 @@ impl Widgets { if ui .add(Button::new("Click me").enabled(self.button_enabled)) .on_hover_text("This will just increase a counter.") - .clicked + .clicked() { self.count += 1; } @@ -146,7 +146,7 @@ impl Widgets { ui.horizontal(|ui| { ui.label("Single line text input:"); let response = ui.text_edit_singleline(&mut self.single_line_text_input); - if response.lost_kb_focus { + if response.lost_kb_focus() { // The user pressed enter. } }); diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index 1f3365ba..6dee9f84 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -113,8 +113,8 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut epi::Frame<'_>, url: &mut String) -> Op ui.horizontal(|ui| { ui.label("URL:"); - trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus; - trigger_fetch |= ui.button("GET").clicked; + trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus(); + trigger_fetch |= ui.button("GET").clicked(); }); if frame.is_web() { @@ -122,14 +122,14 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut epi::Frame<'_>, url: &mut String) -> Op } ui.horizontal(|ui| { - if ui.button("Source code for this example").clicked { + if ui.button("Source code for this example").clicked() { *url = format!( "https://raw.githubusercontent.com/emilk/egui/master/{}", file!() ); trigger_fetch = true; } - if ui.button("Random image").clicked { + if ui.button("Random image").clicked() { let seed = ui.input().time; let width = 640; let height = 480; @@ -170,7 +170,7 @@ fn ui_resouce( if let Some(text) = &response.text { let tooltip = "Click to copy the response body"; - if ui.button("📋").on_hover_text(tooltip).clicked { + if ui.button("📋").on_hover_text(tooltip).clicked() { ui.output().copied_text = text.clone(); } } diff --git a/egui_demo_lib/src/frame_history.rs b/egui_demo_lib/src/frame_history.rs index e4454a17..d9966e15 100644 --- a/egui_demo_lib/src/frame_history.rs +++ b/egui_demo_lib/src/frame_history.rs @@ -79,9 +79,9 @@ impl FrameHistory { let rect = rect.shrink(4.0); let line_stroke = Stroke::new(1.0, Color32::from_additive_luminance(128)); - if let Some(mouse_pos) = ui.input().mouse.pos { - if rect.contains(mouse_pos) { - let y = mouse_pos.y; + if let Some(pointer_pos) = ui.input().pointer.tooltip_pos() { + if rect.contains(pointer_pos) { + let y = pointer_pos.y; shapes.push(Shape::line_segment( [pos2(rect.left(), y), pos2(rect.right(), y)], line_stroke, diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 4450a335..01e006bd 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -73,7 +73,7 @@ impl epi::App for WrapApp { for (anchor, app) in self.apps.iter_mut() { if ui .selectable_label(self.selected_anchor == anchor, app.name()) - .clicked + .clicked() { self.selected_anchor = anchor.to_owned(); if frame.is_web() { @@ -86,7 +86,7 @@ impl epi::App for WrapApp { if false { // TODO: fix the overlap on small screens if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { - if clock_button(ui, seconds_since_midnight).clicked { + if clock_button(ui, seconds_since_midnight).clicked() { self.selected_anchor = "clock".to_owned(); if frame.is_web() { ui.output().open_url = Some("#clock".to_owned()); @@ -234,11 +234,11 @@ impl BackendPanel { if ui .button("📱 Phone Size") .on_hover_text("Resize the window to be small like a phone.") - .clicked + .clicked() { frame.set_window_size(egui::Vec2::new(375.0, 812.0)); // iPhone 12 mini } - if ui.button("Quit").clicked { + if ui.button("Quit").clicked() { frame.quit(); } } @@ -275,7 +275,7 @@ impl BackendPanel { "Reset scale to native value ({:.1})", native_pixels_per_point )) - .clicked + .clicked() { *pixels_per_point = native_pixels_per_point; } @@ -283,7 +283,7 @@ impl BackendPanel { }); // We wait until mouse release to activate: - if ui.ctx().is_using_mouse() { + if ui.ctx().is_using_pointer() { None } else { Some(*pixels_per_point) diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index c7ff6087..842daaaf 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -29,12 +29,14 @@ use { pub use clipboard::ClipboardContext; // TODO: remove pub struct GliumInputState { + pub pointer_pos_in_points: Option, pub raw: egui::RawInput, } impl GliumInputState { pub fn from_pixels_per_point(pixels_per_point: f32) -> Self { Self { + pointer_pos_in_points: Default::default(), raw: egui::RawInput { pixels_per_point: Some(pixels_per_point), ..Default::default() @@ -52,23 +54,38 @@ pub fn input_to_egui( use glutin::event::WindowEvent; match event { WindowEvent::CloseRequested | WindowEvent::Destroyed => *control_flow = ControlFlow::Exit, - WindowEvent::MouseInput { state, .. } => { - input_state.raw.mouse_down = state == glutin::event::ElementState::Pressed; + WindowEvent::MouseInput { state, button, .. } => { + if let Some(pos_in_points) = input_state.pointer_pos_in_points { + if let Some(button) = translate_mouse_button(button) { + input_state.raw.events.push(egui::Event::PointerButton { + pos: pos_in_points, + button, + pressed: state == glutin::event::ElementState::Pressed, + modifiers: input_state.raw.modifiers, + }); + } + } } WindowEvent::CursorMoved { position: pos_in_pixels, .. } => { - input_state.raw.mouse_pos = Some(pos2( + let pos_in_points = pos2( pos_in_pixels.x as f32 / input_state.raw.pixels_per_point.unwrap(), pos_in_pixels.y as f32 / input_state.raw.pixels_per_point.unwrap(), - )); + ); + input_state.pointer_pos_in_points = Some(pos_in_points); + input_state + .raw + .events + .push(egui::Event::PointerMoved(pos_in_points)); } WindowEvent::CursorLeft { .. } => { - input_state.raw.mouse_pos = None; + input_state.pointer_pos_in_points = None; + input_state.raw.events.push(egui::Event::PointerGone); } WindowEvent::ReceivedCharacter(ch) => { - if printable_char(ch) + if is_printable_char(ch) && !input_state.raw.modifiers.ctrl && !input_state.raw.modifiers.mac_cmd { @@ -79,6 +96,7 @@ pub fn input_to_egui( if let Some(keycode) = input.virtual_keycode { let pressed = input.state == glutin::event::ElementState::Pressed; + // We could also use `WindowEvent::ModifiersChanged` instead, I guess. if matches!(keycode, VirtualKeyCode::LAlt | VirtualKeyCode::RAlt) { input_state.raw.modifiers.alt = pressed; } @@ -157,7 +175,7 @@ pub fn input_to_egui( /// Ignore those. /// We also ignore '\r', '\n', '\t'. /// Newlines are handled by the `Key::Enter` event. -fn printable_char(chr: char) -> bool { +fn is_printable_char(chr: char) -> bool { let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}' || '\u{f0000}' <= chr && chr <= '\u{ffffd}' || '\u{100000}' <= chr && chr <= '\u{10fffd}'; @@ -165,6 +183,15 @@ fn printable_char(chr: char) -> bool { !is_in_private_use_area && !chr.is_ascii_control() } +pub fn translate_mouse_button(button: glutin::event::MouseButton) -> Option { + match button { + glutin::event::MouseButton::Left => Some(egui::PointerButton::Primary), + glutin::event::MouseButton::Right => Some(egui::PointerButton::Secondary), + glutin::event::MouseButton::Middle => Some(egui::PointerButton::Middle), + _ => None, + } +} + pub fn translate_virtual_key_code(key: VirtualKeyCode) -> Option { use VirtualKeyCode::*; diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index f9db526b..6a98966f 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added ⭐ + +* Right-clicks will no longer open browser context menu. + +### Fixed ⭐ + +* Fix a bug where one couldn't select items in a combo box on a touch screen. + ## 0.8.0 - 2021-01-17 diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 90e9842b..34d03242 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -81,6 +81,9 @@ pub struct WebInput { /// Is this a touch screen? If so, we ignore mouse events. pub is_touch: bool, + /// Required because we don't get a position on touched + pub latest_touch_pos: Option, + pub raw: egui::RawInput, } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 95cc05d2..9d48cbe3 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -97,6 +97,15 @@ pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egu } } +pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option { + match event.button() { + 0 => Some(egui::PointerButton::Primary), + 1 => Some(egui::PointerButton::Middle), + 2 => Some(egui::PointerButton::Secondary), + _ => None, + } +} + pub fn pos_from_touch_event(event: &web_sys::TouchEvent) -> egui::Pos2 { let t = event.touches().get(0).unwrap(); egui::Pos2 { @@ -599,19 +608,40 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { use wasm_bindgen::JsCast; let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap(); + { + // By default, right-clicks open a context menu. + // We don't want to do that (right clicks is handled by egui): + let event_name = "contextmenu"; + let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + { let event_name = "mousedown"; let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { let mut runner_lock = runner_ref.0.lock(); if !runner_lock.input.is_touch { - runner_lock.input.raw.mouse_pos = - Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); - runner_lock.input.raw.mouse_down = true; - runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead - runner_lock.needs_repaint.set_true(); - event.stop_propagation(); - event.prevent_default(); + if let Some(button) = button_from_mouse_event(&event) { + let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); + let modifiers = runner_lock.input.raw.modifiers; + runner_lock + .input + .raw + .events + .push(egui::Event::PointerButton { + pos, + button, + pressed: true, + modifiers, + }); + runner_lock.needs_repaint.set_true(); + event.stop_propagation(); + event.prevent_default(); + } } }) as Box); canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; @@ -624,8 +654,12 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { let mut runner_lock = runner_ref.0.lock(); if !runner_lock.input.is_touch { - runner_lock.input.raw.mouse_pos = - Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); + let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); + runner_lock + .input + .raw + .events + .push(egui::Event::PointerMoved(pos)); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -641,12 +675,23 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { let mut runner_lock = runner_ref.0.lock(); if !runner_lock.input.is_touch { - runner_lock.input.raw.mouse_pos = - Some(pos_from_mouse_event(runner_lock.canvas_id(), &event)); - runner_lock.input.raw.mouse_down = false; - runner_lock.needs_repaint.set_true(); - event.stop_propagation(); - event.prevent_default(); + if let Some(button) = button_from_mouse_event(&event) { + let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event); + let modifiers = runner_lock.input.raw.modifiers; + runner_lock + .input + .raw + .events + .push(egui::Event::PointerButton { + pos, + button, + pressed: false, + modifiers, + }); + runner_lock.needs_repaint.set_true(); + event.stop_propagation(); + event.prevent_default(); + } } }) as Box); canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; @@ -659,7 +704,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { let mut runner_lock = runner_ref.0.lock(); if !runner_lock.input.is_touch { - runner_lock.input.raw.mouse_pos = None; + runner_lock.input.raw.events.push(egui::Event::PointerGone); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -673,10 +718,21 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let event_name = "touchstart"; let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let pos = pos_from_touch_event(&event); let mut runner_lock = runner_ref.0.lock(); + runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; - runner_lock.input.raw.mouse_pos = Some(pos_from_touch_event(&event)); - runner_lock.input.raw.mouse_down = true; + let modifiers = runner_lock.input.raw.modifiers; + runner_lock + .input + .raw + .events + .push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: true, + modifiers, + }); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -689,9 +745,15 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let event_name = "touchmove"; let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let pos = pos_from_touch_event(&event); let mut runner_lock = runner_ref.0.lock(); + runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; - runner_lock.input.raw.mouse_pos = Some(pos_from_touch_event(&event)); + runner_lock + .input + .raw + .events + .push(egui::Event::PointerMoved(pos)); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -706,12 +768,25 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { let mut runner_lock = runner_ref.0.lock(); runner_lock.input.is_touch = true; - runner_lock.input.raw.mouse_down = false; // First release mouse to click... - runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead) - runner_lock.input.raw.mouse_pos = None; // ...remove hover effect - runner_lock.needs_repaint.set_true(); - event.stop_propagation(); - event.prevent_default(); + if let Some(pos) = runner_lock.input.latest_touch_pos { + let modifiers = runner_lock.input.raw.modifiers; + // First release mouse to click: + runner_lock + .input + .raw + .events + .push(egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: false, + modifiers, + }); + // Then remove hover effect: + runner_lock.input.raw.events.push(egui::Event::PointerGone); + runner_lock.needs_repaint.set_true(); + event.stop_propagation(); + event.prevent_default(); + } }) as Box); canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; closure.forget(); diff --git a/emath/src/vec2.rs b/emath/src/vec2.rs index 7fede2d7..9e2a42f0 100644 --- a/emath/src/vec2.rs +++ b/emath/src/vec2.rs @@ -21,6 +21,9 @@ pub const fn vec2(x: f32, y: f32) -> Vec2 { Vec2 { x, y } } +// ---------------------------------------------------------------------------- +// Compatibility and convenience conversions to and from [f32; 2]: + impl From<[f32; 2]> for Vec2 { fn from(v: [f32; 2]) -> Self { Self { x: v[0], y: v[1] } @@ -33,6 +36,47 @@ impl From<&[f32; 2]> for Vec2 { } } +impl From for [f32; 2] { + fn from(v: Vec2) -> Self { + [v.x, v.y] + } +} + +impl From<&Vec2> for [f32; 2] { + fn from(v: &Vec2) -> Self { + [v.x, v.y] + } +} + +// ---------------------------------------------------------------------------- +// Compatibility and convenience conversions to and from (f32, f32): + +impl From<(f32, f32)> for Vec2 { + fn from(v: (f32, f32)) -> Self { + Self { x: v.0, y: v.1 } + } +} + +impl From<&(f32, f32)> for Vec2 { + fn from(v: &(f32, f32)) -> Self { + Self { x: v.0, y: v.1 } + } +} + +impl From for (f32, f32) { + fn from(v: Vec2) -> Self { + (v.x, v.y) + } +} + +impl From<&Vec2> for (f32, f32) { + fn from(v: &Vec2) -> Self { + (v.x, v.y) + } +} + +// ---------------------------------------------------------------------------- + impl Vec2 { pub const X: Vec2 = Vec2 { x: 1.0, y: 0.0 }; pub const Y: Vec2 = Vec2 { x: 0.0, y: 1.0 };