Watch
1
0
Fork
You've already forked souveraine
0

tuie: replace hand-rolled widgets with verbatim tuie-demo copies

14 widgets now match the demo source exactly (import paths rewritten):
accordion, button, checkbox, counter, flat_button, focus_pane,
global_chords, horizontal_rule, link, page_layout, point_picker,
progress_bar, radio_group, segmented_control.

Theme simplified: accent color is now Color::YELLOW, which
apply_atmosphere resolves through harmonious to the agent's
current primary — no thread-local, no RGB arithmetic.

Added chord_macro + axis2d deps matching tuie-demo's Cargo.toml.

All 27 broken call sites (field_grid, settings, cockpit,
dropdown, model_picker, text_editor) migrated to demo APIs:
FlatButton::new().child(x), Button::new().children([x]),
Checkbox::new(label).set_checked(b), Counter::new(),
PointPicker::new().point(...), etc.
This commit is contained in:
Fimeg 2026-06-04 18:24:12 -04:00
commit 85aa1f77de
22 changed files with 266 additions and 392 deletions

View file

@ -59,6 +59,8 @@ hound = "3.5"
crossterm = { version = "0.28", features = ["bracketed-paste"] } # Terminal control (bracketed paste for paste detection) — kept for old TUI
ratatui = { version = "0.30", features = ["crossterm"] } # TUI framework with crossterm backend — kept for old TUI
tuie = { path = "../tuie", features = ["harmonious", "images"] } # New composable widget toolkit — replacing ratatui
chord_macro = { path = "../tuie/chord_macro" } # chord!() input-matching macro — the one the tuie-demo widgets use
axis2d = "0.1.0" # Axis2D/Vec2 geometry — re-exported by tuie, used directly by the demo widgets
unicode-width = "0.1"
arboard = { version = "3", features = ["wayland-data-control"] } # Clipboard — click-to-copy a message bubble
colored = "2" # Color gradients and effects

View file

