1use crate::style::{Border, Margin, Padding};
17use crate::{ButtonResponse, Color, UiCtx, UiError, Widget};
18
19pub trait ClipboardProvider {
24 fn get_text(&self) -> Result<Option<String>, UiError>;
26
27 fn set_text(&mut self, text: &str) -> Result<(), UiError>;
29
30 fn get_mime(&self, _mime: &str) -> Result<Option<Vec<u8>>, UiError> {
33 Ok(None)
34 }
35
36 fn set_mime(&mut self, mime: &str, _data: &[u8]) -> Result<(), UiError> {
39 Err(UiError::Clipboard(format!(
40 "MIME type '{mime}' not supported"
41 )))
42 }
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
49pub enum DropEffect {
50 #[default]
52 None,
53 Copy,
55 Move,
57 Link,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct DragData {
64 pub mime: String,
66 pub bytes: Vec<u8>,
68}
69
70impl DragData {
71 pub fn text(s: impl Into<String>) -> Self {
73 Self {
74 mime: "text/plain".to_owned(),
75 bytes: s.into().into_bytes(),
76 }
77 }
78
79 pub fn new(mime: impl Into<String>, bytes: Vec<u8>) -> Self {
81 Self {
82 mime: mime.into(),
83 bytes,
84 }
85 }
86
87 pub fn as_text(&self) -> Option<String> {
89 String::from_utf8(self.bytes.clone()).ok()
90 }
91}
92
93pub trait DragSource {
95 fn drag_data(&self) -> Option<DragData>;
97
98 fn allowed_effects(&self) -> &[DropEffect] {
100 const DEFAULT: &[DropEffect] = &[DropEffect::Copy, DropEffect::Move];
101 DEFAULT
102 }
103}
104
105pub trait DropTarget {
107 fn can_accept(&self, data: &DragData) -> DropEffect;
110
111 fn accept_drop(&mut self, data: &DragData, effect: DropEffect) -> Result<bool, UiError>;
114}
115
116type ClickFn = Box<dyn FnMut()>;
121type HoverFn = Box<dyn FnMut(bool)>;
123
124pub struct Padded<W> {
127 inner: W,
128 pub padding: Padding,
130}
131
132impl<W: Widget> Widget for Padded<W> {
133 fn render(&mut self, ui: &mut dyn UiCtx) {
134 self.inner.render(ui);
135 }
136}
137
138pub struct Margined<W> {
140 inner: W,
141 pub margin: Margin,
143}
144
145impl<W: Widget> Widget for Margined<W> {
146 fn render(&mut self, ui: &mut dyn UiCtx) {
147 self.inner.render(ui);
148 }
149}
150
151pub struct Backgrounded<W> {
153 inner: W,
154 pub background: Color,
156}
157
158impl<W: Widget> Widget for Backgrounded<W> {
159 fn render(&mut self, ui: &mut dyn UiCtx) {
160 self.inner.render(ui);
161 }
162}
163
164pub struct Bordered<W> {
166 inner: W,
167 pub border: Border,
169}
170
171impl<W: Widget> Widget for Bordered<W> {
172 fn render(&mut self, ui: &mut dyn UiCtx) {
173 self.inner.render(ui);
174 }
175}
176
177pub struct OnClick<W> {
185 inner: W,
186 label: String,
187 callback: ClickFn,
188}
189
190impl<W: Widget> OnClick<W> {
191 pub fn probe(&mut self, response: &ButtonResponse) -> bool {
194 if response.clicked {
195 (self.callback)();
196 true
197 } else {
198 false
199 }
200 }
201}
202
203impl<W: Widget> Widget for OnClick<W> {
204 fn render(&mut self, ui: &mut dyn UiCtx) {
205 self.inner.render(ui);
206 let resp = ui.button(&self.label);
207 if resp.clicked {
208 (self.callback)();
209 }
210 }
211}
212
213pub struct OnHover<W> {
215 inner: W,
216 label: String,
217 callback: HoverFn,
218}
219
220impl<W: Widget> OnHover<W> {
221 pub fn probe(&mut self, response: &ButtonResponse) {
224 (self.callback)(response.hovered);
225 }
226}
227
228impl<W: Widget> Widget for OnHover<W> {
229 fn render(&mut self, ui: &mut dyn UiCtx) {
230 self.inner.render(ui);
231 let resp = ui.button(&self.label);
232 (self.callback)(resp.hovered);
233 }
234}
235
236pub trait WidgetExt: Widget + Sized {
242 fn padding(self, padding: Padding) -> Padded<Self> {
244 Padded {
245 inner: self,
246 padding,
247 }
248 }
249
250 fn margin(self, margin: Margin) -> Margined<Self> {
252 Margined {
253 inner: self,
254 margin,
255 }
256 }
257
258 fn background(self, background: Color) -> Backgrounded<Self> {
260 Backgrounded {
261 inner: self,
262 background,
263 }
264 }
265
266 fn border(self, border: Border) -> Bordered<Self> {
268 Bordered {
269 inner: self,
270 border,
271 }
272 }
273
274 fn on_click(self, label: impl Into<String>, callback: impl FnMut() + 'static) -> OnClick<Self> {
277 OnClick {
278 inner: self,
279 label: label.into(),
280 callback: Box::new(callback),
281 }
282 }
283
284 fn on_hover(
287 self,
288 label: impl Into<String>,
289 callback: impl FnMut(bool) + 'static,
290 ) -> OnHover<Self> {
291 OnHover {
292 inner: self,
293 label: label.into(),
294 callback: Box::new(callback),
295 }
296 }
297}
298
299impl<W: Widget> WidgetExt for W {}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::geometry::Insets;
305 use std::cell::Cell;
306 use std::rc::Rc;
307
308 struct Probe(Rc<Cell<u32>>);
310 impl Widget for Probe {
311 fn render(&mut self, _ui: &mut dyn UiCtx) {
312 self.0.set(self.0.get() + 1);
313 }
314 }
315
316 struct StubCtx {
318 clicked: bool,
319 hovered: bool,
320 }
321 impl UiCtx for StubCtx {
322 fn heading(&mut self, _text: &str) {}
323 fn label(&mut self, _text: &str) {}
324 fn button(&mut self, _label: &str) -> ButtonResponse {
325 ButtonResponse {
326 clicked: self.clicked,
327 hovered: self.hovered,
328 }
329 }
330 }
331
332 #[test]
333 fn decorators_record_style_and_forward_render() {
334 let n = Rc::new(Cell::new(0u32));
335 let mut w = Probe(Rc::clone(&n))
336 .padding(Padding::all(4.0))
337 .border(Border::solid(1.0, Color(0, 0, 0, 255)));
338 assert_eq!(w.border.insets, Insets::all(1.0));
340 let mut ctx = StubCtx {
341 clicked: false,
342 hovered: false,
343 };
344 w.render(&mut ctx);
345 assert_eq!(n.get(), 1, "inner widget should still render exactly once");
346 }
347
348 #[test]
349 fn background_and_margin_compose() {
350 let n = Rc::new(Cell::new(0u32));
351 let w = Probe(Rc::clone(&n))
352 .background(Color(10, 20, 30, 255))
353 .margin(Margin::symmetric(2.0, 4.0));
354 assert_eq!(w.margin.insets(), Insets::symmetric(2.0, 4.0));
355 assert_eq!(w.inner.background, Color(10, 20, 30, 255));
357 }
358
359 #[test]
360 fn on_click_fires_callback_when_clicked() {
361 let n = Rc::new(Cell::new(0u32));
362 let clicks = Rc::new(Cell::new(0u32));
363 let clicks_c = Rc::clone(&clicks);
364 let mut w = Probe(Rc::clone(&n)).on_click("ok", move || clicks_c.set(clicks_c.get() + 1));
365
366 let mut ctx = StubCtx {
368 clicked: false,
369 hovered: false,
370 };
371 w.render(&mut ctx);
372 assert_eq!(clicks.get(), 0);
373
374 let mut ctx = StubCtx {
376 clicked: true,
377 hovered: false,
378 };
379 w.render(&mut ctx);
380 assert_eq!(clicks.get(), 1);
381 assert_eq!(n.get(), 2, "inner rendered each frame");
382 }
383
384 #[test]
385 fn on_hover_reports_state() {
386 let n = Rc::new(Cell::new(0u32));
387 let hovered = Rc::new(Cell::new(false));
388 let hovered_c = Rc::clone(&hovered);
389 let mut w = Probe(Rc::clone(&n)).on_hover("h", move |h| hovered_c.set(h));
390 let mut ctx = StubCtx {
391 clicked: false,
392 hovered: true,
393 };
394 w.render(&mut ctx);
395 assert!(hovered.get());
396 }
397
398 #[test]
399 fn on_click_probe_helper() {
400 let fired = Rc::new(Cell::new(false));
401 let fired_c = Rc::clone(&fired);
402 let n = Rc::new(Cell::new(0u32));
403 let mut w = Probe(n).on_click("x", move || fired_c.set(true));
404 assert!(w.probe(&ButtonResponse {
405 clicked: true,
406 hovered: false
407 }));
408 assert!(fired.get());
409 assert!(!w.probe(&ButtonResponse {
410 clicked: false,
411 hovered: false
412 }));
413 }
414
415 #[test]
416 fn drag_data_text_roundtrip() {
417 let d = DragData::text("hello");
418 assert_eq!(d.mime, "text/plain");
419 assert_eq!(d.as_text().as_deref(), Some("hello"));
420 }
421
422 #[test]
423 fn drop_effect_default_is_none() {
424 assert_eq!(DropEffect::default(), DropEffect::None);
425 }
426
427 struct MemClipboard {
429 text: Option<String>,
430 }
431 impl ClipboardProvider for MemClipboard {
432 fn get_text(&self) -> Result<Option<String>, UiError> {
433 Ok(self.text.clone())
434 }
435 fn set_text(&mut self, text: &str) -> Result<(), UiError> {
436 self.text = Some(text.to_owned());
437 Ok(())
438 }
439 }
440
441 #[test]
442 fn clipboard_default_mime_is_unsupported() {
443 let mut c = MemClipboard { text: None };
444 c.set_text("hi").expect("set");
445 assert_eq!(c.get_text().expect("get"), Some("hi".to_string()));
446 assert_eq!(c.get_mime("text/html").expect("mime get"), None);
448 assert!(matches!(
449 c.set_mime("text/html", b"<b>x</b>"),
450 Err(UiError::Clipboard(_))
451 ));
452 }
453}