use std::cell::Cell;
use std::collections::{BTreeSet, HashMap};
use std::rc::Rc;
use std::time::{Duration, Instant};
use iced::alignment::Vertical;
use iced::widget::{column, container, keyed_column, mouse_area, row, scrollable, space, text};
use iced::widget::scrollable::{AbsoluteOffset, Direction, Scrollbar, Viewport};
use iced::{Color, Element, Length, Padding, Task};
use oriel_core::columns::{Column, ColumnId, ColumnLayout};
use oriel_core::data::DataSource;
use oriel_core::query::{QuerySpec, SortDirection, SortSpec};
use oriel_core::selection::Selection;
use oriel_core::viewport::{RowVirtualizer, ScrollAnchor};
pub const DEFAULT_ROW_HEIGHT: f32 = 25.0;
pub const DEFAULT_HEADER_HEIGHT: f32 = 28.0;
pub const DEFAULT_FOOTER_HEIGHT: f32 = 28.0;
const DEFAULT_HANDLE_WIDTH: f32 = 6.0;
pub const DEFAULT_CELL_PAD: f32 = 6.0;
const SCROLLBAR_GUTTER: f32 = 10.0;
const DEFAULT_REORDER_ANIMATION: Duration = Duration::from_millis(250);
const FETCH_ROWS: usize = 256;
const FETCH_MARGIN: usize = 32;
const MAX_VISIBLE_ROWS: usize = 128;
const EDGE: usize = 8;
pub type CellElement<Row> = Element<'static, Message<Row>>;
pub type BoxedColumn<Row> = Box<dyn Column<Element = CellElement<Row>, Row = Row>>;
#[derive(Debug)]
pub struct RowContext<'a, Row> {
pub index: usize,
pub row: Option<&'a Row>,
pub is_selected: bool,
pub is_focused: bool,
pub is_hovered: bool,
}
impl<Row> Clone for RowContext<'_, Row> {
fn clone(&self) -> Self {
*self
}
}
impl<Row> Copy for RowContext<'_, Row> {}
pub type RowStyleFn<Row> =
Box<dyn for<'a> Fn(&iced::Theme, &RowContext<'a, Row>) -> iced::widget::container::Style>;
pub type CellStyleFn<Row> = Box<
dyn for<'a> Fn(
&iced::Theme,
&RowContext<'a, Row>,
ColumnId,
) -> iced::widget::container::Style,
>;
pub type HeaderStyleFn = Box<dyn Fn(&iced::Theme) -> iced::widget::container::Style>;
pub type SortIndicatorFn<Row> =
Box<dyn Fn(SortDirection) -> Element<'static, Message<Row>>>;
pub fn default_row_style<Row>(
theme: &iced::Theme,
row: &RowContext<'_, Row>,
) -> iced::widget::container::Style {
let palette = theme.extended_palette();
let background = if row.is_selected {
Some(palette.primary.weak.color.into())
} else if row.is_focused {
Some(Color { a: 0.45, ..palette.background.strong.color }.into())
} else if row.is_hovered {
Some(Color { a: 0.60, ..palette.background.weak.color }.into())
} else if row.index % 2 == 1 {
Some(Color { a: 0.30, ..palette.background.weak.color }.into())
} else {
None
};
iced::widget::container::Style {
background,
..iced::widget::container::Style::default()
}
}
pub fn default_cell_style<Row>(
_theme: &iced::Theme,
_row: &RowContext<'_, Row>,
_column: ColumnId,
) -> iced::widget::container::Style {
iced::widget::container::Style::default()
}
pub fn default_header_style(theme: &iced::Theme) -> iced::widget::container::Style {
iced::widget::container::Style {
background: Some(theme.extended_palette().background.weak.color.into()),
..iced::widget::container::Style::default()
}
}
pub fn default_footer_style(theme: &iced::Theme) -> iced::widget::container::Style {
default_header_style(theme)
}
pub fn default_sort_indicator<Row: 'static>(
direction: SortDirection,
) -> Element<'static, Message<Row>> {
match direction {
SortDirection::Ascending => crate::arrow::SortArrow::up(),
SortDirection::Descending => crate::arrow::SortArrow::down(),
}
.into()
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Message<Row> {
BodyScrolled(AbsoluteOffset),
Fetched {
generation: u64,
start: usize,
rows: Vec<Row>,
},
RowPressed {
index: usize,
},
RowHovered {
index: usize,
},
RowUnhovered {
index: usize,
},
ColumnResizing {
index: usize,
delta: f32,
},
ColumnResized {
index: usize,
},
ColumnAutoSizeRequested {
index: usize,
},
ScrollbarDragged(f32),
ColumnMeasured {
index: usize,
width: f32,
},
HeaderPressed {
index: usize,
},
}
pub struct VirtualGrid<S: DataSource> {
store: S,
columns: Vec<BoxedColumn<S::Row>>,
layout: ColumnLayout,
virt: RowVirtualizer,
spec: QuerySpec,
selection: Selection<S::RowId>,
focus: Option<S::RowId>,
focus_restoration: bool,
total_rows: usize,
auto_size: BTreeSet<usize>,
hovered: Option<usize>,
scroll_offset: f32,
last_scroll_report: f32,
recession: f32,
window_generation: u64,
auto_size_epoch: u64,
follow: bool,
viewport_height: Cell<f32>,
fetch_generation: u64,
window_start: usize,
window_rows: Vec<S::Row>,
header_id: iced::widget::Id,
body_id: iced::widget::Id,
footer_id: iced::widget::Id,
has_footer: bool,
header_height: f32,
footer_height: f32,
cell_padding: f32,
handle_width: f32,
row_style: RowStyleFn<S::Row>,
cell_style: CellStyleFn<S::Row>,
header_style: HeaderStyleFn,
footer_style: HeaderStyleFn,
sort_indicator: SortIndicatorFn<S::Row>,
row_divider: Option<Color>,
column_divider: Option<Color>,
rule_color: Option<Color>,
reorder_animation: Duration,
flights: HashMap<S::RowId, crate::slide::Flight>,
fades: HashMap<S::RowId, crate::slide::Flight>,
reorder_easing: crate::slide::Easing,
pending_flights: Option<(u64, Vec<(S::RowId, f32)>)>,
}
impl<S: DataSource> VirtualGrid<S> {
pub fn new(store: S, columns: Vec<BoxedColumn<S::Row>>) -> (Self, Task<Message<S::Row>>) {
let total_rows = store.row_count().lower_bound();
let layout = ColumnLayout::new(columns.iter().map(|c| (c.id(), c.sizing())));
let has_footer = columns.iter().any(|c| c.footer().is_some());
let mut grid = Self {
store,
columns,
layout,
virt: RowVirtualizer::new(DEFAULT_ROW_HEIGHT),
spec: QuerySpec::default(),
selection: Selection::new(),
focus: None,
focus_restoration: false,
total_rows,
auto_size: BTreeSet::new(),
hovered: None,
scroll_offset: 0.0,
last_scroll_report: 0.0,
recession: 0.0,
window_generation: 0,
auto_size_epoch: 0,
follow: false,
viewport_height: Cell::new(0.0),
fetch_generation: 0,
window_start: 0,
window_rows: Vec::new(),
header_id: iced::widget::Id::unique(),
body_id: iced::widget::Id::unique(),
footer_id: iced::widget::Id::unique(),
has_footer,
header_height: DEFAULT_HEADER_HEIGHT,
footer_height: DEFAULT_FOOTER_HEIGHT,
cell_padding: DEFAULT_CELL_PAD,
handle_width: DEFAULT_HANDLE_WIDTH,
row_style: Box::new(default_row_style),
cell_style: Box::new(default_cell_style),
header_style: Box::new(default_header_style),
footer_style: Box::new(default_footer_style),
sort_indicator: Box::new(default_sort_indicator),
row_divider: None,
column_divider: None,
rule_color: None,
reorder_animation: DEFAULT_REORDER_ANIMATION,
flights: HashMap::new(),
fades: HashMap::new(),
reorder_easing: Rc::new(crate::slide::low_pass_easing),
pending_flights: None,
};
let init = grid.fetch_task(0);
(grid, init)
}
#[must_use]
pub fn selection(&self) -> &Selection<S::RowId> {
&self.selection
}
pub fn selection_mut(&mut self) -> &mut Selection<S::RowId> {
&mut self.selection
}
#[must_use]
pub fn store(&self) -> &S {
&self.store
}
#[must_use]
pub fn follow(&self) -> bool {
self.follow
}
pub fn set_follow(&mut self, follow: bool) -> Task<Message<S::Row>> {
self.follow = follow;
if follow {
self.jump_to_tail()
} else {
Task::none()
}
}
pub fn transact(&mut self, mutate: impl FnOnce(&mut S)) -> Task<Message<S::Row>> {
let snapshot = self.flight_snapshot();
let task = self.transact_inner(mutate);
self.pending_flights = snapshot.map(|s| (self.fetch_generation, s));
self.try_schedule_flights();
task
}
fn transact_inner(&mut self, mutate: impl FnOnce(&mut S)) -> Task<Message<S::Row>> {
let anchor = self.virt.anchor_at(self.scroll_offset);
let candidates: Vec<S::RowId> = if self.follow {
Vec::new()
} else {
let visible = self.virt.rows_spanned(self.viewport_height.get());
(anchor.row..anchor.row.saturating_add(visible))
.filter_map(|i| self.cached_row(i).map(|row| self.store.row_id(row)))
.collect()
};
mutate(&mut self.store);
self.total_rows = self.store.row_count().lower_bound();
if self.follow {
return self.jump_to_tail();
}
let new_offset = candidates
.iter()
.find_map(|id| self.store.index_of(id))
.map(|index| {
self.virt.flat_offset_of(ScrollAnchor { row: index, offset_px: anchor.offset_px })
})
.unwrap_or(self.scroll_offset)
.clamp(0.0, self.max_scroll());
let scroll = (new_offset != self.scroll_offset).then(|| {
self.scroll_offset = new_offset;
iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: new_offset },
)
});
let fetch = self.fetch_now(self.virt.anchor_at(new_offset).row);
match scroll {
Some(scroll) => Task::batch([scroll, fetch]),
None => fetch,
}
}
#[must_use]
pub fn focus(&self) -> Option<&S::RowId> {
self.focus.as_ref()
}
#[must_use]
pub fn focus_index(&self) -> Option<usize> {
let focus = self.focus.as_ref()?;
self.window_rows
.iter()
.position(|row| self.store.row_id(row) == *focus)
.map(|offset| self.window_start + offset)
.or_else(|| self.focus_restoration.then(|| self.store.index_of(focus)).flatten())
}
pub fn set_focus_restoration(&mut self, enabled: bool) {
self.focus_restoration = enabled;
}
#[must_use]
pub fn focus_restoration(&self) -> bool {
self.focus_restoration
}
pub fn set_row_height(&mut self, px: f32) -> Task<Message<S::Row>> {
if px == self.virt.row_height() {
return Task::none();
}
let anchor = self.scroll_anchor();
let scale = px / self.virt.row_height();
self.virt = RowVirtualizer::new(px);
if self.follow {
return self.jump_to_tail();
}
let target = ScrollAnchor { row: anchor.row, offset_px: anchor.offset_px * scale };
let offset = self.virt.flat_offset_of(target).clamp(0.0, self.max_scroll());
let scroll = (offset != self.scroll_offset).then(|| {
self.scroll_offset = offset;
iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
)
});
let fetch = self.fetch_now(self.virt.anchor_at(offset).row);
match scroll {
Some(scroll) => Task::batch([scroll, fetch]),
None => fetch,
}
}
pub fn set_header_height(&mut self, px: f32) {
self.header_height = px;
}
pub fn set_footer_height(&mut self, px: f32) {
self.footer_height = px;
}
pub fn set_cell_padding(&mut self, px: f32) {
self.cell_padding = px;
}
pub fn set_handle_width(&mut self, px: f32) {
self.handle_width = px;
}
pub fn set_row_style(
&mut self,
style: impl for<'a> Fn(&iced::Theme, &RowContext<'a, S::Row>) -> iced::widget::container::Style
+ 'static,
) {
self.row_style = Box::new(style);
}
pub fn set_cell_style(
&mut self,
style: impl for<'a> Fn(
&iced::Theme,
&RowContext<'a, S::Row>,
ColumnId,
) -> iced::widget::container::Style
+ 'static,
) {
self.cell_style = Box::new(style);
}
pub fn set_header_style(
&mut self,
style: impl Fn(&iced::Theme) -> iced::widget::container::Style + 'static,
) {
self.header_style = Box::new(style);
}
pub fn set_footer_style(
&mut self,
style: impl Fn(&iced::Theme) -> iced::widget::container::Style + 'static,
) {
self.footer_style = Box::new(style);
}
pub fn set_sort_indicator(
&mut self,
indicator: impl Fn(SortDirection) -> Element<'static, Message<S::Row>> + 'static,
) {
self.sort_indicator = Box::new(indicator);
}
pub fn set_row_divider(&mut self, color: Option<Color>) {
self.row_divider = color;
}
pub fn set_column_divider(&mut self, color: Option<Color>) {
self.column_divider = color;
}
pub fn auto_size_all(&mut self) {
self.auto_size.extend(0..self.columns.len());
self.auto_size_epoch += 1;
}
pub fn set_rule_color(&mut self, color: Option<Color>) {
self.rule_color = color;
}
#[must_use]
pub fn row_height(&self) -> f32 {
self.virt.row_height()
}
#[must_use]
pub fn header_height(&self) -> f32 {
self.header_height
}
#[must_use]
pub fn footer_height(&self) -> f32 {
self.footer_height
}
#[must_use]
pub fn cell_padding(&self) -> f32 {
self.cell_padding
}
#[must_use]
pub fn handle_width(&self) -> f32 {
self.handle_width
}
pub fn set_reorder_animation(&mut self, duration: Duration) {
self.reorder_animation = duration;
if duration.is_zero() {
self.flights.clear();
self.fades.clear();
self.pending_flights = None;
}
}
pub fn set_reorder_easing(&mut self, easing: impl Fn(f32) -> f32 + 'static) {
self.reorder_easing = Rc::new(easing);
}
#[must_use]
pub fn animating(&self) -> bool {
let now = Instant::now();
self.flights.values().any(|f| f.is_live(now))
|| self.fades.values().any(|f| f.is_live(now))
}
fn flight_snapshot(&self) -> Option<Vec<(S::RowId, f32)>> {
if self.reorder_animation.is_zero() {
return None;
}
let anchor = self.virt.anchor_at(self.scroll_offset);
let visible = self.virt.rows_spanned(self.viewport_height.get());
let mut snapshot = Vec::new();
for i in anchor.row..anchor.row.saturating_add(visible) {
if let Some(row) = self.cached_row(i) {
snapshot.push((self.store.row_id(row), self.virt.screen_y(i, self.scroll_offset)));
}
}
(!snapshot.is_empty()).then_some(snapshot)
}
fn try_schedule_flights(&mut self) {
let Some((generation, _)) = self.pending_flights.as_ref() else {
return;
};
if *generation != self.fetch_generation {
self.pending_flights = None;
return;
}
if self.window_generation != self.fetch_generation {
return;
}
let Some((_, old)) = self.pending_flights.take() else {
return;
};
let old: HashMap<S::RowId, f32> = old.into_iter().collect();
let now = Instant::now();
let duration = self.reorder_animation;
let anchor = self.virt.anchor_at(self.scroll_offset);
let visible = self.virt.rows_spanned(self.viewport_height.get());
self.flights.retain(|_, f| f.is_live(now));
self.fades.retain(|_, f| f.is_live(now));
for i in anchor.row..anchor.row.saturating_add(visible) {
if let Some(row) = self.cached_row(i) {
let id = self.store.row_id(row);
match old.get(&id) {
Some(old_y) if !self.follow => {
let delta = old_y - self.virt.screen_y(i, self.scroll_offset);
if delta.abs() >= 0.5 {
self.flights.insert(
id,
crate::slide::Flight { from: delta, started: now, duration },
);
}
}
Some(_) => {} None => {
self.fades.insert(
id,
crate::slide::Flight { from: 0.0, started: now, duration },
);
}
}
}
}
}
#[must_use]
pub fn fetch_generation(&self) -> u64 {
self.fetch_generation
}
#[must_use]
pub fn window_range(&self) -> core::ops::Range<usize> {
self.window_start..self.window_start + self.window_rows.len()
}
#[must_use]
pub fn spec(&self) -> &QuerySpec {
&self.spec
}
#[must_use]
pub fn total_rows(&self) -> usize {
self.total_rows
}
#[must_use]
pub fn column_layout(&self) -> &ColumnLayout {
&self.layout
}
pub fn column_layout_mut(&mut self) -> &mut ColumnLayout {
&mut self.layout
}
#[must_use]
pub fn scroll_anchor(&self) -> ScrollAnchor {
self.virt.anchor_at(self.scroll_offset)
}
pub fn scroll_to_anchor(&mut self, anchor: ScrollAnchor) -> Task<Message<S::Row>> {
self.follow = false;
let offset = self.virt.flat_offset_of(anchor).clamp(0.0, self.max_scroll());
let scroll = (offset != self.scroll_offset).then(|| {
self.scroll_offset = offset;
iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
)
});
let fetch = self.fetch_now(self.virt.anchor_at(offset).row);
match scroll {
Some(scroll) => Task::batch([scroll, fetch]),
None => fetch,
}
}
pub fn scroll_to_row(&mut self, index: usize) -> Task<Message<S::Row>> {
self.scroll_to_anchor(ScrollAnchor { row: index, offset_px: 0.0 })
}
pub fn set_spec(&mut self, spec: QuerySpec) -> Task<Message<S::Row>> {
let snapshot = self.flight_snapshot();
let task = self.set_spec_inner(spec);
self.pending_flights = snapshot.map(|s| (self.fetch_generation, s));
self.try_schedule_flights();
task
}
fn set_spec_inner(&mut self, spec: QuerySpec) -> Task<Message<S::Row>> {
if spec.filter != self.spec.filter {
self.selection.clear();
self.focus = None;
}
self.store.apply_spec(&spec);
self.spec = spec;
self.total_rows = self.store.row_count().lower_bound();
self.window_start = 0;
self.window_rows.clear();
if self.follow {
return self.jump_to_tail();
}
if self.focus_restoration {
if let Some(index) = self.focus.as_ref().and_then(|id| self.store.index_of(id)) {
return self.reveal_after_invalidate(index);
}
}
let offset = self.scroll_offset.clamp(0.0, self.max_scroll());
let scroll = (offset != self.scroll_offset).then(|| {
self.scroll_offset = offset;
iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
)
});
let fetch = self.fetch_task(self.virt.anchor_at(offset).row);
match scroll {
Some(scroll) => Task::batch([scroll, fetch]),
None => fetch,
}
}
fn reveal_after_invalidate(&mut self, target: usize) -> Task<Message<S::Row>> {
let offset = self
.virt
.reveal_offset(self.scroll_offset, self.viewport_height.get(), target)
.unwrap_or(self.scroll_offset)
.clamp(0.0, self.max_scroll());
let scroll = (offset != self.scroll_offset).then(|| {
self.scroll_offset = offset;
iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
)
});
let fetch = self.fetch_now(self.virt.anchor_at(offset).row);
match scroll {
Some(scroll) => Task::batch([scroll, fetch]),
None => fetch,
}
}
fn toggled_sort(&self, column: ColumnId) -> Option<SortSpec> {
self.spec.cycled_sort(column)
}
fn fetch_task(&mut self, anchor_row: usize) -> Task<Message<S::Row>> {
self.fetch_generation += 1;
let generation = self.fetch_generation;
let start = anchor_row.saturating_sub(FETCH_MARGIN);
let end = anchor_row.saturating_add(FETCH_ROWS).min(self.total_rows).max(start);
Task::perform(self.store.fetch(start..end), move |rows| Message::Fetched {
generation,
start,
rows,
})
}
fn fetch_now(&mut self, anchor_row: usize) -> Task<Message<S::Row>> {
use core::future::Future;
use core::task::{Context, Poll, Waker};
self.fetch_generation += 1;
let generation = self.fetch_generation;
let start = anchor_row.saturating_sub(FETCH_MARGIN);
let end = anchor_row.saturating_add(FETCH_ROWS).min(self.total_rows).max(start);
let mut future = Box::pin(self.store.fetch(start..end));
match future.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(rows) => {
self.window_start = start;
self.window_rows = rows;
self.window_generation = generation;
Task::none()
}
Poll::Pending => Task::perform(future, move |rows| Message::Fetched {
generation,
start,
rows,
}),
}
}
fn max_scroll(&self) -> f32 {
(self.virt.total_height(self.total_rows) - self.viewport_height.get()).max(0.0)
}
fn jump_to_tail(&mut self) -> Task<Message<S::Row>> {
let offset = self.max_scroll();
let moved = offset != self.scroll_offset;
self.scroll_offset = offset;
let fetch = self.fetch_now(self.virt.anchor_at(offset).row);
if moved {
let scroll = iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
);
Task::batch([scroll, fetch])
} else {
fetch
}
}
fn cached_row(&self, index: usize) -> Option<&S::Row> {
index
.checked_sub(self.window_start)
.and_then(|offset| self.window_rows.get(offset))
}
pub fn focus_next(&mut self) -> Task<Message<S::Row>> {
self.move_focus(1)
}
pub fn focus_prev(&mut self) -> Task<Message<S::Row>> {
self.move_focus(-1)
}
pub fn page_down(&mut self) -> Task<Message<S::Row>> {
self.move_focus(self.virt.page_len(self.viewport_height.get()) as isize)
}
pub fn page_up(&mut self) -> Task<Message<S::Row>> {
self.move_focus(-(self.virt.page_len(self.viewport_height.get()) as isize))
}
fn move_focus(&mut self, delta: isize) -> Task<Message<S::Row>> {
if self.total_rows == 0 {
return Task::none();
}
let last = self.total_rows - 1;
let target = match self.focus_index() {
Some(index) => {
(index as isize + delta).clamp(0, last as isize) as usize
}
None => self.virt.anchor_at(self.scroll_offset).row.min(last),
};
let task = self.ensure_visible(target);
if let Some(row) = self.cached_row(target) {
let id = self.store.row_id(row);
self.selection.replace(id.clone());
self.focus = Some(id);
}
task
}
fn ensure_visible(&mut self, target: usize) -> Task<Message<S::Row>> {
let Some(offset) =
self.virt.reveal_offset(self.scroll_offset, self.viewport_height.get(), target)
else {
return Task::none();
};
if offset < self.scroll_offset {
self.follow = false;
}
self.scroll_offset = offset;
let scroll = iced::widget::operation::scroll_to(
self.body_id.clone(),
AbsoluteOffset { x: self.layout.h_scroll(), y: offset },
);
let anchor = self.virt.anchor_at(offset);
if self.needs_fetch(anchor.row) {
let fetch = self.fetch_now(anchor.row);
Task::batch([scroll, fetch])
} else {
scroll
}
}
fn needs_fetch(&self, anchor_row: usize) -> bool {
if self.window_rows.is_empty() {
return true;
}
let window_end = self.window_start + self.window_rows.len();
let at_top = self.window_start > 0 && anchor_row < self.window_start + EDGE;
let at_bottom =
window_end < self.total_rows && anchor_row + MAX_VISIBLE_ROWS + EDGE > window_end;
at_top || at_bottom
}
pub fn update(&mut self, message: Message<S::Row>) -> Task<Message<S::Row>> {
match message {
Message::BodyScrolled(offset) => {
if offset.y < self.last_scroll_report {
self.recession += self.last_scroll_report - offset.y;
} else {
self.recession = 0.0;
}
let receding = self.recession >= 1.0 && offset.y < self.scroll_offset;
if self.follow && receding {
self.follow = false;
}
self.last_scroll_report = offset.y;
self.scroll_offset = offset.y;
self.layout.set_h_scroll(offset.x);
let mut tasks = vec![iced::widget::operation::scroll_to(
self.header_id.clone(),
AbsoluteOffset { y: 0.0, ..offset },
)];
if self.has_footer {
tasks.push(iced::widget::operation::scroll_to(
self.footer_id.clone(),
AbsoluteOffset { y: 0.0, ..offset },
));
}
let anchor = self.virt.anchor_at(offset.y);
if self.needs_fetch(anchor.row) {
tasks.push(self.fetch_task(anchor.row));
}
Task::batch(tasks)
}
Message::Fetched { generation, start, rows } => {
if generation == self.fetch_generation {
self.window_start = start;
self.window_rows = rows;
self.window_generation = generation;
self.try_schedule_flights();
}
Task::none()
}
Message::RowPressed { index } => {
if let Some(row) = self.cached_row(index) {
let id = self.store.row_id(row);
self.selection.replace(id.clone());
self.focus = Some(id);
}
Task::none()
}
Message::RowHovered { index } => {
self.hovered = Some(index);
Task::none()
}
Message::RowUnhovered { index } => {
if self.hovered == Some(index) {
self.hovered = None;
}
Task::none()
}
Message::ColumnResizing { index, delta } => {
self.layout.preview_resize(index, delta);
Task::none()
}
Message::ColumnResized { index } => {
self.layout.commit_resize(index);
Task::none()
}
Message::ScrollbarDragged(offset) => {
let anchor = self.virt.anchor_at(offset.max(0.0));
self.scroll_to_anchor(anchor)
}
Message::ColumnAutoSizeRequested { index } => {
self.auto_size.insert(index);
self.auto_size_epoch += 1;
Task::none()
}
Message::ColumnMeasured { index, width } => {
if self.auto_size.remove(&index)
&& width < crate::measure::MEASURE_CAP - 0.5
{
self.layout.set_width(index, width + self.cell_padding * 2.0);
}
Task::none()
}
Message::HeaderPressed { index } => match self.layout.id(index) {
Some(column) => {
let spec = QuerySpec {
sort: self.toggled_sort(column),
filter: self.spec.filter.clone(),
};
self.set_spec(spec)
}
None => Task::none(),
},
}
}
pub fn view(&self) -> Element<'_, Message<S::Row>>
where
S::Row: Clone,
{
let columns = &self.columns;
let layout = &self.layout;
let virt = self.virt;
let spec_sort = self.spec.sort;
let scroll_offset = self.scroll_offset;
let total_rows = self.total_rows;
let window_start = self.window_start;
let window_rows = &self.window_rows;
let store = &self.store;
let selection = &self.selection;
let focus = self.focus.as_ref();
let hovered = self.hovered;
let auto_size = &self.auto_size;
let viewport_height = &self.viewport_height;
let header_id = self.header_id.clone();
let body_id = self.body_id.clone();
let header_height = self.header_height;
let footer_height = self.footer_height;
let has_footer = self.has_footer;
let handle_width = self.handle_width;
let cell_padding = self.cell_padding;
let row_style = &self.row_style;
let cell_style = &self.cell_style;
let header_style = &self.header_style;
let footer_style = &self.footer_style;
let sort_indicator = &self.sort_indicator;
let row_divider = self.row_divider;
let column_divider = self.column_divider;
let rule_color = self.rule_color;
let auto_size_epoch = self.auto_size_epoch;
let footer_id = self.footer_id.clone();
let flights = &self.flights;
let fades = &self.fades;
let reorder_easing = &self.reorder_easing;
let fade_cover: crate::slide::CoverFn<iced::Theme> =
Rc::new(crate::slide::theme_background);
iced::widget::responsive(move |size| {
let placements = layout.placements();
let total_width = layout.total_width();
let row_height = virt.row_height();
let footer_space = if has_footer { footer_height + 1.0 } else { 0.0 };
let body_viewport = (size.height - header_height - 1.0 - footer_space).max(0.0);
let rows_height = virt.total_height(total_rows);
let v_over = rows_height > body_viewport;
let h_over =
total_width + if v_over { SCROLLBAR_GUTTER } else { 0.0 } > size.width;
let bottom_gutter = if h_over { SCROLLBAR_GUTTER } else { 0.0 };
let v_over = rows_height + bottom_gutter > body_viewport;
let right_gutter = if v_over { SCROLLBAR_GUTTER } else { 0.0 };
let body_natural = rows_height + bottom_gutter;
let docked = has_footer && body_natural < body_viewport;
let body_view_h = if docked { body_natural } else { body_viewport };
let body_height = if docked {
Length::Fixed(body_natural)
} else {
Length::Fill
};
viewport_height.set((body_view_h - bottom_gutter).max(0.0));
let window = virt.visible(scroll_offset, body_view_h, total_rows);
let cache_end = window_start + window_rows.len();
let cell_pad = Padding {
top: 0.0,
right: cell_padding,
bottom: 0.0,
left: cell_padding,
};
let header_cells = placements.iter().map(|p| {
let col = &columns[p.index];
let mut label_content: Element<'_, Message<S::Row>> = col.header();
if let Some(sort) = spec_sort.filter(|s| s.column == p.id) {
label_content = row![label_content, (sort_indicator)(sort.direction)]
.spacing(4)
.align_y(Vertical::Center)
.into();
}
let label: Element<'_, Message<S::Row>> = container(label_content)
.width(Length::Fill)
.height(Length::Fixed(header_height))
.align_y(Vertical::Center)
.padding(cell_pad)
.into();
let label: Element<'_, Message<S::Row>> = if col.sortable() {
let index = p.index;
mouse_area(label)
.on_press(Message::HeaderPressed { index })
.into()
} else {
label
};
let content: Element<'_, Message<S::Row>> = if col.sizing().is_resizable() {
let index = p.index;
crate::resize::ResizeHandle::new(
label,
handle_width,
move |delta| Message::ColumnResizing { index, delta },
Message::ColumnResized { index },
)
.on_auto_size(Message::ColumnAutoSizeRequested { index })
.into()
} else {
label
};
container(content)
.width(Length::Fixed(p.width))
.height(Length::Fixed(header_height))
.into()
});
let header = container(
row![
scrollable(row(header_cells).width(Length::Fixed(total_width)))
.id(header_id.clone())
.direction(Direction::Horizontal(hidden_scrollbar()))
.width(Length::Fill)
.height(Length::Fixed(header_height)),
space().width(Length::Fixed(right_gutter)),
],
)
.style(move |theme: &iced::Theme| (header_style)(theme));
let body_rows = keyed_column(window.range().map(|i| {
let data = (i >= window_start && i < cache_end)
.then(|| &window_rows[i - window_start]);
let id = data.map(|d| store.row_id(d));
let key = match &id {
Some(id) => keyed_hash(0, id),
None => keyed_hash(1, &i),
};
let ctx = RowContext {
index: i,
row: data,
is_selected: id.as_ref().is_some_and(|id| selection.contains(id)),
is_focused: id.as_ref().zip(focus).is_some_and(|(id, f)| id == f),
is_hovered: hovered == Some(i),
};
let cell_h = row_height - if row_divider.is_some() { 1.0 } else { 0.0 };
let last = placements.len().saturating_sub(1);
let mut cells: Vec<Element<'_, Message<S::Row>>> =
Vec::with_capacity(placements.len() * 2);
for (k, p) in placements.iter().enumerate() {
let content: Element<'_, Message<S::Row>> = match data {
Some(data) => columns[p.index].cell(data),
None => text("…")
.size(13.0)
.style(|theme: &iced::Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().background.strong.color),
})
.into(),
};
let column_id = p.id;
let vband = column_divider.filter(|_| k < last);
let shave = if vband.is_some() { 1.0 } else { 0.0 };
cells.push(
container(content)
.width(Length::Fixed(p.width - shave))
.height(Length::Fixed(cell_h))
.align_y(Vertical::Center)
.padding(cell_pad)
.clip(true)
.style(move |theme: &iced::Theme| {
(cell_style)(theme, &ctx, column_id)
})
.into(),
);
if let Some(color) = vband {
cells.push(
container(space())
.width(Length::Fixed(1.0))
.height(Length::Fixed(cell_h))
.style(move |_theme: &iced::Theme| {
iced::widget::container::Style {
background: Some(color.into()),
..iced::widget::container::Style::default()
}
})
.into(),
);
}
}
let flight = id.as_ref().and_then(|id| flights.get(id)).copied();
let fade = id.as_ref().and_then(|id| fades.get(id)).copied();
let lanes = row(cells).width(Length::Fixed(total_width));
let row_body: Element<'_, Message<S::Row>> = match row_divider {
Some(color) => column![
lanes,
container(space())
.width(Length::Fixed(total_width))
.height(Length::Fixed(1.0))
.style(move |_theme: &iced::Theme| {
iced::widget::container::Style {
background: Some(color.into()),
..iced::widget::container::Style::default()
}
}),
]
.into(),
None => lanes.into(),
};
let row_el: Element<'_, Message<S::Row>> = crate::slide::Slide::new(
mouse_area(
container(row_body)
.style(move |theme: &iced::Theme| (row_style)(theme, &ctx)),
)
.on_press(Message::RowPressed { index: i })
.on_enter(Message::RowHovered { index: i })
.on_exit(Message::RowUnhovered { index: i }),
flight,
fade,
Rc::clone(reorder_easing),
Rc::clone(&fade_cover),
)
.into();
(key, row_el)
}))
.width(Length::Fixed(total_width));
let body = scrollable(
column![
space().height(Length::Fixed(window.space_before)),
body_rows,
space().height(Length::Fixed(window.space_after + bottom_gutter)),
]
.width(Length::Fixed(total_width)),
)
.id(body_id.clone())
.on_scroll(|viewport: Viewport| Message::BodyScrolled(viewport.absolute_offset()))
.direction(Direction::Both {
vertical: hidden_scrollbar(),
horizontal: Scrollbar::new(),
})
.width(Length::Fill)
.height(if v_over { Length::Fill } else { body_height });
let body_pane: Element<'_, Message<S::Row>> = if v_over {
row![
body,
crate::scrollbar::VScrollbar::new(
scroll_offset,
body_view_h,
rows_height + bottom_gutter,
Message::ScrollbarDragged,
),
]
.height(body_height)
.into()
} else {
body.into()
};
let measurers = (0..columns.len()).map(|index| {
let mut measure_children: Vec<Element<'_, Message<S::Row>>> = Vec::new();
if auto_size.contains(&index) {
let col = &columns[index];
let mut cells: Vec<Element<'_, Message<S::Row>>> = Vec::new();
let mut unmeasurable = false;
for i in window.range() {
if i >= window_start && i < cache_end {
match col.measure(&window_rows[i - window_start]) {
Some(element) => cells.push(element),
None => {
unmeasurable = true;
break;
}
}
}
}
if unmeasurable {
measure_children.push(
space().width(Length::Fixed(crate::measure::MEASURE_CAP)).into(),
);
}
else if !cells.is_empty() {
let mut header_el: Element<'_, Message<S::Row>> = col.header();
if let Some(sort) = spec_sort.filter(|s| s.column == col.id()) {
header_el = row![header_el, (sort_indicator)(sort.direction)]
.spacing(4)
.align_y(Vertical::Center)
.into();
}
measure_children.push(
container(header_el)
.padding(Padding {
top: 0.0,
right: handle_width,
bottom: 0.0,
left: 0.0,
})
.into(),
);
measure_children.append(&mut cells);
if let Some(footer_el) = col.measure_footer() {
measure_children.push(
container(footer_el)
.padding(Padding {
top: 0.0,
right: 1.0,
bottom: 0.0,
left: 0.0,
})
.into(),
);
}
}
}
crate::measure::Measure::new(measure_children, auto_size_epoch, move |width| {
Message::ColumnMeasured { index, width }
})
});
let frame_rule = || -> Element<'_, Message<S::Row>> {
match rule_color {
Some(color) => iced::widget::rule::horizontal(1)
.style(move |theme: &iced::Theme| iced::widget::rule::Style {
color,
..iced::widget::rule::default(theme)
})
.into(),
None => iced::widget::rule::horizontal(1).into(),
}
};
let mut children: Vec<Element<'_, Message<S::Row>>> =
vec![header.into(), frame_rule(), body_pane];
if has_footer {
let footer_cells = placements.iter().map(|p| {
let content: Element<'_, Message<S::Row>> = columns[p.index]
.footer()
.unwrap_or_else(|| space().into());
let cell = container(content)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Vertical::Center)
.padding(cell_pad)
.clip(true);
let tick = iced::widget::rule::vertical(1).style(|theme: &iced::Theme| {
iced::widget::rule::Style {
fill_mode: iced::widget::rule::FillMode::Percent(50.0),
..iced::widget::rule::default(theme)
}
});
container(row![cell, tick])
.width(Length::Fixed(p.width))
.height(Length::Fixed(footer_height))
.into()
});
let footer = container(
row![
scrollable(row(footer_cells).width(Length::Fixed(total_width)))
.id(footer_id.clone())
.direction(Direction::Horizontal(hidden_scrollbar()))
.width(Length::Fill)
.height(Length::Fixed(footer_height)),
space().width(Length::Fixed(right_gutter)),
],
)
.style(move |theme: &iced::Theme| (footer_style)(theme));
children.push(frame_rule());
children.push(footer.into());
}
children.extend(measurers.map(Into::into));
column(children).width(Length::Fill).height(Length::Fill).into()
})
.into()
}
}
fn hidden_scrollbar() -> Scrollbar {
Scrollbar::new().width(0).margin(0).scroller_width(0)
}
fn keyed_hash(tag: u8, value: &impl std::hash::Hash) -> u64 {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut hasher = DefaultHasher::new();
tag.hash(&mut hasher);
value.hash(&mut hasher);
hasher.finish()
}