use crate::app_context::ContextManager;
use crate::input::core::coordinator::LayerId;
use crate::input::{InputCoordinator, Sense, WidgetKind};
use crate::core::render::draw_svg_icon;
use crate::render::{RenderContext, TextAlign, TextBaseline};
use crate::ui::assets::icons::ui::{ICON_CLOSE, ICON_NEW_WINDOW};
use crate::types::{Rect, WidgetId, CompositeId};
use super::settings::ChromeSettings;
use super::state::ChromeState;
use super::types::{ChromeRenderKind, ChromeTabConfig, ChromeView};
const TAB_LEFT_MARGIN: f64 = 0.0;
const NEW_TAB_BTN_WIDTH: f64 = 28.0;
const BUTTON_WIDTH: f64 = 46.0;
const CLOSE_WINDOW_BTN_WIDTH: f64 = 36.0;
const MENU_BTN_WIDTH: f64 = 36.0;
const NEW_WINDOW_BTN_WIDTH: f64 = 36.0;
struct ButtonPositions {
close_x: f64,
maximize_x: f64,
minimize_x: f64,
group_right: Option<f64>,
close_window_left: Option<f64>,
menu_left: Option<f64>,
new_window_left: Option<f64>,
}
impl ButtonPositions {
fn compute(width: f64, view: &super::types::ChromeView<'_>) -> Self {
let close_x = width - BUTTON_WIDTH;
let maximize_x = width - BUTTON_WIDTH * 2.0;
let minimize_x = width - BUTTON_WIDTH * 3.0;
let mut cursor = minimize_x; let mut close_window_left: Option<f64> = None;
let mut menu_left: Option<f64> = None;
let mut new_window_left: Option<f64> = None;
let effective_close_window =
view.show_close_window_btn || view.show_new_window_btn;
if effective_close_window {
cursor -= CLOSE_WINDOW_BTN_WIDTH;
close_window_left = Some(cursor);
}
if view.show_menu_btn {
cursor -= MENU_BTN_WIDTH;
menu_left = Some(cursor);
}
if view.show_new_window_btn {
cursor -= NEW_WINDOW_BTN_WIDTH;
new_window_left = Some(cursor);
}
let any_enabled = close_window_left.is_some()
|| menu_left.is_some()
|| new_window_left.is_some();
let group_right = if any_enabled { Some(minimize_x) } else { None };
Self {
close_x,
maximize_x,
minimize_x,
group_right,
close_window_left,
menu_left,
new_window_left,
}
}
}
pub fn register_input_coordinator_chrome(
coord: &mut InputCoordinator,
id: impl Into<WidgetId>,
rect: Rect,
state: &ChromeState,
view: &ChromeView<'_>,
settings: &ChromeSettings,
kind: &ChromeRenderKind,
layer: &LayerId,
) -> CompositeId {
let chrome_id = coord.register_composite(
id,
WidgetKind::Chrome,
rect,
Sense::NONE,
layer,
);
if matches!(kind, ChromeRenderKind::Custom(_)) {
return chrome_id;
}
let style = settings.style.as_ref();
let bp = ButtonPositions::compute(rect.width, view);
let h = style.chrome_height();
let show_tabs = !matches!(kind, ChromeRenderKind::WindowControlsOnly);
if show_tabs {
let uniform_w = uniform_tab_width(view.tabs, style.tab_padding_h(), style.tab_close_size());
let mut x = rect.x + TAB_LEFT_MARGIN;
for (i, tab) in view.tabs.iter().enumerate() {
let cached = tab_width(tab, state, i, style.tab_padding_h(), style.tab_close_size());
let tab_w = if cached > 0.0 { cached } else { uniform_w };
let tab_rect = Rect::new(x, rect.y, tab_w, h);
coord.register_child(
&chrome_id,
format!("{}:tab:{}", chrome_id.0.0, i),
WidgetKind::Button,
tab_rect,
Sense::CLICK | Sense::HOVER,
);
if tab.closable {
let close_w = style.tab_close_size();
let close_x = x + tab_w - close_w - style.tab_padding_h() / 2.0;
let close_y = rect.y + (h - close_w) / 2.0;
coord.register_child(
&chrome_id,
format!("{}:tab_close:{}", chrome_id.0.0, i),
WidgetKind::Button,
Rect::new(close_x, close_y, close_w, close_w),
Sense::CLICK | Sense::HOVER,
);
}
x += tab_w + style.tab_gap();
}
if view.show_new_tab_btn {
coord.register_child(
&chrome_id,
format!("{}:new_tab", chrome_id.0.0),
WidgetKind::Button,
Rect::new(x, rect.y, NEW_TAB_BTN_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
x += NEW_TAB_BTN_WIDTH;
}
let group_left = bp.new_window_left
.or(bp.menu_left)
.or(bp.close_window_left)
.unwrap_or(bp.minimize_x);
let drag_right = rect.x + group_left;
let drag_w = (drag_right - x).max(0.0);
if drag_w > 0.0 {
coord.register_child(
&chrome_id,
format!("{}:drag", chrome_id.0.0),
WidgetKind::DragHandle,
Rect::new(x, rect.y, drag_w, h),
Sense::DRAG,
);
}
} else {
let drag_end = rect.x + bp.minimize_x;
let drag_w = drag_end - rect.x;
if drag_w > 0.0 {
coord.register_child(
&chrome_id,
format!("{}:drag", chrome_id.0.0),
WidgetKind::DragHandle,
Rect::new(rect.x, rect.y, drag_w, h),
Sense::DRAG,
);
}
}
let show_window_controls = !matches!(kind, ChromeRenderKind::Minimal);
if show_window_controls {
let optional_visible = !matches!(kind, ChromeRenderKind::WindowControlsOnly);
if optional_visible {
if let Some(left) = bp.new_window_left {
coord.register_child(
&chrome_id,
format!("{}:new_win", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + left, rect.y, NEW_WINDOW_BTN_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
}
if let Some(left) = bp.menu_left {
coord.register_child(
&chrome_id,
format!("{}:menu", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + left, rect.y, MENU_BTN_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
}
if let Some(left) = bp.close_window_left {
coord.register_child(
&chrome_id,
format!("{}:close_win", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + left, rect.y, CLOSE_WINDOW_BTN_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
}
}
coord.register_child(
&chrome_id,
format!("{}:min", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + bp.minimize_x, rect.y, BUTTON_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
coord.register_child(
&chrome_id,
format!("{}:max", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + bp.maximize_x, rect.y, BUTTON_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
coord.register_child(
&chrome_id,
format!("{}:close", chrome_id.0.0),
WidgetKind::Button,
Rect::new(rect.x + bp.close_x, rect.y, BUTTON_WIDTH, h),
Sense::CLICK | Sense::HOVER,
);
}
chrome_id
}
pub fn register_context_manager_chrome(
ctx_mgr: &mut ContextManager,
render: &mut dyn RenderContext,
id: impl Into<WidgetId>,
rect: Rect,
state: &mut ChromeState,
view: &ChromeView<'_>,
settings: &ChromeSettings,
kind: &ChromeRenderKind,
layer: &LayerId,
) -> CompositeId {
let coord = &mut ctx_mgr.input;
let chrome_id =
register_input_coordinator_chrome(coord, id, rect, state, view, settings, kind, layer);
draw_chrome(render, rect, state, view, settings, kind);
chrome_id
}
pub fn draw_chrome(
ctx: &mut dyn RenderContext,
rect: Rect,
state: &ChromeState,
view: &ChromeView<'_>,
settings: &ChromeSettings,
kind: &ChromeRenderKind,
) {
match kind {
ChromeRenderKind::Custom(f) => {
f(ctx, rect, view, settings);
return;
}
_ => {}
}
let theme = settings.theme.as_ref();
let style = settings.style.as_ref();
let h = style.chrome_height();
let w = rect.width;
let bp = ButtonPositions::compute(w, view);
ctx.set_fill_color(theme.background());
ctx.fill_rect(rect.x, rect.y, w, h);
let show_tabs = !matches!(kind, ChromeRenderKind::WindowControlsOnly);
let show_controls = !matches!(kind, ChromeRenderKind::Minimal);
if show_tabs {
let uniform_w = uniform_tab_width(view.tabs, style.tab_padding_h(), style.tab_close_size());
let mut x = rect.x + TAB_LEFT_MARGIN;
for (i, tab) in view.tabs.iter().enumerate() {
let cached = tab_width(tab, state, i, style.tab_padding_h(), style.tab_close_size());
let tw = if cached > 0.0 { cached } else { uniform_w };
let tab_rect = Rect::new(x, rect.y, tw, h);
let ts = state.tabs_state.get(i);
let hovered = ts.map(|s| s.hovered).unwrap_or(false);
let close_hovered = ts.map(|s| s.close_hovered).unwrap_or(false);
let active = view.active_tab_id.map(|id| id == tab.id).unwrap_or(false);
let bg_color = if active {
theme.tab_bg_active()
} else if hovered || close_hovered {
theme.tab_bg_hover()
} else {
theme.tab_bg_normal()
};
if bg_color != "transparent" {
ctx.set_fill_color(bg_color);
ctx.fill_rect(tab_rect.x, tab_rect.y, tab_rect.width, tab_rect.height);
}
let text_color = if active {
theme.tab_text_active()
} else if hovered {
theme.tab_text_hover()
} else {
theme.tab_text_normal()
};
ctx.set_fill_color(text_color);
ctx.set_font("12px sans-serif");
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(
tab.label,
tab_rect.x + style.tab_padding_h(),
tab_rect.y + h / 2.0,
);
if tab.closable {
let icon_sz = style.tab_close_icon_size();
let close_bw = style.tab_close_size();
let cx = x + tw - close_bw + (close_bw - icon_sz) / 2.0;
let cy = rect.y + (h - icon_sz) / 2.0;
let icon_color = if close_hovered {
theme.close_hover()
} else {
theme.icon_normal()
};
draw_cross(ctx, cx, cy, icon_sz, icon_color, 1.0);
}
x += tw + style.tab_gap();
}
if view.show_new_tab_btn {
let nt_x = rect.x
+ TAB_LEFT_MARGIN
+ tab_total_width(view.tabs, state, style.tab_padding_h(),
style.tab_close_size(), style.tab_gap());
ctx.set_fill_color(theme.separator());
ctx.fill_rect(nt_x, rect.y + 4.0, 1.0, h - 8.0);
let cx = nt_x + NEW_TAB_BTN_WIDTH / 2.0;
let cy = rect.y + h / 2.0;
let arm = 5.0_f64;
ctx.set_stroke_color(theme.icon_normal());
ctx.set_stroke_width(1.5);
ctx.set_line_dash(&[]);
ctx.move_to(cx - arm, cy);
ctx.line_to(cx + arm, cy);
ctx.stroke();
ctx.move_to(cx, cy - arm);
ctx.line_to(cx, cy + arm);
ctx.stroke();
}
}
if show_controls {
use super::types::ChromeHit;
let icon_sz = style.action_icon_size();
let hover_bg = theme.tab_bg_hover();
let icon_normal = theme.icon_normal();
let icon_hover = theme.tab_text_hover();
if show_tabs {
if let Some(left) = bp.new_window_left {
let bx = rect.x + left;
let by = rect.y;
let hovered = state.hovered == ChromeHit::NewWindowBtn;
if hovered {
ctx.set_fill_color(hover_bg);
ctx.fill_rect(bx, by, NEW_WINDOW_BTN_WIDTH, h);
}
let color = if hovered { icon_hover } else { icon_normal };
let s = icon_sz + 2.0;
let ix = bx + (NEW_WINDOW_BTN_WIDTH - s) / 2.0;
let iy = by + (h - s) / 2.0;
draw_svg_icon(ctx, ICON_NEW_WINDOW, ix, iy, s, s, color);
}
}
if show_tabs {
if let Some(left) = bp.menu_left {
let menu_cx = rect.x + left + MENU_BTN_WIDTH / 2.0;
let menu_cy = rect.y + h / 2.0;
let line_w = icon_sz;
ctx.set_fill_color(theme.icon_normal());
for i in 0_i32..3_i32 {
let ly = menu_cy - 3.0 + i as f64 * 3.0;
ctx.fill_rect(menu_cx - line_w / 2.0, ly - 0.75, line_w, 1.5);
}
}
}
if bp.group_right.is_some() {
ctx.set_fill_color(theme.separator());
ctx.fill_rect(rect.x + bp.minimize_x - 1.0, rect.y + 6.0, 1.0, h - 12.0);
}
if show_tabs {
if let Some(left) = bp.close_window_left {
let bx = rect.x + left;
let by = rect.y;
let hovered = state.hovered == ChromeHit::CloseWindowBtn;
if hovered {
ctx.set_fill_color(hover_bg);
ctx.fill_rect(bx, by, CLOSE_WINDOW_BTN_WIDTH, h);
}
let color = if hovered { icon_hover } else { icon_normal };
let s = icon_sz;
let ix = bx + (CLOSE_WINDOW_BTN_WIDTH - s) / 2.0;
let iy = by + (h - s) / 2.0;
draw_svg_icon(ctx, ICON_CLOSE, ix, iy, s, s, color);
}
}
{
let btn_rect = Rect::new(rect.x + bp.minimize_x, rect.y, BUTTON_WIDTH, h);
if state.hovered == super::types::ChromeHit::MinBtn {
ctx.set_fill_color(theme.button_hover());
ctx.fill_rect(btn_rect.x, btn_rect.y, btn_rect.width, btn_rect.height);
}
let mid_x = rect.x + bp.minimize_x + BUTTON_WIDTH / 2.0;
let mid_y = rect.y + h / 2.0;
ctx.set_fill_color(theme.icon_normal());
ctx.fill_rect(mid_x - 5.0, mid_y - 0.5, 10.0, 1.0);
}
{
let btn_rect = Rect::new(rect.x + bp.maximize_x, rect.y, BUTTON_WIDTH, h);
if state.hovered == super::types::ChromeHit::MaxBtn {
ctx.set_fill_color(theme.button_hover());
ctx.fill_rect(btn_rect.x, btn_rect.y, btn_rect.width, btn_rect.height);
}
let mid_x = rect.x + bp.maximize_x + BUTTON_WIDTH / 2.0;
let mid_y = rect.y + h / 2.0;
ctx.set_stroke_color(theme.icon_normal());
ctx.set_stroke_width(1.0);
ctx.set_line_dash(&[]);
if view.is_maximized {
ctx.stroke_rect(mid_x - 3.0, mid_y - 5.0, 8.0, 8.0);
ctx.stroke_rect(mid_x - 5.0, mid_y - 3.0, 8.0, 8.0);
} else {
ctx.stroke_rect(mid_x - 5.0, mid_y - 5.0, 10.0, 10.0);
}
}
{
let close_rect = Rect::new(rect.x + bp.close_x, rect.y, BUTTON_WIDTH, h);
if state.hovered == super::types::ChromeHit::CloseBtn {
ctx.set_fill_color(theme.close_hover());
ctx.fill_rect(close_rect.x, close_rect.y, close_rect.width, close_rect.height);
}
let cx = rect.x + bp.close_x + BUTTON_WIDTH / 2.0;
let cy = rect.y + h / 2.0;
let arm = 5.0_f64;
draw_cross(ctx, cx - arm, cy - arm, arm * 2.0, theme.icon_normal(), 1.5);
}
}
if show_tabs {
let uniform_w = uniform_tab_width(view.tabs, style.tab_padding_h(), style.tab_close_size());
let mut x = rect.x + TAB_LEFT_MARGIN;
for (i, tab) in view.tabs.iter().enumerate() {
let cached = tab_width(tab, state, i, style.tab_padding_h(), style.tab_close_size());
let tw = if cached > 0.0 { cached } else { uniform_w };
let active = view.active_tab_id.map(|id| id == tab.id).unwrap_or(false);
let ts = state.tabs_state.get(i);
let hovered = ts.map(|s| s.hovered).unwrap_or(false);
if active || hovered {
let accent_color = if active {
theme.tab_accent()
} else {
theme.button_hover()
};
ctx.set_fill_color(accent_color);
ctx.fill_rect(
x,
rect.y + h - style.tab_accent_height(),
tw,
style.tab_accent_height(),
);
}
x += tw + style.tab_gap();
}
}
if style.show_bottom_border() {
ctx.set_fill_color(theme.separator());
ctx.fill_rect(rect.x, rect.y + h - 1.0, w, 1.0);
}
}
fn draw_cross(
ctx: &mut dyn RenderContext,
x: f64,
y: f64,
size: f64,
color: &str,
stroke_w: f64,
) {
ctx.set_stroke_color(color);
ctx.set_stroke_width(stroke_w);
ctx.set_line_dash(&[]);
ctx.begin_path();
ctx.move_to(x, y);
ctx.line_to(x + size, y + size);
ctx.stroke();
ctx.begin_path();
ctx.move_to(x + size, y);
ctx.line_to(x, y + size);
ctx.stroke();
}
pub fn measure(
view: &ChromeView<'_>,
state: &ChromeState,
settings: &ChromeSettings,
kind: &ChromeRenderKind,
) -> (f64, f64) {
let style = settings.style.as_ref();
let pad_h = style.tab_padding_h();
let close_sz = style.tab_close_size();
let gap = style.tab_gap();
let left_mrg = style.tab_left_margin();
let tabs_w = tab_total_width(view.tabs, state, pad_h, close_sz, gap);
let new_tab = if view.show_new_tab_btn { style.new_tab_btn_width() } else { 0.0 };
let drag_w = style.drag_zone_min_width();
let controls_w: f64 = match kind {
ChromeRenderKind::Minimal => 0.0,
ChromeRenderKind::WindowControlsOnly | ChromeRenderKind::Default | ChromeRenderKind::Custom(_) => {
let mut w = 0.0;
if view.show_menu_btn { w += style.button_size_max(); }
if view.show_new_window_btn { w += style.button_size_max(); }
w += style.button_size_min() * 3.0;
if view.show_close_window_btn || view.show_new_window_btn {
w += style.button_size_close();
}
w
}
};
let total_w = left_mrg + tabs_w + new_tab + drag_w + controls_w;
let total_h = style.chrome_height();
(total_w, total_h)
}
fn tab_width(
_tab: &ChromeTabConfig<'_>,
state: &ChromeState,
i: usize,
_padding_h: f64,
_close_size: f64,
) -> f64 {
if let Some(&w) = state.tab_widths.get(i) {
return w;
}
0.0
}
fn uniform_tab_width(
tabs: &[ChromeTabConfig<'_>],
padding_h: f64,
close_size: f64,
) -> f64 {
let max_label = tabs.iter().map(|t| t.label.len()).max().unwrap_or(0) as f64;
let text_w = max_label * 7.0;
padding_h + text_w + close_size + padding_h
}
fn tab_total_width(
tabs: &[ChromeTabConfig<'_>],
state: &ChromeState,
padding_h: f64,
close_size: f64,
gap: f64,
) -> f64 {
if !tabs.is_empty() && state.tab_widths.len() == tabs.len() {
let mut total = 0.0_f64;
for (i, _) in tabs.iter().enumerate() {
total += state.tab_widths[i];
if i + 1 < tabs.len() {
total += gap;
}
}
return total;
}
let uw = uniform_tab_width(tabs, padding_h, close_size);
let n = tabs.len() as f64;
if n == 0.0 { 0.0 } else { uw * n + gap * (n - 1.0) }
}