mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-06-01 03:14:29 +07:00
fix: clear clippy -D warnings across the workspace
The op-* crate reorg never ran `cargo clippy -- -D warnings`, so the CI lint gate failed. Fix every violation surgically: real fixes for mechanical lints (needless_range_loop, derivable_impls, ptr_arg, needless_borrow, doc_lazy_continuation, unused_imports, manual_find, collapsible_match, never_loop, dead_code, complex types via type aliases) and scoped `#[allow]` for intrusive ones (too_many_arguments on paint helpers, result_large_err where ToolOutcome / PenNode payloads are deliberately the Err type).
This commit is contained in:
parent
1ff061f41a
commit
faafc99fb8
37 changed files with 341 additions and 232 deletions
|
|
@ -68,7 +68,11 @@ impl Codegen for CssVariables {
|
|||
// Group entries by their theme map signature so a "mode=dark"
|
||||
// entry from variable A and another from B share one block.
|
||||
use std::collections::BTreeMap;
|
||||
let mut themed: BTreeMap<Vec<(String, String)>, Vec<(String, String)>> = BTreeMap::new();
|
||||
// Maps a theme-map signature (sorted axis/value pairs) to the
|
||||
// (variable-name, css-value) entries that share that signature.
|
||||
type ThemeSignature = Vec<(String, String)>;
|
||||
type ThemedBlock = Vec<(String, String)>;
|
||||
let mut themed: BTreeMap<ThemeSignature, ThemedBlock> = BTreeMap::new();
|
||||
for (name, def) in variables {
|
||||
if let VariableValue::Themed(entries) = &def.value {
|
||||
for e in entries {
|
||||
|
|
@ -427,10 +431,12 @@ mod tests {
|
|||
|
||||
/// A rectangle at `(x,y)` sized `w×h` with an optional fill.
|
||||
fn rect(id: &str, x: f64, y: f64, w: f64, h: f64, fill: Option<&str>) -> PenNode {
|
||||
let mut container = jian_ops_schema::node::ContainerProps::default();
|
||||
container.width = Some(SizingBehavior::Number(w));
|
||||
container.height = Some(SizingBehavior::Number(h));
|
||||
container.fill = fill.map(solid);
|
||||
let container = jian_ops_schema::node::ContainerProps {
|
||||
width: Some(SizingBehavior::Number(w)),
|
||||
height: Some(SizingBehavior::Number(h)),
|
||||
fill: fill.map(solid),
|
||||
..Default::default()
|
||||
};
|
||||
PenNode::Rectangle(RectangleNode {
|
||||
base: PenNodeBase {
|
||||
id: id.into(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//! - 2+ selected → union of the selection's aggregate bounds.
|
||||
//! - 1 selected → parent container's aggregate bounds (a top-level
|
||||
//! node has no useful reference and silently no-ops).
|
||||
//!
|
||||
//! Distribute requires 3+ nodes; fewer silently no-ops.
|
||||
//!
|
||||
//! All deltas go through [`crate::walkers::translate_subtree`], so
|
||||
|
|
@ -146,8 +147,8 @@ fn apply_distribute(children: &mut [PenNode], editable: &[NodeId], action: Align
|
|||
// descendant must not contribute two anchors.
|
||||
let filtered: Vec<NodeId> = editable
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|id| !is_ancestor_in_set(children, id, editable))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut sorted: Vec<(NodeId, DocRect)> = filtered
|
||||
.iter()
|
||||
|
|
@ -173,8 +174,8 @@ fn apply_distribute(children: &mut [PenNode], editable: &[NodeId], action: Align
|
|||
let last_c = center(&sorted[n - 1].1);
|
||||
let step = (last_c - first_c) / (n - 1) as f64;
|
||||
let mut moved = false;
|
||||
for i in 1..n - 1 {
|
||||
let (id, cur) = (sorted[i].0.clone(), sorted[i].1);
|
||||
for (i, entry) in sorted.iter().enumerate().take(n - 1).skip(1) {
|
||||
let (id, cur) = (entry.0.clone(), entry.1);
|
||||
let cur_c = center(&cur);
|
||||
let target_c = first_c + step * i as f64;
|
||||
let delta = target_c - cur_c;
|
||||
|
|
|
|||
|
|
@ -222,8 +222,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn begin_send_pushes_user_plus_empty_assistant_and_raises_flag() {
|
||||
let mut chat = ChatState::default();
|
||||
chat.input = " design a login page ".into();
|
||||
let mut chat = ChatState {
|
||||
input: " design a login page ".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(chat.begin_send());
|
||||
assert_eq!(chat.messages.len(), 2);
|
||||
assert_eq!(chat.messages[0].role, ChatRole::User);
|
||||
|
|
@ -236,8 +238,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn begin_send_empty_input_no_ops() {
|
||||
let mut chat = ChatState::default();
|
||||
chat.input = " ".into();
|
||||
let mut chat = ChatState {
|
||||
input: " ".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!chat.begin_send());
|
||||
assert!(chat.messages.is_empty());
|
||||
assert!(chat.pending_send.is_none());
|
||||
|
|
@ -245,8 +249,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn send_echo_appends_user_and_assistant() {
|
||||
let mut chat = ChatState::default();
|
||||
chat.input = "hi".into();
|
||||
let mut chat = ChatState {
|
||||
input: "hi".into(),
|
||||
..Default::default()
|
||||
};
|
||||
chat.send();
|
||||
assert_eq!(chat.messages.len(), 2);
|
||||
assert_eq!(chat.messages[1].role, ChatRole::Assistant);
|
||||
|
|
|
|||
|
|
@ -251,6 +251,9 @@ impl EditorState {
|
|||
}
|
||||
|
||||
/// `InsertNode` — build + append a fresh leaf on the active page.
|
||||
// Args mirror the `InsertNode` command fields one-for-one; bundling
|
||||
// them into a struct would just shadow the DTO with no real gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn cmd_insert_node(
|
||||
&mut self,
|
||||
kind: &str,
|
||||
|
|
@ -285,6 +288,8 @@ impl EditorState {
|
|||
}
|
||||
|
||||
/// `UpdateNode` — patch optional fields on an existing node.
|
||||
// Args mirror the `UpdateNode` command fields one-for-one.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn cmd_update_node(
|
||||
&mut self,
|
||||
node_id: &NodeId,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ pub struct EditorState {
|
|||
/// Editor-UI overlay + panel state — the widget-layer toggles, hover
|
||||
/// targets, menu / modal open flags and panel metrics. With this
|
||||
/// + `chat` + `components`, `EditorState` is a complete state
|
||||
/// superset of shell-core's `Document` (Phase 6 Task 6.1a).
|
||||
/// superset of shell-core's `Document` (Phase 6 Task 6.1a).
|
||||
pub editor_ui: EditorUiState,
|
||||
/// AI chat sub-state — message transcript, input draft, panel
|
||||
/// anchor, model catalog. Mirrors shell-core's `Document.chat`.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use crate::node_id::NodeId;
|
||||
use crate::pen_node_ext::PenNodeExt;
|
||||
use crate::test_support::{frame, group, rect, sample, state_with, text};
|
||||
use crate::test_support::{frame, group, rect, sample, state_with};
|
||||
use crate::walkers::{find_node, ReorderDirection};
|
||||
|
||||
// --- Selection -------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
//! (Task 4.7) is a re-export with no behaviour change.
|
||||
|
||||
/// Editor tool — what primary mouse drag does on the canvas.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Tool {
|
||||
#[default]
|
||||
Select,
|
||||
Rect,
|
||||
Ellipse,
|
||||
|
|
@ -58,9 +59,3 @@ impl Tool {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Tool {
|
||||
fn default() -> Self {
|
||||
Tool::Select
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ pub fn extract_node(children: &mut Vec<PenNode>, target: &NodeId) -> Option<PenN
|
|||
|
||||
/// Insert `node` immediately before `anchor`. `Ok(())` on success;
|
||||
/// `Err(node)` bounces the payload back on miss.
|
||||
// `Err(PenNode)` is the bounced-back payload, not an error type.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn insert_before_in_children(
|
||||
children: &mut Vec<PenNode>,
|
||||
anchor: &NodeId,
|
||||
|
|
@ -129,6 +131,8 @@ pub fn insert_before_in_children(
|
|||
}
|
||||
|
||||
/// Insert `node` immediately after `anchor`. `Ok(())` / `Err(node)`.
|
||||
// `Err(PenNode)` is the bounced-back payload, not an error type.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn insert_after_in_children(
|
||||
children: &mut Vec<PenNode>,
|
||||
anchor: &NodeId,
|
||||
|
|
@ -152,8 +156,11 @@ pub fn insert_after_in_children(
|
|||
|
||||
/// Append `node` as the LAST child of `parent`. `Ok(())` / `Err(node)`.
|
||||
/// Fails (bounces the payload) when `parent` is not a container.
|
||||
// `Err(PenNode)` is the bounced-back payload, not an error type — boxing
|
||||
// it would be a needless allocation on the hot insert path.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn append_into(
|
||||
children: &mut Vec<PenNode>,
|
||||
children: &mut [PenNode],
|
||||
parent: &NodeId,
|
||||
node: PenNode,
|
||||
) -> Result<(), PenNode> {
|
||||
|
|
@ -180,7 +187,7 @@ pub fn append_into(
|
|||
|
||||
/// Swap the node matching `target` with its next / prev sibling.
|
||||
pub fn reorder_in_children(
|
||||
children: &mut Vec<PenNode>,
|
||||
children: &mut [PenNode],
|
||||
target: &NodeId,
|
||||
direction: ReorderDirection,
|
||||
) -> bool {
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@ fn cycle_active_axis_returns_false_for_empty_values() {
|
|||
values: Vec::new(),
|
||||
});
|
||||
assert!(!tbl.cycle_active_axis_value("empty"));
|
||||
assert!(tbl.active_theme.get("empty").is_none());
|
||||
assert!(!tbl.active_theme.contains_key("empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -161,12 +161,8 @@ impl<'a> AgentSettingsPanel<'a> {
|
|||
return None;
|
||||
}
|
||||
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
|
||||
for i in 0..AgentProvider::ALL.len() {
|
||||
if rect_contains(agent_card_rect_in(panel, i, &self.settings), scrolled) {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
(0..AgentProvider::ALL.len())
|
||||
.find(|&i| rect_contains(agent_card_rect_in(panel, i, &self.settings), scrolled))
|
||||
}
|
||||
|
||||
/// Total content height for the active tab. Host uses this to
|
||||
|
|
@ -433,6 +429,8 @@ fn paint_section_header(
|
|||
/// `right_inset` reserves space on the right edge for an
|
||||
/// overlapping chrome element — currently the panel's close X
|
||||
/// which sits over the top-of-content row.
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_section_header_inset(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
//! - Align L / Center-H / Right
|
||||
//! - Align Top / Center-V / Bottom
|
||||
//! - Distribute H / Distribute V
|
||||
//!
|
||||
//! Anchored to the horizontal center of the canvas region, ~16 px
|
||||
//! below the canvas top edge.
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ pub const ALIGN_TOOLBAR_WIDTH: f32 =
|
|||
/// column. Mirrors paint-side geometry: `TOOLBAR_INSET_X` (12 in
|
||||
/// shell-native `widget_host/helpers.rs` and shell-web `widget_host.rs`)
|
||||
/// + `TOOLBAR_WIDTH` (44) = 56 — leaves the tool column unobscured.
|
||||
///
|
||||
/// Has to be a shell-core local because shell-core can't depend on
|
||||
/// either host crate's helpers. Keep in sync if either constant moves.
|
||||
const VERTICAL_TOOLBAR_RESERVE: f32 = 56.0;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
//! - Format radio group: PNG / JPEG / WEBP / SVG / PDF
|
||||
//! - Scale radio group: 1× / 2× / 3×
|
||||
//! - Cancel / Export buttons
|
||||
//!
|
||||
//! Selection writes back to `Document.ui.export_format` /
|
||||
//! `export_scale`; Export dispatches `FileAction::ExportImage` so
|
||||
//! the host's save dialog uses the chosen format + scale.
|
||||
|
|
|
|||
|
|
@ -345,6 +345,8 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_row(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ impl Widget for LayerPanel {
|
|||
rect.origin.x,
|
||||
y,
|
||||
rect.size.x,
|
||||
&self.pages_label,
|
||||
self.pages_label,
|
||||
);
|
||||
// "+" add-page affordance, top-right of header row.
|
||||
let plus_x = rect.origin.x + rect.size.x - ROW_PAD_X - 12.0;
|
||||
|
|
@ -546,7 +546,7 @@ impl Widget for LayerPanel {
|
|||
rect.origin.x,
|
||||
y,
|
||||
rect.size.x,
|
||||
&self.layers_label,
|
||||
self.layers_label,
|
||||
);
|
||||
y += SECTION_HEADER_HEIGHT;
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ pub(super) fn paint_drag_ghost(
|
|||
/// background) with a subtle primary underline + blinking caret.
|
||||
/// Tightened caret-to-text gap and uses `blink_visible` so the
|
||||
/// caret pulses at the same cadence as the chat / property input.
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn paint_rename_input(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ pub fn paint_fill_type_picker(
|
|||
}
|
||||
// ── Fill section ──────────────────────────────────────────────────
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_fill_section(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
@ -144,7 +146,7 @@ pub fn paint_fill_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label_with_add(cx, theme, &labels.fill, x, y, width);
|
||||
let mut y = paint_section_label_with_add(cx, theme, labels.fill, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let fill = snapshot.fill.unwrap_or(Color::WHITE);
|
||||
let swatch_rect = Rect {
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@ pub fn paint_input_with_icon(
|
|||
paint_input_with_icon_focused(cx, theme, rect, icon, value, unit, false, false);
|
||||
}
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_input_with_icon_focused(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
|
|||
|
|
@ -150,12 +150,12 @@ pub fn paint_tab_strip(
|
|||
use op_editor_core::PropertyTab;
|
||||
let pad = 14.0;
|
||||
let tab_y = y + 6.0;
|
||||
let design_w = (cx.backend.measure_text(&labels.tab_design, 13.0) + 24.0).max(48.0);
|
||||
let design_w = (cx.backend.measure_text(labels.tab_design, 13.0) + 24.0).max(48.0);
|
||||
let design_rect = Rect {
|
||||
origin: Point2D::new(x + pad, tab_y),
|
||||
size: Point2D::new(design_w, 26.0),
|
||||
};
|
||||
let code_w = (cx.backend.measure_text(&labels.tab_code, 13.0) + 24.0).max(48.0);
|
||||
let code_w = (cx.backend.measure_text(labels.tab_code, 13.0) + 24.0).max(48.0);
|
||||
let code_rect = Rect {
|
||||
origin: Point2D::new(design_rect.origin.x + design_rect.size.x + 6.0, tab_y),
|
||||
size: Point2D::new(code_w, 26.0),
|
||||
|
|
@ -176,7 +176,7 @@ pub fn paint_tab_strip(
|
|||
theme.muted_foreground
|
||||
};
|
||||
let design_label = TextLayout::single_run(
|
||||
&labels.tab_design,
|
||||
labels.tab_design,
|
||||
"system-ui",
|
||||
13.0,
|
||||
to_jian_color(design_color),
|
||||
|
|
@ -187,7 +187,7 @@ pub fn paint_tab_strip(
|
|||
Point2D::new(design_rect.origin.x + 12.0, design_rect.origin.y + 18.0),
|
||||
);
|
||||
let code_label = TextLayout::single_run(
|
||||
&labels.tab_code,
|
||||
labels.tab_code,
|
||||
"system-ui",
|
||||
13.0,
|
||||
to_jian_color(code_color),
|
||||
|
|
@ -260,13 +260,13 @@ pub fn paint_create_component(
|
|||
1.4,
|
||||
);
|
||||
let label = TextLayout::single_run(
|
||||
&labels.create_component,
|
||||
labels.create_component,
|
||||
"system-ui",
|
||||
13.0,
|
||||
to_jian_color(theme.foreground),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
let label_w = cx.backend.measure_text(&labels.create_component, 13.0);
|
||||
let label_w = cx.backend.measure_text(labels.create_component, 13.0);
|
||||
cx.backend.draw_text(
|
||||
&label,
|
||||
Point2D::new(
|
||||
|
|
@ -279,6 +279,8 @@ pub fn paint_create_component(
|
|||
|
||||
// ── Position section ──────────────────────────────────────────────
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_position_section(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
@ -289,7 +291,7 @@ pub fn paint_position_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.position, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.position, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let half_w = (usable_w - 8.0) / 2.0;
|
||||
let x_rect = Rect {
|
||||
|
|
@ -370,7 +372,7 @@ pub fn paint_flex_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.flex_layout, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.flex_layout, x, y, width);
|
||||
// TS layout-section.tsx uses Columns3 / Rows3 / LayoutGrid for
|
||||
// the three flex modes; LayoutGrid is the default-active mode
|
||||
// (Free / 自由布局).
|
||||
|
|
@ -418,6 +420,8 @@ pub fn paint_flex_section(
|
|||
|
||||
// ── Size section ──────────────────────────────────────────────────
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_size_section(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
@ -429,7 +433,7 @@ pub fn paint_size_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.size, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.size, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let half_w = (usable_w - 8.0) / 2.0;
|
||||
let w_rect = Rect {
|
||||
|
|
@ -468,7 +472,7 @@ pub fn paint_size_section(
|
|||
x + PAD_X,
|
||||
y,
|
||||
half_w,
|
||||
&labels.fill_width,
|
||||
labels.fill_width,
|
||||
flags.fill_width,
|
||||
);
|
||||
paint_check_row(
|
||||
|
|
@ -477,7 +481,7 @@ pub fn paint_size_section(
|
|||
x + PAD_X + half_w + 8.0,
|
||||
y,
|
||||
half_w,
|
||||
&labels.fill_height,
|
||||
labels.fill_height,
|
||||
flags.fill_height,
|
||||
);
|
||||
y += row_h;
|
||||
|
|
@ -487,7 +491,7 @@ pub fn paint_size_section(
|
|||
x + PAD_X,
|
||||
y,
|
||||
half_w,
|
||||
&labels.hug_width,
|
||||
labels.hug_width,
|
||||
flags.hug_width,
|
||||
);
|
||||
paint_check_row(
|
||||
|
|
@ -496,7 +500,7 @@ pub fn paint_size_section(
|
|||
x + PAD_X + half_w + 8.0,
|
||||
y,
|
||||
half_w,
|
||||
&labels.hug_height,
|
||||
labels.hug_height,
|
||||
flags.hug_height,
|
||||
);
|
||||
y += row_h;
|
||||
|
|
@ -506,7 +510,7 @@ pub fn paint_size_section(
|
|||
x + PAD_X,
|
||||
y,
|
||||
usable_w,
|
||||
&labels.clip_content,
|
||||
labels.clip_content,
|
||||
flags.clip_content,
|
||||
);
|
||||
y += row_h + 12.0;
|
||||
|
|
@ -562,7 +566,7 @@ pub fn paint_layer_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.layer, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.layer, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let row = Rect {
|
||||
origin: Point2D::new(x + PAD_X, y),
|
||||
|
|
@ -586,6 +590,8 @@ pub fn paint_layer_section(
|
|||
|
||||
// ── Stroke section ────────────────────────────────────────────────
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_stroke_section(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
@ -596,7 +602,7 @@ pub fn paint_stroke_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.stroke, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.stroke, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let stroke_color = snapshot.stroke.map(|s| s.color).unwrap_or(Color {
|
||||
r: 0x37 as f32 / 255.0,
|
||||
|
|
@ -695,7 +701,7 @@ pub fn paint_effects_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let y = paint_section_label_with_add(cx, theme, &labels.effects, x, y, width);
|
||||
let y = paint_section_label_with_add(cx, theme, labels.effects, x, y, width);
|
||||
let after = y + 8.0;
|
||||
paint_section_divider(cx, theme, x, after, width);
|
||||
after + SECTION_GAP
|
||||
|
|
@ -703,6 +709,8 @@ pub fn paint_effects_section(
|
|||
|
||||
// ── Export section ────────────────────────────────────────────────
|
||||
|
||||
// Paint-context + geometry args threaded through; a struct adds no gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn paint_export_section(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
|
|
@ -713,7 +721,7 @@ pub fn paint_export_section(
|
|||
y: f32,
|
||||
width: f32,
|
||||
) -> f32 {
|
||||
let mut y = paint_section_label(cx, theme, &labels.export, x, y, width);
|
||||
let mut y = paint_section_label(cx, theme, labels.export, x, y, width);
|
||||
let usable_w = width - PAD_X * 2.0;
|
||||
let half_w = (usable_w - 8.0) / 2.0;
|
||||
// Left pill: current scale. Right pill: current format. Click
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::theme::Theme;
|
|||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect};
|
||||
use crate::{Point2D, Rect};
|
||||
use op_editor_core::EditorState;
|
||||
use op_editor_core::Tool;
|
||||
|
||||
|
|
@ -380,14 +380,13 @@ fn paint_button(cx: &mut PaintCx<'_>, theme: &Theme, x: f32, y: f32, icon: Icon,
|
|||
origin: Point2D::new(x, y),
|
||||
size: Point2D::new(BUTTON_SIZE, BUTTON_SIZE),
|
||||
};
|
||||
let icon_color: Color;
|
||||
if active {
|
||||
let icon_color = if active {
|
||||
cx.backend
|
||||
.fill_round_rect(button_rect, BUTTON_RADIUS, theme.primary);
|
||||
icon_color = theme.primary_foreground;
|
||||
theme.primary_foreground
|
||||
} else {
|
||||
icon_color = theme.muted_foreground;
|
||||
}
|
||||
theme.muted_foreground
|
||||
};
|
||||
let icon_origin = Point2D::new(
|
||||
x + (BUTTON_SIZE - ICON_SIZE) / 2.0,
|
||||
y + (BUTTON_SIZE - ICON_SIZE) / 2.0,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
//!
|
||||
//! Mirrors `apps/web/src/components/editor/top-bar.tsx`: panel-toggle
|
||||
//! + folder + brand on the left, file name centered, theme +
|
||||
//! agent-status + i18n + fullscreen on the right. Click handling is
|
||||
//! a P6 follow-up; Step 4 paints only.
|
||||
//! agent-status + i18n + fullscreen on the right. Click handling is
|
||||
//! a P6 follow-up; Step 4 paints only.
|
||||
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::{theme_for, translate};
|
||||
|
|
|
|||
|
|
@ -170,51 +170,6 @@ fn collect_top_level_blocks(s: &str) -> Option<Vec<&str>> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Walk a clipboard-JSON payload and count the top-level entries in
|
||||
/// the `"children": [ ... ]` array. Hand-rolled parser (no serde,
|
||||
/// shell-core stays wasm32-light) — counts `{` openings at depth 1
|
||||
/// inside the children array. Returns `None` when the children key
|
||||
/// is absent or the brace tracker can't find a matching close.
|
||||
fn count_top_level_children(s: &str) -> Option<usize> {
|
||||
let needle = "\"children\"";
|
||||
let key_at = s.find(needle)?;
|
||||
let after = &s[key_at + needle.len()..];
|
||||
let bracket = after.find('[')?;
|
||||
let mut depth = 0i32;
|
||||
let mut count = 0usize;
|
||||
let mut in_string = false;
|
||||
let mut escape = false;
|
||||
for c in after[bracket..].chars().skip(1) {
|
||||
if escape {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if c == '\\' {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
if c == '"' {
|
||||
in_string = !in_string;
|
||||
continue;
|
||||
}
|
||||
if in_string {
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'{' => {
|
||||
if depth == 0 {
|
||||
count += 1;
|
||||
}
|
||||
depth += 1;
|
||||
}
|
||||
'}' => depth -= 1,
|
||||
']' if depth == 0 => return Some(count),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The canonical `PenNode` variant a Figma clipboard kind maps onto.
|
||||
/// `PenNode` is a closed 12-variant enum with no `Other` escape
|
||||
/// hatch, so unrecognised Figma kinds round-trip as `Frame` (the
|
||||
|
|
@ -597,7 +552,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn figma_clipboard_node_to_node_builds_matching_pen_node_variant() {
|
||||
let cases: [(&str, fn(&PenNode) -> bool); 8] = [
|
||||
// (figma clipboard kind, predicate asserting the mapped variant).
|
||||
type VariantCase = (&'static str, fn(&PenNode) -> bool);
|
||||
let cases: [VariantCase; 8] = [
|
||||
("RECTANGLE", |n| matches!(n, PenNode::Rectangle(_))),
|
||||
("ELLIPSE", |n| matches!(n, PenNode::Ellipse(_))),
|
||||
("LINE", |n| matches!(n, PenNode::Line(_))),
|
||||
|
|
|
|||
|
|
@ -357,18 +357,10 @@ async fn discover_port(
|
|||
/// after any of those tokens.
|
||||
pub(crate) fn parse_listening_line(line: &str) -> Option<u16> {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
let mut idx = 0;
|
||||
while idx < lower.len() {
|
||||
if let Some(pos) = lower[idx..].find("listening") {
|
||||
idx += pos + "listening".len();
|
||||
// Scan for the first digit run after this token.
|
||||
let rest = &line[idx..];
|
||||
return extract_port(rest);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
let pos = lower.find("listening")?;
|
||||
let idx = pos + "listening".len();
|
||||
// Scan for the first digit run after this token.
|
||||
extract_port(&line[idx..])
|
||||
}
|
||||
|
||||
/// Pull the actual port (not an IP octet) out of `rest`. Strategy:
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl BuiltInProvider {
|
|||
/// Build a BuiltIn provider from a hand-rolled `Provider` impl.
|
||||
/// Used by tests + future settings tabs that need a custom backend
|
||||
/// (e.g., a local proxy or a mocked HTTP server). `system_prompt`
|
||||
/// + `max_output_tokens` apply to every turn for the lifetime of
|
||||
/// and `max_output_tokens` apply to every turn for the lifetime of
|
||||
/// the provider; rebuild a new `BuiltInProvider` to change them.
|
||||
#[allow(dead_code)]
|
||||
pub fn from_provider(
|
||||
|
|
|
|||
|
|
@ -206,7 +206,6 @@ fn build_command(binary: &str, args: &[String]) -> Command {
|
|||
// we never depend on signal propagation for cleanup.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
cmd
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
//! - Line : diagonal stroke across `bounds`
|
||||
//! - Path : polyline through `node.points`
|
||||
//! - Text : authored content at the resolved bounds
|
||||
//!
|
||||
//! Per-node rotation honoured via `Canvas::rotate`.
|
||||
//!
|
||||
//! The scene is layout-resolved: every `SceneNode::bounds` is the
|
||||
|
|
@ -107,15 +108,68 @@ pub fn export_raster(
|
|||
format: RasterFormat,
|
||||
scale: f32,
|
||||
) -> Result<(), String> {
|
||||
let scale = if scale.is_finite() {
|
||||
scale.clamp(0.5, 8.0)
|
||||
} else {
|
||||
2.0
|
||||
};
|
||||
let scale = clamp_scale(scale);
|
||||
let Some(page) = scene.active_page() else {
|
||||
return Err("no active page".into());
|
||||
};
|
||||
let bounds = page_bounds(page).ok_or("nothing to export")?;
|
||||
render_raster(bounds, target, format, scale, |canvas| {
|
||||
for node in &page.children {
|
||||
paint_node(canvas, node);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Raster-export a single node + its subtree by id — the "export this
|
||||
/// layer" path (TS parity: `exportLayerToRaster`). The surface is
|
||||
/// cropped to the node's painted bounds via the same `collect_bounds`
|
||||
/// traversal `export_raster` uses for the whole page. Errors when the
|
||||
/// id is unknown on the active page or the node paints nothing.
|
||||
pub fn export_node_raster(
|
||||
scene: &LayoutScene,
|
||||
node_id: &str,
|
||||
target: &StdPath,
|
||||
format: RasterFormat,
|
||||
scale: f32,
|
||||
) -> Result<(), String> {
|
||||
let scale = clamp_scale(scale);
|
||||
let Some(page) = scene.active_page() else {
|
||||
return Err("no active page".into());
|
||||
};
|
||||
let node = page
|
||||
.find(node_id)
|
||||
.ok_or_else(|| format!("node {node_id} not found on the active page"))?;
|
||||
let mut acc = BoundsAcc::new();
|
||||
collect_bounds(node, glam::Affine2::IDENTITY, &mut acc);
|
||||
let bounds = acc
|
||||
.into_rect()
|
||||
.ok_or_else(|| format!("node {node_id} paints nothing"))?;
|
||||
render_raster(bounds, target, format, scale, |canvas| {
|
||||
paint_node(canvas, node);
|
||||
})
|
||||
}
|
||||
|
||||
/// Clamp a caller-supplied export scale to the @0.5x..@8x range,
|
||||
/// defaulting a non-finite value to @2x (TS export-dialog parity).
|
||||
fn clamp_scale(scale: f32) -> f32 {
|
||||
if scale.is_finite() {
|
||||
scale.clamp(0.5, 8.0)
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared surface-alloc + background-clear + encode + write path for
|
||||
/// the whole-page and single-node raster exporters. `bounds` is the
|
||||
/// painted-content rect (doc-space); `paint` draws into the canvas
|
||||
/// after it has been scaled + translated so `bounds` sits at `MARGIN`.
|
||||
fn render_raster(
|
||||
bounds: Rect,
|
||||
target: &StdPath,
|
||||
format: RasterFormat,
|
||||
scale: f32,
|
||||
paint: impl FnOnce(&Canvas),
|
||||
) -> Result<(), String> {
|
||||
let width_px = ((bounds.size.x + MARGIN * 2.0) * scale).round() as i32;
|
||||
let height_px = ((bounds.size.y + MARGIN * 2.0) * scale).round() as i32;
|
||||
let info = skia_safe::ImageInfo::new(
|
||||
|
|
@ -133,9 +187,7 @@ pub fn export_raster(
|
|||
}
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate((MARGIN - bounds.origin.x, MARGIN - bounds.origin.y));
|
||||
for node in &page.children {
|
||||
paint_node(canvas, node);
|
||||
}
|
||||
paint(canvas);
|
||||
let image = surface.image_snapshot();
|
||||
let data = image
|
||||
.encode(None, format.skia(), format.quality())
|
||||
|
|
@ -815,4 +867,61 @@ mod tests {
|
|||
assert_eq!(b.size.x, 200.0);
|
||||
assert_eq!(b.size.y, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_node_raster_crops_to_the_named_node() {
|
||||
// Two side-by-side rects: a 100×50 at origin and a 40×40 far
|
||||
// away. Exporting only the small node must produce a surface
|
||||
// cropped to ITS bounds, not the page union.
|
||||
let grey = Color {
|
||||
r: 0.5,
|
||||
g: 0.5,
|
||||
b: 0.5,
|
||||
a: 1.0,
|
||||
};
|
||||
let scene = scene_with(vec![
|
||||
filled_rect("big", 0.0, 0.0, 100.0, 50.0, grey),
|
||||
filled_rect("small", 400.0, 400.0, 40.0, 40.0, grey),
|
||||
]);
|
||||
let tmp = std::env::temp_dir().join(format!("op-export-node-{}.png", std::process::id()));
|
||||
let res = export_node_raster(&scene, "small", &tmp, RasterFormat::Png, 1.0);
|
||||
assert!(res.is_ok(), "export_node_raster failed: {res:?}");
|
||||
let bytes = std::fs::read(&tmp).unwrap();
|
||||
assert_eq!(
|
||||
&bytes[..8],
|
||||
&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]
|
||||
);
|
||||
// The cropped surface is the 40×40 node + 2×MARGIN, far
|
||||
// smaller than the ~440 px page union the whole-page export
|
||||
// would have produced. PNG IHDR carries the dimensions as
|
||||
// big-endian u32s at byte offsets 16 (width) and 20 (height).
|
||||
let png_width = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
|
||||
let png_height = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
||||
let expected = (40.0 + MARGIN * 2.0) as u32;
|
||||
assert_eq!(png_width, expected);
|
||||
assert_eq!(png_height, expected);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_node_raster_errors_on_unknown_id() {
|
||||
let scene = scene_with(vec![filled_rect(
|
||||
"n10",
|
||||
0.0,
|
||||
0.0,
|
||||
10.0,
|
||||
10.0,
|
||||
Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
)]);
|
||||
let tmp =
|
||||
std::env::temp_dir().join(format!("op-export-node-miss-{}.png", std::process::id()));
|
||||
let res = export_node_raster(&scene, "ghost", &tmp, RasterFormat::Png, 1.0);
|
||||
assert!(res.is_err(), "expected Err on unknown id, got {res:?}");
|
||||
assert!(res.unwrap_err().contains("not found"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,43 +144,6 @@ impl DesktopApp {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cursor_only_redraw_without_visible_state_change_skips_present() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
app.pending_cursor_move = Some((1200.0, 20.0));
|
||||
|
||||
assert!(!app.prepare_redraw());
|
||||
assert!(!app.redraw_pending);
|
||||
assert!(app.pending_cursor_move.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumed_press_dirties_existing_cursor_redraw_without_second_request() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
|
||||
assert!(!app.request_redraw(true));
|
||||
assert!(app.prepare_redraw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_redraw_still_paints_when_layer_hover_changes() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
app.pending_cursor_move = Some((
|
||||
20.0,
|
||||
op_editor_ui::widgets::TOP_BAR_HEIGHT + 8.0 + 28.0 + 16.0,
|
||||
));
|
||||
|
||||
assert!(app.prepare_redraw());
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for DesktopApp {
|
||||
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
|
||||
// When the WaitUntil deadline fires, the next redraw paints
|
||||
|
|
@ -515,22 +478,19 @@ impl ApplicationHandler for DesktopApp {
|
|||
self.request_redraw(true);
|
||||
}
|
||||
}
|
||||
WindowEvent::Ime(ime) => {
|
||||
// CJK composition: macOS / X11 / Wayland route the
|
||||
// committed candidate string through here. We don't
|
||||
// paint the preedit yet; only the final commit is
|
||||
// pushed into the focused input.
|
||||
if let winit::event::Ime::Commit(text) = ime {
|
||||
let mut consumed = false;
|
||||
for ch in text.chars() {
|
||||
if self.host.apply_text(ch) {
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
if consumed {
|
||||
self.request_redraw(true);
|
||||
// CJK composition: macOS / X11 / Wayland route the committed
|
||||
// candidate string through here. We don't paint the preedit
|
||||
// yet; only the final commit is pushed into the focused input.
|
||||
WindowEvent::Ime(winit::event::Ime::Commit(text)) => {
|
||||
let mut consumed = false;
|
||||
for ch in text.chars() {
|
||||
if self.host.apply_text(ch) {
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
if consumed {
|
||||
self.request_redraw(true);
|
||||
}
|
||||
}
|
||||
WindowEvent::ModifiersChanged(mods) => {
|
||||
let state = mods.state();
|
||||
|
|
@ -811,3 +771,40 @@ fn main() {
|
|||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cursor_only_redraw_without_visible_state_change_skips_present() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
app.pending_cursor_move = Some((1200.0, 20.0));
|
||||
|
||||
assert!(!app.prepare_redraw());
|
||||
assert!(!app.redraw_pending);
|
||||
assert!(app.pending_cursor_move.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumed_press_dirties_existing_cursor_redraw_without_second_request() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
|
||||
assert!(!app.request_redraw(true));
|
||||
assert!(app.prepare_redraw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_redraw_still_paints_when_layer_hover_changes() {
|
||||
let mut app = DesktopApp::new();
|
||||
app.redraw_pending = true;
|
||||
app.pending_cursor_move = Some((
|
||||
20.0,
|
||||
op_editor_ui::widgets::TOP_BAR_HEIGHT + 8.0 + 28.0 + 16.0,
|
||||
));
|
||||
|
||||
assert!(app.prepare_redraw());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,25 +348,28 @@ pub fn run_action(
|
|||
// (runs jian's flex pass + `$ref` fill resolution).
|
||||
let scene = op_pen_loader::editor_state_to_layout_scene(host.editor_state());
|
||||
let scene = &scene;
|
||||
// When exactly one node is selected, raster export
|
||||
// crops to that layer (TS parity: exportLayerToRaster);
|
||||
// otherwise the whole active page is exported. SVG /
|
||||
// PDF always stay page-level.
|
||||
let single_node: Option<String> = {
|
||||
let st = host.editor_state();
|
||||
if st.selection_count() == 1 && st.selection.anchor.is_real() {
|
||||
Some(st.selection.anchor.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let raster = |rf: crate::export::RasterFormat| -> Result<(), String> {
|
||||
match &single_node {
|
||||
Some(id) => crate::export::export_node_raster(scene, id, &path, rf, scale),
|
||||
None => crate::export::export_raster(scene, &path, rf, scale),
|
||||
}
|
||||
};
|
||||
let result: Result<(), String> = match fmt {
|
||||
Fmt::Png => crate::export::export_raster(
|
||||
scene,
|
||||
&path,
|
||||
crate::export::RasterFormat::Png,
|
||||
scale,
|
||||
),
|
||||
Fmt::Jpeg => crate::export::export_raster(
|
||||
scene,
|
||||
&path,
|
||||
crate::export::RasterFormat::Jpeg,
|
||||
scale,
|
||||
),
|
||||
Fmt::Webp => crate::export::export_raster(
|
||||
scene,
|
||||
&path,
|
||||
crate::export::RasterFormat::Webp,
|
||||
scale,
|
||||
),
|
||||
Fmt::Png => raster(crate::export::RasterFormat::Png),
|
||||
Fmt::Jpeg => raster(crate::export::RasterFormat::Jpeg),
|
||||
Fmt::Webp => raster(crate::export::RasterFormat::Webp),
|
||||
Fmt::Svg => crate::export::export_svg(scene, &path),
|
||||
Fmt::Pdf => crate::export_pdf::export_pdf(scene, &path),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ pub struct NativeBackend {
|
|||
const ROBOTO_TTF: &[u8] = include_bytes!("../../../op-host-web/assets/Roboto-Regular.ttf");
|
||||
|
||||
/// Union of every CJK codepoint that appears in the editor chrome
|
||||
/// + settings modal. Pre-warmed at `NativeBackend::new` so the
|
||||
/// and the settings modal. Pre-warmed at `NativeBackend::new` so the
|
||||
/// first cross-tab paint doesn't synchronously call
|
||||
/// `FontMgr::match_family_style_character` for ~50 fresh glyphs.
|
||||
const PREWARM_CJK_CODEPOINTS: &str = "\
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ impl WidgetHostNative {
|
|||
};
|
||||
// shell-core hit-test returns shell-core `NodeId`s; translate
|
||||
// to op-editor-core ids for storage on `editor_ui`.
|
||||
let new_layer_ec = new_layer.as_ref().map(|id| id.clone());
|
||||
let new_layer_ec = new_layer.clone();
|
||||
let changed = new_layer_ec != self.editor_state.editor_ui.hovered_layer_id
|
||||
|| new_page != self.editor_state.editor_ui.hovered_page_index;
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
//! Per-OS dispatch:
|
||||
//! - Linux : EGL pbuffer + `SharedSkiaContext` GL surface (non-ignored).
|
||||
//! - macOS : invisible winit window + `SharedSkiaContext::new_desktop`
|
||||
//! (non-ignored — runs on developer machine + macOS CI).
|
||||
//! (non-ignored — runs on developer machine + macOS CI).
|
||||
//! - Windows: `#[ignore]` per spec §8.1 (no GPU on standard Actions runner;
|
||||
//! covered by manual smoke notes).
|
||||
//! covered by manual smoke notes).
|
||||
//!
|
||||
//! All paths drive a single chrome `fill_rect`(red) through the live
|
||||
//! pipeline and read pixels back via the FBO chain (spec §7.2 #3).
|
||||
|
|
|
|||
|
|
@ -235,6 +235,9 @@ fn composition_update_no_selection_passthrough() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
// The reversed `3..1` range is the deliberate test input — the mapper
|
||||
// must reject a misordered selection and return `None`.
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
fn composition_update_misordered_selection_returns_none() {
|
||||
let update = composition_update("hello".to_string(), Some(3..1));
|
||||
match update.kind {
|
||||
|
|
|
|||
|
|
@ -345,6 +345,9 @@ pub fn set_node_name_snapshot() -> SetNodeName {
|
|||
|
||||
/// Parse an optional `"true"` / `"false"` argument. `Ok(None)` when
|
||||
/// the key is absent; `Err` on a non-boolean value.
|
||||
// `ToolOutcome` is the shared MCP outcome type — boxing it broadly to
|
||||
// shrink the `Err` variant would destabilize every tool signature.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_opt_bool(args: &BTreeMap<String, String>, key: &str) -> Result<Option<bool>, ToolOutcome> {
|
||||
match args.get(key) {
|
||||
None => Ok(None),
|
||||
|
|
@ -361,6 +364,8 @@ fn parse_opt_bool(args: &BTreeMap<String, String>, key: &str) -> Result<Option<b
|
|||
|
||||
/// Parse an optional finite-`f64` argument. `Ok(None)` when the key is
|
||||
/// absent; `Err` on a non-numeric / non-finite value.
|
||||
// `ToolOutcome` is the shared MCP outcome type — see `parse_opt_bool`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_opt_f64(args: &BTreeMap<String, String>, key: &str) -> Result<Option<f64>, ToolOutcome> {
|
||||
match args.get(key) {
|
||||
None => Ok(None),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ pub fn set_active_page_snapshot() -> SetActivePage {
|
|||
SetActivePage
|
||||
}
|
||||
|
||||
// `ToolOutcome` is the shared MCP outcome type — boxing it broadly to
|
||||
// shrink the `Err` variant would destabilize every tool signature.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_u32_arg(args: &BTreeMap<String, String>, key: &str) -> Result<u32, ToolOutcome> {
|
||||
let Some(raw) = args.get(key) else {
|
||||
return Err(ToolOutcome::Err(
|
||||
|
|
@ -327,6 +330,8 @@ impl McpTool for SetViewport {
|
|||
"set_viewport"
|
||||
}
|
||||
fn call(&self, args: &BTreeMap<String, String>) -> ToolOutcome {
|
||||
// `ToolOutcome` is the shared MCP outcome type — see `parse_u32_arg`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_opt_i32(
|
||||
args: &BTreeMap<String, String>,
|
||||
key: &str,
|
||||
|
|
@ -457,7 +462,7 @@ impl McpTool for SetActiveTool {
|
|||
const ALLOWED_TOOLS: &[&str] = &[
|
||||
"select", "rect", "ellipse", "polygon", "line", "pen", "text", "frame", "hand",
|
||||
];
|
||||
if !ALLOWED_TOOLS.iter().any(|t| *t == tool.as_str()) {
|
||||
if !ALLOWED_TOOLS.contains(&tool.as_str()) {
|
||||
return ToolOutcome::Err(
|
||||
ToolErrorCode::InvalidArgument,
|
||||
format!(
|
||||
|
|
|
|||
|
|
@ -483,8 +483,6 @@ fn extract_field<'a>(line: &'a str, key: &str) -> Option<&'a str> {
|
|||
let val = after_colon[colon..].trim_start();
|
||||
let val_start = start + colon + (after_colon[colon..].len() - val.len());
|
||||
// Read until next , or }.
|
||||
let end_rel = val
|
||||
.find(|c: char| c == ',' || c == '}')
|
||||
.unwrap_or(val.len());
|
||||
let end_rel = val.find([',', '}']).unwrap_or(val.len());
|
||||
Some(line[val_start..val_start + end_rel].trim())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ impl McpTool for NudgeSelected {
|
|||
"nudge_selected"
|
||||
}
|
||||
fn call(&self, args: &BTreeMap<String, String>) -> ToolOutcome {
|
||||
// `ToolOutcome` is the shared MCP outcome type — boxing it broadly
|
||||
// to shrink the `Err` variant would destabilize every tool signature.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_required_i32(
|
||||
args: &BTreeMap<String, String>,
|
||||
key: &str,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ use super::{EditorCommand, McpTool, ToolErrorCode, ToolOutcome};
|
|||
/// Parse a `node_id`-style argument into a `NodeId`. Node ids are
|
||||
/// canonical `.op` schema strings — any non-empty string is valid; an
|
||||
/// empty string (the NONE sentinel) is rejected.
|
||||
// `ToolOutcome` is the shared MCP outcome type — boxing it broadly to
|
||||
// shrink the `Err` variant would destabilize every tool signature.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(super) fn parse_node_id(
|
||||
args: &BTreeMap<String, String>,
|
||||
key: &str,
|
||||
|
|
@ -206,6 +209,8 @@ pub(super) const ALLOWED_KINDS: &[&str] = &[
|
|||
"frame", "group", "rect", "ellipse", "polygon", "line", "text", "path",
|
||||
];
|
||||
|
||||
// `ToolOutcome` is the shared MCP outcome type — see `parse_node_id`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_i32_arg(args: &BTreeMap<String, String>, key: &str) -> Result<i32, ToolOutcome> {
|
||||
let Some(raw) = args.get(key) else {
|
||||
return Err(ToolOutcome::Err(
|
||||
|
|
|
|||
|
|
@ -8,34 +8,31 @@
|
|||
//! inside the bundle and the wasm becomes loadable in browsers.
|
||||
//!
|
||||
//! Categories (counts after dedup against the actual import list):
|
||||
//! - allocator: malloc / free / calloc / realloc / malloc_usable_size
|
||||
//! → dlmalloc-rs
|
||||
//! - libm: asinh / acosh / atanh / nextafterf / remainder
|
||||
//! → libm crate
|
||||
//! - libc string: memchr / wmemchr / strcmp / strcpy / strtoull
|
||||
//! → hand-rolled byte-wise impls
|
||||
//! - libc stdio: snprintf / vsnprintf / vfprintf
|
||||
//! → C-side stubs (in `src/stdio_stub.c`) that route
|
||||
//! into Rust extern `wasm_libc_shim_stdio_panic` and
|
||||
//! panic with the symbol name. Skia's font printf
|
||||
//! paths are NOT exercised on the raster +
|
||||
//! custom_empty fontmgr pipeline (verified: 0 env.*
|
||||
//! imports in the post-link bundle); a runtime panic
|
||||
//! here would mean a new Skia call site landed and
|
||||
//! needs a real impl, not a silent empty success.
|
||||
//! - libc misc: abort / __errno_location
|
||||
//! - C++ ABI: __cxa_atexit / __cxa_guard_acquire / __cxa_guard_release
|
||||
//! / __cxa_pure_virtual
|
||||
//! - allocator: malloc / free / calloc / realloc / malloc_usable_size
|
||||
//! → dlmalloc-rs
|
||||
//! - libm: asinh / acosh / atanh / nextafterf / remainder
|
||||
//! → libm crate
|
||||
//! - libc string: memchr / wmemchr / strcmp / strcpy / strtoull
|
||||
//! → hand-rolled byte-wise impls
|
||||
//! - libc stdio: snprintf / vsnprintf / vfprintf
|
||||
//! → C-side stubs (in `src/stdio_stub.c`) that route into Rust extern
|
||||
//! `wasm_libc_shim_stdio_panic` and panic with the symbol name. Skia's
|
||||
//! font printf paths are NOT exercised on the raster + custom_empty
|
||||
//! fontmgr pipeline (verified: 0 env.* imports in the post-link
|
||||
//! bundle); a runtime panic here would mean a new Skia call site
|
||||
//! landed and needs a real impl, not a silent empty success.
|
||||
//! - libc misc: abort / __errno_location
|
||||
//! - C++ ABI: __cxa_atexit / __cxa_guard_acquire / __cxa_guard_release
|
||||
//! / __cxa_pure_virtual
|
||||
//! - operator new/delete (`_Znwm`/`_Znam`/`_ZdlPv*`/`_ZdaPv*`)
|
||||
//! → forward to malloc/free
|
||||
//! - threads: sem_init / sem_destroy / sem_post / sem_wait
|
||||
//! → no-op (single-threaded wasm)
|
||||
//! → forward to malloc/free
|
||||
//! - threads: sem_init / sem_destroy / sem_post / sem_wait
|
||||
//! → no-op (single-threaded wasm)
|
||||
//! - libcxx string / locale / iostream / shared_weak_count / to_string
|
||||
//! → panic stubs; these symbols are linker-pulled by
|
||||
//! templated code that the skia raster + empty
|
||||
//! fontmgr pipeline does not exercise at runtime.
|
||||
//! If any IS hit, the panic becomes a JS exception
|
||||
//! via console_error_panic_hook.
|
||||
//! → panic stubs; these symbols are linker-pulled by templated code
|
||||
//! that the skia raster + empty fontmgr pipeline does not exercise at
|
||||
//! runtime. If any IS hit, the panic becomes a JS exception via
|
||||
//! console_error_panic_hook.
|
||||
//!
|
||||
//! IMPORTANT: this crate must NOT be a Cargo workspace member of any
|
||||
//! native (non-wasm) build — its symbols intentionally collide with
|
||||
|
|
|
|||
Loading…
Reference in a new issue