refactor(editor): migrate scroll fields to shared scroll state

This commit is contained in:
Kayshen-X 2026-06-14 01:13:03 +08:00
parent 94f8be6844
commit e4125fd75d
33 changed files with 292 additions and 194 deletions

View file

@ -410,14 +410,14 @@ pub struct AgentSettings {
pub builtin_agents: Vec<BuiltinAgentConfig>,
pub builtin_agent_draft: Option<BuiltinAgentConfig>,
pub builtin_preset_menu_open: Option<BuiltinAgentPresetMenuTarget>,
pub builtin_preset_menu_scroll: f32,
pub builtin_preset_menu_scroll: jian_core::scroll::ScrollState,
pub builtin_preset_menu_hover: Option<BuiltinAgentPresetKey>,
pub next_builtin_agent_id: u64,
pub acp_agents: Vec<AcpAgentConfig>,
pub acp_agent_draft: Option<AcpAgentConfig>,
pub next_acp_agent_id: u64,
/// Vertical scroll offset of the right content pane in px.
pub scroll_y: f32,
pub scroll_y: jian_core::scroll::ScrollState,
pub mcp_server: McpServer,
pub mcp_cli_enabled: [bool; 6],
pub mcp_client_config_copied_at_ms: Option<u64>,
@ -468,13 +468,13 @@ impl Default for AgentSettings {
builtin_agents: Vec::new(),
builtin_agent_draft: None,
builtin_preset_menu_open: None,
builtin_preset_menu_scroll: 0.0,
builtin_preset_menu_scroll: Default::default(),
builtin_preset_menu_hover: None,
next_builtin_agent_id: 1,
acp_agents: Vec::new(),
acp_agent_draft: None,
next_acp_agent_id: 1,
scroll_y: 0.0,
scroll_y: Default::default(),
mcp_server: McpServer::default(),
mcp_cli_enabled: [false; 6],
mcp_client_config_copied_at_ms: None,
@ -586,7 +586,7 @@ impl AgentSettings {
pub fn cancel_builtin_agent_draft(&mut self) {
self.builtin_agent_draft = None;
self.builtin_preset_menu_open = None;
self.builtin_preset_menu_scroll = 0.0;
self.builtin_preset_menu_scroll.offset = 0.0;
self.builtin_preset_menu_hover = None;
}
@ -777,3 +777,19 @@ impl AgentSettings {
pub enum AgentSettingsDrag {
Reserved,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_settings_scroll_fields_use_scroll_state() {
let mut s = AgentSettings::default();
s.builtin_preset_menu_scroll.offset = 18.0;
s.scroll_y.offset = 42.0;
assert_eq!(s.builtin_preset_menu_scroll.offset, 18.0);
assert_eq!(s.scroll_y.offset, 42.0);
}
}

View file

@ -344,7 +344,7 @@ pub struct ChatState {
/// pinned above the input, mirroring the TS checklist header.
pub checklist_collapsed: bool,
/// Vertical scroll offset inside the fixed design checklist rows.
pub checklist_scroll: f32,
pub checklist_scroll: jian_core::scroll::ScrollState,
/// Set by `begin_send` to the just-sent user text; the desktop
/// event loop drains this each frame. `None` = idle.
pub pending_send: Option<String>,
@ -418,7 +418,7 @@ impl Default for ChatState {
collapsed: false,
maximized: false,
checklist_collapsed: false,
checklist_scroll: 0.0,
checklist_scroll: Default::default(),
pending_send: None,
pending_new_chat: false,
pending_stop_chat: false,
@ -532,7 +532,7 @@ impl ChatState {
// Empty streaming assistant bubble — provider deltas append here.
self.messages.push(ChatMessage::assistant_streaming());
self.input.set_text("");
self.checklist_scroll = 0.0;
self.checklist_scroll.offset = 0.0;
self.pending_send = Some(trimmed);
true
}
@ -578,7 +578,7 @@ impl ChatState {
self.pending_stop_chat = false;
self.pending_copy_text = None;
self.transcript_selection = None;
self.checklist_scroll = 0.0;
self.checklist_scroll.offset = 0.0;
self.pending_attachments.clear();
self.pending_attachment_pick = false;
self.pending_new_chat = true;
@ -739,7 +739,7 @@ impl ChatState {
pub fn toggle_checklist_collapsed(&mut self) {
self.checklist_collapsed = !self.checklist_collapsed;
if self.checklist_collapsed {
self.checklist_scroll = 0.0;
self.checklist_scroll.offset = 0.0;
}
}
@ -1182,6 +1182,15 @@ mod tests {
assert!(!chat.checklist_collapsed);
}
#[test]
fn checklist_scroll_uses_scroll_state() {
let mut chat = ChatState::default();
chat.checklist_scroll.offset = 20.0;
assert_eq!(chat.checklist_scroll.offset, 20.0);
}
#[test]
fn nearest_anchor_picks_corner() {
let p = crate::render_backend::Point2D::new(10.0, 10.0);

View file

@ -161,13 +161,13 @@ pub struct AssetMeta {
}
/// The Code panel's full state. Mirror of `ChatState`'s role for chat.
/// `PartialEq` only (not `Eq`) — `framework_scroll` carries an `f32`.
/// `PartialEq` only (not `Eq`) — scroll offsets carry `f32` values.
#[derive(Debug, Clone, PartialEq)]
pub struct CodegenState {
pub framework: Framework,
/// Horizontal scroll offset (px, ≥ 0) of the framework tab strip, so the
/// single-row selector scrolls to reach off-screen frameworks (TS parity).
pub framework_scroll: f32,
pub framework_scroll: jian_core::scroll::ScrollState,
/// The inactive framework chip the cursor is hovering, for a subtle
/// background highlight. `None` when the cursor is off the strip.
pub framework_hover: Option<Framework>,
@ -177,7 +177,7 @@ pub struct CodegenState {
pub progress: CodeGenProgress,
pub code: String,
/// Vertical scroll offset (px, >= 0) inside the generated-code preview.
pub code_scroll: f32,
pub code_scroll: jian_core::scroll::ScrollState,
/// Text selection inside the generated-code preview.
pub code_selection: Option<CodeSelection>,
pub degraded: bool,
@ -210,13 +210,13 @@ impl Default for CodegenState {
fn default() -> Self {
Self {
framework: Framework::React,
framework_scroll: 0.0,
framework_scroll: Default::default(),
framework_hover: None,
action_hover: None,
phase: CodegenPhase::Idle,
progress: CodeGenProgress::default(),
code: String::new(),
code_scroll: 0.0,
code_scroll: Default::default(),
code_selection: None,
degraded: false,
assets: Vec::new(),
@ -282,7 +282,7 @@ mod tests {
assert_eq!(s.framework, Framework::React);
assert_eq!(s.phase, CodegenPhase::Idle);
assert!(s.code.is_empty());
assert_eq!(s.code_scroll, 0.0);
assert_eq!(s.code_scroll.offset, 0.0);
assert!(!s.degraded);
assert!(s.error.is_none());
assert!(!s.pending_generate);
@ -292,6 +292,17 @@ mod tests {
assert!(!s.pending_cancel);
}
#[test]
fn codegen_scroll_fields_use_scroll_state() {
let mut s = CodegenState::default();
s.framework_scroll.offset = 16.0;
s.code_scroll.offset = 32.0;
assert_eq!(s.framework_scroll.offset, 16.0);
assert_eq!(s.code_scroll.offset, 32.0);
}
#[test]
fn selected_code_text_returns_non_collapsed_range() {
let s = CodegenState {

View file

@ -835,20 +835,20 @@ pub struct EditorUiState {
/// (≥ 0). A wheel / trackpad pan over the inspector advances it;
/// paint + hit-test shift the section content up by this amount
/// so a tall inspector (many effects, etc.) stays reachable.
pub property_panel_scroll: f32,
pub property_panel_scroll: jian_core::scroll::ScrollState,
/// Vertical scroll offset (px, ≥ 0) of the LayerPanel's 页面
/// (Pages) section — that section has a bounded height, so a
/// long page list scrolls within it.
pub layer_pages_scroll: f32,
pub layer_pages_scroll: jian_core::scroll::ScrollState,
/// Vertical scroll offset (px, ≥ 0) of the LayerPanel's 图层
/// (Layers) section row viewport.
pub layer_layers_scroll: f32,
pub layer_layers_scroll: jian_core::scroll::ScrollState,
/// Horizontal scroll offset (px, ≥ 0) of the LayerPanel's 页面
/// row content. The row chrome stays fixed; only tree content shifts.
pub layer_pages_h_scroll: f32,
pub layer_pages_h_scroll: jian_core::scroll::ScrollState,
/// Horizontal scroll offset (px, ≥ 0) of the LayerPanel's 图层
/// tree content. Needed for deeply nested layer trees.
pub layer_layers_h_scroll: f32,
pub layer_layers_h_scroll: jian_core::scroll::ScrollState,
/// "Import from Figma" modal.
pub figma_import_open: bool,
/// Which Figma-import target the cursor is over (close / drop-zone)
@ -1045,7 +1045,7 @@ pub struct EditorUiState {
/// Whether the variables-panel search box owns the keyboard.
pub variables_search_focus: bool,
/// Vertical scroll offset (px) of the variables row list.
pub variables_scroll: f32,
pub variables_scroll: jian_core::scroll::ScrollState,
/// Variable row whose `⋯` overflow menu (Rename / Delete) is open.
/// Indexes the UNFILTERED `doc.variables` order.
pub variables_row_menu: Option<usize>,
@ -1198,11 +1198,11 @@ impl Default for EditorUiState {
export_picker_hover: None,
property_action_hover: None,
property_tab_hover: None,
property_panel_scroll: 0.0,
layer_pages_scroll: 0.0,
layer_layers_scroll: 0.0,
layer_pages_h_scroll: 0.0,
layer_layers_h_scroll: 0.0,
property_panel_scroll: Default::default(),
layer_pages_scroll: Default::default(),
layer_layers_scroll: Default::default(),
layer_pages_h_scroll: Default::default(),
layer_layers_h_scroll: Default::default(),
figma_import_open: false,
figma_import_hover: None,
figma_import_in_progress: false,
@ -1274,7 +1274,7 @@ impl Default for EditorUiState {
variable_row_input: jian_core::text_input::TextInputState::default(),
variables_search: String::new(),
variables_search_focus: false,
variables_scroll: 0.0,
variables_scroll: Default::default(),
variables_row_menu: None,
variables_panel_size: None,
effect_param_focus: None,
@ -1498,10 +1498,10 @@ impl EditorUiState {
self.hovered_layer_id = None;
self.hovered_page_index = None;
self.layer_context_menu = None;
self.layer_pages_scroll = 0.0;
self.layer_layers_scroll = 0.0;
self.layer_pages_h_scroll = 0.0;
self.layer_layers_h_scroll = 0.0;
self.layer_pages_scroll.offset = 0.0;
self.layer_layers_scroll.offset = 0.0;
self.layer_pages_h_scroll.offset = 0.0;
self.layer_layers_h_scroll.offset = 0.0;
self.collapsed_layers.clear();
self.last_layer_click = None;
self.last_canvas_click = None;
@ -1523,7 +1523,7 @@ impl EditorUiState {
// metric and is preserved.
self.variables_search.clear();
self.variables_search_focus = false;
self.variables_scroll = 0.0;
self.variables_scroll.offset = 0.0;
self.variables_row_menu = None;
self.effect_param_focus = None;
// Document-derived: set true only by a Figma import to keep that
@ -1555,6 +1555,25 @@ mod tests {
assert!(c.collapsed_layers.is_empty());
}
#[test]
fn editor_pixel_scroll_fields_use_scroll_state() {
let mut s = EditorUiState::default();
s.property_panel_scroll.offset = 12.0;
s.layer_pages_scroll.offset = 24.0;
s.layer_layers_scroll.offset = 36.0;
s.layer_pages_h_scroll.offset = 48.0;
s.layer_layers_h_scroll.offset = 60.0;
s.variables_scroll.offset = 72.0;
assert_eq!(s.property_panel_scroll.offset, 12.0);
assert_eq!(s.layer_pages_scroll.offset, 24.0);
assert_eq!(s.layer_layers_scroll.offset, 36.0);
assert_eq!(s.layer_pages_h_scroll.offset, 48.0);
assert_eq!(s.layer_layers_h_scroll.offset, 60.0);
assert_eq!(s.variables_scroll.offset, 72.0);
}
#[test]
fn theme_mode_flips() {
assert_eq!(ThemeMode::Dark.flipped(), ThemeMode::Light);

View file

@ -93,7 +93,7 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D) -> Buil
if let Some(preset) = agent_settings_builtin_parts::preset_at(
card,
point,
settings.builtin_preset_menu_scroll,
settings.builtin_preset_menu_scroll.offset,
) {
return BuiltinHit::SelectPreset {
index: Some(index),
@ -146,7 +146,7 @@ pub fn hit_test(content: Rect, settings: &AgentSettings, point: Point2D) -> Buil
if let Some(preset) = agent_settings_builtin_parts::preset_at(
card,
point,
settings.builtin_preset_menu_scroll,
settings.builtin_preset_menu_scroll.offset,
) {
return BuiltinHit::SelectPreset {
index: None,
@ -207,7 +207,11 @@ pub fn preset_hover_at(
point: Point2D,
) -> Option<BuiltinAgentPresetKey> {
let card = open_preset_menu_card(content, settings, point)?;
agent_settings_builtin_parts::preset_hover_at(card, point, settings.builtin_preset_menu_scroll)
agent_settings_builtin_parts::preset_hover_at(
card,
point,
settings.builtin_preset_menu_scroll.offset,
)
}
pub fn preset_scroll_max_at(

View file

@ -159,8 +159,10 @@ pub fn paint_preset_menu(
cx.backend.stroke_round_rect(menu, 6.0, theme.border, 1.0);
cx.backend.save();
cx.backend.clip_rect(menu);
cx.backend
.translate(Point2D::new(0.0, -settings.builtin_preset_menu_scroll));
cx.backend.translate(Point2D::new(
0.0,
-settings.builtin_preset_menu_scroll.offset,
));
for (i, preset) in BUILTIN_AGENT_PRESETS.iter().enumerate() {
let item = preset_item_rect(card, i);
let active = agent.preset == preset.key;
@ -335,21 +337,23 @@ fn preset_menu_view_height() -> f32 {
preset_content_height().min(max_h)
}
fn paint_menu_scrollbar(cx: &mut PaintCx<'_>, theme: &Theme, menu: Rect, scroll: f32) {
let max = preset_scroll_max();
if max <= 0.0 {
return;
}
fn paint_menu_scrollbar(
cx: &mut PaintCx<'_>,
theme: &Theme,
menu: Rect,
scroll: jian_core::scroll::ScrollState,
) {
let content_h = preset_content_height();
let thumb_h = (menu.size.y * menu.size.y / content_h).clamp(24.0, menu.size.y);
let range = (menu.size.y - thumb_h).max(1.0);
let t = (scroll.clamp(0.0, max) / max).clamp(0.0, 1.0);
let track_h = (menu.size.y - 8.0).max(0.0);
let Some(thumb_geom) = scroll.thumb(track_h, content_h, menu.size.y, 24.0) else {
return;
};
let thumb = Rect {
origin: Point2D::new(
menu.origin.x + menu.size.x - 5.0,
menu.origin.y + 4.0 + t * range,
menu.origin.y + 4.0 + thumb_geom.offset,
),
size: Point2D::new(2.0, (thumb_h - 8.0).max(16.0)),
size: Point2D::new(2.0, thumb_geom.len),
};
cx.backend.fill_round_rect(
thumb,

View file

@ -149,7 +149,7 @@ impl<'a> AgentSettingsPanel<'a> {
}
// Translate the cursor into the scrolled content frame
// for hit-tests over scrollable rows.
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
match self.settings.tab {
AgentSettingsTab::Agents => {
match agent_settings_builtin::hit_test(
@ -303,7 +303,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return None;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
(0..AgentProvider::ALL.len())
.find(|&i| (agent_card_rect_in(panel, i, &self.settings)).contains(scrolled))
}
@ -312,7 +312,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return None;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_builtin::card_at(content_rect(panel), &self.settings, scrolled)
}
@ -321,7 +321,7 @@ impl<'a> AgentSettingsPanel<'a> {
return None;
}
let content = content_rect(panel);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
let section_y = acp_section_y(content, &self.settings);
agent_settings_acp::card_at(content, &self.settings, scrolled, section_y)
}
@ -334,7 +334,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return None;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_builtin::preset_hover_at(content_rect(panel), &self.settings, scrolled)
}
@ -342,7 +342,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return None;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_builtin::preset_scroll_max_at(content_rect(panel), &self.settings, scrolled)
}
@ -350,7 +350,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return false;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_images::search_test_button_hover_at(
content_rect(panel),
&self.settings,
@ -362,7 +362,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return false;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_images::add_gen_button_hover_at(
content_rect(panel),
&self.settings,
@ -378,7 +378,7 @@ impl<'a> AgentSettingsPanel<'a> {
if !(panel).contains(point) {
return None;
}
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y);
let scrolled = Point2D::new(point.x, point.y + self.settings.scroll_y.offset);
agent_settings_images::profile_test_button_hover_at(
content_rect(panel),
&self.settings,
@ -447,7 +447,8 @@ fn paint_panel(
let content_rect = content_rect(panel);
cx.backend.save();
cx.backend.clip_rect(content_rect);
cx.backend.translate(Point2D::new(0.0, -settings.scroll_y));
cx.backend
.translate(Point2D::new(0.0, -settings.scroll_y.offset));
match settings.tab {
AgentSettingsTab::Agents => {
paint_agents_tab(cx, theme, settings, _ui, content_rect, now_ms)

View file

@ -434,19 +434,14 @@ pub fn paint_model_picker(
// unscrolled card space. Shown only when the content overflows.
let content_h = picker_list_height(models, search);
let view_h = list_rect.size.y;
if content_h > view_h + 0.5 {
let track_h = view_h - 8.0;
let thumb_h = (track_h * view_h / content_h).max(24.0);
let max_scroll = (content_h - view_h).max(0.0);
let t = if max_scroll > 0.0 {
(scroll / max_scroll).clamp(0.0, 1.0)
} else {
0.0
};
let thumb_y = list_rect.origin.y + 4.0 + t * (track_h - thumb_h);
let track_h = (view_h - 8.0).max(0.0);
if let Some(thumb_geom) =
(jian_core::scroll::ScrollState { offset: scroll }).thumb(track_h, content_h, view_h, 24.0)
{
let thumb_y = list_rect.origin.y + 4.0 + thumb_geom.offset;
let thumb = Rect {
origin: Point2D::new(rect.origin.x + rect.size.x - 6.0, thumb_y),
size: Point2D::new(3.0, thumb_h),
size: Point2D::new(3.0, thumb_geom.len),
};
cx.backend
.fill_round_rect(thumb, 1.5, theme.muted_foreground);

View file

@ -476,7 +476,7 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
fixed_checklist_rect(rect, input_h, checklist_h),
&self.state.messages,
self.state.checklist_collapsed,
self.state.checklist_scroll,
self.state.checklist_scroll.offset,
);
}
let input_rect = Rect {

View file

@ -118,10 +118,10 @@ impl LayerPanel {
drag_ghost: None,
now_ms: 0,
rename_input: state.ui.layer_rename.as_ref().map(|r| r.input.clone()),
pages_scroll: state.editor_ui.layer_pages_scroll,
layers_scroll: state.editor_ui.layer_layers_scroll,
pages_h_scroll: state.editor_ui.layer_pages_h_scroll,
layers_h_scroll: state.editor_ui.layer_layers_h_scroll,
pages_scroll: state.editor_ui.layer_pages_scroll.offset,
layers_scroll: state.editor_ui.layer_layers_scroll.offset,
pages_h_scroll: state.editor_ui.layer_pages_h_scroll.offset,
layers_h_scroll: state.editor_ui.layer_layers_h_scroll.offset,
}
}
@ -174,10 +174,10 @@ impl LayerPanel {
drag_ghost: None,
now_ms: 0,
rename_input: state.ui.layer_rename.as_ref().map(|r| r.input.clone()),
pages_scroll: state.editor_ui.layer_pages_scroll,
layers_scroll: state.editor_ui.layer_layers_scroll,
pages_h_scroll: state.editor_ui.layer_pages_h_scroll,
layers_h_scroll: state.editor_ui.layer_layers_h_scroll,
pages_scroll: state.editor_ui.layer_pages_scroll.offset,
layers_scroll: state.editor_ui.layer_layers_scroll.offset,
pages_h_scroll: state.editor_ui.layer_pages_h_scroll.offset,
layers_h_scroll: state.editor_ui.layer_layers_h_scroll.offset,
}
}

View file

@ -508,7 +508,7 @@ fn deep_layer_tree_exposes_horizontal_scroll_range() {
fn layer_horizontal_scroll_offset_is_clamped() {
run_deep_layer_fixture(|| {
let mut state = state_from(&nested_frame_doc(50));
state.editor_ui.layer_layers_h_scroll = 10_000.0;
state.editor_ui.layer_layers_h_scroll.offset = 10_000.0;
let panel = LayerPanel::from_editor(&state);
let rect = Rect {
origin: Point2D::new(0.0, 0.0),

View file

@ -403,7 +403,7 @@ impl PropertyPanel {
export_scale_picker_open: ui.export_scale_picker_open,
export_format_picker_open: ui.export_format_picker_open,
export_picker_hover: ui.export_picker_hover,
scroll: ui.property_panel_scroll.max(0.0),
scroll: ui.property_panel_scroll.offset.max(0.0),
locale: ui.locale,
// Inert in the multi-select aggregate view.
effect_param_focus: if is_multi {

View file

@ -479,7 +479,7 @@ fn paint_framework_chips(
// Clip the strip so scrolled-off chips don't bleed past the panel edges.
cx.backend.save();
cx.backend.clip_rect(band);
for (fw, chip) in framework_chip_rects(x, y, w, state.framework_scroll) {
for (fw, chip) in framework_chip_rects(x, y, w, state.framework_scroll.offset) {
// Skip chips fully outside the visible band (cheap cull).
if chip.origin.x + chip.size.x < band.origin.x
|| chip.origin.x > band.origin.x + band.size.x
@ -523,7 +523,7 @@ fn paint_framework_chips(
theme,
Icon::ChevronLeft,
left,
state.framework_scroll > 0.0,
state.framework_scroll.offset > 0.0,
action_hovered(state, CodegenHover::ScrollFrameworksLeft),
);
paint_chevron(
@ -531,7 +531,7 @@ fn paint_framework_chips(
theme,
Icon::ChevronRight,
right,
state.framework_scroll < max,
state.framework_scroll.offset < max,
action_hovered(state, CodegenHover::ScrollFrameworksRight),
);
}
@ -908,7 +908,7 @@ fn code_action_rects_with_bottom(
}
let inset = if zones.is_some() { CHEVRON_ZONE_W } else { 0.0 };
let (band_l, band_r) = (x + PAD_X + inset, x + w - PAD_X - inset);
for (fw, rect) in framework_chip_rects(x, chips_y, w, state.framework_scroll) {
for (fw, rect) in framework_chip_rects(x, chips_y, w, state.framework_scroll.offset) {
// Clamp the clickable rect to the visible (chevron-inset) band so a
// chip's scrolled-off / clipped portion is NOT clickable (matches the
// painter's clip and the hover hit-test in `framework_at`).

View file

@ -126,7 +126,7 @@ pub(super) fn paint_complete_body_in_panel(
theme,
&state.code,
state.code_selection,
state.code_scroll,
state.code_scroll.offset,
code_rect,
);
let actions = [
@ -158,6 +158,7 @@ pub(super) fn code_text_offset_at_in_panel(
}
let scroll = state
.code_scroll
.offset
.clamp(0.0, max_scroll_for_code(&state.code, rect));
let line_index = ((point.y - code_text_top(rect) + scroll) / CODE_LINE_H)
.floor()

View file

@ -532,7 +532,9 @@ fn complete_code_preview_hit_test_respects_vertical_scroll() {
.join("\n");
let state = CodegenState {
phase: CodegenPhase::Complete,
code_scroll: 16.0 * 10.0,
code_scroll: jian_core::scroll::ScrollState {
offset: 16.0 * 10.0,
},
code,
..CodegenState::default()
};

View file

@ -251,7 +251,7 @@ impl VariablesPanel {
search: state.editor_ui.variables_search.clone(),
search_focus: state.editor_ui.variables_search_focus,
search_input,
scroll: state.editor_ui.variables_scroll,
scroll: state.editor_ui.variables_scroll.offset,
row_menu_open: state.editor_ui.variables_row_menu,
hover: state.editor_ui.variables_panel_hover,
editing_name_row: state.editor_ui.variable_row_focus.and_then(|f| match f {

View file

@ -64,7 +64,7 @@ fn filtered_rows_keep_unfiltered_source_indices() {
#[test]
fn scroll_clamps_and_offsets_row_hits() {
let mut state = state_with_n_colors(20);
state.editor_ui.variables_scroll = 1.0e9; // stale offset self-corrects
state.editor_ui.variables_scroll.offset = 1.0e9; // stale offset self-corrects
let panel = VariablesPanel::for_editor(&state);
let rect = panel_rect(&panel);
let max = panel.max_scroll(rect);

View file

@ -300,7 +300,7 @@ pub fn pump(
{
let cg = &mut host.editor_state_mut().codegen;
cg.code = code.clone();
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
cg.degraded = degraded;
cg.assets = metas;

View file

@ -2088,8 +2088,11 @@ fn codegen_preview_wheel_scrolls_code_not_property_panel() {
viewport_h
));
assert!(host.editor_state().codegen.code_scroll > 0.0);
assert_eq!(host.editor_state().editor_ui.property_panel_scroll, 0.0);
assert!(host.editor_state().codegen.code_scroll.offset > 0.0);
assert_eq!(
host.editor_state().editor_ui.property_panel_scroll.offset,
0.0
);
}
#[test]

View file

@ -170,7 +170,7 @@ impl WidgetHostNative {
self.editor_state.ui.property_caret_anchor_ms = self.now_ms;
// A narrower list invalidates the scroll offset — the
// widget clamps, but reset for a stable reveal-from-top.
self.editor_state.editor_ui.variables_scroll = 0.0;
self.editor_state.editor_ui.variables_scroll.offset = 0.0;
self.mark_dirty();
return true;
}
@ -521,7 +521,7 @@ impl WidgetHostNative {
if self.variables_search_active() {
if self.editor_state.editor_ui.variables_search.pop().is_some() {
self.editor_state.ui.property_caret_anchor_ms = self.now_ms;
self.editor_state.editor_ui.variables_scroll = 0.0;
self.editor_state.editor_ui.variables_scroll.offset = 0.0;
self.mark_dirty();
return true;
}

View file

@ -68,7 +68,7 @@ impl WidgetHostNative {
AgentSettingsHit::SelectTab(t) => {
self.commit_settings_focus_if_any();
self.editor_state.editor_ui.agent_settings.tab = t;
self.editor_state.editor_ui.agent_settings.scroll_y = 0.0;
self.editor_state.editor_ui.agent_settings.scroll_y.offset = 0.0;
}
AgentSettingsHit::Connect(p) => {
// `connected` is indexed by `AgentProvider::ALL` order.
@ -365,7 +365,7 @@ impl WidgetHostNative {
let settings = &mut self.editor_state.editor_ui.agent_settings;
settings.builtin_preset_menu_open =
(settings.builtin_preset_menu_open != Some(target)).then_some(target);
settings.builtin_preset_menu_scroll = 0.0;
settings.builtin_preset_menu_scroll.offset = 0.0;
settings.builtin_preset_menu_hover = None;
}
AgentSettingsHit::SelectBuiltinAgentPreset { index, preset } => {
@ -391,7 +391,8 @@ impl WidgetHostNative {
self.editor_state
.editor_ui
.agent_settings
.builtin_preset_menu_scroll = 0.0;
.builtin_preset_menu_scroll
.offset = 0.0;
self.editor_state
.editor_ui
.agent_settings

View file

@ -437,14 +437,14 @@ impl WidgetHostNative {
cg.pending_generate = true;
cg.phase = CodegenPhase::Generating;
cg.error = None;
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
}
CodegenAction::Regenerate => {
cg.pending_regenerate = true;
cg.phase = CodegenPhase::Generating;
cg.error = None;
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
}
CodegenAction::Cancel => {
@ -481,12 +481,13 @@ impl WidgetHostNative {
op_editor_ui::widgets::property_panel_code::framework_row_overflow(pw);
let step = 100.0;
let cg = &mut self.editor_state.codegen;
cg.framework_scroll =
if matches!(codegen_action, CodegenAction::ScrollFrameworksLeft) {
(cg.framework_scroll - step).clamp(0.0, max)
} else {
(cg.framework_scroll + step).clamp(0.0, max)
};
let delta = if matches!(codegen_action, CodegenAction::ScrollFrameworksLeft)
{
-step
} else {
step
};
cg.framework_scroll.scroll_by(delta, max, 0.0);
}
}
}

View file

@ -4,9 +4,16 @@
//! otherwise zoom / pan the canvas.
use super::WidgetHostNative;
use jian_core::scroll::ScrollState;
use op_editor_ui::widgets::GitPanel;
use op_editor_ui::Point2D;
fn scroll_by_max(scroll: &mut ScrollState, delta: f32, max: f32) -> bool {
let before = scroll.offset;
scroll.scroll_by(delta, max, 0.0);
scroll.offset != before
}
impl WidgetHostNative {
fn try_scroll_chat_checklist(
&mut self,
@ -31,9 +38,7 @@ impl WidgetHostNative {
if !(checklist).contains(point) {
return false;
}
let next = (self.editor_state.chat.checklist_scroll - delta).clamp(0.0, max);
if next != self.editor_state.chat.checklist_scroll {
self.editor_state.chat.checklist_scroll = next;
if scroll_by_max(&mut self.editor_state.chat.checklist_scroll, -delta, max) {
self.mark_dirty();
}
true
@ -56,9 +61,7 @@ impl WidgetHostNative {
return false;
};
let settings = &mut self.editor_state.editor_ui.agent_settings;
let next = (settings.builtin_preset_menu_scroll - delta).clamp(0.0, max);
if next != settings.builtin_preset_menu_scroll {
settings.builtin_preset_menu_scroll = next;
if scroll_by_max(&mut settings.builtin_preset_menu_scroll, -delta, max) {
self.mark_dirty();
}
true
@ -88,9 +91,11 @@ impl WidgetHostNative {
use op_editor_ui::widgets::variables_panel::VariablesPanel;
let panel = VariablesPanel::for_editor(&self.editor_state);
let max = panel.max_scroll(panel_rect);
let next = (self.editor_state.editor_ui.variables_scroll - delta_y).clamp(0.0, max);
if next != self.editor_state.editor_ui.variables_scroll {
self.editor_state.editor_ui.variables_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.variables_scroll,
-delta_y,
max,
) {
self.mark_dirty();
}
true
@ -165,9 +170,7 @@ impl WidgetHostNative {
if y >= band_top && y <= band_bottom {
let max = op_editor_ui::widgets::property_panel_code::framework_row_overflow(pw);
let cg = &mut self.editor_state.codegen;
let next = (cg.framework_scroll - delta).clamp(0.0, max);
if next != cg.framework_scroll {
cg.framework_scroll = next;
if scroll_by_max(&mut cg.framework_scroll, -delta, max) {
self.mark_dirty();
}
return true;
@ -184,18 +187,18 @@ impl WidgetHostNative {
)
.unwrap_or(0.0);
let cg = &mut self.editor_state.codegen;
let next = (cg.code_scroll - delta).clamp(0.0, max);
if next != cg.code_scroll {
cg.code_scroll = next;
if scroll_by_max(&mut cg.code_scroll, -delta, max) {
self.mark_dirty();
}
return true;
}
}
let max = (panel.content_height(property_rect) - property_rect.size.y).max(0.0);
let next = (self.editor_state.editor_ui.property_panel_scroll - delta).clamp(0.0, max);
if next != self.editor_state.editor_ui.property_panel_scroll {
self.editor_state.editor_ui.property_panel_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.property_panel_scroll,
-delta,
max,
) {
self.mark_dirty();
}
true
@ -230,35 +233,39 @@ impl WidgetHostNative {
let mut changed = false;
if y >= r.layers_rows_top {
if delta_y != 0.0 {
let next = (self.editor_state.editor_ui.layer_layers_scroll - delta_y)
.clamp(0.0, r.layers_max_scroll);
if next != self.editor_state.editor_ui.layer_layers_scroll {
self.editor_state.editor_ui.layer_layers_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_layers_scroll,
-delta_y,
r.layers_max_scroll,
) {
changed = true;
}
}
if delta_x != 0.0 {
let next = (self.editor_state.editor_ui.layer_layers_h_scroll - delta_x)
.clamp(0.0, r.layers_max_h_scroll);
if next != self.editor_state.editor_ui.layer_layers_h_scroll {
self.editor_state.editor_ui.layer_layers_h_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_layers_h_scroll,
-delta_x,
r.layers_max_h_scroll,
) {
changed = true;
}
}
} else {
if delta_y != 0.0 {
let next = (self.editor_state.editor_ui.layer_pages_scroll - delta_y)
.clamp(0.0, r.pages_max_scroll);
if next != self.editor_state.editor_ui.layer_pages_scroll {
self.editor_state.editor_ui.layer_pages_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_pages_scroll,
-delta_y,
r.pages_max_scroll,
) {
changed = true;
}
}
if delta_x != 0.0 {
let next = (self.editor_state.editor_ui.layer_pages_h_scroll - delta_x)
.clamp(0.0, r.pages_max_h_scroll);
if next != self.editor_state.editor_ui.layer_pages_h_scroll {
self.editor_state.editor_ui.layer_pages_h_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_pages_h_scroll,
-delta_x,
r.pages_max_h_scroll,
) {
changed = true;
}
}
@ -342,9 +349,11 @@ impl WidgetHostNative {
let total = panel.content_total_height();
let viewport_h_inner = panel_rect.size.y - 48.0;
let max_scroll = (total - viewport_h_inner).max(0.0);
let next = (self.editor_state.editor_ui.agent_settings.scroll_y - delta_y)
.clamp(0.0, max_scroll);
self.editor_state.editor_ui.agent_settings.scroll_y = next;
self.editor_state
.editor_ui
.agent_settings
.scroll_y
.scroll_by(-delta_y, max_scroll, 0.0);
self.mark_dirty();
return true;
}
@ -484,9 +493,11 @@ impl WidgetHostNative {
let total = panel.content_total_height();
let viewport_h_inner = panel_rect.size.y - 48.0;
let max_scroll = (total - viewport_h_inner).max(0.0);
let next = (self.editor_state.editor_ui.agent_settings.scroll_y - dy)
.clamp(0.0, max_scroll);
self.editor_state.editor_ui.agent_settings.scroll_y = next;
self.editor_state
.editor_ui
.agent_settings
.scroll_y
.scroll_by(-dy, max_scroll, 0.0);
self.mark_dirty();
return true;
}

View file

@ -60,7 +60,7 @@ fn layer_panel_trackpad_pan_scrolls_horizontally() {
viewport_h
));
assert!(host.editor_state().editor_ui.layer_layers_h_scroll > 0.0);
assert!(host.editor_state().editor_ui.layer_layers_h_scroll.offset > 0.0);
});
}
@ -89,7 +89,7 @@ fn chat_checklist_wheel_scrolls_pinned_task_list() {
viewport_h
));
assert!(host.editor_state().chat.checklist_scroll > 0.0);
assert!(host.editor_state().chat.checklist_scroll.offset > 0.0);
}
#[test]

View file

@ -332,22 +332,22 @@ fn wheel_over_panel_scrolls_rows_and_clamps() {
// Wheel scroll-down advances the list.
assert!(host.apply_wheel(cx, cy, -60.0, VIEWPORT_W, VIEWPORT_H));
assert!(host.editor_state().editor_ui.variables_scroll > 0.0);
assert!(host.editor_state().editor_ui.variables_scroll.offset > 0.0);
// Huge scroll clamps to max.
assert!(host.apply_wheel(cx, cy, -1.0e6, VIEWPORT_W, VIEWPORT_H));
let panel = VariablesPanel::for_editor(host.editor_state());
let max = panel.max_scroll(rect);
assert!(max > 0.0);
assert_eq!(host.editor_state().editor_ui.variables_scroll, max);
assert_eq!(host.editor_state().editor_ui.variables_scroll.offset, max);
// Scroll back past the top clamps to 0.
assert!(host.apply_wheel(cx, cy, 1.0e6, VIEWPORT_W, VIEWPORT_H));
assert_eq!(host.editor_state().editor_ui.variables_scroll, 0.0);
assert_eq!(host.editor_state().editor_ui.variables_scroll.offset, 0.0);
// A scrolled list maps hits through the offset: with max scroll,
// the LAST row sits just above the footer.
host.editor_state_mut().editor_ui.variables_scroll = max;
host.editor_state_mut().editor_ui.variables_scroll.offset = max;
let panel = VariablesPanel::for_editor(host.editor_state());
let footer_top = rect.origin.y + rect.size.y - 40.0;
let hit = panel.hit_test(rect, Point2D::new(rect.origin.x + 60.0, footer_top - 20.0));

View file

@ -545,7 +545,7 @@ fn start_pump(inner: Rc<RefCell<crate::Inner>>, shared: Shared) {
assets,
} => {
cg.code = code;
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
cg.degraded = degraded;
cg.assets = assets;

View file

@ -544,9 +544,12 @@ impl WidgetHost {
if !(checklist).contains(point) {
return false;
}
let next = (self.editor_state.chat.checklist_scroll - delta).clamp(0.0, max);
if next != self.editor_state.chat.checklist_scroll {
self.editor_state.chat.checklist_scroll = next;
let before = self.editor_state.chat.checklist_scroll.offset;
self.editor_state
.chat
.checklist_scroll
.scroll_by(-delta, max, 0.0);
if self.editor_state.chat.checklist_scroll.offset != before {
self.mark_dirty();
}
true
@ -573,8 +576,9 @@ impl WidgetHost {
.builtin_preset_scroll_max_at(panel_rect, point)
{
let settings = &mut self.editor_state.editor_ui.agent_settings;
settings.builtin_preset_menu_scroll =
(settings.builtin_preset_menu_scroll - delta_y).clamp(0.0, max);
settings
.builtin_preset_menu_scroll
.scroll_by(-delta_y, max, 0.0);
self.mark_dirty();
return true;
}
@ -582,9 +586,11 @@ impl WidgetHost {
let total = panel.content_total_height();
let viewport_h_inner = panel_rect.size.y - 48.0;
let max_scroll = (total - viewport_h_inner).max(0.0);
self.editor_state.editor_ui.agent_settings.scroll_y =
(self.editor_state.editor_ui.agent_settings.scroll_y - delta_y)
.clamp(0.0, max_scroll);
self.editor_state
.editor_ui
.agent_settings
.scroll_y
.scroll_by(-delta_y, max_scroll, 0.0);
self.mark_dirty();
return true;
}

View file

@ -33,7 +33,7 @@ impl WidgetHost {
AgentSettingsHit::SelectTab(tab) => {
self.commit_settings_focus();
self.editor_state.editor_ui.agent_settings.tab = tab;
self.editor_state.editor_ui.agent_settings.scroll_y = 0.0;
self.editor_state.editor_ui.agent_settings.scroll_y.offset = 0.0;
}
AgentSettingsHit::Connect(provider) => {
let idx = AgentProvider::ALL
@ -334,7 +334,7 @@ impl WidgetHost {
let settings = &mut self.editor_state.editor_ui.agent_settings;
settings.builtin_preset_menu_open =
(settings.builtin_preset_menu_open != Some(target)).then_some(target);
settings.builtin_preset_menu_scroll = 0.0;
settings.builtin_preset_menu_scroll.offset = 0.0;
settings.builtin_preset_menu_hover = None;
}
AgentSettingsHit::SelectBuiltinAgentPreset { index, preset } => {
@ -360,7 +360,8 @@ impl WidgetHost {
self.editor_state
.editor_ui
.agent_settings
.builtin_preset_menu_scroll = 0.0;
.builtin_preset_menu_scroll
.offset = 0.0;
self.editor_state
.editor_ui
.agent_settings

View file

@ -43,7 +43,7 @@ impl WidgetHost {
if self.variables_search_active() && !c.is_control() {
self.editor_state.editor_ui.variables_search.push(c);
self.editor_state.ui.property_caret_anchor_ms = self.now_ms;
self.editor_state.editor_ui.variables_scroll = 0.0;
self.editor_state.editor_ui.variables_scroll.offset = 0.0;
self.mark_dirty();
return true;
}
@ -222,7 +222,7 @@ impl WidgetHost {
if self.variables_search_active() {
if self.editor_state.editor_ui.variables_search.pop().is_some() {
self.editor_state.ui.property_caret_anchor_ms = self.now_ms;
self.editor_state.editor_ui.variables_scroll = 0.0;
self.editor_state.editor_ui.variables_scroll.offset = 0.0;
self.mark_dirty();
return true;
}

View file

@ -491,14 +491,14 @@ impl WidgetHost {
cg.pending_generate = true;
cg.phase = CodegenPhase::Generating;
cg.error = None;
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
}
CodegenAction::Regenerate => {
cg.pending_regenerate = true;
cg.phase = CodegenPhase::Generating;
cg.error = None;
cg.code_scroll = 0.0;
cg.code_scroll.offset = 0.0;
cg.code_selection = None;
}
CodegenAction::Cancel => {
@ -552,11 +552,12 @@ impl WidgetHost {
let max = op_editor_ui::widgets::property_panel_code::framework_row_overflow(pw);
let step = 100.0;
let cg = &mut self.editor_state.codegen;
cg.framework_scroll = if matches!(action, CodegenAction::ScrollFrameworksLeft) {
(cg.framework_scroll - step).clamp(0.0, max)
let delta = if matches!(action, CodegenAction::ScrollFrameworksLeft) {
-step
} else {
(cg.framework_scroll + step).clamp(0.0, max)
step
};
cg.framework_scroll.scroll_by(delta, max, 0.0);
}
}
}

View file

@ -114,6 +114,9 @@ fn codegen_preview_wheel_scrolls_code_not_property_panel() {
viewport_h
));
assert!(host.editor_state.codegen.code_scroll > 0.0);
assert_eq!(host.editor_state.editor_ui.property_panel_scroll, 0.0);
assert!(host.editor_state.codegen.code_scroll.offset > 0.0);
assert_eq!(
host.editor_state.editor_ui.property_panel_scroll.offset,
0.0
);
}

View file

@ -2,11 +2,18 @@
//! `widget_host.rs` so the spine stays under the 800-line cap.
//! Mirrors the native host's `widget_host/scroll.rs`.
use jian_core::scroll::ScrollState;
use op_editor_ui::widgets::{LayerPanel, PropertyPanel, TOP_BAR_HEIGHT};
use op_editor_ui::{Point2D, Rect};
use super::WidgetHost;
fn scroll_by_max(scroll: &mut ScrollState, delta: f32, max: f32) -> bool {
let before = scroll.offset;
scroll.scroll_by(delta, max, 0.0);
scroll.offset != before
}
impl WidgetHost {
/// Scroll the floating VariablesPanel row list when the wheel
/// fires over the open panel (TS `overflow-y-auto` rows region).
@ -32,9 +39,11 @@ impl WidgetHost {
use op_editor_ui::widgets::variables_panel::VariablesPanel;
let panel = VariablesPanel::for_editor(&self.editor_state);
let max = panel.max_scroll(panel_rect);
let next = (self.editor_state.editor_ui.variables_scroll - delta_y).clamp(0.0, max);
if next != self.editor_state.editor_ui.variables_scroll {
self.editor_state.editor_ui.variables_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.variables_scroll,
-delta_y,
max,
) {
self.mark_dirty();
}
true
@ -113,9 +122,7 @@ impl WidgetHost {
if y >= band_top && y <= band_bottom {
let max = op_editor_ui::widgets::property_panel_code::framework_row_overflow(pw);
let cg = &mut self.editor_state.codegen;
let next = (cg.framework_scroll - delta_y).clamp(0.0, max);
if next != cg.framework_scroll {
cg.framework_scroll = next;
if scroll_by_max(&mut cg.framework_scroll, -delta_y, max) {
self.mark_dirty();
}
return true;
@ -132,18 +139,18 @@ impl WidgetHost {
)
.unwrap_or(0.0);
let cg = &mut self.editor_state.codegen;
let next = (cg.code_scroll - delta_y).clamp(0.0, max);
if next != cg.code_scroll {
cg.code_scroll = next;
if scroll_by_max(&mut cg.code_scroll, -delta_y, max) {
self.mark_dirty();
}
return true;
}
}
let max = (panel.content_height(property_rect) - property_rect.size.y).max(0.0);
let next = (self.editor_state.editor_ui.property_panel_scroll - delta_y).clamp(0.0, max);
if next != self.editor_state.editor_ui.property_panel_scroll {
self.editor_state.editor_ui.property_panel_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.property_panel_scroll,
-delta_y,
max,
) {
self.mark_dirty();
}
true
@ -172,17 +179,19 @@ impl WidgetHost {
}
let r = LayerPanel::from_editor(&self.editor_state).regions(rect);
if y >= r.layers_rows_top {
let next = (self.editor_state.editor_ui.layer_layers_scroll - delta_y)
.clamp(0.0, r.layers_max_scroll);
if next != self.editor_state.editor_ui.layer_layers_scroll {
self.editor_state.editor_ui.layer_layers_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_layers_scroll,
-delta_y,
r.layers_max_scroll,
) {
self.mark_dirty();
}
} else {
let next = (self.editor_state.editor_ui.layer_pages_scroll - delta_y)
.clamp(0.0, r.pages_max_scroll);
if next != self.editor_state.editor_ui.layer_pages_scroll {
self.editor_state.editor_ui.layer_pages_scroll = next;
if scroll_by_max(
&mut self.editor_state.editor_ui.layer_pages_scroll,
-delta_y,
r.pages_max_scroll,
) {
self.mark_dirty();
}
}

View file

@ -377,16 +377,16 @@ fn wheel_scrolls_rows_and_resize_drag_resizes() {
let cx = rect.origin.x + rect.size.x / 2.0;
let cy = rect.origin.y + rect.size.y / 2.0;
assert!(host.apply_wheel(cx, cy, -60.0, W, H));
assert!(host.editor_state.editor_ui.variables_scroll > 0.0);
assert!(host.editor_state.editor_ui.variables_scroll.offset > 0.0);
// Huge values clamp.
assert!(host.apply_wheel(cx, cy, -1.0e6, W, H));
let panel = VariablesPanel::for_editor(&host.editor_state);
assert_eq!(
host.editor_state.editor_ui.variables_scroll,
host.editor_state.editor_ui.variables_scroll.offset,
panel.max_scroll(rect)
);
assert!(host.apply_wheel(cx, cy, 1.0e6, W, H));
assert_eq!(host.editor_state.editor_ui.variables_scroll, 0.0);
assert_eq!(host.editor_state.editor_ui.variables_scroll.offset, 0.0);
// Right-edge press arms a resize; drag narrows; release ends.
let edge_x = rect.origin.x + rect.size.x - 2.0;