`Button::tooltip( text )` registers a hint string that fires after a 600 ms pointer dwell. `LaidOutWidget` gains a `tooltip: Option<String>` field, `Element::tooltip()` exposes it to the input layer, and the existing pointer-hover path now calls `arm_tooltip` on hover-enter and `cancel_tooltip` on hover-leave / touch. The deadline is polled alongside `next_long_press_wakeup` in `try_run` so an idle pointer still gets a wake-up at the firing instant; on fire, `tooltip_overlay()` synthesises an `OverlaySpec` — a rounded `text_primary @ 95%` pill drawn with `bg` text — anchored above the hovered widget, flipping below or clamping inside the screen if it would clip, and pushed alongside the app's own overlays both in the redraw path and in `reconcile_overlays` so the layer surface is created the same frame the tooltip becomes visible. Pointer-only by design: touch events explicitly cancel because a tap-and-release should never linger into a hint. The `showcase` example wires `.tooltip(..)` on the three button variants as a smoke test. The drag pipeline now reports positions in main-surface (global) coordinates instead of per-surface. `surface_offset_for( focus )` derives the top-left of an overlay surface from `SurfaceState::layer_anchor` — newly stored at `reconcile_overlays` time from `OverlaySpec::anchor` — combined with the main surface's dimensions; `on_drag_move`, `on_drop`, the synthetic move emitted on drag-promotion in `pointer.rs`, and the `pending_drag_inits` push site in `gesture::start_drag` all translate before handing coordinates to the app. The motion and release paths additionally `request_redraw()` every overlay so a dock-style drop target painted on an `Anchor::Bottom` layer surface gets repainted as the drag moves — without that, the visible drop indicator only updates when the cursor re-enters the main surface. Drops still target whichever surface fired the release; only the coordinates are unified. `ForeignToplevelListHandler` previously read `app_id` directly via `ForeignToplevelList::info()`. Clients that never set `app_id` (some winit-windowed compositors, simple test clients) were silently invisible to crustace-style docks because the empty string fell through `unwrap_or_default()` and the dock then keyed entries off `""`. `toplevel_display_id()` cascades: prefer `app_id` for desktop-entry matching, fall back to `title` for human-readable identification, and finally to the protocol-issued `identifier` which is always present and unique per handle. Applied to both `new_toplevel` and `update_toplevel`. `theme::system_fontdb()` lazily loads the system font database once via `OnceLock` and reuses the `Arc` for every `decode_svg_bytes` call. resvg's default `Options::fontdb` is empty, so any SVG containing `<text>` rendered with the built-in fallback font or no font at all; with the system DB attached, icons and decorative SVGs with embedded labels now resolve glyphs correctly. Cached because `load_system_fonts()` walks every font path on the system and is comfortably tens of milliseconds on a cold cache — not something to repeat per icon decode. `themes/default/theme.json` tweaks one variant's slot palette: `surface-alt` from `@indigo/D9` to `@white/D9` and `text-primary` from `@white` to `@navy`, plus a cosmetic re-alignment of the `"value"` columns in the same slots block.
604 lines
19 KiB
Rust
604 lines
19 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use std::sync::Arc;
|
|
use crate::types::{ Color, Rect, WidgetId };
|
|
use crate::render::Canvas;
|
|
use super::Element;
|
|
|
|
// Theme colors driven by the process-wide palette (see `ltk::theme`).
|
|
// Non-colour geometry (radius, font size, focus width, etc.) is static — only
|
|
// palette tokens respond to light/dark mode.
|
|
mod theme
|
|
{
|
|
use crate::types::Color;
|
|
|
|
/// Primary button background — brand accent.
|
|
pub fn p_default_bg() -> Color { crate::theme::palette().accent }
|
|
/// Primary button border — a darker tone of the accent so the pill
|
|
/// reads as a discrete affordance over surfaces that share the
|
|
/// background hue. Computed by darkening the linear-RGB components
|
|
/// of the accent rather than carrying yet another palette slot.
|
|
pub fn p_default_border() -> Color
|
|
{
|
|
let a = crate::theme::palette().accent;
|
|
Color { r: a.r * 0.55, g: a.g * 0.55, b: a.b * 0.55, a: a.a }
|
|
}
|
|
/// Primary button label colour. Falls back to the theme's
|
|
/// `text_primary`; themes whose accent does not pair well with
|
|
/// their text-primary token can override the palette so this
|
|
/// reads cleanly.
|
|
pub fn p_default_text() -> Color { crate::theme::palette().text_primary }
|
|
/// Disabled primary background — uses the theme's divider token,
|
|
/// the lowest-contrast neutral available across modes.
|
|
pub fn p_disabled_bg() -> Color { crate::theme::palette().divider }
|
|
/// Disabled primary / secondary / tertiary text — uses the
|
|
/// theme's secondary text token.
|
|
pub fn p_disabled_text() -> Color { crate::theme::palette().text_secondary }
|
|
/// Secondary button default background — matches the surface_alt surface.
|
|
pub fn s_bg() -> Color { crate::theme::palette().surface_alt }
|
|
/// Secondary button border.
|
|
pub fn s_border() -> Color { crate::theme::palette().text_primary }
|
|
/// Disabled secondary background — slightly lighter than the
|
|
/// disabled primary so the two states stay visually distinct.
|
|
pub fn s_disabled_bg() -> Color { crate::theme::palette().surface_alt }
|
|
/// Disabled secondary border — uses the divider token.
|
|
pub fn s_disabled_border() -> Color { crate::theme::palette().divider }
|
|
/// Tertiary button text colour.
|
|
pub fn t_text() -> Color { crate::theme::palette().text_primary }
|
|
/// Keyboard focus ring / hover circle.
|
|
pub fn focus_color() -> Color { crate::theme::palette().accent }
|
|
|
|
pub const S_BORDER_W: f32 = 2.0;
|
|
pub const P_BORDER_W: f32 = 1.0;
|
|
pub const FOCUS_W: f32 = 3.0;
|
|
pub const HEIGHT: f32 = 48.0;
|
|
pub const RADIUS: f32 = 100.0;
|
|
pub const FONT_SIZE: f32 = 16.0;
|
|
pub const PAD_H: f32 = 24.0;
|
|
}
|
|
|
|
/// Visual style of a text button.
|
|
#[ derive( Clone, Default ) ]
|
|
pub enum ButtonVariant
|
|
{
|
|
/// Filled with the brand color — use for the primary call-to-action.
|
|
#[ default ]
|
|
Primary,
|
|
/// White background with a dark border — use for secondary actions.
|
|
Secondary,
|
|
/// Text-only, no background — use for low-emphasis actions.
|
|
Tertiary,
|
|
}
|
|
|
|
/// Internal content of a button — either a text label or a PNG icon.
|
|
pub enum ButtonContent
|
|
{
|
|
/// A text label rendered with the theme font.
|
|
Text( String ),
|
|
/// An RGBA image used as the button face (Arc avoids per-frame cloning).
|
|
Icon { rgba: Arc<Vec<u8>>, img_w: u32, img_h: u32 },
|
|
}
|
|
|
|
/// A pressable button widget.
|
|
///
|
|
/// Create text buttons with [`button()`](crate::button()) and icon buttons with
|
|
/// [`icon_button()`](crate::icon_button()). Buttons that step a value
|
|
/// (date / time pickers, numeric spinners) can opt into press-and-
|
|
/// hold repeat via [`Self::repeating`] — the runtime then re-fires
|
|
/// `on_press` while the button is held, at the keyboard's repeat
|
|
/// cadence.
|
|
pub struct Button<Msg: Clone>
|
|
{
|
|
/// The visual content of this button.
|
|
pub content: ButtonContent,
|
|
/// Message emitted when the button is pressed, or `None` if disabled.
|
|
pub on_press: Option<Msg>,
|
|
/// Message emitted when the user holds the button for
|
|
/// [`App::long_press_duration`](crate::app::App::long_press_duration)
|
|
/// without moving past the tolerance, OR when the user right-clicks
|
|
/// with the mouse. `None` leaves the button without a context-menu
|
|
/// equivalent. The fire does NOT by itself put the gesture into
|
|
/// drag mode — that is governed by [`Self::on_drag_start`].
|
|
pub on_long_press: Option<Msg>,
|
|
/// Drag-arm message. Fires when the press transitions into a drag:
|
|
/// touch on hold-timer expiry (in addition to `on_long_press`),
|
|
/// mouse on motion past the drag-promotion threshold (without
|
|
/// firing the menu). Independent of `on_long_press` so a button
|
|
/// can open a menu without becoming draggable, or be draggable
|
|
/// without showing a menu.
|
|
pub on_drag_start: Option<Msg>,
|
|
/// Visual variant controlling colors and borders.
|
|
pub variant: ButtonVariant,
|
|
/// Width and height in pixels for icon buttons. Defaults to `48.0`.
|
|
pub icon_size: f32,
|
|
/// Optional stable identifier for focus management.
|
|
pub id: Option<WidgetId>,
|
|
/// Whether this button participates in keyboard focus (Tab). Default: `true`.
|
|
pub focusable: bool,
|
|
/// Override the pointer cursor shape on hover. `None` falls back
|
|
/// to the `Pointer` (hand) default for clickable widgets.
|
|
pub cursor: Option<crate::types::CursorShape>,
|
|
/// When `true`, holding the button down auto-fires the
|
|
/// `on_press` message: one immediate fire on press, then an
|
|
/// initial delay (≈ 500 ms — same as the keyboard) followed by
|
|
/// repeats every ~120 ms (≈ 8 Hz, deliberately slower than the
|
|
/// keyboard's 30 Hz so a stepper does not whip past the
|
|
/// target). The runtime cancels the timer on release, on touch
|
|
/// cancel, and on long-press promotion. Default `false` — most
|
|
/// buttons fire on tap only.
|
|
pub repeating: bool,
|
|
pub tooltip: Option<String>,
|
|
}
|
|
|
|
impl<Msg: Clone> Button<Msg>
|
|
{
|
|
/// Create a text button with the given label.
|
|
pub fn new( label: String ) -> Self
|
|
{
|
|
Self
|
|
{
|
|
content: ButtonContent::Text( label ),
|
|
on_press: None,
|
|
on_long_press: None,
|
|
on_drag_start: None,
|
|
variant: ButtonVariant::Primary,
|
|
icon_size: theme::HEIGHT,
|
|
id: None,
|
|
focusable: true,
|
|
cursor: None,
|
|
repeating: false,
|
|
tooltip: None,
|
|
}
|
|
}
|
|
|
|
/// Hint shown after a 600 ms pointer dwell. Pointer-only.
|
|
pub fn tooltip( mut self, text: impl Into<String> ) -> Self
|
|
{
|
|
self.tooltip = Some( text.into() );
|
|
self
|
|
}
|
|
|
|
/// Override the pointer cursor shape shown on hover.
|
|
pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self
|
|
{
|
|
self.cursor = Some( shape );
|
|
self
|
|
}
|
|
|
|
/// Create an icon button from a shared RGBA buffer.
|
|
///
|
|
/// `img_w` and `img_h` must match the dimensions of `rgba`.
|
|
pub fn new_icon( rgba: Arc<Vec<u8>>, img_w: u32, img_h: u32 ) -> Self
|
|
{
|
|
Self
|
|
{
|
|
content: ButtonContent::Icon { rgba, img_w, img_h },
|
|
on_press: None,
|
|
on_long_press: None,
|
|
on_drag_start: None,
|
|
variant: ButtonVariant::Tertiary,
|
|
icon_size: theme::HEIGHT,
|
|
id: None,
|
|
focusable: true,
|
|
cursor: None,
|
|
repeating: false,
|
|
tooltip: None,
|
|
}
|
|
}
|
|
|
|
/// Set the message emitted when the button is pressed.
|
|
pub fn on_press( mut self, msg: Msg ) -> Self
|
|
{
|
|
self.on_press = Some( msg );
|
|
self
|
|
}
|
|
|
|
/// Optionally set the message — `None` leaves the button disabled.
|
|
pub fn on_press_maybe( mut self, msg: Option<Msg> ) -> Self
|
|
{
|
|
self.on_press = msg;
|
|
self
|
|
}
|
|
|
|
/// Auto-fire `on_press` while the button is held down. The
|
|
/// runtime fires once on press, then re-fires after the
|
|
/// keyboard's repeat *delay* (≈ 500 ms) and at a fixed ~120 ms
|
|
/// (≈ 8 Hz) interval afterwards — slow enough to release on
|
|
/// the value the user wants, fast enough to ramp. Each tick
|
|
/// re-reads `on_press` from the live widget tree, so a
|
|
/// stepper-style button whose message is `"go to value + 1"`
|
|
/// keeps stepping correctly as the value updates.
|
|
///
|
|
/// Mutually compatible with `on_long_press` only in spirit —
|
|
/// once the long-press message fires the gesture machine
|
|
/// transitions to drag mode and the repeat timer is cancelled
|
|
/// regardless of `repeating`. Default `false`.
|
|
pub fn repeating( mut self, on: bool ) -> Self
|
|
{
|
|
self.repeating = on;
|
|
self
|
|
}
|
|
|
|
/// Attach a long-press message. Fires when the press has been held
|
|
/// stationary for [`App::long_press_duration`](crate::app::App::long_press_duration),
|
|
/// or when the user right-clicks with the mouse. By itself this does
|
|
/// NOT put the gesture into drag mode — that is governed by
|
|
/// [`Self::on_drag_start`]. The regular `on_press` is suppressed
|
|
/// only when the press has been promoted to a drag (drag-arm fired).
|
|
pub fn on_long_press( mut self, msg: Msg ) -> Self
|
|
{
|
|
self.on_long_press = Some( msg );
|
|
self
|
|
}
|
|
|
|
/// Attach a drag-arm message. Fires when the press transitions into
|
|
/// drag mode — touch on hold-timer expiry (alongside `on_long_press`),
|
|
/// mouse on motion past the drag-promotion threshold (without firing
|
|
/// `on_long_press`). Independent of the menu so a button can be
|
|
/// draggable without showing a menu, or open a menu without becoming
|
|
/// draggable.
|
|
pub fn on_drag_start( mut self, msg: Msg ) -> Self
|
|
{
|
|
self.on_drag_start = Some( msg );
|
|
self
|
|
}
|
|
|
|
/// Control whether this button receives keyboard focus (Tab navigation).
|
|
/// Set to `false` for purely decorative or status-indicator buttons.
|
|
pub fn focusable( mut self, yes: bool ) -> Self
|
|
{
|
|
self.focusable = yes;
|
|
self
|
|
}
|
|
|
|
/// Set the visual variant.
|
|
pub fn variant( mut self, v: ButtonVariant ) -> Self
|
|
{
|
|
self.variant = v;
|
|
self
|
|
}
|
|
|
|
/// Set the display size (width = height) for icon buttons in pixels.
|
|
pub fn icon_size( mut self, size: f32 ) -> Self
|
|
{
|
|
self.icon_size = size;
|
|
self
|
|
}
|
|
|
|
/// Assign a stable identifier for focus management.
|
|
pub fn id( mut self, id: WidgetId ) -> Self
|
|
{
|
|
self.id = Some( id );
|
|
self
|
|
}
|
|
|
|
/// Bounding box of everything the button can paint at `rect`, across every
|
|
/// interaction state. This is the sum of: icon-button hover/press circle
|
|
/// (radius `rect.min_dim / 2 + 8`), focus ring (grows `FOCUS_W + 1` beyond
|
|
/// that), stroke half-width (`FOCUS_W / 2`), plus ~1 px of antialiasing
|
|
/// bleed. Text buttons only have the focus ring.
|
|
///
|
|
/// The partial-redraw path uses this to know how much canvas area to
|
|
/// invalidate when the button transitions in/out of a state.
|
|
pub fn paint_bounds( &self, rect: crate::types::Rect ) -> crate::types::Rect
|
|
{
|
|
let stroke_bleed = theme::FOCUS_W * 0.5 + 1.0;
|
|
match &self.content
|
|
{
|
|
ButtonContent::Icon { .. } =>
|
|
{
|
|
// The circle grows 8 px beyond the icon rect, the focus ring grows
|
|
// `FOCUS_W + 1` beyond the circle.
|
|
let circle_pad = 8.0_f32;
|
|
let ring_pad = theme::FOCUS_W + 1.0;
|
|
rect.expand( circle_pad + ring_pad + stroke_bleed )
|
|
}
|
|
ButtonContent::Text( _ ) => match self.variant
|
|
{
|
|
ButtonVariant::Primary | ButtonVariant::Secondary =>
|
|
{
|
|
rect.expand( theme::FOCUS_W + 2.0 + stroke_bleed )
|
|
}
|
|
ButtonVariant::Tertiary => rect.expand( 2.0 + stroke_bleed ),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Return the preferred `(width, height)` given available `max_width`.
|
|
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
|
|
{
|
|
match &self.content
|
|
{
|
|
ButtonContent::Text( label ) =>
|
|
{
|
|
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
|
|
let w = (text_w + theme::PAD_H * 2.0).min( max_width );
|
|
( w, theme::HEIGHT )
|
|
}
|
|
ButtonContent::Icon { .. } =>
|
|
{
|
|
let s = self.icon_size.min( max_width );
|
|
( s, s )
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Draw the button into `canvas` at `rect`.
|
|
///
|
|
/// `focused` draws a keyboard-focus ring; `hovered` and `pressed` apply
|
|
/// pointer/touch state overlays (icon buttons only).
|
|
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool, hovered: bool, pressed: bool )
|
|
{
|
|
match &self.content
|
|
{
|
|
ButtonContent::Text( label ) =>
|
|
{
|
|
self.draw_text_button( canvas, rect, focused, label );
|
|
}
|
|
ButtonContent::Icon { rgba, img_w, img_h } =>
|
|
{
|
|
self.draw_icon_button( canvas, rect, focused, hovered, pressed, rgba, *img_w, *img_h );
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_text_button( &self, canvas: &mut Canvas, rect: Rect, focused: bool, label: &str )
|
|
{
|
|
let is_disabled = self.on_press.is_none();
|
|
let text_y = rect.y + (rect.height + theme::FONT_SIZE) / 2.0 - 2.0;
|
|
|
|
match self.variant
|
|
{
|
|
ButtonVariant::Primary =>
|
|
{
|
|
let bg = if is_disabled { theme::p_disabled_bg() } else { theme::p_default_bg() };
|
|
let text_c = if is_disabled { theme::p_disabled_text() } else { theme::p_default_text() };
|
|
let border_c = theme::p_default_border();
|
|
canvas.fill_rect( rect, bg, theme::RADIUS );
|
|
if !is_disabled
|
|
{
|
|
canvas.stroke_rect( rect, border_c, theme::P_BORDER_W, theme::RADIUS );
|
|
}
|
|
if focused
|
|
{
|
|
let ring = rect.expand( theme::FOCUS_W + 2.0 );
|
|
canvas.stroke_rect(
|
|
ring,
|
|
theme::focus_color(),
|
|
theme::FOCUS_W,
|
|
theme::RADIUS + theme::FOCUS_W + 2.0,
|
|
);
|
|
}
|
|
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
|
|
canvas.draw_text(
|
|
label,
|
|
rect.x + (rect.width - text_w) / 2.0,
|
|
text_y,
|
|
theme::FONT_SIZE,
|
|
text_c,
|
|
);
|
|
}
|
|
ButtonVariant::Secondary =>
|
|
{
|
|
let bg = if is_disabled { theme::s_disabled_bg() } else { theme::s_bg() };
|
|
let text_c = if is_disabled { theme::p_disabled_text() } else { theme::t_text() };
|
|
let border_c = if is_disabled { theme::s_disabled_border() } else { theme::s_border() };
|
|
canvas.fill_rect( rect, bg, theme::RADIUS );
|
|
canvas.stroke_rect( rect, border_c, theme::S_BORDER_W, theme::RADIUS );
|
|
if focused
|
|
{
|
|
let ring = rect.expand( theme::FOCUS_W + 2.0 );
|
|
canvas.stroke_rect(
|
|
ring,
|
|
theme::focus_color(),
|
|
theme::FOCUS_W,
|
|
theme::RADIUS + theme::FOCUS_W + 2.0,
|
|
);
|
|
}
|
|
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
|
|
canvas.draw_text(
|
|
label,
|
|
rect.x + (rect.width - text_w) / 2.0,
|
|
text_y,
|
|
theme::FONT_SIZE,
|
|
text_c,
|
|
);
|
|
}
|
|
ButtonVariant::Tertiary =>
|
|
{
|
|
let text_c = if is_disabled { theme::p_disabled_text() } else { theme::t_text() };
|
|
if focused
|
|
{
|
|
let ring = rect.expand( 2.0 );
|
|
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, theme::RADIUS );
|
|
}
|
|
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
|
|
canvas.draw_text(
|
|
label,
|
|
rect.x + (rect.width - text_w) / 2.0,
|
|
text_y,
|
|
theme::FONT_SIZE,
|
|
text_c,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_icon_button(
|
|
&self,
|
|
canvas: &mut Canvas,
|
|
rect: Rect,
|
|
focused: bool,
|
|
hovered: bool,
|
|
pressed: bool,
|
|
rgba: &[u8],
|
|
img_w: u32,
|
|
img_h: u32,
|
|
)
|
|
{
|
|
// Semi-transparent circular overlay behind the icon for hover / press feedback
|
|
let circle_pad = 8.0_f32;
|
|
let r = rect.width.min( rect.height ) / 2.0 + circle_pad;
|
|
let cx = rect.x + rect.width / 2.0;
|
|
let cy = rect.y + rect.height / 2.0;
|
|
let circle = Rect
|
|
{
|
|
x: cx - r,
|
|
y: cy - r,
|
|
width: r * 2.0,
|
|
height: r * 2.0,
|
|
};
|
|
// Hover / press feedback is the theme's primary text colour
|
|
// at low alpha — works as a "lighten" in light mode (where
|
|
// text_primary tends to be dark and the underlying icon is
|
|
// dark) and as a subtle wash in dark mode without baking in
|
|
// a fixed white.
|
|
let fp = crate::theme::palette().text_primary;
|
|
if pressed
|
|
{
|
|
canvas.fill_rect( circle, Color::rgba( fp.r, fp.g, fp.b, 0.18 ), r );
|
|
} else if hovered {
|
|
canvas.fill_rect( circle, Color::rgba( fp.r, fp.g, fp.b, 0.10 ), r );
|
|
}
|
|
if focused
|
|
{
|
|
let ring = circle.expand( theme::FOCUS_W + 1.0 );
|
|
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, r + theme::FOCUS_W + 1.0 );
|
|
}
|
|
canvas.draw_image_data( rgba, img_w, img_h, rect, 1.0 );
|
|
}
|
|
|
|
/// Wrap this button in an [`Element`].
|
|
pub fn into_element( self ) -> Element<Msg>
|
|
{
|
|
Element::Button( self )
|
|
}
|
|
|
|
/// Re-tag this button's three message slots through `f`. Called by
|
|
/// [`Element::map`] while walking a sub-tree.
|
|
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Button<U>
|
|
where
|
|
U: Clone + 'static,
|
|
Msg: 'static,
|
|
{
|
|
Button
|
|
{
|
|
content: self.content,
|
|
on_press: self.on_press.map( |m| ( *f )( m ) ),
|
|
on_long_press: self.on_long_press.map( |m| ( *f )( m ) ),
|
|
on_drag_start: self.on_drag_start.map( |m| ( *f )( m ) ),
|
|
variant: self.variant,
|
|
icon_size: self.icon_size,
|
|
id: self.id,
|
|
focusable: self.focusable,
|
|
cursor: self.cursor,
|
|
repeating: self.repeating,
|
|
tooltip: self.tooltip,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[ cfg( test ) ]
|
|
mod tests
|
|
{
|
|
use super::*;
|
|
|
|
#[ test ]
|
|
fn text_button_focusable_by_default()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() );
|
|
assert!( b.focusable );
|
|
}
|
|
|
|
#[ test ]
|
|
fn icon_button_focusable_by_default()
|
|
{
|
|
let b = Button::<()>::new_icon( Arc::new( vec![] ), 0, 0 );
|
|
assert!( b.focusable );
|
|
}
|
|
|
|
#[ test ]
|
|
fn focusable_builder_disables_focus()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).focusable( false );
|
|
assert!( !b.focusable );
|
|
}
|
|
|
|
#[ test ]
|
|
fn focusable_builder_re_enables_focus()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).focusable( false ).focusable( true );
|
|
assert!( b.focusable );
|
|
}
|
|
|
|
#[ test ]
|
|
fn on_press_none_by_default()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() );
|
|
assert!( b.on_press.is_none() );
|
|
}
|
|
|
|
#[ test ]
|
|
fn tooltip_none_by_default()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() );
|
|
assert!( b.tooltip.is_none() );
|
|
let b = Button::<()>::new_icon( Arc::new( vec![] ), 0, 0 );
|
|
assert!( b.tooltip.is_none() );
|
|
}
|
|
|
|
#[ test ]
|
|
fn tooltip_builder_sets_text()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).tooltip( "Save document" );
|
|
assert_eq!( b.tooltip.as_deref(), Some( "Save document" ) );
|
|
}
|
|
|
|
#[ test ]
|
|
fn tooltip_survives_map_msg()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).tooltip( "Hint" );
|
|
let f: super::super::MapFn<(), u32> = std::sync::Arc::new( |()| 0u32 );
|
|
let mapped = b.map_msg( &f );
|
|
assert_eq!( mapped.tooltip.as_deref(), Some( "Hint" ) );
|
|
}
|
|
|
|
#[ test ]
|
|
fn element_tooltip_returns_button_tooltip()
|
|
{
|
|
let e: super::super::Element<()> = Button::<()>::new( "ok".into() ).tooltip( "Hi" ).into_element();
|
|
assert_eq!( e.tooltip(), Some( "Hi" ) );
|
|
}
|
|
|
|
#[ test ]
|
|
fn element_tooltip_none_for_non_button_widgets()
|
|
{
|
|
let t: super::super::Element<()> = super::super::text( "label" ).into();
|
|
assert!( t.tooltip().is_none() );
|
|
}
|
|
|
|
#[ test ]
|
|
fn on_press_maybe_none_leaves_disabled()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).on_press_maybe( None );
|
|
assert!( b.on_press.is_none() );
|
|
}
|
|
|
|
#[ test ]
|
|
fn repeating_off_by_default()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() );
|
|
assert!( !b.repeating );
|
|
}
|
|
|
|
#[ test ]
|
|
fn repeating_builder_sets_flag()
|
|
{
|
|
let b = Button::<()>::new( "ok".into() ).repeating( true );
|
|
assert!( b.repeating );
|
|
let b = b.repeating( false );
|
|
assert!( !b.repeating );
|
|
}
|
|
}
|