use rio_backend::error::{RioError, RioErrorLevel};
use rio_backend::sugarloaf::text::DrawOpts;
use rio_backend::sugarloaf::Sugarloaf;
#[inline]
fn color_u8(c: [f32; 4]) -> [u8; 4] {
[
(c[0].clamp(0.0, 1.0) * 255.0) as u8,
(c[1].clamp(0.0, 1.0) * 255.0) as u8,
(c[2].clamp(0.0, 1.0) * 255.0) as u8,
(c[3].clamp(0.0, 1.0) * 255.0) as u8,
]
}
const OVERLAY_WIDTH: f32 = 480.0;
const OVERLAY_CORNER_RADIUS: f32 = 10.0;
const OVERLAY_MARGIN_TOP: f32 = 8.0;
const OVERLAY_MARGIN_RIGHT: f32 = 8.0;
const OVERLAY_PADDING: f32 = 16.0;
const HEADING_FONT_SIZE: f32 = 16.0;
const BODY_FONT_SIZE: f32 = 12.0;
const BUTTON_FONT_SIZE: f32 = 14.0;
const LINK_FONT_SIZE: f32 = 12.0;
const BUTTON_SIZE: f32 = 24.0;
const BUTTON_CORNER_RADIUS: f32 = 4.0;
const LINE_HEIGHT: f32 = 18.0;
const HEADING_HEIGHT: f32 = 28.0;
const LINK_ROW_HEIGHT: f32 = 24.0;
const MAX_VISIBLE_LINES: usize = 16;
const DOCS_URL: &str = "rioterm.com/docs/config";
const BACKDROP_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 0.35];
const BG_COLOR: [f32; 4] = [0.12, 0.12, 0.12, 0.98];
const HEADING_COLOR_ERROR: [f32; 4] = [1.0, 0.07, 0.38, 1.0];
const HEADING_COLOR_WARNING: [f32; 4] = [0.99, 0.73, 0.16, 1.0];
const TEXT_COLOR: [f32; 4] = [0.85, 0.85, 0.85, 1.0];
const LINK_COLOR: [f32; 4] = [0.40, 0.60, 1.0, 1.0];
const BUTTON_TEXT_COLOR: [f32; 4] = [0.70, 0.70, 0.70, 1.0];
const BUTTON_HOVER_BG: [f32; 4] = [0.25, 0.25, 0.28, 1.0];
const DEPTH_BACKDROP: f32 = 0.0;
const DEPTH_BG: f32 = 0.1;
const DEPTH_ELEMENT: f32 = 0.2;
const ORDER: u8 = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssistantOverlayAction {
Close,
OpenDocs,
}
pub struct AssistantOverlay {
error: Option<RioError>,
hovered_button: Option<AssistantOverlayAction>,
link_button_width: f32,
}
impl Default for AssistantOverlay {
fn default() -> Self {
Self {
error: None,
hovered_button: None,
link_button_width: 0.0,
}
}
}
impl AssistantOverlay {
#[inline]
pub fn is_active(&self) -> bool {
self.error.is_some()
}
#[inline]
pub fn set_error(&mut self, error: RioError) {
self.error = Some(error);
}
#[inline]
pub fn clear(&mut self) {
self.error = None;
}
fn overlay_rect(&self, window_width: f32, scale_factor: f32) -> (f32, f32, f32, f32) {
let logical_width = window_width / scale_factor;
let x = logical_width - OVERLAY_WIDTH - OVERLAY_MARGIN_RIGHT;
let y = OVERLAY_MARGIN_TOP;
let line_count = self.body_line_count().min(MAX_VISIBLE_LINES);
let h = OVERLAY_PADDING
+ HEADING_HEIGHT
+ (line_count as f32 * LINE_HEIGHT)
+ LINK_ROW_HEIGHT
+ OVERLAY_PADDING;
(x, y, OVERLAY_WIDTH, h)
}
fn body_line_count(&self) -> usize {
if let Some(error) = &self.error {
error.report.to_string().lines().count().max(1)
} else {
0
}
}
fn close_button_rect(
&self,
overlay_x: f32,
overlay_y: f32,
overlay_width: f32,
) -> (f32, f32, f32, f32) {
let bx = overlay_x + overlay_width - OVERLAY_PADDING - BUTTON_SIZE;
let by = overlay_y + OVERLAY_PADDING / 2.0;
(bx, by, BUTTON_SIZE, BUTTON_SIZE)
}
fn docs_button_rect(&self, overlay_x: f32, overlay_y: f32) -> (f32, f32, f32, f32) {
let line_count = self.body_line_count().min(MAX_VISIBLE_LINES);
let by = overlay_y
+ OVERLAY_PADDING
+ HEADING_HEIGHT
+ (line_count as f32 * LINE_HEIGHT);
let bx = overlay_x + OVERLAY_PADDING - 4.0;
let bw = self.link_button_width + 8.0;
(bx, by, bw, LINK_ROW_HEIGHT)
}
#[inline]
pub fn hovered_button(&self) -> Option<AssistantOverlayAction> {
self.hovered_button
}
fn hit_test_button(
mouse_x: f32,
mouse_y: f32,
bx: f32,
by: f32,
bw: f32,
bh: f32,
) -> bool {
mouse_x >= bx && mouse_x <= bx + bw && mouse_y >= by && mouse_y <= by + bh
}
pub fn hit_test(
&self,
mouse_x: f32,
mouse_y: f32,
window_width: f32,
scale_factor: f32,
) -> Result<Option<AssistantOverlayAction>, ()> {
if !self.is_active() {
return Err(());
}
let (ox, oy, ow, oh) = self.overlay_rect(window_width, scale_factor);
if mouse_x < ox || mouse_x > ox + ow || mouse_y < oy || mouse_y > oy + oh {
return Err(());
}
let (bx, by, bw, bh) = self.close_button_rect(ox, oy, ow);
if Self::hit_test_button(mouse_x, mouse_y, bx, by, bw, bh) {
return Ok(Some(AssistantOverlayAction::Close));
}
let (bx, by, bw, bh) = self.docs_button_rect(ox, oy);
if Self::hit_test_button(mouse_x, mouse_y, bx, by, bw, bh) {
return Ok(Some(AssistantOverlayAction::OpenDocs));
}
Ok(None)
}
pub fn hover(
&mut self,
mouse_x: f32,
mouse_y: f32,
window_width: f32,
scale_factor: f32,
) -> bool {
if !self.is_active() {
return false;
}
let (ox, oy, ow, _oh) = self.overlay_rect(window_width, scale_factor);
let (bx, by, bw, bh) = self.close_button_rect(ox, oy, ow);
let mut new_hover = if Self::hit_test_button(mouse_x, mouse_y, bx, by, bw, bh) {
Some(AssistantOverlayAction::Close)
} else {
None
};
if new_hover.is_none() {
let (bx, by, bw, bh) = self.docs_button_rect(ox, oy);
if Self::hit_test_button(mouse_x, mouse_y, bx, by, bw, bh) {
new_hover = Some(AssistantOverlayAction::OpenDocs);
}
}
if new_hover != self.hovered_button {
self.hovered_button = new_hover;
return true;
}
false
}
pub fn render(&mut self, sugarloaf: &mut Sugarloaf, dimensions: (f32, f32, f32)) {
if !self.is_active() {
return;
}
let (window_width, window_height, scale_factor) = dimensions;
let (ox, oy, ow, oh) = self.overlay_rect(window_width, scale_factor);
sugarloaf.rect(
None,
0.0,
0.0,
window_width / scale_factor,
window_height / scale_factor,
BACKDROP_COLOR,
DEPTH_BACKDROP,
ORDER,
);
sugarloaf.rounded_rect(
None,
ox,
oy,
ow,
oh,
BG_COLOR,
DEPTH_BG,
OVERLAY_CORNER_RADIUS,
ORDER,
);
let error = self.error.clone().unwrap();
let is_error = error.level == RioErrorLevel::Error;
let heading_color = if is_error {
HEADING_COLOR_ERROR
} else {
HEADING_COLOR_WARNING
};
let heading_text = if is_error { "Error" } else { "Warning" };
let text_x = ox + OVERLAY_PADDING;
let heading_y = oy + OVERLAY_PADDING;
let heading_opts = DrawOpts {
font_size: HEADING_FONT_SIZE,
color: color_u8(heading_color),
..DrawOpts::default()
};
sugarloaf
.text_mut()
.draw(text_x, heading_y, heading_text, &heading_opts);
let body_y_start = heading_y + HEADING_HEIGHT;
let report_text = error.report.to_string();
let lines: Vec<&str> = report_text.lines().collect();
let visible_count = lines.len().min(MAX_VISIBLE_LINES);
let body_opts = DrawOpts {
font_size: BODY_FONT_SIZE,
color: color_u8(TEXT_COLOR),
..DrawOpts::default()
};
for (i, line_text) in lines.iter().take(visible_count).enumerate() {
let line_y = body_y_start + (i as f32 * LINE_HEIGHT);
sugarloaf
.text_mut()
.draw(text_x, line_y, line_text, &body_opts);
}
let line_count = visible_count;
let link_area_y =
oy + OVERLAY_PADDING + HEADING_HEIGHT + (line_count as f32 * LINE_HEIGHT);
let link_x = ox + OVERLAY_PADDING;
let link_y = link_area_y + (LINK_ROW_HEIGHT - LINK_FONT_SIZE) / 2.0;
let link_opts = DrawOpts {
font_size: LINK_FONT_SIZE,
color: color_u8(LINK_COLOR),
..DrawOpts::default()
};
let rendered_width = sugarloaf
.text_mut()
.draw(link_x, link_y, DOCS_URL, &link_opts);
self.link_button_width = rendered_width;
let (dbx, dby, dbw, dbh) = self.docs_button_rect(ox, oy);
let docs_hovered = self.hovered_button == Some(AssistantOverlayAction::OpenDocs);
if docs_hovered {
sugarloaf.rounded_rect(
None,
dbx,
dby,
dbw,
dbh,
BUTTON_HOVER_BG,
DEPTH_ELEMENT,
BUTTON_CORNER_RADIUS,
ORDER,
);
}
let (bx, by, bw, bh) = self.close_button_rect(ox, oy, ow);
let is_hovered = self.hovered_button == Some(AssistantOverlayAction::Close);
if is_hovered {
sugarloaf.rounded_rect(
None,
bx,
by,
bw,
bh,
BUTTON_HOVER_BG,
DEPTH_ELEMENT,
BUTTON_CORNER_RADIUS,
ORDER,
);
}
let close_opts = DrawOpts {
font_size: BUTTON_FONT_SIZE,
color: color_u8(BUTTON_TEXT_COLOR),
..DrawOpts::default()
};
let ui = sugarloaf.text_mut();
let label_w = ui.measure("\u{2022}", &close_opts);
let label_x = bx + (bw - label_w) / 2.0;
let label_y = by + (bh - BUTTON_FONT_SIZE) / 2.0;
ui.draw(label_x, label_y, "\u{2022}", &close_opts);
}
}