use crate::style::{Border, Margin, Padding};
use crate::{ButtonResponse, Color, UiCtx, UiError, Widget};
pub trait ClipboardProvider {
fn get_text(&self) -> Result<Option<String>, UiError>;
fn set_text(&mut self, text: &str) -> Result<(), UiError>;
fn get_mime(&self, _mime: &str) -> Result<Option<Vec<u8>>, UiError> {
Ok(None)
}
fn set_mime(&mut self, mime: &str, _data: &[u8]) -> Result<(), UiError> {
Err(UiError::Clipboard(format!(
"MIME type '{mime}' not supported"
)))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum DropEffect {
#[default]
None,
Copy,
Move,
Link,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DragData {
pub mime: String,
pub bytes: Vec<u8>,
}
impl DragData {
pub fn text(s: impl Into<String>) -> Self {
Self {
mime: "text/plain".to_owned(),
bytes: s.into().into_bytes(),
}
}
pub fn new(mime: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
mime: mime.into(),
bytes,
}
}
pub fn as_text(&self) -> Option<String> {
String::from_utf8(self.bytes.clone()).ok()
}
}
pub trait DragSource {
fn drag_data(&self) -> Option<DragData>;
fn allowed_effects(&self) -> &[DropEffect] {
const DEFAULT: &[DropEffect] = &[DropEffect::Copy, DropEffect::Move];
DEFAULT
}
}
pub trait DropTarget {
fn can_accept(&self, data: &DragData) -> DropEffect;
fn accept_drop(&mut self, data: &DragData, effect: DropEffect) -> Result<bool, UiError>;
}
type ClickFn = Box<dyn FnMut()>;
type HoverFn = Box<dyn FnMut(bool)>;
pub struct Padded<W> {
inner: W,
pub padding: Padding,
}
impl<W: Widget> Widget for Padded<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
}
}
pub struct Margined<W> {
inner: W,
pub margin: Margin,
}
impl<W: Widget> Widget for Margined<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
}
}
pub struct Backgrounded<W> {
inner: W,
pub background: Color,
}
impl<W: Widget> Widget for Backgrounded<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
}
}
pub struct Bordered<W> {
inner: W,
pub border: Border,
}
impl<W: Widget> Widget for Bordered<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
}
}
pub struct OnClick<W> {
inner: W,
label: String,
callback: ClickFn,
}
impl<W: Widget> OnClick<W> {
pub fn probe(&mut self, response: &ButtonResponse) -> bool {
if response.clicked {
(self.callback)();
true
} else {
false
}
}
}
impl<W: Widget> Widget for OnClick<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
let resp = ui.button(&self.label);
if resp.clicked {
(self.callback)();
}
}
}
pub struct OnHover<W> {
inner: W,
label: String,
callback: HoverFn,
}
impl<W: Widget> OnHover<W> {
pub fn probe(&mut self, response: &ButtonResponse) {
(self.callback)(response.hovered);
}
}
impl<W: Widget> Widget for OnHover<W> {
fn render(&mut self, ui: &mut dyn UiCtx) {
self.inner.render(ui);
let resp = ui.button(&self.label);
(self.callback)(resp.hovered);
}
}
pub trait WidgetExt: Widget + Sized {
fn padding(self, padding: Padding) -> Padded<Self> {
Padded {
inner: self,
padding,
}
}
fn margin(self, margin: Margin) -> Margined<Self> {
Margined {
inner: self,
margin,
}
}
fn background(self, background: Color) -> Backgrounded<Self> {
Backgrounded {
inner: self,
background,
}
}
fn border(self, border: Border) -> Bordered<Self> {
Bordered {
inner: self,
border,
}
}
fn on_click(self, label: impl Into<String>, callback: impl FnMut() + 'static) -> OnClick<Self> {
OnClick {
inner: self,
label: label.into(),
callback: Box::new(callback),
}
}
fn on_hover(
self,
label: impl Into<String>,
callback: impl FnMut(bool) + 'static,
) -> OnHover<Self> {
OnHover {
inner: self,
label: label.into(),
callback: Box::new(callback),
}
}
}
impl<W: Widget> WidgetExt for W {}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Insets;
use std::cell::Cell;
use std::rc::Rc;
struct Probe(Rc<Cell<u32>>);
impl Widget for Probe {
fn render(&mut self, _ui: &mut dyn UiCtx) {
self.0.set(self.0.get() + 1);
}
}
struct StubCtx {
clicked: bool,
hovered: bool,
}
impl UiCtx for StubCtx {
fn heading(&mut self, _text: &str) {}
fn label(&mut self, _text: &str) {}
fn button(&mut self, _label: &str) -> ButtonResponse {
ButtonResponse {
clicked: self.clicked,
hovered: self.hovered,
}
}
}
#[test]
fn decorators_record_style_and_forward_render() {
let n = Rc::new(Cell::new(0u32));
let mut w = Probe(Rc::clone(&n))
.padding(Padding::all(4.0))
.border(Border::solid(1.0, Color(0, 0, 0, 255)));
assert_eq!(w.border.insets, Insets::all(1.0));
let mut ctx = StubCtx {
clicked: false,
hovered: false,
};
w.render(&mut ctx);
assert_eq!(n.get(), 1, "inner widget should still render exactly once");
}
#[test]
fn background_and_margin_compose() {
let n = Rc::new(Cell::new(0u32));
let w = Probe(Rc::clone(&n))
.background(Color(10, 20, 30, 255))
.margin(Margin::symmetric(2.0, 4.0));
assert_eq!(w.margin.insets(), Insets::symmetric(2.0, 4.0));
assert_eq!(w.inner.background, Color(10, 20, 30, 255));
}
#[test]
fn on_click_fires_callback_when_clicked() {
let n = Rc::new(Cell::new(0u32));
let clicks = Rc::new(Cell::new(0u32));
let clicks_c = Rc::clone(&clicks);
let mut w = Probe(Rc::clone(&n)).on_click("ok", move || clicks_c.set(clicks_c.get() + 1));
let mut ctx = StubCtx {
clicked: false,
hovered: false,
};
w.render(&mut ctx);
assert_eq!(clicks.get(), 0);
let mut ctx = StubCtx {
clicked: true,
hovered: false,
};
w.render(&mut ctx);
assert_eq!(clicks.get(), 1);
assert_eq!(n.get(), 2, "inner rendered each frame");
}
#[test]
fn on_hover_reports_state() {
let n = Rc::new(Cell::new(0u32));
let hovered = Rc::new(Cell::new(false));
let hovered_c = Rc::clone(&hovered);
let mut w = Probe(Rc::clone(&n)).on_hover("h", move |h| hovered_c.set(h));
let mut ctx = StubCtx {
clicked: false,
hovered: true,
};
w.render(&mut ctx);
assert!(hovered.get());
}
#[test]
fn on_click_probe_helper() {
let fired = Rc::new(Cell::new(false));
let fired_c = Rc::clone(&fired);
let n = Rc::new(Cell::new(0u32));
let mut w = Probe(n).on_click("x", move || fired_c.set(true));
assert!(w.probe(&ButtonResponse {
clicked: true,
hovered: false
}));
assert!(fired.get());
assert!(!w.probe(&ButtonResponse {
clicked: false,
hovered: false
}));
}
#[test]
fn drag_data_text_roundtrip() {
let d = DragData::text("hello");
assert_eq!(d.mime, "text/plain");
assert_eq!(d.as_text().as_deref(), Some("hello"));
}
#[test]
fn drop_effect_default_is_none() {
assert_eq!(DropEffect::default(), DropEffect::None);
}
struct MemClipboard {
text: Option<String>,
}
impl ClipboardProvider for MemClipboard {
fn get_text(&self) -> Result<Option<String>, UiError> {
Ok(self.text.clone())
}
fn set_text(&mut self, text: &str) -> Result<(), UiError> {
self.text = Some(text.to_owned());
Ok(())
}
}
#[test]
fn clipboard_default_mime_is_unsupported() {
let mut c = MemClipboard { text: None };
c.set_text("hi").expect("set");
assert_eq!(c.get_text().expect("get"), Some("hi".to_string()));
assert_eq!(c.get_mime("text/html").expect("mime get"), None);
assert!(matches!(
c.set_mime("text/html", b"<b>x</b>"),
Err(UiError::Clipboard(_))
));
}
}