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:
@@ -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 |
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user