* Let the Navigator preview fill its dock group, drop the scroll fade (#2726) The Navigator drew a fixed 150 pt preview inside the dock's outer ScrollArea, so a taller group left dead space below the zoom slider, and a shorter one scrolled with egui's scroll fade painting a dark gradient over the thumbnail. The same fade darkened the Color field and swatches at the bottom of their groups. The Navigator tab now lays itself out (like Layers and History): the preview takes the group's height above the zoom controls, keeps the document's aspect and stays centred. The dock's outer scroll areas no longer paint the fade; their scroll bar already shows there is more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Navigator wheel test: point below the preview, which now fills the group Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
3c70d3b34f
commit
953390a36e
|
|
@ -111,9 +111,10 @@ impl Group {
|
|||
}
|
||||
}
|
||||
|
||||
/// Tabs that lay out their own scrolling list and footer (they fill the group).
|
||||
/// Tabs that lay out their own scrolling list and footer, or size themselves to the group
|
||||
/// (the Navigator's preview grows with it, as in Photoshop: #2726).
|
||||
pub fn scrolls_itself(self, tab: usize) -> bool {
|
||||
matches!((self, tab), (Group::Layers, 0) | (Group::History, 0))
|
||||
matches!((self, tab), (Group::Layers, 0) | (Group::History, 0) | (Group::Navigator, 0))
|
||||
}
|
||||
|
||||
fn tab_mut(self, tabs: &mut DockTabs) -> &mut usize {
|
||||
|
|
@ -481,6 +482,9 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui, shown: &[Group], mut bod
|
|||
if t.pro && g == Group::Color && tab == 0 {
|
||||
ui.data_mut(|d| d.insert_temp(crate::panels::color_field_fill_id(), inner));
|
||||
}
|
||||
// No scroll fade: it darkens real colours, swatches and previews at the edge
|
||||
// (#2726); the scroll bar already shows there is more.
|
||||
ui.spacing_mut().scroll.fade.strength = 0.0;
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(("dock-scroll", g.key(), tab))
|
||||
.max_height(inner)
|
||||
|
|
|
|||
|
|
@ -821,6 +821,43 @@ fn tab_hides_all_panels_and_shift_tab_only_the_dock() {
|
|||
assert!(serde_json::from_value::<crate::state::Panels>(v).unwrap().dock);
|
||||
}
|
||||
|
||||
/// #2726: the Navigator sizes its own preview to the group (no outer scroll area, so no scroll
|
||||
/// fade over the thumbnail), keeping the document's aspect, with the zoom controls below it.
|
||||
#[test]
|
||||
fn navigator_preview_fills_its_group_and_keeps_aspect() {
|
||||
assert!(Group::Navigator.scrolls_itself(0), "the Navigator tab lays itself out");
|
||||
assert!(!Group::Navigator.scrolls_itself(1) && !Group::Navigator.scrolls_itself(2), "Histogram and Info still scroll");
|
||||
for theme in [ThemeKind::ProMedium, ThemeKind::Studio] {
|
||||
let mut sizes = Vec::new();
|
||||
for tall in [150.0, 260.0, 420.0] {
|
||||
let mut h = Harness::builder().with_size(vec2(1200.0, 1000.0)).with_max_steps(64).build_eframe(move |cc| {
|
||||
PhotocraftApp::setup_context(&cc.egui_ctx, theme);
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
app.ui.theme = theme;
|
||||
app.ui.panels.navigator = true;
|
||||
app.ui.dock.heights.insert(Group::Navigator, tall);
|
||||
app.run("file.new", json!({"width": 200, "height": 300})).unwrap();
|
||||
app.sync_views();
|
||||
app
|
||||
});
|
||||
h.run_steps(8);
|
||||
let group = rect_of(&h, Group::Navigator);
|
||||
let preview = crate::panels::last_navigator_preview(&h.ctx).unwrap_or_else(|| panic!("{theme:?} {tall}: no preview drawn"));
|
||||
assert!((group.height() - tall).abs() < 1.0, "{theme:?}: group {group:?}");
|
||||
assert!(group.contains_rect(preview), "{theme:?} {tall}: preview {preview:?} outside {group:?}");
|
||||
assert!((preview.width() / preview.height() - 2.0 / 3.0).abs() < 0.02, "{theme:?} {tall}: aspect {preview:?}");
|
||||
// The zoom controls stay inside the group, below the preview.
|
||||
let controls = crate::panels::last_navigator_controls(&h.ctx).unwrap_or_else(|| panic!("{theme:?} {tall}: no controls"));
|
||||
assert!(controls.top() >= preview.bottom() && controls.bottom() <= group.bottom() + 0.5, "{theme:?} {tall}: {controls:?} {preview:?} {group:?}");
|
||||
sizes.push(preview);
|
||||
}
|
||||
// A portrait document grows with every extra point of height the group gets.
|
||||
for pair in sizes.windows(2) {
|
||||
assert!(pair[1].height() > pair[0].height() + 80.0, "{theme:?}: {sizes:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_tabs(h: &Harness<'static, PhotocraftApp>, g: Group) -> Vec<(usize, Rect)> {
|
||||
last_strips(&h.ctx).into_iter().find(|s| s.group == g).map(|s| s.tabs).unwrap_or_else(|| panic!("{g:?} strip not drawn"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1645,15 +1645,18 @@ fn navigator(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
let Some(tex) = crate::canvas::navigator_texture(app, &ctx, idx) else { return };
|
||||
let size = app.session.documents()[idx].doc.size;
|
||||
let avail = ui.available_width();
|
||||
let box_h = 150.0;
|
||||
// The preview fills the group's height above the zoom controls (their height as measured
|
||||
// last frame), so it grows when the group is made taller, as in Photoshop (#2726).
|
||||
let controls_h = ui.data(|d| d.get_temp::<f32>(navigator_controls_id())).unwrap_or(48.0);
|
||||
let left = ui.available_height() - controls_h;
|
||||
let box_h = if left.is_finite() { left.max(40.0) } else { 150.0 };
|
||||
let (frame, resp) = ui.allocate_exact_size(vec2(avail, box_h), Sense::click_and_drag());
|
||||
ui.painter().rect_filled(frame, t.radius_sm, t.canvas);
|
||||
let aspect = size.width as f32 / size.height.max(1) as f32;
|
||||
let (w, h) = if aspect > avail / box_h { (avail - 16.0, (avail - 16.0) / aspect) } else { ((box_h - 16.0) * aspect, box_h - 16.0) };
|
||||
let rect = Rect::from_center_size(frame.center(), vec2(w, h));
|
||||
let rect = navigator_fit(frame, size.width, size.height);
|
||||
ui.data_mut(|d| d.insert_temp(navigator_preview_id(), rect));
|
||||
widgets::checker(ui.painter(), rect, 6.0);
|
||||
ui.painter().image(tex, rect, Rect::from_min_max(egui::Pos2::ZERO, pos2(1.0, 1.0)), Color32::WHITE);
|
||||
let s = w / size.width as f32;
|
||||
let s = rect.width() / size.width.max(1) as f32;
|
||||
if (resp.clicked() || resp.dragged())
|
||||
&& let Some(pp) = resp.interact_pointer_pos()
|
||||
{
|
||||
|
|
@ -1685,28 +1688,69 @@ fn navigator(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
let c = pos2(rect.min.x + v.center[0] * s, rect.min.y + v.center[1] * s);
|
||||
let vr = Rect::from_center_size(c, vec2(vw, vh)).intersect(frame.shrink(1.0));
|
||||
ui.painter().rect_stroke(vr, 2.0, Stroke::new(1.5, Color32::from_rgb(255, 84, 84)), StrokeKind::Middle);
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
label(ui, tl!("Zoom"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
for (lbl, z) in [("200%", 2.0), ("100%", 1.0)] {
|
||||
if widgets::pill_tab(ui, lbl, (v.zoom - z).abs() < 1e-3).clicked() {
|
||||
app.ui.views[idx].zoom = z;
|
||||
app.ui.views[idx].fit_pending = false;
|
||||
let controls = ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
label(ui, tl!("Zoom"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
for (lbl, z) in [("200%", 2.0), ("100%", 1.0)] {
|
||||
if widgets::pill_tab(ui, lbl, (v.zoom - z).abs() < 1e-3).clicked() {
|
||||
app.ui.views[idx].zoom = z;
|
||||
app.ui.views[idx].fit_pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if widgets::pill_tab(ui, tl!("Fit"), false).clicked() {
|
||||
app.ui.views[idx].fit_pending = true;
|
||||
}
|
||||
if widgets::pill_tab(ui, tl!("Fit"), false).clicked() {
|
||||
app.ui.views[idx].fit_pending = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
// The whole zoom range, and always the current zoom: a narrower slider would pull it back.
|
||||
let (lo, hi) = (crate::zoom_levels::min(v.doc_size).min(v.zoom).log2(), crate::zoom_levels::MAX.max(v.zoom).log2());
|
||||
let mut lz = v.zoom.log2();
|
||||
if lz.is_finite() && widgets::slider(ui, &mut lz, lo..=hi, None).changed() {
|
||||
app.ui.views[idx].zoom = crate::zoom_levels::clamp(2f32.powf(lz), v.doc_size);
|
||||
app.ui.views[idx].fit_pending = false;
|
||||
}
|
||||
});
|
||||
// The whole zoom range, and always the current zoom: a narrower slider would pull it back.
|
||||
let (lo, hi) = (crate::zoom_levels::min(v.doc_size).min(v.zoom).log2(), crate::zoom_levels::MAX.max(v.zoom).log2());
|
||||
let mut lz = v.zoom.log2();
|
||||
if lz.is_finite() && widgets::slider(ui, &mut lz, lo..=hi, None).changed() {
|
||||
app.ui.views[idx].zoom = crate::zoom_levels::clamp(2f32.powf(lz), v.doc_size);
|
||||
app.ui.views[idx].fit_pending = false;
|
||||
}
|
||||
// From the preview's bottom, so the spacing above the controls counts too.
|
||||
let controls = Rect::from_min_max(pos2(frame.left(), frame.bottom()), pos2(frame.right(), controls.response.rect.bottom()));
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(navigator_controls_id(), controls.height());
|
||||
d.insert_temp(navigator_controls_rect_id(), controls);
|
||||
});
|
||||
}
|
||||
|
||||
/// The document's `w` × `h` fitted inside the Navigator's `frame` with an 8 pt inset, centred and
|
||||
/// keeping its aspect.
|
||||
fn navigator_fit(frame: Rect, w: u32, h: u32) -> Rect {
|
||||
let inner = (frame.size() - vec2(16.0, 16.0)).max(vec2(1.0, 1.0));
|
||||
let (w, h) = (w.max(1) as f32, h.max(1) as f32);
|
||||
let s = (inner.x / w).min(inner.y / h);
|
||||
Rect::from_center_size(frame.center(), vec2(w * s, h * s))
|
||||
}
|
||||
|
||||
fn navigator_preview_id() -> egui::Id {
|
||||
egui::Id::new("navigator-preview-rect")
|
||||
}
|
||||
|
||||
fn navigator_controls_id() -> egui::Id {
|
||||
egui::Id::new("navigator-controls-height")
|
||||
}
|
||||
|
||||
fn navigator_controls_rect_id() -> egui::Id {
|
||||
egui::Id::new("navigator-controls-rect")
|
||||
}
|
||||
|
||||
/// Where the Navigator drew its document preview last frame.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn last_navigator_preview(ctx: &egui::Context) -> Option<Rect> {
|
||||
ctx.data(|d| d.get_temp(navigator_preview_id()))
|
||||
}
|
||||
|
||||
/// The Navigator's zoom controls (below the preview) last frame.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn last_navigator_controls(ctx: &egui::Context) -> Option<Rect> {
|
||||
ctx.data(|d| d.get_temp(navigator_controls_rect_id()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -1741,7 +1785,9 @@ mod navigator_wheel_tests {
|
|||
assert_eq!(app.ui.views[0].center, center, "Navigator wheel must not pan");
|
||||
frame(&mut app, &ctx, pos2(50.0, 55.0), -1.0);
|
||||
assert!((app.ui.views[0].zoom - 1.0).abs() < 1e-4);
|
||||
frame(&mut app, &ctx, pos2(50.0, 400.0), 1.0);
|
||||
// Below the preview (it now fills the group, #2726): over the zoom controls.
|
||||
let below = last_navigator_controls(&ctx).map_or(pos2(50.0, 455.0), |r| pos2(r.left() + 4.0, r.bottom() - 2.0));
|
||||
frame(&mut app, &ctx, below, 1.0);
|
||||
assert!((app.ui.views[0].zoom - 1.0).abs() < 1e-4, "wheel outside preview leaves zoom unchanged");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue