1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05:40:03 -04:00
This commit is contained in:
obellish
2024-03-12 09:36:59 -04:00
60 changed files with 1487 additions and 345 deletions

View File

@@ -44,7 +44,7 @@ Please keep pull requests small and focused. The smaller it is, the more likely
Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs! Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs!
It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpituity, so we have a high bar on what get merged! It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged!
When reviewing, we look for: When reviewing, we look for:
* The PR title and description should be helpful * The PR title and description should be helpful
@@ -123,7 +123,7 @@ with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards.
`egui` uses logical _points_ as its coordinate system. `egui` uses logical _points_ as its coordinate system.
Those related to physical _pixels_ by the `pixels_per_point` scale factor. Those related to physical _pixels_ by the `pixels_per_point` scale factor.
For example, a high-dpi screeen can have `pixels_per_point = 2.0`, For example, a high-dpi screen can have `pixels_per_point = 2.0`,
meaning there are two physical screen pixels for each logical point. meaning there are two physical screen pixels for each logical point.
Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. Angles are in radians, and are measured clockwise from the X-axis, which has angle=0.

9
Cargo.lock generated
View File

@@ -1072,6 +1072,15 @@ dependencies = [
"env_logger", "env_logger",
] ]
[[package]]
name = "custom_keypad"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]] [[package]]
name = "custom_plot_manipulation" name = "custom_plot_manipulation"
version = "0.1.0" version = "0.1.0"

View File

@@ -239,4 +239,8 @@ web-sys = { version = "0.3.58", features = [
# optional web: # optional web:
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
wgpu = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends
"webgpu",
] }

View File

@@ -196,6 +196,24 @@ pub trait App {
fn persist_egui_memory(&self) -> bool { fn persist_egui_memory(&self) -> bool {
true true
} }
/// A hook for manipulating or filtering raw input before it is processed by [`Self::update`].
///
/// This function provides a way to modify or filter input events before they are processed by egui.
///
/// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui.
///
/// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard.
///
/// # Arguments
///
/// * `_ctx` - The context of the egui, which provides access to the current state of the egui.
/// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes.
///
/// # Note
///
/// This function does not return a value. Any changes to the input should be made directly to `_raw_input`.
fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {}
} }
/// Selects the level of hardware graphics acceleration. /// Selects the level of hardware graphics acceleration.

View File

@@ -274,6 +274,8 @@ impl EpiIntegration {
let close_requested = raw_input.viewport().close_requested(); let close_requested = raw_input.viewport().close_requested();
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
if let Some(viewport_ui_cb) = viewport_ui_cb { if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport // Child viewport

View File

@@ -446,6 +446,33 @@ impl WinitApp for GlowWinitApp {
} }
} }
winit::event::Event::DeviceEvent {
device_id: _,
event: winit::event::DeviceEvent::MouseMotion { delta },
} => {
if let Some(running) = &mut self.running {
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(*delta);
}
if let Some(window) = viewport.window.as_ref() {
EventResult::RepaintNext(window.id())
} else {
EventResult::Wait
}
} else {
EventResult::Wait
}
} else {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, window_id }, accesskit_winit::ActionRequestEvent { request, window_id },

View File

@@ -456,6 +456,33 @@ impl WinitApp for WgpuWinitApp {
} }
} }
winit::event::Event::DeviceEvent {
device_id: _,
event: winit::event::DeviceEvent::MouseMotion { delta },
} => {
if let Some(running) = &mut self.running {
let mut shared = running.shared.borrow_mut();
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(*delta);
}
if let Some(window) = viewport.window.as_ref() {
EventResult::RepaintNext(window.id())
} else {
EventResult::Wait
}
} else {
EventResult::Wait
}
} else {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, window_id }, accesskit_winit::ActionRequestEvent { request, window_id },

View File

