🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
//! `Button` — przycisk z etykietą (warianty: domyślny, płaski, sugerowany, destrukcyjny).
//!
//! Widget immediate: trzyma wygląd (etykieta, wariant, stan), a aplikacja wywołuje
//! [`Button::paint`] i [`Button::click_at`].
use libluna_core::style::{weight, ColorScheme};
use libluna_core::{Color, Point, Rect, Theme};
use libluna_render::TextEngine;
use crate::ctx::PaintCtx;
use crate::icon;
/// Wariant wizualny przycisku.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonVariant {
/// Tło „shade” (jak przyciski w HeaderBarze).
#[default]
Default,
/// Bez tła; tło pojawia się przy najechaniu/wciśnięciu.
Flat,
/// Sugerowana akcja (akcent).
Suggested,
/// Akcja destrukcyjna (kolor błędu).
Destructive,
}
/// Przycisk z etykietą.
#[derive(Debug, Clone)]
pub struct Button {
label: String,
variant: ButtonVariant,
dropdown: bool,
enabled: bool,
hovered: bool,
pressed: bool,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
variant: ButtonVariant::Default,
dropdown: false,
enabled: true,
hovered: false,
pressed: false,
}
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
/// Dodaje na końcu wskaźnik rozwijania (chevron) — przyciski menu.
pub fn dropdown(mut self, dropdown: bool) -> Self {
self.dropdown = dropdown;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn set_hovered(&mut self, hovered: bool) {
self.hovered = hovered;
}
pub fn set_pressed(&mut self, pressed: bool) {
self.pressed = pressed;
}
pub fn label(&self) -> &str {
&self.label
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Szerokość dopasowana do etykiety.
pub fn width(&self, text: &TextEngine, theme: &Theme) -> f64 {
let m = theme.metrics.button;
let body = theme.typography.body.size;
let mut w = m.pad_x * 2.0 + text.measure(&self.label, body, weight::NORMAL).width;
if self.dropdown {
w += m.chevron_gap + m.chevron;
}
w
}
pub fn height(&self, theme: &Theme) -> f64 {
theme.metrics.button.height
}
pub fn hit(&self, rect: Rect, p: Point) -> bool {
rect.contains(p)
}
/// Zwraca `true`, jeśli klik trafił w aktywny przycisk.
pub fn click_at(&self, rect: Rect, p: Point) -> bool {
self.enabled && rect.contains(p)
}
pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
let theme = *ctx.theme;
let colors = theme.colors;
let radius = theme.radius.small;
let strong = self.pressed || self.hovered;
match self.variant {
ButtonVariant::Suggested => {
ctx.scene.fill_rounded_rect(rect, radius, colors.accent);
}
ButtonVariant::Destructive => {
ctx.scene.fill_rounded_rect(rect, radius, colors.error);
}
ButtonVariant::Default => {
ctx.scene
.fill_rounded_rect(rect, radius, shade(&colors, strong));
}
ButtonVariant::Flat => {
if strong {
ctx.scene
.fill_rounded_rect(rect, radius, shade(&colors, false));
}
}
}
let fg = match self.variant {
ButtonVariant::Suggested | ButtonVariant::Destructive => colors.accent_fg,
_ if self.enabled => colors.text,
_ => colors.dim_text,
};
let body = theme.typography.body.size;
let m = theme.metrics.button;
let cy = rect.center().y;
let label_w = ctx.measure(&self.label, body, weight::NORMAL);
let content_w = label_w
+ if self.dropdown {
m.chevron_gap + m.chevron
} else {
0.0
};
let start_x = rect.center().x - content_w / 2.0;
ctx.label_left(start_x, cy, &self.label, body, weight::NORMAL, fg);
if self.dropdown {
let cx = start_x + label_w + m.chevron_gap + m.chevron / 2.0;
icon::chevron_down(ctx, fg, Point::new(cx, cy), 4.0, 2.0);
}
}
}
fn shade(colors: &ColorScheme, strong: bool) -> Color {
if strong {
colors.shade.scale_alpha(1.8)
} else {
colors.shade
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn click_only_when_enabled_and_inside() {
let rect = Rect::from_xywh(0.0, 0.0, 100.0, 34.0);
let b = Button::new("OK");
assert!(b.click_at(rect, Point::new(50.0, 17.0)));
assert!(!b.click_at(rect, Point::new(200.0, 17.0)));
let disabled = Button::new("OK").enabled(false);
assert!(!disabled.click_at(rect, Point::new(50.0, 17.0)));
}
#[test]
fn width_grows_with_dropdown() {
let theme = Theme::light();
let text = TextEngine::system();
let plain = Button::new("Opcje");
let dd = Button::new("Opcje").dropdown(true);
assert!(dd.width(&text, &theme) > plain.width(&text, &theme));
}
}