//! 用统一主题绘制宿主管理控件,并同步注册交互目标。 //! 每个控件的图形、角色、启用状态和 action 一起登记,绘制几何与命中几何保持一致。 //! use super::{ Axis, Interaction, Role, Target, VisualState, WidgetId, theme::{self, Theme}, }; use crate::{ display_list::{Color, DisplayList, DrawCommand}, geometry::{Point, Rect}, text::{self, TextStyle}, }; use std::hash::Hash; /// 一次按钮绘制的借用描述,包括显示、角色和启用/选中状态。 pub struct Button<'a> { pub label: &'a str, pub selected: bool, pub enabled: bool, pub role: Role, } impl<'a> Button<'a> { /// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。 pub fn new(label: &'a str) -> Self { Self { label, selected: false, enabled: true, role: Role::Button, } } /// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。 pub fn selected(mut self, value: bool) -> Self { self.selected = value; self } /// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。 pub fn enabled(mut self, value: bool) -> Self { self.enabled = value; self } /// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。 pub fn role(mut self, value: Role) -> Self { self.role = value; self } } /// 本帧宿主控件绘制上下文,共同拥有显示列表与交互目标登记入口。 pub struct Ui { pub list: DisplayList, pub targets: Vec>, pub theme: Theme, interaction: Interaction, } impl Ui { /// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。 pub fn new(theme: Theme, interaction: Interaction) -> Self { Self { list: DisplayList::default(), targets: Vec::new(), theme, interaction, } } /// 使用当前主题绘制控件填充、轮廓或文字原语。 pub fn fill(&mut self, r: Rect, color: Color) { self.list.push(DrawCommand::FillRoundedRect { rect: r, radius: 0.0, color, }); } /// 使用当前主题绘制控件填充、轮廓或文字原语。 fn outline(&mut self, r: Rect, color: Color, width: f32) { self.list.push(DrawCommand::StrokeRoundedRect { rect: Rect::new( r.x + width / 2.0, r.y + width / 2.0, (r.width - width).max(0.0), (r.height - width).max(0.0), ), radius: 3.0, width, color, }); } /// 使用当前主题绘制控件填充、轮廓或文字原语。 pub fn label(&mut self, x: f32, y: f32, value: &str, color: Color, code: bool) { self.list.push(DrawCommand::Text { origin: Point::new(x, y), value: value.into(), style: if code { TextStyle::code() } else { TextStyle { font_size: 12.0, line_height: 18.0, ..TextStyle::ui(1) } }, color, }); } /// 把控件身份、几何、角色、启用状态和 action 一起加入本帧交互目标。 fn register( &mut self, id: WidgetId, rect: Rect, role: Role, action: Option, selected: bool, enabled: bool, ) -> VisualState { let response = self.interaction.visual(id, selected, enabled); self.targets.push(Target { id, rect, role, enabled, action, }); response } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 fn surface(&mut self, rect: Rect, s: VisualState, base: Option) { let color = if !s.enabled { base } else if s.pressed { Some(self.theme.pressed()) } else if s.hovered { Some(self.theme.hover()) } else if s.selected { Some(self.theme.soft()) } else { base }; if let Some(color) = color { self.fill(rect, color); } if s.focused { self.outline(rect, self.theme.accent, 1.5); } } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn button(&mut self, rect: Rect, action: A, button: Button<'_>) -> VisualState { let id = WidgetId::new(&action); let s = self.register( id, rect, button.role, Some(action), button.selected, button.enabled, ); self.surface(rect, s, None); self.list.push(DrawCommand::PushClip(rect)); let label = text::fit( button.label, (rect.width - 16.0).max(0.0), TextStyle { font_size: 12.0, line_height: 18.0, ..TextStyle::ui(1) }, ); self.label( rect.x + 8.0, rect.y + (rect.height - 18.0) / 2.0, &label, if s.enabled { theme::TEXT } else { Color::rgb(151, 160, 172) }, false, ); if s.selected && button.role == Role::Tab { self.fill( Rect::new(rect.x, rect.y + rect.height - 2.0, rect.width, 2.0), self.theme.accent, ); } self.list.push(DrawCommand::PopClip); s } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn text_field(&mut self, rect: Rect, action: A, focused: bool) { let s = self.register( WidgetId::new(&action), rect, Role::TextField, Some(action), false, true, ); self.surface(rect, s, Some(theme::BG)); self.outline( rect, if focused || s.focused { self.theme.accent } else if s.hovered { theme::MUTED } else { theme::BORDER }, if focused || s.focused { 1.5 } else { 1.0 }, ); } /// A terminal prompt shares its surface with output; its caret is the focus cue. /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn inline_input(&mut self, rect: Rect, action: A) { self.register( WidgetId::new(&action), rect, Role::TextField, Some(action), false, true, ); } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn row(&mut self, paint: Rect, hit: Rect, action: A, selected: bool) { let s = self.register( WidgetId::new(&action), hit, Role::Row, Some(action), selected, true, ); self.surface(paint, s, None); } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn swatch(&mut self, rect: Rect, action: A, color: Color, selected: bool) { let s = self.register( WidgetId::new(&action), rect, Role::Swatch, Some(action), selected, true, ); self.fill( rect, if s.pressed { theme::mix(color, theme::TEXT, 0.2) } else { color }, ); if s.hovered || s.focused || selected { self.outline( Rect::new( rect.x - 3.0, rect.y - 3.0, rect.width + 6.0, rect.height + 6.0, ), if s.focused { self.theme.accent } else { theme::TEXT }, if s.pressed { 3.0 } else { 1.5 }, ); } if selected { self.label(rect.x + 10.0, rect.y + 6.0, "✓", theme::BG, false); } } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn splitter(&mut self, rect: Rect, id: WidgetId, axis: Axis) { let hit = Rect::new( rect.x - 3.0, rect.y - 3.0, rect.width + 6.0, rect.height + 6.0, ); let mut s = self.register(id, hit, Role::Splitter(axis), None, false, true); s.pressed |= self.interaction.captured(id); let color = if s.pressed || s.hovered { self.theme.accent } else { theme::BORDER }; let line = if s.hovered || s.pressed { match axis { Axis::Horizontal => Rect::new(rect.x - 1.0, rect.y, 3.0, rect.height), Axis::Vertical => Rect::new(rect.x, rect.y - 1.0, rect.width, 3.0), } } else { rect }; self.fill(line, color); } /// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。 pub fn scrollbar(&mut self, rect: Rect, id: WidgetId, axis: Axis) { let mut s = self.register(id, rect, Role::Scrollbar(axis), None, false, true); s.pressed |= self.interaction.captured(id); self.fill( rect, if s.pressed { self.theme.accent } else if s.hovered { theme::mix(theme::MUTED, self.theme.accent, 0.55) } else { Color::rgb(161, 173, 190) }, ); } }