File tabs: stop the close button from flickering (#1914)

* File tabs: the close button no longer flickers under the pointer

An inactive drawing tab drew its X only while the tab was hovered. Once
the pointer reached the X, egui gave the hover to the X, so the next frame
left the X out, the tab got the hover back, and the frame after drew the X
again. The tab background and its X flickered for as long as the pointer
stayed there, and a click could land on the tab instead of the X.

The tab now asks whether the pointer is inside its rectangle
(`Ui::rect_contains_pointer`), which the X does not change, both for the
hover fill and for showing the X.

Regression test: tests/file_tab_close.rs points at an inactive tab's X,
checks the X is there on each of the next frames, and clicks it to close
that drawing. It failed on the first frame before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* File tabs: plainer comment on why the tab checks the pointer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kshitij Varshney
2026-10-24 01:51:16 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 78c1653f50
commit 6842b8774b
2 changed files with 48 additions and 2 deletions
+5 -2
View File
@@ -198,12 +198,15 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
|ui: &mut egui::Ui, label: &str, dirty: bool, active: bool, closable: bool, w: f32, id: egui::Id| -> (bool, bool, egui::Response) {
let tr = Rect::from_min_size(pos2(x, r.top()), vec2(w, r.height()));
let resp = ui.interact(tr, id, Sense::click());
// The close button takes the tab's hover, so `resp.hovered()` would make the tab and
// its X flicker while the pointer is on the X.
let hovered = ui.rect_contains_pointer(tr);
ui.painter().rect_filled(
tr,
0.0,
if active {
t.tab_active
} else if resp.hovered() {
} else if hovered {
t.chrome
} else {
t.chrome_dark
@@ -223,7 +226,7 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
}
let resp = if cut { resp.on_hover_text(format!("{label}{mark}")) } else { resp };
let mut close = false;
if closable && (active || resp.hovered()) {
if closable && (active || hovered) {
let cr = Rect::from_center_size(pos2(tr.right() - 11.0, tr.center().y), vec2(12.0, 12.0));
let cresp = ui.interact(cr, id.with("x"), Sense::click());
icons::paint(ui.painter(), cr, Icon::Close, false);
+43
View File
@@ -0,0 +1,43 @@
//! The close button of an inactive file tab stays while the pointer is on it, and a click on it
//! closes that drawing. It used to take the tab's hover, so the next frame dropped it and the one
//! after drew it again: the tab and its X flickered.
use cadcraft_engine::Session;
use cadcraft_ui_egui::{CadApp, Services};
use egui::accesskit::Role;
use egui::vec2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
#[test]
fn an_inactive_tabs_close_button_stays_under_the_pointer_and_closes_it() {
let mut app = CadApp::new(Session::default(), Services::default());
app.run("new", serde_json::json!({})).unwrap();
app.run("new", serde_json::json!({})).unwrap();
app.ui.start_tab = false;
assert_ne!(app.session.active, 0, "the first drawing's tab is inactive");
let first = app.session.docs[0].title.clone();
let open = app.session.docs.len();
let mut h = Harness::builder().with_size(vec2(1200.0, 760.0)).build_ui_state(
|ui, app: &mut CadApp| {
app.logic(&ui.ctx().clone());
app.ui(ui);
},
app,
);
h.run_steps(5);
// Pointing at the tab shows its close button.
h.get_by_role_and_label(Role::Tab, &first).hover();
h.run_steps(3);
let close = format!("Close {first}");
h.get_by_label(&close).hover();
for frame in 0..6 {
h.step();
assert!(h.query_by_label(&close).is_some(), "the close button is gone on frame {frame} under the pointer");
}
h.get_by_label(&close).click();
h.run_steps(3);
let titles: Vec<&str> = h.state().session.docs.iter().map(|d| d.title.as_str()).collect();
assert_eq!(titles.len(), open - 1, "one drawing closed: {titles:?}");
assert!(!titles.contains(&first.as_str()), "the clicked tab's drawing is the one closed: {titles:?}");
}