Skip to content

Commit bdf030b

Browse files
authored
add canvas widget (#785)
1 parent ded3c62 commit bdf030b

File tree

7 files changed

+198
-1
lines changed

7 files changed

+198
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use floem::{kurbo::Rect, prelude::*};
2+
use palette::css;
3+
4+
use crate::form::{form, form_item};
5+
6+
pub fn canvas_view() -> impl IntoView {
7+
let rounded = RwSignal::new(true);
8+
9+
form((form_item(
10+
"Simple Canvas:",
11+
h_stack((
12+
canvas(move |cx, size| {
13+
cx.fill(
14+
&Rect::ZERO
15+
.with_size(size)
16+
.to_rounded_rect(if rounded.get() { 8. } else { 0. }),
17+
css::PURPLE,
18+
0.,
19+
);
20+
})
21+
.style(|s| s.size(100, 300)),
22+
button("toggle")
23+
.action(move || rounded.update(|s| *s = !*s))
24+
.style(|s| s.height(30)),
25+
))
26+
.style(|s| s.gap(10).items_center()),
27+
),))
28+
}

examples/widget-gallery/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod animation;
22
pub mod buttons;
3+
pub mod canvas;
34
pub mod checkbox;
45
pub mod clipboard;
56
pub mod context_menu;
@@ -32,6 +33,7 @@ fn app_view() -> impl IntoView {
3233
"Checkbox",
3334
"Radio",
3435
"Input",
36+
"Canvas",
3537
"List",
3638
"Menu",
3739
"RichText",
@@ -52,6 +54,7 @@ fn app_view() -> impl IntoView {
5254
"Checkbox" => checkbox::checkbox_view().into_any(),
5355
"Radio" => radio_buttons::radio_buttons_view().into_any(),
5456
"Input" => inputs::text_input_view().into_any(),
57+
"Canvas" => canvas::canvas_view().into_any(),
5558
"List" => lists::virt_list_view().into_any(),
5659
"Menu" => context_menu::menu_view().into_any(),
5760
"RichText" => rich_text::rich_text_view().into_any(),

reactive/src/effect.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,79 @@ where
280280
mem::take(&mut *self.observers.borrow_mut())
281281
}
282282
}
283+
284+
pub struct SignalTracker {
285+
id: Id,
286+
on_change: Rc<dyn Fn()>,
287+
}
288+
289+
impl Drop for SignalTracker {
290+
fn drop(&mut self) {
291+
self.id.dispose();
292+
}
293+
}
294+
295+
pub fn create_tracker(on_change: impl Fn() + 'static) -> SignalTracker {
296+
let id = Id::next();
297+
298+
SignalTracker {
299+
id,
300+
on_change: Rc::new(on_change),
301+
}
302+
}
303+
304+
impl SignalTracker {
305+
pub fn track<T: 'static>(&self, f: impl FnOnce() -> T) -> T {
306+
// Clear any previous tracking by disposing the old effect
307+
self.id.dispose();
308+
309+
let prev_effect = RUNTIME.with(|runtime| runtime.current_effect.borrow_mut().take());
310+
311+
let tracking_effect = Rc::new(TrackingEffect {
312+
id: self.id,
313+
observers: RefCell::new(HashSet::default()),
314+
on_change: self.on_change.clone(),
315+
});
316+
317+
RUNTIME.with(|runtime| {
318+
*runtime.current_effect.borrow_mut() = Some(tracking_effect.clone());
319+
});
320+
321+
let effect_scope = Scope(self.id, PhantomData);
322+
let result = with_scope(effect_scope, || {
323+
effect_scope.track();
324+
f()
325+
});
326+
327+
RUNTIME.with(|runtime| {
328+
*runtime.current_effect.borrow_mut() = prev_effect;
329+
});
330+
331+
result
332+
}
333+
}
334+
335+
struct TrackingEffect {
336+
id: Id,
337+
observers: RefCell<HashSet<Id>>,
338+
on_change: Rc<dyn Fn()>,
339+
}
340+
341+
impl EffectTrait for TrackingEffect {
342+
fn id(&self) -> Id {
343+
self.id
344+
}
345+
346+
fn run(&self) -> bool {
347+
(self.on_change)();
348+
true
349+
}
350+
351+
fn add_observer(&self, id: Id) {
352+
self.observers.borrow_mut().insert(id);
353+
}
354+
355+
fn clear_observers(&self) -> HashSet<Id> {
356+
mem::take(&mut *self.observers.borrow_mut())
357+
}
358+
}

