mod color;
mod state;
pub use state::WelcomePanelState;
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
use crate::formatters::style_tokens;
use crate::widgets::spinner::SPINNER_FRAMES;
use color::hsl_to_rgb;
pub struct WelcomePanelWidget<'a> {
state: &'a WelcomePanelState,
version: &'a str,
mode: &'a str,
}
impl<'a> WelcomePanelWidget<'a> {
pub fn new(state: &'a WelcomePanelState) -> Self {
Self {
state,
version: env!("CARGO_PKG_VERSION"),
mode: "NORMAL",
}
}
pub fn version(mut self, version: &'a str) -> Self {
self.version = version;
self
}
pub fn mode(mut self, mode: &'a str) -> Self {
self.mode = mode;
self
}
#[inline]
fn put(buf: &mut Buffer, area: Rect, x: u16, y: u16, ch: char, fg: Color) {
if x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height {
let cell = buf.cell_mut((x, y)).unwrap();
cell.set_char(ch);
cell.set_fg(fg);
}
}
fn write_gradient_line(
&self,
buf: &mut Buffer,
area: Rect,
y: u16,
text: &str,
line_offset: u16,
) {
let text_len = text.chars().count() as u16;
let start_x = area.x + area.width.saturating_sub(text_len) / 2;
for (i, ch) in text.chars().enumerate() {
if ch == ' ' {
continue;
}
let sweep = (i as u16 * 3 + line_offset * 10 + self.state.gradient_offset) % 360;
let hue = 190.0 + (sweep as f64 / 360.0) * 60.0; let sat = 0.75 * self.state.fade_progress as f64;
let lit = 0.55 * self.state.fade_progress as f64
+ 0.1 * (1.0 - self.state.fade_progress as f64);
let color = hsl_to_rgb(hue, sat, lit);
Self::put(buf, area, start_x + i as u16, y, ch, color);
}
}
fn draw_border(&self, buf: &mut Buffer, area: Rect, bx: u16, by: u16, bw: u16, bh: u16) {
let offset = self.state.gradient_offset;
let fade = self.state.fade_progress as f64;
let perimeter = 2 * (bw + bh);
let border_color = |idx: u16| -> Color {
let t = ((idx as f64 / perimeter as f64) + offset as f64 / 360.0) % 1.0;
let hue = 195.0 + t * 50.0; hsl_to_rgb(hue, 0.6 * fade, 0.35 * fade + 0.08 * (1.0 - fade))
};
Self::put(
buf,
area,
bx,
by,
style_tokens::BOX_TL.chars().next().unwrap(),
border_color(0),
);
for i in 1..bw - 1 {
Self::put(
buf,
area,
bx + i,
by,
style_tokens::BOX_H.chars().next().unwrap(),
border_color(i),
);
}
Self::put(
buf,
area,
bx + bw - 1,
by,
style_tokens::BOX_TR.chars().next().unwrap(),
border_color(bw),
);
Self::put(
buf,
area,
bx,
by + bh - 1,
style_tokens::BOX_BL.chars().next().unwrap(),
border_color(bw + bh),
);
for i in 1..bw - 1 {
Self::put(
buf,
area,
bx + i,
by + bh - 1,
style_tokens::BOX_H.chars().next().unwrap(),
border_color(bw + bh + i),
);
}
Self::put(
buf,
area,
bx + bw - 1,
by + bh - 1,
style_tokens::BOX_BR.chars().next().unwrap(),
border_color(2 * bw + bh),
);
let v = style_tokens::BOX_V.chars().next().unwrap();
for j in 1..bh - 1 {
Self::put(buf, area, bx, by + j, v, border_color(bw + j));
Self::put(
buf,
area,
bx + bw - 1,
by + j,
v,
border_color(2 * bw + bh + j),
);
}
}
const LOGO_LINES: [&'static str; 3] = [
"─────────────────────────────",
" O P E N D E V ",
"─────────────────────────────",
];
const LOGO_WIDTH: usize = 29;
const LOGO_HEIGHT: usize = 3;
fn render_rain(&self, buf: &mut Buffer, area: Rect, rx: u16, ry: u16, rw: usize, rh: usize) {
let fade = self.state.fade_progress as f64;
let base_hue = 200.0;
let show_block = rh >= 5 && rw >= Self::LOGO_WIDTH;
let compact_logo = "O p e n D e v";
let compact_len = compact_logo.len();
let show_compact = !show_block && rw >= compact_len;
let bl_start_col = rw.saturating_sub(Self::LOGO_WIDTH) / 2;
let bl_end_col = bl_start_col + Self::LOGO_WIDTH;
let bl_start_row = rh.saturating_sub(Self::LOGO_HEIGHT) / 2;
let bl_end_row = bl_start_row + Self::LOGO_HEIGHT;
let cl_start_col = rw.saturating_sub(compact_len) / 2;
let cl_end_col = cl_start_col + compact_len;
let cl_row = rh / 2;
for (col_idx, rain_col) in self.state.rain_columns.iter().enumerate().take(rw) {
let head_y = rain_col.y as i32;
let trail = rain_col.trail_len as i32;
let col_hue = base_hue + rain_col.hue_offset as f64;
for row in 0..rh {
let row_i = row as i32;
let dist = head_y - row_i;
if show_block
&& row >= bl_start_row
&& row < bl_end_row
&& col_idx >= bl_start_col
&& col_idx < bl_end_col
{
let logo_r = row - bl_start_row;
let logo_c = col_idx - bl_start_col;
if let Some(ch) = Self::LOGO_LINES[logo_r].chars().nth(logo_c)
&& ch != ' '
{
let is_frame = ch == '─';
if is_frame {
let color = hsl_to_rgb(210.0, 0.5 * fade, 0.22 * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, ch, color);
} else {
let letter_t = logo_c as f64 / Self::LOGO_WIDTH as f64;
let letter_hue = 190.0 + letter_t * 40.0;
let breathe = 0.35 + 0.25 * (1.0 + self.state.breathe_phase.sin());
let color = hsl_to_rgb(letter_hue, 0.9 * fade, breathe * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, ch, color);
}
}
continue;
}
if show_compact && row == cl_row && col_idx >= cl_start_col && col_idx < cl_end_col
{
let ch_idx = col_idx - cl_start_col;
let ch = compact_logo.as_bytes()[ch_idx] as char;
if ch != ' ' {
let letter_t = ch_idx as f64 / compact_len as f64;
let letter_hue = 190.0 + letter_t * 40.0;
let breathe = 0.30 + 0.22 * (1.0 + self.state.breathe_phase.sin());
let color = hsl_to_rgb(letter_hue, 0.85 * fade, breathe * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, ch, color);
}
continue;
}
if dist >= 0 && dist <= trail {
let t = dist as f64 / trail as f64;
if dist == 0 {
let color = hsl_to_rgb(col_hue, 0.5 * fade, 0.75 * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, '▓', color);
} else if dist == 1 {
let color = hsl_to_rgb(col_hue, 0.75 * fade, 0.55 * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, '░', color);
} else {
let lightness = 0.45 * (1.0 - t) + 0.10 * t;
let frame_idx =
(rain_col.char_offset as usize + row + self.state.braille_offset)
% SPINNER_FRAMES.len();
let ch = SPINNER_FRAMES[frame_idx];
let color = hsl_to_rgb(col_hue, 0.8 * fade, lightness * fade);
Self::put(buf, area, rx + col_idx as u16, ry + row as u16, ch, color);
}
}
}
}
}
}
impl Widget for WelcomePanelWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 10 || area.height < 3 {
return;
}
let line1 = format!(
"\u{2550}\u{2550}\u{2550} O P E N D E V v{} \u{2550}\u{2550}\u{2550}",
self.version
);
let line3 =
"/help \u{2502} /models \u{2502} Shift+Tab plan mode \u{2502} @file context";
if area.height < 5 {
let cy = area.y + area.height / 2;
self.write_gradient_line(buf, area, cy, &line1, 0);
} else if area.height < 11 {
let box_w = (area.width.saturating_sub(4)).min(90);
let box_h = area.height.min(7);
let bx = area.x + (area.width.saturating_sub(box_w)) / 2;
let by = area.y + (area.height.saturating_sub(box_h)) / 2;
self.draw_border(buf, area, bx, by, box_w, box_h);
let title_y = by + 2;
self.write_gradient_line(buf, area, title_y, &line1, 0);
if box_h > 4 {
let help_y = by + box_h.saturating_sub(2);
self.write_gradient_line(buf, area, help_y, line3, 2);
}
} else {
let box_w = (area.width.saturating_sub(4)).min(90);
let box_h = 5u16;
let has_rain = !self.state.rain_columns.is_empty();
let rain_h = if has_rain {
(area.height.saturating_sub(box_h + 2)).clamp(4, 20) as usize
} else {
0
};
let rain_w = ((box_w as f32 * 0.7) as usize).clamp(20, 90);
let total_h = rain_h as u16 + box_h;
let start_y = area.y + area.height.saturating_sub(total_h) / 2;
let center_x = area.x + (area.width.saturating_sub(box_w)) / 2;
if has_rain {
let rain_x = area.x + (area.width.saturating_sub(rain_w as u16)) / 2;
let rain_y = start_y;
self.render_rain(buf, area, rain_x, rain_y, rain_w, rain_h);
}
let by = start_y + rain_h as u16;
self.draw_border(buf, area, center_x, by, box_w, box_h);
let title_y = by + 1;
self.write_gradient_line(buf, area, title_y, &line1, 0);
let help_y = by + 3;
self.write_gradient_line(buf, area, help_y, line3, 2);
}
}
}
#[cfg(test)]
mod tests;