1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 23:13:13 -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!
It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpituity, so we have a high bar on what get merged!
It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged!
When reviewing, we look for:
* The PR title and description should be helpful
@@ -123,7 +123,7 @@ with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards.
`egui` uses logical _points_ as its coordinate system.
Those related to physical _pixels_ by the `pixels_per_point` scale factor.
For example, a high-dpi screeen can have `pixels_per_point = 2.0`,
For example, a high-dpi screen can have `pixels_per_point = 2.0`,
meaning there are two physical screen pixels for each logical point.
Angles are in radians, and are measured clockwise from the X-axis, which has angle=0.

9
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

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")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
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")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, window_id },

View File

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

View File

@@ -115,7 +115,23 @@ pub fn translate_key(key: &str) -> Option<egui::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 {
alt: event.alt_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`].
///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
@@ -808,12 +815,14 @@ impl State {
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
crate::profile_scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime);
}
if let Some(ime) = ime {
let rect = ime.rect;
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
crate::profile_scope!("set_ime_cursor_area");
window.set_ime_cursor_area(
winit::dpi::PhysicalPosition {
x: pixels_per_point * rect.min.x,
@@ -829,6 +838,7 @@ impl State {
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_ref() {
if let Some(update) = accesskit_update {
crate::profile_scope!("accesskit");
accesskit.update_if_active(|| update);
}
}

View File

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

View File

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

View File

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

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

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
#[derive(Default)]
struct ViewportState {
@@ -350,7 +256,7 @@ struct ViewportState {
#[derive(Clone, Debug)]
pub struct RepaintCause {
/// What file had the call that requested the repaint?
pub file: String,
pub file: &'static str,
/// What line number of the the call that requested the repaint?
pub line: u32,
@@ -363,7 +269,7 @@ impl RepaintCause {
pub fn new() -> Self {
let caller = Location::caller();
Self {
file: caller.file().to_owned(),
file: caller.file(),
line: caller.line(),
}
}
@@ -546,12 +452,7 @@ impl ContextImpl {
.map(|(i, id)| (*id, i))
.collect();
let mut layers: Vec<LayerId> = viewport
.widgets_prev_frame
.by_layer
.keys()
.copied()
.collect();
let mut layers: Vec<LayerId> = viewport.widgets_prev_frame.layer_ids().collect();
layers.sort_by(|a, b| {
if a.order == b.order {
@@ -595,7 +496,6 @@ impl ContextImpl {
pivot: Align2::LEFT_TOP,
size: screen_rect.size(),
interactable: true,
edges_padded_for_resize: false,
},
);
@@ -1118,29 +1018,20 @@ impl Context {
///
/// If the widget already exists, its state (sense, Rect, etc) will be updated.
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response {
if !w.enabled {
w.sense.click = false;
w.sense.drag = false;
}
pub(crate) fn create_widget(&self, w: WidgetRect) -> Response {
// Remember this widget
self.write(|ctx| {
let viewport = ctx.viewport();
if w.interact_rect.is_positive() {
// Remember this widget
self.write(|ctx| {
let viewport = ctx.viewport();
// We add all widgets here, even non-interactive ones,
// because we need this list not only for checking for blocking widgets,
// but also to know when we have reached the widget we are checking for cover.
viewport.widgets_this_frame.insert(w.layer_id, w);
// We add all widgets here, even non-interactive ones,
// because we need this list not only for checking for blocking widgets,
// but also to know when we have reached the widget we are checking for cover.
viewport.widgets_this_frame.insert(w.layer_id, w);
if w.sense.focusable {
ctx.memory.interested_in_focus(w.id);
}
});
} else {
// Don't remember invisible widgets
}
if w.sense.focusable {
ctx.memory.interested_in_focus(w.id);
}
});
if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() {
// Not interested or allowed input:
@@ -1175,9 +1066,8 @@ impl Context {
let viewport = ctx.viewport();
viewport
.widgets_this_frame
.by_id
.get(&id)
.or_else(|| viewport.widgets_prev_frame.by_id.get(&id))
.get(id)
.or_else(|| viewport.widgets_prev_frame.get(id))
.copied()
})
.map(|widget_rect| self.get_response(widget_rect))
@@ -1235,7 +1125,8 @@ impl Context {
let input = &viewport.input;
let memory = &mut ctx.memory;
if sense.click
if enabled
&& sense.click
&& memory.has_focus(id)
&& (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter))
{
@@ -1244,7 +1135,10 @@ impl Context {
}
#[cfg(feature = "accesskit")]
if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) {
if enabled
&& sense.click
&& input.has_accesskit_action_request(id, accesskit::Action::Default)
{
res.clicked[PointerButton::Primary as usize] = true;
}
@@ -1264,7 +1158,7 @@ impl Context {
for pointer_event in &input.pointer.pointer_events {
if let PointerEvent::Released { click, button } = pointer_event {
if sense.click && clicked {
if enabled && sense.click && clicked {
if let Some(click) = click {
res.clicked[*button as usize] = true;
res.double_clicked[*button as usize] = click.is_double();
@@ -1916,13 +1810,16 @@ impl Context {
#[cfg(debug_assertions)]
fn debug_painting(&self) {
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING);
painter.debug_rect(widget.interact_rect, color, text);
let rect = widget.interact_rect;
if rect.is_positive() {
let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING);
painter.debug_rect(rect, color, text);
}
};
let paint_widget_id = |id: Id, text: &str, color: Color32| {
if let Some(widget) =
self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned())
self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned())
{
paint_widget(&widget, text, color);
}
@@ -1931,8 +1828,8 @@ impl Context {
if self.style().debug.show_interactive_widgets {
// Show all interactive widgets:
let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone());
for (layer_id, rects) in rects.by_layer {
let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING);
for (layer_id, rects) in rects.layers() {
let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING);
for rect in rects {
if rect.sense.interactive() {
let (color, text) = if rect.sense.click && rect.sense.drag {
@@ -2432,9 +2329,7 @@ impl Context {
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.memory(|mem| {
mem.layer_id_at(pos, mem.options.style.interaction.resize_grab_radius_side)
})
self.memory(|mem| mem.layer_id_at(pos))
}
/// Moves the given area to the top in its [`Order`].

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -622,7 +622,7 @@ impl Margin {
};
#[inline]
pub fn same(margin: f32) -> Self {
pub const fn same(margin: f32) -> Self {
Self {
left: margin,
right: margin,
@@ -633,7 +633,7 @@ impl Margin {
/// Margins with the same size on opposing sides
#[inline]
pub fn symmetric(x: f32, y: f32) -> Self {
pub const fn symmetric(x: f32, y: f32) -> Self {
Self {
left: x,
right: x,
@@ -649,12 +649,12 @@ impl Margin {
}
#[inline]
pub fn left_top(&self) -> Vec2 {
pub const fn left_top(&self) -> Vec2 {
vec2(self.left, self.top)
}
#[inline]
pub fn right_bottom(&self) -> Vec2 {
pub const fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom)
}
@@ -702,6 +702,116 @@ impl std::ops::Add for Margin {
}
}
impl std::ops::Add<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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1391,6 +1391,11 @@ pub struct GridMark {
pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
let log_base = log_base as f64;
let step_sizes = move |input: GridInput| -> Vec<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
// to the next-bigger power of base
let smallest_visible_unit = next_power(input.base_step_size, log_base);
@@ -1693,7 +1698,7 @@ impl PreparedPlot {
/// assert_eq!(next_power(0.2, 10.0), 1);
/// ```
fn next_power(value: f64, base: f64) -> f64 {
assert_ne!(value, 0.0); // can be negative (typical for Y axis)
debug_assert_ne!(value, 0.0); // can be negative (typical for Y axis)
base.powi(value.abs().log(base).ceil() as i32)
}
@@ -1708,7 +1713,7 @@ fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
/// 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)) {
assert!(max > min);
debug_assert!(max > min);
let first = (min / 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).
/// All times are in seconds.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct History<T> {
/// In elements, i.e. of `values.len()`.
/// 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)
}
/// 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

