169 lines
5.4 KiB
Rust
169 lines
5.4 KiB
Rust
|
|
//! Coding-data sharing upsell banner (Figma "Data Sharing Upsell",
|
||
|
|
//! node 8698:3690). Shared by the welcome tip slot and the agent-view
|
||
|
|
//! banner slot; visibility is gated by `AppView::privacy_banner_should_show`.
|
||
|
|
|
||
|
|
use crate::theme::Theme;
|
||
|
|
use ratatui::buffer::Buffer;
|
||
|
|
use ratatui::layout::Rect;
|
||
|
|
use ratatui::style::{Modifier, Style};
|
||
|
|
use ratatui::text::{Line, Span};
|
||
|
|
use ratatui::widgets::{Paragraph, Widget};
|
||
|
|
|
||
|
|
/// Legal line copy — used for both render spans and mouse hit width.
|
||
|
|
const PRIVACY_BANNER_LEGAL: &str = "Learn more and read Terms and Privacy Policy.";
|
||
|
|
|
||
|
|
/// Click target for the legal line links.
|
||
|
|
pub(crate) const PRIVACY_BANNER_LEGAL_URL: &str = "https://x.ai/legal";
|
||
|
|
|
||
|
|
/// Hit rects returned by [`render`] for mouse handling.
|
||
|
|
pub(crate) struct PrivacyBannerRects {
|
||
|
|
pub accept: Rect,
|
||
|
|
pub customize: Rect,
|
||
|
|
pub legal: Rect,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Render the banner: copy left, `[Customize in settings]` / `[Accept]`
|
||
|
|
/// right, legal links on the second row. Needs `area.height >= 2`.
|
||
|
|
/// Hover styling mirrors the plugin CTA buttons.
|
||
|
|
pub(crate) fn render(
|
||
|
|
area: Rect,
|
||
|
|
buf: &mut Buffer,
|
||
|
|
theme: &Theme,
|
||
|
|
mouse_pos: Option<(u16, u16)>,
|
||
|
|
) -> PrivacyBannerRects {
|
||
|
|
let customize_label = "[Customize in settings]";
|
||
|
|
let accept_label = "[Accept]";
|
||
|
|
let right_w = (customize_label.len() + 1 + accept_label.len()) as u16;
|
||
|
|
// Buttons render whole or not at all: a clipped/overflowing [Accept]
|
||
|
|
// must never leave a click target in the blank margin (a stray click
|
||
|
|
// there would silently opt the user in).
|
||
|
|
let buttons_fit = area.width > right_w;
|
||
|
|
let left_w = if buttons_fit {
|
||
|
|
area.width - right_w - 1
|
||
|
|
} else {
|
||
|
|
area.width
|
||
|
|
};
|
||
|
|
|
||
|
|
let left = Rect {
|
||
|
|
x: area.x,
|
||
|
|
y: area.y,
|
||
|
|
width: left_w,
|
||
|
|
height: area.height.min(2),
|
||
|
|
};
|
||
|
|
let right = Rect {
|
||
|
|
x: area.x + left_w + 1,
|
||
|
|
y: area.y,
|
||
|
|
width: right_w,
|
||
|
|
height: 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
let hovered = |r: Rect| {
|
||
|
|
mouse_pos.is_some_and(|(mx, my)| r.contains(ratatui::layout::Position::new(mx, my)))
|
||
|
|
};
|
||
|
|
|
||
|
|
let legal_w = if left.width as usize >= PRIVACY_BANNER_LEGAL.len() {
|
||
|
|
PRIVACY_BANNER_LEGAL.len()
|
||
|
|
} else {
|
||
|
|
"Learn more".len().min(left.width as usize)
|
||
|
|
};
|
||
|
|
// The legal line only exists when the slot really has a second row —
|
||
|
|
// otherwise its rect would make the blank row below clickable.
|
||
|
|
let legal_rect = if area.height >= 2 {
|
||
|
|
Rect {
|
||
|
|
x: left.x,
|
||
|
|
y: left.y.saturating_add(1),
|
||
|
|
width: legal_w as u16,
|
||
|
|
height: 1,
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Rect::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
// Figma node 8698:3806: title fg/primary, description fg/secondary,
|
||
|
|
// legal line fg/tertiary with underlined links in the same color.
|
||
|
|
// The whole legal line is one click target, so its links brighten together.
|
||
|
|
let link_fg = if hovered(legal_rect) {
|
||
|
|
theme.gray_bright
|
||
|
|
} else {
|
||
|
|
theme.gray
|
||
|
|
};
|
||
|
|
let link = Style::default()
|
||
|
|
.fg(link_fg)
|
||
|
|
.add_modifier(Modifier::UNDERLINED);
|
||
|
|
let gray = Style::default().fg(theme.gray);
|
||
|
|
let title = Span::styled("Help improve Grok", Style::default().fg(theme.text_primary));
|
||
|
|
let desc = "Allow your sessions to improve SpaceXAI's models.";
|
||
|
|
// Drop trailing spans whole rather than clipping mid-word when narrow.
|
||
|
|
let line1 = if left.width as usize >= "Help improve Grok ".len() + desc.len() {
|
||
|
|
Line::from(vec![
|
||
|
|
title,
|
||
|
|
Span::raw(" "),
|
||
|
|
Span::styled(desc, Style::default().fg(theme.gray_bright)),
|
||
|
|
])
|
||
|
|
} else {
|
||
|
|
Line::from(title)
|
||
|
|
};
|
||
|
|
// Span pieces must reassemble to PRIVACY_BANNER_LEGAL.
|
||
|
|
let line2 = if left.width as usize >= PRIVACY_BANNER_LEGAL.len() {
|
||
|
|
Line::from(vec![
|
||
|
|
Span::styled("Learn more", link),
|
||
|
|
Span::styled(" and read ", gray),
|
||
|
|
Span::styled("Terms", link),
|
||
|
|
Span::styled(" and ", gray),
|
||
|
|
Span::styled("Privacy Policy", link),
|
||
|
|
Span::styled(".", gray),
|
||
|
|
])
|
||
|
|
} else {
|
||
|
|
Line::from(Span::styled("Learn more", link))
|
||
|
|
};
|
||
|
|
Paragraph::new(vec![line1, line2]).render(left, buf);
|
||
|
|
|
||
|
|
if !buttons_fit {
|
||
|
|
return PrivacyBannerRects {
|
||
|
|
accept: Rect::default(),
|
||
|
|
customize: Rect::default(),
|
||
|
|
legal: legal_rect,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
let customize_rect = Rect {
|
||
|
|
x: right.x,
|
||
|
|
y: right.y,
|
||
|
|
width: customize_label.len() as u16,
|
||
|
|
height: 1,
|
||
|
|
};
|
||
|
|
let accept_rect = Rect {
|
||
|
|
x: right.x + customize_label.len() as u16 + 1,
|
||
|
|
y: right.y,
|
||
|
|
width: accept_label.len() as u16,
|
||
|
|
height: 1,
|
||
|
|
};
|
||
|
|
let customize_style = if hovered(customize_rect) {
|
||
|
|
Style::default().fg(theme.text_primary).bg(theme.bg_hover)
|
||
|
|
} else {
|
||
|
|
Style::default().fg(theme.gray_bright)
|
||
|
|
};
|
||
|
|
let accept_style = if hovered(accept_rect) {
|
||
|
|
Style::default().fg(theme.link_fg).bg(theme.bg_hover)
|
||
|
|
} else {
|
||
|
|
Style::default().fg(theme.text_primary)
|
||
|
|
};
|
||
|
|
buf.set_stringn(
|
||
|
|
customize_rect.x,
|
||
|
|
customize_rect.y,
|
||
|
|
customize_label,
|
||
|
|
customize_rect.width as usize,
|
||
|
|
customize_style,
|
||
|
|
);
|
||
|
|
buf.set_stringn(
|
||
|
|
accept_rect.x,
|
||
|
|
accept_rect.y,
|
||
|
|
accept_label,
|
||
|
|
accept_rect.width as usize,
|
||
|
|
accept_style,
|
||
|
|
);
|
||
|
|
PrivacyBannerRects {
|
||
|
|
accept: accept_rect,
|
||
|
|
customize: customize_rect,
|
||
|
|
legal: legal_rect,
|
||
|
|
}
|
||
|
|
}
|