fix(editor-ui): resolve conflict markers swept into the chat panel during the rebase
The background fmt commit captured the editor-ui pick's conflicted state mid-rebase; both sides kept (ab-v9 concurrency-chip spacing + tests_paint module, batch tests_transcript module + footer tests), paint helpers promoted for cross-module reuse.
This commit is contained in:
parent
14482a5d86
commit
9fd78d2d8a
|
|
@ -585,13 +585,9 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
self.theme.muted_foreground,
|
||||
1.4,
|
||||
);
|
||||
<<<<<<< HEAD
|
||||
model_x += 18.0;
|
||||
// Concurrency chip — `Zap` + `{n}x`. A solo 1x team rests as a
|
||||
// faint ghost; a staffed team (>1) gets a primary-tinted chip so
|
||||
// the parallel mode reads as active.
|
||||
=======
|
||||
>>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
|
||||
let chip = footer.agent_team;
|
||||
let team_size = self.state.agent_team_size;
|
||||
let team_active = team_size > 1;
|
||||
|
|
@ -782,13 +778,10 @@ pub(crate) fn to_jian_color(c: Color) -> jian_core::scene::Color {
|
|||
#[cfg(test)]
|
||||
#[path = "ai_chat_panel/tests.rs"]
|
||||
mod tests;
|
||||
<<<<<<< HEAD
|
||||
#[cfg(test)]
|
||||
#[path = "ai_chat_panel/tests_paint.rs"]
|
||||
mod tests_paint;
|
||||
=======
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ai_chat_panel/tests_transcript.rs"]
|
||||
mod tests_transcript;
|
||||
>>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
//! Split into a sibling file to keep `ai_chat_panel.rs` under the
|
||||
//! 800-line cap.
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::tests_paint::{assert_close, color_close, rect_close};
|
||||
use super::*;
|
||||
use crate::widgets::ai_chat_hit::{AIChatHit, ChatResizeEdge};
|
||||
|
||||
|
|
@ -243,38 +245,6 @@ fn footer_agent_team_chip_is_clickable_and_hoverable() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
fn footer_selection_count_sits_close_to_agent_team_chip() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
s.selection.set = vec![op_editor_core::NodeId::new("n1")];
|
||||
s.selection.anchor = op_editor_core::NodeId::new("n1");
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let input = panel.input_rect(rect);
|
||||
let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
|
||||
let footer = panel.footer_layout(rect, input, toolbar_top);
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
let selected_text =
|
||||
op_i18n::translate(panel.locale, "common.selected").replace("{{count}}", "1");
|
||||
let (_, _, _, origin) = backend
|
||||
.texts
|
||||
.iter()
|
||||
.find(|(text, _, _, _)| text == &selected_text)
|
||||
.expect("footer should paint selected-count label");
|
||||
let gap = origin.x - (footer.agent_team.origin.x + footer.agent_team.size.x);
|
||||
assert_close(gap, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_input_expands_above_footer_toolbar() {
|
||||
let mut s = EditorState::new();
|
||||
s.chat.input =
|
||||
|
|
@ -289,140 +259,6 @@ fn multiline_input_expands_above_footer_toolbar() {
|
|||
assert!(panel.input_height() > INPUT_BASE_HEIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_send_button_hover_adds_visible_feedback() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
s.chat.input = "design a login page".into();
|
||||
s.editor_ui.chat_footer_hover = Some(op_editor_core::ChatFooterButton::Send);
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
let send_rect = Rect {
|
||||
origin: Point2D::new(AI_CHAT_WIDTH - PAD - 24.0, toolbar_center_y() - 24.0 / 2.0),
|
||||
size: Point2D::new(24.0, 24.0),
|
||||
};
|
||||
let fills: Vec<_> = backend
|
||||
.round_rects
|
||||
.iter()
|
||||
.filter(|(r, _, _)| rect_close(*r, send_rect))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
fills.len() >= 2,
|
||||
"hovered send button should paint feedback over its base fill"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_footer_neutral_hovers_use_visible_feedback() {
|
||||
let cases = [
|
||||
op_editor_core::ChatFooterButton::ModelPicker,
|
||||
op_editor_core::ChatFooterButton::AgentTeam,
|
||||
op_editor_core::ChatFooterButton::AddAttachment,
|
||||
op_editor_core::ChatFooterButton::Send,
|
||||
];
|
||||
|
||||
for hover in cases {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
s.editor_ui.chat_footer_hover = Some(hover);
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let input = panel.input_rect(rect);
|
||||
let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
|
||||
let footer = panel.footer_layout(rect, input, toolbar_top);
|
||||
let target = match hover {
|
||||
op_editor_core::ChatFooterButton::ModelPicker => footer.model,
|
||||
op_editor_core::ChatFooterButton::AgentTeam => footer.agent_team,
|
||||
op_editor_core::ChatFooterButton::AddAttachment => footer.attach,
|
||||
op_editor_core::ChatFooterButton::Send => footer.send,
|
||||
op_editor_core::ChatFooterButton::Stop => unreachable!(),
|
||||
};
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
assert!(
|
||||
backend.round_rects.iter().any(|(r, _, color)| {
|
||||
rect_close(*r, target)
|
||||
&& !color_close(*color, panel.theme.muted)
|
||||
&& color.a > panel.theme.button_hover.a + 0.01
|
||||
}),
|
||||
"{hover:?} hover should paint a visible neutral wash"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_model_picker_hover_stays_inside_model_chip() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
s.editor_ui.chat_footer_hover = Some(op_editor_core::ChatFooterButton::ModelPicker);
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let input = panel.input_rect(rect);
|
||||
let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
|
||||
let footer = panel.footer_layout(rect, input, toolbar_top);
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
let hover = backend
|
||||
.round_rects
|
||||
.iter()
|
||||
.find(|(r, _, color)| {
|
||||
rect_close(*r, footer.model)
|
||||
&& color_close(*color, chat_neutral_hover_color(&panel.theme))
|
||||
})
|
||||
.expect("model picker hover should paint a visible wash");
|
||||
|
||||
assert!(
|
||||
hover.0.origin.x + hover.0.size.x <= footer.agent_team.origin.x - 6.0,
|
||||
"model hover should leave visible spacing before the Agent Team chip"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_expanded_header_title_hover_adds_visible_feedback_across_label() {
|
||||
let mut s = EditorState::new();
|
||||
s.editor_ui.chat_header_hover = Some(op_editor_core::ChatHeaderButton::ToggleCollapse);
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
assert!(
|
||||
backend.round_rects.iter().any(|(r, _, color)| {
|
||||
r.origin.x <= PAD
|
||||
&& r.origin.y <= 6.0
|
||||
&& r.size.x >= 108.0
|
||||
&& r.size.y >= 28.0
|
||||
&& r.size.y <= 34.0
|
||||
&& color_close(*color, chat_neutral_hover_color(&panel.theme))
|
||||
}),
|
||||
"expanded New Chat title hover should cover the label, not only the chevron"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
>>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
|
||||
fn hit_test_resolves_model_search_clear_button() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
|
|
@ -599,184 +435,6 @@ fn hit_test_resolves_header_new_chat_button() {
|
|||
assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::NewChat));
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
#[test]
|
||||
fn body_rect_reserves_space_for_fixed_step_checklist() {
|
||||
let mut s = EditorState::new();
|
||||
let mut message = op_editor_core::ChatMessage::assistant_streaming();
|
||||
message.content =
|
||||
r#"<step title="Checking guidelines" status="streaming">Analyzing request...</step>"#
|
||||
.into();
|
||||
s.chat.messages.push(message);
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let body = panel.body_rect(rect);
|
||||
let legacy_bottom = rect.origin.y + rect.size.y - INPUT_BASE_HEIGHT - PAD - 8.0;
|
||||
|
||||
assert!(
|
||||
body.origin.y + body.size.y < legacy_bottom - 1.0,
|
||||
"fixed step checklist should reserve bottom space outside transcript"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_height_preserves_transcript_space_above_full_checklist() {
|
||||
let mut s = EditorState::new();
|
||||
let mut message = op_editor_core::ChatMessage::assistant_streaming();
|
||||
message.content = (0..17)
|
||||
.map(|idx| {
|
||||
format!(r#"<step title="Subtask {idx}" status="done">Generated section {idx}</step>"#)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
s.chat.messages.push(message);
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let body = panel.body_rect(rect);
|
||||
|
||||
assert!(
|
||||
body.size.y >= 150.0,
|
||||
"default chat panel height should leave room for prior chat above the pinned checklist, got {}",
|
||||
body.size.y
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_rect_reserves_less_space_when_fixed_step_checklist_collapsed() {
|
||||
let mut expanded_state = EditorState::new();
|
||||
let mut message = op_editor_core::ChatMessage::assistant_streaming();
|
||||
message.content = r#"<step title="Plan" status="done"></step>
|
||||
<step title="Draw" status="streaming"></step>"#
|
||||
.into();
|
||||
expanded_state.chat.messages.push(message.clone());
|
||||
|
||||
let mut collapsed_state = EditorState::new();
|
||||
collapsed_state.chat.messages.push(message);
|
||||
collapsed_state.chat.checklist_collapsed = true;
|
||||
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let expanded = AIChatPlaceholder::from_editor(&expanded_state).body_rect(rect);
|
||||
let collapsed = AIChatPlaceholder::from_editor(&collapsed_state).body_rect(rect);
|
||||
|
||||
assert!(collapsed.size.y > expanded.size.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_resolves_fixed_checklist_header_toggle() {
|
||||
let mut s = EditorState::new();
|
||||
let mut message = op_editor_core::ChatMessage::assistant_streaming();
|
||||
message.content = r#"<step title="Plan" status="done"></step>
|
||||
<step title="Draw" status="streaming"></step>"#
|
||||
.into();
|
||||
s.chat.messages.push(message);
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let checklist_h = fixed_checklist_height(&s.chat.messages, s.chat.checklist_collapsed);
|
||||
let checklist = fixed_checklist_rect(rect, INPUT_BASE_HEIGHT, checklist_h);
|
||||
let p = Point2D::new(
|
||||
checklist.origin.x + checklist.size.x / 2.0,
|
||||
checklist.origin.y + 2.0 + 32.0 / 2.0,
|
||||
);
|
||||
|
||||
assert_eq!(panel.hit_test(rect, p), Some(AIChatHit::ToggleChecklist));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_resolves_individual_tool_card_header_toggle() {
|
||||
let mut s = EditorState::new();
|
||||
let mut message = op_editor_core::ChatMessage::assistant("answer");
|
||||
message.tools_collapsed = false;
|
||||
message.tool_calls.push(op_editor_core::ChatToolCall {
|
||||
name: "snapshot_layout".into(),
|
||||
args: r#"{"args":{"pageId":"page-1"}}"#.into(),
|
||||
});
|
||||
s.chat.messages.push(message);
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let card_header = crate::widgets::ai_chat_transcript::build_transcript(
|
||||
&s.chat.messages,
|
||||
panel.body_rect(rect),
|
||||
panel.locale,
|
||||
)[0]
|
||||
.tools
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.cards[0]
|
||||
.header;
|
||||
let p = Point2D::new(
|
||||
card_header.origin.x + card_header.size.x / 2.0,
|
||||
card_header.origin.y + card_header.size.y / 2.0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
panel.hit_test(rect, p),
|
||||
Some(AIChatHit::SetToolCallCardExpanded(0, 0, true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_resolves_design_block_header_toggle() {
|
||||
let mut s = EditorState::new();
|
||||
s.chat.messages.push(op_editor_core::ChatMessage::assistant(
|
||||
r#"```json
|
||||
[{"id":"frame-1","type":"Frame"}]
|
||||
```"#,
|
||||
));
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let header = crate::widgets::ai_chat_transcript::build_transcript(
|
||||
&s.chat.messages,
|
||||
panel.body_rect(rect),
|
||||
panel.locale,
|
||||
)[0]
|
||||
.design_blocks[0]
|
||||
.header;
|
||||
let p = Point2D::new(
|
||||
header.origin.x + header.size.x / 2.0,
|
||||
header.origin.y + header.size.y / 2.0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
panel.hit_test(rect, p),
|
||||
Some(AIChatHit::SetDesignBlockExpanded(0, 0, true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_resolves_design_block_copy_button() {
|
||||
let code = r#"[{"id":"frame-1","type":"Frame"}]"#;
|
||||
let mut s = EditorState::new();
|
||||
s.chat
|
||||
.messages
|
||||
.push(op_editor_core::ChatMessage::assistant(format!(
|
||||
r#"```json
|
||||
{code}
|
||||
```"#
|
||||
)));
|
||||
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let block = &crate::widgets::ai_chat_transcript::build_transcript(
|
||||
&s.chat.messages,
|
||||
panel.body_rect(rect),
|
||||
panel.locale,
|
||||
)[0]
|
||||
.design_blocks[0];
|
||||
let p = Point2D::new(
|
||||
block.header.origin.x + block.header.size.x - 38.0,
|
||||
block.header.origin.y + block.header.size.y / 2.0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
panel.hit_test(rect, p),
|
||||
Some(AIChatHit::CopyDesignBlock(code.to_string()))
|
||||
);
|
||||
=======
|
||||
// Shared paint-assertion infrastructure — also used by the
|
||||
// sibling `tests_transcript` module (split at the 800-line cap).
|
||||
#[derive(Default)]
|
||||
|
|
@ -835,5 +493,4 @@ pub(in super::super) fn has_fill_rect(fills: &[(Rect, crate::Color)], expected:
|
|||
&& (rect.size.x - expected.size.x).abs() < 1e-4
|
||||
&& (rect.size.y - expected.size.y).abs() < 1e-4
|
||||
})
|
||||
>>>>>>> 926f84b6 (feat(editor-ui): canvas + panel widget batch for TS parity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,21 @@
|
|||
use super::tests::{seed_available_model, toolbar_center_y};
|
||||
use super::*;
|
||||
|
||||
fn assert_close(actual: f32, expected: f32) {
|
||||
pub(in super::super) fn assert_close(actual: f32, expected: f32) {
|
||||
assert!(
|
||||
(actual - expected).abs() < 1e-4,
|
||||
"expected {actual} to be close to {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
fn rect_close(actual: Rect, expected: Rect) -> bool {
|
||||
pub(in super::super) fn rect_close(actual: Rect, expected: Rect) -> bool {
|
||||
(actual.origin.x - expected.origin.x).abs() < 0.01
|
||||
&& (actual.origin.y - expected.origin.y).abs() < 0.01
|
||||
&& (actual.size.x - expected.size.x).abs() < 0.01
|
||||
&& (actual.size.y - expected.size.y).abs() < 0.01
|
||||
}
|
||||
|
||||
fn color_close(actual: crate::Color, expected: crate::Color) -> bool {
|
||||
pub(in super::super) fn color_close(actual: crate::Color, expected: crate::Color) -> bool {
|
||||
(actual.r - expected.r).abs() < 0.001
|
||||
&& (actual.g - expected.g).abs() < 0.001
|
||||
&& (actual.b - expected.b).abs() < 0.001
|
||||
|
|
@ -217,6 +217,35 @@ fn paint_model_picker_hover_stays_inside_model_chip() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footer_selection_count_sits_close_to_agent_team_chip() {
|
||||
let mut s = EditorState::new();
|
||||
seed_available_model(&mut s);
|
||||
s.selection.set = vec![op_editor_core::NodeId::new("n1")];
|
||||
s.selection.anchor = op_editor_core::NodeId::new("n1");
|
||||
let panel = AIChatPlaceholder::from_editor(&s);
|
||||
let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT);
|
||||
let input = panel.input_rect(rect);
|
||||
let toolbar_top = input.origin.y + INPUT_AREA_HEIGHT;
|
||||
let footer = panel.footer_layout(rect, input, toolbar_top);
|
||||
let mut backend = PanelPaintBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
let selected_text =
|
||||
op_i18n::translate(panel.locale, "common.selected").replace("{{count}}", "1");
|
||||
let (_, _, _, origin) = backend
|
||||
.texts
|
||||
.iter()
|
||||
.find(|(text, _, _, _)| text == &selected_text)
|
||||
.expect("footer should paint selected-count label");
|
||||
let gap = origin.x - (footer.agent_team.origin.x + footer.agent_team.size.x);
|
||||
assert_close(gap, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_expanded_header_title_hover_adds_visible_feedback_across_label() {
|
||||
let mut s = EditorState::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue