1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! Inline rename popup for pane titles.
//!
//! Opened by right-clicking a pane title bar or by the `rename_pane`
//! action on the focused pane. Mirrors the tab bar's inline rename row
//! (tab_bar_ui/context_menu.rs): single-line TextEdit, Enter submits,
//! Escape cancels, a click outside closes, and leaving the field blank
//! reverts the pane to automatic titles.
use crate::pane::PaneId;
/// What the popup produced this frame.
#[derive(Debug, PartialEq)]
pub(crate) enum PaneRenameOutcome {
/// Enter pressed: apply this name (possibly empty — empty reverts).
Submit {
pane_id: PaneId,
name: String,
},
None,
}
/// State for the pane-title rename popup.
pub(crate) struct PaneRenameUI {
/// `Some(pane)` while the popup is open.
pane_id: Option<PaneId>,
buffer: String,
/// Popup origin in logical (egui) coordinates.
pos: egui::Pos2,
/// Frame the popup first rendered in. The click that opened it is still
/// in egui's input that frame, so click-away dismissal must wait for a
/// later frame — the tab bar context menu's `*_activated_frame` guard.
opened_frame: Option<u64>,
/// The opening click's RELEASE still completes one frame later and
/// counts as `any_click()`; click-away waits for the pointer to come
/// up once first, or the popup would close the instant the user
/// releases the button that opened it.
awaiting_open_release: bool,
}
impl Default for PaneRenameUI {
fn default() -> Self {
Self {
pane_id: None,
buffer: String::new(),
pos: egui::Pos2::ZERO,
opened_frame: None,
awaiting_open_release: false,
}
}
}
impl PaneRenameUI {
/// Open the popup for `pane_id`, seeding the buffer with its current
/// title so a rename starts from what is shown.
pub(crate) fn open(&mut self, pane_id: PaneId, current_title: &str, pos: egui::Pos2) {
self.pane_id = Some(pane_id);
self.buffer = current_title.to_string();
self.pos = pos;
self.opened_frame = None;
self.awaiting_open_release = true;
}
/// Whether the popup is open (its text field owns the keyboard).
pub(crate) fn is_open(&self) -> bool {
self.pane_id.is_some()
}
fn close(&mut self) {
self.pane_id = None;
self.opened_frame = None;
}
/// Close without submitting (the overlay stack's Escape path).
pub(crate) fn cancel(&mut self) {
self.close();
}
/// Render the popup if open. Runs inside the egui pass; the returned
/// outcome is applied by the caller after the egui borrow ends.
pub(crate) fn render(&mut self, ctx: &egui::Context) -> PaneRenameOutcome {
let Some(pane_id) = self.pane_id else {
return PaneRenameOutcome::None;
};
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
self.close();
return PaneRenameOutcome::None;
}
let mut outcome = PaneRenameOutcome::None;
let pos = self.pos;
let buffer = &mut self.buffer;
let area = egui::Area::new(egui::Id::new("pane_rename_popup"))
.fixed_pos(pos)
.constrain(true)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::popup(ui.style())
.inner_margin(egui::Margin::symmetric(8, 6))
.show(ui, |ui| {
ui.set_min_width(200.0);
ui.horizontal(|ui| {
let response =
ui.add(egui::TextEdit::singleline(buffer).hint_text("Pane name"));
if !response.has_focus() {
response.request_focus();
}
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
outcome = PaneRenameOutcome::Submit {
pane_id,
name: buffer.trim().to_string(),
};
}
});
ui.label(
egui::RichText::new("Leave blank to use auto title")
.weak()
.small(),
);
})
});
// Click-away dismissal, guarded against the opening click: its
// press shares the popup's first rendered frame, and its release
// completes a frame later as any_click() — both must pass before a
// real click can close the popup.
let frame = ctx.cumulative_frame_nr();
if self.opened_frame.is_none() {
self.opened_frame = Some(frame);
}
if self.awaiting_open_release {
if !ctx.input(|i| i.pointer.any_down()) {
self.awaiting_open_release = false;
}
} else if frame > self.opened_frame.unwrap_or(u64::MAX)
&& ctx.input(|i| i.pointer.any_click())
&& !area.response.clicked()
{
self.close();
}
if outcome != PaneRenameOutcome::None {
self.close();
}
outcome
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_seeds_the_buffer() {
let mut ui = PaneRenameUI::default();
ui.open(7, "build box", egui::Pos2::ZERO);
assert_eq!(ui.pane_id, Some(7));
assert_eq!(ui.buffer, "build box");
assert!(ui.is_open());
}
}