View File

@@ -1,5 +1,7 @@
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use crate::Vec2b;
/// A vector has a direction and length.
/// A [`Vec2`] is often used to represent a size.
///
@@ -86,6 +88,16 @@ impl From<&Vec2> for (f32, f32) {
}
}
impl From<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

View File

@@ -19,6 +19,30 @@ impl Vec2b {
pub fn any(&self) -> bool {
self.x || self.y
}
/// Are both `x` and `y` true?
#[inline]
pub fn all(&self) -> bool {
self.x && self.y
}
#[inline]
pub fn and(&self, other: impl Into<Self>) -> 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 {

View File

@@ -120,6 +120,16 @@ impl ColorImage {
Self { size, pixels }
}
/// Alternative method to `from_gray`.
/// Create a [`ColorImage`] from iterator over flat opaque gray data.
///
/// Panics if `size[0] * size[1] != gray_iter.len()`.
pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator<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]`
#[cfg(feature = "bytemuck")]
pub fn as_raw(&self) -> &[u8] {

View File

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

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.
///
/// If using a [`PaintCallback`], note that only the rect is scaled as opposed
@@ -387,6 +403,7 @@ impl Shape {
Self::Rect(rect_shape) => {
rect_shape.rect = transform * rect_shape.rect;
rect_shape.stroke.width *= transform.scaling;
rect_shape.rounding *= transform.scaling;
}
Self::Text(text_shape) => {
text_shape.pos = transform * text_shape.pos;
@@ -701,7 +718,7 @@ impl Rounding {
};
#[inline]
pub fn same(radius: f32) -> Self {
pub const fn same(radius: f32) -> Self {
Self {
nw: radius,
ne: radius,
@@ -739,6 +756,130 @@ impl Rounding {
}
}
impl std::ops::Add for Rounding {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
}
}
}
impl std::ops::AddAssign for Rounding {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
};
}
}
impl std::ops::AddAssign<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.

View File

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

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