fix(editor): restore chat toolbar chrome polish
- run the divider above the input across the full panel width - paint the built-in model key with the real lucide Key glyph instead of a hand-rolled ring+shaft approximation - concurrency chip rests as a faint ghost at 1x and gets a primary-tinted chip when the agent team is staffed, keeping the footer hover + cycle wiring - attach/send are ghost icons; send fades to muted-foreground/30 when there is nothing to send Re-lands 94b9aa1b (dropped during a re-batch) merged with the agent-team footer layout.
This commit is contained in:
parent
09e1f5f606
commit
79f2a13ae1
|
|
@ -568,24 +568,15 @@ fn with_alpha(color: Color, a: f32) -> Color {
|
|||
}
|
||||
|
||||
pub(crate) fn paint_key_glyph(cx: &mut PaintCx<'_>, top_left: Point2D, size: f32, color: Color) {
|
||||
let cy = top_left.y + size * 0.5;
|
||||
let ring = Rect {
|
||||
origin: Point2D::new(top_left.x, cy - size * 0.28),
|
||||
size: Point2D::new(size * 0.55, size * 0.55),
|
||||
};
|
||||
cx.backend
|
||||
.stroke_round_rect(ring, ring.size.x / 2.0, color, 1.3);
|
||||
cx.backend.stroke_line(
|
||||
Point2D::new(top_left.x + size * 0.52, cy),
|
||||
Point2D::new(top_left.x + size, cy),
|
||||
// Use the real lucide `Key` glyph (TS renders `<Key/>`) rather than a
|
||||
// hand-rolled ring+shaft approximation.
|
||||
crate::widgets::icons::draw_icon(
|
||||
cx.backend,
|
||||
crate::widgets::icons::Icon::Key,
|
||||
top_left,
|
||||
size,
|
||||
color,
|
||||
1.3,
|
||||
);
|
||||
cx.backend.stroke_line(
|
||||
Point2D::new(top_left.x + size * 0.82, cy),
|
||||
Point2D::new(top_left.x + size * 0.82, cy + size * 0.25),
|
||||
color,
|
||||
1.3,
|
||||
1.4,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -555,19 +555,50 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
1.4,
|
||||
);
|
||||
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.
|
||||
let chip = footer.agent_team;
|
||||
cx.backend.fill_round_rect(chip, 6.0, self.theme.muted);
|
||||
if self.footer_hover == Some(ChatFooterButton::AgentTeam) {
|
||||
cx.backend
|
||||
.fill_round_rect(chip, 6.0, chat_neutral_hover_color(&self.theme));
|
||||
let team_size = self.state.agent_team_size;
|
||||
let team_active = team_size > 1;
|
||||
let conc_color = if team_active {
|
||||
self.theme.primary
|
||||
} else {
|
||||
Color {
|
||||
a: 0.5,
|
||||
..self.theme.muted_foreground
|
||||
}
|
||||
};
|
||||
let primary_wash = Color {
|
||||
a: 0.1,
|
||||
..self.theme.primary
|
||||
};
|
||||
if team_active {
|
||||
cx.backend.fill_round_rect(chip, 6.0, primary_wash);
|
||||
}
|
||||
let team_label = format!("{}x", self.state.agent_team_size);
|
||||
if self.footer_hover == Some(ChatFooterButton::AgentTeam) {
|
||||
let wash = if team_active {
|
||||
primary_wash
|
||||
} else {
|
||||
chat_neutral_hover_color(&self.theme)
|
||||
};
|
||||
cx.backend.fill_round_rect(chip, 6.0, wash);
|
||||
}
|
||||
draw_icon(
|
||||
cx.backend,
|
||||
Icon::Zap,
|
||||
Point2D::new(chip.origin.x + 4.0, chip.origin.y + 5.0),
|
||||
10.0,
|
||||
conc_color,
|
||||
1.4,
|
||||
);
|
||||
let team_label = format!("{}x", team_size);
|
||||
draw_label(
|
||||
cx,
|
||||
&team_label,
|
||||
11.0,
|
||||
self.theme.muted_foreground,
|
||||
chip.origin.x + 7.0,
|
||||
conc_color,
|
||||
chip.origin.x + 16.0,
|
||||
chip.origin.y + 14.0,
|
||||
);
|
||||
model_x += 36.0;
|
||||
|
|
@ -583,9 +614,9 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
toolbar_center_y + 4.0,
|
||||
);
|
||||
|
||||
// Right cluster — attach + send (TS ghost buttons: bare icons
|
||||
// that only get a wash while hovered).
|
||||
let attach_rect = footer.attach;
|
||||
cx.backend
|
||||
.fill_round_rect(attach_rect, 6.0, self.theme.muted);
|
||||
if self.footer_hover == Some(ChatFooterButton::AddAttachment) {
|
||||
cx.backend
|
||||
.fill_round_rect(attach_rect, 6.0, chat_neutral_hover_color(&self.theme));
|
||||
|
|
@ -604,26 +635,26 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
let send_active = can_use_model
|
||||
&& (!self.state.input.trim().is_empty() || !self.state.pending_attachments.is_empty());
|
||||
let streaming = self.is_streaming();
|
||||
let (send_bg, icon_color, send_icon) = if streaming {
|
||||
(
|
||||
self.theme.destructive,
|
||||
self.theme.primary_foreground,
|
||||
Icon::Square,
|
||||
)
|
||||
// TS: streaming → red stop; sendable → primary; otherwise a faded
|
||||
// muted-foreground/30 icon so the send button reads as disabled.
|
||||
let (icon_color, send_icon) = if streaming {
|
||||
(self.theme.destructive, Icon::Square)
|
||||
} else if send_active {
|
||||
(self.theme.primary, Icon::Send)
|
||||
} else {
|
||||
(
|
||||
self.theme.primary,
|
||||
self.theme.primary_foreground,
|
||||
Color {
|
||||
a: 0.3,
|
||||
..self.theme.muted_foreground
|
||||
},
|
||||
Icon::Send,
|
||||
)
|
||||
} else {
|
||||
(self.theme.muted, self.theme.muted_foreground, Icon::Send)
|
||||
};
|
||||
cx.backend.fill_round_rect(send_rect, 6.0, send_bg);
|
||||
if self.footer_hover == Some(ChatFooterButton::Send)
|
||||
|| self.footer_hover == Some(ChatFooterButton::Stop)
|
||||
{
|
||||
paint_footer_hover_overlay(cx, send_rect, send_active || streaming, &self.theme);
|
||||
cx.backend
|
||||
.fill_round_rect(send_rect, 6.0, chat_neutral_hover_color(&self.theme));
|
||||
}
|
||||
draw_icon(
|
||||
cx.backend,
|
||||
|
|
@ -674,20 +705,6 @@ fn draw_label(cx: &mut PaintCx<'_>, text: &str, size: f32, color: Color, x: f32,
|
|||
cx.backend.draw_text(&label, Point2D::new(x, y));
|
||||
}
|
||||
|
||||
fn paint_footer_hover_overlay(cx: &mut PaintCx<'_>, rect: Rect, bright: bool, theme: &Theme) {
|
||||
let color = if bright {
|
||||
Color {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
a: 0.14,
|
||||
}
|
||||
} else {
|
||||
chat_neutral_hover_color(theme)
|
||||
};
|
||||
cx.backend.fill_round_rect(rect, 6.0, color);
|
||||
}
|
||||
|
||||
fn chat_neutral_hover_color(theme: &Theme) -> Color {
|
||||
Color {
|
||||
r: theme.foreground.r,
|
||||
|
|
|
|||
|
|
@ -371,9 +371,11 @@ fn paint_send_button_hover_adds_visible_feedback() {
|
|||
.filter(|(r, _, _)| rect_close(*r, send_rect))
|
||||
.collect();
|
||||
|
||||
// Ghost button: no resting fill, so hover feedback is the only
|
||||
// wash painted over the send rect.
|
||||
assert!(
|
||||
fills.len() >= 2,
|
||||
"hovered send button should paint feedback over its base fill"
|
||||
!fills.is_empty(),
|
||||
"hovered send button should paint a hover wash"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -839,6 +841,7 @@ struct PanelPaintBackend {
|
|||
round_rects: Vec<(Rect, f32, crate::Color)>,
|
||||
texts: Vec<(String, f32, jian_core::scene::Color, Point2D)>,
|
||||
svg_strokes: Vec<(Point2D, f32, crate::Color, f32)>,
|
||||
svg_paths: Vec<String>,
|
||||
stroke_lines: usize,
|
||||
}
|
||||
|
||||
|
|
@ -868,12 +871,13 @@ impl crate::RenderBackend for PanelPaintBackend {
|
|||
fn stroke_round_rect(&mut self, _: Rect, _: f32, _: crate::Color, _: f32) {}
|
||||
fn stroke_svg_path(
|
||||
&mut self,
|
||||
_: &str,
|
||||
d: &str,
|
||||
top_left: Point2D,
|
||||
size: f32,
|
||||
color: crate::Color,
|
||||
width: f32,
|
||||
) {
|
||||
self.svg_paths.push(d.to_string());
|
||||
self.svg_strokes.push((top_left, size, color, width));
|
||||
}
|
||||
fn resize(&mut self, _: u32, _: u32) {}
|
||||
|
|
@ -903,9 +907,12 @@ fn paint_model_chip_uses_key_glyph_for_builtin_model() {
|
|||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
let key_paths = crate::widgets::icons::Icon::Key.paths();
|
||||
assert!(
|
||||
backend.stroke_lines >= 2,
|
||||
"built-in selected model chip should paint the TS-style Key glyph"
|
||||
key_paths
|
||||
.iter()
|
||||
.all(|kp| backend.svg_paths.iter().any(|p| p == kp)),
|
||||
"built-in selected model chip should paint the lucide Key glyph"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,10 +119,10 @@ pub(crate) fn paint_panel_body_chrome(cx: &mut PaintCx<'_>, theme: &Theme, rect:
|
|||
with_alpha(theme.background, 0.8),
|
||||
);
|
||||
}
|
||||
cx.backend.fill_rect(
|
||||
Rect::xywh(rect.origin.x + PAD, sep_y, rect.size.x - PAD * 2.0, 1.0),
|
||||
theme.border,
|
||||
);
|
||||
// Full-width divider above the input (TS `border-t` spans the whole
|
||||
// panel, not just the padded content column).
|
||||
cx.backend
|
||||
.fill_rect(Rect::xywh(inner_x, sep_y, inner_w, 1.0), theme.border);
|
||||
}
|
||||
|
||||
/// Paint the empty-state hint line + the 2×2 example-card grid.
|
||||
|
|
|
|||
Loading…
Reference in a new issue