fix: add hand tool scale buttons

This commit is contained in:
Rozaq authored and BFlat committed 2026-10-09 03:43:42 +07:00
1 parent 6197270230
commit 8e09dcf655
8 files changed
+102 -5

No files matched your search

+35
View File
@@ -620,3 +620,38 @@ fn options_bar_reaches_brush_settings_and_the_preset_library() {
h.run_steps(3);
assert!(h.state().ui.panels.brush_settings && h.state().ui.brush_tab == 0);
}
#[test]
fn hand_tool_offers_scale_buttons_and_requests_the_right_view() {
use egui_kittest::kittest::Queryable;
let mut app = app();
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
app.sync_views();
app.ui.tool = crate::state::Tool::Hand;
app.ui.views[0].zoom = 2.0;
let mut h = Harness::builder().with_size(vec2(1400.0, 60.0)).build_ui_state(
|ui, app: &mut PhotocraftApp| {
if ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
crate::panels::options_bar(app, ui);
}
},
app,
);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::Studio);
h.run_steps(4);
h.get_by_label("100%").click();
h.run_steps(1);
assert_eq!(h.state().ui.views[0].zoom, 1.0);
h.get_by_label("Fit Screen").click();
h.run_steps(1);
assert!(h.state().ui.views[0].fit_pending);
assert!(!h.state().ui.views[0].fill_pending);
h.get_by_label("Fill Screen").click();
h.run_steps(1);
assert!(h.state().ui.views[0].fill_pending);
assert!(!h.state().ui.views[0].fit_pending);
}
+29
View File
@@ -394,6 +394,18 @@ pub fn fit_view(view: &mut View, doc: &Document, area: Vec2) {
view.zoom = zoom;
view.center = [w / 2.0, h / 2.0];
view.fit_pending = false;
view.fill_pending = false;
}
/// Centre the document and zoom until it fills the canvas. One axis can extend beyond the
/// viewport, matching the Hand tool's Fill Screen action.
pub fn fill_view(view: &mut View, doc: &Document, area: Vec2) {
let (w, h) = (doc.size.width as f32, doc.size.height as f32);
let zoom = (area.x / w).max(area.y / h).clamp(0.01, 32.0);
view.zoom = zoom;
view.center = [w / 2.0, h / 2.0];
view.fit_pending = false;
view.fill_pending = false;
}
/// Zoom steps like Photoshop's (⌘+ / ⌘−).
@@ -1545,6 +1557,9 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
if view.fit_pending && rect.width() > 50.0 {
fit_view(&mut view, &doc, rect.size());
}
if view.fill_pending && rect.width() > 50.0 {
fill_view(&mut view, &doc, rect.size());
}
// Preferences › Tools › Overscroll off: clamp before anything is drawn (scrollbars.rs).
if !app.session.prefs().tools.overscroll && crate::scrollbars::clamp_view(&mut view, rect.size()) {
ctx.request_repaint();
@@ -3534,6 +3549,20 @@ mod tests {
assert_eq!(zoom_step(32.0, 1), 32.0);
}
#[test]
fn fill_view_covers_the_canvas_and_centres_the_document() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
let doc = app.session.active().unwrap().doc.clone();
let mut view = View { fill_pending: true, ..View::default() };
fill_view(&mut view, &doc, vec2(600.0, 600.0));
assert_eq!(view.zoom, 3.0, "the short edge fills the available height");
assert_eq!(view.center, [200.0, 100.0]);
assert!(!view.fill_pending);
}
#[test]
fn damage_grows_by_nested_effect_reach() {
use photocraft_doc::{Effect, Layer};
+3
View File
@@ -295,15 +295,18 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest)
if let Some(z) = p.get("zoom").and_then(Value::as_f64) {
app.ui.views[i].zoom = (z as f32).clamp(0.01, 64.0);
app.ui.views[i].fit_pending = false;
app.ui.views[i].fill_pending = false;
}
if let Some(c) = p.get("center").and_then(Value::as_array)
&& c.len() == 2
{
app.ui.views[i].center = [c[0].as_f64().unwrap_or(0.0) as f32, c[1].as_f64().unwrap_or(0.0) as f32];
app.ui.views[i].fit_pending = false;
app.ui.views[i].fill_pending = false;
}
if p.get("fit").and_then(Value::as_bool) == Some(true) {
app.ui.views[i].fit_pending = true;
app.ui.views[i].fill_pending = false;
}
}
if let Some(name) = s("theme") {
+1 -1
View File
@@ -447,7 +447,7 @@ mod tests {
fn space_on_the_canvas_moves_the_frame_not_the_view() {
use egui::{Event, Key, PointerButton, pos2};
let mut a = app(SampleType::U8);
let view = crate::state::View { zoom: 2.0, center: [100.0, 50.0], fit_pending: false, doc_size: [200, 100] };
let view = crate::state::View { zoom: 2.0, center: [100.0, 50.0], fit_pending: false, fill_pending: false, doc_size: [200, 100] };
a.ui.views = vec![view.clone()];
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(600.0, 400.0)).build_ui_state(
|ui, app: &mut PhotocraftApp| {
+6
View File
@@ -268,6 +268,12 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
"view.fitOnScreen" => v.fit_pending = true,
_ => v.zoom = 1.0,
}
if id == "view.fitOnScreen" {
v.fill_pending = false;
} else {
v.fit_pending = false;
v.fill_pending = false;
}
Ok(Value::Null)
}
"window.newWindowForDocument" => {
+22 -1
View File
@@ -883,7 +883,28 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
app.ui.views[i].zoom = 1.0;
}
}
Tool::Hand => hint(ui, tl!("Drag to pan · hold Space with any tool")),
Tool::Hand => {
hint(ui, tl!("Drag to pan · hold Space with any tool"));
if widgets::secondary_button(ui, "100%", 0.0).clicked()
&& let Some(i) = app.session.active_index()
{
app.ui.views[i].zoom = 1.0;
app.ui.views[i].fit_pending = false;
app.ui.views[i].fill_pending = false;
}
if widgets::secondary_button(ui, tl!("Fit Screen"), 0.0).clicked()
&& let Some(i) = app.session.active_index()
{
app.ui.views[i].fit_pending = true;
app.ui.views[i].fill_pending = false;
}
if widgets::secondary_button(ui, tl!("Fill Screen"), 0.0).clicked()
&& let Some(i) = app.session.active_index()
{
app.ui.views[i].fill_pending = true;
app.ui.views[i].fit_pending = false;
}
}
Tool::Lasso | Tool::PolygonLasso => hint(
ui,
&crate::i18n::fmt(
+2 -2
View File
@@ -280,7 +280,7 @@ mod tests {
assert_eq!(clamp_axis(600.0, 1000.0, 500.0, 1.0), 600.0);
// Smaller than the view: centred.
assert_eq!(clamp_axis(-90.0, 1000.0, 500.0, 0.2), 500.0);
let mut v = View { zoom: 1.0, center: [-500.0, 20.0], fit_pending: false, doc_size: [1000, 800] };
let mut v = View { zoom: 1.0, center: [-500.0, 20.0], fit_pending: false, fill_pending: false, doc_size: [1000, 800] };
assert!(clamp_view(&mut v, vec2(500.0, 400.0)));
assert_eq!(v.center, [250.0, 200.0]);
assert!(!clamp_view(&mut v, vec2(500.0, 400.0)), "stable: no repaint loop");
@@ -299,7 +299,7 @@ mod tests {
let (at, len) = s.thumb((0.0, 1000.0), 0.0);
assert!(at.is_finite() && len.is_finite());
assert_eq!(s.doc_per_point((0.0, 1000.0), 0.0), 0.0);
let mut v = View { zoom: 1.0, center: [0.0, 0.0], fit_pending: false, doc_size: [0, 0] };
let mut v = View { zoom: 1.0, center: [0.0, 0.0], fit_pending: false, fill_pending: false, doc_size: [0, 0] };
assert!(!clamp_view(&mut v, vec2(500.0, 400.0)));
}
+4 -1
View File
@@ -367,6 +367,9 @@ pub struct View {
pub center: [f32; 2],
/// Recompute fit-to-screen on next frame.
pub fit_pending: bool,
/// Recompute fill-screen on next frame. Unlike fit, this may crop an edge of the document.
#[serde(default)]
pub fill_pending: bool,
/// Document size this view last showed; a change (Image/Canvas Size, crop) re-centres it.
#[serde(default)]
pub doc_size: [u32; 2],
@@ -374,7 +377,7 @@ pub struct View {
impl Default for View {
fn default() -> Self {
Self { zoom: 1.0, center: [0.0, 0.0], fit_pending: true, doc_size: [0, 0] }
Self { zoom: 1.0, center: [0.0, 0.0], fit_pending: true, fill_pending: false, doc_size: [0, 0] }
}
}