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 ⭐
* `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

View file

@ -51,7 +51,7 @@ ui.horizontal(|ui| {
ui.text_edit_singleline(&mut name);
});
ui.add(egui::Slider::u32(&mut age, 0..=120).text("age"));
if ui.button("Click each year").clicked {
if ui.button("Click each year").clicked() {
age += 1;
}
ui.label(format!("Hello '{}', age {}", name, age));

View file

@ -29,7 +29,7 @@ impl epi::App for MyApp {
ui.text_edit_singleline(name);
});
ui.add(egui::Slider::u32(age, 0..=120).text("age"));
if ui.button("Click each year").clicked {
if ui.button("Click each year").clicked() {
*age += 1;
}
ui.label(format!("Hello '{}', age {}", name, age));

View file

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

View file

@ -199,7 +199,7 @@ impl CollapsingHeader {
);
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open);
if header_response.clicked {
if header_response.clicked() {
state.toggle(ui);
}

View file

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

View file

@ -2,7 +2,7 @@
use crate::*;
/// Show a tooltip at the current mouse position (if any).
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
@ -10,7 +10,7 @@ use crate::*;
///
/// ```
/// # let mut ui = egui::Ui::__test();
/// if ui.ui_contains_mouse() {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), |ui| {
/// ui.label("Helpful text");
/// });
@ -21,9 +21,9 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) {
let window_pos = if let Some(tooltip_rect) = tooltip_rect {
tooltip_rect.left_bottom()
} else if let Some(mouse_pos) = ctx.input().mouse.pos {
} else if let Some(pointer_pos) = ctx.input().pointer.tooltip_pos() {
let expected_size = vec2(ctx.style().spacing.tooltip_width, 32.0);
let position = mouse_pos + vec2(16.0, 16.0);
let position = pointer_pos + vec2(16.0, 16.0);
let position = position.min(ctx.input().screen_rect().right_bottom() - expected_size);
let position = position.max(ctx.input().screen_rect().left_top());
position
@ -41,7 +41,7 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) {
ctx.frame_state().tooltip_rect = Some(tooltip_rect.union(response.rect));
}
/// Show some text at the current mouse position (if any).
/// Show some text at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_text`].
///
@ -49,7 +49,7 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) {
///
/// ```
/// # let mut ui = egui::Ui::__test();
/// if ui.ui_contains_mouse() {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), "Helpful text");
/// }
/// ```
@ -89,7 +89,7 @@ fn show_tooltip_area(
/// # let ui = &mut egui::Ui::__test();
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked {
/// if response.clicked() {
/// ui.memory().toggle_popup(popup_id);
/// }
/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| {
@ -121,7 +121,8 @@ pub fn popup_below_widget(
});
});
if ui.input().key_pressed(Key::Escape) || ui.input().mouse.click && !widget_response.clicked
if ui.input().key_pressed(Key::Escape)
|| ui.input().pointer.any_click() && !widget_response.clicked()
{
ui.memory().close_popup();
}

View file

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

View file

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

View file

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

View file

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

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.
#[derive(Clone, Debug)]
pub struct RawInput {
/// Is the button currently down?
/// NOTE: egui currently only supports the primary mouse button.
pub mouse_down: bool,
/// Current position of the mouse in points.
pub mouse_pos: Option<Pos2>,
/// How many points (logical pixels) the user scrolled
pub scroll_delta: Vec2,
@ -56,8 +49,6 @@ impl Default for RawInput {
fn default() -> Self {
#![allow(deprecated)] // for screen_size
Self {
mouse_down: false,
mouse_pos: None,
scroll_delta: Vec2::zero(),
screen_size: Default::default(),
screen_rect: None,
@ -75,8 +66,6 @@ impl RawInput {
pub fn take(&mut self) -> RawInput {
#![allow(deprecated)] // for screen_size
RawInput {
mouse_down: self.mouse_down,
mouse_pos: self.mouse_pos,
scroll_delta: std::mem::take(&mut self.scroll_delta),
screen_size: self.screen_size,
screen_rect: self.screen_rect,
@ -107,8 +96,38 @@ pub enum Event {
pressed: bool,
modifiers: Modifiers,
},
PointerMoved(Pos2),
PointerButton {
pos: Pos2,
button: PointerButton,
pressed: bool,
/// The state of the modifier keys at the time of the event
modifiers: Modifiers,
},
/// The mouse left the screen, or the last/primary touch input disappeared.
///
/// This means there is no longer a cursor on the screen for hovering etc.
///
/// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`.
PointerGone,
}
/// Mouse button (or similar for touch input)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PointerButton {
/// The primary mouse button is usually the left one.
Primary = 0,
/// The secondary mouse button is usually the right one,
/// and most often used for context menus or other optional things.
Secondary = 1,
/// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel).
Middle = 2,
}
/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`].
pub const NUM_POINTER_BUTTONS: usize = 3;
/// State of the modifier keys. These must be fed to egui.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Modifiers {
@ -208,8 +227,6 @@ impl RawInput {
pub fn ui(&self, ui: &mut crate::Ui) {
#![allow(deprecated)] // for screen_size
let Self {
mouse_down,
mouse_pos,
scroll_delta,
screen_size: _,
screen_rect,
@ -220,10 +237,6 @@ impl RawInput {
events,
} = self;
// TODO: simpler way to show values, e.g. `ui.value("Mouse Pos:", self.mouse_pos);
// TODO: `ui.style_mut().text_style = TextStyle::Monospace`;
ui.label(format!("mouse_down: {}", mouse_down));
ui.label(format!("mouse_pos: {:.1?}", mouse_pos));
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))

View file

@ -5,20 +5,21 @@ use crate::data::input::*;
pub use crate::data::input::Key;
/// If mouse moves more than this, it is no longer a click (but maybe a drag)
/// If the pointer moves more than this, it is no longer a click (but maybe a drag)
const MAX_CLICK_DIST: f32 = 6.0; // TODO: move to settings
/// The new mouse press must come within this many seconds from previous mouse release
/// The new pointer press must come within this many seconds from previous pointer release
const MAX_CLICK_DELAY: f64 = 0.3; // TODO: move to settings
/// Input state that egui updates each frame.
#[derive(Clone, Debug)]
pub struct InputState {
/// The raw input we got this frame
/// The raw input we got this frame from the backend.
pub raw: RawInput,
pub mouse: CursorState,
/// State of the mouse or touch.
pub pointer: PointerState,
/// How many pixels the user scrolled
/// How many pixels the user scrolled.
pub scroll_delta: Vec2,
/// Position and size of the egui area.
@ -52,7 +53,7 @@ impl Default for InputState {
fn default() -> Self {
Self {
raw: Default::default(),
mouse: Default::default(),
pointer: Default::default(),
scroll_delta: Default::default(),
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
pixels_per_point: 1.0,
@ -66,71 +67,6 @@ impl Default for InputState {
}
}
/// Mouse (or touch) state.
#[derive(Clone, Debug)]
pub struct CursorState {
/// Is the button currently down?
/// true the frame when it is pressed,
/// false the frame it is released.
pub down: bool,
/// The mouse went from !down to down
pub pressed: bool,
/// The mouse went from down to !down
pub released: bool,
/// If the mouse is down, will it register as a click when released?
/// Set to true on mouse down, set to false when mouse moves too much.
pub could_be_click: bool,
/// Was there a click?
/// Did a mouse button get released this frame closely after going down?
pub click: bool,
/// Was there a double-click?
pub double_click: bool,
/// When did the mouse get click last?
/// Used to check for double-clicks.
pub last_click_time: f64,
/// Current position of the mouse in points.
/// None for touch screens when finger is not down.
pub pos: Option<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 {
#[must_use]
pub fn begin_frame(self, new: RawInput) -> InputState {
@ -147,7 +83,7 @@ impl InputState {
self.screen_rect
}
});
let mouse = self.mouse.begin_frame(time, &new);
let pointer = self.pointer.begin_frame(time, &new);
let mut keys_down = self.keys_down;
for event in &new.events {
if let Event::Key { key, pressed, .. } = event {
@ -159,7 +95,7 @@ impl InputState {
}
}
InputState {
mouse,
pointer,
scroll_delta: new.scroll_delta,
screen_rect,
pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point),
@ -178,11 +114,7 @@ impl InputState {
}
pub fn wants_repaint(&self) -> bool {
self.mouse.pressed
|| self.mouse.released
|| self.mouse.delta != Vec2::zero()
|| self.scroll_delta != Vec2::zero()
|| !self.events.is_empty()
self.pointer.wants_repaint() || self.scroll_delta != Vec2::zero() || !self.events.is_empty()
}
/// Was the given key pressed this frame?
@ -236,85 +168,315 @@ impl InputState {
}
}
impl CursorState {
// ----------------------------------------------------------------------------
/// A pointer (mouse or touch) click.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Click {
pub pos: Pos2,
pub button: PointerButton,
/// 1 or 2 (double-click)
pub count: u32,
/// Allows you to check for e.g. shift-click
pub modifiers: Modifiers,
}
impl Click {
pub fn is_double(&self) -> bool {
self.count == 2
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum PointerEvent {
Moved(Pos2),
Pressed(Pos2),
Released(Option<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]
pub fn begin_frame(mut self, time: f64, new: &RawInput) -> CursorState {
let delta = new
.mouse_pos
.and_then(|new| self.pos.map(|last| new - last))
.unwrap_or_default();
let pressed = !self.down && new.mouse_down;
pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> PointerState {
self.pointer_events.clear();
let released = self.down && !new.mouse_down;
let click = released && self.could_be_click;
let double_click = click && (time - self.last_click_time) < MAX_CLICK_DELAY;
let mut press_origin = self.press_origin;
let mut could_be_click = self.could_be_click;
let mut last_click_time = self.last_click_time;
if click {
last_click_time = time
}
let old_pos = self.latest_pos;
self.interact_pos = self.latest_pos;
if pressed {
press_origin = new.mouse_pos;
could_be_click = true;
} else if !self.down || self.pos.is_none() {
press_origin = None;
}
for event in &new.events {
match event {
Event::PointerMoved(pos) => {
let pos = *pos;
if let (Some(press_origin), Some(mouse_pos)) = (new.mouse_pos, press_origin) {
could_be_click &= press_origin.distance(mouse_pos) < MAX_CLICK_DIST;
self.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 {
could_be_click = false;
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 let Some(mouse_pos) = new.mouse_pos {
self.pos_history.add(time, mouse_pos);
if pressed {
self.press_origin = Some(pos);
self.could_be_click = true;
self.pointer_events.push(PointerEvent::Pressed(pos));
} else {
// we do not clear the `mouse_tracker` here, because it is exactly when a finger has
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.
}
_ => {}
}
}
self.delta = if let (Some(old_pos), Some(new_pos)) = (old_pos, self.latest_pos) {
new_pos - old_pos
} else {
Vec2::zero()
};
if let Some(pos) = self.latest_pos {
self.pos_history.add(time, pos);
} else {
// we do not clear the `pos_history` here, because it is exactly when a finger has
// released from the touch screen that we may want to assign a velocity to whatever
// the user tried to throw
// the user tried to throw.
}
self.pos_history.flush(time);
let velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 {
self.velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 {
self.pos_history.velocity().unwrap_or_default()
} else {
Vec2::default()
};
CursorState {
down: new.mouse_down && new.mouse_pos.is_some(),
pressed,
released,
could_be_click,
click,
double_click,
last_click_time,
pos: new.mouse_pos,
press_origin,
delta,
velocity,
pos_history: self.pos_history,
}
self
}
fn wants_repaint(&self) -> bool {
!self.pointer_events.is_empty() || self.delta != Vec2::zero()
}
/// How much the pointer moved compared to last frame, in points.
pub fn delta(&self) -> Vec2 {
self.delta
}
/// Current velocity of pointer.
pub fn velocity(&self) -> Vec2 {
self.velocity
}
/// Where did the current click/drag originate?
/// `None` if no mouse button is down.
pub fn press_origin(&self) -> Option<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 {
self.velocity != Vec2::zero()
}
/// Was any pointer button pressed (`!down -> down`) this frame?
/// This can sometimes return `true` even if `any_down() == false`
/// because a press can be shorted than one frame.
pub fn any_pressed(&self) -> bool {
self.pointer_events.iter().any(|event| event.is_press())
}
/// Was any pointer button released (`down -> !down`) this frame?
pub fn any_released(&self) -> bool {
self.pointer_events.iter().any(|event| event.is_release())
}
/// Is any pointer button currently down?
pub fn any_down(&self) -> bool {
self.down.iter().any(|&down| down)
}
/// Were there any type of click this frame?
pub fn any_click(&self) -> bool {
self.pointer_events.iter().any(|event| event.is_click())
}
// /// Was this button pressed (`!down -> down`) this frame?
// /// This can sometimes return `true` even if `any_down() == false`
// /// because a press can be shorted than one frame.
// pub fn button_pressed(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_press())
// }
// /// Was this button released (`down -> !down`) this frame?
// pub fn button_released(&self, button: PointerButton) -> bool {
// self.pointer_events.iter().any(|event| event.is_release())
// }
/// Is this button currently down?
pub fn button_down(&self, button: PointerButton) -> bool {
self.down[button as usize]
}
}
impl InputState {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
raw,
mouse,
pointer,
scroll_delta,
screen_rect,
pixels_per_point,
@ -329,10 +491,10 @@ impl InputState {
ui.style_mut().body_text_style = crate::paint::TextStyle::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("🖱 Mouse")
crate::containers::CollapsingHeader::new("🖱 Pointer")
.default_open(true)
.show(ui, |ui| {
mouse.ui(ui);
pointer.ui(ui);
});
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
@ -354,36 +516,32 @@ impl InputState {
}
}
impl CursorState {
impl PointerState {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
down,
pressed,
released,
could_be_click,
click,
double_click,
last_click_time,
pos,
press_origin,
latest_pos,
interact_pos,
delta,
velocity,
pos_history: _,
down,
press_origin,
could_be_click,
last_click_time,
pointer_events,
} = self;
ui.label(format!("down: {}", down));
ui.label(format!("pressed: {}", pressed));
ui.label(format!("released: {}", released));
ui.label(format!("could_be_click: {}", could_be_click));
ui.label(format!("click: {}", click));
ui.label(format!("double_click: {}", double_click));
ui.label(format!("last_click_time: {:.3}", last_click_time));
ui.label(format!("pos: {:?}", pos));
ui.label(format!("press_origin: {:?}", press_origin));
ui.label(format!("latest_pos: {:?}", latest_pos));
ui.label(format!("interact_pos: {:?}", interact_pos));
ui.label(format!("delta: {:?}", delta));
ui.label(format!(
"velocity: [{:3.0} {:3.0}] points/sec",
velocity.x, velocity.y
));
ui.label(format!("down: {:#?}", down));
ui.label(format!("press_origin: {:?}", press_origin));
ui.label(format!("could_be_click: {:#?}", could_be_click));
ui.label(format!("last_click_time: {:#?}", last_click_time));
ui.label(format!("pointer_events: {:?}", pointer_events));
}
}

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 pointer_pos = response.interact_pointer_pos();
response.on_hover_ui(|ui| {
let pos = ui
.input()
.mouse
.pos
.unwrap_or_else(|| ui.min_rect().left_top());
let pos = pointer_pos.unwrap_or_else(|| ui.min_rect().left_top());
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);

View file

@ -125,7 +125,8 @@ pub(crate) struct Interaction {
}
impl Interaction {
pub fn is_using_mouse(&self) -> bool {
/// Are we currently clicking or dragging an egui widget?
pub fn is_using_pointer(&self) -> bool {
self.click_id.is_some() || self.drag_id.is_some()
}
@ -138,12 +139,8 @@ impl Interaction {
self.click_interest = false;
self.drag_interest = false;
if !prev_input.mouse.could_be_click {
self.click_id = None;
}
if !prev_input.mouse.down || prev_input.mouse.pos.is_none() {
// mouse was not down last frame
if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
// pointer button was not down last frame
self.click_id = None;
self.drag_id = None;
}
@ -175,7 +172,7 @@ impl Memory {
) {
self.interaction.begin_frame(prev_input, new_input);
if !prev_input.mouse.down {
if !prev_input.pointer.any_down() {
self.window_interaction = None;
}
}

View file

@ -7,7 +7,7 @@
//!
//! menu::bar(ui, |ui| {
//! menu::menu(ui, "File", |ui| {
//! if ui.button("Open").clicked {
//! if ui.button("Open").clicked() {
//! // ...
//! }
//! });
@ -83,14 +83,14 @@ fn menu_impl<'c>(
}
let button_response = ui.add(button);
if button_response.clicked {
if button_response.clicked() {
// Toggle
if bar_state.open_menu == Some(menu_id) {
bar_state.open_menu = None;
} else {
bar_state.open_menu = Some(menu_id);
}
} else if button_response.hovered && bar_state.open_menu.is_some() {
} else if button_response.hovered() && bar_state.open_menu.is_some() {
bar_state.open_menu = Some(menu_id);
}
@ -116,7 +116,8 @@ fn menu_impl<'c>(
});
// TODO: this prevents sub-menus in menus. We should fix that.
if ui.input().key_pressed(Key::Escape) || ui.input().mouse.click && !button_response.clicked
if ui.input().key_pressed(Key::Escape)
|| ui.input().pointer.any_click() && !button_response.clicked()
{
bar_state.open_menu = None;
}

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};
// ----------------------------------------------------------------------------
@ -27,35 +30,34 @@ pub struct Response {
pub sense: Sense,
// OUT:
/// The mouse is hovering above this.
pub hovered: bool,
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
pub(crate) hovered: bool,
/// The mouse clicked this thing this frame.
pub clicked: bool,
/// The pointer clicked this thing this frame.
pub(crate) clicked: [bool; NUM_POINTER_BUTTONS],
/// The thing was double-clicked.
pub double_clicked: bool,
pub(crate) double_clicked: [bool; NUM_POINTER_BUTTONS],
/// The mouse is interacting with this thing (e.g. dragging it).
pub active: bool,
/// The widgets is being dragged
pub(crate) dragged: bool,
/// The widget was being dragged, but now it has been released.
pub(crate) drag_released: bool,
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
pub(crate) is_pointer_button_down_on: bool,
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
pub(crate) interact_pointer_pos: Option<Pos2>,
/// This widget has the keyboard focus (i.e. is receiving key pressed).
pub has_kb_focus: bool,
pub(crate) has_kb_focus: bool,
/// The widget had keyboard focus and lost it,
/// perhaps because the user pressed enter.
/// If you want to do an action when a user presses enter in a text field,
/// use this.
///
/// ```
/// # let mut ui = egui::Ui::__test();
/// # let mut my_text = String::new();
/// # fn do_request(_: &str) {}
/// if ui.text_edit_singleline(&mut my_text).lost_kb_focus {
/// do_request(&my_text);
/// }
/// ```
pub lost_kb_focus: bool,
/// The widget had keyboard focus and lost it.
pub(crate) lost_kb_focus: bool,
}
impl std::fmt::Debug for Response {
@ -69,7 +71,10 @@ impl std::fmt::Debug for Response {
hovered,
clicked,
double_clicked,
active,
dragged,
drag_released,
is_pointer_button_down_on,
interact_pointer_pos,
has_kb_focus,
lost_kb_focus,
} = self;
@ -81,7 +86,10 @@ impl std::fmt::Debug for Response {
.field("hovered", hovered)
.field("clicked", clicked)
.field("double_clicked", double_clicked)
.field("active", active)
.field("dragged", dragged)
.field("drag_released", drag_released)
.field("is_pointer_button_down_on", is_pointer_button_down_on)
.field("interact_pointer_pos", interact_pointer_pos)
.field("has_kb_focus", has_kb_focus)
.field("lost_kb_focus", lost_kb_focus)
.finish()
@ -89,10 +97,89 @@ impl std::fmt::Debug for Response {
}
impl Response {
/// Returns true if this widget was clicked this frame by the primary button.
pub fn clicked(&self) -> bool {
self.clicked[PointerButton::Primary as usize]
}
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
pub fn secondary_clicked(&self) -> bool {
self.clicked[PointerButton::Secondary as usize]
}
/// Returns true if this widget was clicked this frame by the middle mouse button.
pub fn middle_clicked(&self) -> bool {
self.clicked[PointerButton::Middle as usize]
}
/// Returns true if this widget was double-clicked this frame by the primary button.
pub fn double_clicked(&self) -> bool {
self.double_clicked[PointerButton::Primary as usize]
}
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
pub fn hovered(&self) -> bool {
self.hovered
}
/// The widget had keyboard focus and lost it,
/// perhaps because the user pressed enter.
/// If you want to do an action when a user presses enter in a text field,
/// use this.
///
/// ```
/// # let mut ui = egui::Ui::__test();
/// # let mut my_text = String::new();
/// # fn do_request(_: &str) {}
/// if ui.text_edit_singleline(&mut my_text).lost_kb_focus() {
/// do_request(&my_text);
/// }
/// ```
pub fn lost_kb_focus(&self) -> bool {
self.lost_kb_focus
}
/// The widgets is being dragged.
///
/// To find out which button(s), query [`PointerState::button_down`]
/// (`ui.input().pointer.button_down(…)`).
pub fn dragged(&self) -> bool {
self.dragged
}
/// The widget was being dragged, but now it has been released.
pub fn drag_released(&self) -> bool {
self.drag_released
}
/// Returns true if this widget was clicked this frame by the given button.
pub fn clicked_by(&self, button: PointerButton) -> bool {
self.clicked[button as usize]
}
/// Returns true if this widget was double-clicked this frame by the given button.
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
self.double_clicked[button as usize]
}
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
pub fn interact_pointer_pos(&self) -> Option<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).
/// If you call this multiple times the tooltips will stack underneath the previous ones.
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.hovered || self.ctx.memory().everything_is_visible() {
if (self.hovered() && self.ctx.input().pointer.tooltip_pos().is_some())
|| self.ctx.memory().everything_is_visible()
{
crate::containers::show_tooltip(&self.ctx, add_contents);
}
self
@ -116,9 +203,9 @@ impl Response {
/// ```
/// # let mut ui = egui::Ui::__test();
/// let response = ui.label("hello");
/// assert!(!response.clicked); // labels don't sense clicks
/// assert!(!response.clicked()); // labels don't sense clicks
/// let response = response.interact(egui::Sense::click());
/// if response.clicked { /* … */ }
/// if response.clicked() { /* … */ }
/// ```
pub fn interact(&self, sense: Sense) -> Self {
self.ctx
@ -133,7 +220,7 @@ impl Response {
/// egui::ScrollArea::auto_sized().show(ui, |ui| {
/// for i in 0..1000 {
/// let response = ui.button(format!("Button {}", i));
/// if response.clicked {
/// if response.clicked() {
/// response.scroll_to_me(Align::Center);
/// }
/// }
@ -161,9 +248,21 @@ impl Response {
rect: self.rect.union(other.rect),
sense: self.sense.union(other.sense),
hovered: self.hovered || other.hovered,
clicked: self.clicked || other.clicked,
double_clicked: self.double_clicked || other.double_clicked,
active: self.active || other.active,
clicked: [
self.clicked[0] || other.clicked[0],
self.clicked[1] || other.clicked[1],
self.clicked[2] || other.clicked[2],
],
double_clicked: [
self.double_clicked[0] || other.double_clicked[0],
self.double_clicked[1] || other.double_clicked[1],
self.double_clicked[2] || other.double_clicked[2],
],
dragged: self.dragged || other.dragged,
drag_released: self.drag_released || other.drag_released,
is_pointer_button_down_on: self.is_pointer_button_down_on
|| other.is_pointer_button_down_on,
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
has_kb_focus: self.has_kb_focus || other.has_kb_focus,
lost_kb_focus: self.lost_kb_focus || other.lost_kb_focus,
}
@ -195,7 +294,7 @@ impl std::ops::BitOr for Response {
/// let mut response = ui.add(widget_a);
/// response |= ui.add(widget_b);
/// response |= ui.add(widget_c);
/// if response.active { ui.label("You are interacting with one of the widgets"); }
/// if response.hovered() { ui.label("You hovered at least one of the widgets"); }
/// ```
impl std::ops::BitOrAssign for Response {
fn bitor_assign(&mut self, rhs: Self) {

View file

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

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

View file

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

View file

@ -94,11 +94,9 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if response.active {
if let Some(mpos) = ui.input().mouse.pos {
if let Some(mpos) = response.interact_pointer_pos() {
*value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
}
}
let visuals = ui.style().interact(&response);
@ -151,12 +149,10 @@ fn color_slider_2d(
let desired_size = Vec2::splat(ui.style().spacing.slider_width);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if response.active {
if let Some(mpos) = ui.input().mouse.pos {
if let Some(mpos) = response.interact_pointer_pos() {
*x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
*y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
}
}
let visuals = ui.style().interact(&response);
let mut triangles = Triangles::default();
@ -217,7 +213,7 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>) {
r, g, b, a
));
if ui.button("📋").on_hover_text("Click to copy").clicked {
if ui.button("📋").on_hover_text("Click to copy").clicked() {
ui.output().copied_text = format!("rgba({}, {}, {}, {})", r, g, b, a);
}
});
@ -323,7 +319,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
let pupup_id = ui.auto_id_with("popup");
let button_response = color_button(ui, (*hsva).into()).on_hover_text("Click to edit color");
if button_response.clicked {
if button_response.clicked() {
ui.memory().toggle_popup(pupup_id);
}
// TODO: make it easier to show a temporary popup that closes when you click outside it
@ -338,8 +334,8 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
})
});
if !button_response.clicked {
let clicked_outside = ui.input().mouse.click && !area_response.hovered;
if !button_response.clicked() {
let clicked_outside = ui.input().pointer.any_pressed() && !area_response.hovered();
if clicked_outside || ui.input().key_pressed(Key::Escape) {
ui.memory().close_popup();
}

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
suffix
));
if response.clicked {
if response.clicked() {
ui.memory().request_kb_focus(kb_edit_id);
ui.memory().temp_edit_string = None; // Filled in next frame
} else if response.active {
let mdelta = ui.input().mouse.delta;
} else if response.dragged() {
let mdelta = ui.input().pointer.delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
let delta_value = speed * delta_points;
if delta_value != 0.0 {

View file

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

View file

@ -2,7 +2,7 @@
//!
//! Example widget uses:
//! * `ui.add(Label::new("Text").text_color(color::red));`
//! * `if ui.add(Button::new("Click me")).clicked { ... }`
//! * `if ui.add(Button::new("Click me")).clicked() { ... }`
#![allow(clippy::new_without_default)]
@ -40,7 +40,10 @@ pub trait Widget {
/// The button is only enabled if the value does not already have its original value.
pub fn reset_button<T: Default + PartialEq>(ui: &mut Ui, value: &mut T) {
let def = T::default();
if ui.add(Button::new("Reset").enabled(*value != def)).clicked {
if ui
.add(Button::new("Reset").enabled(*value != def))
.clicked()
{
*value = def;
}
}

View file

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

View file

@ -252,20 +252,18 @@ impl<'a> Slider<'a> {
let rect = &response.rect;
let x_range = x_range(rect);
if let Some(mouse_pos) = ui.input().mouse.pos {
if response.active {
if let Some(pointer_pos) = response.interact_pointer_pos() {
let new_value = if self.smart_aim {
let aim_radius = ui.input().aim_radius();
crate::math::smart_aim::best_in_range_f64(
self.value_from_x(mouse_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(pointer_pos.x + aim_radius, x_range.clone()),
)
} else {
self.value_from_x(mouse_pos.x, x_range.clone())
self.value_from_x(pointer_pos.x, x_range.clone())
};
self.set_value(new_value);
}
}
// Paint it:
{
@ -350,7 +348,7 @@ impl<'a> Slider<'a> {
self.get_value() as f32 // Show full precision value on-hover. TODO: figure out f64 vs f32
));
// let response = ui.interact(response.rect, kb_edit_id, Sense::click());
if response.clicked {
if response.clicked() {
ui.memory().request_kb_focus(kb_edit_id);
ui.memory().temp_edit_string = None; // Filled in next frame
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,7 +73,7 @@ impl Sliders {
You can always see the full precision value by hovering the value.",
);
if ui.button("Assign PI").clicked {
if ui.button("Assign PI").clicked() {
self.value = std::f64::consts::PI;
}
}

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added ⭐
* Right-clicks will no longer open browser context menu.
### Fixed ⭐
* Fix a bug where one couldn't select items in a combo box on a touch screen.
## 0.8.0 - 2021-01-17

View file

@ -81,6 +81,9 @@ pub struct WebInput {
/// Is this a touch screen? If so, we ignore mouse events.
pub is_touch: bool,
/// Required because we don't get a position on touched
pub latest_touch_pos: Option<egui::Pos2>,
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 {
let t = event.touches().get(0).unwrap();
egui::Pos2 {
@ -599,20 +608,41 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_ref.0.lock().canvas_id()).unwrap();
{
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let event_name = "mousedown";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos =
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
runner_lock.input.raw.mouse_down = true;
runner_lock.logic().unwrap(); // in case we get "mouseup" the same frame. TODO: handle via events instead
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
@ -624,8 +654,12 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos =
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@ -641,13 +675,24 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos =
Some(pos_from_mouse_event(runner_lock.canvas_id(), &event));
runner_lock.input.raw.mouse_down = false;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
@ -659,7 +704,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let mut runner_lock = runner_ref.0.lock();
if !runner_lock.input.is_touch {
runner_lock.input.raw.mouse_pos = None;
runner_lock.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@ -673,10 +718,21 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let event_name = "touchstart";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let pos = pos_from_touch_event(&event);
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock.input.is_touch = true;
runner_lock.input.raw.mouse_pos = Some(pos_from_touch_event(&event));
runner_lock.input.raw.mouse_down = true;
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@ -689,9 +745,15 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let event_name = "touchmove";
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let pos = pos_from_touch_event(&event);
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock.input.is_touch = true;
runner_lock.input.raw.mouse_pos = Some(pos_from_touch_event(&event));
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
@ -706,12 +768,25 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock();
runner_lock.input.is_touch = true;
runner_lock.input.raw.mouse_down = false; // First release mouse to click...
runner_lock.logic().unwrap(); // ...do the clicking... (TODO: handle via events instead)
runner_lock.input.raw.mouse_pos = None; // ...remove hover effect
if let Some(pos) = runner_lock.input.latest_touch_pos {
let modifiers = runner_lock.input.raw.modifiers;
// First release mouse to click:
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
});
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();

View file

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