reactive/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ mod write;
2121
pub use base::{create_base_signal, BaseSignal};
2222
pub use context::{provide_context, use_context};
2323
pub use derived::{create_derived_rw_signal, DerivedRwSignal};
24-
pub use effect::{batch, create_effect, create_stateful_updater, create_updater, untrack};
24+
pub use effect::{
25+
batch, create_effect, create_stateful_updater, create_tracker, create_updater, untrack,
26+
SignalTracker,
27+
};
2528
pub use memo::{create_memo, Memo};
2629
pub use read::{ReadSignalValue, SignalGet, SignalRead, SignalTrack, SignalWith};
2730
pub use scope::{as_child_of_current_scope, with_scope, Scope};

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub mod prelude {
238238
pub use crate::unit::{DurationUnitExt, UnitExt};
239239
pub use crate::view_tuple::ViewTuple;
240240
pub use crate::views::*;
241+
pub use crate::Renderer;
241242
pub use crate::{IntoView, View};
242243
pub use floem_reactive::{
243244
create_rw_signal, create_signal, RwSignal, SignalGet, SignalTrack, SignalUpdate, SignalWith,

src/views/canvas.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#![deny(missing_docs)]
2+
3+
use floem_reactive::{create_tracker, SignalTracker};
4+
use peniko::kurbo::Size;
5+
6+
use crate::{context::PaintCx, id::ViewId, view::View};
7+
8+
/// A canvas view
9+
#[allow(clippy::type_complexity)]
10+
pub struct Canvas {
11+
id: ViewId,
12+
paint_fn: Box<dyn Fn(&mut PaintCx, Size)>,
13+
size: Size,
14+
tracker: Option<SignalTracker>,
15+
}
16+
17+
/// Creates a new Canvas view that can be used for custom painting
18+
///
19+
/// A [`Canvas`] provides a low-level interface for custom drawing operations. The supplied
20+
/// paint function will be called whenever the view needs to be rendered, and any signals accessed
21+
/// within the paint function will automatically trigger repaints when they change.
22+
///
23+
///
24+
/// # Example
25+
/// ```rust
26+
/// use floem::prelude::*;
27+
/// use palette::css;
28+
/// use peniko::kurbo::Rect;
29+
/// canvas(move |cx, size| {
30+
/// cx.fill(
31+
/// &Rect::ZERO
32+
/// .with_size(size)
33+
/// .to_rounded_rect(8.),
34+
/// css::PURPLE,
35+
/// 0.,
36+
/// );
37+
/// })
38+
/// .style(|s| s.size(100, 300));
39+
/// ```
40+
pub fn canvas(paint: impl Fn(&mut PaintCx, Size) + 'static) -> Canvas {
41+
let id = ViewId::new();
42+
43+
Canvas {
44+
id,
45+
paint_fn: Box::new(paint),
46+
size: Default::default(),
47+
tracker: None,
48+
}
49+
}
50+
51+
impl View for Canvas {
52+
fn id(&self) -> ViewId {
53+
self.id
54+
}
55+
56+
fn debug_name(&self) -> std::borrow::Cow<'static, str> {
57+
"Canvas".into()
58+
}
59+
60+
fn compute_layout(
61+
&mut self,
62+
_cx: &mut crate::context::ComputeLayoutCx,
63+
) -> Option<peniko::kurbo::Rect> {
64+
self.size = self.id.get_size().unwrap_or_default();
65+
None
66+
}
67+
68+
fn paint(&mut self, cx: &mut PaintCx) {
69+
let id = self.id;
70+
let paint = &self.paint_fn;
71+
72+
if self.tracker.is_none() {
73+
self.tracker = Some(create_tracker(move || {
74+
id.request_paint();
75+
}));
76+
}
77+
78+
let tracker = self.tracker.as_ref().unwrap();
79+
tracker.track(|| {
80+
paint(cx, self.size);
81+
});
82+
}
83+
}

src/views/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ pub use text_input::*;
124124
mod empty;
125125
pub use empty::*;
126126

127+
mod canvas;
128+
pub use canvas::*;
129+
127130
mod drag_window_area;
128131
pub use drag_window_area::*;
129132

0 commit comments

Comments
 (0)