@ -86,9 +86,9 @@ impl FieldGrid {
let value_widget: Box<dyn Widget> = match value {
EditableValue::Bool(b) => {
let mut id = WidgetId::EMPTY;
let w = Checkbox::new(Text::new().content(""), accent)
.with_checked(*b)
.id(&mut id);
let mut c = Checkbox::new(Text::new().content(""));
c.set_checked(*b);
let w = c.id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
@ -100,23 +100,21 @@ impl FieldGrid {
n if n > 6 => {
let current = labels.get(*index).copied().unwrap_or("?");
let mut id = WidgetId::EMPTY;
let w = FlatButton::new(accent)
.children([
Text::new().content(format!(" {current} \u{25be}")),
])
let w = FlatButton::new()
.child(Text::new().content(format!(" {current} \u{25be}")))
.id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
5 | 6 => {
let mut id = WidgetId::EMPTY;
let w = Dropdown::new(&labels, *index, accent).id(&mut id);
let w = Dropdown::new(&labels, *index).id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
4 => {
let mut id = WidgetId::EMPTY;
let w = RadioGroup::new(&labels, accent)
let w = RadioGroup::new(&labels)
.selected(*index)
.id(&mut id);
widget_id = id.untyped();
@ -124,7 +122,7 @@ impl FieldGrid {
}
_ => {
let mut id = WidgetId::EMPTY;
let w = SegmentedControl::new(&labels, accent)
let w = SegmentedControl::new(&labels)
.selected(*index)
.id(&mut id);
widget_id = id.untyped();
@ -134,7 +132,7 @@ impl FieldGrid {
}
EditableValue::Uint(v) => {
let mut id = WidgetId::EMPTY;
let w = Counter::new("", accent)
let w = Counter::new("")
.value(*v as i32)
.min(0)
.max(i32::MAX)
@ -144,7 +142,7 @@ impl FieldGrid {
}
EditableValue::Int(v) => {
let mut id = WidgetId::EMPTY;
let w = Counter::new("", accent).value(*v as i32).id(&mut id);
let w = Counter::new("").value(*v as i32).id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
@ -157,8 +155,7 @@ impl FieldGrid {
}
EditableValue::Point2D { x, y } => {
let mut id = WidgetId::EMPTY;
let w = PointPicker::new(accent)
.grid(Vec2::new(5, 5))
let w = PointPicker::new()
.point(Vec2::new(*x as u16, *y as u16))
.id(&mut id);
widget_id = id.untyped();
@ -171,24 +168,24 @@ impl FieldGrid {
format!(" {s} ")
};
let mut id = WidgetId::EMPTY;
let w = FlatButton::new(accent)
.children([Text::new().content(display)])
let w = FlatButton::new()
.child(Text::new().content(display))
.id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
EditableValue::Secret(_) => {
let mut id = WidgetId::EMPTY;
let w = FlatButton::new(accent)
.children([Text::new().content(" \u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022} ")])
let w = FlatButton::new()
.child(Text::new().content(" \u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022} "))
.id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>
}
EditableValue::OptionalText(None) => {
let mut id = WidgetId::EMPTY;
let w = FlatButton::new(accent)
.children([Text::new().content(StyledStr::new(" (none) ").fg(dim))])
let w = FlatButton::new()
.child(Text::new().content(StyledStr::new(" (none) ").fg(dim)))
.id(&mut id);
widget_id = id.untyped();
w as Box<dyn Widget>

View file

@ -298,7 +298,7 @@ impl SettingsScreen {
let field_grid_pane = field_grid.widget().id(&mut wide_field_grid_id);
let cat_title = view.selected_category().label().to_string();
let accordion = Accordion::new_expanded(&cat_title, field_grid_pane as Box<dyn Widget>, accent)
let accordion = Accordion::new_expanded(&cat_title, field_grid_pane as Box<dyn Widget>)
.id(&mut wide_accordion_id);
let main_page = Pane::new()
@ -322,7 +322,7 @@ impl SettingsScreen {
let mut narrow_page_layout_id = WidgetId::EMPTY;
let cat_labels: Vec<&str> = Category::all().iter().map(|c| c.label()).collect();
let tabs = SegmentedControl::new(&cat_labels, accent)
let tabs = SegmentedControl::new(&cat_labels)
.id(&mut narrow_tab_id);
let narrow_fields = view.fields_for_category(view.selected_category());

View file

@ -248,7 +248,7 @@ impl ModelPicker {
.placeholder(Text::new().content(StyledStr::new(" fuzzy search\u{2026} ").fg(dim)))
.id(&mut input_id);
let refresh_button = Button::new(accent)
let refresh_button = Button::new()
.children([Text::new().content(StyledStr::new(" \u{21bb} ").fg(Color::BLACK).bold())])
.id(&mut refresh_button_id);

View file

@ -72,11 +72,11 @@ impl TextEditor {
.border_style(Style::new().fg(dim).dim())
.children([input_widget as Box<dyn Widget>]);
let save_button = Button::new(accent)
let save_button = Button::new()
.children([Text::new().content(" Save ")])
.id(&mut save_button_id);
let cancel_button = Button::new(dim)
let cancel_button = Button::new()
.children([Text::new().content(" Cancel ")])
.id(&mut cancel_button_id);

View file

@ -5,8 +5,6 @@
//! `Atmosphere`) onto tuie's Theme slots so the terminal palette tracks the
//! agent's atmospheric shift — live, without restarting.
use std::cell::Cell;
use tuie::prelude::{Color, Style};
use tuie::theme::Theme;
@ -97,44 +95,22 @@ pub fn to_tuie_color(c: ratatui::style::Color) -> Color {
}
}
// ── User-selectable accent color ──────────────────────────────────────────
// ── Accent color ───────────────────────────────────────────────────────────
//
// The accent is not a separately-tracked value. `apply_atmosphere` feeds the
// agent's ChatPalette through tuie's harmonious palette, where `agent_primary`
// lands on the `yellow` theme slot (see `chat_palette_to_theme`). So the accent
// is simply `Color::YELLOW`, which harmonious resolves to the agent's primary at
// render time — tracking every atmosphere shift for free, no thread-local state.
/// Selectable accent color options with display labels.
pub const ACCENT_COLORS: &[(Color, &str)] = &[
(Color::RED, "Red"),
(Color::GREEN, "Green"),
(Color::BLUE, "Blue"),
(Color::YELLOW, "Yellow"),
(Color::MAGENTA, "Magenta"),
(Color::CYAN, "Cyan"),
];
thread_local! {
static ACCENT_COLOR: Cell<Color> = Cell::new(Color::BLUE);
static ACCENT: Cell<Style> = Cell::new(Style::new().fg(Color::BLUE));
}
/// Returns the current accent color.
/// The agent's primary accent color, resolved through the active atmosphere.
pub fn get_accent_color() -> Color {
ACCENT_COLOR.with(|c| c.get())
Color::YELLOW
}
/// Returns the current accent style.
/// The agent's primary accent as a `Style`.
pub fn get_accent() -> Style {
ACCENT.with(|c| c.get())
}
/// Sets the accent color by index into [`ACCENT_COLORS`].
pub fn set_accent_color(index: usize) {
let (color, _) = ACCENT_COLORS[index % ACCENT_COLORS.len()];
ACCENT_COLOR.with(|c| c.set(color));
ACCENT.with(|c| c.set(Style::new().fg(color)));
}
/// Returns the index into [`ACCENT_COLORS`] of the current accent color.
pub fn get_accent_index() -> usize {
let current = get_accent_color();
ACCENT_COLORS.iter().position(|(c, _)| *c == current).unwrap_or(2)
Style::new().fg(Color::YELLOW)
}
// ── GUI color scheme (light/dark) ─────────────────────────────────────────

View file

@ -1,12 +1,9 @@
//! Collapsible panel that animates between a header-only state and a bordered body.
//!
//! Ported from tuie-demo with palette-aware accent color and souveraine input patterns.
use std::time::{Duration, Instant};
use chord_macro::chord;
use tuie::prelude::*;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
struct Animation {
start: Instant,
@ -75,9 +72,9 @@ impl Accordion {
self.expanded = expanded;
let chevron = if expanded {
"\u{25bc}"
"v"
} else {
"\u{25b6}"
">"
};
if let Some(c) = self.root.get_widget_mut(self.chevron_id) {
c.set_content(chevron);
@ -132,8 +129,8 @@ impl DelegateWidget for Accordion {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::MouseDown(MouseButton::Left) => {
match &event.chord {
chord!(LeftClick) => {
let clicked_header = event.mouse_pos.y >= 0
&& event.mouse_pos.y < Self::HEADER_HEIGHT as i32;
if clicked_header {
@ -152,8 +149,8 @@ impl DelegateWidget for Accordion {
}
impl Accordion {
/// Creates a collapsed [`Accordion`] with the given `title`, `content` body, and `accent` color.
pub fn new(title: &str, content: Box<dyn Widget>, accent: Color) -> Box<Self> {
/// Creates a collapsed [`Accordion`] with the given `title` and `content` body.
pub fn new(title: &str, content: Box<dyn Widget>) -> Box<Self> {
let mut inner_id = WidgetId::EMPTY;
let mut clipper_id = WidgetId::EMPTY;
let mut title_id = WidgetId::EMPTY;
@ -182,7 +179,7 @@ impl Accordion {
.y_place(Place::Middle)
.children([
Text::new().content(title.to_string()).flex(1).id(&mut title_id),
Text::new().content("\u{25b6}").id(&mut chevron_id),
Text::new().content(">").id(&mut chevron_id),
]),
]);
@ -198,16 +195,24 @@ impl Accordion {
})
}
/// Creates an [`Accordion`] that starts expanded.
pub fn new_expanded(title: &str, content: Box<dyn Widget>, accent: Color) -> Box<Self> {
let mut acc = Self::new(title, content, accent);
acc.open();
acc
/// Updates the title text.
pub fn set_title(&mut self, title: &str) {
if let Some(t) = self.root.get_widget_mut(self.title_id) {
t.set_content(title.to_string());
}
}
/// Returns whether the accordion is currently expanded.
pub fn is_expanded(&self) -> bool {
self.expanded
/// Creates an expanded [`Accordion`] (opens immediately, no animation).
pub fn new_expanded(title: &str, content: Box<dyn Widget>) -> Box<Self> {
let mut acc = Self::new(title, content);
acc.expanded = true;
if let Some(c) = acc.root.get_widget_mut(acc.chevron_id) {
c.set_content("v");
}
if let Some(clipper) = acc.root.get_widget_mut(acc.clipper_id) {
clipper.set_height(None);
}
acc
}
/// Expands the body.
@ -225,11 +230,4 @@ impl Accordion {
}
self.set_expanded(false);
}
/// Updates the header title text.
pub fn set_title(&mut self, title: &str) {
if let Some(text) = self.root.get_widget_mut(self.title_id) {
text.set_content(title.to_string());
}
}
}

View file

@ -1,11 +1,7 @@
//! Bordered focusable button widget.
//!
//! Ported from tuie-demo with palette-aware accent color.
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use tuie::{delegate_field, prelude::*};
use chord_macro::chord;
use crate::ui::widgets::focus_pane::FocusPane;
@ -21,14 +17,14 @@ impl DelegateWidget for Button {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Enter) => {
match &event.chord {
chord!(Enter) => {
tuie::emit(self.get_id(), ClickEvent);
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
tuie::focus_widget(self.get_id());
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let size = self.get_rect_size();
let released_inside = Axis2D::all(|axis| {
event.mouse_pos[axis] >= 0 && event.mouse_pos[axis] < size[axis] as i32
@ -37,8 +33,8 @@ impl DelegateWidget for Button {
tuie::emit(self.get_id(), ClickEvent);
}
}
Trigger::MouseDrag(MouseButton::Left) => {}
Trigger::MouseHover => {}
chord!(LeftDrag) => {}
chord!(Hover) => {}
_ => return InputResult::Rejected,
}
InputResult::Handled
@ -50,10 +46,10 @@ impl DelegateWidget for Button {
}
impl Button {
/// Creates an empty button with the given accent color.
pub fn new(accent: Color) -> Box<Self> {
/// Creates an empty button.
pub fn new() -> Box<Self> {
Box::new(Self {
focus_pane: FocusPane::new(accent),
focus_pane: FocusPane::new(),
})
}
@ -67,4 +63,7 @@ impl Button {
}
self
}
delegate_field!(border_style: Style => focus_pane);
delegate_field!(selected_border_style: Option<Style> => focus_pane);
}

View file

@ -1,18 +1,15 @@
//! Two-state checkbox widget.
//!
//! Ported from tuie-demo with palette-aware accent color.
use tuie::{field, prelude::*};
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use chord_macro::chord;
/// Two-state checkbox that emits [`ChangeEvent<bool>`].
use crate::ui::theme;
/// Two-state checkbox that emits [`ChangeEvent`].
pub struct Checkbox {
root: Box<Pane>,
indicator_id: WidgetId<Text>,
checked: bool,
accent: Color,
}
impl Checkbox {
@ -22,7 +19,11 @@ impl Checkbox {
}
fn sync_indicator(&mut self) {
let icon = if self.checked { "[x]" } else { "[ ]" };
let icon = if self.checked {
"[x]"
} else {
"[ ]"
};
if let Some(text) = self.root.get_widget_mut(self.indicator_id) {
text.set_content(icon);
}
@ -39,9 +40,10 @@ impl DelegateWidget for Checkbox {
}
fn after_on_state_change(&mut self, state: WidgetState) {
let accent = theme::get_accent();
let style = match state {
WidgetState::Focused | WidgetState::FocusedHover => Style::new().fg(self.accent).bold(),
WidgetState::Active => Style::new().fg(self.accent),
WidgetState::Focused | WidgetState::FocusedHover => accent.bold(),
WidgetState::Active => accent,
_ => Style::new(),
};
self.root.set_style(style);
@ -51,19 +53,18 @@ impl DelegateWidget for Checkbox {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Enter) | Trigger::Key(Key::Char(' ')) => {
match &event.chord {
chord!(Enter | Space) => {
self.toggle();
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
tuie::focus_widget(self.get_id());
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let size = self.get_rect_size();
let inside = Axis2D::all(|a| {
if Axis2D::all(|a| {
event.mouse_pos[a] >= 0 && event.mouse_pos[a] < size[a] as i32
});
if inside {
}) {
self.toggle();
}
}
@ -74,8 +75,8 @@ impl DelegateWidget for Checkbox {
}
impl Checkbox {
/// Creates an unchecked checkbox with the given label and accent color.
pub fn new(label: Box<Text>, accent: Color) -> Box<Self> {
/// Creates an unchecked checkbox with the given label.
pub fn new(label: Box<Text>) -> Box<Self> {
let mut indicator_id = WidgetId::EMPTY;
let root = Pane::new()
.horizontal()
@ -88,14 +89,7 @@ impl Checkbox {
root,
indicator_id,
checked: false,
accent,
})
}
/// Sets the initial checked state.
pub fn with_checked(mut self: Box<Self>, checked: bool) -> Box<Self> {
self.checked = checked;
self.sync_indicator();
self
}
}

View file

@ -92,7 +92,7 @@ impl Cockpit {
.id(&mut pressure_label_id);
let pressure_bar = {
let mut b = ProgressBar::new();
b.set_color(dim);
b.get_layout_mut().style = Style::new().fg(dim);
b
};
let pbar_holder = pressure_bar.flex(1).id(&mut pressure_bar_id);
@ -177,7 +177,7 @@ impl Cockpit {
let color = pressure_color(pressure, palette);
if let Some(b) = self.root.get_widget_mut(self.pressure_bar_id) {
b.set_progress(pressure.clamp(0.0, 1.0));
b.set_color(color);
b.get_layout_mut().style = Style::new().fg(color);
}
if let Some(l) = self.root.get_widget_mut(self.pressure_label_id) {
l.set_content(StyledStr::new(&format!(" ctx {pct:>3}% ")).fg(color));

View file

@ -1,13 +1,9 @@
//! Numeric stepper widget with min/max clamping.
//!
//! Ported from tuie-demo with palette-aware accent color.
//! Numeric stepper widget.
use chord_macro::chord;
use axis2d::Axis2D;
use std::any::Any;
use tuie::{field, prelude::*};
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
fn normalize_numeric(s: &str) -> String {
if s.is_empty() {
@ -15,7 +11,11 @@ fn normalize_numeric(s: &str) -> String {
}
let neg = s.starts_with('-');
let digits = s.trim_start_matches('-').trim_start_matches('0');
let digits = if digits.is_empty() { "0" } else { digits };
let digits = if digits.is_empty() {
"0"
} else {
digits
};
if neg && digits != "0" {
format!("-{}", digits)
} else {
@ -40,8 +40,12 @@ impl NumericBindings {
}
impl InputBindings<Text> for NumericBindings {
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn configure_state(&self, state: &mut EditorState<Text>) {
self.inner.configure_state(state);
state.inclusive_selection = false;
@ -55,6 +59,7 @@ impl InputBindings<Text> for NumericBindings {
fn on_blur(&mut self, state: &mut EditorState<Text>, text: &Text) {
self.inner.on_blur(state, text);
}
fn on_input(
&mut self,
state: &mut EditorState<Text>,
@ -263,8 +268,8 @@ impl DelegateWidget for Counter {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::MouseDown(MouseButton::Left) => {
match &event.chord {
chord!(LeftClick) => {
tuie::focus_widget(self.input_id);
let mouse_pos = event.mouse_pos;
if let Some(which) = self.hit_test_buttons(mouse_pos) {
@ -274,7 +279,7 @@ impl DelegateWidget for Counter {
}
InputResult::Rejected
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let mouse_pos = event.mouse_pos;
queue.next();
if let Some(which) = self.pressed {
@ -286,28 +291,33 @@ impl DelegateWidget for Counter {
self.set_pressed(None);
InputResult::Handled
}
Trigger::Key(Key::Enter) => {
chord!(Enter) => {
queue.next();
self.commit_input();
InputResult::Handled
}
Trigger::Key(Key::Arrow(Direction2D::Up)) => {
chord!(Up) => {
queue.next();
self.adjust(1);
self.select_all_input();
InputResult::Handled
}
Trigger::Key(Key::Arrow(Direction2D::Down)) => {
chord!(Down) => {
queue.next();
self.adjust(-1);
self.select_all_input();
InputResult::Handled
}
Trigger::Key(Key::Tab) => {
chord!(Tab) => {
queue.next();
tuie::focus_next_tab_order(Sign::Positive);
InputResult::Handled
}
chord!(Shift + Tab) => {
queue.next();
tuie::focus_next_tab_order(Sign::Negative);
InputResult::Handled
}
_ => InputResult::Rejected,
}
}
@ -347,8 +357,8 @@ impl DelegateWidget for Counter {
}
impl Counter {
/// Creates a [`Counter`] with an optional trailing `label` and accent `color`.
pub fn new(label: &str, color: Color) -> Box<Self> {
/// Creates a [`Counter`] with an optional trailing `label`.
pub fn new(label: &str) -> Box<Self> {
let mut minus_id = WidgetId::EMPTY;
let mut input_id = WidgetId::EMPTY;
let mut plus_id = WidgetId::EMPTY;
@ -383,9 +393,10 @@ impl Counter {
value: 0,
min: i32::MIN,
max: i32::MAX,
color,
color: Color::BLUE,
selected: false,
pressed: None,
})
}
}

View file

@ -84,16 +84,16 @@ impl DelegateWidget for Dropdown {
impl Dropdown {
/// Creates a dropdown over `labels` with `selected` initially chosen.
pub fn new(labels: &[&str], selected: usize, accent: Color) -> Box<Self> {
pub fn new(labels: &[&str], selected: usize) -> Box<Self> {
let label = labels.get(selected).copied().unwrap_or("");
let text = Text::new().content(format!(" {label} \u{25be}"));
let mut trigger = FocusPane::new(accent);
let mut trigger = FocusPane::new();
trigger.add_child(text as Box<dyn Widget>);
Box::new(Self {
trigger,
labels: labels.iter().map(|s| s.to_string()).collect(),
selected,
accent,
accent: Color::YELLOW,
})
}
@ -110,10 +110,8 @@ impl Dropdown {
for (i, label) in self.labels.iter().enumerate() {
let mut id = WidgetId::EMPTY;
let marker = if i == self.selected { "\u{203a}" } else { " " };
let row = FlatButton::new(accent)
.children([
Text::new().content(format!(" {marker} {label} ")) as Box<dyn Widget>,
])
let row = FlatButton::new()
.child(Text::new().content(format!(" {marker} {label} ")))
.id(&mut id);
row_ids.push(id.untyped());
list = list.children([row as Box<dyn Widget>]);

View file

@ -1,23 +1,15 @@
//! Borderless focusable button with accent hover/focus/active states.
//!
//! Ported from tuie-demo with palette-aware accent color (the demo pulled the
//! accent from a global theme; here it is injected per-instance like the rest
//! of souveraine's widgets).
//! Borderless focusable button widget.
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use chord_macro::chord;
use tuie::render::border;
/// Borderless focusable button that emits [`ClickEvent`]. Unlike [`Button`],
/// it has no border — it tints its background on hover and reverses to the
/// accent color on focus/press.
///
/// [`Button`]: crate::ui::widgets::button::Button
pub struct FlatButton {
use crate::ui::theme;
/// Borderless focusable button that emits [`ClickEvent`].
pub(crate) struct FlatButton {
pane: Box<Pane>,
bg: Color,
accent: Color,
state: WidgetState,
base_style: Option<Style>,
}
@ -34,12 +26,12 @@ impl FlatButton {
}
WidgetState::Focused | WidgetState::FocusedHover => {
style.bg = None;
style.fg = Some(self.accent);
style.fg = Some(theme::get_accent_color());
style.set_reverse(true);
}
WidgetState::Active => {
style.bg = None;
style.fg = Some(self.accent);
style.fg = Some(theme::get_accent_color());
style.set_reverse(true);
style.set_blend(Some(75));
}
@ -55,17 +47,18 @@ impl DelegateWidget for FlatButton {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Enter) => {
match &event.chord {
chord!(Enter) => {
tuie::emit(self.get_id(), ClickEvent);
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
tuie::focus_widget(self.get_id());
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let size = self.get_rect_size();
let inside = Axis2D::all(|a| {
event.mouse_pos[a] >= 0 && event.mouse_pos[a] < size[a] as i32
event.mouse_pos[a] >= 0
&& event.mouse_pos[a] < size[a] as i32
});
if inside {
tuie::emit(self.get_id(), ClickEvent);
@ -92,32 +85,22 @@ impl DelegateWidget for FlatButton {
}
impl FlatButton {
/// Creates an empty flat button with the given `accent` color. The resting
/// background defaults to a low-contrast surface tint.
pub fn new(accent: Color) -> Box<Self> {
/// Creates an empty flat button with no children.
pub(crate) fn new() -> Box<Self> {
Box::new(Self {
pane: Pane::new(),
bg: Color::grey256(5),
accent,
bg: border::config::get().style.bg.unwrap_or(Color::grey256(5)),
state: WidgetState::None,
base_style: None,
})
}
/// Overrides the resting background tint.
pub fn bg(mut self: Box<Self>, bg: Color) -> Box<Self> {
self.bg = bg;
self
}
/// Appends the given children to the button in order.
pub fn children<const N: usize>(
/// Appends `child` to the button.
pub(crate) fn child<T: Widget + 'static>(
mut self: Box<Self>,
children: [Box<dyn Widget>; N],
child: Box<T>,
) -> Box<Self> {
for child in children {
self.pane.add_child(child);
}
self.pane.add_child(child);
self
}
}

View file

@ -1,31 +1,30 @@
//! Bordered pane that highlights itself when selected or active.
//!
//! Ported from tuie-demo with palette-aware accent color.
use tuie::{delegate_field, field, prelude::*};
use tuie::render::border;
use crate::ui::theme;
/// Bordered [`Pane`] wrapper that highlights its border when focused.
pub struct FocusPane {
pub(crate) struct FocusPane {
pane: Box<Pane>,
border_style: Style,
selected_border_style: Option<Style>,
accent: Color,
}
impl FocusPane {
fn refresh_border(&mut self) {
let cfg = border::config::get();
let focused = tuie::runtime::is_focus_chain(self.pane.get_id());
if focused {
let style = self
.selected_border_style
.unwrap_or_else(|| Style::new().fg(self.accent).bold());
.unwrap_or_else(|| cfg.selected_style.apply(theme::get_accent()));
self.pane.set_border_style(style);
self.pane.set_border(Some(Border::THICK));
self.pane.set_border(Some(cfg.selected_border));
} else {
self.pane.set_border_style(self.border_style);
self.pane.set_border(Some(Border::SINGLE));
self.pane.set_border(Some(cfg.border));
}
}
@ -48,24 +47,23 @@ impl DelegateWidget for FocusPane {
impl FocusPane {
/// Creates an empty bordered focus pane.
pub fn new(accent: Color) -> Box<Self> {
pub(crate) fn new() -> Box<Self> {
let mut pane = Pane::new();
pane.set_bordered(true);
Box::new(Self {
pane,
border_style: Style::new().fg(Color::grey256(6)).dim(),
border_style: Style::new(),
selected_border_style: None,
accent,
})
}
/// Appends a child widget.
pub fn add_child(&mut self, widget: Box<dyn Widget>) {
pub(crate) fn add_child(&mut self, widget: Box<dyn Widget>) {
self.pane.add_child(widget);
}
/// Appends `children` to the pane.
pub fn children<const N: usize>(
pub(crate) fn children<const N: usize>(
mut self: Box<Self>,
children: [Box<dyn Widget>; N],
) -> Box<Self> {
@ -75,12 +73,6 @@ impl FocusPane {
self
}
/// Sets the accent color used for the focused border.
pub fn set_accent(&mut self, accent: Color) {
self.accent = accent;
self.refresh_border();
}
field!(border_style: Style; sync_border_style);
field!(selected_border_style: Option<Style>);

View file

@ -1,13 +1,6 @@
//! App-wide key chord handler.
//!
//! Wraps the root widget and intercepts global keyboard shortcuts using
//! Trigger-based input matching (not chord! macros). Souveraine already
//! handles Ctrl+S (save) and Esc (per-screen back) elsewhere, so those
//! are skipped here.
use tuie::input::key::Key;
use tuie::input::modifiers::Modifier;
use tuie::input::trigger::Trigger;
use chord_macro::chord;
use tuie::prelude::*;
/// Root widget wrapper that intercepts app-wide key chords.
@ -22,41 +15,35 @@ impl DelegateWidget for GlobalChords {
let Some(event) = queue.peek() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Tab)
if !event.chord.modifiers.has(Modifier::Shift) =>
{
match &event.chord {
chord!(Tab) if queue.is_unhandled() => {
queue.next();
tuie::focus_next_tab_order(Sign::Positive);
}
Trigger::Key(Key::Tab)
if event.chord.modifiers.has(Modifier::Shift) =>
{
chord!(Shift + Tab) if queue.is_unhandled() => {
queue.next();
tuie::focus_next_tab_order(Sign::Negative);
}
Trigger::Key(Key::Char('z'))
if event.chord.modifiers.has(Modifier::Ctrl) =>
{
chord!(Ctrl + z) => {
queue.next();
let _ = tuie::suspend();
}
#[cfg(feature = "gui")]
Trigger::Key(Key::Char('+'))
if event.chord.modifiers.has(Modifier::Ctrl) =>
{
chord!(Ctrl + Char('+')) => {
queue.next();
let cur = tuie::gui::config::get().font_size;
tuie::gui::set_font_size((cur + 1.0).min(72.0));
}
#[cfg(feature = "gui")]
Trigger::Key(Key::Char('-'))
if event.chord.modifiers.has(Modifier::Ctrl) =>
{
chord!(Ctrl + Char('-')) => {
queue.next();
let cur = tuie::gui::config::get().font_size;
tuie::gui::set_font_size((cur - 1.0).max(6.0));
}
chord!(Ctrl + (c | q)) => {
queue.next();
tuie::quit(0);
}
_ => return InputResult::Rejected,
}
InputResult::Handled

View file

@ -3,7 +3,7 @@
use tuie::prelude::*;
/// One-cell-tall horizontal divider.
pub struct HorizontalRule {
pub(crate) struct HorizontalRule {
layout: Layout,
}
@ -30,22 +30,15 @@ impl Widget for HorizontalRule {
fn render(&self, mut ctx: RenderContext) {
ctx.set_style(self.layout.style);
ctx.fill("\u{2500}");
ctx.fill("");
}
}
impl HorizontalRule {
/// Creates a [`HorizontalRule`] with a dim foreground.
pub fn new() -> Box<Self> {
pub(crate) fn new() -> Box<Self> {
let mut layout = Layout::new();
layout.style = Style::new().fg(Color::grey256(6));
Box::new(Self { layout })
}
/// Creates a [`HorizontalRule`] with the given accent color.
pub fn with_color(color: Color) -> Box<Self> {
let mut layout = Layout::new();
layout.style = Style::new().fg(color);
Box::new(Self { layout })
}
}

View file

@ -1,16 +1,12 @@
//! Clickable text link widget.
//!
//! Ported from tuie-demo with palette-aware accent color and souveraine's
//! `Trigger`-based input matching. Renders an underlined label that opens its
//! URL in the system browser when activated.
use std::cell::Cell;
use std::process::{Command, Stdio};
use chord_macro::chord;
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use crate::ui::theme;
/// Focusable text link that opens a URL when activated.
pub struct Link {
@ -18,7 +14,6 @@ pub struct Link {
label: String,
url: String,
state: Cell<WidgetState>,
accent: Color,
}
impl Link {
@ -79,9 +74,9 @@ impl Widget for Link {
fn render(&self, mut ctx: RenderContext) {
let base = self.layout.style;
let style = if matches!(self.state.get(), WidgetState::Active) {
base.fg(self.accent).underline(UnderlineType::Single)
theme::get_accent().apply(base).underline(UnderlineType::Single)
} else if self.is_focus_chain() {
base.fg(self.accent).bold().underline(UnderlineType::Single)
theme::get_accent().bold().apply(base).underline(UnderlineType::Single)
} else {
base.underline(UnderlineType::Single)
};
@ -96,11 +91,11 @@ impl Widget for Link {
let Some(event) = queue.next() else {
return InputResult::Rejected;
};
match &event.chord.trigger {
Trigger::Key(Key::Enter) => {
match &event.chord {
chord!(Enter) => {
self.open_url();
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let size = self.get_rect_size();
if Axis2D::all(|a| {
event.mouse_pos[a] >= 0 && event.mouse_pos[a] < size[a] as i32
@ -109,7 +104,7 @@ impl Widget for Link {
self.open_url();
}
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
tuie::focus_widget(self.get_id());
}
_ => return InputResult::Rejected,
@ -120,13 +115,12 @@ impl Widget for Link {
impl Link {
/// Creates a link with the given visible `label` that opens `url` when activated.
pub fn new(label: &str, url: &str, accent: Color) -> Box<Self> {
pub fn new(label: &str, url: &str) -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
label: label.to_string(),
url: url.to_string(),
state: Cell::new(WidgetState::None),
accent,
})
}
}

View file

@ -1,7 +1,4 @@
//! Stack-based page navigation widget.
//!
//! Manages a stack of pages with push/pop navigation and focus restoration.
//! Ported from tuie-demo.
use tuie::prelude::*;
@ -11,7 +8,7 @@ struct PageEntry {
}
/// Page navigation stack.
pub struct PageLayout {
pub(crate) struct PageLayout {
pane: Box<Pane>,
stack: Vec<PageEntry>,
top_id: WidgetId,
@ -22,8 +19,7 @@ impl DelegateWidget for PageLayout {
}
impl PageLayout {
/// Creates a new [`PageLayout`] with `home` as the root page.
pub fn new(home: Box<dyn Widget>) -> Box<Self> {
pub(crate) fn new(home: Box<dyn Widget>) -> Box<Self> {
let top_id = home.get_id();
let pane = Pane::new()
.vertical()
@ -42,8 +38,7 @@ impl PageLayout {
})
}
/// Pushes `content` onto the page stack, saving the current focus.
pub fn push(&mut self, content: Box<dyn Widget>) {
pub(crate) fn push(&mut self, content: Box<dyn Widget>) {
if let Some(entry) = self.stack.last_mut() {
entry.saved_widget = tuie::get_focused_widget();
}
@ -62,8 +57,7 @@ impl PageLayout {
tuie::dirty_layout();
}
/// Pops the top page, restoring the previous page and its focus.
pub fn pop(&mut self) {
pub(crate) fn pop(&mut self) {
if self.stack.len() <= 1 {
return;
}
@ -82,9 +76,4 @@ impl PageLayout {
}
}
}
/// Returns the number of pages on the stack.
pub fn depth(&self) -> usize {
self.stack.len()
}
}

View file

@ -1,15 +1,11 @@
//! Grid-based point picker widget.
//!
//! Ported from tuie-demo with palette-aware accent color and souveraine's
//! `Trigger`-based input matching. A 2D analog of a radio group — pick one cell
//! from an `n×m` grid. Emits `ChangeEvent<Vec2<u16>>` on selection.
use std::cell::Cell;
use chord_macro::chord;
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use crate::ui::theme;
/// Interactive grid for selecting a point cell.
pub struct PointPicker {
@ -17,7 +13,6 @@ pub struct PointPicker {
size: Cell<Vec2<u16>>,
selected: Cell<Vec2<u16>>,
pressed: Cell<Option<Vec2<u16>>>,
accent: Color,
}
impl PointPicker {
@ -101,21 +96,22 @@ impl Widget for PointPicker {
let cell = Vec2::new(col, row);
let is_selected = cell == selected;
let is_pressed = pressed == Some(cell);
let accent = theme::get_accent_color();
let style = if is_selected {
if focused {
Style::new().fg(self.accent).bold()
Style::new().fg(accent).bold()
} else {
Style::new().fg(Color::Foreground).bold()
}
} else if is_pressed {
Style::new().fg(self.accent)
Style::new().fg(accent)
} else {
Style::new().fg(Color::grey256(7))
};
let marker = if is_selected && !is_pressed {
"\u{25cf}"
""
} else {
"\u{00b7}"
"·"
};
let x = (col * Self::CELL_W + Self::CELL_W / 2) as i32;
let y = (row * Self::CELL_H) as i32;
@ -137,34 +133,34 @@ impl Widget for PointPicker {
let size = self.size.get();
let selected = self.selected.get();
match &event.chord.trigger {
Trigger::Key(Key::Arrow(Direction2D::Left)) => {
match &event.chord {
chord!(Left | h) => {
if selected.x > 0 {
self.select_cell(Vec2::new(selected.x - 1, selected.y));
}
}
Trigger::Key(Key::Arrow(Direction2D::Right)) => {
chord!(Right | l) => {
if selected.x + 1 < size.x {
self.select_cell(Vec2::new(selected.x + 1, selected.y));
}
}
Trigger::Key(Key::Arrow(Direction2D::Up)) => {
chord!(Up | k) => {
if selected.y > 0 {
self.select_cell(Vec2::new(selected.x, selected.y - 1));
}
}
Trigger::Key(Key::Arrow(Direction2D::Down)) => {
chord!(Down | j) => {
if selected.y + 1 < size.y {
self.select_cell(Vec2::new(selected.x, selected.y + 1));
}
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
if let Some(cell) = self.hit_cell(event.mouse_pos) {
tuie::focus_widget(self.get_id());
self.set_pressed(Some(cell));
}
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let pressed = self.pressed.get();
self.set_pressed(None);
if let Some(cell) = self.hit_cell(event.mouse_pos) {
@ -180,23 +176,16 @@ impl Widget for PointPicker {
}
impl PointPicker {
/// Creates a `3×3` [`PointPicker`] with the top-left cell selected.
pub fn new(accent: Color) -> Box<Self> {
/// Creates a [`PointPicker`] with the top-left cell selected.
pub fn new() -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
size: Cell::new(Vec2::new(3, 3)),
selected: Cell::new(Vec2::of(0)),
pressed: Cell::new(None),
accent,
})
}
/// Sets the grid dimensions in cells.
pub fn grid(self: Box<Self>, size: Vec2<u16>) -> Box<Self> {
self.size.set(size);
self
}
/// Sets the initially selected `point`.
pub fn point(self: Box<Self>, point: Vec2<u16>) -> Box<Self> {
self.selected.set(point);

View file

@ -1,46 +1,22 @@
//! Horizontal eighth-block progress bar widget.
//! Horizontal progress bar widget.
use tuie::{field, prelude::*};
/// Horizontal progress bar using eighth-block Unicode characters for smooth
/// fill rendering. Supports an accent color that can be changed at runtime.
pub struct ProgressBar {
/// Horizontal progress bar.
pub(crate) struct ProgressBar {
layout: Layout,
progress: f32,
color: Color,
}
impl ProgressBar {
/// Creates a new [`ProgressBar`] with zero progress and a dim grey color.
pub fn new() -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
progress: 0.0,
color: Color::grey256(6),
})
}
fn clamp_and_dirty(&mut self) {
self.progress = self.progress.clamp(0.0, 1.0);
self.dirty_paint();
}
/// Sets the bar's fill color.
pub fn set_color(&mut self, color: Color) {
self.color = color;
self.dirty_paint();
}
field!(progress: f32; clamp_and_dirty);
}
impl Widget for ProgressBar {
fn get_layout(&self) -> &Layout {
&self.layout
}
fn get_layout_mut(&mut self) -> &mut Layout {
&mut self.layout
}
fn get_name(&self) -> &'static str {
"ProgressBar"
}
@ -67,23 +43,37 @@ impl Widget for ProgressBar {
row.push('\u{2588}');
}
if remainder > 0 && full_cells < width {
row.push(match remainder {
let partial = match remainder {
7 => '\u{2589}',
6 => '\u{258A}',
5 => '\u{258B}',
4 => '\u{258C}',
3 => '\u{258D}',
2 => '\u{258E}',
_ => '\u{258F}',
});
}
let drawn = row.chars().count() as u16;
for _ in drawn..width {
row.push('\u{2591}');
1 => '\u{258F}',
_ => unreachable!(),
};
row.push(partial);
}
ctx.set_style(Style::new().fg(self.color));
ctx.set_style(self.layout.style);
ctx.move_to((0, 0).into());
ctx.write(&row);
}
}
impl ProgressBar {
pub(crate) fn new() -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
progress: 0.0,
})
}
fn clamp_and_dirty(&mut self) {
self.progress = self.progress.clamp(0.0, 1.0);
self.dirty_paint();
}
field!(progress: f32; clamp_and_dirty);
}

View file

@ -1,26 +1,18 @@
//! Vertical radio group control.
//!
//! Ported from tuie-demo with palette-aware accent color and souveraine's
//! `Trigger`-based input matching. Good for small mutually-exclusive enums
//! where a horizontal [`SegmentedControl`] gets cramped.
//!
//! [`SegmentedControl`]: crate::ui::widgets::segmented_control::SegmentedControl
use std::cell::Cell;
use chord_macro::chord;
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
/// Vertical group of mutually exclusive labeled radio options. Emits
/// `ChangeEvent<usize>` when the selection changes.
use crate::ui::theme;
/// Vertical group of mutually exclusive labeled radio options.
pub struct RadioGroup {
layout: Layout,
labels: Vec<String>,
selected: Cell<usize>,
pressed: Cell<Option<usize>>,
accent: Color,
}
impl RadioGroup {
@ -92,12 +84,13 @@ impl Widget for RadioGroup {
let selected = self.selected.get();
let pressed = self.pressed.get();
let base = self.layout.style;
let accent = theme::get_accent_color();
let marker_style = if self.is_focus_chain() {
base.fg(self.accent).bold()
base.fg(accent).bold()
} else {
base.bold()
};
let pressed_style = base.fg(self.accent);
let pressed_style = base.fg(accent);
ctx.set_style(base);
ctx.clear();
@ -105,7 +98,11 @@ impl Widget for RadioGroup {
for (i, label) in self.labels.iter().enumerate() {
let is_pressed = pressed == Some(i);
let is_selected = i == selected;
let marker = if is_selected { "(*)" } else { "( )" };
let marker = if is_selected {
"(*)"
} else {
"( )"
};
let row_style = if is_pressed {
pressed_style
} else if is_selected {
@ -129,24 +126,24 @@ impl Widget for RadioGroup {
let count = self.labels.len();
let selected = self.selected.get();
match &event.chord.trigger {
Trigger::Key(Key::Arrow(Direction2D::Up)) => {
match &event.chord {
chord!(Up | k) => {
if selected > 0 {
self.select_index(selected - 1);
}
}
Trigger::Key(Key::Arrow(Direction2D::Down)) => {
chord!(Down | j) => {
if selected + 1 < count {
self.select_index(selected + 1);
}
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
if let Some(i) = self.hit_option(event.mouse_pos) {
tuie::focus_widget(self.get_id());
self.set_pressed(Some(i));
}
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let pressed = self.pressed.get();
self.set_pressed(None);
if let Some(i) = self.hit_option(event.mouse_pos) {
@ -163,13 +160,12 @@ impl Widget for RadioGroup {
impl RadioGroup {
/// Creates a [`RadioGroup`] with one option per label.
pub fn new(labels: &[&str], accent: Color) -> Box<Self> {
pub fn new(labels: &[&str]) -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
labels: labels.iter().map(|&l| l.to_string()).collect(),
selected: Cell::new(0),
pressed: Cell::new(None),
accent,
})
}
@ -178,9 +174,4 @@ impl RadioGroup {
self.selected.set(index);
self
}
/// Returns the selected option index.
pub fn get_selected(&self) -> usize {
self.selected.get()
}
}

View file

@ -1,13 +1,11 @@
//! Horizontal toggle control with mutually exclusive labeled segments.
//!
//! Ported from tuie-demo with palette-aware accent color.
use std::cell::Cell;
use chord_macro::chord;
use tuie::prelude::*;
use tuie::input::key::Key;
use tuie::input::mouse::MouseButton;
use tuie::input::trigger::Trigger;
use crate::ui::theme;
/// Horizontal toggle with mutually exclusive labeled segments.
pub struct SegmentedControl {
@ -16,7 +14,6 @@ pub struct SegmentedControl {
selected: Cell<usize>,
pressed: Cell<Option<usize>>,
disabled: Cell<u32>,
accent: Color,
}
impl SegmentedControl {
@ -106,12 +103,13 @@ impl Widget for SegmentedControl {
let selected = self.selected.get();
let pressed = self.pressed.get();
let base = self.layout.style;
let accent = theme::get_accent_color();
let selected_style = if self.is_focus_chain() {
Style::new().fg(Color::BLACK).bg(self.accent).bold()
Style::new().fg(Color::BLACK).bg(accent).bold()
} else {
base.reverse().bold()
};
let pressed_style = base.fg(self.accent);
let pressed_style = base.fg(accent);
ctx.set_style(base);
ctx.clear();
@ -134,7 +132,7 @@ impl Widget for SegmentedControl {
if needs_separator {
ctx.set_style(base.fg(Color::Rgb(80, 80, 80)));
write!(ctx, "\u{258f}");
write!(ctx, "");
ctx.set_style(style);
write!(ctx, "{} ", label);
} else {
@ -150,18 +148,18 @@ impl Widget for SegmentedControl {
};
let selected = self.selected.get();
match &event.chord.trigger {
Trigger::Key(Key::Arrow(Direction2D::Left)) => {
match &event.chord {
chord!(Left | h) => {
if let Some(i) = self.step(selected, -1) {
self.select_index(i);
}
}
Trigger::Key(Key::Arrow(Direction2D::Right)) => {
chord!(Right | l) => {
if let Some(i) = self.step(selected, 1) {
self.select_index(i);
}
}
Trigger::MouseDown(MouseButton::Left) => {
chord!(LeftClick) => {
if let Some(i) = self.hit_segment(event.mouse_pos) {
if !self.is_disabled(i) {
tuie::focus_widget(self.get_id());
@ -169,7 +167,7 @@ impl Widget for SegmentedControl {
}
}
}
Trigger::MouseUp(MouseButton::Left) => {
chord!(LeftRelease) => {
let pressed = self.pressed.get();
self.set_pressed(None);
if let Some(i) = self.hit_segment(event.mouse_pos) {
@ -186,14 +184,13 @@ impl Widget for SegmentedControl {
impl SegmentedControl {
/// Creates a [`SegmentedControl`] with one segment per label.
pub fn new(labels: &[&str], accent: Color) -> Box<Self> {
pub fn new(labels: &[&str]) -> Box<Self> {
Box::new(Self {
layout: Layout::new(),
labels: labels.iter().map(|&l| l.to_string()).collect(),
selected: Cell::new(0),
pressed: Cell::new(None),
disabled: Cell::new(0),
accent,
})
}
@ -239,10 +236,4 @@ impl SegmentedControl {
tuie::dirty_paint();
}
}
/// Sets the accent color.
pub fn set_accent(&mut self, accent: Color) {
self.accent = accent;
tuie::dirty_paint();
}
}