refactor(editor): split layer-panel hit-test into a sibling file
The pages-row-height scroll fix pushed layer_panel.rs past the 800-line ceiling. Move hit_test + drop_target_at into layer_panel_hit.rs via an impl block; no behavior change.
This commit is contained in:
parent
ec45fd99c1
commit
eb853f6ccf
|
|
@ -12,7 +12,7 @@ use crate::widgets::editor_state_ext::theme_for;
|
|||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::layer_panel_walkers::{
|
||||
apply_layer_rename, icon_for_node, kind_label, layer_regions, layers_content_width,
|
||||
pages_content_width, pages_from_state, row_index_at, visible_row_range, walk, walk_excluding,
|
||||
pages_content_width, pages_from_state, visible_row_range, walk, walk_excluding,
|
||||
LayerRegionInput, LayerRegions, LayerScrollSnapshot, RenameView, WalkCx,
|
||||
};
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
|
|
@ -241,191 +241,6 @@ impl LayerPanel {
|
|||
layers: self.layers_scroll,
|
||||
})
|
||||
}
|
||||
|
||||
/// Drop target for a drag-in-progress. Over a row: top-25 %
|
||||
/// Before, middle 50 % Into (container rows only), bottom 25 %
|
||||
/// After. Below-rows area: After-last. Outside / above rows:
|
||||
/// None.
|
||||
pub fn drop_target_at(&self, rect: Rect, point: Point2D) -> Option<DropTarget> {
|
||||
if !(rect).contains(point) {
|
||||
return None;
|
||||
}
|
||||
let r = self.regions(rect);
|
||||
// A drop is only valid when the cursor is inside the visible
|
||||
// (clipped) Layers viewport — a row scrolled out of view, or
|
||||
// the cursor over the Pages section / headers, is no drop
|
||||
// target. Mirrors the paint clip so the drop-indicator the
|
||||
// user sees and where the node actually lands always agree.
|
||||
if point.y < r.layers_rows_top || point.y > r.layers_rows_top + r.layers_view_h {
|
||||
return None;
|
||||
}
|
||||
let layers_top = r.layers_rows_top - r.layers.offset;
|
||||
if let Some((index, row_top)) = row_index_at(
|
||||
self.items.len(),
|
||||
r.layers_rows_top,
|
||||
r.layers.offset,
|
||||
r.layers_view_h,
|
||||
LAYER_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let item = &self.items[index];
|
||||
let row_bottom = row_top + LAYER_ROW_HEIGHT;
|
||||
// Container rows use Before / Into / After bands; leaves
|
||||
// fall back to a two-way Before / After split.
|
||||
let local = point.y - row_top;
|
||||
let position = if item.is_container {
|
||||
if local < LAYER_ROW_HEIGHT * 0.25 {
|
||||
DropPosition::Before
|
||||
} else if local > LAYER_ROW_HEIGHT * 0.75 {
|
||||
DropPosition::After
|
||||
} else {
|
||||
DropPosition::Into
|
||||
}
|
||||
} else if local < LAYER_ROW_HEIGHT / 2.0 {
|
||||
DropPosition::Before
|
||||
} else {
|
||||
DropPosition::After
|
||||
};
|
||||
let indicator_y = match position {
|
||||
DropPosition::Before => row_top,
|
||||
DropPosition::After => row_bottom,
|
||||
DropPosition::Into => row_top,
|
||||
};
|
||||
return Some(DropTarget {
|
||||
anchor: item.node_id.clone(),
|
||||
position,
|
||||
indicator_y,
|
||||
});
|
||||
}
|
||||
// Cursor is below the last row but still inside the panel —
|
||||
// drop at end (anchor = last layer, position = After). `y`
|
||||
// already points at the bottom of the final row, which is
|
||||
// exactly where the indicator paints.
|
||||
if point.y > layers_top {
|
||||
if let Some(last) = self.items.last() {
|
||||
let y = layers_top + self.items.len() as f32 * LAYER_ROW_HEIGHT;
|
||||
return Some(DropTarget {
|
||||
anchor: last.node_id.clone(),
|
||||
position: DropPosition::After,
|
||||
indicator_y: y,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Hit test a (rect, point) — returns `Page(idx)` for a page
|
||||
/// row, `Layer(node_id)` for a layer row, eye/lock/chevron
|
||||
/// toggles for the trailing icons, or `AddPage` for the `+`
|
||||
/// on the Pages section header.
|
||||
pub fn hit_test(&self, rect: Rect, point: Point2D) -> Option<LayerPanelHit> {
|
||||
if !(rect).contains(point) {
|
||||
return None;
|
||||
}
|
||||
// Pages section header — `+` add-page affordance at top-right.
|
||||
let plus_x = rect.origin.x + rect.size.x - ROW_PAD_X - 12.0;
|
||||
let plus_y = rect.origin.y + 8.0 + (SECTION_HEADER_HEIGHT - 14.0) / 2.0;
|
||||
let slop = 4.0;
|
||||
if point.x >= plus_x - slop
|
||||
&& point.x <= plus_x + 14.0 + slop
|
||||
&& point.y >= plus_y - slop
|
||||
&& point.y <= plus_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::AddPage);
|
||||
}
|
||||
// Bounded Pages / Layers viewports — a row only counts as a
|
||||
// hit when the cursor is inside its (clipped) viewport, so a
|
||||
// row scrolled out of view can't be clicked through.
|
||||
let r = self.regions(rect);
|
||||
if let Some((index, y)) = row_index_at(
|
||||
self.pages.len(),
|
||||
r.pages_rows_top,
|
||||
r.pages.offset,
|
||||
r.pages_view_h,
|
||||
PAGE_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let page = &self.pages[index];
|
||||
let row = Rect {
|
||||
origin: Point2D::new(rect.origin.x, y),
|
||||
size: Point2D::new(rect.size.x, PAGE_ROW_HEIGHT),
|
||||
};
|
||||
if (row).contains(point) && page.hovered {
|
||||
// × delete button — only hit-tested when the row is
|
||||
// hovered (paint shows it under the same gate).
|
||||
let close_x = rect.origin.x + rect.size.x - ROW_PAD_X - 14.0;
|
||||
let close_y = y + (PAGE_ROW_HEIGHT - 14.0) / 2.0;
|
||||
let slop = 4.0;
|
||||
if point.x >= close_x - slop
|
||||
&& point.x <= close_x + 14.0 + slop
|
||||
&& point.y >= close_y - slop
|
||||
&& point.y <= close_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::DeletePage(page.page_index));
|
||||
}
|
||||
}
|
||||
if (row).contains(point) {
|
||||
return Some(LayerPanelHit::Page(page.page_index));
|
||||
}
|
||||
}
|
||||
if let Some((index, y)) = row_index_at(
|
||||
self.items.len(),
|
||||
r.layers_rows_top,
|
||||
r.layers.offset,
|
||||
r.layers_view_h,
|
||||
LAYER_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let item = &self.items[index];
|
||||
let row = Rect {
|
||||
origin: Point2D::new(rect.origin.x, y),
|
||||
size: Point2D::new(rect.size.x, LAYER_ROW_HEIGHT),
|
||||
};
|
||||
if !(row).contains(point) {
|
||||
return None;
|
||||
}
|
||||
// Match the paint geometry — same 14 px icon boxes with
|
||||
// 4 px slop so small mouse offsets still register.
|
||||
let inner = Rect {
|
||||
origin: Point2D::new(row.origin.x + 6.0, y + 2.0),
|
||||
size: Point2D::new(row.size.x - 12.0, LAYER_ROW_HEIGHT - 4.0),
|
||||
};
|
||||
let trailing_right = inner.origin.x + inner.size.x - 8.0;
|
||||
let lock_x = trailing_right - 14.0;
|
||||
let eye_x = lock_x - 22.0;
|
||||
let icon_y = inner.origin.y + 6.0;
|
||||
let slop = 4.0;
|
||||
if item.hovered
|
||||
&& point.x >= lock_x - slop
|
||||
&& point.x <= lock_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleLocked(item.node_id.clone()));
|
||||
}
|
||||
if item.hovered
|
||||
&& point.x >= eye_x - slop
|
||||
&& point.x <= eye_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleHidden(item.node_id.clone()));
|
||||
}
|
||||
if item.has_children {
|
||||
let indent = ROW_PAD_X + item.depth as f32 * 12.0;
|
||||
let chev_x = inner.origin.x + indent - r.layers.horizontal_offset;
|
||||
if point.x >= chev_x - slop
|
||||
&& point.x <= chev_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleCollapsed(item.node_id.clone()));
|
||||
}
|
||||
}
|
||||
return Some(LayerPanelHit::Layer(item.node_id.clone()));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
|
|||
195
crates/op-editor-ui/src/widgets/layer_panel_hit.rs
Normal file
195
crates/op-editor-ui/src/widgets/layer_panel_hit.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//! `LayerPanel` hit-test + drop-target resolution — split out of
|
||||
//! `layer_panel.rs` to honor the 800-line file ceiling. These methods
|
||||
//! mirror the paint geometry (same clipped viewports + row heights) so
|
||||
//! clicks and drop indicators land on the rows the user sees.
|
||||
|
||||
use super::layer_panel::*;
|
||||
use super::layer_panel_walkers::row_index_at;
|
||||
use crate::{Point2D, Rect};
|
||||
|
||||
impl LayerPanel {
|
||||
/// Drop target for a drag-in-progress. Over a row: top-25 %
|
||||
/// Before, middle 50 % Into (container rows only), bottom 25 %
|
||||
/// After. Below-rows area: After-last. Outside / above rows:
|
||||
/// None.
|
||||
pub fn drop_target_at(&self, rect: Rect, point: Point2D) -> Option<DropTarget> {
|
||||
if !(rect).contains(point) {
|
||||
return None;
|
||||
}
|
||||
let r = self.regions(rect);
|
||||
// A drop is only valid when the cursor is inside the visible
|
||||
// (clipped) Layers viewport — a row scrolled out of view, or
|
||||
// the cursor over the Pages section / headers, is no drop
|
||||
// target. Mirrors the paint clip so the drop-indicator the
|
||||
// user sees and where the node actually lands always agree.
|
||||
if point.y < r.layers_rows_top || point.y > r.layers_rows_top + r.layers_view_h {
|
||||
return None;
|
||||
}
|
||||
let layers_top = r.layers_rows_top - r.layers.offset;
|
||||
if let Some((index, row_top)) = row_index_at(
|
||||
self.items.len(),
|
||||
r.layers_rows_top,
|
||||
r.layers.offset,
|
||||
r.layers_view_h,
|
||||
LAYER_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let item = &self.items[index];
|
||||
let row_bottom = row_top + LAYER_ROW_HEIGHT;
|
||||
// Container rows use Before / Into / After bands; leaves
|
||||
// fall back to a two-way Before / After split.
|
||||
let local = point.y - row_top;
|
||||
let position = if item.is_container {
|
||||
if local < LAYER_ROW_HEIGHT * 0.25 {
|
||||
DropPosition::Before
|
||||
} else if local > LAYER_ROW_HEIGHT * 0.75 {
|
||||
DropPosition::After
|
||||
} else {
|
||||
DropPosition::Into
|
||||
}
|
||||
} else if local < LAYER_ROW_HEIGHT / 2.0 {
|
||||
DropPosition::Before
|
||||
} else {
|
||||
DropPosition::After
|
||||
};
|
||||
let indicator_y = match position {
|
||||
DropPosition::Before => row_top,
|
||||
DropPosition::After => row_bottom,
|
||||
DropPosition::Into => row_top,
|
||||
};
|
||||
return Some(DropTarget {
|
||||
anchor: item.node_id.clone(),
|
||||
position,
|
||||
indicator_y,
|
||||
});
|
||||
}
|
||||
// Cursor is below the last row but still inside the panel —
|
||||
// drop at end (anchor = last layer, position = After). `y`
|
||||
// already points at the bottom of the final row, which is
|
||||
// exactly where the indicator paints.
|
||||
if point.y > layers_top {
|
||||
if let Some(last) = self.items.last() {
|
||||
let y = layers_top + self.items.len() as f32 * LAYER_ROW_HEIGHT;
|
||||
return Some(DropTarget {
|
||||
anchor: last.node_id.clone(),
|
||||
position: DropPosition::After,
|
||||
indicator_y: y,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Hit test a (rect, point) — returns `Page(idx)` for a page
|
||||
/// row, `Layer(node_id)` for a layer row, eye/lock/chevron
|
||||
/// toggles for the trailing icons, or `AddPage` for the `+`
|
||||
/// on the Pages section header.
|
||||
pub fn hit_test(&self, rect: Rect, point: Point2D) -> Option<LayerPanelHit> {
|
||||
if !(rect).contains(point) {
|
||||
return None;
|
||||
}
|
||||
// Pages section header — `+` add-page affordance at top-right.
|
||||
let plus_x = rect.origin.x + rect.size.x - ROW_PAD_X - 12.0;
|
||||
let plus_y = rect.origin.y + 8.0 + (SECTION_HEADER_HEIGHT - 14.0) / 2.0;
|
||||
let slop = 4.0;
|
||||
if point.x >= plus_x - slop
|
||||
&& point.x <= plus_x + 14.0 + slop
|
||||
&& point.y >= plus_y - slop
|
||||
&& point.y <= plus_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::AddPage);
|
||||
}
|
||||
// Bounded Pages / Layers viewports — a row only counts as a
|
||||
// hit when the cursor is inside its (clipped) viewport, so a
|
||||
// row scrolled out of view can't be clicked through.
|
||||
let r = self.regions(rect);
|
||||
if let Some((index, y)) = row_index_at(
|
||||
self.pages.len(),
|
||||
r.pages_rows_top,
|
||||
r.pages.offset,
|
||||
r.pages_view_h,
|
||||
PAGE_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let page = &self.pages[index];
|
||||
let row = Rect {
|
||||
origin: Point2D::new(rect.origin.x, y),
|
||||
size: Point2D::new(rect.size.x, PAGE_ROW_HEIGHT),
|
||||
};
|
||||
if (row).contains(point) && page.hovered {
|
||||
// × delete button — only hit-tested when the row is
|
||||
// hovered (paint shows it under the same gate).
|
||||
let close_x = rect.origin.x + rect.size.x - ROW_PAD_X - 14.0;
|
||||
let close_y = y + (PAGE_ROW_HEIGHT - 14.0) / 2.0;
|
||||
let slop = 4.0;
|
||||
if point.x >= close_x - slop
|
||||
&& point.x <= close_x + 14.0 + slop
|
||||
&& point.y >= close_y - slop
|
||||
&& point.y <= close_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::DeletePage(page.page_index));
|
||||
}
|
||||
}
|
||||
if (row).contains(point) {
|
||||
return Some(LayerPanelHit::Page(page.page_index));
|
||||
}
|
||||
}
|
||||
if let Some((index, y)) = row_index_at(
|
||||
self.items.len(),
|
||||
r.layers_rows_top,
|
||||
r.layers.offset,
|
||||
r.layers_view_h,
|
||||
LAYER_ROW_HEIGHT,
|
||||
point.y,
|
||||
) {
|
||||
let item = &self.items[index];
|
||||
let row = Rect {
|
||||
origin: Point2D::new(rect.origin.x, y),
|
||||
size: Point2D::new(rect.size.x, LAYER_ROW_HEIGHT),
|
||||
};
|
||||
if !(row).contains(point) {
|
||||
return None;
|
||||
}
|
||||
// Match the paint geometry — same 14 px icon boxes with
|
||||
// 4 px slop so small mouse offsets still register.
|
||||
let inner = Rect {
|
||||
origin: Point2D::new(row.origin.x + 6.0, y + 2.0),
|
||||
size: Point2D::new(row.size.x - 12.0, LAYER_ROW_HEIGHT - 4.0),
|
||||
};
|
||||
let trailing_right = inner.origin.x + inner.size.x - 8.0;
|
||||
let lock_x = trailing_right - 14.0;
|
||||
let eye_x = lock_x - 22.0;
|
||||
let icon_y = inner.origin.y + 6.0;
|
||||
let slop = 4.0;
|
||||
if item.hovered
|
||||
&& point.x >= lock_x - slop
|
||||
&& point.x <= lock_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleLocked(item.node_id.clone()));
|
||||
}
|
||||
if item.hovered
|
||||
&& point.x >= eye_x - slop
|
||||
&& point.x <= eye_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleHidden(item.node_id.clone()));
|
||||
}
|
||||
if item.has_children {
|
||||
let indent = ROW_PAD_X + item.depth as f32 * 12.0;
|
||||
let chev_x = inner.origin.x + indent - r.layers.horizontal_offset;
|
||||
if point.x >= chev_x - slop
|
||||
&& point.x <= chev_x + 14.0 + slop
|
||||
&& point.y >= icon_y - slop
|
||||
&& point.y <= icon_y + 14.0 + slop
|
||||
{
|
||||
return Some(LayerPanelHit::ToggleCollapsed(item.node_id.clone()));
|
||||
}
|
||||
}
|
||||
return Some(LayerPanelHit::Layer(item.node_id.clone()));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ pub mod tree;
|
|||
// `op_editor_core::EditorState`).
|
||||
pub mod layer_context_menu;
|
||||
pub mod layer_panel;
|
||||
mod layer_panel_hit;
|
||||
mod layer_panel_paint;
|
||||
#[cfg(test)]
|
||||
mod layer_panel_tests;
|
||||
|
|
|
|||
Loading…
Reference in a new issue