qframe/widgets/
icon_button.rs1use std::time::Duration;
4
5use super::placement::Placement;
6use super::press::{self, Press};
7use super::tooltip;
8use crate::event::Event;
9use crate::geometry::{Rect, Size};
10use crate::style::CellStyle;
11use crate::text;
12use crate::theme::State;
13use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
14
15pub struct IconButton<Msg> {
55 icon: String,
56 tooltip: Option<String>,
57 disabled: bool,
58 on_press: Option<Msg>,
59}
60
61#[derive(Debug, Default)]
63struct IconButtonMemory {
64 hovered_since: Option<Duration>,
65 shown_since: Option<Duration>,
66}
67
68const SIDES: u16 = 2;
70
71impl<Msg> IconButton<Msg> {
72 #[must_use]
74 pub fn new(key: impl Into<String>) -> Self {
75 Self { icon: key.into(), tooltip: None, disabled: false, on_press: None }
76 }
77
78 #[must_use]
80 pub fn on_press(mut self, message: Msg) -> Self {
81 self.on_press = Some(message);
82 self
83 }
84
85 #[must_use]
88 pub fn tooltip(mut self, text: impl Into<String>) -> Self {
89 self.tooltip = Some(text.into());
90 self
91 }
92
93 #[must_use]
95 pub fn disabled(mut self, disabled: bool) -> Self {
96 self.disabled = disabled;
97 self
98 }
99
100 fn active(&self) -> bool {
101 !self.disabled && self.on_press.is_some()
102 }
103}
104
105impl<Msg: Clone + 'static> Widget<Msg> for IconButton<Msg> {
106 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
107 let glyph = text::width(&cx.env().icons().glyph(&self.icon));
108 Size::new(glyph.saturating_add(SIDES), 1).min(available)
109 }
110
111 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
112 let active = self.active();
113 let mut states = if active { cx.pressable_states() } else { Vec::new() };
114 if self.disabled {
115 states.push(State::Disabled);
116 }
117 let style = cx.style("icon-button", None, &states).text();
118 if let Some(bg) = style.bg {
120 cx.fill(area, bg);
121 }
122 if active {
123 cx.register_hit(area);
124 }
125 let glyph = cx.env().icons().glyph(&self.icon).into_owned();
126 let width = text::width(&glyph);
127 let x = area.x + i32::from(area.width.saturating_sub(width) / 2);
128 cx.text(x, area.y, &glyph, CellStyle { bg: None, ..style }, area.width);
129 self.schedule_tooltip(cx, area, &states);
130 }
131
132 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
133 let Some(text) = &self.tooltip else {
134 return;
135 };
136 let since = cx.memory::<IconButtonMemory>().shown_since.unwrap_or_default();
137 tooltip::paint_tip(cx, anchor, text, Placement::Below, since);
138 }
139
140 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
141 if !self.active() {
142 return false;
143 }
144 match press::read(cx, event) {
145 Press::Ignored => false,
146 Press::Used => true,
147 Press::Key | Press::Click(..) => {
148 if let Some(message) = &self.on_press {
149 cx.flash();
150 cx.emit(message.clone());
151 }
152 true
153 }
154 }
155 }
156
157 fn focusable(&self) -> bool {
158 self.active()
159 }
160}
161
162impl<Msg: Clone + 'static> IconButton<Msg> {
163 fn schedule_tooltip(&self, cx: &mut PaintCx<'_>, area: Rect, states: &[State]) {
166 if self.tooltip.is_none() {
167 return;
168 }
169 let now = cx.now();
170 let delay = cx.env().theme().motion().hover_delay;
171 let hovered = states.contains(&State::Hover);
172 let keyboard = states.contains(&State::Focus);
173 let memory = cx.memory::<IconButtonMemory>();
174 memory.hovered_since = if hovered { Some(memory.hovered_since.unwrap_or(now)) } else { None };
175 let due = memory.hovered_since.map(|since| since + delay);
176 let visible = keyboard || due.is_some_and(|due| now >= due);
177 memory.shown_since = if visible { Some(memory.shown_since.unwrap_or(now)) } else { None };
178 if visible {
179 cx.request_overlay(area);
180 } else if let Some(due) = due {
181 cx.request_frame_in(due.saturating_sub(now));
182 }
183 }
184}