use crate::core::{Point, Rect, Size, TextDirection};
use crate::event::{Event, EventHandler};
use crate::layout::flow::{
FlowAlignment, FlowDirection, FlowLayout as FlowEngine, FlowLayoutConfig,
};
use crate::layout::wrap::{WrapAlignment, WrapDirection, WrapLayout as WrapEngine};
use crate::layout::{AlignItems, ChildInfo, FlexDirection, FlexItem, FlexLayout, FlexWrap, Hints};
use crate::layout::{JustifyContent, Layout};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::coercion::{expect_bool, expect_string, expect_u32};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::container_widgets::layout_family::{paint_item_box, resolve_palette};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, PartialEq)]
pub struct FlexItemSpec {
label: String,
size: Size,
grow: u32,
widget_id: Option<crate::core::ObjectId>,
}
impl FlexItemSpec {
pub fn new(label: &str, size: Size) -> Self {
Self { label: label.to_string(), size, grow: 0, widget_id: None }
}
pub fn with_widget(mut self, widget_id: crate::core::ObjectId) -> Self {
self.widget_id = Some(widget_id);
self
}
pub fn widget_id(&self) -> Option<crate::core::ObjectId> {
self.widget_id
}
pub fn with_grow(mut self, grow: u32) -> Self {
self.grow = grow;
self
}
pub fn label(&self) -> &str {
&self.label
}
pub fn size(&self) -> Size {
self.size
}
pub fn grow(&self) -> u32 {
self.grow
}
}
fn synthetic_id(index: usize) -> u64 {
index as u64 + 1
}
fn index_of(id: u64) -> Option<usize> {
id.checked_sub(1).map(|index| index as usize)
}
fn collect_rects(count: usize, arrange: impl FnOnce(&mut dyn FnMut(u64, Rect))) -> Vec<Rect> {
let mut rects = vec![Rect::new(0, 0, 0, 0); count];
arrange(&mut |id, rect| {
if let Some(index) = index_of(id) {
if let Some(slot) = rects.get_mut(index) {
*slot = rect;
}
}
});
rects
}
fn paint_items(
ctx: &mut RenderContext,
base: &BaseWidget,
role: &str,
outer: Rect,
rects: &[Rect],
items: &[FlexItemSpec],
) {
let palette = resolve_palette(base, role);
ctx.push_clip(outer.x, outer.y, outer.width, outer.height);
let bottom = outer.y.saturating_add(outer.height as i32);
for (item, rect) in items.iter().zip(rects) {
if rect.height == 0 || rect.y >= bottom {
continue;
}
paint_item_box(ctx, *rect, item.label(), &palette, 3);
}
ctx.pop_clip();
}
pub struct Flex {
base: BaseWidget,
items: Vec<FlexItemSpec>,
direction: FlexDirection,
wrap: FlexWrap,
justify: JustifyContent,
align: AlignItems,
gap: u32,
rect_version: u64,
rect_cache: std::cell::RefCell<Option<(u64, std::rc::Rc<Vec<Rect>>)>>,
pub item_clicked: Signal1<usize>,
}
impl Flex {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Flex, geometry, "Flex"),
items: Vec::new(),
direction: FlexDirection::default(),
wrap: FlexWrap::default(),
justify: JustifyContent::default(),
align: AlignItems::default(),
gap: 0,
rect_version: 1,
rect_cache: std::cell::RefCell::new(None),
item_clicked: Signal1::new(),
}
}
pub fn items(&self) -> &[FlexItemSpec] {
&self.items
}
pub fn add_item(&mut self, item: FlexItemSpec) {
if let Some(widget_id) = item.widget_id() {
self.base.add_child_linked(widget_id);
crate::set_widget_visible(widget_id, true);
}
self.items.push(item);
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn remove_item(&mut self, index: usize) -> bool {
if index >= self.items.len() {
return false;
}
let item = self.items.remove(index);
if let Some(widget_id) = item.widget_id() {
self.base.remove_child_linked(widget_id);
crate::set_widget_visible(widget_id, false);
}
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
true
}
fn place_item_widgets(&self) {
for (item, rect) in self.items.iter().zip(self.cached_item_rects().iter()) {
if let Some(widget_id) = item.widget_id() {
crate::set_widget_geometry(widget_id, rect.x, rect.y, rect.width, rect.height);
}
}
}
pub fn clear_items(&mut self) {
if self.items.is_empty() {
return;
}
self.items.clear();
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn direction(&self) -> FlexDirection {
self.direction
}
pub fn set_direction(&mut self, direction: FlexDirection) {
if self.direction == direction {
return;
}
self.direction = direction;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn wrap(&self) -> FlexWrap {
self.wrap
}
pub fn set_wrap(&mut self, wrap: FlexWrap) {
if self.wrap == wrap {
return;
}
self.wrap = wrap;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn justify(&self) -> JustifyContent {
self.justify
}
pub fn set_justify(&mut self, justify: JustifyContent) {
if self.justify == justify {
return;
}
self.justify = justify;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_redraw();
}
pub fn align(&self) -> AlignItems {
self.align
}
pub fn set_align(&mut self, align: AlignItems) {
if self.align == align {
return;
}
self.align = align;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_redraw();
}
pub fn gap(&self) -> u32 {
self.gap
}
pub fn set_gap(&mut self, gap: u32) {
if self.gap == gap {
return;
}
self.gap = gap;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn item_rects(&self) -> Vec<Rect> {
(*self.cached_item_rects()).clone()
}
fn cached_item_rects(&self) -> std::rc::Rc<Vec<Rect>> {
if let Some((version, rects)) = self.rect_cache.borrow().as_ref() {
if *version == self.rect_version {
return std::rc::Rc::clone(rects);
}
}
let rects = std::rc::Rc::new(self.compute_item_rects());
*self.rect_cache.borrow_mut() = Some((self.rect_version, std::rc::Rc::clone(&rects)));
rects
}
pub fn item_at(&self, point: Point) -> Option<usize> {
let rects = self.cached_item_rects();
if let Some(index) = self.uniform_run_candidate(point) {
if rects.get(index).is_some_and(|rect| rect.contains_point(point)) {
return Some(index);
}
}
rects.iter().position(|rect| rect.contains_point(point))
}
fn uniform_run_candidate(&self, point: Point) -> Option<usize> {
if self.items.is_empty()
|| self.wrap != FlexWrap::NoWrap
|| !matches!(self.justify, JustifyContent::FlexStart)
|| matches!(self.direction, FlexDirection::RowReverse | FlexDirection::ColumnReverse)
{
return None;
}
let first = self.items.first()?;
if self.items.iter().any(|item| item.size() != first.size() || item.grow() != 0) {
return None;
}
let outer = self.geometry();
let gap = self.gap as i32;
let (start, step, extent, coordinate) = match self.direction {
FlexDirection::Row => (outer.x, first.size().width as i32 + gap, outer.width, point.x),
_ => (outer.y, first.size().height as i32 + gap, outer.height, point.y),
};
if step <= 0 || coordinate < start || coordinate >= start + extent as i32 {
return None;
}
let slot = ((coordinate - start) / step) as usize;
(slot < self.items.len()).then_some(slot)
}
fn compute_item_rects(&self) -> Vec<Rect> {
let layout = self.build_layout();
let children: Vec<ChildInfo> = self
.items
.iter()
.enumerate()
.map(|(index, item)| {
ChildInfo::new(
synthetic_id(index),
Hints::fixed(item.size().width, item.size().height),
)
})
.collect();
collect_rects(self.items.len(), |out| {
layout.arrange(self.geometry(), &children, &mut |id, rect| out(id, rect));
})
}
fn build_layout(&self) -> FlexLayout {
let mut layout = FlexLayout::with_params(
self.direction,
self.wrap,
self.justify,
self.align,
self.gap as i32,
0,
);
for (index, item) in self.items.iter().enumerate() {
layout.items_mut().push(FlexItem {
widget_id: Some(synthetic_id(index)),
flex_grow: item.grow() as f32,
..FlexItem::default()
});
}
layout
}
}
impl Widget for Flex {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_geometry(&mut self, geometry: Rect) {
self.base.set_geometry(geometry);
self.rect_version = self.rect_version.wrapping_add(1);
self.place_item_widgets();
}
fn size_hint(&self) -> Size {
let horizontal = matches!(self.direction, FlexDirection::Row | FlexDirection::RowReverse);
let gaps = (self.items.len().saturating_sub(1) as u32).saturating_mul(self.gap);
let mut main: u32 = 0;
let mut cross: u32 = 0;
for item in &self.items {
let size = item.size();
if horizontal {
main = main.saturating_add(size.width);
cross = cross.max(size.height);
} else {
main = main.saturating_add(size.height);
cross = cross.max(size.width);
}
}
let main = main.saturating_add(gaps);
if horizontal {
Size::new(main, cross)
} else {
Size::new(cross, main)
}
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"item_clicked" => {
Some(EventSignalRef::mapped("item_clicked", &self.item_clicked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
_ => None,
}
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Flex {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"item_count" => Ok(CapabilityValue::UInt(self.items.len() as u64)),
"direction" => Ok(CapabilityValue::String(flex_direction_token(self.direction).into())),
"wrap" => Ok(CapabilityValue::String(flex_wrap_token(self.wrap).into())),
"justify_content" => Ok(CapabilityValue::String(justify_token(self.justify).into())),
"align_items" => Ok(CapabilityValue::String(align_token(self.align).into())),
"gap" => Ok(CapabilityValue::UInt(u64::from(self.gap))),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"item_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"direction" => {
let token = expect_string(value)?;
let direction =
flex_direction_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_direction(direction);
Ok(())
}
"wrap" => {
let token = expect_string(value)?;
let wrap = flex_wrap_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_wrap(wrap);
Ok(())
}
"justify_content" => {
let token = expect_string(value)?;
let justify =
justify_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_justify(justify);
Ok(())
}
"align_items" => {
let token = expect_string(value)?;
let align = align_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_align(align);
Ok(())
}
"gap" => {
self.set_gap(expect_u32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"item_count",
"direction",
"wrap",
"justify_content",
"align_items",
"gap",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Flex {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let rects = self.cached_item_rects();
self.place_item_widgets();
paint_items(ctx, &self.base, "flex", outer, &rects, &self.items);
}
}
impl EventHandler for Flex {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if let Event::MousePress { pos, button: 1, .. } = event {
if let Some(index) = self.item_at(*pos) {
self.item_clicked.emit(index);
}
}
}
}
pub struct FlowLayout {
base: BaseWidget,
items: Vec<FlexItemSpec>,
direction: FlowDirection,
alignment: FlowAlignment,
spacing: u32,
padding: u32,
wrap: bool,
text_direction: TextDirection,
rect_version: u64,
rect_cache: std::cell::RefCell<Option<(u64, std::rc::Rc<Vec<Rect>>)>>,
pub item_clicked: Signal1<usize>,
}
impl FlowLayout {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::FlowLayout, geometry, "FlowLayout"),
items: Vec::new(),
direction: FlowDirection::Horizontal,
alignment: FlowAlignment::Start,
spacing: 8,
padding: 8,
wrap: true,
text_direction: TextDirection::default(),
rect_version: 1,
rect_cache: std::cell::RefCell::new(None),
item_clicked: Signal1::new(),
}
}
pub fn text_direction(&self) -> TextDirection {
self.text_direction
}
pub fn set_text_direction(&mut self, direction: TextDirection) {
if self.text_direction == direction {
return;
}
self.text_direction = direction;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn items(&self) -> &[FlexItemSpec] {
&self.items
}
pub fn add_item(&mut self, item: FlexItemSpec) {
self.items.push(item);
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn clear_items(&mut self) {
if self.items.is_empty() {
return;
}
self.items.clear();
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn remove_item(&mut self, index: usize) -> bool {
if index >= self.items.len() {
return false;
}
self.items.remove(index);
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
true
}
pub fn direction(&self) -> FlowDirection {
self.direction
}
pub fn set_direction(&mut self, direction: FlowDirection) {
if self.direction == direction {
return;
}
self.direction = direction;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn alignment(&self) -> FlowAlignment {
self.alignment
}
pub fn set_alignment(&mut self, alignment: FlowAlignment) {
if self.alignment == alignment {
return;
}
self.alignment = alignment;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_redraw();
}
pub fn spacing(&self) -> u32 {
self.spacing
}
pub fn set_spacing(&mut self, spacing: u32) {
if self.spacing == spacing {
return;
}
self.spacing = spacing;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn padding(&self) -> u32 {
self.padding
}
pub fn set_padding(&mut self, padding: u32) {
if self.padding == padding {
return;
}
self.padding = padding;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn wraps(&self) -> bool {
self.wrap
}
pub fn set_wraps(&mut self, wrap: bool) {
if self.wrap == wrap {
return;
}
self.wrap = wrap;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn item_rects(&self) -> Vec<Rect> {
(*self.cached_item_rects()).clone()
}
fn cached_item_rects(&self) -> std::rc::Rc<Vec<Rect>> {
if let Some((version, rects)) = self.rect_cache.borrow().as_ref() {
if *version == self.rect_version {
return std::rc::Rc::clone(rects);
}
}
let rects = std::rc::Rc::new(self.compute_item_rects());
*self.rect_cache.borrow_mut() = Some((self.rect_version, std::rc::Rc::clone(&rects)));
rects
}
fn compute_item_rects(&self) -> Vec<Rect> {
let mut engine = FlowEngine::with_config(FlowLayoutConfig {
direction: self.direction,
alignment: self.alignment,
spacing: self.spacing as i32,
padding: self.padding as i32,
wrap: self.wrap,
});
engine.set_text_direction(self.text_direction);
let children: Vec<ChildInfo> = self
.items
.iter()
.enumerate()
.map(|(index, item)| {
let id = synthetic_id(index);
engine.add_widget(id, 0);
ChildInfo::new(id, Hints::fixed(item.size().width, item.size().height))
})
.collect();
collect_rects(self.items.len(), |out| {
engine.arrange(self.geometry(), &children, &mut |id, rect| out(id, rect));
})
}
pub fn item_at(&self, point: Point) -> Option<usize> {
self.cached_item_rects().iter().position(|rect| rect.contains_point(point))
}
}
impl Widget for FlowLayout {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_geometry(&mut self, geometry: Rect) {
self.base.set_geometry(geometry);
self.rect_version = self.rect_version.wrapping_add(1);
}
fn size_hint(&self) -> Size {
let horizontal = matches!(self.direction, FlowDirection::Horizontal);
let gaps = (self.items.len().saturating_sub(1) as u32).saturating_mul(self.spacing);
let mut main: u32 = 0;
let mut cross: u32 = 0;
for item in &self.items {
let size = item.size();
if horizontal {
main = main.saturating_add(size.width);
cross = cross.max(size.height);
} else {
main = main.saturating_add(size.height);
cross = cross.max(size.width);
}
}
let main = main.saturating_add(gaps).saturating_add(self.padding.saturating_mul(2));
let cross = cross.saturating_add(self.padding.saturating_mul(2));
if horizontal {
Size::new(main, cross)
} else {
Size::new(cross, main)
}
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"item_clicked" => {
Some(EventSignalRef::mapped("item_clicked", &self.item_clicked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
_ => None,
}
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for FlowLayout {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"item_count" => Ok(CapabilityValue::UInt(self.items.len() as u64)),
"direction" => Ok(CapabilityValue::String(flow_direction_token(self.direction).into())),
"alignment" => Ok(CapabilityValue::String(flow_alignment_token(self.alignment).into())),
"spacing" => Ok(CapabilityValue::UInt(u64::from(self.spacing))),
"padding" => Ok(CapabilityValue::UInt(u64::from(self.padding))),
"wrap" => Ok(CapabilityValue::Bool(self.wrap)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"item_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"direction" => {
let token = expect_string(value)?;
let direction =
flow_direction_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_direction(direction);
Ok(())
}
"alignment" => {
let token = expect_string(value)?;
let alignment =
flow_alignment_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_alignment(alignment);
Ok(())
}
"spacing" => {
self.set_spacing(expect_u32(value)?);
Ok(())
}
"padding" => {
self.set_padding(expect_u32(value)?);
Ok(())
}
"wrap" => {
self.set_wraps(expect_bool(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"item_count",
"direction",
"alignment",
"spacing",
"padding",
"wrap",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for FlowLayout {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let rects = self.cached_item_rects();
paint_items(ctx, &self.base, "flow_layout", outer, &rects, &self.items);
}
}
impl EventHandler for FlowLayout {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if let Event::MousePress { pos, button: 1, .. } = event {
if let Some(index) = self.item_at(*pos) {
self.item_clicked.emit(index);
}
}
}
}
pub struct WrapLayout {
base: BaseWidget,
items: Vec<FlexItemSpec>,
direction: WrapDirection,
alignment: WrapAlignment,
spacing: u32,
run_spacing: Option<u32>,
padding: u32,
text_direction: TextDirection,
rect_version: u64,
rect_cache: std::cell::RefCell<Option<(u64, std::rc::Rc<Vec<Rect>>)>>,
pub item_clicked: Signal1<usize>,
}
impl WrapLayout {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::WrapLayout, geometry, "WrapLayout"),
items: Vec::new(),
direction: WrapDirection::Horizontal,
alignment: WrapAlignment::Start,
spacing: 8,
run_spacing: None,
padding: 8,
text_direction: TextDirection::default(),
rect_version: 1,
rect_cache: std::cell::RefCell::new(None),
item_clicked: Signal1::new(),
}
}
pub fn items(&self) -> &[FlexItemSpec] {
&self.items
}
pub fn add_item(&mut self, item: FlexItemSpec) {
self.items.push(item);
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn clear_items(&mut self) {
if self.items.is_empty() {
return;
}
self.items.clear();
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn remove_item(&mut self, index: usize) -> bool {
if index >= self.items.len() {
return false;
}
self.items.remove(index);
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
true
}
pub fn direction(&self) -> WrapDirection {
self.direction
}
pub fn set_direction(&mut self, direction: WrapDirection) {
if self.direction == direction {
return;
}
self.direction = direction;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn alignment(&self) -> WrapAlignment {
self.alignment
}
pub fn set_alignment(&mut self, alignment: WrapAlignment) {
if self.alignment == alignment {
return;
}
self.alignment = alignment;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_redraw();
}
pub fn spacing(&self) -> u32 {
self.spacing
}
pub fn set_spacing(&mut self, spacing: u32) {
if self.spacing == spacing {
return;
}
self.spacing = spacing;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn run_spacing(&self) -> Option<u32> {
self.run_spacing
}
pub fn set_run_spacing(&mut self, run_spacing: Option<u32>) {
if self.run_spacing == run_spacing {
return;
}
self.run_spacing = run_spacing;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn padding(&self) -> u32 {
self.padding
}
pub fn set_padding(&mut self, padding: u32) {
if self.padding == padding {
return;
}
self.padding = padding;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn text_direction(&self) -> TextDirection {
self.text_direction
}
pub fn set_text_direction(&mut self, direction: TextDirection) {
if self.text_direction == direction {
return;
}
self.text_direction = direction;
self.rect_version = self.rect_version.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn item_rects(&self) -> Vec<Rect> {
(*self.cached_item_rects()).clone()
}
fn cached_item_rects(&self) -> std::rc::Rc<Vec<Rect>> {
if let Some((version, rects)) = self.rect_cache.borrow().as_ref() {
if *version == self.rect_version {
return std::rc::Rc::clone(rects);
}
}
let rects = std::rc::Rc::new(self.compute_item_rects());
*self.rect_cache.borrow_mut() = Some((self.rect_version, std::rc::Rc::clone(&rects)));
rects
}
fn compute_item_rects(&self) -> Vec<Rect> {
let mut engine = WrapEngine::new(
self.direction,
self.alignment,
self.spacing as i32,
self.padding as i32,
);
engine.set_run_spacing(self.run_spacing.map(|spacing| spacing as i32));
engine.set_text_direction(self.text_direction);
let children: Vec<ChildInfo> = self
.items
.iter()
.enumerate()
.map(|(index, item)| {
let id = synthetic_id(index);
engine.add_widget(id, 0);
engine.set_child_size(id, item.size());
ChildInfo::new(id, Hints::fixed(item.size().width, item.size().height))
})
.collect();
collect_rects(self.items.len(), |out| {
engine.arrange(self.geometry(), &children, &mut |id, rect| out(id, rect));
})
}
pub fn item_at(&self, point: Point) -> Option<usize> {
self.cached_item_rects().iter().position(|rect| rect.contains_point(point))
}
}
impl Widget for WrapLayout {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_geometry(&mut self, geometry: Rect) {
self.base.set_geometry(geometry);
self.rect_version = self.rect_version.wrapping_add(1);
}
fn size_hint(&self) -> Size {
let horizontal = matches!(self.direction, WrapDirection::Horizontal);
let gap = (self.items.len().saturating_sub(1) as u32).saturating_mul(self.spacing);
let mut main: u32 = 0;
let mut cross: u32 = 0;
for item in &self.items {
let size = item.size();
if horizontal {
main = main.saturating_add(size.width);
cross = cross.max(size.height);
} else {
main = main.saturating_add(size.height);
cross = cross.max(size.width);
}
}
let main = main.saturating_add(gap).saturating_add(self.padding.saturating_mul(2));
let cross = cross.saturating_add(self.padding.saturating_mul(2));
if horizontal {
Size::new(main, cross)
} else {
Size::new(cross, main)
}
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"item_clicked" => {
Some(EventSignalRef::mapped("item_clicked", &self.item_clicked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
_ => None,
}
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for WrapLayout {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"item_count" => Ok(CapabilityValue::UInt(self.items.len() as u64)),
"direction" => Ok(CapabilityValue::String(wrap_direction_token(self.direction).into())),
"alignment" => Ok(CapabilityValue::String(wrap_alignment_token(self.alignment).into())),
"spacing" => Ok(CapabilityValue::UInt(u64::from(self.spacing))),
"run_spacing" => Ok(match self.run_spacing {
Some(spacing) => CapabilityValue::UInt(u64::from(spacing)),
None => CapabilityValue::Null,
}),
"padding" => Ok(CapabilityValue::UInt(u64::from(self.padding))),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"item_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"direction" => {
let token = expect_string(value)?;
let direction =
wrap_direction_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_direction(direction);
Ok(())
}
"alignment" => {
let token = expect_string(value)?;
let alignment =
wrap_alignment_from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_alignment(alignment);
Ok(())
}
"spacing" => {
self.set_spacing(expect_u32(value)?);
Ok(())
}
"run_spacing" => {
let spacing = match value {
CapabilityValue::Null => None,
other => Some(expect_u32(other)?),
};
self.set_run_spacing(spacing);
Ok(())
}
"padding" => {
self.set_padding(expect_u32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"item_count",
"direction",
"alignment",
"spacing",
"run_spacing",
"padding",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for WrapLayout {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let rects = self.cached_item_rects();
paint_items(ctx, &self.base, "wrap_layout", outer, &rects, &self.items);
}
}
impl EventHandler for WrapLayout {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if let Event::MousePress { pos, button: 1, .. } = event {
if let Some(index) = self.item_at(*pos) {
self.item_clicked.emit(index);
}
}
}
}
fn flex_direction_token(direction: FlexDirection) -> &'static str {
match direction {
FlexDirection::Row => "row",
FlexDirection::RowReverse => "row_reverse",
FlexDirection::Column => "column",
FlexDirection::ColumnReverse => "column_reverse",
}
}
fn flex_direction_from_token(token: &str) -> Option<FlexDirection> {
match token {
"row" => Some(FlexDirection::Row),
"row_reverse" => Some(FlexDirection::RowReverse),
"column" => Some(FlexDirection::Column),
"column_reverse" => Some(FlexDirection::ColumnReverse),
_ => None,
}
}
fn flex_wrap_token(wrap: FlexWrap) -> &'static str {
match wrap {
FlexWrap::NoWrap => "no_wrap",
FlexWrap::Wrap => "wrap",
FlexWrap::WrapReverse => "wrap_reverse",
}
}
fn flex_wrap_from_token(token: &str) -> Option<FlexWrap> {
match token {
"no_wrap" => Some(FlexWrap::NoWrap),
"wrap" => Some(FlexWrap::Wrap),
"wrap_reverse" => Some(FlexWrap::WrapReverse),
_ => None,
}
}
fn justify_token(justify: JustifyContent) -> &'static str {
match justify {
JustifyContent::FlexStart => "flex_start",
JustifyContent::FlexEnd => "flex_end",
JustifyContent::Center => "center",
JustifyContent::SpaceBetween => "space_between",
JustifyContent::SpaceAround => "space_around",
JustifyContent::SpaceEvenly => "space_evenly",
}
}
fn justify_from_token(token: &str) -> Option<JustifyContent> {
match token {
"flex_start" => Some(JustifyContent::FlexStart),
"flex_end" => Some(JustifyContent::FlexEnd),
"center" => Some(JustifyContent::Center),
"space_between" => Some(JustifyContent::SpaceBetween),
"space_around" => Some(JustifyContent::SpaceAround),
"space_evenly" => Some(JustifyContent::SpaceEvenly),
_ => None,
}
}
fn align_token(align: AlignItems) -> &'static str {
match align {
AlignItems::Stretch => "stretch",
AlignItems::FlexStart => "flex_start",
AlignItems::FlexEnd => "flex_end",
AlignItems::Center => "center",
AlignItems::Baseline => "baseline",
}
}
fn align_from_token(token: &str) -> Option<AlignItems> {
match token {
"stretch" => Some(AlignItems::Stretch),
"flex_start" => Some(AlignItems::FlexStart),
"flex_end" => Some(AlignItems::FlexEnd),
"center" => Some(AlignItems::Center),
"baseline" => Some(AlignItems::Baseline),
_ => None,
}
}
fn flow_direction_token(direction: FlowDirection) -> &'static str {
match direction {
FlowDirection::Horizontal => "horizontal",
FlowDirection::Vertical => "vertical",
}
}
fn flow_direction_from_token(token: &str) -> Option<FlowDirection> {
match token {
"horizontal" => Some(FlowDirection::Horizontal),
"vertical" => Some(FlowDirection::Vertical),
_ => None,
}
}
fn flow_alignment_token(alignment: FlowAlignment) -> &'static str {
match alignment {
FlowAlignment::Start => "start",
FlowAlignment::Center => "center",
FlowAlignment::End => "end",
FlowAlignment::SpaceBetween => "space_between",
FlowAlignment::SpaceAround => "space_around",
}
}
fn flow_alignment_from_token(token: &str) -> Option<FlowAlignment> {
match token {
"start" => Some(FlowAlignment::Start),
"center" => Some(FlowAlignment::Center),
"end" => Some(FlowAlignment::End),
"space_between" => Some(FlowAlignment::SpaceBetween),
"space_around" => Some(FlowAlignment::SpaceAround),
_ => None,
}
}
fn wrap_direction_token(direction: WrapDirection) -> &'static str {
match direction {
WrapDirection::Horizontal => "horizontal",
WrapDirection::Vertical => "vertical",
}
}
fn wrap_direction_from_token(token: &str) -> Option<WrapDirection> {
match token {
"horizontal" => Some(WrapDirection::Horizontal),
"vertical" => Some(WrapDirection::Vertical),
_ => None,
}
}
fn wrap_alignment_token(alignment: WrapAlignment) -> &'static str {
match alignment {
WrapAlignment::Start => "start",
WrapAlignment::Center => "center",
WrapAlignment::End => "end",
WrapAlignment::SpaceBetween => "space_between",
WrapAlignment::SpaceAround => "space_around",
}
}
fn wrap_alignment_from_token(token: &str) -> Option<WrapAlignment> {
match token {
"start" => Some(WrapAlignment::Start),
"center" => Some(WrapAlignment::Center),
"end" => Some(WrapAlignment::End),
"space_between" => Some(WrapAlignment::SpaceBetween),
"space_around" => Some(WrapAlignment::SpaceAround),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn published_item_clicked_resolves_on_every_layout() {
let flex = Flex::new(Rect::new(0, 0, 300, 100));
let flow = FlowLayout::new(Rect::new(0, 0, 300, 100));
let wrap = WrapLayout::new(Rect::new(0, 0, 300, 100));
assert!(flex.event_signal_dyn("item_clicked").is_some(), "flex must resolve");
assert!(flow.event_signal_dyn("item_clicked").is_some(), "flow_layout must resolve");
assert!(wrap.event_signal_dyn("item_clicked").is_some(), "wrap_layout must resolve");
assert!(flex.event_signal_dyn("no_such_event").is_none());
assert!(flow.event_signal_dyn("no_such_event").is_none());
assert!(wrap.event_signal_dyn("no_such_event").is_none());
}
fn spec(label: &str, width: u32, height: u32) -> FlexItemSpec {
FlexItemSpec::new(label, Size::new(width, height))
}
#[test]
fn flex_distributes_weights_in_item_order() {
let mut flex = Flex::new(Rect::new(0, 0, 300, 40));
flex.set_gap(10);
flex.add_item(spec("a", 40, 40));
flex.add_item(spec("b", 40, 40).with_grow(1));
flex.add_item(spec("c", 40, 40).with_grow(1));
let rects = flex.item_rects();
assert_eq!(rects.len(), 3);
assert!(rects[0].x < rects[1].x && rects[1].x < rects[2].x);
assert!(rects[1].width > rects[0].width, "{rects:?}");
assert_eq!(rects[1].width, rects[2].width, "{rects:?}");
}
#[test]
fn flex_reports_the_size_its_items_need() {
let mut flex = Flex::new(Rect::new(0, 0, 300, 40));
flex.set_gap(5);
flex.add_item(spec("a", 30, 20));
flex.add_item(spec("b", 20, 24));
assert_eq!(flex.size_hint(), Size::new(55, 24));
flex.set_direction(FlexDirection::Column);
assert_eq!(flex.size_hint(), Size::new(30, 49));
}
#[test]
fn flex_item_count_is_derived_and_read_only() {
let mut flex = Flex::new(Rect::new(0, 0, 300, 40));
flex.add_item(spec("a", 10, 10));
assert_eq!(flex.get("item_count"), Ok(CapabilityValue::UInt(1)));
assert_eq!(
flex.set("item_count", CapabilityValue::UInt(9)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
}
#[test]
fn flow_layout_wraps_a_long_run_onto_the_next_line() {
let mut flow = FlowLayout::new(Rect::new(0, 0, 100, 200));
flow.set_spacing(0);
flow.set_padding(0);
for _ in 0..4 {
flow.add_item(spec("item", 40, 20));
}
let rects = flow.item_rects();
assert_eq!(rects.len(), 4);
assert_eq!(rects[0].y, rects[1].y, "the first two fit on one run");
assert!(rects[2].y > rects[0].y, "the third starts the next run: {rects:?}");
}
#[test]
fn flow_layout_honours_its_padding() {
let mut flow = FlowLayout::new(Rect::new(0, 0, 200, 200));
flow.set_padding(12);
flow.set_spacing(0);
flow.add_item(spec("item", 20, 10));
let rects = flow.item_rects();
assert_eq!((rects[0].x, rects[0].y), (12, 12));
}
#[test]
fn wrap_layout_publishes_a_separate_run_spacing() {
let mut wrap = WrapLayout::new(Rect::new(0, 0, 100, 200));
wrap.set_spacing(0);
wrap.set_padding(0);
for _ in 0..3 {
wrap.add_item(spec("item", 40, 20));
}
let tight = wrap.item_rects();
wrap.set_run_spacing(Some(10));
let loose = wrap.item_rects();
assert_eq!(tight.len(), 3);
assert!(loose[2].y > tight[2].y || tight[2].y == loose[2].y);
assert_eq!(loose[2].y - tight[2].y, 10, "raising the run spacing moves later runs down");
}
#[test]
fn wrap_layout_run_spacing_null_means_follow_the_item_spacing() {
let mut wrap = WrapLayout::new(Rect::new(0, 0, 100, 200));
assert_eq!(wrap.get("run_spacing"), Ok(CapabilityValue::Null));
wrap.set("run_spacing", CapabilityValue::UInt(4)).expect("writable");
assert_eq!(wrap.run_spacing(), Some(4));
wrap.set("run_spacing", CapabilityValue::Null).expect("nullable");
assert_eq!(wrap.run_spacing(), None);
}
#[test]
fn the_enum_tokens_round_trip() {
for token in ["row", "row_reverse", "column", "column_reverse"] {
assert_eq!(
flex_direction_token(
flex_direction_from_token(token).expect("a published direction")
),
token
);
}
for token in ["no_wrap", "wrap", "wrap_reverse"] {
assert_eq!(
flex_wrap_token(flex_wrap_from_token(token).expect("a published wrap")),
token
);
}
for token in
["flex_start", "flex_end", "center", "space_between", "space_around", "space_evenly"]
{
assert_eq!(
justify_token(justify_from_token(token).expect("a published justify")),
token
);
}
for token in ["stretch", "flex_start", "flex_end", "center", "baseline"] {
assert_eq!(align_token(align_from_token(token).expect("a published align")), token);
}
for token in ["horizontal", "vertical"] {
assert_eq!(
flow_direction_token(flow_direction_from_token(token).expect("a direction")),
token
);
assert_eq!(
wrap_direction_token(wrap_direction_from_token(token).expect("a direction")),
token
);
}
for token in ["start", "center", "end", "space_between", "space_around"] {
assert_eq!(
flow_alignment_token(flow_alignment_from_token(token).expect("an alignment")),
token
);
assert_eq!(
wrap_alignment_token(wrap_alignment_from_token(token).expect("an alignment")),
token
);
}
assert_eq!(flex_direction_from_token("diagonal"), None);
assert_eq!(justify_from_token("middle"), None);
}
#[test]
fn the_distribution_controls_paint_their_items() {
let mut flex = Flex::new(Rect::new(0, 0, 200, 60));
flex.add_item(spec("one", 60, 40));
flex.add_item(spec("two", 60, 40));
let svg = crate::widget::svg::render_to_svg(&mut flex);
assert_eq!(
crate::widget::svg::text_ink_boxes(&svg).len(),
2,
"both item labels must be painted: {svg}"
);
let mut flow = FlowLayout::new(Rect::new(0, 0, 200, 60));
flow.add_item(spec("alpha", 40, 20));
let svg = crate::widget::svg::render_to_svg(&mut flow);
assert_eq!(crate::widget::svg::text_ink_boxes(&svg).len(), 1, "{svg}");
let mut wrap = WrapLayout::new(Rect::new(0, 0, 200, 60));
wrap.add_item(spec("beta", 40, 20));
let svg = crate::widget::svg::render_to_svg(&mut wrap);
assert_eq!(crate::widget::svg::text_ink_boxes(&svg).len(), 1, "{svg}");
}
#[test]
fn an_item_past_the_bottom_edge_is_not_painted() {
let mut flex = Flex::new(Rect::new(0, 0, 100, 40));
flex.set_direction(FlexDirection::Column);
for _ in 0..6 {
flex.add_item(spec("row", 40, 30));
}
let svg = crate::widget::svg::render_to_svg(&mut flex);
assert!(svg.contains("<clipPath"), "the items must be clipped to the control: {svg}");
}
#[test]
fn the_distribution_controls_ignore_events_calmly() {
let mut flex = Flex::new(Rect::new(0, 0, 100, 40));
flex.handle_event(&Event::MouseMove { pos: crate::core::Point::new(3, 3) });
let mut flow = FlowLayout::new(Rect::new(0, 0, 100, 40));
flow.handle_event(&Event::KeyDown((65, 0)));
let mut wrap = WrapLayout::new(Rect::new(0, 0, 100, 40));
wrap.handle_event(&Event::KeyUp((65, 0)));
}
#[test]
fn items_carry_child_ids_and_clicks_name_them() {
let mut flex = Flex::new(Rect::new(0, 0, 120, 30));
flex.add_item(FlexItemSpec::new("a", Size::new(50, 20)).with_widget(7));
flex.add_item(FlexItemSpec::new("b", Size::new(50, 20)));
assert_eq!(flex.items()[0].widget_id(), Some(7));
let rects = flex.item_rects();
assert_eq!(flex.item_at(crate::core::Point::new(rects[0].x + 1, rects[0].y + 1)), Some(0));
assert_eq!(flex.item_at(crate::core::Point::new(500, 500)), None);
let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(usize::MAX));
let sink = std::sync::Arc::clone(&seen);
flex.item_clicked.connect(move |index| {
sink.store(*index, std::sync::atomic::Ordering::SeqCst);
});
flex.handle_event(&Event::MousePress {
pos: crate::core::Point::new(rects[0].x + 1, rects[0].y + 1),
button: 1,
modifiers: 0,
});
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[test]
fn flow_and_wrap_controls_mirror_for_right_to_left() {
let mut flow = FlowLayout::new(Rect::new(0, 0, 100, 40));
flow.set_padding(0);
flow.add_item(FlexItemSpec::new("a", Size::new(30, 10)));
assert_eq!(flow.item_rects()[0].x, 0);
flow.set_text_direction(TextDirection::RightToLeft);
assert!(flow.text_direction().is_right_to_left());
assert_eq!(flow.item_rects()[0].x, 70, "the run begins at the right edge");
let mut wrap = WrapLayout::new(Rect::new(0, 0, 100, 40));
wrap.set_padding(0);
wrap.add_item(FlexItemSpec::new("a", Size::new(30, 10)));
assert_eq!(wrap.item_rects()[0].x, 0);
wrap.set_text_direction(TextDirection::RightToLeft);
assert!(wrap.text_direction().is_right_to_left());
assert_eq!(wrap.item_rects()[0].x, 70, "the wrap run mirrors too");
}
}