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
This commit is contained in:
Emil Ernerfeldt 2021-01-25 18:50:19 +01:00 committed by GitHub
parent 18b9214575
commit 247026149c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1052 additions and 580 deletions

View file

@ -11,7 +11,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ⭐ ### 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 ## 0.8.0 - 2021-01-17 - Grid layout & new visual style

View file

@ -51,7 +51,7 @@ ui.horizontal(|ui| {
ui.text_edit_singleline(&mut name); ui.text_edit_singleline(&mut name);
}); });
ui.add(egui::Slider::u32(&mut age, 0..=120).text("age")); 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; age += 1;
} }
ui.label(format!("Hello '{}', age {}", name, age)); ui.label(format!("Hello '{}', age {}", name, age));

View file

@ -29,7 +29,7 @@ impl epi::App for MyApp {
ui.text_edit_singleline(name); ui.text_edit_singleline(name);
}); });
ui.add(egui::Slider::u32(age, 0..=120).text("age")); 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; *age += 1;
} }
ui.label(format!("Hello '{}', age {}", name, age)); ui.label(format!("Hello '{}', age {}", name, age));

View file

@ -249,14 +249,14 @@ impl Prepared {
sense, sense,
); );
if move_response.active && movable { if move_response.dragged() && movable {
state.pos += ctx.input().mouse.delta; state.pos += ctx.input().pointer.delta();
} }
state.pos = ctx.constrain_window_rect(state.rect()).min; state.pos = ctx.constrain_window_rect(state.rect()).min;
if (move_response.active || move_response.clicked) if (move_response.dragged() || move_response.clicked())
|| mouse_pressed_on_area(ctx, layer_id) || pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory().areas.visible_last_frame(&layer_id) || !ctx.memory().areas.visible_last_frame(&layer_id)
{ {
ctx.memory().areas.move_to_top(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 { fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(mouse_pos) = ctx.input().mouse.pos { if let Some(pointer_pos) = ctx.input().pointer.interact_pos() {
ctx.input().mouse.pressed && ctx.layer_id_at(mouse_pos) == Some(layer_id) ctx.input().pointer.any_pressed() && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else { } else {
false false
} }

View file

@ -199,7 +199,7 @@ impl CollapsingHeader {
); );
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); 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); state.toggle(ui);
} }

View file

@ -84,7 +84,7 @@ pub fn combo_box(
.galley(text_rect.min, galley, text_style, visuals.text_color()); .galley(text_rect.min, galley, text_style, visuals.text_color());
}); });
if button_response.clicked { if button_response.clicked() {
ui.memory().toggle_popup(popup_id); ui.memory().toggle_popup(popup_id);
} }
const MAX_COMBO_HEIGHT: f32 = 128.0; 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)); outer_rect.set_height(outer_rect.height().at_least(interact_size.y));
let mut response = ui.interact(outer_rect, id, sense); 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); let visuals = ui.style().interact(&response);
ui.painter().set( ui.painter().set(

View file

@ -2,7 +2,7 @@
use crate::*; 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`]. /// 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(); /// # let mut ui = egui::Ui::__test();
/// if ui.ui_contains_mouse() { /// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), |ui| { /// egui::show_tooltip(ui.ctx(), |ui| {
/// ui.label("Helpful text"); /// 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 { let window_pos = if let Some(tooltip_rect) = tooltip_rect {
tooltip_rect.left_bottom() 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 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.min(ctx.input().screen_rect().right_bottom() - expected_size);
let position = position.max(ctx.input().screen_rect().left_top()); let position = position.max(ctx.input().screen_rect().left_top());
position 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)); 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`]. /// 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(); /// # let mut ui = egui::Ui::__test();
/// if ui.ui_contains_mouse() { /// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), "Helpful text"); /// egui::show_tooltip_text(ui.ctx(), "Helpful text");
/// } /// }
/// ``` /// ```
@ -89,7 +89,7 @@ fn show_tooltip_area(
/// # let ui = &mut egui::Ui::__test(); /// # let ui = &mut egui::Ui::__test();
/// let response = ui.button("Open popup"); /// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id"); /// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked { /// if response.clicked() {
/// ui.memory().toggle_popup(popup_id); /// ui.memory().toggle_popup(popup_id);
/// } /// }
/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| { /// 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(); ui.memory().close_popup();
} }

View file

@ -191,12 +191,11 @@ impl Resize {
Rect::from_min_size(position + state.desired_size - corner_size, corner_size); Rect::from_min_size(position + state.desired_size - corner_size, corner_size);
let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag()); let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag());
if corner_response.active { if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
if let Some(mouse_pos) = ui.input().mouse.pos { user_requested_size =
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
Some(mouse_pos - position + 0.5 * corner_response.rect.size());
}
} }
Some(corner_response) Some(corner_response)
} else { } else {
None None
@ -295,7 +294,7 @@ impl Resize {
if let Some(corner_response) = corner_response { if let Some(corner_response) = corner_response {
paint_resize_corner(ui, &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; ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
} }
} }

View file

@ -212,9 +212,9 @@ impl Prepared {
let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag()); let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag());
let input = ui.input(); let input = ui.input();
if content_response.active { if content_response.dragged() {
state.offset.y -= input.mouse.delta.y; state.offset.y -= input.pointer.delta().y;
state.vel = input.mouse.velocity; state.vel = input.pointer.velocity();
} else { } else {
let stop_speed = 20.0; // Pixels per second. let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared. let friction_coeff = 1000.0; // Pixels per second squared.
@ -234,7 +234,7 @@ impl Prepared {
} }
let max_offset = content_size.y - inner_rect.height(); 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 mut frame_state = ui.ctx().frame_state();
let scroll_delta = frame_state.scroll_delta; let scroll_delta = frame_state.scroll_delta;
@ -283,26 +283,24 @@ impl Prepared {
let interact_id = id.with("vertical"); let interact_id = id.with("vertical");
let response = ui.interact(outer_scroll_rect, interact_id, Sense::click_and_drag()); let response = ui.interact(outer_scroll_rect, interact_id, Sense::click_and_drag());
if response.active { if let Some(pointer_pos) = response.interact_pointer_pos() {
if let Some(mouse_pos) = ui.input().mouse.pos { let scroll_start_offset_from_top =
let scroll_start_offset_from_top = state.scroll_start_offset_from_top.get_or_insert_with(|| {
state.scroll_start_offset_from_top.get_or_insert_with(|| { if handle_rect.contains(pointer_pos) {
if handle_rect.contains(mouse_pos) { pointer_pos.y - handle_rect.top()
mouse_pos.y - handle_rect.top() } else {
} else { let handle_top_pos_at_bottom = bottom - handle_rect.height();
let handle_top_pos_at_bottom = bottom - handle_rect.height(); // Calculate the new handle top position, centering the handle on the mouse.
// Calculate the new handle top position, centering the handle on the mouse. let new_handle_top_pos = clamp(
let new_handle_top_pos = clamp( pointer_pos.y - handle_rect.height() / 2.0,
mouse_pos.y - handle_rect.height() / 2.0, top..=handle_top_pos_at_bottom,
top..=handle_top_pos_at_bottom, );
); pointer_pos.y - new_handle_top_pos
mouse_pos.y - new_handle_top_pos }
} });
});
let new_handle_top = mouse_pos.y - *scroll_start_offset_from_top; 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); state.offset.y = remap(new_handle_top, top..=bottom, 0.0..=content_size.y);
}
} else { } else {
state.scroll_start_offset_from_top = None; state.scroll_start_offset_from_top = None;
} }

View file

