use std::cell::RefCell;
use std::rc::Rc;
use teksilo_canvas::{Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::signal::Signal;
use teksilo_core::widget::{LayoutContext, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use super::PaneBoundaries;
use super::layout::band_rects;
pub(crate) type SharedColumnWidths = Rc<RefCell<Vec<f32>>>;
fn has_pinning(boundaries: PaneBoundaries, cell_count: usize) -> bool {
boundaries.leading_count > 0 || boundaries.middle_end < cell_count
}
#[derive(Debug)]
pub(crate) struct BodyRow {
cells: Vec<WidgetId>,
row_index_1based: usize,
selected: bool,
row_height: Option<f32>,
widths: SharedColumnWidths,
pane_boundaries: PaneBoundaries,
scroll_x: Signal<f32>,
announce_a11y: bool,
bands: Option<[Option<WidgetId>; 3]>,
}
impl BodyRow {
pub(crate) fn new(
cells: Vec<WidgetId>,
row_index_1based: usize,
selected: bool,
row_height: Option<f32>,
widths: SharedColumnWidths,
pane_boundaries: PaneBoundaries,
scroll_x: Signal<f32>,
) -> Self {
Self {
cells,
row_index_1based,
selected,
row_height,
widths,
pane_boundaries,
scroll_x,
announce_a11y: true,
bands: None,
}
}
pub(crate) fn a11y_hidden(mut self) -> Self {
self.announce_a11y = false;
self
}
fn has_pinning(&self) -> bool {
has_pinning(self.pane_boundaries, self.cells.len())
}
}
impl Widget for BodyRow {
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 = match self.row_height {
Some(h) => h,
None => {
let widths: Vec<f32> = self.widths.borrow().clone();
let mut max_h = 0.0_f32;
for (i, cell) in self.cells.iter().enumerate() {
let w = widths.get(i).copied().unwrap_or(width);
if let Some(size) = ctx.child_size(*cell, SizeProposal::with_width(w)) {
max_h = max_h.max(size.height);
}
}
max_h
}
};
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 accessibility(&self, builder: &mut AccessNodeBuilder) {
if !self.announce_a11y {
builder.set_hidden();
return;
}
builder.set_role(teksilo_core::accesskit::Role::Row);
builder.set_selected(self.selected);
builder.set_row_index(self.row_index_1based);
}
fn children(&self) -> Vec<WidgetId> {
match self.bands {
Some(bands) => bands.iter().copied().flatten().collect(),
None => self.cells.clone(),
}
}
}
#[derive(Debug)]
pub(crate) struct RowBand {
cells: Vec<WidgetId>,
widths: SharedColumnWidths,
widths_start: usize,
scroll_x: Option<Signal<f32>>,
}
impl RowBand {
pub(crate) fn new(
cells: Vec<WidgetId>,
widths: SharedColumnWidths,
widths_start: usize,
) -> Self {
Self {
cells,
widths,
widths_start,
scroll_x: None,
}
}
pub(crate) fn scrollable(mut self, scroll_x: Signal<f32>) -> Self {
self.scroll_x = Some(scroll_x);
self
}
}
impl Widget for RowBand {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
let widths = self.widths.borrow();
let scroll = self.scroll_x.as_ref().map(|s| s.get()).unwrap_or(0.0);
if ctx.is_rtl() {
let mut x = bounds.right() + scroll;
for (i, child) in children.iter_mut().enumerate() {
let w = widths.get(self.widths_start + i).copied().unwrap_or(0.0);
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(self.widths_start + i).copied().unwrap_or(0.0);
child.origin = Point::new(x, bounds.y);
child.size = Size::new(w, bounds.height);
x += w;
}
}
}
fn children(&self) -> Vec<WidgetId> {
self.cells.clone()
}
fn clips_children(&self) -> bool {
self.scroll_x.is_some()
}
}