use std::cell::{Cell, RefCell};
use std::rc::Rc;
use teksilo_canvas::{Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_data::{DragEligibility, RowState};
use super::BUFFER_ITEMS;
use crate::common::row_metrics::SharedRowMetrics;
use crate::data_views::{RowSelection, ViewId, default_placeholder};
use crate::list_source::ListSource;
pub(crate) struct ListBodyPane<T: 'static> {
pub(crate) source: ListSource<T>,
pub(crate) delegate: Rc<dyn Fn(usize, &T, bool) -> Box<dyn Widget>>,
pub(crate) row_tooltips: crate::data_views::RowTooltips<T>,
pub(crate) metrics: SharedRowMetrics,
pub(crate) row_selection: Option<RowSelection>,
pub(crate) focused_index: Rc<Cell<Option<usize>>>,
pub(crate) reorderable: bool,
pub(crate) export: crate::data_views::RowExport<T>,
pub(crate) on_activate: Option<Rc<dyn Fn(usize, &mut teksilo_core::widget::EventContext)>>,
pub(crate) activate_on: crate::data_views::ActivateOn,
pub(crate) model_id: ViewId,
pub(crate) root_id: WidgetId,
pub(crate) scroll_y: Signal<f32>,
pub(crate) viewport_height: Rc<Cell<f32>>,
pub(crate) placed_content_width: Rc<Cell<f32>>,
pub(crate) version: Signal<u64>,
pub(crate) total_refresh: Signal<u64>,
pub(crate) prev_built_start: Rc<Cell<usize>>,
pub(crate) prev_built_end: Rc<Cell<usize>>,
pub(crate) item_entries: Vec<(usize, WidgetId)>,
pub(crate) row_map: Rc<RefCell<Vec<(usize, WidgetId)>>>,
}
impl<T: 'static> ListBodyPane<T> {
fn visible_range(&self) -> (usize, usize) {
self.metrics.borrow_mut().visible_range(
self.scroll_y.get(),
self.viewport_height.get(),
self.source.len(),
BUFFER_ITEMS,
)
}
}
impl<T: 'static> std::fmt::Debug for ListBodyPane<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListBodyPane")
.field("rows", &self.item_entries.len())
.field("scroll_y", &self.scroll_y.get())
.finish()
}
}
impl<T: 'static> Widget for ListBodyPane<T> {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let version = self.version.clone();
version.bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
self.scroll_y.bind_to(
ctx.self_id(),
ctx.binding_registry(),
BindingLevel::Relayout,
);
let (initial_start, initial_end) = self.visible_range();
self.prev_built_start.set(initial_start);
self.prev_built_end.set(initial_end);
let v_for_scroll = version.clone();
let vp_h = self.viewport_height.clone();
let len_for_scroll = self.source.len_fn.clone();
let scroll_handle = self.scroll_y.observe({
let pbs = self.prev_built_start.clone();
let pbe = self.prev_built_end.clone();
let metrics = self.metrics.clone();
move |y| {
let count = (len_for_scroll)();
let (visible_start, visible_end) =
metrics.borrow_mut().visible_range(*y, vp_h.get(), count, 0);
if visible_start < pbs.get() || visible_end > pbe.get() {
let new_start = visible_start.saturating_sub(BUFFER_ITEMS);
let new_end = (visible_end + BUFFER_ITEMS).min(count);
pbs.set(new_start);
pbe.set(new_end);
v_for_scroll.set(v_for_scroll.get() + 1);
}
}
});
ctx.own_handle(scroll_handle);
if let Some(ref sel) = self.row_selection {
let v = version.clone();
let counter = Rc::new(Cell::new(0_u64));
let handle = sel.observe_for_rebuild(move || {
counter.set(counter.get() + 1);
v.set(counter.get());
});
ctx.own_handle(handle);
}
let (start, end) = self.visible_range();
self.item_entries.clear();
(self.source.dnd.request_window_fn)(start..end);
if (self.source.dnd.can_fetch_more_fn)() && end + BUFFER_ITEMS >= self.source.len() {
(self.source.dnd.fetch_more_fn)();
}
let selection = &self.row_selection;
let is_drag_source = self.export.is_drag_source(self.reorderable);
let model_id = self.model_id;
let root_id = self.root_id;
let row_state_fn = self.source.dnd.row_state_fn.clone();
ctx.begin_view_focus_for(root_id);
for i in start..end {
let selected = selection
.as_ref()
.map(|s| s.is_selected(i))
.unwrap_or(false);
let pending_tip: std::cell::RefCell<Option<crate::data_views::ResolvedRowTooltip>> =
std::cell::RefCell::new(None);
let tips = &self.row_tooltips;
let row_widget = (self.source.with_item_fn)(i, &|item| {
if tips.is_set() {
*pending_tip.borrow_mut() = tips.resolve(i, item);
}
(self.delegate)(i, item, selected)
})
.or_else(|| ((row_state_fn)(i) == RowState::Loading).then(default_placeholder));
if let Some(widget) = row_widget {
let inner_id = ctx.add_boxed(widget);
if let Some(tip) = pending_tip.into_inner() {
self.row_tooltips.attach_resolved(ctx, inner_id, tip);
}
let child_id = ctx.add(crate::list_item_a11y::ListItemWrapper::new(
inner_id, selected,
));
if let Some(ref sel) = self.row_selection {
let sel_click = sel.clone();
let click_anchor = self.source.anchor(i);
let fi_click = self.focused_index.clone();
let pending_collapse = Rc::new(Cell::new(false));
ctx.apply_handlers(
child_id,
HandlerSet::new().on_pointer_event(move |event, ctx| match event {
teksilo_core::event::WidgetEvent::PointerDown {
modifiers,
button: teksilo_core::event::PointerButton::Primary,
..
} => {
let Some(row) = click_anchor.index() else {
return teksilo_core::event::EventResponse::Ignored;
};
if crate::data_views::deferred_select::on_down(
&sel_click,
row,
*modifiers,
&pending_collapse,
ctx,
) {
fi_click.set(Some(row));
}
teksilo_core::event::EventResponse::Ignored
}
teksilo_core::event::WidgetEvent::PointerUp {
button: teksilo_core::event::PointerButton::Primary,
..
} => {
match click_anchor.index() {
Some(row) => crate::data_views::deferred_select::on_up(
&sel_click,
row,
&pending_collapse,
ctx,
),
None => pending_collapse.set(false),
}
teksilo_core::event::EventResponse::Ignored
}
_ => teksilo_core::event::EventResponse::Ignored,
}),
);
}
if let Some(ref cb) = self.on_activate {
let cb = cb.clone();
let a = self.source.anchor(i);
let handlers = match self.activate_on {
crate::data_views::ActivateOn::SingleClick => {
let a = a.clone();
HandlerSet::new().on_tap(move |_tap, ctx| {
if let Some(cur) = a.index() {
cb(cur, ctx)
}
})
}
crate::data_views::ActivateOn::DoubleClick => HandlerSet::new()
.on_double_tap(move |_tap, ctx| {
if let Some(cur) = a.index() {
cb(cur, ctx)
}
}),
};
ctx.apply_handlers(child_id, handlers);
}
{
let sel_a11y = self.row_selection.clone();
let anchor_a11y = self.source.anchor(i);
let fi_a11y = self.focused_index.clone();
let metrics_a11y = self.metrics.clone();
let scroll_a11y = self.scroll_y.clone();
let vh_a11y = self.viewport_height.clone();
let len_a11y = self.source.len_fn.clone();
let activate_a11y = self.on_activate.clone();
ctx.apply_handlers(
child_id,
HandlerSet::new().on_access_action(move |action, ctx| {
use teksilo_core::accesskit::Action;
use teksilo_core::event::EventResponse;
let Some(row) = anchor_a11y.index() else {
return EventResponse::Ignored;
};
match action {
Action::Click => {
if let Some(ref sel) = sel_a11y {
sel.select(row);
}
fi_a11y.set(Some(row));
if let Some(ref cb) = activate_a11y {
cb(row, ctx);
}
EventResponse::Handled
}
Action::ScrollIntoView => {
let viewport = vh_a11y.get();
let scroll = scroll_a11y.get();
let new_scroll = {
let mut m = metrics_a11y.borrow_mut();
let total = m.total_height((len_a11y)());
let max = (total - viewport).max(0.0);
m.scroll_for_ensure_visible(row, scroll, viewport, max)
};
if (new_scroll - scroll).abs() > f32::EPSILON {
scroll_a11y.set(new_scroll);
}
EventResponse::Handled
}
_ => EventResponse::Ignored,
}
}),
);
}
if is_drag_source {
let drag_index = i;
let drag_model_id = model_id;
let drag_self_id = root_id;
let delegate_for_preview = self.delegate.clone();
let with_item_for_preview = self.source.with_item_fn.clone();
let metrics_for_preview = self.metrics.clone();
let width_for_preview = self.placed_content_width.clone();
let drag_gate = self.source.dnd.drag_fn.clone();
let sel_for_drag = self.row_selection.clone();
let export_for_drag = self.export.clone();
let read_for_drag = self.source.read_item_fn.clone();
let snapshot_for_drag = self.source.dnd.snapshot_out_fn.clone();
ctx.apply_handlers(
child_id,
HandlerSet::new().on_drag(move |phase, ctx| {
if let teksilo_core::gesture::DragPhase::Started { .. } = phase {
if (drag_gate)(drag_index) == DragEligibility::NoDrag {
return;
}
let rows: Vec<usize> = match sel_for_drag.as_ref() {
Some(s) if s.is_selected(drag_index) => {
let mut v = s.selected_indices();
v.sort_unstable();
if v.len() <= 1 { vec![drag_index] } else { v }
}
_ => vec![drag_index],
};
let Some(payload) = export_for_drag.build_payload(
drag_model_id,
rows,
&*read_for_drag,
&snapshot_for_drag,
) else {
return;
};
let delegate = delegate_for_preview.clone();
let w = width_for_preview.get().max(120.0);
let h = metrics_for_preview.borrow_mut().row_height(drag_index);
let preview_opt = (with_item_for_preview)(drag_index, &|item| {
Box::new(crate::drag_preview::DragPreview::new(
w,
h,
delegate(drag_index, item, false),
)) as Box<dyn Widget>
});
if let Some(preview) = preview_opt {
ctx.start_drag_with_preview(drag_self_id, payload, preview);
} else {
ctx.start_drag(drag_self_id, payload);
}
}
}),
);
}
self.item_entries.push((i, child_id));
}
}
ctx.end_view_focus();
*self.row_map.borrow_mut() = self.item_entries.clone();
self.item_entries.iter().map(|(_, id)| *id).collect()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
crate::common::viewport::viewport_size(
proposal,
&self.viewport_height,
Size::new(300.0, 200.0),
)
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
crate::common::viewport::record_viewport_height(&self.viewport_height, bounds.height);
let item_count = self.item_entries.len();
if item_count == 0 {
return;
}
let count = self.source.len();
if self.metrics.borrow().needs_measure() {
let pre_total = self.metrics.borrow_mut().total_height(count);
let mut measured = Vec::with_capacity(item_count);
for (idx, child) in children.iter().enumerate() {
if idx < item_count
&& let Some(size) =
ctx.child_size(child.id, SizeProposal::with_width(bounds.width))
{
let (model_index, _) = self.item_entries[idx];
measured.push((model_index, size.height));
}
}
let anchor = self
.metrics
.borrow_mut()
.observe_measured(&measured, self.scroll_y.get());
if anchor.abs() > 0.01 {
self.scroll_y.set((self.scroll_y.get() + anchor).max(0.0));
}
let (vs, ve) = self.metrics.borrow_mut().visible_range(
self.scroll_y.get(),
self.viewport_height.get(),
count,
0,
);
if vs < self.prev_built_start.get() || ve > self.prev_built_end.get() {
self.prev_built_start.set(vs.saturating_sub(BUFFER_ITEMS));
self.prev_built_end.set((ve + BUFFER_ITEMS).min(count));
self.version.set(self.version.get() + 1);
}
let post_total = self.metrics.borrow_mut().total_height(count);
if (post_total - pre_total).abs() > 0.01 {
self.total_refresh.set(self.total_refresh.get() + 1);
}
}
let scroll_y = self.scroll_y.get();
for (idx, child) in children.iter_mut().enumerate() {
if idx < item_count {
let (model_index, _) = self.item_entries[idx];
let (top, height) = {
let mut m = self.metrics.borrow_mut();
(m.row_top(model_index), m.row_height(model_index))
};
let y = bounds.y + top - scroll_y;
child.origin = Point::new(bounds.x, y);
child.size = Size::new(bounds.width, height);
}
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::Group);
}
fn children(&self) -> Vec<WidgetId> {
self.item_entries.iter().map(|(_, id)| *id).collect()
}
fn clips_children(&self) -> bool {
true
}
}