feat(editor-ui): hit-test Code panel actions
This commit is contained in:
parent
146cac371d
commit
c4e439bfe4
|
|
@ -347,6 +347,17 @@ impl PropertyPanel {
|
|||
// Multi-select inputs / toggles are inert in v1.
|
||||
return None;
|
||||
}
|
||||
if matches!(self.tab, op_editor_core::PropertyTab::Code) {
|
||||
// Same content origin the Code paint uses: panel left, the
|
||||
// pinned tab-strip bottom (`+ TAB_HEIGHT`), panel width.
|
||||
let cy0 = panel_rect.origin.y + crate::widgets::property_panel_inputs::TAB_HEIGHT;
|
||||
let rects =
|
||||
code_action_rects(panel_rect.origin.x, cy0, panel_rect.size.x, &self.codegen);
|
||||
return rects
|
||||
.into_iter()
|
||||
.find(|(_, r)| rect_contains(*r, point))
|
||||
.map(|(a, _)| PropertyPanelAction::Codegen(a));
|
||||
}
|
||||
if self.image_fill_popover_open {
|
||||
if let Some(action) = sections::image_fill_popover_action_at(
|
||||
self.scrolled_rect(panel_rect),
|
||||
|
|
@ -473,7 +484,7 @@ fn rect_contains(r: Rect, p: Point2D) -> bool {
|
|||
&& p.y <= r.origin.y + r.size.y
|
||||
}
|
||||
|
||||
use crate::widgets::property_panel_code::paint_code_panel;
|
||||
use crate::widgets::property_panel_code::{code_action_rects, paint_code_panel};
|
||||
|
||||
impl Widget for PropertyPanel {
|
||||
fn id(&self) -> WidgetId {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::property_panel_action::CodegenAction;
|
||||
use crate::widgets::property_panel_inputs::{
|
||||
paint_section_label, to_jian_color, INPUT_HEIGHT, INPUT_RADIUS, PAD_X, SECTION_GAP,
|
||||
SECTION_HEADER_HEIGHT,
|
||||
};
|
||||
use crate::widgets::PaintCx;
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use op_editor_core::codegen::{ChunkStatus, CodegenPhase, CodegenState};
|
||||
use op_editor_core::codegen::{ChunkStatus, CodegenPhase, CodegenState, Framework};
|
||||
|
||||
/// Height of a framework chip + the gap below the chip row.
|
||||
const CHIP_HEIGHT: f32 = 26.0;
|
||||
|
|
@ -37,6 +39,20 @@ fn origin() -> Point2D {
|
|||
Point2D::new(0.0, 0.0)
|
||||
}
|
||||
|
||||
/// Geometry for a full-width action button at `y`. Shared by the painter
|
||||
/// and `code_action_rects` so a click lands on the drawn button.
|
||||
fn full_button_rect(x: f32, y: f32, w: f32) -> Rect {
|
||||
Rect {
|
||||
origin: Point2D::new(x + PAD_X, y),
|
||||
size: Point2D::new(w - PAD_X * 2.0, INPUT_HEIGHT),
|
||||
}
|
||||
}
|
||||
|
||||
/// y after a full-width button row + its trailing gap.
|
||||
fn full_button_advance(y: f32) -> f32 {
|
||||
y + INPUT_HEIGHT + 12.0
|
||||
}
|
||||
|
||||
/// Paint a full-width action button at `y`. `filled` → primary fill +
|
||||
/// primary-foreground label; otherwise a muted outline (border stroke)
|
||||
/// + foreground label. Returns the y after the button + its gap.
|
||||
|
|
@ -49,10 +65,7 @@ fn paint_full_button(
|
|||
w: f32,
|
||||
filled: bool,
|
||||
) -> f32 {
|
||||
let btn = Rect {
|
||||
origin: Point2D::new(x + PAD_X, y),
|
||||
size: Point2D::new(w - PAD_X * 2.0, INPUT_HEIGHT),
|
||||
};
|
||||
let btn = full_button_rect(x, y, w);
|
||||
if filled {
|
||||
cx.backend.fill_round_rect(btn, INPUT_RADIUS, theme.primary);
|
||||
} else {
|
||||
|
|
@ -73,7 +86,59 @@ fn paint_full_button(
|
|||
btn.origin.x + (btn.size.x - tw) / 2.0,
|
||||
btn.origin.y + 19.0,
|
||||
);
|
||||
y + INPUT_HEIGHT + 12.0
|
||||
full_button_advance(y)
|
||||
}
|
||||
|
||||
/// Backend-free advance estimate for a chip label at 13px, matching the
|
||||
/// `RenderBackend::measure_text` trait-default heuristic (0.55 × size per
|
||||
/// ASCII char, 1.0 × size per non-ASCII). Both the painter and the
|
||||
/// hit-test walker measure chips through THIS helper — not
|
||||
/// `cx.backend.measure_text` — so the chip wrapping math is identical
|
||||
/// whether or not a real typeface is mounted. Framework wire tokens are
|
||||
/// all ASCII, so the heuristic is exact here.
|
||||
fn chip_label_width(label: &str) -> f32 {
|
||||
label.chars().fold(0.0, |w, c| {
|
||||
w + if c.is_ascii() { 13.0 * 0.55 } else { 13.0 }
|
||||
})
|
||||
}
|
||||
|
||||
/// Pure-geometry walker for the framework chip row — one `(Framework,
|
||||
/// Rect)` per `Framework::ALL`, with the SAME wrapping math the painter
|
||||
/// applies. `paint_framework_chips` draws these rects and
|
||||
/// `code_action_rects` hit-tests them, so paint + click can't drift.
|
||||
fn framework_chip_rects(x: f32, y: f32, w: f32) -> Vec<(Framework, Rect)> {
|
||||
let left = x + PAD_X;
|
||||
let right = x + w - PAD_X;
|
||||
let mut chip_x = left;
|
||||
let mut chip_y = y;
|
||||
let mut out = Vec::with_capacity(Framework::ALL.len());
|
||||
for fw in Framework::ALL {
|
||||
let chip_w = chip_label_width(fw.as_wire()) + CHIP_PAD_X * 2.0;
|
||||
// Wrap to a new row when this chip would overflow the usable
|
||||
// width (but never wrap the very first chip on a row).
|
||||
if chip_x > left && chip_x + chip_w > right {
|
||||
chip_x = left;
|
||||
chip_y += CHIP_HEIGHT + CHIP_GAP;
|
||||
}
|
||||
out.push((
|
||||
fw,
|
||||
Rect {
|
||||
origin: Point2D::new(chip_x, chip_y),
|
||||
size: Point2D::new(chip_w, CHIP_HEIGHT),
|
||||
},
|
||||
));
|
||||
chip_x += chip_w + CHIP_GAP;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The y after the chip row + its trailing gap — the start of the
|
||||
/// phase-specific body. Derived from the last chip's rect so it tracks
|
||||
/// the (multi-row) wrapping exactly.
|
||||
fn chips_body_top(x: f32, y: f32, w: f32) -> f32 {
|
||||
let rects = framework_chip_rects(x, y, w);
|
||||
let last_y = rects.last().map(|(_, r)| r.origin.y).unwrap_or(y);
|
||||
last_y + CHIP_HEIGHT + SECTION_GAP
|
||||
}
|
||||
|
||||
/// Paint the framework tab row: one small rounded-rect chip per
|
||||
|
|
@ -88,25 +153,7 @@ fn paint_framework_chips(
|
|||
y: f32,
|
||||
w: f32,
|
||||
) -> f32 {
|
||||
use op_editor_core::codegen::Framework;
|
||||
let left = x + PAD_X;
|
||||
let right = x + w - PAD_X;
|
||||
let mut chip_x = left;
|
||||
let mut chip_y = y;
|
||||
for fw in Framework::ALL {
|
||||
let label = fw.as_wire();
|
||||
let tw = cx.backend.measure_text(label, 13.0);
|
||||
let chip_w = tw + CHIP_PAD_X * 2.0;
|
||||
// Wrap to a new row when this chip would overflow the usable
|
||||
// width (but never wrap the very first chip on a row).
|
||||
if chip_x > left && chip_x + chip_w > right {
|
||||
chip_x = left;
|
||||
chip_y += CHIP_HEIGHT + CHIP_GAP;
|
||||
}
|
||||
let chip = Rect {
|
||||
origin: Point2D::new(chip_x, chip_y),
|
||||
size: Point2D::new(chip_w, CHIP_HEIGHT),
|
||||
};
|
||||
for (fw, chip) in framework_chip_rects(x, y, w) {
|
||||
let active = state.framework == fw;
|
||||
let (fill, text_color) = if active {
|
||||
(theme.primary, theme.primary_foreground)
|
||||
|
|
@ -116,14 +163,13 @@ fn paint_framework_chips(
|
|||
cx.backend.fill_round_rect(chip, 6.0, fill);
|
||||
draw_line(
|
||||
cx,
|
||||
label,
|
||||
fw.as_wire(),
|
||||
text_color,
|
||||
chip.origin.x + CHIP_PAD_X,
|
||||
chip.origin.y + 17.0,
|
||||
);
|
||||
chip_x += chip_w + CHIP_GAP;
|
||||
}
|
||||
chip_y + CHIP_HEIGHT + SECTION_GAP
|
||||
chips_body_top(x, y, w)
|
||||
}
|
||||
|
||||
/// Map a `ChunkStatus` to its trailing status glyph + tint. Pending /
|
||||
|
|
@ -174,6 +220,16 @@ fn phase_glyph(theme: &Theme, done: Option<bool>) -> (Icon, Color) {
|
|||
}
|
||||
}
|
||||
|
||||
/// y of the Idle phase's first (Generate) button — past the hint row and
|
||||
/// the optional error row. Shared by the painter and `code_action_rects`.
|
||||
fn idle_generate_y(state: &CodegenState, y: f32) -> f32 {
|
||||
let mut y = y + 28.0 + SECTION_GAP;
|
||||
if state.error.is_some() {
|
||||
y += PROGRESS_ROW_H;
|
||||
}
|
||||
y
|
||||
}
|
||||
|
||||
/// Idle body: a hint + a primary "Generate <fw>" button + a secondary
|
||||
/// "Export AI Bundle" button. Any error is surfaced above the buttons.
|
||||
fn paint_idle_body(
|
||||
|
|
@ -181,7 +237,7 @@ fn paint_idle_body(
|
|||
theme: &Theme,
|
||||
state: &CodegenState,
|
||||
x: f32,
|
||||
mut y: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
) -> f32 {
|
||||
draw_icon(
|
||||
|
|
@ -199,10 +255,17 @@ fn paint_idle_body(
|
|||
x + PAD_X + 28.0,
|
||||
y + 15.0,
|
||||
);
|
||||
y += 28.0 + SECTION_GAP;
|
||||
let mut y = idle_generate_y(state, y);
|
||||
if let Some(err) = state.error.as_ref() {
|
||||
draw_line(cx, err, theme.destructive, x + PAD_X, y + 14.0);
|
||||
y += PROGRESS_ROW_H;
|
||||
// The error row sits one PROGRESS_ROW_H above the button, which
|
||||
// `idle_generate_y` already accounted for — draw it there.
|
||||
draw_line(
|
||||
cx,
|
||||
err,
|
||||
theme.destructive,
|
||||
x + PAD_X,
|
||||
y - PROGRESS_ROW_H + 14.0,
|
||||
);
|
||||
}
|
||||
let generate = format!("Generate {}", state.framework.as_wire());
|
||||
y = paint_full_button(cx, theme, &generate, x, y, w, true);
|
||||
|
|
@ -210,6 +273,14 @@ fn paint_idle_body(
|
|||
y
|
||||
}
|
||||
|
||||
/// y of the Generating phase's Cancel button — past the header,
|
||||
/// planning, per-chunk, and assembly rows + the trailing section gap.
|
||||
/// Shared by the painter and `code_action_rects`.
|
||||
fn generating_cancel_y(state: &CodegenState, y: f32) -> f32 {
|
||||
// header + planning + chunks + assembly rows, then SECTION_GAP.
|
||||
y + PROGRESS_ROW_H * (3 + state.progress.chunks.len()) as f32 + SECTION_GAP
|
||||
}
|
||||
|
||||
/// Generating body: a header, a planning row, one row per chunk, an
|
||||
/// assembly row, and a full-width Cancel button.
|
||||
fn paint_generating_body(
|
||||
|
|
@ -290,6 +361,35 @@ fn paint_action_chip(cx: &mut PaintCx<'_>, theme: &Theme, icon: Icon, label: &st
|
|||
);
|
||||
}
|
||||
|
||||
/// y at which the Complete phase's 4-chip action row sits — accounts
|
||||
/// for the optional degraded + assets notice rows and the code-preview
|
||||
/// area above it. Shared by the painter and `code_action_rects`.
|
||||
fn complete_action_row_y(state: &CodegenState, y: f32) -> f32 {
|
||||
let mut y = y;
|
||||
if state.degraded {
|
||||
y += PROGRESS_ROW_H;
|
||||
}
|
||||
if !state.assets.is_empty() {
|
||||
y += PROGRESS_ROW_H;
|
||||
}
|
||||
// The code-preview area advances by its height + the trailing gap;
|
||||
// mirror `paint_code_area`'s return without painting.
|
||||
y + CODE_AREA_H + 12.0
|
||||
}
|
||||
|
||||
/// Geometry for the Complete phase's 4 action chips (Copy / Save /
|
||||
/// Bundle / Regen), left-to-right across the usable width. Both the
|
||||
/// painter and `code_action_rects` consume these rects.
|
||||
fn action_chip_rects(x: f32, y: f32, w: f32) -> [Rect; 4] {
|
||||
let usable = w - PAD_X * 2.0;
|
||||
let gap = 8.0;
|
||||
let btn_w = (usable - gap * 3.0) / 4.0;
|
||||
std::array::from_fn(|i| Rect {
|
||||
origin: Point2D::new(x + PAD_X + (btn_w + gap) * i as f32, y),
|
||||
size: Point2D::new(btn_w, INPUT_HEIGHT),
|
||||
})
|
||||
}
|
||||
|
||||
/// Complete body: optional degraded warning + asset notice, the code
|
||||
/// preview area, and an action row (Copy / Download / AI Bundle /
|
||||
/// Regenerate).
|
||||
|
|
@ -326,25 +426,25 @@ fn paint_complete_body(
|
|||
}
|
||||
y = paint_code_area(cx, theme, &state.code, x, y, w);
|
||||
// Action row: 4 small buttons spread across the usable width.
|
||||
let usable = w - PAD_X * 2.0;
|
||||
let gap = 8.0;
|
||||
let btn_w = (usable - gap * 3.0) / 4.0;
|
||||
let actions = [
|
||||
(Icon::Copy, "Copy"),
|
||||
(Icon::Download, "Save"),
|
||||
(Icon::Sparkles, "Bundle"),
|
||||
(Icon::RefreshCw, "Regen"),
|
||||
];
|
||||
for (i, (icon, label)) in actions.iter().enumerate() {
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(x + PAD_X + (btn_w + gap) * i as f32, y),
|
||||
size: Point2D::new(btn_w, INPUT_HEIGHT),
|
||||
};
|
||||
let rects = action_chip_rects(x, y, w);
|
||||
for ((icon, label), rect) in actions.iter().zip(rects) {
|
||||
paint_action_chip(cx, theme, *icon, label, rect);
|
||||
}
|
||||
y + INPUT_HEIGHT + 12.0
|
||||
}
|
||||
|
||||
/// y of the Error phase's Regenerate button — past the error-text row.
|
||||
/// Shared by the painter and `code_action_rects`.
|
||||
fn error_regenerate_y(y: f32) -> f32 {
|
||||
y + PROGRESS_ROW_H + SECTION_GAP
|
||||
}
|
||||
|
||||
/// Error body: the error text in the destructive color + a full-width
|
||||
/// Regenerate button.
|
||||
fn paint_error_body(
|
||||
|
|
@ -352,13 +452,12 @@ fn paint_error_body(
|
|||
theme: &Theme,
|
||||
state: &CodegenState,
|
||||
x: f32,
|
||||
mut y: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
) -> f32 {
|
||||
let msg = state.error.as_deref().unwrap_or("Generation failed");
|
||||
draw_line(cx, msg, theme.destructive, x + PAD_X, y + 16.0);
|
||||
y += PROGRESS_ROW_H + SECTION_GAP;
|
||||
paint_full_button(cx, theme, "Regenerate", x, y, w, true)
|
||||
paint_full_button(cx, theme, "Regenerate", x, error_regenerate_y(y), w, true)
|
||||
}
|
||||
|
||||
/// Paint the full Code panel from `state`. Layout: a framework chip
|
||||
|
|
@ -384,6 +483,57 @@ pub fn paint_code_panel(
|
|||
}
|
||||
}
|
||||
|
||||
/// Hit-test geometry for the Code panel — the clickable rects the panel
|
||||
/// draws, in draw order. Takes the SAME `(x, y, w)` content origin the
|
||||
/// host passes to `paint_code_panel` (panel left, tab-strip bottom,
|
||||
/// panel width). Reuses the panel's shared geometry helpers
|
||||
/// (`framework_chip_rects` / `full_button_rect` / `action_chip_rects`
|
||||
/// and the per-phase `*_y` offsets) so a click always lands on what is
|
||||
/// drawn — paint and hit-test cannot drift.
|
||||
pub fn code_action_rects(
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
state: &CodegenState,
|
||||
) -> Vec<(CodegenAction, Rect)> {
|
||||
let mut out: Vec<(CodegenAction, Rect)> = Vec::new();
|
||||
// Section label sits first, then the framework chip row.
|
||||
let chips_y = y + SECTION_HEADER_HEIGHT;
|
||||
for (fw, rect) in framework_chip_rects(x, chips_y, w) {
|
||||
out.push((CodegenAction::SelectFramework(fw), rect));
|
||||
}
|
||||
// Phase-specific body starts after the chip row + its gap.
|
||||
let body_y = chips_body_top(x, chips_y, w);
|
||||
match state.phase {
|
||||
CodegenPhase::Idle => {
|
||||
let gen_y = idle_generate_y(state, body_y);
|
||||
out.push((CodegenAction::Generate, full_button_rect(x, gen_y, w)));
|
||||
let bundle_y = full_button_advance(gen_y);
|
||||
out.push((
|
||||
CodegenAction::ExportBundle,
|
||||
full_button_rect(x, bundle_y, w),
|
||||
));
|
||||
}
|
||||
CodegenPhase::Generating => {
|
||||
let cancel_y = generating_cancel_y(state, body_y);
|
||||
out.push((CodegenAction::Cancel, full_button_rect(x, cancel_y, w)));
|
||||
}
|
||||
CodegenPhase::Complete => {
|
||||
let row_y = complete_action_row_y(state, body_y);
|
||||
let [copy, save, bundle, regen] = action_chip_rects(x, row_y, w);
|
||||
out.push((CodegenAction::Copy, copy));
|
||||
out.push((CodegenAction::Download, save));
|
||||
out.push((CodegenAction::ExportBundle, bundle));
|
||||
out.push((CodegenAction::Regenerate, regen));
|
||||
}
|
||||
CodegenPhase::Error => {
|
||||
let regen_y = error_regenerate_y(body_y);
|
||||
out.push((CodegenAction::Regenerate, full_button_rect(x, regen_y, w)));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -475,4 +625,124 @@ mod tests {
|
|||
};
|
||||
assert!(paint(&error) > 0.0);
|
||||
}
|
||||
|
||||
fn contains(r: Rect, p: Point2D) -> bool {
|
||||
p.x >= r.origin.x
|
||||
&& p.x <= r.origin.x + r.size.x
|
||||
&& p.y >= r.origin.y
|
||||
&& p.y <= r.origin.y + r.size.y
|
||||
}
|
||||
|
||||
fn center(r: Rect) -> Point2D {
|
||||
Point2D::new(r.origin.x + r.size.x / 2.0, r.origin.y + r.size.y / 2.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_action_rects_idle_has_generate_and_bundle() {
|
||||
let mut s = op_editor_core::codegen::CodegenState::default(); // Idle, React
|
||||
let rects = code_action_rects(0.0, 0.0, 280.0, &s);
|
||||
assert!(rects
|
||||
.iter()
|
||||
.any(|(a, _)| matches!(a, CodegenAction::Generate)));
|
||||
assert!(rects
|
||||
.iter()
|
||||
.any(|(a, _)| matches!(a, CodegenAction::ExportBundle)));
|
||||
// all 8 framework chips present
|
||||
let chips = rects
|
||||
.iter()
|
||||
.filter(|(a, _)| matches!(a, CodegenAction::SelectFramework(_)))
|
||||
.count();
|
||||
assert_eq!(chips, 8);
|
||||
let _ = &mut s;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_action_rects_complete_has_copy_and_regen() {
|
||||
// Struct-update form (not `default()` + field reassignment) so
|
||||
// the pre-commit clippy `field_reassign_with_default` gate stays
|
||||
// clean; assertions match the spec's Complete-phase test.
|
||||
let s = CodegenState {
|
||||
phase: CodegenPhase::Complete,
|
||||
code: "x".into(),
|
||||
..CodegenState::default()
|
||||
};
|
||||
let rects = code_action_rects(0.0, 0.0, 280.0, &s);
|
||||
assert!(rects.iter().any(|(a, _)| matches!(a, CodegenAction::Copy)));
|
||||
assert!(rects
|
||||
.iter()
|
||||
.any(|(a, _)| matches!(a, CodegenAction::Regenerate)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_action_rects_generating_has_cancel_and_chips() {
|
||||
let s = CodegenState {
|
||||
phase: CodegenPhase::Generating,
|
||||
..CodegenState::default()
|
||||
};
|
||||
let rects = code_action_rects(0.0, 0.0, 280.0, &s);
|
||||
assert!(rects
|
||||
.iter()
|
||||
.any(|(a, _)| matches!(a, CodegenAction::Cancel)));
|
||||
assert_eq!(
|
||||
rects
|
||||
.iter()
|
||||
.filter(|(a, _)| matches!(a, CodegenAction::SelectFramework(_)))
|
||||
.count(),
|
||||
8
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_action_rects_error_has_regenerate_only_button() {
|
||||
let s = CodegenState {
|
||||
phase: CodegenPhase::Error,
|
||||
error: Some("boom".into()),
|
||||
..CodegenState::default()
|
||||
};
|
||||
let rects = code_action_rects(0.0, 0.0, 280.0, &s);
|
||||
let buttons: Vec<_> = rects
|
||||
.iter()
|
||||
.filter(|(a, _)| !matches!(a, CodegenAction::SelectFramework(_)))
|
||||
.collect();
|
||||
assert_eq!(buttons.len(), 1);
|
||||
assert!(matches!(buttons[0].0, CodegenAction::Regenerate));
|
||||
}
|
||||
|
||||
/// The geometry round-trips: a click at a button's centre hits that
|
||||
/// button's rect and no other action's rect (the layout the host's
|
||||
/// `hit_test_action` walks).
|
||||
#[test]
|
||||
fn code_action_rects_generate_center_round_trips() {
|
||||
let s = CodegenState::default(); // Idle.
|
||||
let rects = code_action_rects(0.0, 0.0, 280.0, &s);
|
||||
let (_, gen_rect) = rects
|
||||
.iter()
|
||||
.find(|(a, _)| matches!(a, CodegenAction::Generate))
|
||||
.expect("Generate rect present");
|
||||
let p = center(*gen_rect);
|
||||
// Exactly one action contains the Generate button's centre, and
|
||||
// it is Generate (full-width button + chips don't overlap it).
|
||||
let hits: Vec<_> = rects
|
||||
.iter()
|
||||
.filter(|(_, r)| contains(*r, p))
|
||||
.map(|(a, _)| *a)
|
||||
.collect();
|
||||
assert_eq!(hits, vec![CodegenAction::Generate]);
|
||||
}
|
||||
|
||||
/// The framework chips wrap onto multiple rows at the real panel
|
||||
/// width (280 px), and every chip carries a positive-size rect.
|
||||
#[test]
|
||||
fn framework_chips_wrap_and_have_size() {
|
||||
let chips = framework_chip_rects(0.0, 0.0, 280.0);
|
||||
assert_eq!(chips.len(), 8);
|
||||
for (_, r) in &chips {
|
||||
assert!(r.size.x > 0.0 && r.size.y > 0.0);
|
||||
}
|
||||
// At 280px the 8 wire tokens don't fit on one row → at least
|
||||
// two distinct chip rows.
|
||||
let rows: std::collections::BTreeSet<i32> =
|
||||
chips.iter().map(|(_, r)| r.origin.y as i32).collect();
|
||||
assert!(rows.len() >= 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue