use super::types::{ThemeInfoPopup, ThemeKeyInfo};
use super::Editor;
use crate::services::plugins::hooks::HookArgs;
use crate::view::theme::color_to_rgb;
use anyhow::Result as AnyhowResult;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
impl Editor {
pub(super) fn show_theme_info_popup(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
if let Some(info) = self.resolve_theme_key_at(col, row) {
self.active_window_mut().mouse_state.lsp_hover_state = None;
self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
self.dismiss_transient_popups();
let popup_x = col.saturating_add(1);
let popup_y = row.saturating_add(1);
self.active_window_mut().theme_info_popup = Some(ThemeInfoPopup {
position: (popup_x, popup_y),
info,
button_highlighted: false,
});
}
Ok(())
}
pub(super) fn fire_theme_inspect_hook(&mut self, key: String) {
let theme_name = self
.theme_registry
.resolve_key(&self.config.theme.0)
.unwrap_or_else(|| self.config.theme.0.clone());
self.plugin_manager.read().unwrap().run_hook(
"theme_inspect_key",
HookArgs::ThemeInspectKey { theme_name, key },
);
}
pub(super) fn inspect_theme_at_cursor(&mut self) {
let active_split = self
.windows
.get(&self.active_window)
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
.expect("active window must have a populated split layout")
.active_split();
let active_buffer = self.active_buffer();
let (content_rect, gutter_width, compose_width, primary_cursor) = match self
.active_layout()
.split_areas
.iter()
.find(|(sid, bid, ..)| *sid == active_split && *bid == active_buffer)
{
Some((split_id, buffer_id, rect, ..)) => {
let gw = self
.buffers()
.get(buffer_id)
.map(|s| s.margins.left_total_width() as u16)
.unwrap_or(0);
let vs = match self
.windows
.get(&self.active_window)
.and_then(|w| w.buffers.splits())
.map(|(_, vs)| vs)
.expect("active window must have a populated split layout")
.get(split_id)
{
Some(vs) => vs,
None => return,
};
(*rect, gw, vs.compose_width, *vs.cursors.primary())
}
None => return,
};
let viewport = self
.active_window()
.buffers
.splits()
.expect("active window must have a populated split layout")
.1[&active_split]
.viewport
.clone();
let state = match self.active_window_mut().buffers.get_mut(&active_buffer) {
Some(s) => s,
None => return,
};
let cursor_rel = viewport.cursor_screen_position(&mut state.buffer, &primary_cursor);
let adjusted_rect =
super::click_geometry::adjust_content_rect_for_compose(content_rect, compose_width);
let screen_col = cursor_rel.0 + adjusted_rect.x + gutter_width;
let screen_row = cursor_rel.1 + content_rect.y;
if let Some(info) = self.resolve_theme_key_at(screen_col, screen_row) {
if let Some(key) = info.fg_key {
self.fire_theme_inspect_hook(key);
}
}
}
fn resolve_theme_key_at(&self, col: u16, row: u16) -> Option<ThemeKeyInfo> {
let cell = self.active_chrome().cell_theme_at(col, row)?;
let theme = &*self.theme.read().unwrap();
let fg_color = cell
.fg_key
.as_ref()
.and_then(|k| theme.resolve_theme_key(k));
let bg_color = cell
.bg_key
.as_ref()
.and_then(|k| theme.resolve_theme_key(k));
let region = if let Some(cat) = cell.syntax_category.as_ref() {
format!("Syntax: {}", cat)
} else {
cell.region.to_string()
};
Some(ThemeKeyInfo {
fg_key: cell.fg_key.as_ref().map(|k| k.to_string()),
bg_key: cell.bg_key.as_ref().map(|k| k.to_string()),
region,
fg_color,
bg_color,
syntax_category: cell.syntax_category.as_ref().map(|c| c.to_string()),
})
}
pub(super) fn render_theme_info_popup(&self, frame: &mut Frame) {
let popup = match &self.active_window().theme_info_popup {
Some(p) => p,
None => return,
};
let theme = &*self.theme.read().unwrap();
let info = &popup.info;
let key_style = Style::default()
.fg(theme.popup_text_fg)
.add_modifier(ratatui::style::Modifier::BOLD);
let has_keys = info.fg_key.is_some() || info.bg_key.is_some();
let mut lines = vec![];
if !info.region.is_empty() {
lines.push(Line::from(format!(" Region: {}", info.region)));
lines.push(Line::from(""));
}
if !has_keys {
lines.push(Line::from(Span::styled(
" No theme key recorded here. ",
Style::default().fg(theme.popup_text_fg),
)));
lines.push(Line::from(Span::styled(
" This element isn't inspectable yet. ",
Style::default().fg(theme.menu_disabled_fg),
)));
let width = POPUP_WIDTH;
let height = lines.len() as u16 + 2;
let screen = frame.area();
let rect =
compute_popup_rect(popup.position, width, height, screen.width, screen.height);
frame.render_widget(Clear, rect);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.popup_border_fg))
.title(" Theme Info ")
.style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, rect);
return;
}
if let Some(ref fg_key) = info.fg_key {
lines.push(Line::from(vec![
Span::styled(" Foreground: ", Style::default().fg(theme.popup_text_fg)),
Span::styled(fg_key.clone(), key_style),
]));
if let Some(color) = info.fg_color {
let rgb_str = format_color_rgb(color);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("\u{2589} ", Style::default().fg(color)),
Span::raw(rgb_str),
]));
}
if let Some(ref cat) = info.syntax_category {
lines.push(Line::from(format!(" Category: {}", cat)));
}
}
lines.push(Line::from(""));
if let Some(ref bg_key) = info.bg_key {
lines.push(Line::from(vec![
Span::styled(" Background: ", Style::default().fg(theme.popup_text_fg)),
Span::styled(bg_key.clone(), key_style),
]));
if let Some(color) = info.bg_color {
let rgb_str = format_color_rgb(color);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("\u{2589} ", Style::default().fg(color)),
Span::raw(rgb_str),
]));
}
}
lines.push(Line::from(""));
let button_style = if popup.button_highlighted {
Style::default()
.fg(theme.popup_selection_fg)
.bg(theme.popup_selection_bg)
} else {
Style::default().fg(theme.popup_text_fg)
};
lines.push(Line::from(Span::styled(
" \u{25b6} Open in Theme Editor ",
button_style,
)));
let width = POPUP_WIDTH;
let height = lines.len() as u16 + 2;
let screen = frame.area();
let rect = compute_popup_rect(popup.position, width, height, screen.width, screen.height);
frame.render_widget(Clear, rect);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.popup_border_fg))
.title(" Theme Info ")
.style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, rect);
}
pub(super) fn theme_info_popup_rect(&self) -> Option<(Rect, Option<u16>)> {
let popup = self.active_window().theme_info_popup.as_ref()?;
let info = &popup.info;
let has_keys = info.fg_key.is_some() || info.bg_key.is_some();
let mut line_count: u16 = 0;
if !info.region.is_empty() {
line_count += 2; }
let button_row_offset = if has_keys {
if info.fg_key.is_some() {
line_count += 1; if info.fg_color.is_some() {
line_count += 1; }
if info.syntax_category.is_some() {
line_count += 1; }
}
line_count += 1; if info.bg_key.is_some() {
line_count += 1; if info.bg_color.is_some() {
line_count += 1; }
}
line_count += 1; line_count += 1; Some(line_count)
} else {
line_count += 2; None
};
let width = POPUP_WIDTH;
let height = line_count + 2;
let screen_w = self.active_chrome().last_frame.width;
let screen_h = self.active_chrome().last_frame.height;
let rect = compute_popup_rect(popup.position, width, height, screen_w, screen_h);
Some((rect, button_row_offset))
}
}
const POPUP_WIDTH: u16 = 40;
fn compute_popup_rect(
position: (u16, u16),
width: u16,
height: u16,
screen_w: u16,
screen_h: u16,
) -> Rect {
let x = if position.0 + width > screen_w {
screen_w.saturating_sub(width)
} else {
position.0
};
let y = if position.1 + height > screen_h {
position.1.saturating_sub(height + 1)
} else {
position.1
};
Rect::new(x, y, width.min(screen_w), height.min(screen_h))
}
fn format_color_rgb(color: Color) -> String {
if let Some((r, g, b)) = color_to_rgb(color) {
format!("RGB({}, {}, {})", r, g, b)
} else {
format!("{:?}", color)
}
}