1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Merge branch 'master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2024-12-19 23:21:09 +01:00
committed by GitHub
138 changed files with 2386 additions and 1394 deletions

View File

@@ -62,10 +62,6 @@ mint = ["epaint/mint"]
## Enable persistence of memory (window positions etc).
persistence = ["serde", "epaint/serde", "ron"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
puffin = ["dep:puffin", "epaint/puffin"]
## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon).
##
@@ -85,6 +81,7 @@ epaint = { workspace = true, default-features = false }
ahash.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
#! ### Optional dependencies
accesskit = { version = "0.17.0", optional = true }
@@ -95,10 +92,5 @@ backtrace = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
log = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
[dev-dependencies]
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -467,7 +467,7 @@ impl Area {
id: interact_id,
layer_id,
rect: state.rect(),
interact_rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),
sense,
enabled,
},

View File

@@ -93,8 +93,8 @@ pub fn show_tooltip_at_pointer<R>(
pointer_rect.min.x = pointer_pos.x;
// Transform global coords to layer coords:
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
pointer_rect = transform.inverse() * pointer_rect;
if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) {
pointer_rect = from_global * pointer_rect;
}
show_tooltip_at_dyn(
@@ -162,8 +162,8 @@ fn show_tooltip_at_dyn<'c, R>(
) -> R {
// Transform layer coords to global coords:
let mut widget_rect = *widget_rect;
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
widget_rect = transform * widget_rect;
if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) {
widget_rect = to_global * widget_rect;
}
remember_that_tooltip_was_shown(ctx);
@@ -404,11 +404,12 @@ pub fn popup_above_or_below_widget<R>(
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
};
if let Some(transform) = parent_ui
if let Some(to_global) = parent_ui
.ctx()
.memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied())
.layer_transform_to_global(parent_ui.layer_id())
{
pos = transform * pos;
pos = to_global * pos;
}
let frame = Frame::popup(parent_ui.style());

View File

