pub mod background;
pub mod hit;
pub mod pan;
pub mod selection;
pub mod snap;
pub mod spatial;
pub mod viewport;
pub mod zoom;
use std::collections::HashSet;
use std::rc::Rc;
use std::sync::Arc;
use blinc_core::draw::Stroke;
use blinc_core::events::event_types;
use blinc_core::layer::{Affine2D, Color, CornerRadius, Point, Rect};
use blinc_core::{BlincContextState, Brush, SignalId, State};
use blinc_layout::canvas::{canvas, CanvasBounds};
use blinc_layout::div::{div, Div};
use blinc_layout::event_handler::EventContext;
pub use background::{CanvasBackground, PatternConfig, ZoomAdaptive};
pub use hit::{CanvasDragEvent, CanvasEvent, HitRegion, InteractionState};
pub use pan::PanController;
pub use selection::{CanvasTool, MarqueeState, SelectionChangeEvent, SelectionState};
pub use snap::SnapController;
pub use spatial::SpatialIndex;
pub use viewport::{affine_inverse, CanvasViewport};
pub use zoom::ZoomController;
pub mod prelude {
pub use crate::background::{CanvasBackground, PatternConfig, ZoomAdaptive};
pub use crate::hit::{CanvasDragEvent, CanvasEvent, HitRegion, InteractionState};
pub use crate::pan::PanController;
pub use crate::selection::{CanvasTool, MarqueeState, SelectionChangeEvent, SelectionState};
pub use crate::snap::SnapController;
pub use crate::spatial::SpatialIndex;
pub use crate::viewport::{affine_inverse, CanvasViewport};
pub use crate::zoom::ZoomController;
pub use crate::CanvasKit;
}
type EventCallback = Arc<dyn Fn(&CanvasEvent) + Send + Sync>;
type DragCallback = Arc<dyn Fn(&CanvasDragEvent) + Send + Sync>;
type SelectionCallback = Arc<dyn Fn(&SelectionChangeEvent) + Send + Sync>;
#[derive(Clone)]
pub struct CanvasKit {
viewport: State<CanvasViewport>,
pan: State<PanController>,
zoom_controller: ZoomController,
hit_regions: State<SpatialIndex>,
interaction: State<InteractionState>,
selection: State<SelectionState>,
tool: State<CanvasTool>,
screen_bounds: State<(f32, f32)>,
snap: SnapController,
background: CanvasBackground,
on_click_cb: Option<EventCallback>,
on_hover_cb: Option<EventCallback>,
on_drag_cb: Option<DragCallback>,
on_drag_end_cb: Option<EventCallback>,
on_selection_change_cb: Option<SelectionCallback>,
}
impl CanvasKit {
pub fn new(key: &str) -> Self {
let ctx = BlincContextState::get();
Self {
viewport: ctx.use_state_keyed(&format!("{key}_vp"), CanvasViewport::new),
pan: ctx.use_state_keyed(&format!("{key}_pan"), PanController::new),
zoom_controller: ZoomController::new(),
hit_regions: ctx.use_state_keyed(&format!("{key}_hit"), || SpatialIndex::new(100.0)),
interaction: ctx.use_state_keyed(&format!("{key}_ia"), InteractionState::default),
selection: ctx.use_state_keyed(&format!("{key}_sel"), SelectionState::new),
tool: ctx.use_state_keyed(&format!("{key}_tool"), CanvasTool::default),
screen_bounds: ctx.use_state_keyed(&format!("{key}_sb"), || (0.0, 0.0)),
snap: SnapController::disabled(),
background: CanvasBackground::None,
on_click_cb: None,
on_hover_cb: None,
on_drag_cb: None,
on_drag_end_cb: None,
on_selection_change_cb: None,
}
}
pub fn with_zoom_controller(mut self, zc: ZoomController) -> Self {
self.zoom_controller = zc;
self
}
pub fn with_background(mut self, bg: CanvasBackground) -> Self {
self.background = bg;
self
}
pub fn set_background(&mut self, bg: CanvasBackground) {
self.background = bg;
}
pub fn with_spatial_cell_size(self, cell_size: f32) -> Self {
self.hit_regions.update(|_| SpatialIndex::new(cell_size));
self
}
pub fn with_tool(self, tool: CanvasTool) -> Self {
self.tool.set(tool);
self
}
pub fn set_tool(&self, tool: CanvasTool) {
self.tool.set(tool);
}
pub fn tool(&self) -> CanvasTool {
self.tool.get()
}
pub fn with_snap(mut self, spacing: f32) -> Self {
self.snap = SnapController::new(spacing);
self
}
pub fn set_snap_enabled(&mut self, enabled: bool) {
self.snap.enabled = enabled;
}
pub fn set_snap_spacing(&mut self, spacing: f32) {
self.snap.spacing = spacing.max(1.0);
}
pub fn snap_enabled(&self) -> bool {
self.snap.enabled
}
pub fn snap_point(&self, pt: Point) -> Point {
self.snap.snap_point(pt)
}
pub fn snap_rect(&self, rect: Rect) -> Rect {
let snapped = self.snap.snap_point(Point::new(rect.x(), rect.y()));
Rect::new(snapped.x, snapped.y, rect.width(), rect.height())
}
pub fn on_element_click(&mut self, cb: impl Fn(&CanvasEvent) + Send + Sync + 'static) {
self.on_click_cb = Some(Arc::new(cb));
}
pub fn on_element_hover(&mut self, cb: impl Fn(&CanvasEvent) + Send + Sync + 'static) {
self.on_hover_cb = Some(Arc::new(cb));
}
pub fn on_element_drag(&mut self, cb: impl Fn(&CanvasDragEvent) + Send + Sync + 'static) {
self.on_drag_cb = Some(Arc::new(cb));
}
pub fn on_element_drag_end(&mut self, cb: impl Fn(&CanvasEvent) + Send + Sync + 'static) {
self.on_drag_end_cb = Some(Arc::new(cb));
}
pub fn on_selection_change(
&mut self,
cb: impl Fn(&SelectionChangeEvent) + Send + Sync + 'static,
) {
self.on_selection_change_cb = Some(Arc::new(cb));
}
pub fn selection(&self) -> SelectionState {
self.selection.get()
}
pub fn selection_signal(&self) -> SignalId {
self.selection.signal_id()
}
pub fn set_selection(&self, ids: HashSet<String>) {
let old = self.selection.get();
let added: HashSet<_> = ids.difference(&old.selected).cloned().collect();
let removed: HashSet<_> = old.selected.difference(&ids).cloned().collect();
if added.is_empty() && removed.is_empty() {
return;
}
self.selection.update(|mut s| {
s.selected = ids.clone();
s
});
if let Some(ref cb) = self.on_selection_change_cb {
cb(&SelectionChangeEvent {
selected: ids,
added,
removed,
});
}
}
pub fn clear_selection(&self) {
self.set_selection(HashSet::new());
}
pub fn is_selected(&self, id: &str) -> bool {
self.selection.get().is_selected(id)
}
pub fn hit_rect(&self, id: impl Into<String>, rect: Rect) {
self.hit_regions.update(|mut index| {
index.insert(HitRegion::new(id, rect));
index
});
}
pub fn begin_frame(&self) {
self.hit_regions.update(|mut index| {
index.clear();
index
});
}
pub fn hit_test(&self, content_point: Point) -> Option<String> {
self.hit_regions.get().hit_test(content_point)
}
pub fn query_rect(&self, rect: Rect) -> HashSet<String> {
self.hit_regions.get().query_rect(&rect)
}
pub fn is_visible(&self, rect: Rect) -> bool {
let (sw, sh) = self.screen_bounds.get();
if sw <= 0.0 || sh <= 0.0 {
return true;
}
let vp = self.viewport.get();
vp.is_visible(rect.x(), rect.y(), rect.width(), rect.height(), sw, sh)
}
pub fn handle_event(&self, evt: &EventContext) {
let screen_pt = Point::new(evt.local_x, evt.local_y);
match evt.event_type {
event_types::POINTER_DOWN => {
self.handle_pointer_down(evt, screen_pt);
}
event_types::POINTER_MOVE => {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
let hit = self.hit_test(content_pt);
let mut ia = self.interaction.get();
let changed = ia.hovered != hit;
ia.hovered = hit;
self.interaction.set(ia);
if changed {
if let Some(ref cb) = self.on_hover_cb {
let ia = self.interaction.get();
cb(&CanvasEvent {
content_point: content_pt,
screen_point: screen_pt,
region_id: ia.hovered.clone(),
});
}
}
}
event_types::POINTER_UP => {
self.handle_pointer_up(evt, screen_pt);
}
event_types::DRAG => {
self.handle_drag(evt, screen_pt);
}
event_types::DRAG_END => {
self.handle_drag_end(evt, screen_pt);
}
event_types::SCROLL => {
let cursor = screen_pt;
let zc = self.zoom_controller.clone();
self.viewport.update(|mut vp| {
zc.on_scroll(&mut vp, evt.scroll_delta_y, cursor);
vp
});
}
event_types::PINCH => {
let center = Point::new(evt.pinch_center_x, evt.pinch_center_y);
let zc = self.zoom_controller.clone();
self.viewport.update(|mut vp| {
zc.on_pinch(&mut vp, evt.pinch_scale, center);
vp
});
}
_ => {}
}
}
fn handle_pointer_down(&self, evt: &EventContext, screen_pt: Point) {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
let hit = self.hit_test(content_pt);
let tool = self.tool.get();
if let Some(ref region_id) = hit {
let sel = self.selection.get();
if evt.meta || evt.ctrl {
let mut new_sel = sel.selected.clone();
if new_sel.contains(region_id) {
new_sel.remove(region_id);
} else {
new_sel.insert(region_id.clone());
}
self.set_selection(new_sel);
} else if evt.shift {
let mut new_sel = sel.selected.clone();
new_sel.insert(region_id.clone());
self.set_selection(new_sel);
} else if !sel.is_selected(region_id) {
let mut new_sel = HashSet::new();
new_sel.insert(region_id.clone());
self.set_selection(new_sel);
}
self.interaction.set(InteractionState {
hovered: self.interaction.get().hovered,
active: hit,
drag_start: Some(content_pt),
did_drag: false,
});
} else {
let should_marquee = tool == CanvasTool::Select || evt.shift;
if should_marquee {
let additive = evt.shift;
self.selection.update(|mut s| {
s.marquee = Some(MarqueeState {
anchor: content_pt,
current: content_pt,
additive,
base_selection: if additive {
s.selected.clone()
} else {
HashSet::new()
},
});
s
});
}
if !evt.shift && !evt.meta && !evt.ctrl && tool != CanvasTool::Select {
self.clear_selection();
}
self.interaction.set(InteractionState {
hovered: self.interaction.get().hovered,
active: None,
drag_start: Some(content_pt),
did_drag: false,
});
}
}
fn handle_pointer_up(&self, evt: &EventContext, screen_pt: Point) {
let ia = self.interaction.get();
let sel = self.selection.get();
if ia.active.is_some() && !ia.did_drag {
if let Some(ref cb) = self.on_click_cb {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
cb(&CanvasEvent {
content_point: content_pt,
screen_point: screen_pt,
region_id: ia.active.clone(),
});
}
if !evt.shift && !evt.meta && !evt.ctrl {
if let Some(ref id) = ia.active {
if sel.selected.len() > 1 {
let mut new_sel = HashSet::new();
new_sel.insert(id.clone());
self.set_selection(new_sel);
}
}
}
}
if sel.marquee.is_some() {
let base = sel
.marquee
.as_ref()
.map(|m| m.base_selection.clone())
.unwrap_or_default();
self.selection.update(|mut s| {
s.marquee = None;
s
});
let final_sel = self.selection.get();
if let Some(ref cb) = self.on_selection_change_cb {
let added = final_sel.selected.difference(&base).cloned().collect();
let removed = base.difference(&final_sel.selected).cloned().collect();
cb(&SelectionChangeEvent {
selected: final_sel.selected.clone(),
added,
removed,
});
}
}
self.interaction.set(InteractionState {
hovered: ia.hovered,
active: None,
drag_start: None,
did_drag: false,
});
}
fn handle_drag(&self, evt: &EventContext, screen_pt: Point) {
let ia = self.interaction.get();
let sel = self.selection.get();
if ia.active.is_some() {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
let delta = if let Some(start) = ia.drag_start {
Point::new(content_pt.x - start.x, content_pt.y - start.y)
} else {
Point::new(0.0, 0.0)
};
if let Some(ref cb) = self.on_drag_cb {
for selected_id in &sel.selected {
cb(&CanvasDragEvent {
content_point: content_pt,
content_delta: delta,
screen_point: screen_pt,
region_id: selected_id.clone(),
});
}
if let Some(ref active_id) = ia.active {
if !sel.selected.contains(active_id) {
cb(&CanvasDragEvent {
content_point: content_pt,
content_delta: delta,
screen_point: screen_pt,
region_id: active_id.clone(),
});
}
}
}
self.interaction.set(InteractionState {
hovered: ia.hovered,
active: ia.active,
drag_start: Some(content_pt),
did_drag: true,
});
} else if sel.marquee.is_some() {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
let marquee_anchor = sel.marquee.as_ref().map(|m| m.anchor).unwrap_or(content_pt);
let marquee_rect = Rect::from_points(marquee_anchor, content_pt);
let hits = self.query_rect(marquee_rect);
self.selection.update(|mut s| {
if let Some(ref mut m) = s.marquee {
m.current = content_pt;
if m.additive {
s.selected = m.base_selection.clone();
s.selected.extend(hits);
} else {
s.selected = hits;
}
}
s
});
} else {
let dx = evt.drag_delta_x;
let dy = evt.drag_delta_y;
let mut vp = self.viewport.get();
let mut pan = self.pan.get();
pan.on_drag(&mut vp, dx, dy);
self.pan.set(pan);
self.viewport.set(vp);
}
}
fn handle_drag_end(&self, evt: &EventContext, screen_pt: Point) {
let ia = self.interaction.get();
if ia.active.is_some() {
if let Some(ref cb) = self.on_drag_end_cb {
let vp = self.viewport.get();
let content_pt = vp.screen_to_content(screen_pt);
cb(&CanvasEvent {
content_point: content_pt,
screen_point: screen_pt,
region_id: ia.active.clone(),
});
}
self.interaction.set(InteractionState {
hovered: ia.hovered,
active: None,
drag_start: None,
did_drag: false,
});
} else {
let sel = self.selection.get();
if sel.marquee.is_none() {
self.pan.update(|mut pan| {
pan.on_drag_end();
pan
});
}
let _ = evt;
}
}
pub fn handler(&self) -> Arc<dyn Fn(&EventContext) + Send + Sync + 'static> {
let kit = self.clone();
Arc::new(move |evt: &EventContext| {
kit.handle_event(evt);
})
}
pub fn element<F>(&self, render_fn: F) -> Div
where
F: Fn(&mut dyn blinc_core::DrawContext, CanvasBounds) + 'static,
{
let kit_render = self.clone();
let render = Rc::new(render_fn);
let h = self.handler();
let h_drag = h.clone();
let h_drag_end = h.clone();
let h_scroll = h.clone();
let h_pinch = h.clone();
let h_ptr_down = h.clone();
let h_ptr_up = h.clone();
let h_ptr_move = h;
div()
.w_full()
.h_full()
.on_event(event_types::POINTER_DOWN, move |evt| h_ptr_down(evt))
.on_event(event_types::POINTER_UP, move |evt| h_ptr_up(evt))
.on_event(event_types::POINTER_MOVE, move |evt| h_ptr_move(evt))
.on_drag(move |evt| h_drag(evt))
.on_drag_end(move |evt| h_drag_end(evt))
.on_scroll(move |evt| h_scroll(evt))
.on_event(event_types::PINCH, move |evt| h_pinch(evt))
.child(
canvas(move |ctx, bounds| {
kit_render.screen_bounds.set((bounds.width, bounds.height));
kit_render.begin_frame();
let vp = kit_render.viewport();
let transform = vp.transform();
ctx.push_transform(transform.into());
kit_render
.background
.draw(ctx, &vp, bounds.width, bounds.height);
render(ctx, bounds);
let sel = kit_render.selection.get();
if let Some(ref marquee) = sel.marquee {
let rect = marquee.rect();
let stroke = Stroke::new(1.0 / vp.zoom)
.with_dash(vec![4.0 / vp.zoom, 3.0 / vp.zoom], 0.0);
ctx.stroke_rect(
rect,
CornerRadius::uniform(0.0),
&stroke,
Brush::Solid(Color::rgba(0.2, 0.5, 1.0, 0.8)),
);
ctx.fill_rect(
rect,
CornerRadius::uniform(0.0),
Brush::Solid(Color::rgba(0.2, 0.5, 1.0, 0.15)),
);
}
ctx.pop_transform();
})
.w_full()
.h_full(),
)
}
pub fn transform(&self) -> Affine2D {
self.viewport.get().transform()
}
pub fn viewport(&self) -> CanvasViewport {
self.viewport.get()
}
pub fn update_viewport(&self, f: impl FnOnce(&mut CanvasViewport)) {
self.viewport.update(|mut vp| {
f(&mut vp);
vp
});
}
pub fn viewport_signal(&self) -> SignalId {
self.viewport.signal_id()
}
pub fn screen_to_content(&self, screen: Point) -> Point {
self.viewport.get().screen_to_content(screen)
}
pub fn content_to_screen(&self, content: Point) -> Point {
self.viewport.get().content_to_screen(content)
}
pub fn interaction(&self) -> InteractionState {
self.interaction.get()
}
pub fn interaction_signal(&self) -> SignalId {
self.interaction.signal_id()
}
}