use crate::widgets::containers::{Container, Nothing}; use crate::{ Autocomplete, Button, Checkbox, Choice, Color, Drawable, Dropdown, EventCtx, Filler, GeomBatch, GfxCtx, HorizontalAlignment, JustDraw, Menu, PersistentSplit, RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, Spinner, TextBox, VerticalAlignment, WidgetImpl, }; use geom::{Distance, Polygon}; use std::collections::HashSet; use stretch::geometry::{Rect, Size}; use stretch::node::{Node, Stretch}; use stretch::number::Number; use stretch::style::{ AlignItems, Dimension, FlexDirection, FlexWrap, JustifyContent, PositionType, Style, }; pub struct Widget { // TODO pub just for Container. Just move that here? pub(crate) widget: Box, layout: LayoutStyle, pub(crate) rect: ScreenRectangle, bg: Option, id: Option, } struct LayoutStyle { bg_color: Option, // (thickness, color) outline: Option<(f64, Color)>, // If None, as round as possible rounded_radius: Option, style: Style, } // Layouting // TODO Maybe I just want margin, not padding. And maybe more granular controls per side. And to // apply margin to everything in a row or column. // TODO Row and columns feel backwards when using them. impl Widget { pub fn centered(mut self) -> Widget { self.layout.style.align_items = AlignItems::Center; self.layout.style.justify_content = JustifyContent::SpaceAround; self } pub fn centered_horiz(self) -> Widget { Widget::row(vec![self]).centered() } pub fn centered_vert(self) -> Widget { Widget::col(vec![self]).centered() } pub fn centered_cross(mut self) -> Widget { self.layout.style.align_items = AlignItems::Center; self } pub fn evenly_spaced(mut self) -> Widget { self.layout.style.justify_content = JustifyContent::SpaceBetween; self } // This one is really weird. percent_width should be LESS than the max_size_percent given to // the overall Composite, otherwise weird things happen. // Only makes sense for rows/columns. pub fn flex_wrap(mut self, ctx: &EventCtx, percent_width: usize) -> Widget { self.layout.style.size = Size { width: Dimension::Points( (ctx.canvas.window_width * (percent_width as f64) / 100.0) as f32, ), height: Dimension::Undefined, }; self.layout.style.flex_wrap = FlexWrap::Wrap; self.layout.style.justify_content = JustifyContent::SpaceAround; self } // Only for rows/columns. Used to force table columns to line up. pub fn force_width(mut self, ctx: &EventCtx, percent_width: usize) -> Widget { self.layout.style.size.width = Dimension::Points((ctx.canvas.window_width * (percent_width as f64) / 100.0) as f32); self } pub fn bg(mut self, color: Color) -> Widget { self.layout.bg_color = Some(color); self } // Callers have to adjust padding too, probably pub fn outline(mut self, thickness: f64, color: Color) -> Widget { self.layout.outline = Some((thickness, color)); self } pub fn fully_rounded(mut self) -> Widget { self.layout.rounded_radius = None; self } // TODO Alright, this seems to not work on JustDraw's (or at least SVGs). pub fn padding(mut self, pixels: usize) -> Widget { self.layout.style.padding = Rect { start: Dimension::Points(pixels as f32), end: Dimension::Points(pixels as f32), top: Dimension::Points(pixels as f32), bottom: Dimension::Points(pixels as f32), }; self } pub fn margin(mut self, pixels: usize) -> Widget { self.layout.style.margin = Rect { start: Dimension::Points(pixels as f32), end: Dimension::Points(pixels as f32), top: Dimension::Points(pixels as f32), bottom: Dimension::Points(pixels as f32), }; self } pub fn margin_above(mut self, pixels: usize) -> Widget { self.layout.style.margin.top = Dimension::Points(pixels as f32); self } pub fn margin_below(mut self, pixels: usize) -> Widget { self.layout.style.margin.bottom = Dimension::Points(pixels as f32); self } pub fn margin_left(mut self, pixels: usize) -> Widget { self.layout.style.margin.start = Dimension::Points(pixels as f32); self } pub fn margin_right(mut self, pixels: usize) -> Widget { self.layout.style.margin.end = Dimension::Points(pixels as f32); self } pub fn margin_horiz(mut self, pixels: usize) -> Widget { self.layout.style.margin.start = Dimension::Points(pixels as f32); self.layout.style.margin.end = Dimension::Points(pixels as f32); self } pub fn margin_vert(mut self, pixels: usize) -> Widget { self.layout.style.margin.top = Dimension::Points(pixels as f32); self.layout.style.margin.bottom = Dimension::Points(pixels as f32); self } pub fn align_left(mut self) -> Widget { self.layout.style.margin.end = Dimension::Auto; self } pub fn align_right(mut self) -> Widget { self.layout.style.margin = Rect { start: Dimension::Auto, end: Dimension::Undefined, top: Dimension::Undefined, bottom: Dimension::Undefined, }; self } // This doesn't count against the entire container pub fn align_vert_center(mut self) -> Widget { self.layout.style.margin = Rect { start: Dimension::Undefined, end: Dimension::Undefined, top: Dimension::Auto, bottom: Dimension::Auto, }; self } fn abs(mut self, x: f64, y: f64) -> Widget { self.layout.style.position_type = PositionType::Absolute; self.layout.style.position = Rect { start: Dimension::Points(x as f32), end: Dimension::Undefined, top: Dimension::Points(y as f32), bottom: Dimension::Undefined, }; self } pub fn named>(mut self, id: I) -> Widget { assert!(self.id.is_none()); self.id = Some(id.into()); self } } // Convenient?? constructors impl Widget { pub fn new(widget: Box) -> Widget { Widget { widget, layout: LayoutStyle { bg_color: None, outline: None, rounded_radius: Some(5.0), style: Style { ..Default::default() }, }, rect: ScreenRectangle::placeholder(), bg: None, id: None, } } // TODO These are literally just convenient APIs to avoid importing JustDraw. Do we want this // or not? pub fn draw_batch(ctx: &EventCtx, batch: GeomBatch) -> Widget { JustDraw::wrap(ctx, batch) } pub fn draw_svg(ctx: &EventCtx, filename: &str) -> Widget { JustDraw::svg(ctx, filename) } pub fn draw_svg_transform(ctx: &EventCtx, filename: &str, rewrite: RewriteColor) -> Widget { JustDraw::svg_transform(ctx, filename, rewrite) } // TODO Likewise pub fn text_entry(ctx: &EventCtx, prefilled: String, exclusive_focus: bool) -> Widget { // TODO Hardcoded style, max chars Widget::new(Box::new(TextBox::new(ctx, 50, prefilled, exclusive_focus))) } // TODO Likewise pub fn dropdown( ctx: &EventCtx, label: &str, default_value: T, choices: Vec>, ) -> Widget { Widget::new(Box::new(Dropdown::new( ctx, label, default_value, choices, false, ))) .named(label) .outline(ctx.style().outline_thickness, ctx.style().outline_color) } pub fn row(widgets: Vec) -> Widget { Widget::new(Box::new(Container::new(true, widgets))) } pub fn col(widgets: Vec) -> Widget { Widget::new(Box::new(Container::new(false, widgets))) } pub fn nothing() -> Widget { Widget::new(Box::new(Nothing {})) } } // Internals impl Widget { pub(crate) fn draw(&self, g: &mut GfxCtx) { // Don't draw these yet; clipping is still in effect. if self.id == Some("horiz scrollbar".to_string()) || self.id == Some("vert scrollbar".to_string()) { return; } if let Some(ref bg) = self.bg { g.redraw_at(ScreenPt::new(self.rect.x1, self.rect.y1), bg); } self.widget.draw(g); } // Populate a flattened list of Nodes, matching the traversal order fn get_flexbox(&self, parent: Node, stretch: &mut Stretch, nodes: &mut Vec) { if let Some(container) = self.widget.downcast_ref::() { let mut style = self.layout.style.clone(); style.flex_direction = if container.is_row { FlexDirection::Row } else { FlexDirection::Column }; let node = stretch.new_node(style, Vec::new()).unwrap(); nodes.push(node); for widget in &container.members { widget.get_flexbox(node, stretch, nodes); } stretch.add_child(parent, node).unwrap(); return; } else { let mut style = self.layout.style.clone(); style.size = Size { width: Dimension::Points(self.widget.get_dims().width as f32), height: Dimension::Points(self.widget.get_dims().height as f32), }; let node = stretch.new_node(style, Vec::new()).unwrap(); stretch.add_child(parent, node).unwrap(); nodes.push(node); } } fn apply_flexbox( &mut self, stretch: &Stretch, nodes: &mut Vec, dx: f64, dy: f64, scroll_offset: (f64, f64), ctx: &EventCtx, recompute_layout: bool, ) { let result = stretch.layout(nodes.pop().unwrap()).unwrap(); let x: f64 = result.location.x.into(); let y: f64 = result.location.y.into(); let width: f64 = result.size.width.into(); let height: f64 = result.size.height.into(); // Don't scroll the scrollbars let top_left = if self.id == Some("horiz scrollbar".to_string()) || self.id == Some("vert scrollbar".to_string()) { ScreenPt::new(x, y) } else { ScreenPt::new(x + dx - scroll_offset.0, y + dy - scroll_offset.1) }; self.rect = ScreenRectangle::top_left(top_left, ScreenDims::new(width, height)); // Assume widgets don't dynamically change, so we just upload the background once. if (self.bg.is_none() || recompute_layout) && (self.layout.bg_color.is_some() || self.layout.outline.is_some()) { let mut batch = GeomBatch::new(); if let Some(c) = self.layout.bg_color { batch.push( c, Polygon::rounded_rectangle(width, height, self.layout.rounded_radius), ); } if let Some((thickness, color)) = self.layout.outline { batch.push( color, Polygon::rounded_rectangle(width, height, self.layout.rounded_radius) .to_outline(Distance::meters(thickness)), ); } self.bg = Some(ctx.upload(batch)); } if let Some(container) = self.widget.downcast_mut::() { // layout() doesn't return absolute position; it's relative to the container. for widget in &mut container.members { widget.apply_flexbox( stretch, nodes, x + dx, y + dy, scroll_offset, ctx, recompute_layout, ); } } else { self.widget.set_pos(top_left); } } fn get_all_click_actions(&self, actions: &mut HashSet) { if let Some(btn) = self.widget.downcast_ref::