@ -363,12 +363,14 @@ impl<'open> Window<'open> {
ctx.style().visuals.widgets.active, ctx.style().visuals.widgets.active,
); );
} else if let Some(hover_interaction) = hover_interaction { } else if let Some(hover_interaction) = hover_interaction {
paint_frame_interaction( if ctx.input().pointer.has_pointer() {
&mut area_content_ui, paint_frame_interaction(
outer_rect, &mut area_content_ui,
hover_interaction, outer_rect,
ctx.style().visuals.widgets.hovered, hover_interaction,
); ctx.style().visuals.widgets.hovered,
);
}
} }
} }
let full_response = area.end(ctx, area_content_ui); 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<Rect> { fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option<Rect> {
window_interaction.set_cursor(ctx); 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 let mut rect = window_interaction.start_rect; // prevent drift
if window_interaction.is_resize() { if window_interaction.is_resize() {
if window_interaction.left { 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 { } 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 { 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 { } 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 { } else {
// movement // movement
rect = rect.translate(mouse_pos - ctx.input().mouse.press_origin?); rect = rect.translate(pointer_pos - ctx.input().pointer.press_origin()?);
} }
Some(rect) Some(rect)
@ -491,7 +493,7 @@ fn window_interaction(
if window_interaction.is_none() { if window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx); 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_id = Some(id);
ctx.memory().interaction.drag_is_window = true; ctx.memory().interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction); window_interaction = Some(hover_window_interaction);
@ -517,13 +519,13 @@ fn resize_hover(
area_layer_id: LayerId, area_layer_id: LayerId,
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
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) 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 { if top_layer_id != area_layer_id && top_layer_id.order != Order::Background {
return None; // Another window is on top here 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 side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; 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; return None;
} }
let (mut left, mut right, mut top, mut bottom) = Default::default(); let (mut left, mut right, mut top, mut bottom) = Default::default();
if possible.resizable { if possible.resizable {
right = (rect.right() - mouse_pos.x).abs() <= side_grab_radius; right = (rect.right() - pointer_pos.x).abs() <= side_grab_radius;
bottom = (rect.bottom() - mouse_pos.y).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; right = true;
bottom = true; bottom = true;
} }
if possible.movable { if possible.movable {
left = (rect.left() - mouse_pos.x).abs() <= side_grab_radius; left = (rect.left() - pointer_pos.x).abs() <= side_grab_radius;
top = (rect.top() - mouse_pos.y).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; right = true;
top = 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; left = true;
top = 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; left = true;
bottom = true; bottom = true;
} }
@ -671,7 +673,7 @@ fn show_title_bar(
let (_id, rect) = ui.allocate_space(button_size); let (_id, rect) = ui.allocate_space(button_size);
let collapse_button_response = ui.interact(rect, collapsing_id, Sense::click()); let collapse_button_response = ui.interact(rect, collapsing_id, Sense::click());
if collapse_button_response.clicked { if collapse_button_response.clicked() {
collapsing.toggle(ui); collapsing.toggle(ui);
} }
let openness = collapsing.openness(ui.ctx(), collapsing_id); let openness = collapsing.openness(ui.ctx(), collapsing_id);
@ -721,7 +723,7 @@ impl TitleBar {
if let Some(open) = open { if let Some(open) = open {
// Add close button now that we know our full width: // 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; *open = false;
} }
} }
@ -752,7 +754,7 @@ impl TitleBar {
if ui if ui
.interact(self.rect, self.id, Sense::click()) .interact(self.rect, self.id, Sense::click())
.double_clicked .double_clicked()
&& collapsible && collapsible
{ {
collapsing.toggle(ui); collapsing.toggle(ui);

View file

@ -8,6 +8,7 @@ use std::sync::{
use crate::{ use crate::{
animation_manager::AnimationManager, animation_manager::AnimationManager,
data::output::Output, data::output::Output,
input_state::*,
layers::GraphicLayers, layers::GraphicLayers,
mutex::{Mutex, MutexGuard}, mutex::{Mutex, MutexGuard},
paint::{stats::*, text::Fonts, *}, paint::{stats::*, text::Fonts, *},
@ -190,8 +191,8 @@ impl CtxRef {
let show_error = |pos: Pos2, text: String| { let show_error = |pos: Pos2, text: String| {
let painter = self.debug_painter(); let painter = self.debug_painter();
let rect = painter.error(pos, text); let rect = painter.error(pos, text);
if let Some(mouse_pos) = self.input.mouse.pos { if let Some(pointer_pos) = self.input.pointer.tooltip_pos() {
if rect.contains(mouse_pos) { if rect.contains(pointer_pos) {
painter.error( painter.error(
rect.left_bottom() + vec2(2.0, 4.0), rect.left_bottom() + vec2(2.0, 4.0),
"ID clashes happens when things like Windows or CollpasingHeaders share names,\n\ "ID clashes happens when things like Windows or CollpasingHeaders share names,\n\
@ -227,7 +228,7 @@ impl CtxRef {
sense: Sense, sense: Sense,
) -> Response { ) -> Response {
let interact_rect = rect.expand2((0.5 * item_spacing).min(Vec2::splat(5.0))); // make it easier to click 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) self.interact_with_hovered(layer_id, id, rect, sense, hovered)
} }
@ -241,26 +242,28 @@ impl CtxRef {
hovered: bool, hovered: bool,
) -> Response { ) -> Response {
let has_kb_focus = self.memory().has_kb_focus(id); 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 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() { if sense == Sense::hover() || !layer_id.allow_interaction() {
// Not interested or allowed input: // Not interested or allowed input:
return Response { return response;
ctx: self.clone(),
layer_id,
id,
rect,
sense,
hovered,
clicked: false,
double_clicked: false,
active: false,
has_kb_focus,
lost_kb_focus,
};
} }
self.register_interaction_id(id, rect.min); self.register_interaction_id(id, rect.min);
@ -270,102 +273,62 @@ impl CtxRef {
memory.interaction.click_interest |= hovered && sense.click; memory.interaction.click_interest |= hovered && sense.click;
memory.interaction.drag_interest |= hovered && sense.drag; memory.interaction.drag_interest |= hovered && sense.drag;
let active = response.dragged = memory.interaction.drag_id == Some(id);
memory.interaction.click_id == Some(id) || 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 { for pointer_event in &self.input.pointer.pointer_events {
if hovered { match pointer_event {
let mut response = Response { PointerEvent::Moved(pos) => {
ctx: self.clone(), if response.is_pointer_button_down_on {
layer_id, response.interact_pointer_pos = Some(*pos);
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;
} }
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 if sense.drag
&& (memory.interaction.drag_id.is_none() || memory.interaction.drag_is_window) && (memory.interaction.drag_id.is_none()
{ || memory.interaction.drag_is_window)
// start of a drag {
memory.interaction.drag_id = Some(id); // potential start of a drag
memory.interaction.drag_is_window = false; response.interact_pointer_pos = Some(*pos);
memory.window_interaction = None; // HACK: stop moving windows (if any) memory.interaction.drag_id = Some(id);
response.active = true; 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 if hovered && response.is_pointer_button_down_on {
} else { if let Some(click) = click {
// miss let clicked = hovered && response.is_pointer_button_down_on;
Response { response.interact_pointer_pos = Some(click.pos);
ctx: self.clone(), response.clicked[click.button as usize] = clicked;
layer_id, response.double_clicked[click.button as usize] =
id, clicked && click.is_double();
rect, }
sense, }
hovered,
clicked: false,
double_clicked: false,
active: false,
has_kb_focus,
lost_kb_focus,
} }
} }
} 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 { pub fn debug_painter(&self) -> Painter {
@ -648,12 +611,12 @@ impl Context {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/// Is the mouse over any egui area? /// Is the pointer (mouse/touch) over any egui area?
pub fn is_mouse_over_area(&self) -> bool { pub fn is_pointer_over_area(&self) -> bool {
if let Some(mouse_pos) = self.input.mouse.pos { if let Some(pointer_pos) = self.input.pointer.interact_pos() {
if let Some(layer) = self.layer_id_at(mouse_pos) { if let Some(layer) = self.layer_id_at(pointer_pos) {
if layer.order == Order::Background { if layer.order == Order::Background {
!self.frame_state().unused_rect.contains(mouse_pos) !self.frame_state().unused_rect.contains(pointer_pos)
} else { } else {
true true
} }
@ -665,19 +628,29 @@ impl Context {
} }
} }
/// True if egui is currently interested in the mouse. /// True if egui is currently interested in the pointer (mouse or touch).
/// Could be the mouse is hovering over a [`Window`] or the user is dragging a widget. /// Could be the pointer is hovering over a [`Window`] or the user is dragging a widget.
/// If `false`, the mouse is outside of any egui area and so /// 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). /// 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. /// Returns `false` if a drag started outside of egui and then moved over an egui area.
pub fn wants_mouse_input(&self) -> bool { pub fn wants_pointer_input(&self) -> bool {
self.is_using_mouse() || (self.is_mouse_over_area() && !self.input().mouse.down) 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). /// Is egui currently using the pointer position (e.g. dragging a slider).
/// NOTE: this will return `false` if the mouse is just hovering over an egui area. /// 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 { 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`]). /// 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) self.memory().layer_id_at(pos, resize_grab_radius_side)
} }
pub(crate) fn rect_contains_mouse(&self, layer_id: LayerId, rect: Rect) -> bool { pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
if let Some(mouse_pos) = self.input.mouse.pos { if let Some(pointer_pos) = self.input.pointer.interact_pos() {
rect.contains(mouse_pos) && self.layer_id_at(mouse_pos) == Some(layer_id) rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id)
} else { } else {
false false
} }
@ -766,10 +739,12 @@ impl Context {
pub fn inspection_ui(&self, ui: &mut Ui) { pub fn inspection_ui(&self, ui: &mut Ui) {
use crate::containers::*; use crate::containers::*;
ui.label(format!("Is using mouse: {}", self.is_using_mouse())) ui.label(format!("Is using pointer: {}", self.is_using_pointer()))
.on_hover_text("Is egui currently using the mouse actively (e.g. dragging a slider)?"); .on_hover_text(
ui.label(format!("Wants mouse input: {}", self.wants_mouse_input())) "Is egui currently using the pointer actively (e.g. dragging a slider)?",
.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!("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!( ui.label(format!(
"Wants keyboard input: {}", "Wants keyboard input: {}",
self.wants_keyboard_input() self.wants_keyboard_input()
@ -792,7 +767,7 @@ impl Context {
if ui if ui
.button("Reset all") .button("Reset all")
.on_hover_text("Reset all egui state") .on_hover_text("Reset all egui state")
.clicked .clicked()
{ {
*self.memory() = Default::default(); *self.memory() = Default::default();
} }
@ -802,7 +777,7 @@ impl Context {
"{} areas (window positions)", "{} areas (window positions)",
self.memory().areas.count() self.memory().areas.count()
)); ));
if ui.button("Reset").clicked { if ui.button("Reset").clicked() {
self.memory().areas = Default::default(); self.memory().areas = Default::default();
} }
}); });
@ -835,28 +810,28 @@ impl Context {
"{} collapsing headers", "{} collapsing headers",
self.memory().collapsing_headers.len() self.memory().collapsing_headers.len()
)); ));
if ui.button("Reset").clicked { if ui.button("Reset").clicked() {
self.memory().collapsing_headers = Default::default(); self.memory().collapsing_headers = Default::default();
} }
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("{} menu bars", self.memory().menu_bar.len())); 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(); self.memory().menu_bar = Default::default();
} }
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("{} scroll areas", self.memory().scroll_areas.len())); 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(); self.memory().scroll_areas = Default::default();
} }
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(format!("{} resize areas", self.memory().resize.len())); ui.label(format!("{} resize areas", self.memory().resize.len()));
if ui.button("Reset").clicked { if ui.button("Reset").clicked() {
self.memory().resize = Default::default(); self.memory().resize = Default::default();
} }
}); });

View file

@ -9,13 +9,6 @@ use crate::math::*;
/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner. /// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RawInput { 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<Pos2>,
/// How many points (logical pixels) the user scrolled /// How many points (logical pixels) the user scrolled
pub scroll_delta: Vec2, pub scroll_delta: Vec2,
@ -56,8 +49,6 @@ impl Default for RawInput {
fn default() -> Self { fn default() -> Self {
#![allow(deprecated)] // for screen_size #![allow(deprecated)] // for screen_size
Self { Self {
mouse_down: false,
mouse_pos: None,
scroll_delta: Vec2::zero(), scroll_delta: Vec2::zero(),
screen_size: Default::default(), screen_size: Default::default(),
screen_rect: None, screen_rect: None,
@ -75,8 +66,6 @@ impl RawInput {
pub fn take(&mut self) -> RawInput { pub fn take(&mut self) -> RawInput {
#![allow(deprecated)] // for screen_size #![allow(deprecated)] // for screen_size
RawInput { RawInput {
mouse_down: self.mouse_down,
mouse_pos: self.mouse_pos,
scroll_delta: std::mem::take(&mut self.scroll_delta), scroll_delta: std::mem::take(&mut self.scroll_delta),
screen_size: self.screen_size, screen_size: self.screen_size,
screen_rect: self.screen_rect, screen_rect: self.screen_rect,
@ -107,8 +96,38 @@ pub enum Event {
pressed: bool, pressed: bool,
modifiers: Modifiers, 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. /// State of the modifier keys. These must be fed to egui.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Modifiers { pub struct Modifiers {
@ -208,8 +227,6 @@ impl RawInput {
pub fn ui(&self, ui: &mut crate::Ui) { pub fn ui(&self, ui: &mut crate::Ui) {
#![allow(deprecated)] // for screen_size #![allow(deprecated)] // for screen_size
let Self { let Self {
mouse_down,
mouse_pos,
scroll_delta, scroll_delta,
screen_size: _, screen_size: _,
screen_rect, screen_rect,
@ -220,10 +237,6 @@ impl RawInput {
events, events,
} = self; } = 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!("scroll_delta: {:?} points", scroll_delta));
ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!("pixels_per_point: {:?}", pixels_per_point)) ui.label(format!("pixels_per_point: {:?}", pixels_per_point))

View file

@ -5,20 +5,21 @@ use crate::data::input::*;
pub use crate::data::input::Key; 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 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 const MAX_CLICK_DELAY: f64 = 0.3; // TODO: move to settings
/// Input state that egui updates each frame. /// Input state that egui updates each frame.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct InputState { pub struct InputState {
/// The raw input we got this frame /// The raw input we got this frame from the backend.
pub raw: RawInput, 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, pub scroll_delta: Vec2,
/// Position and size of the egui area. /// Position and size of the egui area.
@ -52,7 +53,7 @@ impl Default for InputState {
fn default() -> Self { fn default() -> Self {
Self { Self {
raw: Default::default(), raw: Default::default(),
mouse: Default::default(), pointer: Default::default(),
scroll_delta: Default::default(), scroll_delta: Default::default(),
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
pixels_per_point: 1.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<Pos2>,
/// Where did the current click/drag originate?
pub press_origin: Option<Pos2>,
/// 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<Pos2>,
}
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 { impl InputState {
#[must_use] #[must_use]
pub fn begin_frame(self, new: RawInput) -> InputState { pub fn begin_frame(self, new: RawInput) -> InputState {
@ -147,7 +83,7 @@ impl InputState {
self.screen_rect 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; let mut keys_down = self.keys_down;
for event in &new.events { for event in &new.events {
if let Event::Key { key, pressed, .. } = event { if let Event::Key { key, pressed, .. } = event {
@ -159,7 +95,7 @@ impl InputState {
} }
} }
InputState { InputState {
mouse, pointer,
scroll_delta: new.scroll_delta, scroll_delta: new.scroll_delta,
screen_rect, screen_rect,
pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), 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 { pub fn wants_repaint(&self) -> bool {
self.mouse.pressed self.pointer.wants_repaint() || self.scroll_delta != Vec2::zero() || !self.events.is_empty()
|| self.mouse.released
|| self.mouse.delta != Vec2::zero()
|| self.scroll_delta != Vec2::zero()
|| !self.events.is_empty()
} }
/// Was the given key pressed this frame? /// 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<Click>),
}
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<Pos2>,
/// 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<Pos2>,
/// 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<Pos2>,
down: [bool; NUM_POINTER_BUTTONS],
/// Where did the current click/drag originate?
/// `None` if no mouse button is down.
press_origin: Option<Pos2>,
/// 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<Click>,
/// All button events that occurred this frame
pub(crate) pointer_events: Vec<PointerEvent>,
}
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] #[must_use]
pub fn begin_frame(mut self, time: f64, new: &RawInput) -> CursorState { pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> PointerState {
let delta = new self.pointer_events.clear();
.mouse_pos
.and_then(|new| self.pos.map(|last| new - last))
.unwrap_or_default();
let pressed = !self.down && new.mouse_down;
let released = self.down && !new.mouse_down; let old_pos = self.latest_pos;
let click = released && self.could_be_click; self.interact_pos = self.latest_pos;
let double_click = click && (time - self.last_click_time) < MAX_CLICK_DELAY;
let mut press_origin = self.press_origin; for event in &new.events {
let mut could_be_click = self.could_be_click; match event {
let mut last_click_time = self.last_click_time; Event::PointerMoved(pos) => {
if click { let pos = *pos;
last_click_time = time
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 { self.delta = if let (Some(old_pos), Some(new_pos)) = (old_pos, self.latest_pos) {
press_origin = new.mouse_pos; new_pos - old_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;
} else { } else {
could_be_click = false; Vec2::zero()
} };
if pressed { if let Some(pos) = self.latest_pos {
// Start of a drag: we want to track the velocity for during the drag self.pos_history.add(time, pos);
// and ignore any incoming movement
self.pos_history.clear();
}
if let Some(mouse_pos) = new.mouse_pos {
self.pos_history.add(time, mouse_pos);
} else { } 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 // 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); 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() self.pos_history.velocity().unwrap_or_default()
} else { } else {
Vec2::default() Vec2::default()
}; };
CursorState { self
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,
}
} }
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<Pos2> {
self.press_origin
}
/// Latest reported pointer position.
/// When tapping a touch screen, this will be `None`.
pub(crate) fn latest_pos(&self) -> Option<Pos2> {
self.latest_pos
}
/// If it is a good idea to show a tooltip, where is pointer?
pub fn tooltip_pos(&self) -> Option<Pos2> {
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<Pos2> {
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 { pub fn is_moving(&self) -> bool {
self.velocity != Vec2::zero() 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 { impl InputState {
pub fn ui(&self, ui: &mut crate::Ui) { pub fn ui(&self, ui: &mut crate::Ui) {
let Self { let Self {
raw, raw,
mouse, pointer,
scroll_delta, scroll_delta,
screen_rect, screen_rect,
pixels_per_point, pixels_per_point,
@ -329,10 +491,10 @@ impl InputState {
ui.style_mut().body_text_style = crate::paint::TextStyle::Monospace; ui.style_mut().body_text_style = crate::paint::TextStyle::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui)); ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("🖱 Mouse") crate::containers::CollapsingHeader::new("🖱 Pointer")
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
mouse.ui(ui); pointer.ui(ui);
}); });
ui.label(format!("scroll_delta: {:?} points", scroll_delta)); 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) { pub fn ui(&self, ui: &mut crate::Ui) {
let Self { let Self {
down, latest_pos,
pressed, interact_pos,
released,
could_be_click,
click,
double_click,
last_click_time,
pos,
press_origin,
delta, delta,
velocity, velocity,
pos_history: _, pos_history: _,
down,
press_origin,
could_be_click,
last_click_time,
pointer_events,
} = self; } = self;
ui.label(format!("down: {}", down)); ui.label(format!("latest_pos: {:?}", latest_pos));
ui.label(format!("pressed: {}", pressed)); ui.label(format!("interact_pos: {:?}", interact_pos));
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!("delta: {:?}", delta)); ui.label(format!("delta: {:?}", delta));
ui.label(format!( ui.label(format!(
"velocity: [{:3.0} {:3.0}] points/sec", "velocity: [{:3.0} {:3.0}] points/sec",
velocity.x, velocity.y 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));
} }
} }

View file

@ -29,12 +29,10 @@ impl Widget for &epaint::Texture {
let (tex_w, tex_h) = (self.width as f32, self.height as f32); 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| { response.on_hover_ui(|ui| {
let pos = ui let pos = pointer_pos.unwrap_or_else(|| ui.min_rect().left_top());
.input()
.mouse
.pos
.unwrap_or_else(|| ui.min_rect().left_top());
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); 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 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); let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);

View file

@ -125,7 +125,8 @@ pub(crate) struct Interaction {
} }
impl 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() self.click_id.is_some() || self.drag_id.is_some()
} }
@ -138,12 +139,8 @@ impl Interaction {
self.click_interest = false; self.click_interest = false;
self.drag_interest = false; self.drag_interest = false;
if !prev_input.mouse.could_be_click { if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
self.click_id = None; // pointer button was not down last frame
}
if !prev_input.mouse.down || prev_input.mouse.pos.is_none() {
// mouse was not down last frame
self.click_id = None; self.click_id = None;
self.drag_id = None; self.drag_id = None;
} }
@ -175,7 +172,7 @@ impl Memory {
) { ) {
self.interaction.begin_frame(prev_input, new_input); self.interaction.begin_frame(prev_input, new_input);
if !prev_input.mouse.down { if !prev_input.pointer.any_down() {
self.window_interaction = None; self.window_interaction = None;
} }
} }

View file

@ -7,7 +7,7 @@
//! //!
//! menu::bar(ui, |ui| { //! menu::bar(ui, |ui| {
//! menu::menu(ui, "File", |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); let button_response = ui.add(button);
if button_response.clicked { if button_response.clicked() {
// Toggle // Toggle
if bar_state.open_menu == Some(menu_id) { if bar_state.open_menu == Some(menu_id) {
bar_state.open_menu = None; bar_state.open_menu = None;
} else { } else {
bar_state.open_menu = Some(menu_id); 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); 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. // 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; bar_state.open_menu = None;
} }

View file

@ -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}; use crate::{CtxRef, Id, LayerId, Sense, Ui};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -27,35 +30,34 @@ pub struct Response {
pub sense: Sense, pub sense: Sense,
// OUT: // OUT:
/// The mouse is hovering above this. /// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
pub hovered: bool, pub(crate) hovered: bool,
/// The mouse clicked this thing this frame. /// The pointer clicked this thing this frame.
pub clicked: bool, pub(crate) clicked: [bool; NUM_POINTER_BUTTONS],
/// The thing was double-clicked. /// 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). /// The widgets is being dragged
pub active: bool, 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<Pos2>,
/// This widget has the keyboard focus (i.e. is receiving key pressed). /// 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, /// The widget had keyboard focus and lost it.
/// perhaps because the user pressed enter. pub(crate) lost_kb_focus: bool,
/// 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,
} }
impl std::fmt::Debug for Response { impl std::fmt::Debug for Response {
@ -69,7 +71,10 @@ impl std::fmt::Debug for Response {
hovered, hovered,
clicked, clicked,
double_clicked, double_clicked,
active, dragged,
drag_released,
is_pointer_button_down_on,
interact_pointer_pos,
has_kb_focus, has_kb_focus,
lost_kb_focus, lost_kb_focus,
} = self; } = self;
@ -81,7 +86,10 @@ impl std::fmt::Debug for Response {
.field("hovered", hovered) .field("hovered", hovered)
.field("clicked", clicked) .field("clicked", clicked)
.field("double_clicked", double_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("has_kb_focus", has_kb_focus)
.field("lost_kb_focus", lost_kb_focus) .field("lost_kb_focus", lost_kb_focus)
.finish() .finish()
@ -89,10 +97,89 @@ impl std::fmt::Debug for Response {
} }
impl 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<Pos2> {
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). /// 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. /// 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 { 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); crate::containers::show_tooltip(&self.ctx, add_contents);
} }
self self
@ -116,9 +203,9 @@ impl Response {
/// ``` /// ```
/// # let mut ui = egui::Ui::__test(); /// # let mut ui = egui::Ui::__test();
/// let response = ui.label("hello"); /// 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()); /// let response = response.interact(egui::Sense::click());
/// if response.clicked { /* … */ } /// if response.clicked() { /* … */ }
/// ``` /// ```
pub fn interact(&self, sense: Sense) -> Self { pub fn interact(&self, sense: Sense) -> Self {
self.ctx self.ctx
@ -133,7 +220,7 @@ impl Response {
/// egui::ScrollArea::auto_sized().show(ui, |ui| { /// egui::ScrollArea::auto_sized().show(ui, |ui| {
/// for i in 0..1000 { /// for i in 0..1000 {
/// let response = ui.button(format!("Button {}", i)); /// let response = ui.button(format!("Button {}", i));
/// if response.clicked { /// if response.clicked() {
/// response.scroll_to_me(Align::Center); /// response.scroll_to_me(Align::Center);
/// } /// }
/// } /// }
@ -161,9 +248,21 @@ impl Response {
rect: self.rect.union(other.rect), rect: self.rect.union(other.rect),
sense: self.sense.union(other.sense), sense: self.sense.union(other.sense),
hovered: self.hovered || other.hovered, hovered: self.hovered || other.hovered,
clicked: self.clicked || other.clicked, clicked: [
double_clicked: self.double_clicked || other.double_clicked, self.clicked[0] || other.clicked[0],
active: self.active || other.active, 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, has_kb_focus: self.has_kb_focus || other.has_kb_focus,
lost_kb_focus: self.lost_kb_focus || other.lost_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); /// let mut response = ui.add(widget_a);
/// response |= ui.add(widget_b); /// response |= ui.add(widget_b);
/// response |= ui.add(widget_c); /// 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 { impl std::ops::BitOrAssign for Response {
fn bitor_assign(&mut self, rhs: Self) { fn bitor_assign(&mut self, rhs: Self) {

View file

@ -195,11 +195,11 @@ pub struct Widgets {
impl Widgets { impl Widgets {
pub fn style(&self, response: &Response) -> &WidgetVisuals { 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 &self.active
} else if response.sense == crate::Sense::hover() { } else if response.sense == crate::Sense::hover() {
&self.disabled &self.disabled
} else if response.hovered { } else if response.hovered() {
&self.hovered &self.hovered
} else { } else {
&self.inactive &self.inactive

View file

@ -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() 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`? /// Is the pointer (mouse/touch) above this `Ui`?
/// Equivalent to `ui.rect_contains_mouse(ui.min_rect())` /// 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 { 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())"] #[deprecated = "Use: interact(rect, id, Sense::hover())"]
@ -379,7 +393,7 @@ impl Ui {
self.interact(rect, self.auto_id_with("hover_rect"), Sense::hover()) 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 { pub fn hovered(&self, rect: Rect) -> bool {
self.interact(rect, self.id, Sense::hover()).hovered self.interact(rect, self.id, Sense::hover()).hovered
} }
@ -410,7 +424,7 @@ impl Ui {
/// ``` /// ```
/// # let mut ui = egui::Ui::__test(); /// # let mut ui = egui::Ui::__test();
/// let response = ui.allocate_response(egui::vec2(100.0, 200.0), egui::Sense::click()); /// 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)); /// 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 { pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
@ -572,7 +586,7 @@ impl Ui {
/// # use egui::Align; /// # use egui::Align;
/// # let mut ui = &mut egui::Ui::__test(); /// # let mut ui = &mut egui::Ui::__test();
/// egui::ScrollArea::auto_sized().show(ui, |ui| { /// 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 { /// for i in 0..1000 {
/// ui.label(format!("Item {}", i)); /// ui.label(format!("Item {}", i));
/// } /// }
@ -654,20 +668,20 @@ impl Ui {
self.add(TextEdit::multiline(text)) 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))` /// 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<String>) -> Response { pub fn button(&mut self, text: impl Into<String>) -> Response {
self.add(Button::new(text)) self.add(Button::new(text))
} }
/// A button as small as normal body 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())` /// 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<String>) -> Response { pub fn small_button(&mut self, text: impl Into<String>) -> Response {
self.add(Button::new(text).small()) self.add(Button::new(text).small())
} }
@ -694,7 +708,7 @@ impl Ui {
text: impl Into<String>, text: impl Into<String>,
) -> Response { ) -> Response {
let response = self.radio(*current_value == selected_value, text); let response = self.radio(*current_value == selected_value, text);
if response.clicked { if response.clicked() {
*current_value = selected_value; *current_value = selected_value;
} }
response response
@ -716,7 +730,7 @@ impl Ui {
text: impl Into<String>, text: impl Into<String>,
) -> Response { ) -> Response {
let response = self.selectable_label(*current_value == selected_value, text); let response = self.selectable_label(*current_value == selected_value, text);
if response.clicked { if response.clicked() {
*current_value = selected_value; *current_value = selected_value;
} }
response response

View file

@ -191,7 +191,7 @@ impl<'a> Widget for Checkbox<'a> {
desired_size = desired_size.at_least(spacing.interact_size); desired_size = desired_size.at_least(spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width); desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked { if response.clicked() {
*checked = !*checked; *checked = !*checked;
} }

View file

@ -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()); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if response.active { if let Some(mpos) = response.interact_pointer_pos() {
if let Some(mpos) = ui.input().mouse.pos { *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
*value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
}
} }
let visuals = ui.style().interact(&response); 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 desired_size = Vec2::splat(ui.style().spacing.slider_width);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if response.active { if let Some(mpos) = response.interact_pointer_pos() {
if let Some(mpos) = ui.input().mouse.pos { *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
*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);
*y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
}
} }
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
@ -217,7 +213,7 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>) {
r, g, b, a 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); 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 pupup_id = ui.auto_id_with("popup");
let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color"); 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); ui.memory().toggle_popup(pupup_id);
} }
// TODO: make it easier to show a temporary popup that closes when you click outside it // 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 { if !button_response.clicked() {
let clicked_outside = ui.input().mouse.click && !area_response.hovered; let clicked_outside = ui.input().pointer.any_pressed() && !area_response.hovered();
if clicked_outside || ui.input().key_pressed(Key::Escape) { if clicked_outside || ui.input().key_pressed(Key::Escape) {
ui.memory().close_popup(); ui.memory().close_popup();
} }

View file

@ -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 value as f32, // Show full precision value on-hover. TODO: figure out f64 vs f32
suffix suffix
)); ));
if response.clicked { if response.clicked() {
ui.memory().request_kb_focus(kb_edit_id); ui.memory().request_kb_focus(kb_edit_id);
ui.memory().temp_edit_string = None; // Filled in next frame ui.memory().temp_edit_string = None; // Filled in next frame
} else if response.active { } else if response.dragged() {
let mdelta = ui.input().mouse.delta; let mdelta = ui.input().pointer.delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
let delta_value = speed * delta_points; let delta_value = speed * delta_points;
if delta_value != 0.0 { if delta_value != 0.0 {

View file

@ -48,17 +48,17 @@ impl Widget for Hyperlink {
let galley = font.layout_multiline(text, ui.available_width()); let galley = font.layout_multiline(text, ui.available_width());
let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click()); let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click());
if response.hovered { if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand; ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
} }
if response.clicked { if response.clicked() {
ui.ctx().output().open_url = Some(url.clone()); ui.ctx().output().open_url = Some(url.clone());
} }
let color = ui.style().visuals.hyperlink_color; let color = ui.style().visuals.hyperlink_color;
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
if response.hovered { if response.hovered() {
// Underline: // Underline:
for line in &galley.rows { for line in &galley.rows {
let pos = rect.min; let pos = rect.min;

View file

@ -2,7 +2,7 @@
//! //!
//! Example widget uses: //! Example widget uses:
//! * `ui.add(Label::new("Text").text_color(color::red));` //! * `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)] #![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. /// The button is only enabled if the value does not already have its original value.
pub fn reset_button<T: Default + PartialEq>(ui: &mut Ui, value: &mut T) { pub fn reset_button<T: Default + PartialEq>(ui: &mut Ui, value: &mut T) {
let def = T::default(); 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; *value = def;
} }
} }

View file

@ -42,7 +42,7 @@ impl Widget for SelectableLabel {
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
if selected || response.hovered { if selected || response.hovered() {
let rect = rect.expand(visuals.expansion); let rect = rect.expand(visuals.expansion);
let fill = if selected { let fill = if selected {
ui.style().visuals.selection.bg_fill ui.style().visuals.selection.bg_fill

View file

@ -252,19 +252,17 @@ impl<'a> Slider<'a> {
let rect = &response.rect; let rect = &response.rect;
let x_range = x_range(rect); let x_range = x_range(rect);
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(pointer_pos) = response.interact_pointer_pos() {
if response.active { let new_value = if self.smart_aim {
let new_value = if self.smart_aim { let aim_radius = ui.input().aim_radius();
let aim_radius = ui.input().aim_radius(); crate::math::smart_aim::best_in_range_f64(
crate::math::smart_aim::best_in_range_f64( self.value_from_x(pointer_pos.x - aim_radius, x_range.clone()),
self.value_from_x(mouse_pos.x - aim_radius, x_range.clone()), self.value_from_x(pointer_pos.x + aim_radius, x_range.clone()),
self.value_from_x(mouse_pos.x + aim_radius, x_range.clone()), )
) } else {
} else { self.value_from_x(pointer_pos.x, x_range.clone())
self.value_from_x(mouse_pos.x, x_range.clone()) };
}; self.set_value(new_value);
self.set_value(new_value);
}
} }
// Paint it: // 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 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()); // 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().request_kb_focus(kb_edit_id);
ui.memory().temp_edit_string = None; // Filled in next frame ui.memory().temp_edit_string = None; // Filled in next frame
} }

View file

@ -115,7 +115,7 @@ impl CCursorPair {
/// # let mut ui = egui::Ui::__test(); /// # let mut ui = egui::Ui::__test();
/// # let mut my_string = String::new(); /// # let mut my_string = String::new();
/// let response = ui.add(egui::TextEdit::singleline(&mut my_string)); /// let response = ui.add(egui::TextEdit::singleline(&mut my_string));
/// if response.lost_kb_focus { /// if response.lost_kb_focus() {
/// // use my_string /// // use my_string
/// } /// }
/// ``` /// ```
@ -318,45 +318,45 @@ impl<'t> TextEdit<'t> {
} }
if enabled { 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: triple-click to select whole paragraph
// TODO: drag selected text to either move or clone (ctrl on windows, alt on mac) // 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: // 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: // Select word:
let center = cursor_at_mouse; let center = cursor_at_pointer;
let ccursorp = select_word_at(text, center.ccursor); let ccursorp = select_word_at(text, center.ccursor);
state.cursorp = Some(CursorPair { state.cursorp = Some(CursorPair {
primary: galley.from_ccursor(ccursorp.primary), primary: galley.from_ccursor(ccursorp.primary),
secondary: galley.from_ccursor(ccursorp.secondary), 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); ui.memory().request_kb_focus(id);
if ui.input().modifiers.shift { if ui.input().modifiers.shift {
if let Some(cursorp) = &mut state.cursorp { if let Some(cursorp) = &mut state.cursorp {
cursorp.primary = cursor_at_mouse; cursorp.primary = cursor_at_pointer;
} else { } else {
state.cursorp = Some(CursorPair::one(cursor_at_mouse)); state.cursorp = Some(CursorPair::one(cursor_at_pointer));
} }
} else { } 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 { 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 // User clicked somewhere else
ui.memory().surrender_kb_focus(id); ui.memory().surrender_kb_focus(id);
} }
@ -365,7 +365,7 @@ impl<'t> TextEdit<'t> {
ui.memory().surrender_kb_focus(id); ui.memory().surrender_kb_focus(id);
} }
if response.hovered && enabled { if response.hovered() && enabled {
ui.output().cursor_icon = CursorIcon::Text; ui.output().cursor_icon = CursorIcon::Text;
} }
@ -471,7 +471,7 @@ impl<'t> TextEdit<'t> {
modifiers, modifiers,
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers), } => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
Event::Key { .. } => None, _ => None,
}; };
if let Some(new_ccursorp) = did_mutate_text { if let Some(new_ccursorp) = did_mutate_text {

View file

@ -57,7 +57,7 @@ impl DemoWindow {
ui.columns(self.num_columns, |cols| { ui.columns(self.num_columns, |cols| {
for (i, col) in cols.iter_mut().enumerate() { for (i, col) in cols.iter_mut().enumerate() {
col.label(format!("Column {} out of {}", i + 1, self.num_columns)); 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; self.num_columns -= 1;
} }
} }
@ -287,15 +287,15 @@ impl LayoutDemo {
pub fn content_ui(&mut self, ui: &mut Ui) { pub fn content_ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Top-down").clicked { if ui.button("Top-down").clicked() {
*self = Default::default(); *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 = Default::default();
self.cross_align = Align::Center; self.cross_align = Align::Center;
self.cross_justify = true; self.cross_justify = true;
} }
if ui.button("Horizontal wrapped").clicked { if ui.button("Horizontal wrapped").clicked() {
*self = Default::default(); *self = Default::default();
self.main_dir = Direction::LeftToRight; self.main_dir = Direction::LeftToRight;
self.cross_align = Align::Center; self.cross_align = Align::Center;
@ -391,7 +391,7 @@ impl Tree {
if depth > 0 if depth > 0
&& ui && ui
.add(Button::new("delete").text_color(Color32::RED)) .add(Button::new("delete").text_color(Color32::RED))
.clicked .clicked()
{ {
return Action::Delete; return Action::Delete;
} }
@ -409,7 +409,7 @@ impl Tree {
}) })
.collect(); .collect();
if ui.button("+").clicked { if ui.button("+").clicked() {
self.0.push(Tree::default()); self.0.push(Tree::default());
} }

View file

@ -14,6 +14,7 @@ impl Default for Demos {
fn default() -> Self { fn default() -> Self {
let demos: Vec<Box<dyn super::Demo>> = vec![ let demos: Vec<Box<dyn super::Demo>> = vec![
Box::new(super::WidgetGallery::default()), Box::new(super::WidgetGallery::default()),
Box::new(super::input_test::InputTest::default()),
Box::new(super::FontBook::default()), Box::new(super::FontBook::default()),
Box::new(super::Painting::default()), Box::new(super::Painting::default()),
Box::new(super::DancingStrings::default()), Box::new(super::DancingStrings::default()),
@ -86,7 +87,7 @@ impl DemoWindows {
ui.separator(); ui.separator();
if ui.button("Organize windows").clicked { if ui.button("Organize windows").clicked() {
ui.ctx().memory().reset_areas(); ui.ctx().memory().reset_areas();
} }
}); });
@ -255,13 +256,13 @@ fn show_menu_bar(ui: &mut Ui) {
menu::bar(ui, |ui| { menu::bar(ui, |ui| {
menu::menu(ui, "File", |ui| { menu::menu(ui, "File", |ui| {
if ui.button("Organize windows").clicked { if ui.button("Organize windows").clicked() {
ui.ctx().memory().reset_areas(); ui.ctx().memory().reset_areas();
} }
if ui if ui
.button("Clear egui memory") .button("Clear egui memory")
.on_hover_text("Forget scroll, collapsing headers etc") .on_hover_text("Forget scroll, collapsing headers etc")
.clicked .clicked()
{ {
*ui.ctx().memory() = Default::default(); *ui.ctx().memory() = Default::default();
} }

View file

@ -8,7 +8,7 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
// Check for drags: // Check for drags:
let response = ui.interact(response.rect, id, Sense::drag()); let response = ui.interact(response.rect, id, Sense::drag());
if response.hovered { if response.hovered() {
ui.output().cursor_icon = CursorIcon::Grab; ui.output().cursor_icon = CursorIcon::Grab;
} }
} else { } 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`) // (anything with `Order::Tooltip` always gets an empty `Response`)
// So this is fine! // So this is fine!
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
let delta = mouse_pos - response.rect.center(); let delta = pointer_pos - response.rect.center();
ui.ctx().translate_layer(layer_id, delta); ui.ctx().translate_layer(layer_id, delta);
} }
} }
@ -49,7 +49,7 @@ pub fn drop_target<R>(
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); 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 (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 ui.style().visuals.widgets.active
} else if is_being_dragged && can_accept_what_is_being_dragged { } else if is_being_dragged && can_accept_what_is_being_dragged {
ui.style().visuals.widgets.inactive ui.style().visuals.widgets.inactive
@ -126,8 +126,7 @@ impl super::View for DragAndDropDemo {
ui.label(item); ui.label(item);
}); });
let this_item_being_dragged = ui.memory().is_being_dragged(item_id); if ui.memory().is_being_dragged(item_id) {
if this_item_being_dragged {
source_col_row = Some((col_idx, row_idx)); source_col_row = Some((col_idx, row_idx));
} }
} }
@ -135,7 +134,7 @@ impl super::View for DragAndDropDemo {
.1; .1;
let is_being_dragged = ui.memory().is_anything_being_dragged(); 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); 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((source_col, source_row)) = source_col_row {
if let Some(drop_col) = drop_col { if let Some(drop_col) = drop_col {
if ui.input().mouse.released { if ui.input().pointer.any_released() {
// do the drop: // do the drop:
let item = self.columns[source_col].remove(source_row); let item = self.columns[source_col].remove(source_row);
self.columns[drop_col].push(item); self.columns[drop_col].push(item);

View file

@ -31,7 +31,7 @@ impl FontBook {
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32)); 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(); ui.output().copied_text = chr.to_string();
} }
} }
@ -81,7 +81,7 @@ impl super::View for FontBook {
ui.label("Filter:"); ui.label("Filter:");
ui.text_edit_singleline(&mut self.filter); ui.text_edit_singleline(&mut self.filter);
self.filter = self.filter.to_lowercase(); self.filter = self.filter.to_lowercase();
if ui.button("").clicked { if ui.button("").clicked() {
self.filter.clear(); self.filter.clear();
} }
}); });

View file

@ -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);
}
}

View file

@ -12,6 +12,7 @@ mod drag_and_drop;
mod font_book; mod font_book;
pub mod font_contents_emoji; pub mod font_contents_emoji;
pub mod font_contents_ubuntu; pub mod font_contents_ubuntu;
pub mod input_test;
mod painting; mod painting;
mod scrolls; mod scrolls;
mod sliders; mod sliders;

View file

@ -21,7 +21,7 @@ impl Painting {
ui.horizontal(|ui| { ui.horizontal(|ui| {
egui::stroke_ui(ui, &mut self.stroke, "Stroke"); egui::stroke_ui(ui, &mut self.stroke, "Stroke");
ui.separator(); ui.separator();
if ui.button("Clear Painting").clicked { if ui.button("Clear Painting").clicked() {
self.lines.clear(); self.lines.clear();
} }
}); });
@ -38,12 +38,10 @@ impl Painting {
let current_line = self.lines.last_mut().unwrap(); let current_line = self.lines.last_mut().unwrap();
if response.active { if let Some(pointer_pos) = response.interact_pointer_pos() {
if let Some(mouse_pos) = ui.input().mouse.pos { let canvas_pos = pointer_pos - rect.min;
let canvas_pos = mouse_pos - rect.min; if current_line.last() != Some(&canvas_pos) {
if current_line.last() != Some(&canvas_pos) { current_line.push(canvas_pos);
current_line.push(canvas_pos);
}
} }
} else if !current_line.is_empty() { } else if !current_line.is_empty() {
self.lines.push(vec![]); self.lines.push(vec![]);

View file

@ -33,13 +33,13 @@ impl Scrolls {
ui.add(Slider::usize(&mut self.track_item, 1..=50).text("Track Item")); ui.add(Slider::usize(&mut self.track_item, 1..=50).text("Track Item"));
}); });
let (scroll_offset, _) = ui.horizontal(|ui| { 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")); ui.add(DragValue::f32(&mut self.offset).speed(1.0).suffix("px"));
scroll_offset scroll_offset
}); });
let scroll_top = ui.button("Scroll to top").clicked; let scroll_top = ui.button("Scroll to top").clicked();
let scroll_bottom = ui.button("Scroll to bottom").clicked; let scroll_bottom = ui.button("Scroll to bottom").clicked();
if scroll_bottom || scroll_top { if scroll_bottom || scroll_top {
self.tracking = false; self.tracking = false;
} }

View file

@ -73,7 +73,7 @@ impl Sliders {
You can always see the full precision value by hovering the value.", 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; self.value = std::f64::consts::PI;
} }
} }

View file

@ -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()); let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
// 3. Interact: Time to check for clicks!. // 3. Interact: Time to check for clicks!.
if response.clicked { if response.clicked() {
*on = !*on; *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 { fn toggle_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
let desired_size = ui.style().spacing.interact_size; let desired_size = ui.style().spacing.interact_size;
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); 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 how_on = ui.ctx().animate_bool(response.id, *on);
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);

View file

@ -115,7 +115,7 @@ impl super::View for WidgetGallery {
ui.end_row(); ui.end_row();
ui.label("Button:"); ui.label("Button:");
if ui.button("Toggle boolean").clicked { if ui.button("Toggle boolean").clicked() {
*boolean = !*boolean; *boolean = !*boolean;
} }
ui.end_row(); ui.end_row();
@ -123,7 +123,7 @@ impl super::View for WidgetGallery {
ui.label("ImageButton:"); ui.label("ImageButton:");
if ui if ui
.add(egui::ImageButton::new(egui::TextureId::Egui, [24.0, 16.0])) .add(egui::ImageButton::new(egui::TextureId::Egui, [24.0, 16.0]))
.clicked .clicked()
{ {
*boolean = !*boolean; *boolean = !*boolean;
} }

View file

@ -97,7 +97,7 @@ impl Widgets {
if ui if ui
.add(Button::new("Click me").enabled(self.button_enabled)) .add(Button::new("Click me").enabled(self.button_enabled))
.on_hover_text("This will just increase a counter.") .on_hover_text("This will just increase a counter.")
.clicked .clicked()
{ {
self.count += 1; self.count += 1;
} }
@ -146,7 +146,7 @@ impl Widgets {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Single line text input:"); ui.label("Single line text input:");
let response = ui.text_edit_singleline(&mut self.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. // The user pressed enter.
} }
}); });

View file

@ -113,8 +113,8 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut epi::Frame<'_>, url: &mut String) -> Op
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("URL:"); ui.label("URL:");
trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus; trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus();
trigger_fetch |= ui.button("GET").clicked; trigger_fetch |= ui.button("GET").clicked();
}); });
if frame.is_web() { 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| { ui.horizontal(|ui| {
if ui.button("Source code for this example").clicked { if ui.button("Source code for this example").clicked() {
*url = format!( *url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}", "https://raw.githubusercontent.com/emilk/egui/master/{}",
file!() file!()
); );
trigger_fetch = true; trigger_fetch = true;
} }
if ui.button("Random image").clicked { if ui.button("Random image").clicked() {
let seed = ui.input().time; let seed = ui.input().time;
let width = 640; let width = 640;
let height = 480; let height = 480;
@ -170,7 +170,7 @@ fn ui_resouce(
if let Some(text) = &response.text { if let Some(text) = &response.text {
let tooltip = "Click to copy the response body"; 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(); ui.output().copied_text = text.clone();
} }
} }

View file

@ -79,9 +79,9 @@ impl FrameHistory {
let rect = rect.shrink(4.0); let rect = rect.shrink(4.0);
let line_stroke = Stroke::new(1.0, Color32::from_additive_luminance(128)); let line_stroke = Stroke::new(1.0, Color32::from_additive_luminance(128));
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(pointer_pos) = ui.input().pointer.tooltip_pos() {
if rect.contains(mouse_pos) { if rect.contains(pointer_pos) {
let y = mouse_pos.y; let y = pointer_pos.y;
shapes.push(Shape::line_segment( shapes.push(Shape::line_segment(
[pos2(rect.left(), y), pos2(rect.right(), y)], [pos2(rect.left(), y), pos2(rect.right(), y)],
line_stroke, line_stroke,

View file

@ -73,7 +73,7 @@ impl epi::App for WrapApp {
for (anchor, app) in self.apps.iter_mut() { for (anchor, app) in self.apps.iter_mut() {
if ui if ui
.selectable_label(self.selected_anchor == anchor, app.name()) .selectable_label(self.selected_anchor == anchor, app.name())
.clicked .clicked()
{ {
self.selected_anchor = anchor.to_owned(); self.selected_anchor = anchor.to_owned();
if frame.is_web() { if frame.is_web() {
@ -86,7 +86,7 @@ impl epi::App for WrapApp {
if false { if false {
// TODO: fix the overlap on small screens // TODO: fix the overlap on small screens
if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { 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(); self.selected_anchor = "clock".to_owned();
if frame.is_web() { if frame.is_web() {
ui.output().open_url = Some("#clock".to_owned()); ui.output().open_url = Some("#clock".to_owned());
@ -234,11 +234,11 @@ impl BackendPanel {
if ui if ui
.button("📱 Phone Size") .button("📱 Phone Size")
.on_hover_text("Resize the window to be small like a phone.") .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 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(); frame.quit();
} }
} }
@ -275,7 +275,7 @@ impl BackendPanel {
"Reset scale to native value ({:.1})", "Reset scale to native value ({:.1})",
native_pixels_per_point native_pixels_per_point
)) ))
.clicked .clicked()
{ {
*pixels_per_point = native_pixels_per_point; *pixels_per_point = native_pixels_per_point;
} }
@ -283,7 +283,7 @@ impl BackendPanel {
}); });
// We wait until mouse release to activate: // We wait until mouse release to activate:
if ui.ctx().is_using_mouse() { if ui.ctx().is_using_pointer() {
None None
} else { } else {
Some(*pixels_per_point) Some(*pixels_per_point)

View file

@ -29,12 +29,14 @@ use {
pub use clipboard::ClipboardContext; // TODO: remove pub use clipboard::ClipboardContext; // TODO: remove
pub struct GliumInputState { pub struct GliumInputState {
pub pointer_pos_in_points: Option<Pos2>,
pub raw: egui::RawInput, pub raw: egui::RawInput,
} }
impl GliumInputState { impl GliumInputState {
pub fn from_pixels_per_point(pixels_per_point: f32) -> Self { pub fn from_pixels_per_point(pixels_per_point: f32) -> Self {
Self { Self {
pointer_pos_in_points: Default::default(),
raw: egui::RawInput { raw: egui::RawInput {
pixels_per_point: Some(pixels_per_point), pixels_per_point: Some(pixels_per_point),
..Default::default() ..Default::default()
@ -52,23 +54,38 @@ pub fn input_to_egui(
use glutin::event::WindowEvent; use glutin::event::WindowEvent;
match event { match event {
WindowEvent::CloseRequested | WindowEvent::Destroyed => *control_flow = ControlFlow::Exit, WindowEvent::CloseRequested | WindowEvent::Destroyed => *control_flow = ControlFlow::Exit,
WindowEvent::MouseInput { state, .. } => { WindowEvent::MouseInput { state, button, .. } => {
input_state.raw.mouse_down = state == glutin::event::ElementState::Pressed; 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 { WindowEvent::CursorMoved {
position: pos_in_pixels, 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.x as f32 / input_state.raw.pixels_per_point.unwrap(),
pos_in_pixels.y 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 { .. } => { 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) => { WindowEvent::ReceivedCharacter(ch) => {
if printable_char(ch) if is_printable_char(ch)
&& !input_state.raw.modifiers.ctrl && !input_state.raw.modifiers.ctrl
&& !input_state.raw.modifiers.mac_cmd && !input_state.raw.modifiers.mac_cmd
{ {
@ -79,6 +96,7 @@ pub fn input_to_egui(
if let Some(keycode) = input.virtual_keycode { if let Some(keycode) = input.virtual_keycode {
let pressed = input.state == glutin::event::ElementState::Pressed; let pressed = input.state == glutin::event::ElementState::Pressed;
// We could also use `WindowEvent::ModifiersChanged` instead, I guess.
if matches!(keycode, VirtualKeyCode::LAlt | VirtualKeyCode::RAlt) { if matches!(keycode, VirtualKeyCode::LAlt | VirtualKeyCode::RAlt) {
input_state.raw.modifiers.alt = pressed; input_state.raw.modifiers.alt = pressed;
} }
@ -157,7 +175,7 @@ pub fn input_to_egui(
/// Ignore those. /// Ignore those.
/// We also ignore '\r', '\n', '\t'. /// We also ignore '\r', '\n', '\t'.
/// Newlines are handled by the `Key::Enter` event. /// 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}' let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}'
|| '\u{f0000}' <= chr && chr <= '\u{ffffd}' || '\u{f0000}' <= chr && chr <= '\u{ffffd}'
|| '\u{100000}' <= chr && chr <= '\u{10fffd}'; || '\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() !is_in_private_use_area && !chr.is_ascii_control()
} }
pub fn translate_mouse_button(button: glutin::event::MouseButton) -> Option<egui::PointerButton> {
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<egui::Key> { pub fn translate_virtual_key_code(key: VirtualKeyCode) -> Option<egui::Key> {
use VirtualKeyCode::*; use VirtualKeyCode::*;

View file

@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## 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 ## 0.8.0 - 2021-01-17

View file

@ -81,6 +81,9 @@ pub struct WebInput {
/// Is this a touch screen? If so, we ignore mouse events. /// Is this a touch screen? If so, we ignore mouse events.
pub is_touch: bool, pub is_touch: bool,
/// Required because we don't get a position on touched
pub latest_touch_pos: Option<egui::Pos2>,
pub raw: egui::RawInput, pub raw: egui::RawInput,
} }

View file

@ -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<egui::PointerButton> {
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 { pub fn pos_from_touch_event(event: &web_sys::TouchEvent) -> egui::Pos2 {
let t = event.touches().get(0).unwrap(); let t = event.touches().get(0).unwrap();
egui::Pos2 { egui::Pos2 {
@ -599,19 +608,40 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap(); 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<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
{ {
let event_name = "mousedown"; let event_name = "mousedown";
let runner_ref = runner_ref.clone(); let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch { if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos = if let Some(button) = button_from_mouse_event(&event) {
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.mouse_down = true; let modifiers = runner_lock.input.raw.modifiers;
runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead runner_lock
runner_lock.needs_repaint.set_true(); .input
event.stop_propagation(); .raw
event.prevent_default(); .events
.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
} }
}) as Box<dyn FnMut(_)>); }) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; 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 closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch { if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos = let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
Some(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(); runner_lock.needs_repaint.set_true();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); 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 closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch { if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos = if let Some(button) = button_from_mouse_event(&event) {
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.mouse_down = false; let modifiers = runner_lock.input.raw.modifiers;
runner_lock.needs_repaint.set_true(); runner_lock
event.stop_propagation(); .input
event.prevent_default(); .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<dyn FnMut(_)>); }) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; 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 closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch { 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(); runner_lock.needs_repaint.set_true();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -673,10 +718,21 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let event_name = "touchstart"; let event_name = "touchstart";
let runner_ref = runner_ref.clone(); let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { 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(); 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.is_touch = true;
runner_lock.input.raw.mouse_pos = Some(pos_from_touch_event(&event)); let modifiers = runner_lock.input.raw.modifiers;
runner_lock.input.raw.mouse_down = true; runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.set_true(); runner_lock.needs_repaint.set_true();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); event.prevent_default();
@ -689,9 +745,15 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let event_name = "touchmove"; let event_name = "touchmove";
let runner_ref = runner_ref.clone(); let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { 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(); 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.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(); runner_lock.needs_repaint.set_true();
event.stop_propagation(); event.stop_propagation();
event.prevent_default(); 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 closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
runner_lock.input.is_touch = true; runner_lock.input.is_touch = true;
runner_lock.input.raw.mouse_down = false; // First release mouse to click... if let Some(pos) = runner_lock.input.latest_touch_pos {
runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead) let modifiers = runner_lock.input.raw.modifiers;
runner_lock.input.raw.mouse_pos = None; // ...remove hover effect // First release mouse to click:
runner_lock.needs_repaint.set_true(); runner_lock
event.stop_propagation(); .input
event.prevent_default(); .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<dyn FnMut(_)>); }) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget(); closure.forget();

View file

@ -21,6 +21,9 @@ pub const fn vec2(x: f32, y: f32) -> Vec2 {
Vec2 { x, y } Vec2 { x, y }
} }
// ----------------------------------------------------------------------------
// Compatibility and convenience conversions to and from [f32; 2]:
impl From<[f32; 2]> for Vec2 { impl From<[f32; 2]> for Vec2 {
fn from(v: [f32; 2]) -> Self { fn from(v: [f32; 2]) -> Self {
Self { x: v[0], y: v[1] } Self { x: v[0], y: v[1] }
@ -33,6 +36,47 @@ impl From<&[f32; 2]> for Vec2 {
} }
} }
impl From<Vec2> 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<Vec2> 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 { impl Vec2 {
pub const X: Vec2 = Vec2 { x: 1.0, y: 0.0 }; pub const X: Vec2 = Vec2 { x: 1.0, y: 0.0 };
pub const Y: Vec2 = Vec2 { x: 0.0, y: 1.0 }; pub const Y: Vec2 = Vec2 { x: 0.0, y: 1.0 };