mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Fix label selection in deferred viewports (#8242)
Label text selection before the fix in a deferred viewport: <img width="484" height="172" alt="before_the_fix" src="https://github.com/user-attachments/assets/2214a7d9-9585-497d-9920-dd336a7df7ea" /> After the fix: <img width="484" height="172" alt="after_the_fix" src="https://github.com/user-attachments/assets/0999ed8e-22d4-4109-a5b5-f468f99e692d" /> ## What changed - Keep label text-selection state separate for each viewport. - Route pass lifecycle and label painting through the current `ViewportId`. - Drop inactive per-viewport state after its pass. - Add a regression test that verifies an unrelated viewport pass cannot clear a child viewport's selection, while the owning viewport still clears selections whose labels disappear. ## Why Issue #4758 identified that deferred viewports need independent label-selection state. PR #4760 fixed it by keying the temporary state by viewport. The plugin refactor in PR #7385 moved that state into one context-wide `LabelSelectionState`, which accidentally removed the viewport isolation. A pass in another viewport then fails to encounter the selected widgets and clears the selection. This restores the behavior of #4760 within the current plugin architecture. Applications do not need any special handling.
This commit is contained in:
committed by
GitHub
parent
13d6b5afcf
commit
2c8c27c5df
@@ -4,7 +4,7 @@ use emath::TSTransform;
|
||||
|
||||
use crate::{
|
||||
Context, CursorIcon, Event, Galley, Id, LayerId, Plugin, Pos2, Rect, Response, Ui,
|
||||
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange,
|
||||
ViewportIdMap, layers::ShapeIdx, text::CCursor, text_selection::CCursorRange,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -80,9 +80,15 @@ struct CurrentSelection {
|
||||
|
||||
/// Handles text selection in labels (NOT in [`crate::TextEdit`])s.
|
||||
///
|
||||
/// One state for all labels, because we only support text selection in one label at a time.
|
||||
#[derive(Clone, Debug)]
|
||||
/// Each viewport has its own state, because viewports are rendered in separate passes.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LabelSelectionState {
|
||||
states: ViewportIdMap<ViewportLabelSelectionState>,
|
||||
}
|
||||
|
||||
/// Text selection state for all labels in one viewport.
|
||||
#[derive(Clone, Debug)]
|
||||
struct ViewportLabelSelectionState {
|
||||
/// The current selection, if any.
|
||||
selection: Option<CurrentSelection>,
|
||||
|
||||
@@ -111,7 +117,7 @@ pub struct LabelSelectionState {
|
||||
painted_selections: Vec<(ShapeIdx, Vec<RowVertexIndices>)>,
|
||||
}
|
||||
|
||||
impl Default for LabelSelectionState {
|
||||
impl Default for ViewportLabelSelectionState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selection: Default::default(),
|
||||
@@ -134,6 +140,64 @@ impl Plugin for LabelSelectionState {
|
||||
}
|
||||
|
||||
fn on_begin_pass(&mut self, ui: &mut Ui) {
|
||||
self.states
|
||||
.entry(ui.ctx().viewport_id())
|
||||
.or_default()
|
||||
.on_begin_pass(ui);
|
||||
}
|
||||
|
||||
fn on_end_pass(&mut self, ui: &mut Ui) {
|
||||
let viewport_id = ui.ctx().viewport_id();
|
||||
let state = self.states.entry(viewport_id).or_default();
|
||||
state.on_end_pass(ui);
|
||||
if !state.is_active() {
|
||||
self.states.remove(&viewport_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelSelectionState {
|
||||
/// Is there a label text selection in any viewport?
|
||||
pub fn has_selection(&self) -> bool {
|
||||
self.states
|
||||
.values()
|
||||
.any(ViewportLabelSelectionState::has_selection)
|
||||
}
|
||||
|
||||
/// Clear all label text selections in all viewports.
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.states.clear();
|
||||
}
|
||||
|
||||
/// Handle text selection state for a label or similar widget.
|
||||
/// This also takes care of painting the galley.
|
||||
pub fn label_text_selection(
|
||||
ui: &Ui,
|
||||
response: &Response,
|
||||
galley_pos: Pos2,
|
||||
mut galley: Arc<Galley>,
|
||||
fallback_color: epaint::Color32,
|
||||
underline: epaint::Stroke,
|
||||
) {
|
||||
let plugin = ui.ctx().plugin::<Self>();
|
||||
let mut plugin = plugin.lock();
|
||||
let state = plugin.states.entry(ui.ctx().viewport_id()).or_default();
|
||||
let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
|
||||
|
||||
let shape_idx = ui.painter().add(
|
||||
epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
|
||||
);
|
||||
|
||||
if !new_vertex_indices.is_empty() {
|
||||
state
|
||||
.painted_selections
|
||||
.push((shape_idx, new_vertex_indices));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewportLabelSelectionState {
|
||||
fn on_begin_pass(&mut self, ui: &Ui) {
|
||||
if ui.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
|
||||
// Maybe a new selection is about to begin, but the old one is over:
|
||||
// state.selection = None; // TODO(emilk): this makes sense, but doesn't work as expected.
|
||||
@@ -150,7 +214,7 @@ impl Plugin for LabelSelectionState {
|
||||
self.painted_selections.clear();
|
||||
}
|
||||
|
||||
fn on_end_pass(&mut self, ui: &mut Ui) {
|
||||
fn on_end_pass(&mut self, ui: &Ui) {
|
||||
if self.is_dragging {
|
||||
ui.set_cursor_icon(CursorIcon::Text);
|
||||
}
|
||||
@@ -212,15 +276,13 @@ impl Plugin for LabelSelectionState {
|
||||
ui.copy_text(text_to_copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelSelectionState {
|
||||
pub fn has_selection(&self) -> bool {
|
||||
self.selection.is_some()
|
||||
fn is_active(&self) -> bool {
|
||||
self.selection.is_some() || self.is_dragging
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selection = None;
|
||||
fn has_selection(&self) -> bool {
|
||||
self.selection.is_some()
|
||||
}
|
||||
|
||||
fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) {
|
||||
@@ -269,34 +331,6 @@ impl LabelSelectionState {
|
||||
self.last_copied_galley_rect = Some(new_galley_rect);
|
||||
}
|
||||
|
||||
/// Handle text selection state for a label or similar widget.
|
||||
///
|
||||
/// Make sure the widget senses clicks and drags.
|
||||
///
|
||||
/// This also takes care of painting the galley.
|
||||
pub fn label_text_selection(
|
||||
ui: &Ui,
|
||||
response: &Response,
|
||||
galley_pos: Pos2,
|
||||
mut galley: Arc<Galley>,
|
||||
fallback_color: epaint::Color32,
|
||||
underline: epaint::Stroke,
|
||||
) {
|
||||
let plugin = ui.ctx().plugin::<Self>();
|
||||
let mut state = plugin.lock();
|
||||
let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
|
||||
|
||||
let shape_idx = ui.painter().add(
|
||||
epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
|
||||
);
|
||||
|
||||
if !new_vertex_indices.is_empty() {
|
||||
state
|
||||
.painted_selections
|
||||
.push((shape_idx, new_vertex_indices));
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_for(
|
||||
&mut self,
|
||||
ui: &Ui,
|
||||
@@ -693,3 +727,66 @@ fn estimate_row_height(galley: &Galley) -> f32 {
|
||||
galley.size().y
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{RawInput, ViewportId, ViewportInfo};
|
||||
|
||||
fn child_viewport_input(viewport_id: ViewportId) -> RawInput {
|
||||
let mut input = RawInput {
|
||||
viewport_id,
|
||||
..Default::default()
|
||||
};
|
||||
input.viewports.insert(
|
||||
viewport_id,
|
||||
ViewportInfo {
|
||||
parent: Some(ViewportId::ROOT),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
input
|
||||
}
|
||||
|
||||
fn test_selection() -> CurrentSelection {
|
||||
let cursor = WidgetTextCursor {
|
||||
widget_id: Id::new("selected_label"),
|
||||
ccursor: CCursor::default(),
|
||||
pos: Pos2::ZERO,
|
||||
};
|
||||
CurrentSelection {
|
||||
layer_id: LayerId::background(),
|
||||
primary: cursor,
|
||||
secondary: cursor,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_passes_only_clean_up_their_own_label_selection() {
|
||||
let ctx = Context::default();
|
||||
let child_viewport_id = ViewportId::from_hash_of("child_viewport");
|
||||
let plugin = ctx.plugin::<LabelSelectionState>();
|
||||
plugin
|
||||
.lock()
|
||||
.states
|
||||
.entry(child_viewport_id)
|
||||
.or_default()
|
||||
.selection = Some(test_selection());
|
||||
|
||||
let _ = ctx.run_ui(RawInput::default(), |_| {});
|
||||
assert!(
|
||||
plugin
|
||||
.lock()
|
||||
.states
|
||||
.get(&child_viewport_id)
|
||||
.is_some_and(ViewportLabelSelectionState::has_selection),
|
||||
"a pass in another viewport must not clear the child viewport selection"
|
||||
);
|
||||
|
||||
let _ = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
|
||||
assert!(
|
||||
!plugin.lock().has_selection(),
|
||||
"the selection must be cleared when its labels disappear from the same viewport"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user