refactor(host): split design_session; keep GUI pumps in op-host-desktop (Phase 4, Task 4.4)
Worker spawn (start, run_design_worker) + viewport-fit math (fit_design_viewport_to_content, active_content_bounds, design_canvas_size) move to op_web_daemon::design_session. The host-coupled UI pumps (pump_commands/pump_progress — take &mut WidgetHostNative) + the progress-line renderer (render_progress/progress_label, used only by pump_progress, so kept with their caller per the call graph — deviates from the plan's MOVE list) stay as the residual. active_content_bounds/design_canvas_size made pub for the KEEP-side fit tests; residual re-exports DesignSession (pub use) for main.rs zero-churn. web_chat_standard fit + chat_session_launch start/DesignSession + chat_intent run_design_worker repointed. Completes Phase 4. design_session 8 + op-web-daemon 215 tests green; no dep/lock change.
This commit is contained in:
parent
199d9bbec9
commit
18a162c732
|
|
@ -51,7 +51,7 @@ use op_orchestrator::{AppendContext, DesignRequest};
|
|||
|
||||
use op_web_daemon::chat_canvas_tools::UiChatToolExecutor;
|
||||
use op_web_daemon::chat_provider_llm::ChatProviderLlmClient;
|
||||
use crate::design_session::{run_design_worker, DesignCmdReq, DesignDelta};
|
||||
use op_web_daemon::design_session::{run_design_worker, DesignCmdReq, DesignDelta};
|
||||
|
||||
/// Internal host-op name the modify worker sends over the chat tool
|
||||
/// channel; intercepted by `chat_session::drain_tool_requests` (never
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use op_web_daemon::chat_subprocess::SubprocessProvider;
|
|||
use op_web_daemon::chat_system_prompt::{
|
||||
build_agent_system_prompt, build_chat_system_prompt, chat_history_from_transcript,
|
||||
};
|
||||
use crate::design_session::DesignSession;
|
||||
use op_editor_host_core::design::DesignSession;
|
||||
|
||||
use super::ChatSession;
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ pub fn launch_if_pending(
|
|||
crate::chat_intent::detect_append_intent(host.editor_state(), &user_text);
|
||||
let initial_state = host.editor_state().clone();
|
||||
let request = build_design_request(user_text, &initial_state, append_context);
|
||||
*current_design = Some(crate::design_session::start(llm, request, initial_state));
|
||||
*current_design = Some(op_web_daemon::design_session::start(llm, request, initial_state));
|
||||
return true;
|
||||
}
|
||||
// Design intent but the selected agent has no ChatProvider
|
||||
|
|
|
|||
|
|
@ -1,133 +1,25 @@
|
|||
//! Background design-turn runner — orchestrator counterpart of [`ChatSession`](crate::chat_session).
|
||||
//! Desktop GUI pumps for the background design turn — the host-coupled
|
||||
//! half of the design session.
|
||||
//!
|
||||
//! The orchestrator (`op_orchestrator::Orchestrator::run`) is `async`,
|
||||
//! takes `&mut sink` (the `DocSink` trait — synchronous read + write
|
||||
//! against `EditorState`), and runs to completion across multiple
|
||||
//! `apply()` calls during scaffold → subtasks → cleanup. Two competing
|
||||
//! constraints shape the threading model:
|
||||
//! The worker spawn + viewport-fit math live in
|
||||
//! [`op_web_daemon::design_session`]; this residual keeps the two UI-loop
|
||||
//! pumps (`pump_commands` / `pump_progress`, which take `&mut
|
||||
//! WidgetHostNative` — orphan rule) plus the progress-line renderer they
|
||||
//! fold into the chat transcript.
|
||||
//!
|
||||
//! - **UI owns the canonical `EditorState`.** `EditorCommand::apply`
|
||||
//! does ID remapping + history bookkeeping that must run on the UI
|
||||
//! thread (see `command_apply.rs`).
|
||||
//! - **Don't freeze the UI for the whole turn.** A design turn can take
|
||||
//! 10+ seconds; `block_on(run(...))` on the UI thread would lock the
|
||||
//! window during that span.
|
||||
//!
|
||||
//! Resolution: the worker thread owns a **`RemoteDocSink`** that
|
||||
//! forwards each `apply(cmd)` over an mpsc channel to the UI thread,
|
||||
//! which `apply()`s on the real state and replies with an ack carrying
|
||||
//! a fresh `EditorState` snapshot. `RemoteDocSink::state()` reads from
|
||||
//! a locally cached mirror updated by each ack. The orchestrator never
|
||||
//! sees the channel — it just calls `sink.apply()` synchronously, and
|
||||
//! the worker's `apply` blocks until UI acks.
|
||||
//!
|
||||
//! Progress events emitted by the orchestrator (`Planning`,
|
||||
//! `SubtaskStarted`, etc.) ride a separate channel into the chat
|
||||
//! transcript, mirroring `ChatSession`'s delta channel.
|
||||
//!
|
||||
//! ## Lifecycle
|
||||
//!
|
||||
//! 1. Caller (`chat_session::launch_if_pending`) classifies intent.
|
||||
//! For `Intent::Design` + a configured `agent::Provider`, builds a
|
||||
//! `DesignSession` via [`DesignSession::start`].
|
||||
//! 2. `start` clones the current `EditorState` for the worker's
|
||||
//! initial mirror and spawns the worker thread. The worker calls
|
||||
//! `block_on(Orchestrator::new().run(...))` against its
|
||||
//! `RemoteDocSink`.
|
||||
//! 3. UI event loop drains pending `DesignCmdReq` each frame via
|
||||
//! [`pump_commands`] — applies on the real state, replies ack.
|
||||
//! 4. UI event loop also drains `DesignDelta` via [`pump_progress`]
|
||||
//! and renders progress into the trailing chat bubble.
|
||||
//! 5. On `Done`, the session is dropped and the channels close.
|
||||
//!
|
||||
//! Aborting a turn drops `DesignSession`; the worker's next `apply`
|
||||
//! sees the channel closed and returns `false`, ending the turn.
|
||||
//! - UI event loop drains pending `DesignCmdReq` each frame via
|
||||
//! [`pump_commands`] — applies on the real state, replies ack.
|
||||
//! - UI event loop also drains `DesignDelta` via [`pump_progress`] and
|
||||
//! renders progress into the trailing chat bubble.
|
||||
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::thread;
|
||||
|
||||
use op_editor_core::{DocRect, EditorState, Viewport};
|
||||
pub use op_editor_host_core::design::{
|
||||
DesignCmdAck, DesignCmdOp, DesignCmdReq, DesignDelta, DesignSession, RemoteDocSink,
|
||||
};
|
||||
use op_editor_ui::widgets::TOP_BAR_HEIGHT;
|
||||
use op_editor_host_core::design::{DesignCmdAck, DesignCmdOp};
|
||||
// Re-export so `crate::design_session::DesignSession` (the DesktopApp
|
||||
// field type in main.rs) resolves with zero churn.
|
||||
pub use op_editor_host_core::design::DesignSession;
|
||||
use op_host_native::WidgetHostNative;
|
||||
use op_orchestrator::{
|
||||
AbortFlag, DesignRequest, LlmClient, Orchestrator, Progress, SkippedScreenshotProvider,
|
||||
SkippedVisionLlmClient, ValidationProviders,
|
||||
};
|
||||
use op_orchestrator::Progress;
|
||||
|
||||
use op_web_daemon::chat_runtime::shared_runtime;
|
||||
use op_web_daemon::pre_validator::LintPreValidator;
|
||||
|
||||
/// Spawn a worker that runs `Orchestrator::run` against a `RemoteDocSink`.
|
||||
pub fn start<L: LlmClient + Send + 'static>(
|
||||
llm: L,
|
||||
request: DesignRequest,
|
||||
initial_state: EditorState,
|
||||
) -> DesignSession {
|
||||
let (delta_tx, delta_rx) = mpsc::channel::<DesignDelta>();
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel::<DesignCmdReq>();
|
||||
|
||||
let indicator_epoch = op_editor_core::agent_indicators::begin();
|
||||
|
||||
thread::Builder::new()
|
||||
.name("op-design-turn".into())
|
||||
.spawn(move || {
|
||||
run_design_worker(
|
||||
llm,
|
||||
request,
|
||||
initial_state,
|
||||
delta_tx,
|
||||
cmd_tx,
|
||||
indicator_epoch,
|
||||
)
|
||||
})
|
||||
.expect("spawn op-design-turn thread");
|
||||
|
||||
DesignSession::from_channels_with_epoch(delta_rx, cmd_rx, indicator_epoch)
|
||||
}
|
||||
|
||||
/// One full design turn against a `RemoteDocSink` — the body of
|
||||
/// [`DesignSession::start`]'s worker thread, callable directly by the
|
||||
/// CLI intent router's worker (which already runs off the UI thread).
|
||||
pub(crate) fn run_design_worker<L: LlmClient + Send>(
|
||||
llm: L,
|
||||
request: DesignRequest,
|
||||
initial_state: EditorState,
|
||||
delta_tx: Sender<DesignDelta>,
|
||||
cmd_tx: Sender<DesignCmdReq>,
|
||||
indicator_epoch: u64,
|
||||
) {
|
||||
let mut sink = RemoteDocSink::new(cmd_tx, initial_state);
|
||||
let abort = AbortFlag::new();
|
||||
let pre_validator = LintPreValidator;
|
||||
let screenshot = SkippedScreenshotProvider;
|
||||
let vision = SkippedVisionLlmClient;
|
||||
let providers = ValidationProviders {
|
||||
pre_validator: &pre_validator,
|
||||
screenshot: &screenshot,
|
||||
vision: &vision,
|
||||
system_prompt: String::new(),
|
||||
};
|
||||
let delta_tx_for_progress = delta_tx.clone();
|
||||
let mut on_progress = move |p: Progress| {
|
||||
let _ = delta_tx_for_progress.send(DesignDelta::Progress(p));
|
||||
};
|
||||
let summary = shared_runtime().block_on(
|
||||
Orchestrator::new()
|
||||
.with_indicator_epoch(indicator_epoch)
|
||||
.run(
|
||||
request,
|
||||
&mut sink,
|
||||
&llm,
|
||||
&mut on_progress,
|
||||
&abort,
|
||||
&providers,
|
||||
),
|
||||
);
|
||||
let _ = delta_tx.send(DesignDelta::Done(summary));
|
||||
}
|
||||
use op_web_daemon::design_session::fit_design_viewport_to_content;
|
||||
|
||||
/// Drain every pending apply request from the in-flight design
|
||||
/// session and execute it against the real `EditorState`. Each
|
||||
|
|
@ -182,92 +74,6 @@ pub fn pump_commands(
|
|||
any_applied
|
||||
}
|
||||
|
||||
const DESIGN_FIT_PADDING: f32 = 48.0;
|
||||
|
||||
/// Keep the generated design centered and fully visible while the
|
||||
/// orchestrator progressively applies scaffold/subtask nodes.
|
||||
pub(crate) fn fit_design_viewport_to_content(
|
||||
state: &mut EditorState,
|
||||
viewport_width: f32,
|
||||
viewport_height: f32,
|
||||
) -> bool {
|
||||
let Some(bounds) = active_content_bounds(state) else {
|
||||
return false;
|
||||
};
|
||||
let (canvas_w, canvas_h) = design_canvas_size(state, viewport_width, viewport_height);
|
||||
if canvas_w <= 1.0 || canvas_h <= 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pad_x = DESIGN_FIT_PADDING.min(canvas_w / 4.0);
|
||||
let pad_y = DESIGN_FIT_PADDING.min(canvas_h / 4.0);
|
||||
let fit_w = (canvas_w - pad_x * 2.0).max(1.0);
|
||||
let fit_h = (canvas_h - pad_y * 2.0).max(1.0);
|
||||
let content_w = (bounds.w as f32).max(1.0);
|
||||
let content_h = (bounds.h as f32).max(1.0);
|
||||
let zoom = (fit_w / content_w)
|
||||
.min(fit_h / content_h)
|
||||
.clamp(Viewport::MIN_ZOOM, Viewport::MAX_ZOOM);
|
||||
let center_x = (bounds.x + bounds.w / 2.0) as f32;
|
||||
let center_y = (bounds.y + bounds.h / 2.0) as f32;
|
||||
let next_pan_x = canvas_w / 2.0 - center_x * zoom;
|
||||
let next_pan_y = canvas_h / 2.0 - center_y * zoom;
|
||||
|
||||
let changed = (state.viewport.zoom - zoom).abs() > 0.001
|
||||
|| (state.viewport.pan_x - next_pan_x).abs() > 0.5
|
||||
|| (state.viewport.pan_y - next_pan_y).abs() > 0.5;
|
||||
state.viewport.zoom = zoom;
|
||||
state.viewport.pan_x = next_pan_x;
|
||||
state.viewport.pan_y = next_pan_y;
|
||||
changed
|
||||
}
|
||||
|
||||
fn active_content_bounds(state: &EditorState) -> Option<DocRect> {
|
||||
let scene = op_pen_loader::editor_state_to_layout_scene(state);
|
||||
let page = scene.active_page()?;
|
||||
let mut iter = page
|
||||
.children
|
||||
.iter()
|
||||
.map(|node| node.aggregate_bounds())
|
||||
.filter(|rect| rect.size.x > 0.0 || rect.size.y > 0.0);
|
||||
let first = iter.next()?;
|
||||
let (mut min_x, mut min_y) = (first.origin.x, first.origin.y);
|
||||
let (mut max_x, mut max_y) = (first.origin.x + first.size.x, first.origin.y + first.size.y);
|
||||
for rect in iter {
|
||||
min_x = min_x.min(rect.origin.x);
|
||||
min_y = min_y.min(rect.origin.y);
|
||||
max_x = max_x.max(rect.origin.x + rect.size.x);
|
||||
max_y = max_y.max(rect.origin.y + rect.size.y);
|
||||
}
|
||||
Some(DocRect {
|
||||
x: min_x as f64,
|
||||
y: min_y as f64,
|
||||
w: (max_x - min_x) as f64,
|
||||
h: (max_y - min_y) as f64,
|
||||
})
|
||||
}
|
||||
|
||||
fn design_canvas_size(
|
||||
state: &EditorState,
|
||||
viewport_width: f32,
|
||||
viewport_height: f32,
|
||||
) -> (f32, f32) {
|
||||
let canvas_left = if state.editor_ui.sidebar_open {
|
||||
state.editor_ui.layer_panel_width
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let canvas_right = if state.right_rail_visible() {
|
||||
viewport_width - state.editor_ui.property_panel_width
|
||||
} else {
|
||||
viewport_width
|
||||
};
|
||||
(
|
||||
(canvas_right - canvas_left).max(0.0),
|
||||
(viewport_height - TOP_BAR_HEIGHT).max(0.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Drain every pending progress delta and fold it into the trailing
|
||||
/// assistant message. Clears `current` once the terminal `Done`
|
||||
/// arrives. Returns true when the transcript changed.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
use super::*;
|
||||
use op_editor_core::EditorCommand;
|
||||
use op_web_daemon::design_session::{
|
||||
active_content_bounds, design_canvas_size, fit_design_viewport_to_content,
|
||||
};
|
||||
use op_editor_core::{EditorCommand, EditorState};
|
||||
use op_editor_host_core::design::{DesignCmdReq, DesignDelta, RemoteDocSink};
|
||||
use op_orchestrator::{DocSink, RunSummary, SubtaskOutcome};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// `RemoteDocSink::apply` blocks until UI acks. When the UI side
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ impl DocSink for WebDesignDocSink<'_> {
|
|||
let mut guard = self.state.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let applied = guard.editor.apply(cmd);
|
||||
let version = if applied {
|
||||
crate::design_session::fit_design_viewport_to_content(
|
||||
op_web_daemon::design_session::fit_design_viewport_to_content(
|
||||
&mut guard.editor,
|
||||
1440.0,
|
||||
900.0,
|
||||
|
|
|
|||
188
crates/op-web-daemon/src/design_session.rs
Normal file
188
crates/op-web-daemon/src/design_session.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
//! Background design-turn worker + viewport-fit math — the host-free
|
||||
//! half carved out of `op-host-desktop`'s `design_session.rs` (the GUI
|
||||
//! pumps `pump_commands` / `pump_progress` stay desktop-side).
|
||||
//!
|
||||
//! The orchestrator (`op_orchestrator::Orchestrator::run`) is `async`,
|
||||
//! takes `&mut sink` (the `DocSink` trait — synchronous read + write
|
||||
//! against `EditorState`), and runs to completion across multiple
|
||||
//! `apply()` calls during scaffold → subtasks → cleanup. The worker
|
||||
//! thread owns a `RemoteDocSink` that forwards each `apply(cmd)` over an
|
||||
//! mpsc channel to the UI thread, which `apply()`s on the real state and
|
||||
//! replies with an ack carrying a fresh `EditorState` snapshot. The UI
|
||||
//! drains those requests via the desktop residual's `pump_commands`, and
|
||||
//! progress deltas via `pump_progress`.
|
||||
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::thread;
|
||||
|
||||
use op_editor_core::{DocRect, EditorState, Viewport};
|
||||
pub use op_editor_host_core::design::{DesignCmdReq, DesignDelta, DesignSession, RemoteDocSink};
|
||||
use op_editor_ui::widgets::TOP_BAR_HEIGHT;
|
||||
use op_orchestrator::{
|
||||
AbortFlag, DesignRequest, LlmClient, Orchestrator, Progress, SkippedScreenshotProvider,
|
||||
SkippedVisionLlmClient, ValidationProviders,
|
||||
};
|
||||
|
||||
use crate::chat_runtime::shared_runtime;
|
||||
use crate::pre_validator::LintPreValidator;
|
||||
|
||||
/// Spawn a worker that runs `Orchestrator::run` against a `RemoteDocSink`.
|
||||
pub fn start<L: LlmClient + Send + 'static>(
|
||||
llm: L,
|
||||
request: DesignRequest,
|
||||
initial_state: EditorState,
|
||||
) -> DesignSession {
|
||||
let (delta_tx, delta_rx) = mpsc::channel::<DesignDelta>();
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel::<DesignCmdReq>();
|
||||
|
||||
let indicator_epoch = op_editor_core::agent_indicators::begin();
|
||||
|
||||
thread::Builder::new()
|
||||
.name("op-design-turn".into())
|
||||
.spawn(move || {
|
||||
run_design_worker(
|
||||
llm,
|
||||
request,
|
||||
initial_state,
|
||||
delta_tx,
|
||||
cmd_tx,
|
||||
indicator_epoch,
|
||||
)
|
||||
})
|
||||
.expect("spawn op-design-turn thread");
|
||||
|
||||
DesignSession::from_channels_with_epoch(delta_rx, cmd_rx, indicator_epoch)
|
||||
}
|
||||
|
||||
/// One full design turn against a `RemoteDocSink` — the body of
|
||||
/// [`start`]'s worker thread, callable directly by the CLI intent
|
||||
/// router's worker (which already runs off the UI thread).
|
||||
pub fn run_design_worker<L: LlmClient + Send>(
|
||||
llm: L,
|
||||
request: DesignRequest,
|
||||
initial_state: EditorState,
|
||||
delta_tx: Sender<DesignDelta>,
|
||||
cmd_tx: Sender<DesignCmdReq>,
|
||||
indicator_epoch: u64,
|
||||
) {
|
||||
let mut sink = RemoteDocSink::new(cmd_tx, initial_state);
|
||||
let abort = AbortFlag::new();
|
||||
let pre_validator = LintPreValidator;
|
||||
let screenshot = SkippedScreenshotProvider;
|
||||
let vision = SkippedVisionLlmClient;
|
||||
let providers = ValidationProviders {
|
||||
pre_validator: &pre_validator,
|
||||
screenshot: &screenshot,
|
||||
vision: &vision,
|
||||
system_prompt: String::new(),
|
||||
};
|
||||
let delta_tx_for_progress = delta_tx.clone();
|
||||
let mut on_progress = move |p: Progress| {
|
||||
let _ = delta_tx_for_progress.send(DesignDelta::Progress(p));
|
||||
};
|
||||
let summary = shared_runtime().block_on(
|
||||
Orchestrator::new()
|
||||
.with_indicator_epoch(indicator_epoch)
|
||||
.run(
|
||||
request,
|
||||
&mut sink,
|
||||
&llm,
|
||||
&mut on_progress,
|
||||
&abort,
|
||||
&providers,
|
||||
),
|
||||
);
|
||||
let _ = delta_tx.send(DesignDelta::Done(summary));
|
||||
}
|
||||
|
||||
const DESIGN_FIT_PADDING: f32 = 48.0;
|
||||
|
||||
/// Keep the generated design centered and fully visible while the
|
||||
/// orchestrator progressively applies scaffold/subtask nodes. Called
|
||||
/// by the desktop residual's `pump_commands` after each applied command.
|
||||
pub fn fit_design_viewport_to_content(
|
||||
state: &mut EditorState,
|
||||
viewport_width: f32,
|
||||
viewport_height: f32,
|
||||
) -> bool {
|
||||
let Some(bounds) = active_content_bounds(state) else {
|
||||
return false;
|
||||
};
|
||||
let (canvas_w, canvas_h) = design_canvas_size(state, viewport_width, viewport_height);
|
||||
if canvas_w <= 1.0 || canvas_h <= 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pad_x = DESIGN_FIT_PADDING.min(canvas_w / 4.0);
|
||||
let pad_y = DESIGN_FIT_PADDING.min(canvas_h / 4.0);
|
||||
let fit_w = (canvas_w - pad_x * 2.0).max(1.0);
|
||||
let fit_h = (canvas_h - pad_y * 2.0).max(1.0);
|
||||
let content_w = (bounds.w as f32).max(1.0);
|
||||
let content_h = (bounds.h as f32).max(1.0);
|
||||
let zoom = (fit_w / content_w)
|
||||
.min(fit_h / content_h)
|
||||
.clamp(Viewport::MIN_ZOOM, Viewport::MAX_ZOOM);
|
||||
let center_x = (bounds.x + bounds.w / 2.0) as f32;
|
||||
let center_y = (bounds.y + bounds.h / 2.0) as f32;
|
||||
let next_pan_x = canvas_w / 2.0 - center_x * zoom;
|
||||
let next_pan_y = canvas_h / 2.0 - center_y * zoom;
|
||||
|
||||
let changed = (state.viewport.zoom - zoom).abs() > 0.001
|
||||
|| (state.viewport.pan_x - next_pan_x).abs() > 0.5
|
||||
|| (state.viewport.pan_y - next_pan_y).abs() > 0.5;
|
||||
state.viewport.zoom = zoom;
|
||||
state.viewport.pan_x = next_pan_x;
|
||||
state.viewport.pan_y = next_pan_y;
|
||||
changed
|
||||
}
|
||||
|
||||
/// Bounding box of the active page's content in document space, or
|
||||
/// `None` for an empty page. Public so the desktop residual's
|
||||
/// viewport-fit tests can exercise it across the crate boundary.
|
||||
pub fn active_content_bounds(state: &EditorState) -> Option<DocRect> {
|
||||
let scene = op_pen_loader::editor_state_to_layout_scene(state);
|
||||
let page = scene.active_page()?;
|
||||
let mut iter = page
|
||||
.children
|
||||
.iter()
|
||||
.map(|node| node.aggregate_bounds())
|
||||
.filter(|rect| rect.size.x > 0.0 || rect.size.y > 0.0);
|
||||
let first = iter.next()?;
|
||||
let (mut min_x, mut min_y) = (first.origin.x, first.origin.y);
|
||||
let (mut max_x, mut max_y) = (first.origin.x + first.size.x, first.origin.y + first.size.y);
|
||||
for rect in iter {
|
||||
min_x = min_x.min(rect.origin.x);
|
||||
min_y = min_y.min(rect.origin.y);
|
||||
max_x = max_x.max(rect.origin.x + rect.size.x);
|
||||
max_y = max_y.max(rect.origin.y + rect.size.y);
|
||||
}
|
||||
Some(DocRect {
|
||||
x: min_x as f64,
|
||||
y: min_y as f64,
|
||||
w: (max_x - min_x) as f64,
|
||||
h: (max_y - min_y) as f64,
|
||||
})
|
||||
}
|
||||
|
||||
/// The visible canvas region (width, height) given the current sidebar /
|
||||
/// right-rail state. Public for the desktop residual's viewport-fit tests.
|
||||
pub fn design_canvas_size(
|
||||
state: &EditorState,
|
||||
viewport_width: f32,
|
||||
viewport_height: f32,
|
||||
) -> (f32, f32) {
|
||||
let canvas_left = if state.editor_ui.sidebar_open {
|
||||
state.editor_ui.layer_panel_width
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let canvas_right = if state.right_rail_visible() {
|
||||
viewport_width - state.editor_ui.property_panel_width
|
||||
} else {
|
||||
viewport_width
|
||||
};
|
||||
(
|
||||
(canvas_right - canvas_left).max(0.0),
|
||||
(viewport_height - TOP_BAR_HEIGHT).max(0.0),
|
||||
)
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ pub mod chat_spawn;
|
|||
pub mod chat_subprocess;
|
||||
pub mod chat_subprocess_quirks;
|
||||
pub mod chat_system_prompt;
|
||||
pub mod design_session;
|
||||
pub mod doc_io;
|
||||
pub mod export;
|
||||
pub mod export_pdf;
|
||||
|
|
|
|||
Loading…
Reference in a new issue