1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Icon buttons: one glyph, pressable, for the small controls at the edge of a header or a row.
use std::time::Duration;
use super::placement::Placement;
use super::press::{self, Press};
use super::tooltip;
use crate::event::Event;
use crate::geometry::{Rect, Size};
use crate::style::CellStyle;
use crate::text;
use crate::theme::State;
use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
/// A button that is one icon: a space, the glyph and a space, three cells in every glyph mode.
///
/// It stands on the ground around it, with no raised surface, so a row of them reads as quiet
/// marks rather than as buttons with labels. The pointer lightens all three cells, keyboard focus
/// lightens them one step further, and a press flashes them one step more; there is no pillar,
/// because three cells have no room for one before the glyph. Enter or Space while focused, or a
/// click released over it, sends its message, like a [`Button`](super::Button).
///
/// Its meaning is only the glyph, so give it a [`tooltip`](Self::tooltip): the words show below it
/// after the pointer rests on it for the theme's hover delay, and at once when it is reached with
/// the keyboard.
///
/// Style keys: `icon-button` (`fg`, `bg`, `bold`) with states `hover`, `focus`, `pressed` and
/// `disabled`; `tooltip` for its words.
///
/// ```
/// use qframe::prelude::*;
/// use qframe::widgets::IconButton;
///
/// struct Header;
///
/// impl App for Header {
/// type Msg = ();
/// fn update(&mut self, (): ()) -> Command<()> {
/// Command::none()
/// }
/// fn view(&self, ui: &mut View<'_, ()>) {
/// ui.row(|ui| {
/// ui.add(Text::new("Packages")).fill_width();
/// ui.add(IconButton::new("settings").tooltip("Settings").on_press(()));
/// })
/// .fill_width();
/// }
/// }
///
/// let app = Harness::new(Header, 20, 1);
/// let glyph = app.env().icons().glyph("settings").into_owned();
/// assert_eq!(app.find(&glyph), Some((18, 0)), "a space, the glyph and a space at the end");
/// ```
pub struct IconButton<Msg> {
icon: String,
tooltip: Option<String>,
disabled: bool,
on_press: Option<Msg>,
}
/// When the pointer came to rest on the button and when its tooltip began to show.
#[derive(Debug, Default)]
struct IconButtonMemory {
hovered_since: Option<Duration>,
shown_since: Option<Duration>,
}
/// Cells an icon button takes around its glyph: one space on each side.
const SIDES: u16 = 2;
impl<Msg> IconButton<Msg> {
/// A button showing the icon `key` of the icon set, such as `"settings"` or `"close"`.
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self { icon: key.into(), tooltip: None, disabled: false, on_press: None }
}
/// The message sent when the button is pressed.
#[must_use]
pub fn on_press(mut self, message: Msg) -> Self {
self.on_press = Some(message);
self
}
/// Words that say what the button does, shown below it after the hover delay and at once
/// when it is reached with the keyboard.
#[must_use]
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.tooltip = Some(text.into());
self
}
/// Greys the button out; it cannot be focused or pressed.
#[must_use]
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
fn active(&self) -> bool {
!self.disabled && self.on_press.is_some()
}
}
impl<Msg: Clone + 'static> Widget<Msg> for IconButton<Msg> {
fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
let glyph = text::width(&cx.env().icons().glyph(&self.icon));
Size::new(glyph.saturating_add(SIDES), 1).min(available)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
let active = self.active();
let mut states = if active { cx.pressable_states() } else { Vec::new() };
if self.disabled {
states.push(State::Disabled);
}
let style = cx.style("icon-button", None, &states).text();
// At rest the theme gives no ground, so the button keeps whatever it stands on.
if let Some(bg) = style.bg {
cx.fill(area, bg);
}
if active {
cx.register_hit(area);
}
let glyph = cx.env().icons().glyph(&self.icon).into_owned();
let width = text::width(&glyph);
let x = area.x + i32::from(area.width.saturating_sub(width) / 2);
cx.text(x, area.y, &glyph, CellStyle { bg: None, ..style }, area.width);
self.schedule_tooltip(cx, area, &states);
}
fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
let Some(text) = &self.tooltip else {
return;
};
let since = cx.memory::<IconButtonMemory>().shown_since.unwrap_or_default();
tooltip::paint_tip(cx, anchor, text, Placement::Below, since);
}
fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
if !self.active() {
return false;
}
match press::read(cx, event) {
Press::Ignored => false,
Press::Used => true,
Press::Key | Press::Click(..) => {
if let Some(message) = &self.on_press {
cx.flash();
cx.emit(message.clone());
}
true
}
}
}
fn focusable(&self) -> bool {
self.active()
}
}
impl<Msg: Clone + 'static> IconButton<Msg> {
/// Tracks how long the pointer has rested on the button and asks for the overlay once its
/// tooltip is due, or at once while keyboard focus is on it.
fn schedule_tooltip(&self, cx: &mut PaintCx<'_>, area: Rect, states: &[State]) {
if self.tooltip.is_none() {
return;
}
let now = cx.now();
let delay = cx.env().theme().motion().hover_delay;
let hovered = states.contains(&State::Hover);
let keyboard = states.contains(&State::Focus);
let memory = cx.memory::<IconButtonMemory>();
memory.hovered_since = if hovered { Some(memory.hovered_since.unwrap_or(now)) } else { None };
let due = memory.hovered_since.map(|since| since + delay);
let visible = keyboard || due.is_some_and(|due| now >= due);
memory.shown_since = if visible { Some(memory.shown_since.unwrap_or(now)) } else { None };
if visible {
cx.request_overlay(area);
} else if let Some(due) = due {
cx.request_frame_in(due.saturating_sub(now));
}
}
}