@@ -205,7 +205,7 @@ struct Prepared {
}
impl Resize {
fn begin(&mut self, ui: &mut Ui) -> Prepared {
fn begin(&self, ui: &mut Ui) -> Prepared {
let position = ui.available_rect_before_wrap().min;
let id = self.id.unwrap_or_else(|| {
let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize"));
@@ -295,7 +295,7 @@ impl Resize {
}
}
pub fn show<R>(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
let mut prepared = self.begin(ui);
let ret = add_contents(&mut prepared.content_ui);
self.end(ui, prepared);

View File

@@ -109,13 +109,13 @@ struct Plugins {
impl Plugins {
fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) {
crate::profile_scope!("plugins", _cb_name);
profiling::scope!("plugins", _cb_name);
for NamedContextCallback {
debug_name: _name,
callback,
} in callbacks
{
crate::profile_scope!("plugin", _name);
profiling::scope!("plugin", _name);
(callback)(ctx);
}
}
@@ -498,19 +498,8 @@ impl ContextImpl {
viewport.this_pass.begin_pass(screen_rect);
{
let area_order = self.memory.areas().order_map();
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
layers.sort_by(|a, b| {
if a.order == b.order {
// Maybe both are windows, so respect area order:
area_order.get(a).cmp(&area_order.get(b))
} else {
// comparing e.g. background to tooltips
a.order.cmp(&b.order)
}
});
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
let interact_radius = self.memory.options.style().interaction.interact_radius;
@@ -518,7 +507,7 @@ impl ContextImpl {
crate::hit_test::hit_test(
&viewport.prev_pass.widgets,
&layers,
&self.memory.layer_transforms,
&self.memory.to_global,
pos,
interact_radius,
)
@@ -549,7 +538,7 @@ impl ContextImpl {
#[cfg(feature = "accesskit")]
if self.is_accesskit_enabled {
crate::profile_scope!("accesskit");
profiling::scope!("accesskit");
use crate::pass_state::AccessKitPassState;
let id = crate::accesskit_root_id();
let mut root_node = accesskit::Node::new(accesskit::Role::Window);
@@ -568,8 +557,7 @@ impl ContextImpl {
/// Load fonts unless already loaded.
fn update_fonts_mut(&mut self) {
crate::profile_function!();
profiling::function_scope!();
let input = &self.viewport().input;
let pixels_per_point = input.pixels_per_point();
let max_texture_side = input.max_texture_side;
@@ -616,7 +604,7 @@ impl ContextImpl {
log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}");
is_new = true;
crate::profile_scope!("Fonts::new");
profiling::scope!("Fonts::new");
Fonts::new(
pixels_per_point,
max_texture_side,
@@ -625,12 +613,12 @@ impl ContextImpl {
});
{
crate::profile_scope!("Fonts::begin_pass");
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side);
}
if is_new && self.memory.options.preload_font_glyphs {
crate::profile_scope!("preload_font_glyphs");
profiling::scope!("preload_font_glyphs");
// Preload the most common characters for the most common fonts.
// This is not very important to do, but may save a few GPU operations.
for font_id in self.memory.options.style().text_styles.values() {
@@ -812,8 +800,7 @@ impl Context {
/// ```
#[must_use]
pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
crate::profile_function!();
profiling::function_scope!();
let viewport_id = new_input.viewport_id;
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
@@ -821,9 +808,13 @@ impl Context {
debug_assert_eq!(output.platform_output.num_completed_passes, 0);
loop {
crate::profile_scope!(
profiling::scope!(
"pass",
output.platform_output.num_completed_passes.to_string()
output
.platform_output
.num_completed_passes
.to_string()
.as_str()
);
// We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`]
@@ -886,7 +877,7 @@ impl Context {
/// // handle full_output
/// ```
pub fn begin_pass(&self, new_input: RawInput) {
crate::profile_function!();
profiling::function_scope!();
self.write(|ctx| ctx.begin_pass(new_input));
@@ -1329,11 +1320,11 @@ impl Context {
res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped;
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(transform), Some(pos)) = (
memory.layer_transforms.get(&res.layer_id),
if let (Some(to_global), Some(pos)) = (
memory.to_global.get(&res.layer_id),
&mut res.interact_pointer_pos,
) {
*pos = transform.inverse() * *pos;
*pos = to_global.inverse() * *pos;
}
}
@@ -1760,7 +1751,7 @@ impl Context {
/// The new fonts will become active at the start of the next pass.
/// This will overwrite the existing fonts.
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
@@ -1788,7 +1779,7 @@ impl Context {
/// The new font will become active at the start of the next pass.
/// This will keep the existing fonts.
pub fn add_font(&self, new_font: FontInsert) {
crate::profile_function!();
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
@@ -2152,7 +2143,7 @@ impl Context {
/// Call at the end of each frame if you called [`Context::begin_pass`].
#[must_use]
pub fn end_pass(&self) -> FullOutput {
crate::profile_function!();
profiling::function_scope!();
if self.options(|o| o.zoom_with_keyboard) {
crate::gui_zoom::zoom_with_keyboard(self);
@@ -2246,7 +2237,8 @@ impl Context {
for id in contains_pointer {
let mut widget_text = format!("{id:?}");
if let Some(rect) = widget_rects.get(id) {
widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense);
widget_text +=
&format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense);
}
if let Some(info) = widget_rects.info(id) {
widget_text += &format!(" {info:?}");
@@ -2272,11 +2264,17 @@ impl Context {
if self.style().debug.show_widget_hits {
let hits = self.write(|ctx| ctx.viewport().hits.clone());
let WidgetHits {
close,
contains_pointer,
click,
drag,
} = hits;
if false {
for widget in &close {
paint_widget(widget, "close", Color32::from_gray(70));
}
}
if true {
for widget in &contains_pointer {
paint_widget(widget, "contains_pointer", Color32::BLUE);
@@ -2342,7 +2340,7 @@ impl ContextImpl {
// https://github.com/emilk/egui/issues/3664
// at the cost of a lot of performance.
// (This will override any smaller delta that was uploaded above.)
crate::profile_scope!("full_font_atlas_update");
profiling::scope!("full_font_atlas_update");
let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options());
tex_mngr.set(TextureId::default(), full_delta);
}
@@ -2356,7 +2354,7 @@ impl ContextImpl {
#[cfg(feature = "accesskit")]
{
crate::profile_scope!("accesskit");
profiling::scope!("accesskit");
let state = viewport.this_pass.accesskit_state.take();
if let Some(state) = state {
let root_id = crate::accesskit_root_id().accesskit_id();
@@ -2381,12 +2379,12 @@ impl ContextImpl {
let shapes = viewport
.graphics
.drain(self.memory.areas().order(), &self.memory.layer_transforms);
.drain(self.memory.areas().order(), &self.memory.to_global);
let mut repaint_needed = false;
if self.memory.options.repaint_on_widget_change {
crate::profile_function!("compare-widget-rects");
profiling::scope!("compare-widget-rects");
if viewport.prev_pass.widgets != viewport.this_pass.widgets {
repaint_needed = true; // Some widget has moved
}
@@ -2525,7 +2523,7 @@ impl Context {
shapes: Vec<ClippedShape>,
pixels_per_point: f32,
) -> Vec<ClippedPrimitive> {
crate::profile_function!();
profiling::function_scope!();
// A tempting optimization is to reuse the tessellation from last frame if the
// shapes are the same, but just comparing the shapes takes about 50% of the time
@@ -2552,7 +2550,7 @@ impl Context {
let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_primitives = {
crate::profile_scope!("tessellator::tessellate_shapes");
profiling::scope!("tessellator::tessellate_shapes");
tessellator::Tessellator::new(
pixels_per_point,
tessellation_options,
@@ -2697,6 +2695,7 @@ impl Context {
/// Transform the graphics of the given layer.
///
/// This will also affect input.
/// The direction of the given transform is "into the global coordinate system".
///
/// This is a sticky setting, remembered from one frame to the next.
///
@@ -2706,13 +2705,28 @@ impl Context {
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
self.memory_mut(|m| {
if transform == TSTransform::IDENTITY {
m.layer_transforms.remove(&layer_id)
m.to_global.remove(&layer_id)
} else {
m.layer_transforms.insert(layer_id, transform)
m.to_global.insert(layer_id, transform)
}
});
}
/// Return how to transform the graphics of the given layer into the global coordinate system.
///
/// Set this with [`Self::layer_transform_to_global`].
pub fn layer_transform_to_global(&self, layer_id: LayerId) -> Option<TSTransform> {
self.memory(|m| m.to_global.get(&layer_id).copied())
}
/// Return how to transform the graphics of the global coordinate system into the local coordinate system of the given layer.
///
/// This returns the inverse of [`Self::layer_transform_to_global`].
pub fn layer_transform_from_global(&self, layer_id: LayerId) -> Option<TSTransform> {
self.layer_transform_to_global(layer_id)
.map(|t| t.inverse())
}
/// Move all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
@@ -2777,12 +2791,11 @@ impl Context {
///
/// See also [`Response::contains_pointer`].
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
let rect =
if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) {
transform * rect
} else {
rect
};
let rect = if let Some(to_global) = self.layer_transform_to_global(layer_id) {
to_global * rect
} else {
rect
};
if !rect.is_positive() {
return false;
}
@@ -3144,28 +3157,26 @@ impl Context {
self.memory_mut(|mem| *mem.areas_mut() = Default::default());
}
});
ui.indent("areas", |ui| {
ui.label("Visible areas, ordered back to front.");
ui.label("Hover to highlight");
ui.indent("layers", |ui| {
ui.label("Layers, ordered back to front.");
let layers_ids: Vec<LayerId> = self.memory(|mem| mem.areas().order().to_vec());
for layer_id in layers_ids {
let area = AreaState::load(self, layer_id.id);
if let Some(area) = area {
if let Some(area) = AreaState::load(self, layer_id.id) {
let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id));
if !is_visible {
continue;
}
let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),);
// TODO(emilk): `Sense::hover_highlight()`
if ui
.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()))
.hovered
&& is_visible
{
let response =
ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()));
if response.hovered && is_visible {
ui.ctx()
.debug_painter()
.debug_rect(area.rect(), Color32::RED, "");
}
} else {
ui.monospace(layer_id.short_debug_format());
}
}
});
@@ -3353,7 +3364,7 @@ impl Context {
pub fn forget_image(&self, uri: &str) {
use load::BytesLoader as _;
crate::profile_function!();
profiling::function_scope!();
let loaders = self.loaders();
@@ -3375,7 +3386,7 @@ impl Context {
pub fn forget_all_images(&self) {
use load::BytesLoader as _;
crate::profile_function!();
profiling::function_scope!();
let loaders = self.loaders();
@@ -3410,7 +3421,7 @@ impl Context {
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let bytes_loaders = loaders.bytes.lock();
@@ -3447,7 +3458,7 @@ impl Context {
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let image_loaders = loaders.image.lock();
@@ -3498,7 +3509,7 @@ impl Context {
texture_options: TextureOptions,
size_hint: load::SizeHint,
) -> load::TextureLoadResult {
crate::profile_function!(uri);
profiling::function_scope!(uri);
let loaders = self.loaders();
let texture_loaders = loaders.texture.lock();
@@ -3516,7 +3527,7 @@ impl Context {
/// The loaders of bytes, images, and textures.
pub fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!();
profiling::function_scope!();
self.read(|this| this.loaders.clone())
}
}
@@ -3648,7 +3659,7 @@ impl Context {
viewport_builder: ViewportBuilder,
viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static,
) {
crate::profile_function!();
profiling::function_scope!();
if self.embed_viewports() {
viewport_ui_cb(self, ViewportClass::Embedded);
@@ -3700,7 +3711,7 @@ impl Context {
builder: ViewportBuilder,
mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T,
) -> T {
crate::profile_function!();
profiling::function_scope!();
if self.embed_viewports() {
return viewport_ui_cb(self, ViewportClass::Embedded);

View File

@@ -27,31 +27,44 @@ impl DragAndDrop {
ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass));
}
/// Interrupt drag-and-drop if the user presses the escape key.
///
/// This needs to happen at frame start so we can properly capture the escape key.
fn begin_pass(ctx: &Context) {
let has_any_payload = Self::has_any_payload(ctx);
if has_any_payload {
let abort_dnd = ctx.input_mut(|i| {
i.pointer.any_released()
|| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)
});
let abort_dnd_due_to_escape_key =
ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
if abort_dnd {
if abort_dnd_due_to_escape_key {
Self::clear_payload(ctx);
}
}
}
/// Interrupt drag-and-drop if the user releases the mouse button.
///
/// This is a catch-all safety net in case user code doesn't capture the drag payload itself.
/// This must happen at end-of-frame such that we don't shadow the mouse release event from user
/// code.
fn end_pass(ctx: &Context) {
let mut is_dragging = false;
let has_any_payload = Self::has_any_payload(ctx);
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
is_dragging = state.payload.is_some();
});
if has_any_payload {
let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released());
if is_dragging {
ctx.set_cursor_icon(CursorIcon::Grabbing);
if abort_dnd_due_to_mouse_release {
Self::clear_payload(ctx);
} else {
// We set the cursor icon only if its default, as the user code might have
// explicitly set it already.
ctx.output_mut(|o| {
if o.cursor_icon == CursorIcon::Default {
o.cursor_icon = CursorIcon::Grabbing;
}
});
}
}
}

View File

@@ -227,7 +227,7 @@ impl GridLayout {
self.col += 1;
}
fn paint_row(&mut self, cursor: &Rect, painter: &Painter) {
fn paint_row(&self, cursor: &Rect, painter: &Painter) {
// handle row color painting based on color-picker function
let Some(color_picker) = self.color_picker.as_ref() else {
return;
@@ -450,7 +450,7 @@ impl Grid {
ui.allocate_new_ui(ui_builder, |ui| {
ui.horizontal(|ui| {
let is_color = color_picker.is_some();
let mut grid = GridLayout {
let grid = GridLayout {
num_columns,
color_picker,
min_cell_size: vec2(min_col_width, min_row_height),

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects};
use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -12,11 +12,18 @@ use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects};
/// or if we're currently already dragging something.
#[derive(Clone, Debug, Default)]
pub struct WidgetHits {
/// All widgets close to the pointer, back-to-front.
///
/// This is a superset of all other widgets in this struct.
pub close: Vec<WidgetRect>,
/// All widgets that contains the pointer, back-to-front.
///
/// i.e. both a Window and the button in it can contain the pointer.
/// i.e. both a Window and the Button in it can contain the pointer.
///
/// Some of these may be widgets in a layer below the top-most layer.
///
/// This will be used for hovering.
pub contains_pointer: Vec<WidgetRect>,
/// If the user would start a clicking now, this is what would be clicked.
@@ -35,18 +42,18 @@ pub struct WidgetHits {
pub fn hit_test(
widgets: &WidgetRects,
layer_order: &[LayerId],
layer_transforms: &HashMap<LayerId, TSTransform>,
layer_to_global: &HashMap<LayerId, TSTransform>,
pos: Pos2,
search_radius: f32,
) -> WidgetHits {
crate::profile_function!();
profiling::function_scope!();
let search_radius_sq = search_radius * search_radius;
// Transform the position into the local coordinate space of each layer:
let pos_in_layers: HashMap<LayerId, Pos2> = layer_transforms
let pos_in_layers: HashMap<LayerId, Pos2> = layer_to_global
.iter()
.map(|(layer_id, t)| (*layer_id, t.inverse() * pos))
.map(|(layer_id, to_global)| (*layer_id, to_global.inverse() * pos))
.collect();
let mut closest_dist_sq = f32::INFINITY;
@@ -63,6 +70,7 @@ pub fn hit_test(
}
let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos);
// TODO(emilk): we should probably do the distance testing in global space instead
let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer);
// In tie, pick last = topmost.
@@ -76,51 +84,103 @@ pub fn hit_test(
.copied()
.collect();
// We need to pick one single layer for the interaction.
if let Some(closest_hit) = closest_hit {
// Select the top layer, and ignore widgets in any other layer:
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;
}
// Transform to global coordinates:
for hit in &mut close {
if let Some(to_global) = layer_to_global.get(&hit.layer_id).copied() {
*hit = hit.transform(to_global);
}
let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos);
let hits = hit_test_on_close(&close, pos_in_layer);
if let Some(drag) = hits.drag {
debug_assert!(drag.sense.drag);
}
if let Some(click) = hits.click {
debug_assert!(click.sense.click);
}
hits
} else {
// No close widgets.
Default::default()
}
}
fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![allow(clippy::collapsible_else_if)]
// When using layer transforms it is common to stack layers close to each other.
// For instance, you may have a resize-separator on a panel, with two
// transform-layers on either side.
// The resize-separator is technically in a layer _behind_ the transform-layers,
// but the user doesn't perceive it as such.
// So how do we handle this case?
//
// If we just allow interactions with ALL close widgets,
// then we might accidentally allow clicks through windows and other bad stuff.
//
// Let's try this:
// * Set up a hit-area (based on search_radius)
// * Iterate over all hits top-to-bottom
// * Stop if any hit covers the whole hit-area, otherwise keep going
// * Collect the layers ids in a set
// * Remove all widgets not in the above layer set
//
// This will most often result in only one layer,
// but if the pointer is at the edge of a layer, we might include widgets in
// a layer behind it.
// Only those widgets directly under the `pos`.
let hits: Vec<WidgetRect> = close
let mut included_layers: ahash::HashSet<LayerId> = Default::default();
for hit in close.iter().rev() {
included_layers.insert(hit.layer_id);
let hit_covers_search_area = contains_circle(hit.interact_rect, pos, search_radius);
if hit_covers_search_area {
break; // nothing behind this layer could ever be interacted with
}
}
close.retain(|hit| included_layers.contains(&hit.layer_id));
// If a 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 mut hits = hit_test_on_close(&close, pos);
hits.contains_pointer = close
.iter()
.filter(|widget| widget.interact_rect.contains(pos))
.copied()
.collect();
let hit_click = hits.iter().copied().filter(|w| w.sense.click).last();
let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last();
hits.close = close;
{
// Undo the to_global-transform we applied earlier,
// go back to local layer-coordinates:
let restore_widget_rect = |w: &mut WidgetRect| {
*w = widgets.get(w.id).copied().unwrap_or(*w);
};
for wr in &mut hits.close {
restore_widget_rect(wr);
}
for wr in &mut hits.contains_pointer {
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.drag {
debug_assert!(wr.sense.drag);
restore_widget_rect(wr);
}
if let Some(wr) = &mut hits.click {
debug_assert!(wr.sense.click);
restore_widget_rect(wr);
}
}
hits
}
/// Returns true if the rectangle contains the whole circle.
fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool {
interact_rect.shrink(radius).contains(pos)
}
fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![allow(clippy::collapsible_else_if)]
// First find the best direct hits:
let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0);
let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0);
match (hit_click, hit_drag) {
(None, None) => {
@@ -136,16 +196,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
if let Some(closest) = closest {
WidgetHits {
contains_pointer: hits,
click: closest.sense.click.then_some(closest),
drag: closest.sense.drag.then_some(closest),
..Default::default()
}
} else {
// Found nothing
WidgetHits {
contains_pointer: hits,
click: None,
drag: None,
..Default::default()
}
}
}
@@ -170,17 +230,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// This is a smaller thing on a big background - help the user hit it,
// and ignore the big drag background.
WidgetHits {
contains_pointer: hits,
click: Some(closest_click),
drag: Some(closest_click),
..Default::default()
}
} else {
// The drag wiudth is separate from the click wiudth,
// so return only the drag widget
// The drag-widget is separate from the click-widget,
// so return only the drag-widget
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
} else {
@@ -194,17 +254,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// The drag widget is a big background thing (scroll area),
// so returning a separate click widget should not be confusing
WidgetHits {
contains_pointer: hits,
click: Some(closest_click),
drag: Some(hit_drag),
..Default::default()
}
} else {
// The two widgets are just two normal small widgets close to each other.
// Highlighting both would be very confusing.
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -229,17 +289,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// `hit_drag` is a big background thing and `closest_drag` is something small on top of it.
// Be helpful and return the small things:
return WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(closest_drag),
..Default::default()
};
}
}
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -253,57 +313,57 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
// where when hovering directly over a drag-widget (like a big ScrollArea),
// we look for close click-widgets (e.g. buttons).
// This is because big background drag-widgets (ScrollArea, Window) are common,
// but bit clickable things aren't.
// but big clickable things aren't.
// Even if they were, I think it would be confusing for a user if clicking
// a drag-only widget would click something _behind_ it.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: None,
..Default::default()
}
}
(Some(hit_click), Some(hit_drag)) => {
// We have a perfect hit on both click and drag. Which is the topmost?
let click_idx = hits.iter().position(|w| *w == hit_click).unwrap();
let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap();
let click_idx = close.iter().position(|w| *w == hit_click).unwrap();
let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap();
let click_is_on_top_of_drag = drag_idx < click_idx;
if click_is_on_top_of_drag {
if hit_click.sense.drag {
// The top thing senses both clicks and drags.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: Some(hit_click),
..Default::default()
}
} else {
// They are interested in different things,
// and click is on top. Report both hits,
// e.g. the top Button and the ScrollArea behind it.
WidgetHits {
contains_pointer: hits,
click: Some(hit_click),
drag: Some(hit_drag),
..Default::default()
}
}
} else {
if hit_drag.sense.click {
// The top thing senses both clicks and drags.
WidgetHits {
contains_pointer: hits,
click: Some(hit_drag),
drag: Some(hit_drag),
..Default::default()
}
} else {
// The top things senses only drags,
// so we ignore the click-widget, because it would be confusing
// if clicking a drag-widget would actually click something else below it.
WidgetHits {
contains_pointer: hits,
click: None,
drag: Some(hit_drag),
..Default::default()
}
}
}
@@ -312,8 +372,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
}
fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<WidgetRect> {
let mut closest = None;
let mut closest_dist_sq = f32::INFINITY;
find_closest_within(widgets, pos, f32::INFINITY)
}
fn find_closest_within(
widgets: impl Iterator<Item = WidgetRect>,
pos: Pos2,
max_dist: f32,
) -> Option<WidgetRect> {
let mut closest: Option<WidgetRect> = None;
let mut closest_dist_sq = max_dist * max_dist;
for widget in widgets {
if widget.interact_rect.is_negative() {
continue;
@@ -321,6 +389,16 @@ fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<
let dist_sq = widget.interact_rect.distance_sq_to_pos(pos);
if let Some(closest) = closest {
if dist_sq == closest_dist_sq {
// It's a tie! Pick the thin candidate over the thick one.
// This makes it easier to hit a thin resize-handle, for instance:
if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) {
continue;
}
}
}
// In case of a tie, take the last one = the one on top.
if dist_sq <= closest_dist_sq {
closest_dist_sq = dist_sq;
@@ -331,6 +409,27 @@ fn find_closest(widgets: impl Iterator<Item = WidgetRect>, pos: Pos2) -> Option<
closest
}
/// Should we prioritizie hits on `back` over those on `front`?
///
/// `back` should be behind the `front` widget.
///
/// Returns true if `back` is a small hit-target and `front` is not.
fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool {
if front.contains_rect(back) {
return false; // back widget is fully occluded; no way to hit it
}
// Reduce each rect to its width or height, whichever is smaller:
let back = back.width().min(back.height());
let front = front.width().min(front.height());
// These are hard-coded heuristics that could surely be improved.
let back_is_much_thinner = back <= 0.5 * front;
let back_is_thin = back <= 16.0;
back_is_much_thinner && back_is_thin
}
#[cfg(test)]
mod tests {
use emath::{pos2, vec2, Rect};

View File

@@ -269,7 +269,7 @@ impl InputState {
pixels_per_point: f32,
options: &crate::Options,
) -> Self {
crate::profile_function!();
profiling::function_scope!();
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
let unstable_dt = (time - self.time) as f32;

View File

@@ -113,7 +113,7 @@ pub(crate) fn interact(
input: &InputState,
interaction: &mut InteractionState,
) -> InteractionSnapshot {
crate::profile_function!();
profiling::function_scope!();
if let Some(id) = interaction.potential_click_id {
if !widgets.contains(id) {
@@ -249,7 +249,7 @@ pub(crate) fn interact(
.copied()
.collect()
} else {
// We may be hovering a an interactive widget or two.
// We may be hovering an interactive widget or two.
// We must also consider the case where non-interactive widgets
// are _on top_ of an interactive widget.
// For instance: a label in a draggable window.
@@ -264,9 +264,9 @@ pub(crate) fn interact(
// but none below it (an interactive widget stops the hover search).
//
// To know when to stop we need to first know the order of the widgets,
// which luckily we have in the `WidgetRects`.
// which luckily we already have in `hits.close`.
let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer
let order = |id| hits.close.iter().position(|w| w.id == id);
let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0);
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);

View File

@@ -11,9 +11,6 @@ pub enum Order {
/// Painted behind all floating windows
Background,
/// Special layer between panels and windows
PanelResizeLine,
/// Normal moveable windows that you reorder by click
Middle,
@@ -30,10 +27,9 @@ pub enum Order {
}
impl Order {
const COUNT: usize = 6;
const COUNT: usize = 5;
const ALL: [Self; Self::COUNT] = [
Self::Background,
Self::PanelResizeLine,
Self::Middle,
Self::Foreground,
Self::Tooltip,
@@ -44,12 +40,9 @@ impl Order {
#[inline(always)]
pub fn allow_interaction(&self) -> bool {
match self {
Self::Background
| Self::PanelResizeLine
| Self::Middle
| Self::Foreground
| Self::Tooltip
| Self::Debug => true,
Self::Background | Self::Middle | Self::Foreground | Self::Tooltip | Self::Debug => {
true
}
}
}
@@ -57,7 +50,6 @@ impl Order {
pub fn short_debug_format(&self) -> &'static str {
match self {
Self::Background => "backg",
Self::PanelResizeLine => "panel",
Self::Middle => "middl",
Self::Foreground => "foreg",
Self::Tooltip => "toolt",
@@ -68,7 +60,7 @@ impl Order {
/// An identifier for a paint layer.
/// Also acts as an identifier for [`crate::Area`]:s.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct LayerId {
pub order: Order,
@@ -110,6 +102,13 @@ impl LayerId {
}
}
impl std::fmt::Debug for LayerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { order, id } = self;
write!(f, "LayerId {{ {order:?} {id:?} }}")
}
}
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -221,9 +220,9 @@ impl GraphicLayers {
pub fn drain(
&mut self,
area_order: &[LayerId],
transforms: &ahash::HashMap<LayerId, TSTransform>,
to_global: &ahash::HashMap<LayerId, TSTransform>,
) -> Vec<ClippedShape> {
crate::profile_function!();
profiling::function_scope!();
let mut all_shapes: Vec<_> = Default::default();
@@ -239,10 +238,10 @@ impl GraphicLayers {
for layer_id in area_order {
if layer_id.order == order {
if let Some(list) = order_map.get_mut(&layer_id.id) {
if let Some(transform) = transforms.get(layer_id) {
if let Some(to_global) = to_global.get(layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *transform * clipped_shape.clip_rect;
clipped_shape.shape.transform(*transform);
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
}
}
all_shapes.append(&mut list.0);
@@ -254,10 +253,10 @@ impl GraphicLayers {
for (id, list) in order_map {
let layer_id = LayerId::new(order, *id);
if let Some(transform) = transforms.get(&layer_id) {
if let Some(to_global) = to_global.get(&layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *transform * clipped_shape.clip_rect;
clipped_shape.shape.transform(*transform);
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
}
}

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.79.0 or later to use `egui`.
//! You need to have rust 1.80.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
@@ -388,6 +388,18 @@
//! ## Installing additional fonts
//! The default egui fonts only support latin and cryllic characters, and some emojis.
//! To use egui with e.g. asian characters you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`].
//!
//! ## Instrumentation
//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation.
//! You can enable features on the profiling crates in your application to add instrumentation for all
//! crates that support it, including egui. See the profiling crate docs for more information.
//! ```toml
//! [dependencies]
//! profiling = "1.0"
//! [features]
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
@@ -691,33 +703,3 @@ pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}
// ---------------------------------------------------------------------------
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::{profile_function, profile_scope};

View File

@@ -95,8 +95,13 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
/// Transforms per layer
pub layer_transforms: HashMap<LayerId, TSTransform>,
/// Transforms per layer.
///
/// Instead of using this directly, use:
/// * [`crate::Context::set_transform_layer`]
/// * [`crate::Context::layer_transform_to_global`]
/// * [`crate::Context::layer_transform_from_global`]
pub to_global: HashMap<LayerId, TSTransform>,
// -------------------------------------------------
// Per-viewport:
@@ -120,7 +125,7 @@ impl Default for Memory {
focus: Default::default(),
viewport_id: Default::default(),
areas: Default::default(),
layer_transforms: Default::default(),
to_global: Default::default(),
popup: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
@@ -774,7 +779,7 @@ impl Focus {
impl Memory {
pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
crate::profile_function!();
profiling::function_scope!();
self.viewport_id = new_raw_input.viewport_id;
@@ -819,7 +824,7 @@ impl Memory {
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
self.areas()
.layer_id_at(pos, &self.layer_transforms)
.layer_id_at(pos, &self.to_global)
.and_then(|layer_id| {
if self.is_above_modal_layer(layer_id) {
Some(layer_id)
@@ -829,6 +834,12 @@ impl Memory {
})
}
/// The currently set transform of a layer.
#[deprecated = "Use `Context::layer_transform_to_global` instead"]
pub fn layer_transforms(&self, layer_id: LayerId) -> Option<TSTransform> {
self.to_global.get(&layer_id).copied()
}
/// An iterator over all layers. Back-to-front, top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas().order().iter().copied()
@@ -1121,15 +1132,18 @@ type OrderMap = HashMap<LayerId, usize>;
pub struct Areas {
areas: IdMap<area::AreaState>,
visible_areas_last_frame: ahash::HashSet<LayerId>,
visible_areas_current_frame: ahash::HashSet<LayerId>,
// ----------------------------
// Everything below this is general to all layers, not just areas.
// TODO(emilk): move this to a separate struct.
/// Back-to-front, top is last.
order: Vec<LayerId>,
/// Actual order of the layers, pre-calculated each frame.
/// Inverse of [`Self::order`], calculated at the end of the frame.
order_map: OrderMap,
visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,
/// When an area wants to be on top, it is assigned here.
/// This is used to reorder the layers at the end of the frame.
/// If several layers want to be on top, they will keep their relative order.
@@ -1137,9 +1151,9 @@ pub struct Areas {
/// results in them being sent to the top and keeping their previous internal order.
wants_to_be_on_top: ahash::HashSet<LayerId>,
/// List of sublayers for each layer.
/// The sublayers that each layer has.
///
/// When a layer has sublayers, they are moved directly above it in the ordering.
/// The parent sublayer is moved directly above the child sublayers in the ordering.
sublayers: ahash::HashMap<LayerId, HashSet<LayerId>>,
}
@@ -1152,17 +1166,13 @@ impl Areas {
self.areas.get(&id)
}
/// Back-to-front, top is last.
/// All layers back-to-front, top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
}
/// For each layer, which [`Self::order`] is it in?
pub(crate) fn order_map(&self) -> &OrderMap {
&self.order_map
}
/// Compare the order of two layers, based on the order list from last frame.
///
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) {
@@ -1172,18 +1182,8 @@ impl Areas {
}
}
/// Calculates the order map.
fn calculate_order_map(&mut self) {
self.order_map = self
.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
}
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
self.visible_current_frame.insert(layer_id);
self.visible_areas_current_frame.insert(layer_id);
self.areas.insert(layer_id.id, state);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
@@ -1194,15 +1194,15 @@ impl Areas {
pub fn layer_id_at(
&self,
pos: Pos2,
layer_transforms: &HashMap<LayerId, TSTransform>,
layer_to_global: &HashMap<LayerId, TSTransform>,
) -> Option<LayerId> {
for layer in self.order.iter().rev() {
if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) {
let mut rect = state.rect();
if state.interactable {
if let Some(transform) = layer_transforms.get(layer) {
rect = *transform * rect;
if let Some(to_global) = layer_to_global.get(layer) {
rect = *to_global * rect;
}
if rect.contains(pos) {
@@ -1216,18 +1216,19 @@ impl Areas {
}
pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id)
self.visible_areas_last_frame.contains(layer_id)
}
pub fn is_visible(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
self.visible_areas_last_frame.contains(layer_id)
|| self.visible_areas_current_frame.contains(layer_id)
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame
self.visible_areas_last_frame
.iter()
.copied()
.chain(self.visible_current_frame.iter().copied())
.chain(self.visible_areas_current_frame.iter().copied())
.collect()
}
@@ -1240,7 +1241,7 @@ impl Areas {
}
pub fn move_to_top(&mut self, layer_id: LayerId) {
self.visible_current_frame.insert(layer_id);
self.visible_areas_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
@@ -1255,8 +1256,21 @@ impl Areas {
///
/// This currently only supports one level of nesting. If `parent` is a sublayer of another
/// layer, the behavior is unspecified.
///
/// The two layers must have the same [`LayerId::order`].
pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) {
debug_assert_eq!(parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order);
self.sublayers.entry(parent).or_default().insert(child);
// Make sure the layers are in the order list:
if !self.order.iter().any(|x| *x == parent) {
self.order.push(parent);
}
if !self.order.iter().any(|x| *x == child) {
self.order.push(child);
}
}
pub fn top_layer_id(&self, order: Order) -> Option<LayerId> {
@@ -1267,26 +1281,42 @@ impl Areas {
.copied()
}
/// If this layer is the sublayer of another layer, return the parent.
pub fn parent_layer(&self, layer_id: LayerId) -> Option<LayerId> {
self.sublayers.iter().find_map(|(parent, children)| {
if children.contains(&layer_id) {
Some(*parent)
} else {
None
}
})
}
/// All the child layers of this layer.
pub fn child_layers(&self, layer_id: LayerId) -> impl Iterator<Item = LayerId> + '_ {
self.sublayers.get(&layer_id).into_iter().flatten().copied()
}
pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool {
self.sublayers
.iter()
.any(|(_, children)| children.contains(layer))
self.parent_layer(*layer).is_some()
}
pub(crate) fn end_pass(&mut self) {
let Self {
visible_last_frame,
visible_current_frame,
visible_areas_last_frame,
visible_areas_current_frame,
order,
wants_to_be_on_top,
sublayers,
..
} = self;
std::mem::swap(visible_last_frame, visible_current_frame);
visible_current_frame.clear();
std::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
visible_areas_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
wants_to_be_on_top.clear();
// For all layers with sublayers, put the sublayers directly after the parent layer:
let sublayers = std::mem::take(sublayers);
for (parent, children) in sublayers {
@@ -1304,7 +1334,13 @@ impl Areas {
};
order.splice(parent_pos..=parent_pos, moved_layers);
}
self.calculate_order_map();
self.order_map = self
.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
}
}

View File

@@ -406,11 +406,8 @@ impl MenuRoot {
}
}
if let Some(transform) = button
.ctx
.memory(|m| m.layer_transforms.get(&button.layer_id).copied())
{
pos = transform * pos;
if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) {
pos = to_global * pos;
}
return MenuResponse::Create(pos, id);

View File

@@ -248,7 +248,7 @@ impl Default for PassState {
impl PassState {
pub(crate) fn begin_pass(&mut self, screen_rect: Rect) {
crate::profile_function!();
profiling::function_scope!();
let Self {
used_ids,
widgets,

View File

@@ -392,11 +392,8 @@ impl Response {
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
let mut delta = self.ctx.input(|i| i.pointer.delta());
if let Some(scaling) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling))
{
delta /= scaling;
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
delta *= from_global.scaling;
}
delta
} else {
@@ -478,11 +475,8 @@ impl Response {
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?;
if let Some(transform) = self
.ctx
.memory(|m| m.layer_transforms.get(&self.layer_id).copied())
{
pos = transform.inverse() * pos;
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
pos = from_global * pos;
}
Some(pos)
} else {

View File

@@ -301,7 +301,7 @@ impl Ui {
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
};
let child_ui = Ui {
let mut child_ui = Ui {
id: stable_id,
unique_id,
next_auto_id_salt,
@@ -316,6 +316,10 @@ impl Ui {
min_rect_already_remembered: false,
};
if disabled {
child_ui.disable();
}
// 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 `remember_min_rect` is called
child_ui.ctx().create_widget(

View File

@@ -308,7 +308,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
use crate::Id;
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value.
/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value.
///
/// In other words, it maps `(Id, TypeId)` to any value you want.
///
@@ -574,7 +574,7 @@ struct PersistedMap(Vec<(u64, SerializedElement)>);
#[cfg(feature = "persistence")]
impl PersistedMap {
fn from_map(map: &IdTypeMap) -> Self {
crate::profile_function!();
profiling::function_scope!();
use std::collections::BTreeMap;
@@ -593,7 +593,7 @@ impl PersistedMap {
let max_bytes_per_type = map.max_bytes_per_type;
{
crate::profile_scope!("gather");
profiling::scope!("gather");
for (hash, element) in &map.map {
if let Some(element) = element.to_serialize() {
let stats = types_map.entry(element.type_id).or_default();
@@ -610,7 +610,7 @@ impl PersistedMap {
let mut persisted = vec![];
{
crate::profile_scope!("gc");
profiling::scope!("gc");
for stats in types_map.values() {
let mut bytes_written = 0;
@@ -634,7 +634,7 @@ impl PersistedMap {
}
fn into_map(self) -> IdTypeMap {
crate::profile_function!();
profiling::function_scope!();
let map = self
.0
.into_iter()
@@ -671,7 +671,7 @@ impl serde::Serialize for IdTypeMap {
where
S: serde::Serializer,
{
crate::profile_scope!("IdTypeMap::serialize");
profiling::scope!("IdTypeMap::serialize");
PersistedMap::from_map(self).serialize(serializer)
}
}
@@ -682,7 +682,7 @@ impl<'de> serde::Deserialize<'de> for IdTypeMap {
where
D: serde::Deserializer<'de>,
{
crate::profile_scope!("IdTypeMap::deserialize");
profiling::scope!("IdTypeMap::deserialize");
<PersistedMap>::deserialize(deserializer).map(PersistedMap::into_map)
}
}

View File

@@ -188,7 +188,7 @@ impl std::fmt::Debug for IconData {
impl From<IconData> for epaint::ColorImage {
fn from(icon: IconData) -> Self {
crate::profile_function!();
profiling::function_scope!();
let IconData {
rgba,
width,
@@ -200,7 +200,7 @@ impl From<IconData> for epaint::ColorImage {
impl From<&IconData> for epaint::ColorImage {
fn from(icon: &IconData) -> Self {
crate::profile_function!();
profiling::function_scope!();
let IconData {
rgba,
width,
@@ -1056,7 +1056,7 @@ pub enum ViewportCommand {
/// Enable mouse pass-through: mouse clicks pass through the window, used for non-interactable overlays.
MousePassthrough(bool),
/// Take a screenshot.
/// Take a screenshot of the next frame after this.
///
/// The results are returned in [`crate::Event::Screenshot`].
Screenshot(crate::UserData),

View File

@@ -20,10 +20,10 @@ pub struct WidgetRect {
/// What layer the widget is on.
pub layer_id: LayerId,
/// The full widget rectangle.
/// The full widget rectangle, in local layer coordinates.
pub rect: Rect,
/// Where the widget is.
/// Where the widget is, in local layer coordinates.
///
/// This is after clipping with the parent ui clip rect.
pub interact_rect: Rect,
@@ -42,6 +42,27 @@ pub struct WidgetRect {
pub enabled: bool,
}
impl WidgetRect {
pub fn transform(self, transform: emath::TSTransform) -> Self {
let Self {
id,
layer_id,
rect,
interact_rect,
sense,
enabled,
} = self;
Self {
id,
layer_id,
rect: transform * rect,
interact_rect: transform * interact_rect,
sense,
enabled,
}
}
}
/// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame.
///
/// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`]

View File

@@ -165,8 +165,11 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
/// * `x_value` - X axis, either saturation or value (0.0-1.0).
/// * `y_value` - Y axis, either saturation or value (0.0-1.0).
/// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider.
/// E.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: top-left: white \[s: 0.0, v: 1.0], top-right: fully saturated color \[s: 1.0, v: 1.0], bottom-right: black \[s: 0.0, v: 1.0].
///
/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows:
/// * top-left: white `[s: 0.0, v: 1.0]`
/// * top-right: fully saturated color `[s: 1.0, v: 1.0]`
/// * bottom-right: black `[s: 0.0, v: 1.0].`
fn color_slider_2d(
ui: &mut Ui,
x_value: &mut f32,

View File

@@ -766,14 +766,15 @@ impl<'t> TextEdit<'t> {
}
// Set IME output (in screen coords) when text is editable and visible
let transform = ui
.memory(|m| m.layer_transforms.get(&ui.layer_id()).copied())
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.ctx().output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: transform * rect,
cursor_rect: transform * primary_cursor_rect,
rect: to_global * rect,
cursor_rect: to_global * primary_cursor_rect,
});
});
}

View File

@@ -89,6 +89,7 @@ impl TextEditState {
self.undoer.lock().clone()
}
#[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability
pub fn set_undoer(&mut self, undoer: TextEditUndoer) {
*self.undoer.lock() = undoer;
}

View File

@@ -1,207 +0,0 @@
//! Tests the accesskit accessibility output of egui.
#![cfg(feature = "accesskit")]
use accesskit::{NodeId, Role, TreeUpdate};
use egui::{CentralPanel, Context, RawInput, Window};
/// Baseline test that asserts there are no spurious nodes in the
/// accesskit output when the ui is empty.
///
/// This gives reasonable certainty that any nodes appearing in the other accesskit outputs
/// are put there because of the widgets rendered.
#[test]
fn empty_ui_should_return_tree_with_only_root_window() {
let output = accesskit_output_single_egui_frame(|ctx| {
CentralPanel::default().show(ctx, |_| {});
});
assert_eq!(
output.nodes.len(),
1,
"Empty ui should produce only the root window."
);
let (id, root) = &output.nodes[0];
assert_eq!(*id, output.tree.unwrap().root);
assert_eq!(root.role(), Role::Window);
}
#[test]
fn button_node() {
let button_text = "This is a test button!";
let output = accesskit_output_single_egui_frame(|ctx| {
CentralPanel::default().show(ctx, |ui| ui.button(button_text));
});
assert_eq!(
output.nodes.len(),
2,
"Expected only the root node and the button."
);
let (_, button) = output
.nodes
.iter()
.find(|(_, node)| node.role() == Role::Button)
.expect("Button should exist in the accesskit output");
assert_eq!(button.label(), Some(button_text));
assert!(!button.is_disabled());
}
#[test]
fn disabled_button_node() {
let button_text = "This is a test button!";
let output = accesskit_output_single_egui_frame(|ctx| {
CentralPanel::default().show(ctx, |ui| {
ui.add_enabled(false, egui::Button::new(button_text))
});
});
assert_eq!(
output.nodes.len(),
2,
"Expected only the root node and the button."
);
let (_, button) = output
.nodes
.iter()
.find(|(_, node)| node.role() == Role::Button)
.expect("Button should exist in the accesskit output");
assert_eq!(button.label(), Some(button_text));
assert!(button.is_disabled());
}
#[test]
fn toggle_button_node() {
let button_text = "A toggle button";
let mut selected = false;
let output = accesskit_output_single_egui_frame(|ctx| {
CentralPanel::default().show(ctx, |ui| ui.toggle_value(&mut selected, button_text));
});
assert_eq!(
output.nodes.len(),
2,
"Expected only the root node and the button."
);
let (_, toggle) = output
.nodes
.iter()
.find(|(_, node)| node.role() == Role::Button)
.expect("Toggle button should exist in the accesskit output");
assert_eq!(toggle.label(), Some(button_text));
assert!(!toggle.is_disabled());
}
#[test]
fn multiple_disabled_widgets() {
let output = accesskit_output_single_egui_frame(|ctx| {
CentralPanel::default().show(ctx, |ui| {
ui.add_enabled_ui(false, |ui| {
let _ = ui.button("Button 1");
let _ = ui.button("Button 2");
let _ = ui.button("Button 3");
})
});
});
assert_eq!(
output.nodes.len(),
4,
"Expected the root node and all the child widgets."
);
assert_eq!(
output
.nodes
.iter()
.filter(|(_, node)| node.is_disabled())
.count(),
3,
"All widgets should be disabled."
);
}
#[test]
fn window_children() {
let output = accesskit_output_single_egui_frame(|ctx| {
let mut open = true;
Window::new("test window")
.open(&mut open)
.resizable(false)
.show(ctx, |ui| {
let _ = ui.button("A button");
});
});
let root = output.tree.as_ref().map(|tree| tree.root).unwrap();
let window_id = assert_window_exists(&output, "test window", root);
assert_button_exists(&output, "A button", window_id);
assert_button_exists(&output, "Close window", window_id);
assert_button_exists(&output, "Hide", window_id);
}
fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&Context)) -> TreeUpdate {
let ctx = Context::default();
// Disable animations, so we do not need to wait for animations to end to see the result.
ctx.style_mut(|style| style.animation_time = 0.0);
ctx.enable_accesskit();
let output = ctx.run(RawInput::default(), run_ui);
output
.platform_output
.accesskit_update
.expect("Missing accesskit update")
}
#[track_caller]
fn assert_button_exists(tree: &TreeUpdate, label: &str, parent: NodeId) {
let (node_id, _) = tree
.nodes
.iter()
.find(|(_, node)| {
!node.is_hidden() && node.role() == Role::Button && node.label() == Some(label)
})
.expect("No visible button with that label exists.");
assert_parent_child(tree, parent, *node_id);
}
#[track_caller]
fn assert_window_exists(tree: &TreeUpdate, title: &str, parent: NodeId) -> NodeId {
let (node_id, _) = tree
.nodes
.iter()
.find(|(_, node)| {
!node.is_hidden() && node.role() == Role::Window && node.label() == Some(title)
})
.expect("No visible window with that title exists.");
assert_parent_child(tree, parent, *node_id);
*node_id
}
#[track_caller]
fn assert_parent_child(tree: &TreeUpdate, parent: NodeId, child: NodeId) {
let (_, parent) = tree
.nodes
.iter()
.find(|(id, _)| id == &parent)
.expect("Parent does not exist.");
assert!(
parent.children().contains(&child),
"Node is not a child of the given parent."
);
}

View File

@@ -1,30 +0,0 @@
use egui::Button;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
#[test]
pub fn focus_should_skip_over_disabled_buttons() {
let mut harness = Harness::new_ui(|ui| {
ui.add(Button::new("Button 1"));
ui.add_enabled(false, Button::new("Button Disabled"));
ui.add(Button::new("Button 3"));
});
harness.press_key(egui::Key::Tab);
harness.run();
let button_1 = harness.get_by_label("Button 1");
assert!(button_1.is_focused());
harness.press_key(egui::Key::Tab);
harness.run();
let button_3 = harness.get_by_label("Button 3");
assert!(button_3.is_focused());
harness.press_key(egui::Key::Tab);
harness.run();
let button_1 = harness.get_by_label("Button 1");
assert!(button_1.is_focused());
}