use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use teksilo_i18n::lit;
use teksilo_canvas::{Canvas, Path, Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::drag_payload::DragPayload;
use teksilo_core::event::{EventResponse, PointerButton, WidgetEvent};
use teksilo_core::signal::Signal;
use teksilo_core::styles::{
SharedTableStyle, SortDirection as StyleSortDirection, TableHeaderCellConfig,
};
use teksilo_core::widget::{
CursorIcon, EventContext, LayoutContext, PaintContext, Widget, WidgetPlacement,
};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_data::SortDirection;
use teksilo_tokens::{BorderRole, SurfaceRole, TextRole, TextStyleRole};
fn style_sort(d: SortDirection) -> StyleSortDirection {
match d {
SortDirection::Ascending => StyleSortDirection::Ascending,
SortDirection::Descending => StyleSortDirection::Descending,
}
}
use crate::primitives::{HStack, Padding, Spacer, TextWidget};
use super::ColumnReorderDragData;
use super::PaneBoundaries;
use super::body::{RowBand, SharedColumnWidths};
use super::column::{ColumnResizePolicy, PinnedSide};
use super::filter::FilterIndicator;
use super::filter::FilterPopoverContent;
use super::layout::{band_rects, insertion_slot_at_x};
use crate::overlay_trigger::OverlayTrigger;
use crate::popover_widget::PopoverWidget;
use teksilo_core::overlay::OverlayPlacement;
const DRAG_REORDER_THRESHOLD: f32 = 5.0;
#[derive(Debug, Clone)]
pub(crate) struct ColumnResizeInfo {
pub id: String,
pub min_width: f32,
pub max_width: Option<f32>,
pub resizable: bool,
}
pub(crate) type ColumnResizeTable = Rc<Vec<ColumnResizeInfo>>;
fn pane_of(slot: usize, b: PaneBoundaries) -> u8 {
if slot < b.leading_count {
0
} else if slot < b.middle_end {
1
} else {
2
}
}
fn draw_band_separators(
canvas: &mut Canvas,
rect: Rect,
slice: &[f32],
scroll: f32,
rtl: bool,
color: teksilo_tokens::Color,
line_w: f32,
) {
if slice.len() < 2 || rect.width <= 0.0 {
return;
}
let mut emit = |x: f32| {
if x >= rect.x && x + line_w <= rect.right() {
canvas.fill_rect(Rect::new(x, rect.y, line_w, rect.height), color);
}
};
if rtl {
let mut x = rect.right() + scroll;
for &w in &slice[..slice.len() - 1] {
x -= w;
emit(x);
}
} else {
let mut x = rect.x - scroll;
for &w in &slice[..slice.len() - 1] {
x += w;
emit(x - line_w);
}
}
}
fn clamp_width(w: f32, min: f32, max: Option<f32>) -> f32 {
w.max(min).min(max.unwrap_or(f32::INFINITY))
}
#[derive(Debug, Clone)]
pub(crate) struct ResizeState {
pub col_id: String,
pub anchor_index: usize,
pub start_pointer_x: f32,
pub start_width: f32,
pub start_divider_x: f32,
pub min_width: f32,
pub max_width: Option<f32>,
}
pub(crate) type ResizeStateHandle = Rc<RefCell<Option<ResizeState>>>;
#[derive(Debug, Clone, Copy)]
struct PressState {
pointer_x: f32,
pointer_y: f32,
}
pub(crate) struct HeaderCellSpec {
pub col_id: String,
pub label: String,
pub col_index_1based: usize,
pub sortable: bool,
pub reorderable: bool,
pub filterable: bool,
pub resize_grip: f32,
pub filter_zone_width: f32,
pub current_sort: Option<SortDirection>,
pub width_index: usize,
pub pane_boundaries: PaneBoundaries,
pub resize_columns: ColumnResizeTable,
pub resize_policy: ColumnResizePolicy,
pub resize_state: ResizeStateHandle,
pub resize_target: Signal<Option<usize>>,
pub resize_preview_x: Signal<Option<f32>>,
pub table_id: usize,
pub sort_signal: Signal<Option<(String, SortDirection)>>,
pub column_widths_signal: Signal<HashMap<String, f32>>,
pub column_widths: SharedColumnWidths,
pub filters_signal: Signal<HashMap<String, String>>,
}
pub(crate) struct HeaderCell {
col_id: String,
label: String,
col_index_1based: usize,
sortable: bool,
reorderable: bool,
resize_grip: f32,
current_sort: Option<SortDirection>,
sort_signal: Signal<Option<(String, SortDirection)>>,
column_widths_signal: Signal<HashMap<String, f32>>,
column_widths: SharedColumnWidths,
width_index: usize,
pane_boundaries: PaneBoundaries,
resize_columns: ColumnResizeTable,
resize_policy: ColumnResizePolicy,
resize_state: ResizeStateHandle,
resize_target: Signal<Option<usize>>,
resize_preview_x: Signal<Option<f32>>,
table_id: usize,
cell_window_x: Rc<Cell<f32>>,
cell_window_w: Rc<Cell<f32>>,
cell_window_h: Rc<Cell<f32>>,
filterable: bool,
filter_zone_width: f32,
filters_signal: Signal<HashMap<String, String>>,
is_hovered: Signal<bool>,
is_resizing: Signal<bool>,
root_child_id: Option<WidgetId>,
}
impl HeaderCell {
pub(crate) fn new(spec: HeaderCellSpec) -> Self {
let width_index = spec.width_index;
let is_resizing = spec.resize_target.map(move |t| *t == Some(width_index));
Self {
col_id: spec.col_id,
label: spec.label,
col_index_1based: spec.col_index_1based,
sortable: spec.sortable,
reorderable: spec.reorderable,
resize_grip: spec.resize_grip,
current_sort: spec.current_sort,
sort_signal: spec.sort_signal,
column_widths_signal: spec.column_widths_signal,
column_widths: spec.column_widths,
width_index,
pane_boundaries: spec.pane_boundaries,
resize_columns: spec.resize_columns,
resize_policy: spec.resize_policy,
resize_state: spec.resize_state,
resize_target: spec.resize_target,
resize_preview_x: spec.resize_preview_x,
table_id: spec.table_id,
cell_window_x: Rc::new(Cell::new(0.0)),
cell_window_w: Rc::new(Cell::new(0.0)),
cell_window_h: Rc::new(Cell::new(0.0)),
filterable: spec.filterable,
filter_zone_width: if spec.filterable {
spec.filter_zone_width
} else {
0.0
},
filters_signal: spec.filters_signal,
is_hovered: Signal::new(false),
is_resizing,
root_child_id: None,
}
}
}
impl std::fmt::Debug for HeaderCell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HeaderCell")
.field("col_id", &self.col_id)
.field("label", &self.label)
.field("sortable", &self.sortable)
.field(
"resizable",
&self
.resize_columns
.get(self.width_index)
.map(|c| c.resizable)
.unwrap_or(false),
)
.field("current_sort", &self.current_sort)
.finish()
}
}
impl Widget for HeaderCell {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
use crate::styles::recipe_table_style as cp;
let label_id = ctx.add(
TextWidget::new(lit!(self.label.clone()))
.style(TextStyleRole::Body)
.color(ColorProp::from(TextRole::Primary))
.single_line()
.a11y_hidden(),
);
let mut row = HStack::new()
.spacing(4.0)
.add_child(label_id)
.add_child(ctx.add(Spacer::new()));
if self.current_sort.is_some() {
let chevron = ctx.add(SortIndicator::new(
self.current_sort,
cp::SORT_INDICATOR_SIZE,
));
row = row.add_child(chevron);
}
if self.filterable {
let filters_signal = self.filters_signal.clone();
let col_id = self.col_id.clone();
let initial = self
.filters_signal
.get()
.get(&self.col_id)
.cloned()
.unwrap_or_default();
let active = !initial.is_empty();
let glyph = FilterIndicator::new(cp::FILTER_INDICATOR_SIZE, active);
let on_change = {
let filters_signal = filters_signal.clone();
let col_id = col_id.clone();
move |s: &str| {
let mut m = filters_signal.get();
if s.is_empty() {
m.remove(&col_id);
} else {
m.insert(col_id.clone(), s.to_string());
}
filters_signal.set(m);
}
};
let popover = PopoverWidget::new(
OverlayTrigger::around(glyph).named(lit!("Filter").resolve_now()),
)
.content(FilterPopoverContent::new(initial).on_change(on_change))
.placement(OverlayPlacement::BelowPreferred)
.show_disclosure_caret(false);
let popover_id = ctx.add(popover);
row = row.add_child(popover_id);
}
let row_id = ctx.add(row);
let padded = ctx.add(
Padding::symmetric(cp::CELL_PADDING_VERTICAL, cp::CELL_PADDING_HORIZONTAL)
.child_id(row_id),
);
let style: SharedTableStyle = ctx
.theme()
.style_slots
.table
.clone()
.unwrap_or_else(|| Rc::new(crate::styles::RecipeTableStyle::default()));
let cell_cfg = TableHeaderCellConfig {
label: padded,
sort: self.current_sort.map(style_sort),
is_hovered: self.is_hovered.clone(),
is_resizing: self.is_resizing.clone(),
};
let cell_root = style.make_header_cell(&cell_cfg, ctx);
self.root_child_id = Some(cell_root);
let sort_signal = self.sort_signal.clone();
let widths_signal = self.column_widths_signal.clone();
let widths_handle = self.column_widths.clone();
let resize_state = self.resize_state.clone();
let resize_target = self.resize_target.clone();
let resize_preview_x = self.resize_preview_x.clone();
let resize_columns = self.resize_columns.clone();
let boundaries = self.pane_boundaries;
let col_id = self.col_id.clone();
let sortable = self.sortable;
let reorderable = self.reorderable;
let grip_base = self.resize_grip;
let policy = self.resize_policy;
let width_index = self.width_index;
let table_id = self.table_id;
let press_state: Rc<Cell<Option<PressState>>> = Rc::new(Cell::new(None));
let self_id = ctx.self_id();
let cell_window_x = self.cell_window_x.clone();
let cell_window_w = self.cell_window_w.clone();
let cell_window_h = self.cell_window_h.clone();
let filter_zone_w = self.filter_zone_width;
let is_hovered = self.is_hovered.clone();
let handlers = HandlerSet::new()
.on_hover({
let is_hovered = is_hovered.clone();
move |entered, _ctx| {
is_hovered.set(entered);
}
})
.on_pointer_event(move |event, ctx: &mut EventContext| {
let cell_x0 = cell_window_x.get();
let cell_h = cell_window_h.get();
let cell_w = {
let placed = cell_window_w.get();
if placed > 0.0 {
placed
} else {
widths_handle
.borrow()
.get(width_index)
.copied()
.unwrap_or(0.0)
}
};
let rtl = ctx.is_rtl();
let target_at = |local_x: f32| -> Option<usize> {
if cell_w <= 0.0 {
return None;
}
let grip = grip_base.min(cell_w * 0.25);
if grip <= 0.0 {
return None;
}
let near_physical_leading = local_x <= grip;
let near_physical_trailing = local_x >= cell_w - grip;
let (on_own_edge, on_predecessor_edge) = if rtl {
(near_physical_leading, near_physical_trailing)
} else {
(near_physical_trailing, near_physical_leading)
};
if on_own_edge && resize_columns.get(width_index).is_some_and(|c| c.resizable) {
return Some(width_index);
}
if on_predecessor_edge && width_index > 0 {
let prev = width_index - 1;
if pane_of(prev, boundaries) == pane_of(width_index, boundaries)
&& resize_columns.get(prev).is_some_and(|c| c.resizable)
{
return Some(prev);
}
}
None
};
match event {
WidgetEvent::PointerMove { position } => {
let local_x = position.x;
let active = resize_state.borrow().clone();
if let Some(state) = active
&& state.anchor_index == width_index
{
let delta = position.x + cell_x0 - state.start_pointer_x;
let signed = if rtl { -delta } else { delta };
let new_w = clamp_width(
state.start_width + signed,
state.min_width,
state.max_width,
);
match policy {
ColumnResizePolicy::Live => {
write_width(&widths_signal, &state.col_id, new_w);
}
ColumnResizePolicy::OnRelease => {
let dir = if rtl { -1.0 } else { 1.0 };
resize_preview_x.set(Some(
state.start_divider_x + (new_w - state.start_width) * dir,
));
}
}
ctx.set_cursor(CursorIcon::ColResize);
return EventResponse::Handled;
}
if reorderable && let Some(p) = press_state.get() {
let dx = local_x - p.pointer_x;
let dy = position.y - p.pointer_y;
if (dx * dx + dy * dy).sqrt() > DRAG_REORDER_THRESHOLD {
press_state.set(None);
let payload = DragPayload::typed(ColumnReorderDragData {
col_id: col_id.clone(),
source_table_id: table_id,
});
ctx.start_drag(self_id, payload);
return EventResponse::Handled;
}
}
let in_cell_y =
cell_h <= 0.0 || (position.y >= 0.0 && position.y <= cell_h);
if in_cell_y {
if target_at(local_x).is_some() {
ctx.set_cursor(CursorIcon::ColResize);
return EventResponse::Handled;
}
ctx.set_cursor(CursorIcon::Default);
}
EventResponse::Ignored
}
WidgetEvent::PointerDown {
position,
button: PointerButton::Primary,
..
} => {
if cell_h > 0.0 && (position.y < 0.0 || position.y > cell_h) {
return EventResponse::Ignored;
}
let local_x = position.x;
if let Some(target) = target_at(local_x) {
let info = &resize_columns[target];
let start_width =
widths_handle.borrow().get(target).copied().unwrap_or(0.0);
let resizing_self = target == width_index;
let own_edge_is_physical_leading = rtl;
let on_physical_leading = if resizing_self {
own_edge_is_physical_leading
} else {
!own_edge_is_physical_leading
};
let start_divider_x = if on_physical_leading {
cell_x0
} else {
cell_x0 + cell_w
};
*resize_state.borrow_mut() = Some(ResizeState {
col_id: info.id.clone(),
anchor_index: width_index,
start_pointer_x: position.x + cell_x0,
start_width,
start_divider_x,
min_width: info.min_width,
max_width: info.max_width,
});
resize_target.set(Some(target));
ctx.set_cursor(CursorIcon::ColResize);
ctx.capture_pointer();
return EventResponse::Handled;
}
let in_filter_zone = if rtl {
local_x < grip_base + filter_zone_w
} else {
local_x > cell_w - grip_base - filter_zone_w
};
if filter_zone_w > 0.0 && cell_w > 0.0 && in_filter_zone {
return EventResponse::Ignored;
}
press_state.set(Some(PressState {
pointer_x: local_x,
pointer_y: position.y,
}));
EventResponse::Handled
}
WidgetEvent::PointerUp { position, .. } => {
let taken = resize_state.borrow_mut().take();
if let Some(state) = taken {
if state.anchor_index == width_index {
if policy == ColumnResizePolicy::OnRelease {
let delta = position.x + cell_x0 - state.start_pointer_x;
let signed = if rtl { -delta } else { delta };
let new_w = clamp_width(
state.start_width + signed,
state.min_width,
state.max_width,
);
write_width(&widths_signal, &state.col_id, new_w);
}
resize_target.set(None);
resize_preview_x.set(None);
ctx.release_pointer();
return EventResponse::Handled;
}
resize_target.set(None);
resize_preview_x.set(None);
}
if press_state.replace(None).is_some() && sortable {
let next = match sort_signal.get() {
None => Some((col_id.clone(), SortDirection::Ascending)),
Some((id, SortDirection::Ascending)) if id == col_id => {
Some((col_id.clone(), SortDirection::Descending))
}
Some((id, SortDirection::Descending)) if id == col_id => None,
Some(_) => Some((col_id.clone(), SortDirection::Ascending)),
};
sort_signal.set(next);
return EventResponse::Handled;
}
EventResponse::Ignored
}
_ => EventResponse::Ignored,
}
})
.on_access_action({
let resize_columns = self.resize_columns.clone();
let widths_handle = self.column_widths.clone();
let widths_signal = self.column_widths_signal.clone();
move |action, _ctx| {
use teksilo_core::accesskit::Action;
if !matches!(action, Action::Increment | Action::Decrement) {
return EventResponse::Ignored;
}
let Some(info) = resize_columns.get(width_index) else {
return EventResponse::Ignored;
};
if !info.resizable {
return EventResponse::Ignored;
}
let current = widths_handle
.borrow()
.get(width_index)
.copied()
.unwrap_or(0.0);
if current <= 0.0 {
return EventResponse::Ignored;
}
let step = if matches!(action, Action::Increment) {
crate::styles::recipe_table_style::COLUMN_RESIZE_STEP
} else {
-crate::styles::recipe_table_style::COLUMN_RESIZE_STEP
};
let next = clamp_width(current + step, info.min_width, info.max_width);
write_width(&widths_signal, &info.id, next);
EventResponse::Handled
}
})
.cursor(CursorIcon::Default)
.focusable(false);
ctx.apply_self_handlers(handlers);
vec![cell_root]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match self.root_child_id {
Some(id) => ctx
.child_size(id, proposal)
.unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
None => proposal.resolve(0.0, 0.0),
}
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
self.cell_window_x.set(bounds.x);
self.cell_window_w.set(bounds.width);
self.cell_window_h.set(bounds.height);
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::ColumnHeader);
builder.set_name(self.label.clone());
if self
.resize_columns
.get(self.width_index)
.is_some_and(|c| c.resizable)
{
builder.add_action(teksilo_core::accesskit::Action::Increment);
builder.add_action(teksilo_core::accesskit::Action::Decrement);
}
let n = builder.inner_mut();
n.set_column_index(self.col_index_1based);
if let Some(dir) = self.current_sort {
let ak_dir = match dir {
SortDirection::Ascending => teksilo_core::accesskit::SortDirection::Ascending,
SortDirection::Descending => teksilo_core::accesskit::SortDirection::Descending,
};
n.set_sort_direction(ak_dir);
}
}
fn children(&self) -> Vec<WidgetId> {
self.root_child_id.into_iter().collect()
}
}
fn write_width(signal: &Signal<HashMap<String, f32>>, col_id: &str, new_w: f32) {
let mut m = signal.get();
m.insert(col_id.to_string(), new_w);
signal.set(m);
}
#[derive(Debug)]
struct SortIndicator {
direction: Option<SortDirection>,
size: f32,
}
impl SortIndicator {
fn new(direction: Option<SortDirection>, size: f32) -> Self {
Self { direction, size }
}
}
impl Widget for SortIndicator {
fn layout_response(
&self,
_proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(self.size, self.size).into()
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let Some(dir) = self.direction else {
return;
};
let color = TextRole::Accent.resolve(&ctx.theme.colors);
let cx = bounds.x + bounds.width / 2.0;
let pad = bounds.height * 0.15;
let top_y = bounds.y + pad;
let bot_y = bounds.y + bounds.height - pad;
let half_w = bounds.width / 2.0 - pad;
let mut path = Path::new();
match dir {
SortDirection::Ascending => {
path.move_to(Point::new(cx, top_y));
path.line_to(Point::new(cx + half_w, bot_y));
path.line_to(Point::new(cx - half_w, bot_y));
path.close();
}
SortDirection::Descending => {
path.move_to(Point::new(cx - half_w, top_y));
path.line_to(Point::new(cx + half_w, top_y));
path.line_to(Point::new(cx, bot_y));
path.close();
}
}
canvas.fill_path(&path, color);
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_hidden();
}
}
#[derive(Debug)]
pub(crate) struct HeaderRow {
cells: Vec<WidgetId>,
widths: SharedColumnWidths,
divider_width: f32,
pane_boundaries: PaneBoundaries,
scroll_x: Signal<f32>,
bands: Option<[Option<WidgetId>; 3]>,
}
impl HeaderRow {
pub(crate) fn new(
cells: Vec<WidgetId>,
widths: SharedColumnWidths,
divider_width: f32,
pane_boundaries: PaneBoundaries,
scroll_x: Signal<f32>,
) -> Self {
Self {
cells,
widths,
divider_width,
pane_boundaries,
scroll_x,
bands: None,
}
}
fn has_pinning(&self) -> bool {
self.pane_boundaries.leading_count > 0 || self.pane_boundaries.middle_end < self.cells.len()
}
}
impl Widget for HeaderRow {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if !self.has_pinning() {
return Vec::new();
}
let b = self.pane_boundaries;
let leading_end = b.leading_count.min(self.cells.len());
let middle_end = b.middle_end.min(self.cells.len()).max(leading_end);
let leading: Vec<WidgetId> = self.cells[..leading_end].to_vec();
let middle: Vec<WidgetId> = self.cells[leading_end..middle_end].to_vec();
let trailing: Vec<WidgetId> = self.cells[middle_end..].to_vec();
let mut bands: [Option<WidgetId>; 3] = [None, None, None];
if !leading.is_empty() {
bands[0] = Some(ctx.add(RowBand::new(leading, self.widths.clone(), 0)));
}
if !middle.is_empty() {
bands[1] = Some(
ctx.add(
RowBand::new(middle, self.widths.clone(), leading_end)
.scrollable(self.scroll_x.clone()),
),
);
}
if !trailing.is_empty() {
bands[2] = Some(ctx.add(RowBand::new(trailing, self.widths.clone(), middle_end)));
}
let out: Vec<WidgetId> = bands.iter().copied().flatten().collect();
self.bands = Some(bands);
out
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let width = proposal
.width
.unwrap_or_else(|| self.widths.borrow().iter().sum());
let height = proposal.height.unwrap_or(32.0);
Size::new(width, height).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
if let Some(bands) = self.bands {
let widths = self.widths.borrow();
let rtl = ctx.is_rtl();
let (leading_rect, middle_rect, trailing_rect) =
band_rects(bounds, &widths, self.pane_boundaries, rtl);
let rects = [leading_rect, middle_rect, trailing_rect];
let mut next = 0;
for (band, rect) in bands.iter().zip(rects.iter()) {
if band.is_some() {
if let Some(child) = children.get_mut(next) {
child.origin = rect.origin();
child.size = rect.size();
}
next += 1;
}
}
return;
}
let widths = self.widths.borrow();
let total_children = children.len();
let fallback_w = if total_children == 0 {
0.0
} else {
bounds.width / total_children as f32
};
let scroll = self.scroll_x.get();
if ctx.is_rtl() {
let mut x = bounds.right() + scroll;
for (i, child) in children.iter_mut().enumerate() {
let w = widths.get(i).copied().unwrap_or(fallback_w);
x -= w;
child.origin = Point::new(x, bounds.y);
child.size = Size::new(w, bounds.height);
}
} else {
let mut x = bounds.x - scroll;
for (i, child) in children.iter_mut().enumerate() {
let w = widths.get(i).copied().unwrap_or(fallback_w);
child.origin = Point::new(x, bounds.y);
child.size = Size::new(w, bounds.height);
x += w;
}
}
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let bg = SurfaceRole::Raised.resolve(&ctx.theme.colors);
canvas.fill_rect(bounds, bg);
let line = BorderRole::DividerStrong.resolve(&ctx.theme.colors);
let dw = self.divider_width.max(1.0);
canvas.fill_rect(
Rect::new(bounds.x, bounds.y + bounds.height - dw, bounds.width, dw),
line,
);
let widths = self.widths.borrow();
if widths.len() > 1 {
let rtl =
ctx.layout_direction == teksilo_core::environment::LayoutDirection::RightToLeft;
let sep = BorderRole::Divider.resolve(&ctx.theme.colors);
let (leading_rect, middle_rect, trailing_rect) =
band_rects(bounds, &widths, self.pane_boundaries, rtl);
let b = self.pane_boundaries;
let leading_end = b.leading_count.min(widths.len());
let middle_end = b.middle_end.min(widths.len()).max(leading_end);
draw_band_separators(
canvas,
leading_rect,
&widths[..leading_end],
0.0,
rtl,
sep,
dw,
);
draw_band_separators(
canvas,
middle_rect,
&widths[leading_end..middle_end],
self.scroll_x.get(),
rtl,
sep,
dw,
);
draw_band_separators(
canvas,
trailing_rect,
&widths[middle_end..],
0.0,
rtl,
sep,
dw,
);
let mut seam = |x: f32| {
canvas.fill_rect(Rect::new(x, bounds.y, dw, bounds.height), sep);
};
if leading_end > 0 {
seam(if rtl {
leading_rect.x
} else {
leading_rect.right() - dw
});
}
if middle_end < widths.len() {
seam(if rtl {
trailing_rect.right() - dw
} else {
trailing_rect.x
});
}
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::Row);
builder.inner_mut().set_row_index(1);
}
fn children(&self) -> Vec<WidgetId> {
match self.bands {
Some(bands) => bands.iter().copied().flatten().collect(),
None => self.cells.clone(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn attach_header_reorder_handlers(
ctx: &mut BuildContext,
header_row_id: WidgetId,
source_table_id: usize,
column_widths: Rc<RefCell<Vec<f32>>>,
display_indices: Rc<RefCell<Vec<usize>>>,
pane_boundaries: Rc<RefCell<PaneBoundaries>>,
column_order_signal: Signal<Vec<String>>,
column_pinning_signal: Signal<HashMap<String, PinnedSide>>,
column_ids: Vec<String>,
header_strip_width: Rc<Cell<f32>>,
scroll_x: Signal<f32>,
) {
let widths_for_drop = column_widths.clone();
let display_for_drop = display_indices.clone();
let panes_for_drop = pane_boundaries.clone();
let order_for_drop = column_order_signal.clone();
let pinning_for_drop = column_pinning_signal.clone();
let ids_for_drop = column_ids;
let strip_width_for_drop = header_strip_width;
let scroll_x_for_drop = scroll_x;
ctx.apply_handlers(
header_row_id,
HandlerSet::new()
.on_drag_hover(|payload, _position, _ctx| {
if payload.has_typed::<ColumnReorderDragData>() {
teksilo_core::DropFeedback::HighlightRect {
rect: teksilo_canvas::Rect::ZERO,
color: teksilo_tokens::Color::TRANSPARENT,
}
} else {
teksilo_core::DropFeedback::NoFeedback
}
})
.on_drop(move |mut payload, position, ctx| {
let drag = match payload.take_typed::<ColumnReorderDragData>() {
Some(d) => d,
None => return false,
};
if drag.source_table_id != source_table_id {
return false;
}
let widths = widths_for_drop.borrow().clone();
let display = display_for_drop.borrow().clone();
let panes = *panes_for_drop.borrow();
let total = display.len();
if total == 0 {
return false;
}
let drop_x = if ctx.is_rtl() {
strip_width_for_drop.get() - position.x
} else {
position.x
};
let insertion_display_idx = insertion_slot_at_x(
&widths,
panes,
scroll_x_for_drop.get(),
strip_width_for_drop.get(),
drop_x,
);
let new_pinning = if insertion_display_idx <= panes.leading_count {
PinnedSide::Leading
} else if insertion_display_idx >= panes.middle_end {
PinnedSide::Trailing
} else {
PinnedSide::None
};
let mut pin_map = pinning_for_drop.get();
match new_pinning {
PinnedSide::None => {
pin_map.remove(&drag.col_id);
}
other => {
pin_map.insert(drag.col_id.clone(), other);
}
}
pinning_for_drop.set(pin_map);
let mut new_order: Vec<String> =
display.iter().map(|&i| ids_for_drop[i].clone()).collect();
let from_pos = new_order.iter().position(|id| id == &drag.col_id);
if let Some(from) = from_pos {
let item = new_order.remove(from);
let to = if from < insertion_display_idx {
insertion_display_idx.saturating_sub(1)
} else {
insertion_display_idx
};
let to = to.min(new_order.len());
new_order.insert(to, item);
order_for_drop.set(new_order);
}
true
}),
);
}