refactor(editor): migrate scroll fields to shared scroll state
This commit is contained in:
parent
94f8be6844
commit
e4125fd75d
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue