mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
fix(editor): fit blank frame on startup
This commit is contained in:
parent
502f301a70
commit
37c421697a
9 changed files with 99 additions and 15 deletions
|
|
@ -45,6 +45,17 @@ impl Viewport {
|
|||
/// the content centre to the canvas centre. No-op for an empty
|
||||
/// content rect or a degenerate canvas.
|
||||
pub fn fit_to(&mut self, content: Rect, canvas_w: f32, canvas_h: f32, padding: f32) {
|
||||
self.fit_to_with_max_zoom(content, canvas_w, canvas_h, padding, Self::MAX_ZOOM);
|
||||
}
|
||||
|
||||
pub fn fit_to_with_max_zoom(
|
||||
&mut self,
|
||||
content: Rect,
|
||||
canvas_w: f32,
|
||||
canvas_h: f32,
|
||||
padding: f32,
|
||||
max_zoom: f32,
|
||||
) {
|
||||
if content.size.x <= 0.0 || content.size.y <= 0.0 || canvas_w <= 0.0 || canvas_h <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -52,7 +63,10 @@ impl Viewport {
|
|||
let avail_h = (canvas_h - 2.0 * padding).max(1.0);
|
||||
let zoom = (avail_w / content.size.x)
|
||||
.min(avail_h / content.size.y)
|
||||
.clamp(Self::MIN_ZOOM, Self::MAX_ZOOM);
|
||||
.clamp(
|
||||
Self::MIN_ZOOM,
|
||||
max_zoom.clamp(Self::MIN_ZOOM, Self::MAX_ZOOM),
|
||||
);
|
||||
let cc_x = content.origin.x + content.size.x / 2.0;
|
||||
let cc_y = content.origin.y + content.size.y / 2.0;
|
||||
self.zoom = zoom;
|
||||
|
|
@ -118,6 +132,21 @@ mod tests {
|
|||
assert!((v.pan_y + 325.0 * v.zoom - 200.0).abs() < 1e-2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_to_with_max_zoom_caps_content_fit() {
|
||||
let mut v = Viewport::IDENTITY;
|
||||
let content = Rect {
|
||||
origin: Point2D::new(100.0, 50.0),
|
||||
size: Point2D::new(200.0, 100.0),
|
||||
};
|
||||
|
||||
v.fit_to_with_max_zoom(content, 1000.0, 800.0, 100.0, 2.0);
|
||||
|
||||
assert_eq!(v.zoom, 2.0);
|
||||
assert!((v.pan_x - 100.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 200.0).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_to_ignores_empty_content() {
|
||||
let mut v = Viewport::IDENTITY;
|
||||
|
|
|
|||
|
|
@ -159,8 +159,12 @@ struct DesktopApp {
|
|||
impl DesktopApp {
|
||||
fn new(initial_file: Option<PathBuf>) -> Self {
|
||||
let mut host = WidgetHostNative::new();
|
||||
let fit_blank_frame = initial_file.is_none();
|
||||
// Best-effort prefs restore onto the host's `EditorState`.
|
||||
settings_io::load(host.editor_state_mut());
|
||||
if fit_blank_frame {
|
||||
host.fit_content_to_viewport(INITIAL_VIEWPORT_W, INITIAL_VIEWPORT_H);
|
||||
}
|
||||
host.mark_editor_state_dirty();
|
||||
// Baseline for the unsaved-changes prompt — the fresh,
|
||||
// empty document is by definition "saved" (nothing to lose).
|
||||
|
|
|
|||
|
|
@ -34,3 +34,13 @@ fn cursor_redraw_still_paints_when_layer_hover_changes() {
|
|||
|
||||
assert!(app.prepare_redraw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_app_fits_blank_frame_like_ts_canvas_init() {
|
||||
let app = DesktopApp::new(None);
|
||||
let v = app.host.editor_state().viewport;
|
||||
|
||||
assert!((v.zoom - 0.66).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.pan_x - 64.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 166.0).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ pub fn run_action(
|
|||
match action {
|
||||
FileAction::New => {
|
||||
// Reset the document to a fresh untitled state.
|
||||
*host.editor_state_mut() = EditorState::new();
|
||||
*host.editor_state_mut() = EditorState::starter();
|
||||
host.mark_editor_state_dirty();
|
||||
*current_path = None;
|
||||
refresh_title(current_path, window);
|
||||
|
|
@ -702,6 +702,45 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_file_action_resets_to_starter_frame() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
host.editor_state_mut().doc.children.clear();
|
||||
let mut current_path = Some(PathBuf::from("/tmp/old.op"));
|
||||
|
||||
let outcome = run_action(
|
||||
op_editor_core::editor_ui_state::FileAction::New,
|
||||
&mut host,
|
||||
&mut current_path,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(outcome, ActionOutcome::Saved);
|
||||
assert!(current_path.is_none());
|
||||
assert_eq!(host.editor_state().doc.children.len(), 1);
|
||||
assert_eq!(
|
||||
host.editor_state().selection.anchor,
|
||||
op_editor_core::NodeId::new("n10")
|
||||
);
|
||||
let frame = match &host.editor_state().doc.children[0] {
|
||||
jian_ops_schema::node::PenNode::Frame(frame) => frame,
|
||||
other => panic!(
|
||||
"new file should create the blank starter frame, got {:?}",
|
||||
other
|
||||
),
|
||||
};
|
||||
assert_eq!(frame.base.x, Some(0.0));
|
||||
assert_eq!(frame.base.y, Some(0.0));
|
||||
assert!(matches!(
|
||||
frame.container.width,
|
||||
Some(jian_ops_schema::sizing::SizingBehavior::Number(1200.0))
|
||||
));
|
||||
assert!(matches!(
|
||||
frame.container.height,
|
||||
Some(jian_ops_schema::sizing::SizingBehavior::Number(800.0))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_doc_payload_is_detected() {
|
||||
// Fix 4: a pre-canonical private `DocPayload` JSON (integer
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ mod settings_dispatch;
|
|||
mod shape_picker_press;
|
||||
mod shortcuts;
|
||||
mod toolbar_hover;
|
||||
mod viewport_fit;
|
||||
|
||||
pub use frame_backend::NativeFrameBackend;
|
||||
|
||||
|
|
|
|||
|
|
@ -486,7 +486,7 @@ impl WidgetHostNative {
|
|||
let (_l, _t, canvas_w, canvas_h) = self.canvas_region(viewport_w, viewport_h);
|
||||
self.editor_state
|
||||
.viewport
|
||||
.fit_to(content, canvas_w, canvas_h, 48.0);
|
||||
.fit_to_with_max_zoom(content, canvas_w, canvas_h, 64.0, 1.0);
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,17 +196,9 @@ fn status_bar_search_click_frames_content_in_viewport() {
|
|||
|
||||
assert!(consumed, "search-icon click must be consumed");
|
||||
let v = host.editor_state().viewport;
|
||||
assert!(
|
||||
(v.zoom - 0.2).abs() > 1e-3,
|
||||
"zoom should change to frame the content, got {}",
|
||||
v.zoom
|
||||
);
|
||||
assert!(
|
||||
v.pan_x > -5000.0 && v.pan_y > -5000.0,
|
||||
"pan should re-anchor toward the content, got ({}, {})",
|
||||
v.pan_x,
|
||||
v.pan_y
|
||||
);
|
||||
assert!((v.zoom - 1.0).abs() < 1e-3, "zoom {}", v.zoom);
|
||||
assert!((v.pan_x - 230.0).abs() < 1e-2, "pan_x {}", v.pan_x);
|
||||
assert!((v.pan_y - 180.0).abs() < 1e-2, "pan_y {}", v.pan_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
9
crates/op-host-native/src/widget_host/viewport_fit.rs
Normal file
9
crates/op-host-native/src/widget_host/viewport_fit.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//! Public viewport-fit adapter for desktop runner startup.
|
||||
|
||||
use super::WidgetHostNative;
|
||||
|
||||
impl WidgetHostNative {
|
||||
pub fn fit_content_to_viewport(&mut self, viewport_w: f32, viewport_h: f32) {
|
||||
self.zoom_to_fit(viewport_w, viewport_h);
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ impl WidgetHost {
|
|||
let (_l, _t, canvas_w, canvas_h) = self.canvas_region(viewport_w, viewport_h);
|
||||
self.editor_state
|
||||
.viewport
|
||||
.fit_to(content, canvas_w, canvas_h, 48.0);
|
||||
.fit_to_with_max_zoom(content, canvas_w, canvas_h, 64.0, 1.0);
|
||||
self.mark_dirty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue