use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use teksilo_core::ObserverHandle;
use teksilo_core::drag_payload::{DragPayload, DropOutcome};
use teksilo_core::widget::{EventContext, Widget};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_data::{
DataChange, DropPosition, ItemKey, KeyedSelectionModel, SelectionMode, SelectionModel,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActivateOn {
SingleClick,
#[default]
DoubleClick,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ViewKind {
List,
Tree,
Table,
TreeTable,
Grid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ViewId(ViewKind, usize);
impl ViewId {
pub(crate) fn next(kind: ViewKind) -> Self {
Self(kind, next_view_id())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DragTransferMode {
Copy,
#[default]
Move,
}
#[derive(Debug)]
pub struct RowDragData<T: 'static> {
pub source: ViewId,
pub rows: Vec<usize>,
pub items: Option<Vec<T>>,
}
impl<T: 'static> RowDragData<T> {
pub fn items(&self) -> Option<&[T]> {
self.items.as_deref()
}
pub fn into_items(self) -> Option<Vec<T>> {
self.items
}
pub fn is_export(&self) -> bool {
self.items.is_some()
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct DropIndicator {
pub(crate) y: f32,
pub(crate) width: f32,
pub(crate) allowed: bool,
}
pub(crate) fn next_view_id() -> usize {
static NEXT: AtomicUsize = AtomicUsize::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
pub(crate) fn flat_insertion_target(insertion: usize, len: usize) -> Option<(usize, DropPosition)> {
if len == 0 {
None
} else if insertion >= len {
Some((len - 1, DropPosition::After))
} else {
Some((insertion, DropPosition::Before))
}
}
pub(crate) enum ResolvedRowTooltip {
Plain(teksilo_i18n::LocalizedString),
Rich(crate::tooltip::RichTooltipSource),
Composite(Box<dyn Widget>),
}
pub(crate) struct RowTooltips<T: 'static> {
plain: Option<Rc<dyn Fn(usize, &T) -> Option<teksilo_i18n::LocalizedString>>>,
rich: Option<Rc<dyn Fn(usize, &T) -> Option<crate::tooltip::RichTooltipSource>>>,
composite: Option<Rc<dyn Fn(usize, &T) -> Option<Box<dyn Widget>>>>,
composite_sticky: bool,
}
impl<T: 'static> Default for RowTooltips<T> {
fn default() -> Self {
Self {
plain: None,
rich: None,
composite: None,
composite_sticky: true,
}
}
}
impl<T: 'static> Clone for RowTooltips<T> {
fn clone(&self) -> Self {
Self {
plain: self.plain.clone(),
rich: self.rich.clone(),
composite: self.composite.clone(),
composite_sticky: self.composite_sticky,
}
}
}
impl<T: 'static> std::fmt::Debug for RowTooltips<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RowTooltips")
.field("plain", &self.plain.is_some())
.field("rich", &self.rich.is_some())
.field("composite", &self.composite.is_some())
.finish()
}
}
impl<T: 'static> RowTooltips<T> {
pub(crate) fn is_set(&self) -> bool {
self.plain.is_some() || self.rich.is_some() || self.composite.is_some()
}
pub(crate) fn set_plain(
&mut self,
f: impl Fn(usize, &T) -> Option<teksilo_i18n::LocalizedString> + 'static,
) {
*self = Self {
plain: Some(Rc::new(f)),
rich: None,
composite: None,
composite_sticky: self.composite_sticky,
};
}
pub(crate) fn set_rich(
&mut self,
f: impl Fn(usize, &T) -> Option<crate::tooltip::RichTooltipSource> + 'static,
) {
*self = Self {
plain: None,
rich: Some(Rc::new(f)),
composite: None,
composite_sticky: self.composite_sticky,
};
}
pub(crate) fn set_composite(
&mut self,
f: impl Fn(usize, &T) -> Option<Box<dyn Widget>> + 'static,
) {
*self = Self {
plain: None,
rich: None,
composite: Some(Rc::new(f)),
composite_sticky: self.composite_sticky,
};
}
pub(crate) fn set_composite_sticky(&mut self, on: bool) {
self.composite_sticky = on;
}
pub(crate) fn resolve(&self, index: usize, item: &T) -> Option<ResolvedRowTooltip> {
if let Some(f) = &self.composite {
f(index, item).map(ResolvedRowTooltip::Composite)
} else if let Some(f) = &self.rich {
f(index, item).map(ResolvedRowTooltip::Rich)
} else if let Some(f) = &self.plain {
f(index, item).map(ResolvedRowTooltip::Plain)
} else {
None
}
}
pub(crate) fn attach_resolved(
&self,
ctx: &mut teksilo_core::build_context::BuildContext,
row_id: teksilo_core::widget_id::WidgetId,
resolved: ResolvedRowTooltip,
) {
let placement = crate::tooltip::TooltipPlacement::Side;
match resolved {
ResolvedRowTooltip::Composite(body) => {
let delay = ctx.theme().motion.tooltip_delay_heavy;
crate::tooltip::attach_composite_tooltip_widget_with_placement(
ctx,
row_id,
crate::tooltip::CompositeTooltipWidget::new()
.content_boxed(body)
.sticky(self.composite_sticky),
delay,
placement,
);
}
ResolvedRowTooltip::Rich(source) => {
let delay = ctx.theme().motion.tooltip_delay;
crate::tooltip::attach_rich_tooltip_source_with_placement(
ctx, row_id, source, delay, placement,
);
}
ResolvedRowTooltip::Plain(text) => {
let delay = ctx.theme().motion.tooltip_delay;
crate::tooltip::attach_plain_tooltip_with_placement(
ctx, row_id, text, delay, placement,
);
}
}
}
}
pub(crate) fn default_placeholder() -> Box<dyn Widget> {
use crate::primitives::{Padding, RectWidget};
Box::new(
Padding::uniform(6.0).child(
RectWidget::new()
.background(teksilo_tokens::SurfaceRole::Hover)
.corner_radius(teksilo_tokens::CornerRadius::uniform(4.0)),
),
)
}
#[derive(Clone)]
pub(crate) struct RowSelection {
mode: SelectionMode,
is_selected: Rc<dyn Fn(usize) -> bool>,
select_fn: Rc<dyn Fn(usize)>,
toggle_fn: Rc<dyn Fn(usize)>,
extend_fn: Rc<dyn Fn(usize)>,
select_all_fn: Rc<dyn Fn(usize)>,
selected_indices_fn: Rc<dyn Fn() -> Vec<usize>>,
has_selection_fn: Rc<dyn Fn() -> bool>,
clear_fn: Rc<dyn Fn()>,
observe_fn: Rc<dyn Fn(Box<dyn Fn()>) -> ObserverHandle>,
on_change_fn: Rc<dyn Fn(&DataChange)>,
prune_fn: Rc<dyn Fn()>,
prune_range_fn: Rc<dyn Fn(usize)>,
}
impl RowSelection {
pub(crate) fn from_index(sel: SelectionModel) -> Self {
let (s_is, s_sel, s_tog, s_ext, s_all, s_idx, s_has, s_clr, s_obs, s_chg, s_range) = (
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
sel.clone(),
);
Self {
mode: sel.mode(),
is_selected: Rc::new(move |i| s_is.is_selected(i)),
select_fn: Rc::new(move |i| s_sel.select(i)),
toggle_fn: Rc::new(move |i| s_tog.toggle(i)),
extend_fn: Rc::new(move |i| s_ext.extend_to(i)),
select_all_fn: Rc::new(move |count| s_all.select_all(count)),
selected_indices_fn: Rc::new(move || s_idx.selected_indices()),
has_selection_fn: Rc::new(move || s_has.count() > 0),
clear_fn: Rc::new(move || s_clr.clear()),
observe_fn: Rc::new(move |cb| s_obs.selection_signal().observe(move |_| cb())),
on_change_fn: Rc::new(move |change| match change {
DataChange::ItemsInserted { range } => {
s_chg.adjust_for_insert(range.start, range.end - range.start);
}
DataChange::ItemsRemoved { range } => {
s_chg.adjust_for_remove(range.start, range.end - range.start);
}
DataChange::ItemsMoved { from, to, count } => {
s_chg.adjust_for_move(*from, *to, *count);
}
DataChange::Reset => s_chg.clear(),
_ => {}
}),
prune_fn: Rc::new(|| {}),
prune_range_fn: Rc::new(move |count| {
let kept: Vec<usize> = s_range
.selected_indices()
.into_iter()
.filter(|&i| i < count)
.collect();
if kept.len() != s_range.count() {
s_range.select_indices(kept, false);
}
}),
}
}
pub(crate) fn from_keyed<K: ItemKey>(
keyed: KeyedSelectionModel<K>,
key_at: Rc<dyn Fn(usize) -> Option<K>>,
len: Rc<dyn Fn() -> usize>,
contains_key: Rc<dyn Fn(&K) -> bool>,
) -> Self {
let mode = keyed.mode();
Self {
mode,
is_selected: {
let (k, ka) = (keyed.clone(), key_at.clone());
Rc::new(move |i| ka(i).map(|key| k.is_selected(&key)).unwrap_or(false))
},
select_fn: {
let (k, ka) = (keyed.clone(), key_at.clone());
Rc::new(move |i| {
if let Some(key) = ka(i) {
k.select(key);
}
})
},
toggle_fn: {
let (k, ka) = (keyed.clone(), key_at.clone());
Rc::new(move |i| {
if let Some(key) = ka(i) {
k.toggle(key);
}
})
},
extend_fn: {
let (k, ka, l) = (keyed.clone(), key_at.clone(), len.clone());
Rc::new(move |i| {
if let Some(target) = ka(i) {
let ordered: Vec<K> = (0..l()).filter_map(|j| ka(j)).collect();
k.extend_to(target, &ordered);
}
})
},
select_all_fn: {
let (k, ka) = (keyed.clone(), key_at.clone());
Rc::new(move |count| {
let keys: Vec<K> = (0..count).filter_map(|i| ka(i)).collect();
k.select_keys(keys, false);
})
},
selected_indices_fn: {
let (k, ka, l) = (keyed.clone(), key_at.clone(), len.clone());
Rc::new(move || {
(0..l())
.filter(|&i| ka(i).map(|key| k.is_selected(&key)).unwrap_or(false))
.collect()
})
},
has_selection_fn: {
let k = keyed.clone();
Rc::new(move || k.count() > 0)
},
clear_fn: {
let k = keyed.clone();
Rc::new(move || k.clear())
},
observe_fn: {
let k = keyed.clone();
Rc::new(move |cb| k.selection_signal().observe(move |_| cb()))
},
on_change_fn: {
let (k, c) = (keyed.clone(), contains_key.clone());
Rc::new(move |change| match change {
DataChange::ItemsRemoved { .. } | DataChange::Reset => {
k.prune_missing(|key| c(key));
}
_ => {}
})
},
prune_fn: {
let (k, c) = (keyed, contains_key);
Rc::new(move || k.prune_missing(|key| c(key)))
},
prune_range_fn: Rc::new(|_count: usize| {}),
}
}
pub(crate) fn mode(&self) -> SelectionMode {
self.mode
}
pub(crate) fn is_selected(&self, index: usize) -> bool {
(self.is_selected)(index)
}
pub(crate) fn select(&self, index: usize) {
(self.select_fn)(index)
}
pub(crate) fn toggle(&self, index: usize) {
(self.toggle_fn)(index)
}
pub(crate) fn extend_to(&self, index: usize) {
(self.extend_fn)(index)
}
pub(crate) fn select_all(&self, count: usize) {
(self.select_all_fn)(count)
}
pub(crate) fn selected_indices(&self) -> Vec<usize> {
(self.selected_indices_fn)()
}
pub(crate) fn has_selection(&self) -> bool {
(self.has_selection_fn)()
}
pub(crate) fn clear(&self) {
(self.clear_fn)()
}
pub(crate) fn observe_for_rebuild(&self, cb: impl Fn() + 'static) -> ObserverHandle {
(self.observe_fn)(Box::new(cb))
}
pub(crate) fn on_data_change(&self, change: &DataChange) {
(self.on_change_fn)(change)
}
pub(crate) fn prune(&self) {
(self.prune_fn)()
}
pub(crate) fn prune_out_of_range(&self, count: usize) {
(self.prune_range_fn)(count)
}
}
pub(crate) type SnapshotOutFn = Rc<dyn Fn(&[usize]) -> Box<dyn Fn()>>;
#[derive(Clone)]
pub struct RowAnchor {
resolve: Rc<dyn Fn() -> Option<usize>>,
}
impl RowAnchor {
pub(crate) fn new(resolve: Rc<dyn Fn() -> Option<usize>>) -> Self {
Self { resolve }
}
pub(crate) fn fixed(index: usize) -> Self {
Self {
resolve: Rc::new(move || Some(index)),
}
}
pub fn index(&self) -> Option<usize> {
(self.resolve)()
}
pub fn is_live(&self) -> bool {
self.index().is_some()
}
}
impl std::fmt::Debug for RowAnchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("RowAnchor(..)")
}
}
pub(crate) fn reconcile_editing_row(
editing_cell: &teksilo_core::signal::Signal<Option<(usize, usize)>>,
slot: &Rc<std::cell::RefCell<Option<RowAnchor>>>,
anchor_of: &dyn Fn(usize) -> RowAnchor,
) {
let Some((row, col)) = editing_cell.get() else {
*slot.borrow_mut() = None;
return;
};
let existing = slot.borrow().clone();
match existing {
None => *slot.borrow_mut() = Some(anchor_of(row)),
Some(anchor) => match anchor.index() {
Some(cur) if cur != row => editing_cell.set(Some((cur, col))),
Some(_) => {}
None => {
editing_cell.set(None);
*slot.borrow_mut() = None;
}
},
}
}
pub(crate) fn drop_into_tint() -> teksilo_tokens::Color {
teksilo_tokens::Color::from_rgba(0.25, 0.47, 0.85, 0.25)
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) enum DropViz {
Line { y: f32, width: f32, depth: usize },
Rect {
top: f32,
height: f32,
width: f32,
depth: usize,
},
}
pub(crate) struct RowExport<T: 'static> {
pub(crate) mode: Option<DragTransferMode>,
#[allow(clippy::type_complexity)]
pub(crate) clone_item_fn: Option<Rc<dyn Fn(&T) -> T>>,
#[allow(clippy::type_complexity)]
pub(crate) export_mime_fn: Option<Rc<dyn Fn(&[T]) -> Vec<(String, Vec<u8>)>>>,
#[allow(clippy::type_complexity)]
pub(crate) on_rows_transferred_out: Option<Rc<dyn Fn(&[usize], &mut EventContext)>>,
pub(crate) accept_foreign_rows: bool,
#[allow(clippy::type_complexity)]
pub(crate) on_rows_received: Option<Rc<dyn Fn(Vec<T>, usize, &mut EventContext)>>,
pub(crate) self_reorder_flag: Rc<Cell<bool>>,
dragged_rows: Rc<RefCell<Vec<usize>>>,
#[allow(clippy::type_complexity)]
removal: Rc<RefCell<Option<Box<dyn Fn()>>>>,
}
impl<T: 'static> Clone for RowExport<T> {
fn clone(&self) -> Self {
Self {
mode: self.mode,
clone_item_fn: self.clone_item_fn.clone(),
export_mime_fn: self.export_mime_fn.clone(),
on_rows_transferred_out: self.on_rows_transferred_out.clone(),
accept_foreign_rows: self.accept_foreign_rows,
on_rows_received: self.on_rows_received.clone(),
self_reorder_flag: self.self_reorder_flag.clone(),
dragged_rows: self.dragged_rows.clone(),
removal: self.removal.clone(),
}
}
}
impl<T: 'static> Default for RowExport<T> {
fn default() -> Self {
Self {
mode: None,
clone_item_fn: None,
export_mime_fn: None,
on_rows_transferred_out: None,
accept_foreign_rows: false,
on_rows_received: None,
self_reorder_flag: Rc::new(Cell::new(false)),
dragged_rows: Rc::new(RefCell::new(Vec::new())),
removal: Rc::new(RefCell::new(None)),
}
}
}
impl<T: 'static> RowExport<T> {
pub(crate) fn set_exportable(&mut self, mode: DragTransferMode)
where
T: Clone,
{
self.mode = Some(mode);
if self.clone_item_fn.is_none() {
self.clone_item_fn = Some(Rc::new(|t: &T| t.clone()));
}
}
pub(crate) fn set_export_external(
&mut self,
f: impl Fn(&[T]) -> Vec<(String, Vec<u8>)> + 'static,
) where
T: Clone,
{
if self.clone_item_fn.is_none() {
self.clone_item_fn = Some(Rc::new(|t: &T| t.clone()));
}
if self.mode.is_none() {
self.mode = Some(DragTransferMode::default());
}
self.export_mime_fn = Some(Rc::new(f));
}
pub(crate) fn set_on_rows_transferred_out(
&mut self,
f: impl Fn(&[usize], &mut EventContext) + 'static,
) {
self.on_rows_transferred_out = Some(Rc::new(f));
}
pub(crate) fn set_on_rows_received(
&mut self,
f: impl Fn(Vec<T>, usize, &mut EventContext) + 'static,
) {
self.on_rows_received = Some(Rc::new(f));
}
pub(crate) fn is_drag_source(&self, reorderable: bool) -> bool {
reorderable || self.mode.is_some()
}
pub(crate) fn is_drop_target(&self, reorderable: bool) -> bool {
reorderable || self.accept_foreign_rows
}
pub(crate) fn build_payload(
&self,
source: ViewId,
mut rows: Vec<usize>,
read: &dyn Fn(usize, &mut dyn FnMut(&T)) -> bool,
snapshot_out: &SnapshotOutFn,
) -> Option<DragPayload> {
let items: Option<Vec<T>> = if let Some(cf) = self.clone_item_fn.as_ref() {
let mut out = Vec::with_capacity(rows.len());
rows.retain(|&r| {
let mut got = None;
read(r, &mut |t| got = Some(cf(t)));
match got {
Some(v) => {
out.push(v);
true
}
None => false,
}
});
Some(out)
} else {
None
};
if rows.is_empty() {
return None;
}
let mime_pairs: Vec<(String, Vec<u8>)> =
match (self.export_mime_fn.as_ref(), items.as_ref()) {
(Some(mf), Some(its)) => mf(its),
_ => Vec::new(),
};
let mut payload = DragPayload::typed(RowDragData::<T> {
source,
rows: rows.clone(),
items,
});
let has_mime = !mime_pairs.is_empty();
for (mime, bytes) in mime_pairs {
payload = payload.with_mime(&mime, bytes);
}
if has_mime {
payload.enrich_external_from_mime();
}
*self.removal.borrow_mut() = Some((snapshot_out)(&rows));
*self.dragged_rows.borrow_mut() = rows;
Some(payload)
}
pub(crate) fn accepts_foreign_export(&self, payload: &DragPayload, source: ViewId) -> bool {
self.accept_foreign_rows
&& self.on_rows_received.is_some()
&& payload
.get_typed::<RowDragData<T>>()
.is_some_and(|rd| rd.source != source && rd.is_export())
}
pub(crate) fn foreign_receive(
&self,
payload: &mut DragPayload,
source: ViewId,
insertion: usize,
ctx: &mut EventContext,
) -> bool {
if self.accepts_foreign_export(payload, source)
&& let Some(cb) = self.on_rows_received.as_ref()
&& let Some(rd) = payload.take_typed::<RowDragData<T>>()
&& let Some(items) = rd.items
{
cb(items, insertion, ctx);
return true;
}
false
}
pub(crate) fn note_self_reorder(&self) {
self.self_reorder_flag.set(true);
}
pub(crate) fn install_completion(&self, handlers: HandlerSet) -> HandlerSet {
let Some(mode) = self.mode else {
return handlers;
};
let flag = self.self_reorder_flag.clone();
let dragged = self.dragged_rows.clone();
let removal = self.removal.clone();
let on_out = self.on_rows_transferred_out.clone();
handlers.on_drag_ended(move |outcome, ctx| {
let handled_by_us = flag.replace(false);
let rows = std::mem::take(&mut *dragged.borrow_mut());
let thunk = removal.borrow_mut().take();
if handled_by_us {
return;
}
let accepted_elsewhere = matches!(
outcome,
DropOutcome::InApp { accepted: true } | DropOutcome::OsMove
);
if mode != DragTransferMode::Move || !accepted_elsewhere || rows.is_empty() {
return;
}
if let Some(cb) = on_out.as_ref() {
let mut desc = rows;
desc.sort_unstable();
desc.reverse();
cb(&desc, ctx);
} else if let Some(thunk) = thunk {
thunk();
}
})
}
}
pub(crate) mod deferred_select {
use std::cell::Cell;
use std::rc::Rc;
use teksilo_core::event::Modifiers;
use teksilo_core::widget::EventContext;
use super::RowSelection;
pub(crate) fn on_down(
sel: &RowSelection,
index: usize,
modifiers: Modifiers,
pending: &Rc<Cell<bool>>,
ctx: &mut EventContext,
) -> bool {
if ctx.press_claimed_by_interactive_child() {
pending.set(false);
return false;
}
if modifiers.command() {
sel.toggle(index);
pending.set(false);
} else if modifiers.shift() {
sel.extend_to(index);
pending.set(false);
} else if sel.is_selected(index) {
pending.set(true);
} else {
sel.select(index);
pending.set(false);
}
true
}
pub(crate) fn on_up(
sel: &RowSelection,
index: usize,
pending: &Rc<Cell<bool>>,
ctx: &mut EventContext,
) {
if ctx.press_claimed_by_interactive_child() {
return;
}
if pending.replace(false) {
sel.select(index);
}
}
}
#[cfg(test)]
mod payload_tests {
use super::*;
fn noop_snapshot() -> SnapshotOutFn {
Rc::new(|_: &[usize]| Box::new(|| {}) as Box<dyn Fn()>)
}
#[test]
fn an_all_unresident_selection_refuses_the_drag() {
let mut export = RowExport::<u64>::default();
export.set_exportable(DragTransferMode::Move);
let read = |_: usize, _: &mut dyn FnMut(&u64)| false;
let payload = export.build_payload(
ViewId::next(ViewKind::List),
vec![0, 1, 2],
&read,
&noop_snapshot(),
);
assert!(payload.is_none());
}
#[test]
fn a_partially_resident_selection_carries_only_the_resident_rows() {
let mut export = RowExport::<u64>::default();
export.set_exportable(DragTransferMode::Copy);
let read = |i: usize, f: &mut dyn FnMut(&u64)| {
if i == 1 {
return false;
}
f(&(i as u64 * 10));
true
};
let payload = export
.build_payload(
ViewId::next(ViewKind::List),
vec![0, 1, 2],
&read,
&noop_snapshot(),
)
.expect("two rows are resident");
let rd = payload.get_typed::<RowDragData<u64>>().unwrap();
assert_eq!(rd.rows, vec![0, 2]);
assert_eq!(rd.items.as_deref(), Some(&[0, 20][..]));
}
}