@@ -87,7 +87,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa
return; return;
} }
let modifiers = modifiers_from_event(&event); let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers; runner.input.raw.modifiers = modifiers;
let key = event.key(); let key = event.key();
@@ -158,7 +158,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa
&document, &document,
"keyup", "keyup",
|event: web_sys::KeyboardEvent, runner| { |event: web_sys::KeyboardEvent, runner| {
let modifiers = modifiers_from_event(&event); let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers; runner.input.raw.modifiers = modifiers;
if let Some(key) = translate_key(&event.key()) { if let Some(key) = translate_key(&event.key()) {
runner.input.raw.events.push(egui::Event::Key { runner.input.raw.events.push(egui::Event::Key {
@@ -301,6 +301,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
&canvas, &canvas,
"mousedown", "mousedown",
|event: web_sys::MouseEvent, runner: &mut AppRunner| { |event: web_sys::MouseEvent, runner: &mut AppRunner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
if let Some(button) = button_from_mouse_event(&event) { if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas_id(), &event); let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let modifiers = runner.input.raw.modifiers; let modifiers = runner.input.raw.modifiers;
@@ -327,6 +329,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
&canvas, &canvas,
"mousemove", "mousemove",
|event: web_sys::MouseEvent, runner| { |event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
let pos = pos_from_mouse_event(runner.canvas_id(), &event); let pos = pos_from_mouse_event(runner.canvas_id(), &event);
runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.input.raw.events.push(egui::Event::PointerMoved(pos));
runner.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
@@ -336,6 +340,8 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
)?; )?;
runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
if let Some(button) = button_from_mouse_event(&event) { if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas_id(), &event); let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let modifiers = runner.input.raw.modifiers; let modifiers = runner.input.raw.modifiers;
@@ -474,7 +480,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
// This if-statement is equivalent to how `Modifiers.command` is determined in // This if-statement is equivalent to how `Modifiers.command` is determined in
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. // `modifiers_from_kb_event()`, but we cannot directly use that fn for a [`WheelEvent`].
if event.ctrl_key() || event.meta_key() { if event.ctrl_key() || event.meta_key() {
let factor = (delta.y / 200.0).exp(); let factor = (delta.y / 200.0).exp();
runner.input.raw.events.push(egui::Event::Zoom(factor)); runner.input.raw.events.push(egui::Event::Zoom(factor));

View File

@@ -115,7 +115,23 @@ pub fn translate_key(key: &str) -> Option<egui::Key> {
egui::Key::from_name(key) egui::Key::from_name(key)
} }
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { pub fn modifiers_from_kb_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
egui::Modifiers {
alt: event.alt_key(),
ctrl: event.ctrl_key(),
shift: event.shift_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
mac_cmd: event.meta_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
command: event.ctrl_key() || event.meta_key(),
}
}
pub fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers {
egui::Modifiers { egui::Modifiers {
alt: event.alt_key(), alt: event.alt_key(),
ctrl: event.ctrl_key(), ctrl: event.ctrl_key(),

View File

@@ -475,6 +475,13 @@ impl State {
} }
} }
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) {
self.egui_input.events.push(egui::Event::MouseMoved(Vec2 {
x: delta.0 as f32,
y: delta.1 as f32,
}));
}
/// Call this when there is a new [`accesskit::ActionRequest`]. /// Call this when there is a new [`accesskit::ActionRequest`].
/// ///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
@@ -808,12 +815,14 @@ impl State {
let allow_ime = ime.is_some(); let allow_ime = ime.is_some();
if self.allow_ime != allow_ime { if self.allow_ime != allow_ime {
self.allow_ime = allow_ime; self.allow_ime = allow_ime;
crate::profile_scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime); window.set_ime_allowed(allow_ime);
} }
if let Some(ime) = ime { if let Some(ime) = ime {
let rect = ime.rect; let rect = ime.rect;
let pixels_per_point = pixels_per_point(&self.egui_ctx, window); let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
crate::profile_scope!("set_ime_cursor_area");
window.set_ime_cursor_area( window.set_ime_cursor_area(
winit::dpi::PhysicalPosition { winit::dpi::PhysicalPosition {
x: pixels_per_point * rect.min.x, x: pixels_per_point * rect.min.x,
@@ -829,6 +838,7 @@ impl State {
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_ref() { if let Some(accesskit) = self.accesskit.as_ref() {
if let Some(update) = accesskit_update { if let Some(update) = accesskit_update {
crate::profile_scope!("accesskit");
accesskit.update_if_active(|| update); accesskit.update_if_active(|| update);
} }
} }

View File

@@ -20,10 +20,6 @@ pub(crate) struct State {
/// If false, clicks goes straight through to what is behind us. /// If false, clicks goes straight through to what is behind us.
/// Good for tooltips etc. /// Good for tooltips etc.
pub interactable: bool, pub interactable: bool,
/// When `true`, this `Area` belongs to a resizable window, so it needs to
/// receive mouse input which occurs a short distance beyond its bounding rect.
pub edges_padded_for_resize: bool,
} }
impl State { impl State {
@@ -52,7 +48,7 @@ impl State {
/// ///
/// ``` /// ```
/// # egui::__run_test_ctx(|ctx| { /// # egui::__run_test_ctx(|ctx| {
/// egui::Area::new("my_area") /// egui::Area::new(egui::Id::new("my_area"))
/// .fixed_pos(egui::pos2(32.0, 32.0)) /// .fixed_pos(egui::pos2(32.0, 32.0))
/// .show(ctx, |ui| { /// .show(ctx, |ui| {
/// ui.label("Floating text!"); /// ui.label("Floating text!");
@@ -75,13 +71,13 @@ pub struct Area {
pivot: Align2, pivot: Align2,
anchor: Option<(Align2, Vec2)>, anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>, new_pos: Option<Pos2>,
edges_padded_for_resize: bool,
} }
impl Area { impl Area {
pub fn new(id: impl Into<Id>) -> Self { /// The `id` must be globally unique.
pub fn new(id: Id) -> Self {
Self { Self {
id: id.into(), id,
movable: true, movable: true,
interactable: true, interactable: true,
constrain: false, constrain: false,
@@ -92,10 +88,12 @@ impl Area {
new_pos: None, new_pos: None,
pivot: Align2::LEFT_TOP, pivot: Align2::LEFT_TOP,
anchor: None, anchor: None,
edges_padded_for_resize: false,
} }
} }
/// Let's you change the `id` that you assigned in [`Self::new`].
///
/// The `id` must be globally unique.
#[inline] #[inline]
pub fn id(mut self, id: Id) -> Self { pub fn id(mut self, id: Id) -> Self {
self.id = id; self.id = id;
@@ -223,14 +221,6 @@ impl Area {
Align2::LEFT_TOP Align2::LEFT_TOP
} }
} }
/// When `true`, this `Area` belongs to a resizable window, so it needs to
/// receive mouse input which occurs a short distance beyond its bounding rect.
#[inline]
pub(crate) fn edges_padded_for_resize(mut self, edges_padded_for_resize: bool) -> Self {
self.edges_padded_for_resize = edges_padded_for_resize;
self
}
} }
pub(crate) struct Prepared { pub(crate) struct Prepared {
@@ -275,7 +265,6 @@ impl Area {
anchor, anchor,
constrain, constrain,
constrain_rect, constrain_rect,
edges_padded_for_resize,
} = self; } = self;
let layer_id = LayerId::new(order, id); let layer_id = LayerId::new(order, id);
@@ -296,11 +285,9 @@ impl Area {
pivot, pivot,
size: Vec2::ZERO, size: Vec2::ZERO,
interactable, interactable,
edges_padded_for_resize,
}); });
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos); state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.interactable = interactable; state.interactable = interactable;
state.edges_padded_for_resize = edges_padded_for_resize;
if let Some((anchor, offset)) = anchor { if let Some((anchor, offset)) = anchor {
let screen = ctx.available_rect(); let screen = ctx.available_rect();

View File

@@ -34,7 +34,7 @@ pub struct Resize {
id_source: Option<Id>, id_source: Option<Id>,
/// If false, we are no enabled /// If false, we are no enabled
resizable: bool, resizable: Vec2b,
pub(crate) min_size: Vec2, pub(crate) min_size: Vec2,
pub(crate) max_size: Vec2, pub(crate) max_size: Vec2,
@@ -49,7 +49,7 @@ impl Default for Resize {
Self { Self {
id: None, id: None,
id_source: None, id_source: None,
resizable: true, resizable: Vec2b::TRUE,
min_size: Vec2::splat(16.0), min_size: Vec2::splat(16.0),
max_size: Vec2::splat(f32::INFINITY), max_size: Vec2::splat(f32::INFINITY),
default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area. default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area.
@@ -152,12 +152,13 @@ impl Resize {
/// ///
/// Default is `true`. /// Default is `true`.
#[inline] #[inline]
pub fn resizable(mut self, resizable: bool) -> Self { pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
self.resizable = resizable; self.resizable = resizable.into();
self self
} }
pub fn is_resizable(&self) -> bool { #[inline]
pub fn is_resizable(&self) -> Vec2b {
self.resizable self.resizable
} }
@@ -175,7 +176,7 @@ impl Resize {
self.default_size = size; self.default_size = size;
self.min_size = size; self.min_size = size;
self.max_size = size; self.max_size = size;
self.resizable = false; self.resizable = Vec2b::FALSE;
self self
} }
@@ -226,7 +227,7 @@ impl Resize {
let mut user_requested_size = state.requested_size.take(); let mut user_requested_size = state.requested_size.take();
let corner_id = self.resizable.then(|| id.with("__resize_corner")); let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
if let Some(corner_id) = corner_id { if let Some(corner_id) = corner_id {
if let Some(corner_response) = ui.ctx().read_response(corner_id) { if let Some(corner_response) = ui.ctx().read_response(corner_id) {
@@ -299,18 +300,21 @@ impl Resize {
// ------------------------------ // ------------------------------
let size = if self.with_stroke || self.resizable { let mut size = state.last_content_size;
// We show how large we are, for d in 0..2 {
// so we must follow the contents: if self.with_stroke || self.resizable[d] {
// We show how large we are,
// so we must follow the contents:
state.desired_size = state.desired_size.max(state.last_content_size); state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]);
// We are as large as we look // We are as large as we look
state.desired_size size[d] = state.desired_size[d];
} else { } else {
// Probably a window. // Probably a window.
state.last_content_size size[d] = state.last_content_size[d];
}; }
}
ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size));
// ------------------------------ // ------------------------------
@@ -369,19 +373,22 @@ use epaint::Stroke;
pub fn paint_resize_corner(ui: &Ui, response: &Response) { pub fn paint_resize_corner(ui: &Ui, response: &Response) {
let stroke = ui.style().interact(response).fg_stroke; let stroke = ui.style().interact(response).fg_stroke;
paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM); paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM);
} }
pub fn paint_resize_corner_with_style( pub fn paint_resize_corner_with_style(
ui: &Ui, ui: &Ui,
rect: &Rect, rect: &Rect,
stroke: impl Into<Stroke>, color: impl Into<Color32>,
corner: Align2, corner: Align2,
) { ) {
let painter = ui.painter(); let painter = ui.painter();
let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect)); let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect));
let mut w = 2.0; let mut w = 2.0;
let stroke = stroke.into(); let stroke = Stroke {
width: 1.0, // Set width to 1.0 to prevent overlapping
color: color.into(),
};
while w <= rect.width() && w <= rect.height() { while w <= rect.width() && w <= rect.height() {
painter.line_segment( painter.line_segment(

View File

@@ -2,6 +2,13 @@
use crate::*; use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct ScrollTarget {
animation_time_span: (f64, f64),
target_offset: f32,
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
@@ -9,6 +16,9 @@ pub struct State {
/// Positive offset means scrolling down/right /// Positive offset means scrolling down/right
pub offset: Vec2, pub offset: Vec2,
/// If set, quickly but smoothly scroll to this target offset.
offset_target: [Option<ScrollTarget>; 2],
/// Were the scroll bars visible last frame? /// Were the scroll bars visible last frame?
show_scroll: Vec2b, show_scroll: Vec2b,
@@ -35,6 +45,7 @@ impl Default for State {
fn default() -> Self { fn default() -> Self {
Self { Self {
offset: Vec2::ZERO, offset: Vec2::ZERO,
offset_target: Default::default(),
show_scroll: Vec2b::FALSE, show_scroll: Vec2b::FALSE,
content_is_too_large: Vec2b::FALSE, content_is_too_large: Vec2b::FALSE,
scroll_bar_interaction: Vec2b::FALSE, scroll_bar_interaction: Vec2b::FALSE,
@@ -559,25 +570,56 @@ impl ScrollArea {
state.vel[d] = input.pointer.velocity()[d]; state.vel[d] = input.pointer.velocity()[d];
}); });
state.scroll_stuck_to_end[d] = false; state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
} else { } else {
state.vel[d] = 0.0; state.vel[d] = 0.0;
} }
} }
} else { } else {
// Kinetic scrolling for d in 0..2 {
let stop_speed = 20.0; // Pixels per second. let dt = ui.input(|i| i.stable_dt).at_most(0.1);
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input(|i| i.unstable_dt);
let friction = friction_coeff * dt; if let Some(scroll_target) = state.offset_target[d] {
if friction > state.vel.length() || state.vel.length() < stop_speed { state.vel[d] = 0.0;
state.vel = Vec2::ZERO;
} else { if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
state.vel -= friction * state.vel.normalized(); // Arrived
// Offset has an inverted coordinate system compared to state.offset[d] = scroll_target.target_offset;
// the velocity, so we subtract it instead of adding it state.offset_target[d] = None;
state.offset -= state.vel * dt; } else {
ctx.request_repaint(); // Move towards target
let t = emath::interpolation_factor(
scroll_target.animation_time_span,
ui.input(|i| i.time),
dt,
emath::ease_in_ease_out,
);
if t < 1.0 {
state.offset[d] =
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
ctx.request_repaint();
} else {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
}
}
} else {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
} }
} }
} }
@@ -716,11 +758,11 @@ impl Prepared {
let scroll_target = content_ui let scroll_target = content_ui
.ctx() .ctx()
.frame_state_mut(|state| state.scroll_target[d].take()); .frame_state_mut(|state| state.scroll_target[d].take());
if let Some((scroll, align)) = scroll_target { if let Some((target_range, align)) = scroll_target {
let min = content_ui.min_rect().min[d]; let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect(); let clip_rect = content_ui.clip_rect();
let visible_range = min..=min + clip_rect.size()[d]; let visible_range = min..=min + clip_rect.size()[d];
let (start, end) = (scroll.min, scroll.max); let (start, end) = (target_range.min, target_range.max);
let clip_start = clip_rect.min[d]; let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d]; let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d]; let mut spacing = ui.spacing().item_spacing[d];
@@ -729,7 +771,7 @@ impl Prepared {
let center_factor = align.to_factor(); let center_factor = align.to_factor();
let offset = let offset =
lerp(scroll, center_factor) - lerp(visible_range, center_factor); lerp(target_range, center_factor) - lerp(visible_range, center_factor);
// Depending on the alignment we need to add or subtract the spacing // Depending on the alignment we need to add or subtract the spacing
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
@@ -745,7 +787,24 @@ impl Prepared {
}; };
if delta != 0.0 { if delta != 0.0 {
state.offset[d] += delta; let target_offset = state.offset[d] + delta;
if let Some(animation) = &mut state.offset_target[d] {
// For instance: the user is continuously calling `ui.scroll_to_cursor`,
// so we don't want to reset the animation, but perhaps update the target:
animation.target_offset = target_offset;
} else {
// The further we scroll, the more time we take.
// TODO(emilk): let users configure this in `Style`.
let now = ui.input(|i| i.time);
let points_per_second = 1000.0;
let animation_duration =
(delta.abs() / points_per_second).clamp(0.1, 0.3);
state.offset_target[d] = Some(ScrollTarget {
animation_time_span: (now, now + animation_duration as f64),
target_offset,
});
}
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
} }
@@ -808,6 +867,7 @@ impl Prepared {
}); });
state.scroll_stuck_to_end[d] = false; state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
} }
} }
} }
@@ -952,6 +1012,7 @@ impl Prepared {
// some manual action taken, scroll not stuck // some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false; state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
} else { } else {
state.scroll_start_offset_from_top_left[d] = None; state.scroll_start_offset_from_top_left[d] = None;
} }

View File

@@ -48,9 +48,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id. /// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self { pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading); let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text())) let area = Area::new(Id::new(title.text())).constrain(true);
.constrain(true)
.edges_padded_for_resize(true);
Self { Self {
title, title,
open: None, open: None,
@@ -119,9 +117,6 @@ impl<'open> Window<'open> {
#[inline] #[inline]
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self { pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
self.resize = mutate(self.resize); self.resize = mutate(self.resize);
self.area = self
.area
.edges_padded_for_resize(self.resize.is_resizable());
self self
} }
@@ -278,7 +273,6 @@ impl<'open> Window<'open> {
#[inline] #[inline]
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self { pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size); self.resize = self.resize.fixed_size(size);
self.area = self.area.edges_padded_for_resize(false);
self self
} }
@@ -296,11 +290,15 @@ impl<'open> Window<'open> {
/// ///
/// Note that even if you set this to `false` the window may still auto-resize. /// Note that even if you set this to `false` the window may still auto-resize.
/// ///
/// You can set the window to only be resizable in one direction by using
/// e.g. `[true, false]` as the argument,
/// making the window only resizable in the x-direction.
///
/// Default is `true`. /// Default is `true`.
#[inline] #[inline]
pub fn resizable(mut self, resizable: bool) -> Self { pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
let resizable = resizable.into();
self.resize = self.resize.resizable(resizable); self.resize = self.resize.resizable(resizable);
self.area = self.area.edges_padded_for_resize(resizable);
self self
} }
@@ -326,7 +324,6 @@ impl<'open> Window<'open> {
pub fn auto_sized(mut self) -> Self { pub fn auto_sized(mut self) -> Self {
self.resize = self.resize.auto_sized(); self.resize = self.resize.auto_sized();
self.scroll = ScrollArea::neither(); self.scroll = ScrollArea::neither();
self.area = self.area.edges_padded_for_resize(false);
self self
} }
@@ -392,7 +389,12 @@ impl<'open> Window<'open> {
let header_color = let header_color =
frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
let window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
// Keep the original inner margin for later use
let window_margin = window_frame.inner_margin;
let border_padding = window_frame.stroke.width / 2.0;
// Add border padding to the inner margin to prevent it from covering the contents
window_frame.inner_margin += border_padding;
let is_explicitly_closed = matches!(open, Some(false)); let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
@@ -420,9 +422,10 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect // Calculate roughly how much larger the window size is compared to the inner rect
let (title_bar_height, title_content_spacing) = if with_title_bar { let (title_bar_height, title_content_spacing) = if with_title_bar {
let style = ctx.style(); let style = ctx.style();
let window_margin = window_frame.inner_margin;
let spacing = window_margin.top + window_margin.bottom; let spacing = window_margin.top + window_margin.bottom;
let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing; let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing;
window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0);
window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0);
(height, spacing) (height, spacing)
} else { } else {
(0.0, 0.0) (0.0, 0.0)
@@ -495,21 +498,33 @@ impl<'open> Window<'open> {
.map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
let outer_rect = frame.end(&mut area_content_ui).rect; let outer_rect = frame.end(&mut area_content_ui).rect;
paint_resize_corner(&area_content_ui, &possible, outer_rect, frame_stroke); paint_resize_corner(
&area_content_ui,
&possible,
outer_rect,
frame_stroke,
window_frame.rounding,
);
// END FRAME -------------------------------- // END FRAME --------------------------------
if let Some(title_bar) = title_bar { if let Some(title_bar) = title_bar {
if on_top && area_content_ui.visuals().window_highlight_topmost { let mut title_rect = Rect::from_min_size(
let rect = Rect::from_min_size( outer_rect.min + vec2(border_padding, border_padding),
outer_rect.min, Vec2 {
Vec2 { x: outer_rect.size().x - border_padding * 2.0,
x: outer_rect.size().x, y: title_bar_height,
y: title_bar_height, },
}, );
);
title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect);
if on_top && area_content_ui.visuals().window_highlight_topmost {
let mut round = window_frame.rounding; let mut round = window_frame.rounding;
// Eliminate the rounding gap between the title bar and the window frame
round -= border_padding;
if !is_collapsed { if !is_collapsed {
round.se = 0.0; round.se = 0.0;
round.sw = 0.0; round.sw = 0.0;
@@ -517,18 +532,18 @@ impl<'open> Window<'open> {
area_content_ui.painter().set( area_content_ui.painter().set(
*where_to_put_header_background, *where_to_put_header_background,
RectShape::filled(rect, round, header_color), RectShape::filled(title_rect, round, header_color),
); );
}; };
// Fix title bar separator line position // Fix title bar separator line position
if let Some(response) = &mut content_response { if let Some(response) = &mut content_response {
response.rect.min.y = outer_rect.min.y + title_bar_height; response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding;
} }
title_bar.ui( title_bar.ui(
&mut area_content_ui, &mut area_content_ui,
outer_rect, title_rect,
&content_response, &content_response,
open, open,
&mut collapsing, &mut collapsing,
@@ -558,23 +573,47 @@ fn paint_resize_corner(
possible: &PossibleInteractions, possible: &PossibleInteractions,
outer_rect: Rect, outer_rect: Rect,
stroke: impl Into<Stroke>, stroke: impl Into<Stroke>,
rounding: impl Into<Rounding>,
) { ) {
let corner = if possible.resize_right && possible.resize_bottom { let stroke = stroke.into();
Align2::RIGHT_BOTTOM let rounding = rounding.into();
let (corner, radius) = if possible.resize_right && possible.resize_bottom {
(Align2::RIGHT_BOTTOM, rounding.se)
} else if possible.resize_left && possible.resize_bottom { } else if possible.resize_left && possible.resize_bottom {
Align2::LEFT_BOTTOM (Align2::LEFT_BOTTOM, rounding.sw)
} else if possible.resize_left && possible.resize_top { } else if possible.resize_left && possible.resize_top {
Align2::LEFT_TOP (Align2::LEFT_TOP, rounding.nw)
} else if possible.resize_right && possible.resize_top { } else if possible.resize_right && possible.resize_top {
Align2::RIGHT_TOP (Align2::RIGHT_TOP, rounding.ne)
} else { } else {
return; // We're not in two directions, but it is still nice to tell the user
// we're resizable by painting the resize corner in the expected place
// (i.e. for windows only resizable in one direction):
if possible.resize_right || possible.resize_bottom {
(Align2::RIGHT_BOTTOM, rounding.se)
} else if possible.resize_left || possible.resize_bottom {
(Align2::LEFT_BOTTOM, rounding.sw)
} else if possible.resize_left || possible.resize_top {
(Align2::LEFT_TOP, rounding.nw)
} else if possible.resize_right || possible.resize_top {
(Align2::RIGHT_TOP, rounding.ne)
} else {
return;
}
}; };
// Adjust the corner offset to accommodate the stroke width and window rounding
let offset = if radius <= 2.0 && stroke.width < 2.0 {
2.0
} else {
// The corner offset is calculated to make the corner appear to be in the correct position
(2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius)
* 45.0_f32.to_radians().cos()
};
let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect);
let corner_rect = corner_rect.translate(-2.0 * corner.to_sign()); // move away from corner let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner
crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke, corner); crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner);
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -592,13 +631,15 @@ struct PossibleInteractions {
impl PossibleInteractions { impl PossibleInteractions {
fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self { fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
let movable = area.is_enabled() && area.is_movable(); let movable = area.is_enabled() && area.is_movable();
let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; let resizable = resize
.is_resizable()
.and(area.is_enabled() && !is_collapsed);
let pivot = area.get_pivot(); let pivot = area.get_pivot();
Self { Self {
resize_left: resizable && (movable || pivot.x() != Align::LEFT), resize_left: resizable.x && (movable || pivot.x() != Align::LEFT),
resize_right: resizable && (movable || pivot.x() != Align::RIGHT), resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT),
resize_top: resizable && (movable || pivot.y() != Align::TOP), resize_top: resizable.y && (movable || pivot.y() != Align::TOP),
resize_bottom: resizable && (movable || pivot.y() != Align::BOTTOM), resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM),
} }
} }
@@ -1036,7 +1077,10 @@ impl TitleBar {
let y = content_response.rect.top(); let y = content_response.rect.top();
// let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5); // let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5);
let stroke = ui.visuals().widgets.noninteractive.bg_stroke; let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
ui.painter().hline(outer_rect.x_range(), y, stroke); // Workaround: To prevent border infringement,
// the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels
let x_range = outer_rect.x_range().shrink(0.1);
ui.painter().hline(x_range, y, stroke);
} }
// Don't cover the close- and collapse buttons: // Don't cover the close- and collapse buttons:

View File

@@ -198,100 +198,6 @@ impl ContextImpl {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Used to store each widget's [Id], [Rect] and [Sense] each frame.
/// Used to check for overlaps between widgets when handling events.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WidgetRect {
/// The globally unique widget id.
///
/// For interactive widgets, this better be globally unique.
/// If not there will be weird bugs,
/// and also big red warning test on the screen in debug builds
/// (see [`Options::warn_on_id_clash`]).
///
/// You can ensure globally unique ids using [`Ui::push_id`].
pub id: Id,
/// What layer the widget is on.
pub layer_id: LayerId,
/// The full widget rectangle.
pub rect: Rect,
/// Where the widget is.
///
/// This is after clipping with the parent ui clip rect.
pub interact_rect: Rect,
/// How the widget responds to interaction.
pub sense: Sense,
/// Is the widget enabled?
pub enabled: bool,
}
/// Stores the positions of all widgets generated during a single egui update/frame.
///
/// Actually, only those that are on screen.
#[derive(Default, Clone, PartialEq, Eq)]
pub struct WidgetRects {
/// All widgets, in painting order.
pub by_layer: HashMap<LayerId, Vec<WidgetRect>>,
/// All widgets
pub by_id: IdMap<WidgetRect>,
}
impl WidgetRects {
/// Clear the contents while retaining allocated memory.
pub fn clear(&mut self) {
let Self { by_layer, by_id } = self;
for rects in by_layer.values_mut() {
rects.clear();
}
by_id.clear();
}
/// Insert the given widget rect in the given layer.
pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) {
if !widget_rect.interact_rect.is_positive() {
return;
}
let Self { by_layer, by_id } = self;
let layer_widgets = by_layer.entry(layer_id).or_default();
match by_id.entry(widget_rect.id) {
std::collections::hash_map::Entry::Vacant(entry) => {
// A new widget
entry.insert(widget_rect);
layer_widgets.push(widget_rect);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
// e.g. calling `response.interact(…)` to add more interaction.
let existing = entry.get_mut();
existing.rect = existing.rect.union(widget_rect.rect);
existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect);
existing.sense |= widget_rect.sense;
existing.enabled |= widget_rect.enabled;
// Find the existing widget in this layer and update it:
for previous in layer_widgets.iter_mut().rev() {
if previous.id == widget_rect.id {
*previous = *existing;
break;
}
}
}
}
}
}
// ----------------------------------------------------------------------------
/// State stored per viewport /// State stored per viewport
#[derive(Default)] #[derive(Default)]
struct ViewportState { struct ViewportState {
@@ -350,7 +256,7 @@ struct ViewportState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RepaintCause { pub struct RepaintCause {
/// What file had the call that requested the repaint? /// What file had the call that requested the repaint?
pub file: String, pub file: &'static str,
/// What line number of the the call that requested the repaint? /// What line number of the the call that requested the repaint?
pub line: u32, pub line: u32,
@@ -363,7 +269,7 @@ impl RepaintCause {
pub fn new() -> Self { pub fn new() -> Self {
let caller = Location::caller(); let caller = Location::caller();
Self { Self {
file: caller.file().to_owned(), file: caller.file(),
line: caller.line(), line: caller.line(),
} }
} }
@@ -546,12 +452,7 @@ impl ContextImpl {
.map(|(i, id)| (*id, i)) .map(|(i, id)| (*id, i))
.collect(); .collect();
let mut layers: Vec<LayerId> = viewport let mut layers: Vec<LayerId> = viewport.widgets_prev_frame.layer_ids().collect();
.widgets_prev_frame
.by_layer
.keys()
.copied()
.collect();
layers.sort_by(|a, b| { layers.sort_by(|a, b| {
if a.order == b.order { if a.order == b.order {
@@ -595,7 +496,6 @@ impl ContextImpl {
pivot: Align2::LEFT_TOP, pivot: Align2::LEFT_TOP,
size: screen_rect.size(), size: screen_rect.size(),
interactable: true, interactable: true,
edges_padded_for_resize: false,
}, },
); );
@@ -1118,29 +1018,20 @@ impl Context {
/// ///
/// If the widget already exists, its state (sense, Rect, etc) will be updated. /// If the widget already exists, its state (sense, Rect, etc) will be updated.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { pub(crate) fn create_widget(&self, w: WidgetRect) -> Response {
if !w.enabled { // Remember this widget
w.sense.click = false; self.write(|ctx| {
w.sense.drag = false; let viewport = ctx.viewport();
}
if w.interact_rect.is_positive() { // We add all widgets here, even non-interactive ones,
// Remember this widget // because we need this list not only for checking for blocking widgets,
self.write(|ctx| { // but also to know when we have reached the widget we are checking for cover.
let viewport = ctx.viewport(); viewport.widgets_this_frame.insert(w.layer_id, w);
// We add all widgets here, even non-interactive ones, if w.sense.focusable {
// because we need this list not only for checking for blocking widgets, ctx.memory.interested_in_focus(w.id);
// but also to know when we have reached the widget we are checking for cover. }
viewport.widgets_this_frame.insert(w.layer_id, w); });
if w.sense.focusable {
ctx.memory.interested_in_focus(w.id);
}
});
} else {
// Don't remember invisible widgets
}
if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() {
// Not interested or allowed input: // Not interested or allowed input:
@@ -1175,9 +1066,8 @@ impl Context {
let viewport = ctx.viewport(); let viewport = ctx.viewport();
viewport viewport
.widgets_this_frame .widgets_this_frame
.by_id .get(id)
.get(&id) .or_else(|| viewport.widgets_prev_frame.get(id))
.or_else(|| viewport.widgets_prev_frame.by_id.get(&id))
.copied() .copied()
}) })
.map(|widget_rect| self.get_response(widget_rect)) .map(|widget_rect| self.get_response(widget_rect))
@@ -1235,7 +1125,8 @@ impl Context {
let input = &viewport.input; let input = &viewport.input;
let memory = &mut ctx.memory; let memory = &mut ctx.memory;
if sense.click if enabled
&& sense.click
&& memory.has_focus(id) && memory.has_focus(id)
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
{ {
@@ -1244,7 +1135,10 @@ impl Context {
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { if enabled
&& sense.click
&& input.has_accesskit_action_request(id, accesskit::Action::Default)
{
res.clicked[PointerButton::Primary as usize] = true; res.clicked[PointerButton::Primary as usize] = true;
} }
@@ -1264,7 +1158,7 @@ impl Context {
for pointer_event in &input.pointer.pointer_events { for pointer_event in &input.pointer.pointer_events {
if let PointerEvent::Released { click, button } = pointer_event { if let PointerEvent::Released { click, button } = pointer_event {
if sense.click && clicked { if enabled && sense.click && clicked {
if let Some(click) = click { if let Some(click) = click {
res.clicked[*button as usize] = true; res.clicked[*button as usize] = true;
res.double_clicked[*button as usize] = click.is_double(); res.double_clicked[*button as usize] = click.is_double();
@@ -1916,13 +1810,16 @@ impl Context {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn debug_painting(&self) { fn debug_painting(&self) {
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); let rect = widget.interact_rect;
painter.debug_rect(widget.interact_rect, color, text); if rect.is_positive() {
let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING);
painter.debug_rect(rect, color, text);
}
}; };
let paint_widget_id = |id: Id, text: &str, color: Color32| { let paint_widget_id = |id: Id, text: &str, color: Color32| {
if let Some(widget) = if let Some(widget) =
self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned()) self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned())
{ {
paint_widget(&widget, text, color); paint_widget(&widget, text, color);
} }
@@ -1931,8 +1828,8 @@ impl Context {
if self.style().debug.show_interactive_widgets { if self.style().debug.show_interactive_widgets {
// Show all interactive widgets: // Show all interactive widgets:
let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone());
for (layer_id, rects) in rects.by_layer { for (layer_id, rects) in rects.layers() {
let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING);
for rect in rects { for rect in rects {
if rect.sense.interactive() { if rect.sense.interactive() {
let (color, text) = if rect.sense.click && rect.sense.drag { let (color, text) = if rect.sense.click && rect.sense.drag {
@@ -2432,9 +2329,7 @@ impl Context {
/// Top-most layer at the given position. /// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> { pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.memory(|mem| { self.memory(|mem| mem.layer_id_at(pos))
mem.layer_id_at(pos, mem.options.style.interaction.resize_grab_radius_side)
})
} }
/// Moves the given area to the top in its [`Order`]. /// Moves the given area to the top in its [`Order`].

View File

@@ -396,6 +396,12 @@ pub enum Event {
/// The mouse or touch moved to a new place. /// The mouse or touch moved to a new place.
PointerMoved(Pos2), PointerMoved(Pos2),
/// The mouse moved, the units are unspecified.
/// Represents the actual movement of the mouse, without acceleration or clamped by screen edges.
/// `PointerMoved` and `MouseMoved` can be sent at the same time.
/// This event is optional. If the integration can not determine unfiltered motion it should not send this event.
MouseMoved(Vec2),
/// A mouse button was pressed or released (or a touch started or stopped). /// A mouse button was pressed or released (or a touch started or stopped).
PointerButton { PointerButton {
/// Where is the pointer? /// Where is the pointer?

View File

@@ -633,6 +633,7 @@ impl WidgetInfo {
WidgetType::ColorButton => "color button", WidgetType::ColorButton => "color button",
WidgetType::ImageButton => "image button", WidgetType::ImageButton => "image button",
WidgetType::CollapsingHeader => "collapsing header", WidgetType::CollapsingHeader => "collapsing header",
WidgetType::ProgressIndicator => "progress indicator",
WidgetType::Label | WidgetType::Other => "", WidgetType::Label | WidgetType::Other => "",
}; };

View File

@@ -56,8 +56,7 @@ pub fn hit_test(
let mut close: Vec<WidgetRect> = layer_order let mut close: Vec<WidgetRect> = layer_order
.iter() .iter()
.filter(|layer| layer.order.allow_interaction()) .filter(|layer| layer.order.allow_interaction())
.filter_map(|layer_id| widgets.by_layer.get(layer_id)) .flat_map(|&layer_id| widgets.get_layer(layer_id))
.flatten()
.filter(|&w| { .filter(|&w| {
let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos);
let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer);
@@ -79,6 +78,16 @@ pub fn hit_test(
let top_layer = closest_hit.layer_id; let top_layer = closest_hit.layer_id;
close.retain(|w| w.layer_id == top_layer); close.retain(|w| w.layer_id == top_layer);
// If the widget is disabled, treat it as if it isn't sensing anything.
// This simplifies the code in `hit_test_on_close` so it doesn't have to check
// the `enabled` flag everywhere:
for w in &mut close {
if !w.enabled {
w.sense.click = false;
w.sense.drag = false;
}
}
let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos);
let hits = hit_test_on_close(&close, pos_in_layer); let hits = hit_test_on_close(&close, pos_in_layer);

View File

@@ -22,6 +22,7 @@ const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings
/// You can check if `egui` is using the inputs using /// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InputState { pub struct InputState {
/// The raw input we got this frame from the backend. /// The raw input we got this frame from the backend.
pub raw: RawInput, pub raw: RawInput,
@@ -222,7 +223,7 @@ impl InputState {
let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta; let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta;
let smooth_scroll_delta; let mut smooth_scroll_delta = Vec2::ZERO;
{ {
// Mouse wheels often go very large steps. // Mouse wheels often go very large steps.
@@ -232,8 +233,15 @@ impl InputState {
let dt = stable_dt.at_most(0.1); let dt = stable_dt.at_most(0.1);
let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize
smooth_scroll_delta = t * unprocessed_scroll_delta; for d in 0..2 {
unprocessed_scroll_delta -= smooth_scroll_delta; if unprocessed_scroll_delta[d].abs() < 1.0 {
smooth_scroll_delta[d] = unprocessed_scroll_delta[d];
unprocessed_scroll_delta[d] = 0.0;
} else {
smooth_scroll_delta[d] = t * unprocessed_scroll_delta[d];
unprocessed_scroll_delta[d] -= smooth_scroll_delta[d];
}
}
} }
let mut modifiers = new.modifiers; let mut modifiers = new.modifiers;
@@ -546,6 +554,7 @@ impl InputState {
/// A pointer (mouse or touch) click. /// A pointer (mouse or touch) click.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct Click { pub(crate) struct Click {
pub pos: Pos2, pub pos: Pos2,
@@ -567,6 +576,7 @@ impl Click {
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) enum PointerEvent { pub(crate) enum PointerEvent {
Moved(Pos2), Moved(Pos2),
Pressed { Pressed {
@@ -595,6 +605,7 @@ impl PointerEvent {
/// Mouse or touch state. /// Mouse or touch state.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PointerState { pub struct PointerState {
/// Latest known time /// Latest known time
time: f64, time: f64,
@@ -617,6 +628,11 @@ pub struct PointerState {
/// How much the pointer moved compared to last frame, in points. /// How much the pointer moved compared to last frame, in points.
delta: Vec2, delta: Vec2,
/// How much the mouse moved since the last frame, in unspecified units.
/// Represents the actual movement of the mouse, without acceleration or clamped by screen edges.
/// May be unavailable on some integrations.
motion: Option<Vec2>,
/// Current velocity of pointer. /// Current velocity of pointer.
velocity: Vec2, velocity: Vec2,
@@ -664,6 +680,7 @@ impl Default for PointerState {
latest_pos: None, latest_pos: None,
interact_pos: None, interact_pos: None,
delta: Vec2::ZERO, delta: Vec2::ZERO,
motion: None,
velocity: Vec2::ZERO, velocity: Vec2::ZERO,
pos_history: History::new(0..1000, 0.1), pos_history: History::new(0..1000, 0.1),
down: Default::default(), down: Default::default(),
@@ -690,6 +707,9 @@ impl PointerState {
let old_pos = self.latest_pos; let old_pos = self.latest_pos;
self.interact_pos = self.latest_pos; self.interact_pos = self.latest_pos;
if self.motion.is_some() {
self.motion = Some(Vec2::ZERO);
}
for event in &new.events { for event in &new.events {
match event { match event {
@@ -775,6 +795,7 @@ impl PointerState {
self.latest_pos = None; self.latest_pos = None;
// NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame.
} }
Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta,
_ => {} _ => {}
} }
} }
@@ -819,6 +840,14 @@ impl PointerState {
self.delta self.delta
} }
/// How much the mouse moved since the last frame, in unspecified units.
/// Represents the actual movement of the mouse, without acceleration or clamped by screen edges.
/// May be unavailable on some integrations.
#[inline(always)]
pub fn motion(&self) -> Option<Vec2> {
self.motion
}
/// Current velocity of pointer. /// Current velocity of pointer.
#[inline(always)] #[inline(always)]
pub fn velocity(&self) -> Vec2 { pub fn velocity(&self) -> Vec2 {
@@ -1139,6 +1168,7 @@ impl PointerState {
latest_pos, latest_pos,
interact_pos, interact_pos,
delta, delta,
motion,
velocity, velocity,
pos_history: _, pos_history: _,
down, down,
@@ -1155,6 +1185,7 @@ impl PointerState {
ui.label(format!("latest_pos: {latest_pos:?}")); ui.label(format!("latest_pos: {latest_pos:?}"));
ui.label(format!("interact_pos: {interact_pos:?}")); ui.label(format!("interact_pos: {interact_pos:?}"));
ui.label(format!("delta: {delta:?}")); ui.label(format!("delta: {delta:?}"));
ui.label(format!("motion: {motion:?}"));
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

View File

@@ -64,6 +64,7 @@ pub struct MultiTouchInfo {
/// The current state (for a specific touch device) of touch events and gestures. /// The current state (for a specific touch device) of touch events and gestures.
#[derive(Clone)] #[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct TouchState { pub(crate) struct TouchState {
/// Technical identifier of the touch device. This is used to identify relevant touch events /// Technical identifier of the touch device. This is used to identify relevant touch events
/// for this [`TouchState`] instance. /// for this [`TouchState`] instance.
@@ -83,6 +84,7 @@ pub(crate) struct TouchState {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct GestureState { struct GestureState {
start_time: f64, start_time: f64,
start_pointer_pos: Pos2, start_pointer_pos: Pos2,
@@ -93,6 +95,7 @@ struct GestureState {
/// Gesture data that can change over time /// Gesture data that can change over time
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct DynGestureState { struct DynGestureState {
/// used for proportional zooming /// used for proportional zooming
avg_distance: f32, avg_distance: f32,
@@ -110,6 +113,7 @@ struct DynGestureState {
/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as
/// long as the finger/pen touches the surface. /// long as the finger/pen touches the surface.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct ActiveTouch { struct ActiveTouch {
/// Current position of this touch, in device coordinates (not necessarily screen position) /// Current position of this touch, in device coordinates (not necessarily screen position)
pos: Pos2, pos: Pos2,
@@ -302,6 +306,7 @@ impl Debug for TouchState {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum PinchType { enum PinchType {
Horizontal, Horizontal,
Vertical, Vertical,

View File

@@ -107,13 +107,13 @@ pub(crate) fn interact(
crate::profile_function!(); crate::profile_function!();
if let Some(id) = interaction.potential_click_id { if let Some(id) = interaction.potential_click_id {
if !widgets.by_id.contains_key(&id) { if !widgets.contains(id) {
// The widget we were interested in clicking is gone. // The widget we were interested in clicking is gone.
interaction.potential_click_id = None; interaction.potential_click_id = None;
} }
} }
if let Some(id) = interaction.potential_drag_id { if let Some(id) = interaction.potential_drag_id {
if !widgets.by_id.contains_key(&id) { if !widgets.contains(id) {
// The widget we were interested in dragging is gone. // The widget we were interested in dragging is gone.
// This is fine! This could be drag-and-drop, // This is fine! This could be drag-and-drop,
// and the widget being dragged is now "in the air" and thus // and the widget being dragged is now "in the air" and thus
@@ -145,7 +145,7 @@ pub(crate) fn interact(
if click.is_some() { if click.is_some() {
if let Some(widget) = interaction if let Some(widget) = interaction
.potential_click_id .potential_click_id
.and_then(|id| widgets.by_id.get(&id)) .and_then(|id| widgets.get(id))
{ {
clicked = Some(widget.id); clicked = Some(widget.id);
} }
@@ -160,22 +160,21 @@ pub(crate) fn interact(
if dragged.is_none() { if dragged.is_none() {
// Check if we started dragging something new: // Check if we started dragging something new:
if let Some(widget) = interaction if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
.potential_drag_id if widget.enabled {
.and_then(|id| widgets.by_id.get(&id)) let is_dragged = if widget.sense.click && widget.sense.drag {
{ // This widget is sensitive to both clicks and drags.
let is_dragged = if widget.sense.click && widget.sense.drag { // When the mouse first is pressed, it could be either,
// This widget is sensitive to both clicks and drags. // so we postpone the decision until we know.
// When the mouse first is pressed, it could be either, input.pointer.is_decidedly_dragging()
// so we postpone the decision until we know. } else {
input.pointer.is_decidedly_dragging() // This widget is just sensitive to drags, so we can mark it as dragged right away:
} else { widget.sense.drag
// This widget is just sensitive to drags, so we can mark it as dragged right away: };
widget.sense.drag
};
if is_dragged { if is_dragged {
dragged = Some(widget.id); dragged = Some(widget.id);
}
} }
} }
} }

View File

@@ -403,6 +403,7 @@ pub mod text_selection;
mod ui; mod ui;
pub mod util; pub mod util;
pub mod viewport; pub mod viewport;
mod widget_rect;
pub mod widget_text; pub mod widget_text;
pub mod widgets; pub mod widgets;
@@ -443,7 +444,7 @@ pub mod text {
pub use { pub use {
containers::*, containers::*,
context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects}, context::{Context, RepaintCause, RequestRepaintInfo},
data::{ data::{
input::*, input::*,
output::{ output::{
@@ -466,6 +467,7 @@ pub use {
text::{Galley, TextFormat}, text::{Galley, TextFormat},
ui::Ui, ui::Ui,
viewport::*, viewport::*,
widget_rect::{WidgetRect, WidgetRects},
widget_text::{RichText, WidgetText}, widget_text::{RichText, WidgetText},
widgets::*, widgets::*,
}; };
@@ -641,6 +643,8 @@ pub enum WidgetType {
CollapsingHeader, CollapsingHeader,
ProgressIndicator,
/// If you cannot fit any of the above slots. /// If you cannot fit any of the above slots.
/// ///
/// If this is something you think should be added, file an issue. /// If this is something you think should be added, file an issue.

View File

@@ -646,9 +646,8 @@ impl Memory {
} }
/// Top-most layer at the given position. /// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> { pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.areas() self.areas().layer_id_at(pos, &self.layer_transforms)
.layer_id_at(pos, resize_interact_radius_side, &self.layer_transforms)
} }
/// An iterator over all layers. Back-to-front. Top is last. /// An iterator over all layers. Back-to-front. Top is last.
@@ -921,7 +920,6 @@ impl Areas {
pub fn layer_id_at( pub fn layer_id_at(
&self, &self,
pos: Pos2, pos: Pos2,
resize_interact_radius_side: f32,
layer_transforms: &HashMap<LayerId, TSTransform>, layer_transforms: &HashMap<LayerId, TSTransform>,
) -> Option<LayerId> { ) -> Option<LayerId> {
for layer in self.order.iter().rev() { for layer in self.order.iter().rev() {
@@ -929,11 +927,6 @@ impl Areas {
if let Some(state) = self.areas.get(&layer.id) { if let Some(state) = self.areas.get(&layer.id) {
let mut rect = state.rect(); let mut rect = state.rect();
if state.interactable { if state.interactable {
if state.edges_padded_for_resize {
// Allow us to resize by dragging just outside the window:
rect = rect.expand(resize_interact_radius_side);
}
if let Some(transform) = layer_transforms.get(layer) { if let Some(transform) = layer_transforms.get(layer) {
rect = *transform * rect; rect = *transform * rect;
} }

View File

@@ -39,13 +39,14 @@ impl BarState {
} }
/// Show a menu at pointer if primary-clicked response. /// Show a menu at pointer if primary-clicked response.
///
/// Should be called from [`Context`] on a [`Response`] /// Should be called from [`Context`] on a [`Response`]
pub fn bar_menu<R>( pub fn bar_menu<R>(
&mut self, &mut self,
response: &Response, response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> { ) -> Option<InnerResponse<R>> {
MenuRoot::stationary_click_interaction(response, &mut self.open_menu, response.id); MenuRoot::stationary_click_interaction(response, &mut self.open_menu);
self.open_menu.show(response, add_contents) self.open_menu.show(response, add_contents)
} }
@@ -134,7 +135,7 @@ pub(crate) fn submenu_button<R>(
/// wrapper for the contents of every menu. /// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>( pub(crate) fn menu_ui<'c, R>(
ctx: &Context, ctx: &Context,
menu_id: impl Into<Id>, menu_id: Id,
menu_state_arc: &Arc<RwLock<MenuState>>, menu_state_arc: &Arc<RwLock<MenuState>>,
add_contents: impl FnOnce(&mut Ui) -> R + 'c, add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
@@ -144,7 +145,7 @@ pub(crate) fn menu_ui<'c, R>(
menu_state.rect.min menu_state.rect.min
}; };
let area = Area::new(menu_id) let area = Area::new(menu_id.with("__menu"))
.order(Order::Foreground) .order(Order::Foreground)
.fixed_pos(pos) .fixed_pos(pos)
.constrain_to(ctx.screen_rect()) .constrain_to(ctx.screen_rect())
@@ -222,13 +223,20 @@ pub(crate) fn context_menu(
let menu_id = Id::new(CONTEXT_MENU_ID_STR); let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let mut bar_state = BarState::load(&response.ctx, menu_id); let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state, response.id); MenuRoot::context_click_interaction(response, &mut bar_state);
let inner_response = bar_state.show(response, add_contents); let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id); bar_state.store(&response.ctx, menu_id);
inner_response inner_response
} }
/// Returns `true` if the context menu is opened for this widget.
pub(crate) fn context_menu_opened(response: &Response) -> bool {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let bar_state = BarState::load(&response.ctx, menu_id);
bar_state.is_menu_open(response.id)
}
/// Stores the state for the context menu. /// Stores the state for the context menu.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub(crate) struct MenuRootManager { pub(crate) struct MenuRootManager {
@@ -237,6 +245,7 @@ pub(crate) struct MenuRootManager {
impl MenuRootManager { impl MenuRootManager {
/// Show a menu at pointer if right-clicked response. /// Show a menu at pointer if right-clicked response.
///
/// Should be called from [`Context`] on a [`Response`] /// Should be called from [`Context`] on a [`Response`]
pub fn show<R>( pub fn show<R>(
&mut self, &mut self,
@@ -308,11 +317,9 @@ impl MenuRoot {
/// Interaction with a stationary menu, i.e. fixed in another Ui. /// Interaction with a stationary menu, i.e. fixed in another Ui.
/// ///
/// Responds to primary clicks. /// Responds to primary clicks.
fn stationary_interaction( fn stationary_interaction(response: &Response, root: &mut MenuRootManager) -> MenuResponse {
response: &Response, let id = response.id;
root: &mut MenuRootManager,
id: Id,
) -> MenuResponse {
if (response.clicked() && root.is_menu_open(id)) if (response.clicked() && root.is_menu_open(id))
|| response.ctx.input(|i| i.key_pressed(Key::Escape)) || response.ctx.input(|i| i.key_pressed(Key::Escape))
{ {
@@ -357,8 +364,8 @@ impl MenuRoot {
MenuResponse::Stay MenuResponse::Stay
} }
/// Interaction with a context menu (secondary clicks). /// Interaction with a context menu (secondary click).
fn context_interaction(response: &Response, root: &mut Option<Self>, id: Id) -> MenuResponse { fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
let response = response.interact(Sense::click()); let response = response.interact(Sense::click());
response.ctx.input(|input| { response.ctx.input(|input| {
let pointer = &input.pointer; let pointer = &input.pointer;
@@ -371,7 +378,7 @@ impl MenuRoot {
} }
if !in_old_menu { if !in_old_menu {
if response.hovered() && response.secondary_clicked() { if response.hovered() && response.secondary_clicked() {
return MenuResponse::Create(pos, id); return MenuResponse::Create(pos, response.id);
} else if (response.hovered() && pointer.primary_down()) || destroy { } else if (response.hovered() && pointer.primary_down()) || destroy {
return MenuResponse::Close; return MenuResponse::Close;
} }
@@ -392,14 +399,14 @@ impl MenuRoot {
} }
/// Respond to secondary (right) clicks. /// Respond to secondary (right) clicks.
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) {
let menu_response = Self::context_interaction(response, root, id); let menu_response = Self::context_interaction(response, root);
Self::handle_menu_response(root, menu_response); Self::handle_menu_response(root, menu_response);
} }
// Responds to primary clicks. // Responds to primary clicks.
pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) { pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager) {
let menu_response = Self::stationary_interaction(response, root, id); let menu_response = Self::stationary_interaction(response, root);
Self::handle_menu_response(root, menu_response); Self::handle_menu_response(root, menu_response);
} }
} }

View File

@@ -152,6 +152,12 @@ impl Painter {
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
self.ctx().round_pos_to_pixels(pos) self.ctx().round_pos_to_pixels(pos)
} }
/// Useful for pixel-perfect rendering.
#[inline]
pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
self.ctx().round_rect_to_pixels(rect)
}
} }
/// ## Low level /// ## Low level

View File

@@ -42,6 +42,13 @@ pub struct Response {
pub interact_rect: Rect, pub interact_rect: Rect,
/// The senses (click and/or drag) that the widget was interested in (if any). /// The senses (click and/or drag) that the widget was interested in (if any).
///
/// Note: if [`Self::enabled`] is `false`, then
/// the widget _effectively_ doesn't sense anything,
/// but can still have the same `Sense`.
/// This is because the sense informs the styling of the widget,
/// but we don't want to change the style when a widget is disabled
/// (that is handled by the `Painter` directly).
pub sense: Sense, pub sense: Sense,
/// Was the widget enabled? /// Was the widget enabled?
@@ -356,6 +363,20 @@ impl Response {
} }
} }
/// If dragged, how far did the mouse move?
/// This will use raw mouse movement if provided by the integration, otherwise will fall back to [`Response::drag_delta`]
/// Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen.
/// This may be useful in certain situations such as draggable values and 3D cameras, where screen position does not matter.
#[inline]
pub fn drag_motion(&self) -> Vec2 {
if self.dragged() {
self.ctx
.input(|i| i.pointer.motion().unwrap_or(i.pointer.delta()))
} else {
Vec2::ZERO
}
}
/// If the user started dragging this widget this frame, store the payload for drag-and-drop. /// If the user started dragging this widget this frame, store the payload for drag-and-drop.
#[doc(alias = "drag and drop")] #[doc(alias = "drag and drop")]
pub fn dnd_set_drag_payload<Payload: Any + Send + Sync>(&self, payload: Payload) { pub fn dnd_set_drag_payload<Payload: Any + Send + Sync>(&self, payload: Payload) {
@@ -520,6 +541,10 @@ impl Response {
return true; return true;
} }
if self.context_menu_opened() {
return false;
}
if self.enabled { if self.enabled {
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) { if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false; return false;
@@ -658,13 +683,14 @@ impl Response {
id: self.id, id: self.id,
rect: self.rect, rect: self.rect,
interact_rect: self.interact_rect, interact_rect: self.interact_rect,
sense, sense: self.sense | sense,
enabled: self.enabled, enabled: self.enabled,
}) })
} }
/// Adjust the scroll position until this UI becomes visible. /// Adjust the scroll position until this UI becomes visible.
/// ///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is `None`, it'll scroll enough to bring the UI into view. /// If `align` is `None`, it'll scroll enough to bring the UI into view.
/// ///
/// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. /// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`].
@@ -764,6 +790,7 @@ impl Response {
WidgetType::Slider => Role::Slider, WidgetType::Slider => Role::Slider,
WidgetType::DragValue => Role::SpinButton, WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell, WidgetType::ColorButton => Role::ColorWell,
WidgetType::ProgressIndicator => Role::ProgressIndicator,
WidgetType::Other => Role::Unknown, WidgetType::Other => Role::Unknown,
}); });
if let Some(label) = info.label { if let Some(label) = info.label {
@@ -834,6 +861,13 @@ impl Response {
menu::context_menu(self, add_contents) menu::context_menu(self, add_contents)
} }
/// Returns whether a context menu is currently open for this widget.
///
/// See [`Self::context_menu`].
pub fn context_menu_opened(&self) -> bool {
menu::context_menu_opened(self)
}
/// Draw a debug rectangle over the response displaying the response's id and whether it is /// Draw a debug rectangle over the response displaying the response's id and whether it is
/// enabled and/or hovered. /// enabled and/or hovered.
/// ///

View File

@@ -622,7 +622,7 @@ impl Margin {
}; };
#[inline] #[inline]
pub fn same(margin: f32) -> Self { pub const fn same(margin: f32) -> Self {
Self { Self {
left: margin, left: margin,
right: margin, right: margin,
@@ -633,7 +633,7 @@ impl Margin {
/// Margins with the same size on opposing sides /// Margins with the same size on opposing sides
#[inline] #[inline]
pub fn symmetric(x: f32, y: f32) -> Self { pub const fn symmetric(x: f32, y: f32) -> Self {
Self { Self {
left: x, left: x,
right: x, right: x,
@@ -649,12 +649,12 @@ impl Margin {
} }
#[inline] #[inline]
pub fn left_top(&self) -> Vec2 { pub const fn left_top(&self) -> Vec2 {
vec2(self.left, self.top) vec2(self.left, self.top)
} }
#[inline] #[inline]
pub fn right_bottom(&self) -> Vec2 { pub const fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom) vec2(self.right, self.bottom)
} }
@@ -702,6 +702,116 @@ impl std::ops::Add for Margin {
} }
} }
impl std::ops::Add<f32> for Margin {
type Output = Self;
#[inline]
fn add(self, v: f32) -> Self {
Self {
left: self.left + v,
right: self.right + v,
top: self.top + v,
bottom: self.bottom + v,
}
}
}
impl std::ops::AddAssign<f32> for Margin {
#[inline]
fn add_assign(&mut self, v: f32) {
self.left += v;
self.right += v;
self.top += v;
self.bottom += v;
}
}
impl std::ops::Div<f32> for Margin {
type Output = Self;
#[inline]
fn div(self, v: f32) -> Self {
Self {
left: self.left / v,
right: self.right / v,
top: self.top / v,
bottom: self.bottom / v,
}
}
}
impl std::ops::DivAssign<f32> for Margin {
#[inline]
fn div_assign(&mut self, v: f32) {
self.left /= v;
self.right /= v;
self.top /= v;
self.bottom /= v;
}
}
impl std::ops::Mul<f32> for Margin {
type Output = Self;
#[inline]
fn mul(self, v: f32) -> Self {
Self {
left: self.left * v,
right: self.right * v,
top: self.top * v,
bottom: self.bottom * v,
}
}
}
impl std::ops::MulAssign<f32> for Margin {
#[inline]
fn mul_assign(&mut self, v: f32) {
self.left *= v;
self.right *= v;
self.top *= v;
self.bottom *= v;
}
}
impl std::ops::Sub for Margin {
type Output = Self;
#[inline]
fn sub(self, other: Self) -> Self {
Self {
left: self.left - other.left,
right: self.right - other.right,
top: self.top - other.top,
bottom: self.bottom - other.bottom,
}
}
}
impl std::ops::Sub<f32> for Margin {
type Output = Self;
#[inline]
fn sub(self, v: f32) -> Self {
Self {
left: self.left - v,
right: self.right - v,
top: self.top - v,
bottom: self.bottom - v,
}
}
}
impl std::ops::SubAssign<f32> for Margin {
#[inline]
fn sub_assign(&mut self, v: f32) {
self.left -= v;
self.right -= v;
self.top -= v;
self.bottom -= v;
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// How and when interaction happens. /// How and when interaction happens.

View File

@@ -75,7 +75,7 @@ impl Ui {
/// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`].
pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self {
let style = ctx.style(); let style = ctx.style();
Ui { let ui = Ui {
id, id,
next_auto_id_source: id.with("auto").value(), next_auto_id_source: id.with("auto").value(),
painter: Painter::new(ctx, layer_id, clip_rect), painter: Painter::new(ctx, layer_id, clip_rect),
@@ -83,7 +83,20 @@ impl Ui {
placer: Placer::new(max_rect, Layout::default()), placer: Placer::new(max_rect, Layout::default()),
enabled: true, enabled: true,
menu_state: None, menu_state: None,
} };
// Register in the widget stack early, to ensure we are behind all widgets we contain:
let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called
ui.ctx().create_widget(WidgetRect {
id: ui.id,
layer_id: ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
sense: Sense::hover(),
enabled: ui.enabled,
});
ui
} }
/// Create a new [`Ui`] at a specific region. /// Create a new [`Ui`] at a specific region.
@@ -101,7 +114,7 @@ impl Ui {
crate::egui_assert!(!max_rect.any_nan()); crate::egui_assert!(!max_rect.any_nan());
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
Ui { let child_ui = Ui {
id: self.id.with(id_source), id: self.id.with(id_source),
next_auto_id_source, next_auto_id_source,
painter: self.painter.clone(), painter: self.painter.clone(),
@@ -109,7 +122,20 @@ impl Ui {
placer: Placer::new(max_rect, layout), placer: Placer::new(max_rect, layout),
enabled: self.enabled, enabled: self.enabled,
menu_state: self.menu_state.clone(), menu_state: self.menu_state.clone(),
} };
// Register in the widget stack early, to ensure we are behind all widgets we contain:
let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called
child_ui.ctx().create_widget(WidgetRect {
id: child_ui.id,
layer_id: child_ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
sense: Sense::hover(),
enabled: child_ui.enabled,
});
child_ui
} }
// ------------------------------------------------- // -------------------------------------------------
@@ -668,6 +694,15 @@ impl Ui {
self.interact(rect, id, sense) self.interact(rect, id, sense)
} }
/// Interact with the background of this [`Ui`],
/// i.e. behind all the widgets.
///
/// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`].
pub fn interact_bg(&self, sense: Sense) -> Response {
// This will update the WidgetRect that was first created in `Ui::new`.
self.interact(self.min_rect(), self.id, sense)
}
/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? /// 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, /// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance,
@@ -967,6 +1002,7 @@ impl Ui {
/// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible. /// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible.
/// ///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is `None`, it'll scroll enough to bring the cursor into view. /// If `align` is `None`, it'll scroll enough to bring the cursor into view.
/// ///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`]. [`Ui::scroll_with_delta`].. /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`]. [`Ui::scroll_with_delta`]..
@@ -993,6 +1029,7 @@ impl Ui {
/// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible. /// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible.
/// ///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is not provided, it'll scroll enough to bring the cursor into view. /// If `align` is not provided, it'll scroll enough to bring the cursor into view.
/// ///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`]. /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`].
@@ -2188,11 +2225,11 @@ impl Ui {
/// ///
/// The given frame is used for its margins, but it color is ignored. /// The given frame is used for its margins, but it color is ignored.
#[doc(alias = "drag and drop")] #[doc(alias = "drag and drop")]
pub fn dnd_drop_zone<Payload>( pub fn dnd_drop_zone<Payload, R>(
&mut self, &mut self,
frame: Frame, frame: Frame,
add_contents: impl FnOnce(&mut Ui), add_contents: impl FnOnce(&mut Ui) -> R,
) -> (Response, Option<Arc<Payload>>) ) -> (InnerResponse<R>, Option<Arc<Payload>>)
where where
Payload: Any + Send + Sync, Payload: Any + Send + Sync,
{ {
@@ -2201,7 +2238,7 @@ impl Ui {
DragAndDrop::has_payload_of_type::<Payload>(self.ctx()); DragAndDrop::has_payload_of_type::<Payload>(self.ctx());
let mut frame = frame.begin(self); let mut frame = frame.begin(self);
add_contents(&mut frame.content_ui); let inner = add_contents(&mut frame.content_ui);
let response = frame.allocate_space(self); let response = frame.allocate_space(self);
// NOTE: we use `response.contains_pointer` here instead of `hovered`, because // NOTE: we use `response.contains_pointer` here instead of `hovered`, because
@@ -2231,7 +2268,7 @@ impl Ui {
let payload = response.dnd_release_payload::<Payload>(); let payload = response.dnd_release_payload::<Payload>();
(response, payload) (InnerResponse { inner, response }, payload)
} }
/// Close the menu we are in (including submenus), if any. /// Close the menu we are in (including submenus), if any.

View File

@@ -0,0 +1,132 @@
use ahash::HashMap;
use crate::*;
/// Used to store each widget's [Id], [Rect] and [Sense] each frame.
///
/// Used to check which widget gets input when a user clicks somewhere.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WidgetRect {
/// The globally unique widget id.
///
/// For interactive widgets, this better be globally unique.
/// If not there will be weird bugs,
/// and also big red warning test on the screen in debug builds
/// (see [`Options::warn_on_id_clash`]).
///
/// You can ensure globally unique ids using [`Ui::push_id`].
pub id: Id,
/// What layer the widget is on.
pub layer_id: LayerId,
/// The full widget rectangle.
pub rect: Rect,
/// Where the widget is.
///
/// This is after clipping with the parent ui clip rect.
pub interact_rect: Rect,
/// How the widget responds to interaction.
///
/// Note: if [`Self::enabled`] is `false`, then
/// the widget _effectively_ doesn't sense anything,
/// but can still have the same `Sense`.
/// This is because the sense informs the styling of the widget,
/// but we don't want to change the style when a widget is disabled
/// (that is handled by the `Painter` directly).
pub sense: Sense,
/// Is the widget enabled?
pub enabled: bool,
}
/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame.
///
/// All [`Ui`]s have a [`WidgetRects`], but whether or not their rects are correct
/// depends on if [`Ui::interact_bg`] was ever called.
#[derive(Default, Clone, PartialEq, Eq)]
pub struct WidgetRects {
/// All widgets, in painting order.
by_layer: HashMap<LayerId, Vec<WidgetRect>>,
/// All widgets, by id, and their order in their respective layer
by_id: IdMap<(usize, WidgetRect)>,
}
impl WidgetRects {
/// All known layers with widgets.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.by_layer.keys().copied()
}
pub fn layers(&self) -> impl Iterator<Item = (&LayerId, &[WidgetRect])> + '_ {
self.by_layer
.iter()
.map(|(layer_id, rects)| (layer_id, &rects[..]))
}
#[inline]
pub fn get(&self, id: Id) -> Option<&WidgetRect> {
self.by_id.get(&id).map(|(_, w)| w)
}
#[inline]
pub fn contains(&self, id: Id) -> bool {
self.by_id.contains_key(&id)
}
/// All widgets in this layer, sorted back-to-front.
#[inline]
pub fn get_layer(&self, layer_id: LayerId) -> impl Iterator<Item = &WidgetRect> + '_ {
self.by_layer.get(&layer_id).into_iter().flatten()
}
/// Clear the contents while retaining allocated memory.
pub fn clear(&mut self) {
let Self { by_layer, by_id } = self;
for rects in by_layer.values_mut() {
rects.clear();
}
by_id.clear();
}
/// Insert the given widget rect in the given layer.
pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) {
let Self { by_layer, by_id } = self;
let layer_widgets = by_layer.entry(layer_id).or_default();
match by_id.entry(widget_rect.id) {
std::collections::hash_map::Entry::Vacant(entry) => {
// A new widget
let idx_in_layer = layer_widgets.len();
entry.insert((idx_in_layer, widget_rect));
layer_widgets.push(widget_rect);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
// This is a known widget, but we might need to update it!
// e.g. calling `response.interact(…)` to add more interaction.
let (idx_in_layer, existing) = entry.get_mut();
egui_assert!(
existing.layer_id == widget_rect.layer_id,
"Widget changed layer_id during the frame"
);
// Update it:
existing.rect = widget_rect.rect; // last wins
existing.interact_rect = widget_rect.interact_rect; // last wins
existing.sense |= widget_rect.sense;
existing.enabled |= widget_rect.enabled;
if existing.layer_id == widget_rect.layer_id {
layer_widgets[*idx_in_layer] = *existing;
}
}
}
}
}

View File

@@ -113,6 +113,17 @@ impl Widget for ProgressBar {
let (outer_rect, response) = let (outer_rect, response) =
ui.allocate_exact_size(vec2(desired_width, height), Sense::hover()); ui.allocate_exact_size(vec2(desired_width, height), Sense::hover());
response.widget_info(|| {
let mut info = if let Some(ProgressBarText::Custom(text)) = &text {
WidgetInfo::labeled(WidgetType::ProgressIndicator, text.text())
} else {
WidgetInfo::new(WidgetType::ProgressIndicator)
};
info.value = Some((progress as f64 * 100.0).floor());
info
});
if ui.is_rect_visible(response.rect) { if ui.is_rect_visible(response.rect) {
if animate { if animate {
ui.ctx().request_repaint(); ui.ctx().request_repaint();

View File

@@ -1,6 +1,6 @@
use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke};
use crate::{Response, Sense, Ui, Widget}; use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType};
/// A spinner widget used to indicate loading. /// A spinner widget used to indicate loading.
/// ///
@@ -66,6 +66,7 @@ impl Widget for Spinner {
.size .size
.unwrap_or_else(|| ui.style().spacing.interact_size.y); .unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
response.widget_info(|| WidgetInfo::new(WidgetType::ProgressIndicator));
self.paint_at(ui, rect); self.paint_at(ui, rect);
response response

View File

@@ -940,6 +940,7 @@ fn events(
if !text_mark.is_empty() { if !text_mark.is_empty() {
text.insert_text_at(&mut ccursor, text_mark, char_limit); text.insert_text_at(&mut ccursor, text_mark, char_limit);
} }
state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor)) Some(CCursorRange::two(start_cursor, ccursor))
} else { } else {
None None
@@ -947,12 +948,16 @@ fn events(
} }
Event::CompositionEnd(prediction) => { Event::CompositionEnd(prediction) => {
// CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, so do not check `state.has_ime = true` in the following statement. // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first,
// so do not check `state.has_ime = true` in the following statement.
if prediction != "\n" && prediction != "\r" { if prediction != "\n" && prediction != "\r" {
state.has_ime = false; state.has_ime = false;
let mut ccursor = text.delete_selected(&cursor_range); let mut ccursor;
if !prediction.is_empty() { if !prediction.is_empty() && cursor_range == state.ime_cursor_range {
ccursor = text.delete_selected(&cursor_range);
text.insert_text_at(&mut ccursor, prediction, char_limit); text.insert_text_at(&mut ccursor, prediction, char_limit);
} else {
ccursor = cursor_range.primary.ccursor;
} }
Some(CCursorRange::one(ccursor)) Some(CCursorRange::one(ccursor))
} else { } else {

View File

@@ -44,6 +44,10 @@ pub struct TextEditState {
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) has_ime: bool, pub(crate) has_ime: bool,
// cursor range for IME candidate.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_cursor_range: CursorRange,
// Visual offset when editing singleline text bigger than the width. // Visual offset when editing singleline text bigger than the width.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) singleline_offset: f32, pub(crate) singleline_offset: f32,

View File

@@ -89,10 +89,12 @@ pub trait TextBuffer {
fn decrease_indentation(&mut self, ccursor: &mut CCursor) { fn decrease_indentation(&mut self, ccursor: &mut CCursor) {
let line_start = find_line_start(self.as_str(), *ccursor); let line_start = find_line_start(self.as_str(), *ccursor);
let remove_len = if self.as_str()[line_start.index..].starts_with('\t') { let remove_len = if self.as_str().chars().nth(line_start.index) == Some('\t') {
Some(1) Some(1)
} else if self.as_str()[line_start.index..] } else if self
.as_str()
.chars() .chars()
.skip(line_start.index)
.take(TAB_SIZE) .take(TAB_SIZE)
.all(|c| c == ' ') .all(|c| c == ' ')
{ {

View File

@@ -55,7 +55,7 @@ impl Custom3d {
let (rect, response) = let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01; self.angle += response.drag_motion().x * 0.01;
// Clone locals so we can move them into the paint callback: // Clone locals so we can move them into the paint callback:
let angle = self.angle; let angle = self.angle;

View File

@@ -173,7 +173,7 @@ impl Custom3d {
let (rect, response) = let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01; self.angle += response.drag_motion().x * 0.01;
ui.painter().add(egui_wgpu::Callback::new_paint_callback( ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect, rect,
CustomTriangleCallback { angle: self.angle }, CustomTriangleCallback { angle: self.angle },

View File

@@ -13,6 +13,7 @@ impl super::Demo for About {
.default_width(320.0) .default_width(320.0)
.default_height(480.0) .default_height(480.0)
.open(open) .open(open)
.resizable([true, false])
.show(ctx, |ui| { .show(ctx, |ui| {
use super::View as _; use super::View as _;
self.ui(ui); self.ui(ui);

View File

@@ -75,6 +75,7 @@ impl super::Demo for CodeExample {
.default_size([800.0, 400.0]) .default_size([800.0, 400.0])
.vscroll(false) .vscroll(false)
.hscroll(true) .hscroll(true)
.resizable([true, false])
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }

View File

@@ -60,7 +60,7 @@ impl super::View for DragAndDropDemo {
let frame = Frame::default().inner_margin(4.0); let frame = Frame::default().inner_margin(4.0);
let (_, dropped_payload) = ui.dnd_drop_zone::<Location>(frame, |ui| { let (_, dropped_payload) = ui.dnd_drop_zone::<Location, ()>(frame, |ui| {
ui.set_min_size(vec2(64.0, 100.0)); ui.set_min_size(vec2(64.0, 100.0));
for (row_idx, item) in column.iter().enumerate() { for (row_idx, item) in column.iter().enumerate() {
let item_id = Id::new(("my_drag_and_drop_demo", col_idx, row_idx)); let item_id = Id::new(("my_drag_and_drop_demo", col_idx, row_idx));

View File

@@ -69,30 +69,25 @@ impl super::View for PanZoom {
} }
} }
for (id, pos, callback) in [ for (i, (pos, callback)) in [
( (
"a",
egui::Pos2::new(0.0, 0.0), egui::Pos2::new(0.0, 0.0),
Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!"))
as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>, as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
), ),
( (
"b",
egui::Pos2::new(0.0, 120.0), egui::Pos2::new(0.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")),
), ),
( (
"c",
egui::Pos2::new(120.0, 120.0), egui::Pos2::new(120.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")),
), ),
( (
"d",
egui::Pos2::new(120.0, 0.0), egui::Pos2::new(120.0, 0.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")),
), ),
( (
"e",
egui::Pos2::new(60.0, 60.0), egui::Pos2::new(60.0, 60.0),
Box::new(|ui, state| { Box::new(|ui, state| {
use egui::epaint::*; use egui::epaint::*;
@@ -110,8 +105,11 @@ impl super::View for PanZoom {
ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value")) ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value"))
}), }),
), ),
] { ]
let id = egui::Area::new(id) .into_iter()
.enumerate()
{
let id = egui::Area::new(id.with(("subarea", i)))
.default_pos(pos) .default_pos(pos)
// Need to cover up the pan_zoom demo window, // Need to cover up the pan_zoom demo window,
// but may also cover over other windows. // but may also cover over other windows.

View File

@@ -252,6 +252,8 @@ impl super::View for ScrollTo {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
ui.label("This shows how you can scroll to a specific item or pixel offset"); ui.label("This shows how you can scroll to a specific item or pixel offset");
let num_items = 500;
let mut track_item = false; let mut track_item = false;
let mut go_to_scroll_offset = false; let mut go_to_scroll_offset = false;
let mut scroll_top = false; let mut scroll_top = false;
@@ -260,7 +262,7 @@ impl super::View for ScrollTo {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Scroll to a specific item index:"); ui.label("Scroll to a specific item index:");
track_item |= ui track_item |= ui
.add(Slider::new(&mut self.track_item, 1..=50).text("Track Item")) .add(Slider::new(&mut self.track_item, 1..=num_items).text("Track Item"))
.dragged(); .dragged();
}); });
@@ -304,7 +306,7 @@ impl super::View for ScrollTo {
ui.scroll_to_cursor(Some(Align::TOP)); ui.scroll_to_cursor(Some(Align::TOP));
} }
ui.vertical(|ui| { ui.vertical(|ui| {
for item in 1..=50 { for item in 1..=num_items {
if track_item && item == self.track_item { if track_item && item == self.track_item {
let response = let response =
ui.colored_label(Color32::YELLOW, format!("This is item {item}")); ui.colored_label(Color32::YELLOW, format!("This is item {item}"));

View File

@@ -50,7 +50,7 @@ impl super::Demo for WidgetGallery {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) { fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name()) egui::Window::new(self.name())
.open(open) .open(open)
.resizable(true) .resizable([true, false])
.default_width(280.0) .default_width(280.0)
.show(ctx, |ui| { .show(ctx, |ui| {
use super::View as _; use super::View as _;

View File

@@ -1,4 +1,4 @@
use egui::{Pos2, Rect, Response, Sense, Ui}; use egui::{Id, Pos2, Rect, Response, Sense, Ui};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) enum CellSize { pub(crate) enum CellSize {
@@ -113,6 +113,7 @@ impl<'l> StripLayout<'l> {
flags: StripLayoutFlags, flags: StripLayoutFlags,
width: CellSize, width: CellSize,
height: CellSize, height: CellSize,
child_ui_id_source: Id,
add_cell_contents: impl FnOnce(&mut Ui), add_cell_contents: impl FnOnce(&mut Ui),
) -> (Rect, Response) { ) -> (Rect, Response) {
let max_rect = self.cell_rect(&width, &height); let max_rect = self.cell_rect(&width, &height);
@@ -145,7 +146,9 @@ impl<'l> StripLayout<'l> {
); );
} }
let used_rect = self.cell(flags, max_rect, add_cell_contents); let child_ui = self.cell(flags, max_rect, child_ui_id_source, add_cell_contents);
let used_rect = child_ui.min_rect();
self.set_pos(max_rect); self.set_pos(max_rect);
@@ -155,7 +158,9 @@ impl<'l> StripLayout<'l> {
max_rect.union(used_rect) max_rect.union(used_rect)
}; };
let response = self.ui.allocate_rect(allocation_rect, self.sense); self.ui.advance_cursor_after_rect(allocation_rect);
let response = child_ui.interact(max_rect, child_ui.id(), self.sense);
(used_rect, response) (used_rect, response)
} }
@@ -182,13 +187,17 @@ impl<'l> StripLayout<'l> {
self.ui.allocate_rect(rect, Sense::hover()); self.ui.allocate_rect(rect, Sense::hover());
} }
/// Return the Ui to which the contents where added
fn cell( fn cell(
&mut self, &mut self,
flags: StripLayoutFlags, flags: StripLayoutFlags,
rect: Rect, rect: Rect,
child_ui_id_source: egui::Id,
add_cell_contents: impl FnOnce(&mut Ui), add_cell_contents: impl FnOnce(&mut Ui),
) -> Rect { ) -> Ui {
let mut child_ui = self.ui.child_ui(rect, self.cell_layout); let mut child_ui =
self.ui
.child_ui_with_id_source(rect, self.cell_layout, child_ui_id_source);
if flags.clip { if flags.clip {
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
@@ -204,7 +213,7 @@ impl<'l> StripLayout<'l> {
add_cell_contents(&mut child_ui); add_cell_contents(&mut child_ui);
child_ui.min_rect() child_ui
} }
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size

View File

@@ -25,6 +25,19 @@ impl FileLoader {
const PROTOCOL: &str = "file://"; const PROTOCOL: &str = "file://";
/// Remove the leading slash from the path if the target OS is Windows.
///
/// This is because Windows paths are not supposed to start with a slash.
/// For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path.
#[inline]
fn trim_extra_slash(s: &str) -> &str {
if cfg!(target_os = "windows") {
s.trim_start_matches('/')
} else {
s
}
}
impl BytesLoader for FileLoader { impl BytesLoader for FileLoader {
fn id(&self) -> &str { fn id(&self) -> &str {
Self::ID Self::ID
@@ -32,12 +45,12 @@ impl BytesLoader for FileLoader {
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
// File loader only supports the `file` protocol. // File loader only supports the `file` protocol.
let Some(path) = uri.strip_prefix(PROTOCOL) else { let Some(path) = uri.strip_prefix(PROTOCOL).map(trim_extra_slash) else {
return Err(LoadError::NotSupported); return Err(LoadError::NotSupported);
}; };
let mut cache = self.cache.lock(); let mut cache = self.cache.lock();
if let Some(entry) = cache.get(path).cloned() { if let Some(entry) = cache.get(uri).cloned() {
// `path` has either begun loading, is loaded, or has failed to load. // `path` has either begun loading, is loaded, or has failed to load.
match entry { match entry {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
@@ -54,7 +67,7 @@ impl BytesLoader for FileLoader {
// Set the file to `pending` until we finish loading it. // Set the file to `pending` until we finish loading it.
let path = path.to_owned(); let path = path.to_owned();
cache.insert(path.clone(), Poll::Pending); cache.insert(uri.to_owned(), Poll::Pending);
drop(cache); drop(cache);
// Spawn a thread to read the file, so that we don't block the render for too long. // Spawn a thread to read the file, so that we don't block the render for too long.
@@ -63,7 +76,7 @@ impl BytesLoader for FileLoader {
.spawn({ .spawn({
let ctx = ctx.clone(); let ctx = ctx.clone();
let cache = self.cache.clone(); let cache = self.cache.clone();
let _uri = uri.to_owned(); let uri = uri.to_owned();
move || { move || {
let result = match std::fs::read(&path) { let result = match std::fs::read(&path) {
Ok(bytes) => { Ok(bytes) => {
@@ -82,10 +95,10 @@ impl BytesLoader for FileLoader {
} }
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
}; };
let prev = cache.lock().insert(path, Poll::Ready(result)); let prev = cache.lock().insert(uri.clone(), Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending))); assert!(matches!(prev, Some(Poll::Pending)));
ctx.request_repaint(); ctx.request_repaint();
log::trace!("finished loading {_uri:?}"); log::trace!("finished loading {uri:?}");
} }
}) })
.expect("failed to spawn thread"); .expect("failed to spawn thread");

View File

@@ -194,7 +194,13 @@ impl<'a, 'b> Strip<'a, 'b> {
clip: self.clip, clip: self.clip,
..Default::default() ..Default::default()
}; };
self.layout.add(flags, width, height, add_contents); self.layout.add(
flags,
width,
height,
egui::Id::new(self.size_index),
add_contents,
);
} }
/// Add an empty cell. /// Add an empty cell.

View File

@@ -1166,7 +1166,13 @@ impl<'a, 'b> TableRow<'a, 'b> {
selected: self.selected, selected: self.selected,
}; };
let (used_rect, response) = self.layout.add(flags, width, height, add_cell_contents); let (used_rect, response) = self.layout.add(
flags,
width,
height,
egui::Id::new((self.row_index, col_index)),
add_cell_contents,
);
if let Some(max_w) = self.max_used_widths.get_mut(col_index) { if let Some(max_w) = self.max_used_widths.get_mut(col_index) {
*max_w = max_w.max(used_rect.width()); *max_w = max_w.max(used_rect.width());

View File

@@ -1391,6 +1391,11 @@ pub struct GridMark {
pub fn log_grid_spacer(log_base: i64) -> GridSpacer { pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
let log_base = log_base as f64; let log_base = log_base as f64;
let step_sizes = move |input: GridInput| -> Vec<GridMark> { let step_sizes = move |input: GridInput| -> Vec<GridMark> {
// handle degenerate cases
if input.base_step_size.abs() < f64::EPSILON {
return Vec::new();
}
// The distance between two of the thinnest grid lines is "rounded" up // The distance between two of the thinnest grid lines is "rounded" up
// to the next-bigger power of base // to the next-bigger power of base
let smallest_visible_unit = next_power(input.base_step_size, log_base); let smallest_visible_unit = next_power(input.base_step_size, log_base);
@@ -1693,7 +1698,7 @@ impl PreparedPlot {
/// assert_eq!(next_power(0.2, 10.0), 1); /// assert_eq!(next_power(0.2, 10.0), 1);
/// ``` /// ```
fn next_power(value: f64, base: f64) -> f64 { fn next_power(value: f64, base: f64) -> f64 {
assert_ne!(value, 0.0); // can be negative (typical for Y axis) debug_assert_ne!(value, 0.0); // can be negative (typical for Y axis)
base.powi(value.abs().log(base).ceil() as i32) base.powi(value.abs().log(base).ceil() as i32)
} }
@@ -1708,7 +1713,7 @@ fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
/// Fill in all values between [min, max] which are a multiple of `step_size` /// Fill in all values between [min, max] which are a multiple of `step_size`
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) { fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
assert!(max > min); debug_assert!(max > min);
let first = (min / step_size).ceil() as i64; let first = (min / step_size).ceil() as i64;
let last = (max / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64;

View File

@@ -16,6 +16,7 @@ use std::collections::VecDeque;
/// or for smoothed velocity (e.g. mouse pointer speed). /// or for smoothed velocity (e.g. mouse pointer speed).
/// All times are in seconds. /// All times are in seconds.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct History<T> { pub struct History<T> {
/// In elements, i.e. of `values.len()`. /// In elements, i.e. of `values.len()`.
/// The length is initially zero, but once past `min_len` will not shrink below it. /// The length is initially zero, but once past `min_len` will not shrink below it.

View File

@@ -383,6 +383,52 @@ pub fn exponential_smooth_factor(
1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds) 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
} }
/// If you have a value animating over time,
/// how much towards its target do you need to move it this frame?
///
/// You only need to store the start time and target value in order to animate using this function.
///
/// ``` rs
/// struct Animation {
/// current_value: f32,
///
/// animation_time_span: (f64, f64),
/// target_value: f32,
/// }
///
/// impl Animation {
/// fn update(&mut self, now: f64, dt: f32) {
/// let t = interpolation_factor(self.animation_time_span, now, dt, ease_in_ease_out);
/// self.current_value = emath::lerp(self.current_value..=self.target_value, t);
/// }
/// }
/// ```
pub fn interpolation_factor(
(start_time, end_time): (f64, f64),
current_time: f64,
dt: f32,
easing: impl Fn(f32) -> f32,
) -> f32 {
let animation_duration = (end_time - start_time) as f32;
let prev_time = current_time - dt as f64;
let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
let end_t = easing((current_time - start_time) as f32 / animation_duration);
if end_t < 1.0 {
(end_t - prev_t) / (1.0 - prev_t)
} else {
1.0
}
}
/// Ease in, ease out.
///
/// `f(0) = 0, f'(0) = 0, f(1) = 1, f'(1) = 0`.
#[inline]
pub fn ease_in_ease_out(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
(3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// An assert that is only active when `emath` is compiled with the `extra_asserts` feature /// An assert that is only active when `emath` is compiled with the `extra_asserts` feature

View File

@@ -1,5 +1,7 @@
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use crate::Vec2b;
/// A vector has a direction and length. /// A vector has a direction and length.
/// A [`Vec2`] is often used to represent a size. /// A [`Vec2`] is often used to represent a size.
/// ///
@@ -86,6 +88,16 @@ impl From<&Vec2> for (f32, f32) {
} }
} }
impl From<Vec2b> for Vec2 {
#[inline(always)]
fn from(v: Vec2b) -> Self {
Self {
x: v.x as i32 as f32,
y: v.y as i32 as f32,
}
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Mint compatibility and convenience conversions // Mint compatibility and convenience conversions

View File

@@ -19,6 +19,30 @@ impl Vec2b {
pub fn any(&self) -> bool { pub fn any(&self) -> bool {
self.x || self.y self.x || self.y
} }
/// Are both `x` and `y` true?
#[inline]
pub fn all(&self) -> bool {
self.x && self.y
}
#[inline]
pub fn and(&self, other: impl Into<Self>) -> Self {
let other = other.into();
Self {
x: self.x && other.x,
y: self.y && other.y,
}
}
#[inline]
pub fn or(&self, other: impl Into<Self>) -> Self {
let other = other.into();
Self {
x: self.x || other.x,
y: self.y || other.y,
}
}
} }
impl From<bool> for Vec2b { impl From<bool> for Vec2b {

View File

@@ -120,6 +120,16 @@ impl ColorImage {
Self { size, pixels } Self { size, pixels }
} }
/// Alternative method to `from_gray`.
/// Create a [`ColorImage`] from iterator over flat opaque gray data.
///
/// Panics if `size[0] * size[1] != gray_iter.len()`.
pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator<Item = u8>) -> Self {
let pixels: Vec<_> = gray_iter.map(Color32::from_gray).collect();
assert_eq!(size[0] * size[1], pixels.len());
Self { size, pixels }
}
/// A view of the underlying data as `&[u8]` /// A view of the underlying data as `&[u8]`
#[cfg(feature = "bytemuck")] #[cfg(feature = "bytemuck")]
pub fn as_raw(&self) -> &[u8] { pub fn as_raw(&self) -> &[u8] {

View File

@@ -19,8 +19,12 @@ impl Shadow {
color: Color32::TRANSPARENT, color: Color32::TRANSPARENT,
}; };
pub const fn new(extrusion: f32, color: Color32) -> Self {
Self { extrusion, color }
}
/// Tooltips, menus, …, for dark mode. /// Tooltips, menus, …, for dark mode.
pub fn small_dark() -> Self { pub const fn small_dark() -> Self {
Self { Self {
extrusion: 16.0, extrusion: 16.0,
color: Color32::from_black_alpha(96), color: Color32::from_black_alpha(96),
@@ -28,7 +32,7 @@ impl Shadow {
} }
/// Tooltips, menus, …, for light mode. /// Tooltips, menus, …, for light mode.
pub fn small_light() -> Self { pub const fn small_light() -> Self {
Self { Self {
extrusion: 16.0, extrusion: 16.0,
color: Color32::from_black_alpha(20), color: Color32::from_black_alpha(20),
@@ -36,7 +40,7 @@ impl Shadow {
} }
/// Used for egui windows in dark mode. /// Used for egui windows in dark mode.
pub fn big_dark() -> Self { pub const fn big_dark() -> Self {
Self { Self {
extrusion: 32.0, extrusion: 32.0,
color: Color32::from_black_alpha(96), color: Color32::from_black_alpha(96),
@@ -44,7 +48,7 @@ impl Shadow {
} }
/// Used for egui windows in light mode. /// Used for egui windows in light mode.
pub fn big_light() -> Self { pub const fn big_light() -> Self {
Self { Self {
extrusion: 32.0, extrusion: 32.0,
color: Color32::from_black_alpha(16), color: Color32::from_black_alpha(16),

View File

@@ -355,6 +355,22 @@ impl Shape {
} }
} }
/// Scale the shape by `factor`, in-place.
///
/// A wrapper around [`Self::transform`].
#[inline(always)]
pub fn scale(&mut self, factor: f32) {
self.transform(TSTransform::from_scaling(factor));
}
/// Move the shape by `delta`, in-place.
///
/// A wrapper around [`Self::transform`].
#[inline(always)]
pub fn translate(&mut self, delta: Vec2) {
self.transform(TSTransform::from_translation(delta));
}
/// Move the shape by this many points, in-place. /// Move the shape by this many points, in-place.
/// ///
/// If using a [`PaintCallback`], note that only the rect is scaled as opposed /// If using a [`PaintCallback`], note that only the rect is scaled as opposed
@@ -387,6 +403,7 @@ impl Shape {
Self::Rect(rect_shape) => { Self::Rect(rect_shape) => {
rect_shape.rect = transform * rect_shape.rect; rect_shape.rect = transform * rect_shape.rect;
rect_shape.stroke.width *= transform.scaling; rect_shape.stroke.width *= transform.scaling;
rect_shape.rounding *= transform.scaling;
} }
Self::Text(text_shape) => { Self::Text(text_shape) => {
text_shape.pos = transform * text_shape.pos; text_shape.pos = transform * text_shape.pos;
@@ -701,7 +718,7 @@ impl Rounding {
}; };
#[inline] #[inline]
pub fn same(radius: f32) -> Self { pub const fn same(radius: f32) -> Self {
Self { Self {
nw: radius, nw: radius,
ne: radius, ne: radius,
@@ -739,6 +756,130 @@ impl Rounding {
} }
} }
impl std::ops::Add for Rounding {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
}
}
}
impl std::ops::AddAssign for Rounding {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
};
}
}
impl std::ops::AddAssign<f32> for Rounding {
#[inline]
fn add_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw + rhs,
ne: self.ne + rhs,
sw: self.sw + rhs,
se: self.se + rhs,
};
}
}
impl std::ops::Sub for Rounding {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
}
}
}
impl std::ops::SubAssign for Rounding {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw - rhs.nw,
ne: self.ne - rhs.ne,
sw: self.sw - rhs.sw,
se: self.se - rhs.se,
};
}
}
impl std::ops::SubAssign<f32> for Rounding {
#[inline]
fn sub_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw - rhs,
ne: self.ne - rhs,
sw: self.sw - rhs,
se: self.se - rhs,
};
}
}
impl std::ops::Div<f32> for Rounding {
type Output = Self;
#[inline]
fn div(self, rhs: f32) -> Self {
Self {
nw: self.nw / rhs,
ne: self.ne / rhs,
sw: self.sw / rhs,
se: self.se / rhs,
}
}
}
impl std::ops::DivAssign<f32> for Rounding {
#[inline]
fn div_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw / rhs,
ne: self.ne / rhs,
sw: self.sw / rhs,
se: self.se / rhs,
};
}
}
impl std::ops::Mul<f32> for Rounding {
type Output = Self;
#[inline]
fn mul(self, rhs: f32) -> Self {
Self {
nw: self.nw * rhs,
ne: self.ne * rhs,
sw: self.sw * rhs,
se: self.se * rhs,
}
}
}
impl std::ops::MulAssign<f32> for Rounding {
#[inline]
fn mul_assign(&mut self, rhs: f32) {
*self = Self {
nw: self.nw * rhs,
ne: self.ne * rhs,
sw: self.sw * rhs,
se: self.se * rhs,
};
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// How to paint some text on screen. /// How to paint some text on screen.

View File

@@ -69,7 +69,7 @@ impl MyApp {
let (rect, response) = let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01; self.angle += response.drag_motion().x * 0.01;
// Clone locals so we can move them into the paint callback: // Clone locals so we can move them into the paint callback:
let angle = self.angle; let angle = self.angle;

View File

@@ -0,0 +1,23 @@
[package]
name = "custom_keypad"
version = "0.1.0"
authors = ["Varphone Wong <varphone@qq.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.72"
publish = false
[dependencies]
eframe = { workspace = true, features = [
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
# For image support:
egui_extras = { workspace = true, features = ["default", "image"] }
env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }

View File

@@ -0,0 +1,7 @@
Example showing how to implements a custom keypad.
```sh
cargo run -p custom_keypad
```
![](screenshot.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,255 @@
use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Transition {
#[default]
None,
CloseOnNextFrame,
CloseImmediately,
}
#[derive(Clone, Debug)]
struct State {
open: bool,
closable: bool,
close_on_next_frame: bool,
start_pos: egui::Pos2,
focus: Option<egui::Id>,
events: Option<Vec<egui::Event>>,
}
impl State {
fn new() -> Self {
Self {
open: false,
closable: false,
close_on_next_frame: false,
start_pos: pos2(100.0, 100.0),
focus: None,
events: None,
}
}
fn queue_char(&mut self, c: char) {
let events = self.events.get_or_insert(vec![]);
if let Some(key) = egui::Key::from_name(&c.to_string()) {
events.push(egui::Event::Key {
key,
physical_key: Some(key),
pressed: true,
repeat: false,
modifiers: Default::default(),
});
}
events.push(egui::Event::Text(c.to_string()));
}
fn queue_key(&mut self, key: egui::Key) {
let events = self.events.get_or_insert(vec![]);
events.push(egui::Event::Key {
key,
physical_key: Some(key),
pressed: true,
repeat: false,
modifiers: Default::default(),
});
}
}
impl Default for State {
fn default() -> Self {
Self::new()
}
}
/// A simple keypad widget.
pub struct Keypad {
id: egui::Id,
}
impl Keypad {
pub fn new() -> Self {
Self {
id: egui::Id::new("keypad"),
}
}
pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
let events = ctx.memory_mut(|m| {
m.data
.get_temp_mut_or_default::<State>(self.id)
.events
.take()
});
if let Some(mut events) = events {
events.append(&mut raw_input.events);
raw_input.events = events;
}
}
fn buttons(ui: &mut Ui, state: &mut State) -> Transition {
let mut trans = Transition::None;
ui.vertical(|ui| {
let window_margin = ui.spacing().window_margin;
let size_1x1 = vec2(32.0, 26.0);
let _size_1x2 = vec2(32.0, 52.0 + window_margin.top);
let _size_2x1 = vec2(64.0 + window_margin.left, 26.0);
ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left);
ui.horizontal(|ui| {
if ui.add_sized(size_1x1, Button::new("1")).clicked() {
state.queue_char('1');
}
if ui.add_sized(size_1x1, Button::new("2")).clicked() {
state.queue_char('2');
}
if ui.add_sized(size_1x1, Button::new("3")).clicked() {
state.queue_char('3');
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::Home);
}
if ui.add_sized(size_1x1, Button::new("🔙")).clicked() {
state.queue_key(egui::Key::Backspace);
}
});
ui.horizontal(|ui| {
if ui.add_sized(size_1x1, Button::new("4")).clicked() {
state.queue_char('4');
}
if ui.add_sized(size_1x1, Button::new("5")).clicked() {
state.queue_char('5');
}
if ui.add_sized(size_1x1, Button::new("6")).clicked() {
state.queue_char('6');
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::End);
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::Enter);
trans = Transition::CloseOnNextFrame;
}
});
ui.horizontal(|ui| {
if ui.add_sized(size_1x1, Button::new("7")).clicked() {
state.queue_char('7');
}
if ui.add_sized(size_1x1, Button::new("8")).clicked() {
state.queue_char('8');
}
if ui.add_sized(size_1x1, Button::new("9")).clicked() {
state.queue_char('9');
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::ArrowUp);
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
trans = Transition::CloseImmediately;
}
});
ui.horizontal(|ui| {
if ui.add_sized(size_1x1, Button::new("0")).clicked() {
state.queue_char('0');
}
if ui.add_sized(size_1x1, Button::new(".")).clicked() {
state.queue_char('.');
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::ArrowLeft);
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::ArrowDown);
}
if ui.add_sized(size_1x1, Button::new("")).clicked() {
state.queue_key(egui::Key::ArrowRight);
}
});
});
trans
}
pub fn show(&self, ctx: &egui::Context) {
let (focus, mut state) = ctx.memory(|m| {
(
m.focus(),
m.data.get_temp::<State>(self.id).unwrap_or_default(),
)
});
let mut is_first_show = false;
if ctx.wants_keyboard_input() && state.focus != focus {
let y = ctx.style().spacing.interact_size.y * 1.25;
state.open = true;
state.start_pos = ctx.input(|i| {
i.pointer
.hover_pos()
.map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y))
});
state.focus = focus;
is_first_show = true;
}
if state.close_on_next_frame {
state.open = false;
state.close_on_next_frame = false;
state.focus = None;
}
let mut open = state.open;
let win = egui::Window::new("⌨ Keypad");
let win = if is_first_show {
win.current_pos(state.start_pos)
} else {
win.default_pos(state.start_pos)
};
let resp = win
.movable(true)
.resizable(false)
.open(&mut open)
.show(ctx, |ui| Self::buttons(ui, &mut state));
state.open = open;
if let Some(resp) = resp {
match resp.inner {
Some(Transition::CloseOnNextFrame) => {
state.close_on_next_frame = true;
}
Some(Transition::CloseImmediately) => {
state.open = false;
state.focus = None;
}
_ => {}
}
if !state.closable && resp.response.hovered() {
state.closable = true;
}
if state.closable && resp.response.clicked_elsewhere() {
state.open = false;
state.closable = false;
state.focus = None;
}
if is_first_show {
ctx.move_to_top(resp.response.layer_id);
}
}
if let (true, Some(focus)) = (state.open, state.focus) {
ctx.memory_mut(|m| {
m.request_focus(focus);
});
}
ctx.memory_mut(|m| m.data.insert_temp(self.id, state));
}
}
impl Default for Keypad {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,68 @@
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
mod keypad;
use keypad::Keypad;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]),
..Default::default()
};
eframe::run_native(
"Custom Keypad App",
options,
Box::new(|cc| {
// Use the dark theme
cc.egui_ctx.set_visuals(egui::Visuals::dark());
// This gives us image support:
egui_extras::install_image_loaders(&cc.egui_ctx);
Box::<MyApp>::default()
}),
)
}
struct MyApp {
name: String,
age: u32,
keypad: Keypad,
}
impl MyApp {}
impl Default for MyApp {
fn default() -> Self {
Self {
name: "Arthur".to_owned(),
age: 42,
keypad: Keypad::new(),
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::Window::new("Custom Keypad")
.default_pos([100.0, 100.0])
.title_bar(true)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Increment").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));
});
self.keypad.show(ctx);
}
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
self.keypad.bump_events(ctx, raw_input);
}
}