use crate::core::{Color, Font, HorizontalAlignment, Point, Rect};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::style::WidgetStyle;
use crate::widget::metrics::{estimate_text_width, role_font};
use crate::widget::Draw;
fn companion_style(control: &str) -> WidgetStyle {
crate::style::resolved_theme_style_shared(control)
.map(|style| (*style).clone())
.unwrap_or_default()
}
fn companion_colors() -> (Color, Color, Color) {
let palette = crate::style::palette_shared();
let surface = palette
.as_ref()
.map(|colors| colors.surface_container)
.unwrap_or(Color::rgb(245, 246, 248));
let ink = palette.as_ref().map(|colors| colors.foreground).unwrap_or(Color::BLACK);
let accent = palette.as_ref().map(|colors| colors.primary).unwrap_or(Color::PRIMARY);
(surface, ink, accent)
}
pub use super::suggestion_popup::{MatchMode, PopupItem, SuggestionPopup};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropSide {
Before,
After,
}
pub struct Chip {
label: String,
removable: bool,
chip_radius: u32,
close_radius: u32,
selected: bool,
accent: Option<Color>,
draggable: bool,
dragging: bool,
drop_indicator: Option<DropSide>,
drag_from: usize,
bounds: Rect,
pub chip_clicked: GenericSignal,
pub remove_requested: GenericSignal,
pub drag_finished: Signal1<(usize, usize)>,
}
impl Chip {
pub fn chip_radius(&self) -> u32 {
self.chip_radius
}
pub fn set_chip_radius(&mut self, radius: u32) {
self.chip_radius = radius;
}
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
removable: true,
chip_radius: 12,
close_radius: 7,
selected: false,
accent: None,
draggable: false,
dragging: false,
drop_indicator: None,
drag_from: 0,
bounds: Rect::new(0, 0, 0, 0),
chip_clicked: GenericSignal::new(),
remove_requested: GenericSignal::new(),
drag_finished: Signal1::new(),
}
}
pub fn set_label(&mut self, label: &str) {
self.label = label.to_string();
}
pub fn label(&self) -> &str {
&self.label
}
pub fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
pub fn bounds(&self) -> Rect {
self.bounds
}
pub fn paint(&self, context: &mut RenderContext, rect: Rect) {
if rect.width == 0 || rect.height == 0 {
return;
}
let style = companion_style("tag_input");
let font = role_font(&style, 12.0);
let (surface, ink, theme_accent) = companion_colors();
let accent = self.accent.unwrap_or(theme_accent);
let fill = if self.selected {
accent
} else if self.dragging {
accent.with_alpha(40)
} else {
surface
};
let border =
if self.selected || self.dragging { accent } else { surface.blend(&ink, 0.35) };
let radius = self.chip_radius.min(rect.height / 2).max(1);
context.fill_rounded_rect(rect, radius, fill);
context.draw_rounded_rect_stroke(rect, radius, border, 1);
let text_ink = if self.selected { accent.contrast_color() } else { ink };
let close_band = if self.removable { self.close_radius * 2 + 12 } else { 0 };
let text_width = rect.width.saturating_sub(12 + close_band);
if text_width > 0 {
context.draw_text_line_cow(
Rect::new(rect.x + 6, rect.y, text_width, rect.height),
&self.label,
&font,
text_ink,
HorizontalAlignment::Left,
);
}
if let Some(center) = self.close_center(rect) {
let reach = self.close_radius.min(rect.height / 2) as i32;
let mark = if self.selected { accent.contrast_color() } else { ink.with_alpha(160) };
context.draw_line(
Point::new(center.x - reach, center.y - reach),
Point::new(center.x + reach, center.y + reach),
mark,
);
context.draw_line(
Point::new(center.x + reach, center.y - reach),
Point::new(center.x - reach, center.y + reach),
mark,
);
}
if let Some(side) = self.drop_indicator {
let x = match side {
DropSide::Before => rect.x,
DropSide::After => rect.right(),
};
context.fill_rect(Rect::new(x.saturating_sub(1), rect.y, 2, rect.height), accent);
}
}
pub fn set_removable(&mut self, removable: bool) {
self.removable = removable;
}
pub fn removable(&self) -> bool {
self.removable
}
pub fn set_selected(&mut self, selected: bool) {
self.selected = selected;
}
pub fn selected(&self) -> bool {
self.selected
}
pub fn set_accent(&mut self, accent: Option<Color>) {
self.accent = accent;
}
pub fn accent(&self) -> Option<Color> {
self.accent
}
pub fn set_draggable(&mut self, draggable: bool) {
self.draggable = draggable;
}
pub fn set_dragging(&mut self, dragging: bool) {
self.dragging = dragging;
}
pub fn is_dragging(&self) -> bool {
self.dragging
}
pub fn set_drop_indicator(&mut self, side: Option<DropSide>) {
self.drop_indicator = side;
}
pub fn drop_indicator(&self) -> Option<DropSide> {
self.drop_indicator
}
pub fn begin_drag(&mut self, from: usize) {
if self.draggable {
self.dragging = true;
self.drag_from = from;
}
}
pub fn finish_drag(&mut self, to: usize) {
if self.dragging {
self.dragging = false;
self.drag_finished.emit((self.drag_from, to));
}
}
pub fn intrinsic_width(&self, font: &Font) -> u32 {
let text = estimate_text_width(&self.label, font, 1.0);
let padding = 12u32;
let close = if self.removable { self.close_radius * 2 + 4 } else { 0 };
text + padding + close
}
pub fn close_center(&self, chip_rect: Rect) -> Option<Point> {
self.removable.then(|| {
Point::new(
chip_rect.x + chip_rect.width as i32 - 6 - self.close_radius as i32,
chip_rect.y + chip_rect.height as i32 / 2,
)
})
}
pub fn press_at(&mut self, pos: Point, chip_rect: Rect) {
if let Some(center) = self.close_center(chip_rect) {
let dx = (pos.x - center.x) as i64;
let dy = (pos.y - center.y) as i64;
let radius = self.close_radius as i64;
if dx * dx + dy * dy <= radius * radius {
self.remove_requested.emit();
return;
}
}
if chip_rect.contains_point(pos) {
self.chip_clicked.emit();
}
}
}
impl Draw for Chip {
fn draw(&mut self, context: &mut RenderContext) {
let rect = if self.bounds.width == 0 || self.bounds.height == 0 {
let style = companion_style("tag_input");
let font = role_font(&style, 12.0);
Rect::new(self.bounds.x, self.bounds.y, self.intrinsic_width(&font), 24)
} else {
self.bounds
};
self.paint(context, rect);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StepOrientation {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepDirection {
Up,
Down,
}
pub struct StepButtons {
bounds: Rect,
orientation: StepOrientation,
repeat_delay_ms: u32,
repeat_interval_ms: u32,
up_enabled: bool,
down_enabled: bool,
repeat_active: Option<StepDirection>,
elapsed: u32,
pub step_requested: Signal1<StepDirection>,
}
impl StepButtons {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
orientation: StepOrientation::Vertical,
repeat_delay_ms: 400,
repeat_interval_ms: 120,
up_enabled: true,
down_enabled: true,
repeat_active: None,
elapsed: 0,
step_requested: Signal1::new(),
}
}
pub fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
pub fn bounds(&self) -> Rect {
self.bounds
}
pub fn set_orientation(&mut self, orientation: StepOrientation) {
self.orientation = orientation;
}
pub fn orientation(&self) -> StepOrientation {
self.orientation
}
pub fn set_repeat(&mut self, delay_ms: u32, interval_ms: u32) {
self.repeat_delay_ms = delay_ms;
self.repeat_interval_ms = interval_ms.max(1);
}
pub fn set_enabled_pair(&mut self, up: bool, down: bool) {
self.up_enabled = up;
self.down_enabled = down;
if (self.repeat_active == Some(StepDirection::Up) && !up)
|| (self.repeat_active == Some(StepDirection::Down) && !down)
{
self.repeat_active = None;
}
}
pub fn enabled_pair(&self) -> (bool, bool) {
(self.up_enabled, self.down_enabled)
}
pub fn up_rect(&self) -> Rect {
match self.orientation {
StepOrientation::Vertical => {
Rect::new(self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height / 2)
}
StepOrientation::Horizontal => {
Rect::new(self.bounds.x, self.bounds.y, self.bounds.width / 2, self.bounds.height)
}
}
}
pub fn down_rect(&self) -> Rect {
match self.orientation {
StepOrientation::Vertical => Rect::new(
self.bounds.x,
self.bounds.y + (self.bounds.height / 2) as i32,
self.bounds.width,
self.bounds.height - self.bounds.height / 2,
),
StepOrientation::Horizontal => Rect::new(
self.bounds.x + (self.bounds.width / 2) as i32,
self.bounds.y,
self.bounds.width - self.bounds.width / 2,
self.bounds.height,
),
}
}
pub fn contains_up(&self, pos: Point) -> bool {
self.up_rect().contains_point(pos)
}
pub fn contains_down(&self, pos: Point) -> bool {
self.down_rect().contains_point(pos)
}
pub fn direction_at(&self, pos: Point) -> Option<StepDirection> {
if self.contains_up(pos) {
Some(StepDirection::Up)
} else if self.contains_down(pos) {
Some(StepDirection::Down)
} else {
None
}
}
pub fn begin_repeat(&mut self, direction: StepDirection) {
let enabled = match direction {
StepDirection::Up => self.up_enabled,
StepDirection::Down => self.down_enabled,
};
if !enabled {
return;
}
self.repeat_active = Some(direction);
self.elapsed = 0;
self.step_requested.emit(direction);
}
pub fn end_repeat(&mut self) {
self.repeat_active = None;
self.elapsed = 0;
}
pub fn repeat_active(&self) -> Option<StepDirection> {
self.repeat_active
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
let Some(direction) = self.repeat_active else {
return false;
};
self.elapsed = self.elapsed.saturating_add(delta_ms);
if self.elapsed < self.repeat_delay_ms {
return true;
}
let mut remaining = self.elapsed - self.repeat_delay_ms;
while remaining >= self.repeat_interval_ms {
remaining -= self.repeat_interval_ms;
self.step_requested.emit(direction);
}
self.elapsed = self.repeat_delay_ms + remaining;
true
}
}
impl Draw for StepButtons {
fn draw(&mut self, context: &mut RenderContext) {
let bounds = self.bounds;
if bounds.width == 0 || bounds.height == 0 {
return;
}
let (surface, ink, _accent) = companion_colors();
let border = surface.blend(&ink, 0.35);
let radius = 4.min(bounds.height / 2).max(1);
context.fill_rounded_rect(bounds, radius, surface);
context.draw_rounded_rect_stroke(bounds, radius, border, 1);
let up = self.up_rect();
let down = self.down_rect();
match self.orientation {
StepOrientation::Vertical => {
let y = up.y + up.height as i32;
context.fill_rect(
Rect::new(bounds.x + 2, y, bounds.width.saturating_sub(4), 1),
border,
);
}
StepOrientation::Horizontal => {
let x = up.x + up.width as i32;
context.fill_rect(
Rect::new(x, bounds.y + 2, 1, bounds.height.saturating_sub(4)),
border,
);
}
}
for (rect, enabled, up_direction) in
[(up, self.up_enabled, true), (down, self.down_enabled, false)]
{
if rect.width == 0 || rect.height == 0 {
continue;
}
let color = if enabled { ink } else { ink.with_alpha(80) };
let cx = rect.x + rect.width as i32 / 2;
let cy = rect.y + rect.height as i32 / 2;
let reach = (rect.width.min(rect.height) as i32 / 4).max(2);
let (tip_y, base_y) = if up_direction {
(cy - reach, cy + reach / 2)
} else {
(cy + reach, cy - reach / 2)
};
context.draw_line(Point::new(cx - reach, base_y), Point::new(cx, tip_y), color);
context.draw_line(Point::new(cx, tip_y), Point::new(cx + reach, base_y), color);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollOrientation {
#[default]
Vertical,
Horizontal,
}
pub struct Scrollbar {
orientation: ScrollOrientation,
content_size: u32,
viewport_size: u32,
scroll_offset: u32,
thickness: u32,
auto_hide: bool,
thumb_min_length: u32,
track: Rect,
pub scroll_offset_changed: Signal1<u32>,
}
impl Scrollbar {
pub fn new() -> Self {
Self {
orientation: ScrollOrientation::Vertical,
content_size: 0,
viewport_size: 0,
scroll_offset: 0,
thickness: 4,
auto_hide: true,
thumb_min_length: 8,
track: Rect::new(0, 0, 0, 0),
scroll_offset_changed: Signal1::new(),
}
}
pub fn set_orientation(&mut self, orientation: ScrollOrientation, thickness: u32) {
self.orientation = orientation;
self.thickness = thickness.max(1);
}
pub fn orientation(&self) -> ScrollOrientation {
self.orientation
}
pub fn set_track(&mut self, track: Rect) {
self.track = track;
}
pub fn track(&self) -> Rect {
self.track
}
pub fn set_metrics(&mut self, content: u32, viewport: u32, offset: u32) {
self.content_size = content;
self.viewport_size = viewport;
self.scroll_offset = offset;
}
pub fn content_size(&self) -> u32 {
self.content_size
}
pub fn viewport_size(&self) -> u32 {
self.viewport_size
}
pub fn scroll_offset(&self) -> u32 {
self.scroll_offset
}
pub fn set_auto_hide(&mut self, auto_hide: bool) {
self.auto_hide = auto_hide;
}
pub fn set_thumb_min_length(&mut self, length: u32) {
self.thumb_min_length = length.max(1);
}
pub fn is_visible(&self) -> bool {
!self.auto_hide || self.content_size > self.viewport_size
}
pub fn thumb_rect(&self, track: Rect) -> Rect {
let (track_len, content, viewport) = self.axis(track);
if content == 0 || viewport == 0 {
return Rect::new(track.x, track.y, track.width, track.height);
}
let max_offset = content.saturating_sub(viewport);
let thumb_len = ((viewport as f32 / content as f32) * track_len as f32)
.max(self.thumb_min_length as f32)
.min(track_len as f32) as u32;
let travel = track_len.saturating_sub(thumb_len);
let fraction = if max_offset == 0 {
0.0
} else {
self.scroll_offset.min(max_offset) as f32 / max_offset as f32
};
match self.orientation {
ScrollOrientation::Vertical => Rect::new(
track.x,
track.y + (travel as f32 * fraction) as i32,
track.width,
thumb_len,
),
ScrollOrientation::Horizontal => Rect::new(
track.x + (travel as f32 * fraction) as i32,
track.y,
thumb_len,
track.height,
),
}
}
pub fn hit_thumb(&self, pos: Point) -> bool {
self.thumb_rect(self.track).contains_point(pos)
}
pub fn drag_thumb(&mut self, pos: Point) -> u32 {
let track = self.track;
let thumb = self.thumb_rect(track);
let (track_len, content, viewport) = self.axis(track);
let max_offset = content.saturating_sub(viewport);
if max_offset == 0 {
return self.scroll_offset;
}
let thumb_len = match self.orientation {
ScrollOrientation::Vertical => thumb.height,
ScrollOrientation::Horizontal => thumb.width,
};
let travel = track_len.saturating_sub(thumb_len).max(1) as f32;
let position = match self.orientation {
ScrollOrientation::Vertical => (pos.y - track.y - thumb_len as i32 / 2) as f32,
ScrollOrientation::Horizontal => (pos.x - track.x - thumb_len as i32 / 2) as f32,
};
let fraction = (position / travel).clamp(0.0, 1.0);
let next = (fraction * max_offset as f32).round() as u32;
if next != self.scroll_offset {
self.scroll_offset = next;
self.scroll_offset_changed.emit(next);
}
next
}
fn axis(&self, track: Rect) -> (u32, u32, u32) {
match self.orientation {
ScrollOrientation::Vertical => (track.height, self.content_size, self.viewport_size),
ScrollOrientation::Horizontal => (track.width, self.content_size, self.viewport_size),
}
}
}
impl Default for Scrollbar {
fn default() -> Self {
Self::new()
}
}
impl Draw for Scrollbar {
fn draw(&mut self, context: &mut RenderContext) {
let track = self.track;
if !self.is_visible() || track.width == 0 || track.height == 0 {
return;
}
let (surface, ink, accent) = companion_colors();
let radius = (self.thickness / 2).max(1);
context.fill_rounded_rect(track, radius, surface.blend(&ink, 0.12));
let thumb = self.thumb_rect(track);
if thumb.width > 0 && thumb.height > 0 {
context.fill_rounded_rect(thumb, radius, accent.with_alpha(210));
}
}
}
use crate::widget::input_widgets::rich_edit::CharFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatActionSet {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub color: bool,
pub clear_format: bool,
}
impl Default for FormatActionSet {
fn default() -> Self {
Self { bold: true, italic: true, underline: true, color: true, clear_format: true }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormatAction {
Bold,
Italic,
Underline,
Color,
ClearFormat,
}
const FORMAT_ACTIONS: [FormatAction; 5] = [
FormatAction::Bold,
FormatAction::Italic,
FormatAction::Underline,
FormatAction::Color,
FormatAction::ClearFormat,
];
const FORMAT_CELL_WIDTH: u32 = 28;
const FORMAT_CELL_GAP: u32 = 2;
const FORMAT_DEFAULT_HEIGHT: u32 = 28;
pub struct FormatToolbar {
actions: FormatActionSet,
active_format: CharFormat,
accent: Option<Color>,
ink: Option<Color>,
compact: bool,
bounds: Rect,
pub format_requested: Signal1<(FormatAction, Option<Color>)>,
}
impl FormatToolbar {
pub fn new() -> Self {
Self {
actions: FormatActionSet::default(),
active_format: CharFormat::default(),
accent: None,
ink: None,
compact: false,
bounds: Rect::new(0, 0, 0, 0),
format_requested: Signal1::new(),
}
}
pub fn active_format(&self) -> CharFormat {
self.active_format.clone()
}
pub fn set_active_format(&mut self, format: CharFormat) {
self.active_format = format;
}
pub fn actions(&self) -> FormatActionSet {
self.actions
}
pub fn set_enabled(&mut self, actions: FormatActionSet) {
self.actions = actions;
}
pub fn set_colors(&mut self, accent: Option<Color>, ink: Option<Color>) {
self.accent = accent;
self.ink = ink;
}
pub fn accent(&self) -> Option<Color> {
self.accent
}
pub fn ink(&self) -> Option<Color> {
self.ink
}
pub fn compact(&self) -> bool {
self.compact
}
pub fn set_compact(&mut self, compact: bool) {
self.compact = compact;
}
pub fn is_enabled(&self, action: FormatAction) -> bool {
match action {
FormatAction::Bold => self.actions.bold,
FormatAction::Italic => self.actions.italic,
FormatAction::Underline => self.actions.underline,
FormatAction::Color => self.actions.color,
FormatAction::ClearFormat => self.actions.clear_format,
}
}
pub fn press(&mut self, action: FormatAction) {
if !self.is_enabled(action) {
return;
}
let value = matches!(action, FormatAction::Color).then_some(self.accent).flatten();
self.format_requested.emit((action, value));
}
pub fn preview(&self, action: FormatAction) -> CharFormat {
let mut format = self.active_format.clone();
match action {
FormatAction::Bold => format.bold = !format.bold,
FormatAction::Italic => format.italic = !format.italic,
FormatAction::Underline => format.underline = !format.underline,
FormatAction::Color => format.color = self.accent,
FormatAction::ClearFormat => format = CharFormat::default(),
}
format
}
pub fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
pub fn bounds(&self) -> Rect {
self.bounds
}
fn cell_rect(&self, action: FormatAction, bounds: Rect) -> Option<Rect> {
if !self.is_enabled(action) {
return None;
}
let column = FORMAT_ACTIONS
.iter()
.take_while(|candidate| **candidate != action)
.filter(|candidate| self.is_enabled(**candidate))
.count();
let width =
if self.compact { FORMAT_CELL_WIDTH.saturating_sub(6) } else { FORMAT_CELL_WIDTH };
let x_offset = column as u32 * (width + FORMAT_CELL_GAP);
if x_offset >= bounds.width {
return None;
}
let available = bounds.width - x_offset;
Some(Rect::new(bounds.x + x_offset as i32, bounds.y, width.min(available), bounds.height))
}
pub fn action_at(&self, pos: Point) -> Option<FormatAction> {
FORMAT_ACTIONS.iter().copied().find(|action| {
self.cell_rect(*action, self.bounds).is_some_and(|rect| rect.contains_point(pos))
})
}
pub fn paint(&self, context: &mut RenderContext, rect: Rect) {
if rect.width == 0 || rect.height == 0 {
return;
}
let style = companion_style("rich_edit");
let font = role_font(&style, 12.0);
let (surface, ink, theme_accent) = companion_colors();
let accent = self.accent.unwrap_or(theme_accent);
let ink = self.ink.unwrap_or(ink);
let border = surface.blend(&ink, 0.3);
context.fill_rounded_rect(rect, 4, surface);
context.draw_rounded_rect_stroke(rect, 4, border, 1);
for action in FORMAT_ACTIONS {
let Some(cell) = self.cell_rect(action, rect) else {
continue;
};
if cell.width == 0 || cell.height == 0 {
continue;
}
let active = match action {
FormatAction::Bold => self.active_format.bold,
FormatAction::Italic => self.active_format.italic,
FormatAction::Underline => self.active_format.underline,
FormatAction::Color => self.active_format.color.is_some(),
FormatAction::ClearFormat => false,
};
let cell_fill = if active { accent.with_alpha(40) } else { surface };
context.fill_rounded_rect(cell, 3, cell_fill);
let color = if active { accent } else { ink };
match action {
FormatAction::Bold => {
context.draw_text_line_cow(
cell,
"B",
&font.with_bold(true),
color,
HorizontalAlignment::Center,
);
}
FormatAction::Italic => {
context.draw_text_line_cow(
cell,
"I",
&font.with_italic(true),
color,
HorizontalAlignment::Center,
);
}
FormatAction::Underline => {
context.draw_text_line_cow(
cell,
"U",
&font,
color,
HorizontalAlignment::Center,
);
let y = cell.y + cell.height as i32 - 5;
context.draw_line(
Point::new(cell.x + 6, y),
Point::new(cell.x + cell.width as i32 - 6, y),
color,
);
}
FormatAction::Color => {
let center =
Point::new(cell.x + cell.width as i32 / 2, cell.y + cell.height as i32 / 2);
context.fill_circle(center, (cell.height / 5).max(3), accent);
}
FormatAction::ClearFormat => {
context.draw_text_line_cow(
cell,
"Tx",
&font,
color,
HorizontalAlignment::Center,
);
}
}
}
}
}
impl Default for FormatToolbar {
fn default() -> Self {
Self::new()
}
}
impl Draw for FormatToolbar {
fn draw(&mut self, context: &mut RenderContext) {
let rect = if self.bounds.width == 0 || self.bounds.height == 0 {
let columns =
FORMAT_ACTIONS.iter().filter(|action| self.is_enabled(**action)).count() as u32;
let width = columns.saturating_mul(FORMAT_CELL_WIDTH + FORMAT_CELL_GAP);
Rect::new(self.bounds.x, self.bounds.y, width, FORMAT_DEFAULT_HEIGHT)
} else {
self.bounds
};
self.paint(context, rect);
}
}