Merge pull request 'merge -- broken but idc, gonna fix' (#1) from master into daddy
Reviewed-on: https://git.ablecorp.us:443/elfein/tuid/pulls/1daddy
commit
291a92d740
|
@ -7,3 +7,4 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
crossterm = "0.22.1"
|
||||
futures = "*"
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
use crate::size::Size;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct BoxConstraints {
|
||||
min: Size,
|
||||
max: Size,
|
||||
}
|
||||
|
||||
impl BoxConstraints {
|
||||
/// An unbounded box constraints object.
|
||||
///
|
||||
/// Can be satisfied by any nonnegative size.
|
||||
pub const BIG: BoxConstraints = BoxConstraints {
|
||||
min: Size::ZERO,
|
||||
max: Size::MAX,
|
||||
};
|
||||
|
||||
/// Create a new box constraints object.
|
||||
///
|
||||
/// Create constraints based on minimum and maximum size.
|
||||
///
|
||||
/// The given sizes are also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: struct.Size.html#method.expand
|
||||
pub fn new(min: Size, max: Size) -> BoxConstraints {
|
||||
BoxConstraints { min, max }
|
||||
}
|
||||
|
||||
/// Create a "tight" box constraints object.
|
||||
///
|
||||
/// A "tight" constraint can only be satisfied by a single size.
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: struct.Size.html#method.expand
|
||||
pub fn tight(size: Size) -> BoxConstraints {
|
||||
let size = size;
|
||||
BoxConstraints {
|
||||
min: size,
|
||||
max: size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a "loose" version of the constraints.
|
||||
///
|
||||
/// Make a version with zero minimum size, but the same maximum size.
|
||||
pub fn loosen(&self) -> BoxConstraints {
|
||||
BoxConstraints {
|
||||
min: Size::ZERO,
|
||||
max: self.max,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamp a given size so that it fits within the constraints.
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: struct.Size.html#method.expand
|
||||
pub fn constrain(&self, size: impl Into<Size>) -> Size {
|
||||
size.into().clamp(self.min, self.max)
|
||||
}
|
||||
|
||||
/// Returns the max size of these constraints.
|
||||
pub fn max(&self) -> Size {
|
||||
self.max
|
||||
}
|
||||
|
||||
/// Returns the min size of these constraints.
|
||||
pub fn min(&self) -> Size {
|
||||
self.min
|
||||
}
|
||||
|
||||
/// Whether there is an upper bound on the width.
|
||||
pub fn is_width_bounded(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether there is an upper bound on the height.
|
||||
pub fn is_height_bounded(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Shrink min and max constraints by size
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: struct.Size.html#method.expand
|
||||
pub fn shrink(&self, diff: impl Into<Size>) -> BoxConstraints {
|
||||
let diff = diff.into();
|
||||
let min = Size::new(
|
||||
(self.min().width - diff.width).max(0),
|
||||
(self.min().height - diff.height).max(0),
|
||||
);
|
||||
let max = Size::new(
|
||||
(self.max().width - diff.width).max(0),
|
||||
(self.max().height - diff.height).max(0),
|
||||
);
|
||||
|
||||
BoxConstraints::new(min, max)
|
||||
}
|
||||
|
||||
/// Test whether these constraints contain the given `Size`.
|
||||
pub fn contains(&self, size: impl Into<Size>) -> bool {
|
||||
let size = size.into();
|
||||
(self.min.width <= size.width && size.width <= self.max.width)
|
||||
&& (self.min.height <= size.height && size.height <= self.max.height)
|
||||
}
|
||||
|
||||
// pub fn constrain_aspect_ratio(&self, aspect_ratio: usize, width: usize) -> Size {
|
||||
// // Minimizing/maximizing based on aspect ratio seems complicated, but in reality everything
|
||||
// // is linear, so the amount of work to do is low.
|
||||
// let ideal_size = Size {
|
||||
// width,
|
||||
// height: width * aspect_ratio,
|
||||
// };
|
||||
|
||||
// // Firstly check if we can simply return the exact requested
|
||||
// if self.contains(ideal_size) {
|
||||
// return ideal_size;
|
||||
// }
|
||||
|
||||
// // Then we check if any `Size`s with our desired aspect ratio are inside the constraints.
|
||||
// // TODO this currently outputs garbage when things are < 0.
|
||||
// let min_w_min_h = self.min.height / self.min.width;
|
||||
// let max_w_min_h = self.min.height / self.max.width;
|
||||
// let min_w_max_h = self.max.height / self.min.width;
|
||||
// let max_w_max_h = self.max.height / self.max.width;
|
||||
|
||||
// // When the aspect ratio line crosses the constraints, the closest point must be one of the
|
||||
// // two points where the aspect ratio enters/exits.
|
||||
|
||||
// // When the aspect ratio line doesn't intersect the box of possible sizes, the closest
|
||||
// // point must be either (max width, min height) or (max height, min width). So all we have
|
||||
// // to do is check which one of these has the closest aspect ratio.
|
||||
|
||||
// // Check each possible intersection (or not) of the aspect ratio line with the constraints
|
||||
// if aspect_ratio > min_w_max_h {
|
||||
// // outside max height min width
|
||||
// Size {
|
||||
// width: self.min.width,
|
||||
// height: self.max.height,
|
||||
// }
|
||||
// } else if aspect_ratio < max_w_min_h {
|
||||
// // outside min height max width
|
||||
// Size {
|
||||
// width: self.max.width,
|
||||
// height: self.min.height,
|
||||
// }
|
||||
// } else if aspect_ratio > min_w_min_h {
|
||||
// // hits the constraints on the min width line
|
||||
// if width < self.min.width {
|
||||
// // we take the point on the min width
|
||||
// Size {
|
||||
// width: self.min.width,
|
||||
// height: self.min.width * aspect_ratio,
|
||||
// }
|
||||
// } else if aspect_ratio < max_w_max_h {
|
||||
// // exits through max.width
|
||||
// Size {
|
||||
// width: self.max.width,
|
||||
// height: self.max.width * aspect_ratio,
|
||||
// }
|
||||
// } else {
|
||||
// // exits through max.height
|
||||
// Size {
|
||||
// width: self.max.height * aspect_ratio.recip(),
|
||||
// height: self.max.height,
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // final case is where we hit constraints on the min height line
|
||||
// if width < self.min.width {
|
||||
// // take the point on the min height
|
||||
// Size {
|
||||
// width: self.min.height * aspect_ratio.recip(),
|
||||
// height: self.min.height,
|
||||
// }
|
||||
// } else if aspect_ratio > max_w_max_h {
|
||||
// // exit thru max height
|
||||
// Size {
|
||||
// width: self.max.height * aspect_ratio.recip(),
|
||||
// height: self.max.height,
|
||||
// }
|
||||
// } else {
|
||||
// // exit thru max width
|
||||
// Size {
|
||||
// width: self.max.width,
|
||||
// height: self.max.width * aspect_ratio,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for BoxConstraints {
|
||||
fn from(s: (u16, u16)) -> Self {
|
||||
Self {
|
||||
min: Size::ZERO,
|
||||
max: s.into(),
|
||||
}
|
||||
}
|
||||
}
|
147
src/lib.rs
147
src/lib.rs
|
@ -1,55 +1,116 @@
|
|||
use crossterm::{
|
||||
cursor, queue,
|
||||
style::{self, Stylize},
|
||||
terminal::{Clear, ClearType, size},
|
||||
ExecutableCommand, QueueableCommand, Result as CTRes,
|
||||
cursor, queue,
|
||||
style::{self, Stylize},
|
||||
terminal::{size, Clear, ClearType},
|
||||
ExecutableCommand, QueueableCommand, Result as CTRes,
|
||||
};
|
||||
use std::io::{stdout, Stdout, Write};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{stdout, Stdout, Write},
|
||||
ops::DerefMut,
|
||||
};
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
mod box_constraints;
|
||||
mod point;
|
||||
mod rect;
|
||||
mod size;
|
||||
mod theme;
|
||||
mod vec2;
|
||||
mod widget;
|
||||
use widget::*;
|
||||
|
||||
pub struct Size {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
fn from(s: (u16, u16)) -> Self {
|
||||
Self {
|
||||
a: s.0 as usize,
|
||||
b: s.1 as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Point {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
pub use box_constraints::*;
|
||||
pub use point::*;
|
||||
pub use rect::*;
|
||||
pub use size::*;
|
||||
pub use vec2::*;
|
||||
pub use widget::*;
|
||||
|
||||
pub trait Data {}
|
||||
|
||||
impl<T> Data for Arc<T> {}
|
||||
|
||||
struct Window<'a, T: Data> {
|
||||
out: Stdout,
|
||||
root_widget: Box<&'a dyn Widget<T>>,
|
||||
pub struct DataWrapper<T> {
|
||||
changed: bool,
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<'a, T: Data> Window<'a, T> {
|
||||
pub fn new(widget: &'a dyn Widget<T>) -> Self {
|
||||
Self {
|
||||
out: stdout(),
|
||||
root_widget: Box::new(widget),
|
||||
}
|
||||
}
|
||||
pub fn draw(&mut self) -> CTRes<()> {
|
||||
queue![self.out, Clear(ClearType::All)]?;
|
||||
let terminal_size = size();
|
||||
self.root_widget.layout(terminal_size.into());
|
||||
self.out.flush();
|
||||
Ok(())
|
||||
}
|
||||
impl<T: Data> DataWrapper<T> {
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
changed: true,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DataWrapper<T> {
|
||||
fn changed(&mut self) -> bool {
|
||||
if self.changed {
|
||||
self.changed = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for DataWrapper<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for DataWrapper<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.changed = true;
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Window<T: Data> {
|
||||
out: Stdout,
|
||||
buf: Vec<char>,
|
||||
data: DataWrapper<T>,
|
||||
root_widget: Box<dyn Widget<T>>,
|
||||
}
|
||||
|
||||
impl<'a, T: Data> Window<T> {
|
||||
pub fn new<W>(data: T, root: W) -> Self
|
||||
where
|
||||
W: Widget<T> + 'static,
|
||||
{
|
||||
Self {
|
||||
out: stdout(),
|
||||
buf: vec![],
|
||||
data: DataWrapper::new(data),
|
||||
root_widget: Box::new(root),
|
||||
}
|
||||
}
|
||||
fn draw(&mut self) -> CTRes<()> {
|
||||
self.root_widget.event(&mut self.data);
|
||||
self.root_widget.update(&self.data);
|
||||
if self.data.changed() {
|
||||
queue![self.out, Clear(ClearType::All)]?;
|
||||
self.out.flush()?;
|
||||
let terminal_size = size()?;
|
||||
self.buf = vec![' '; terminal_size.0 as usize * terminal_size.1 as usize];
|
||||
self.root_widget.deref_mut().layout(&terminal_size.into());
|
||||
self
|
||||
.root_widget
|
||||
.deref_mut()
|
||||
.paint(&mut self.buf, &terminal_size.into());
|
||||
for ch in &self.buf {
|
||||
print!["{}", ch];
|
||||
}
|
||||
self.out.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn run(&mut self) -> CTRes<()> {
|
||||
loop {
|
||||
self.draw()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
use std::ops::Deref;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tuid::Data;
|
||||
use tuid::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ExampleAppState {
|
||||
pub value: i32,
|
||||
}
|
||||
|
||||
impl Data for ExampleAppState {}
|
||||
|
||||
fn make_ui() -> impl Widget<Arc<Mutex<ExampleAppState>>> {
|
||||
let t1 = Text::new(Box::new(
|
||||
|data: &DataWrapper<Arc<Mutex<ExampleAppState>>>| {
|
||||
let s = format!["Hello, {}!", data.deref().lock().unwrap().value];
|
||||
s
|
||||
},
|
||||
));
|
||||
|
||||
// let flex = Flex::new();
|
||||
|
||||
// flex
|
||||
|
||||
t1
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let main_state = Arc::new(Mutex::new(ExampleAppState::default()));
|
||||
let mut window = Window::new(main_state, make_ui());
|
||||
window.run().unwrap();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Point {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Rect {
|
||||
pub x0: usize,
|
||||
pub y0: usize,
|
||||
pub x1: usize,
|
||||
pub y1: usize,
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Size {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub const ZERO: Self = Self {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
pub const MAX: Self = Self {
|
||||
width: usize::MAX,
|
||||
height: usize::MAX,
|
||||
};
|
||||
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
pub fn clamp(&self, min: Size, max: Size) -> Size {
|
||||
Self {
|
||||
width: self.width.clamp(min.width, max.width),
|
||||
height: self.height.clamp(min.height, max.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
fn from(s: (u16, u16)) -> Self {
|
||||
Self {
|
||||
width: s.0 as usize,
|
||||
height: s.1 as usize,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub const WIDGET_PADDING_VERTICAL: usize = 1;
|
||||
pub const WIDGET_PADDING_HORIZONTAL: usize = 1;
|
|
@ -0,0 +1,5 @@
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Vec2 {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
use crate::{rect::Rect, size::Size, vec2::Vec2, Data, Point, box_constraints::BoxConstraints};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Axis {
|
||||
/// The x axis
|
||||
Horizontal,
|
||||
/// The y axis
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
/// Get the axis perpendicular to this one.
|
||||
pub fn cross(self) -> Axis {
|
||||
match self {
|
||||
Axis::Horizontal => Axis::Vertical,
|
||||
Axis::Vertical => Axis::Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract from the argument the magnitude along this axis
|
||||
pub fn major(self, coords: Size) -> usize {
|
||||
match self {
|
||||
Axis::Horizontal => coords.width,
|
||||
Axis::Vertical => coords.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract from the argument the magnitude along the perpendicular axis
|
||||
pub fn minor(self, coords: Size) -> usize {
|
||||
self.cross().major(coords)
|
||||
}
|
||||
|
||||
/// Extract the extent of the argument in this axis as a pair.
|
||||
pub fn major_span(self, rect: Rect) -> (usize, usize) {
|
||||
match self {
|
||||
Axis::Horizontal => (rect.x0, rect.x1),
|
||||
Axis::Vertical => (rect.y0, rect.y1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the extent of the argument in the minor axis as a pair.
|
||||
pub fn minor_span(self, rect: Rect) -> (usize, usize) {
|
||||
self.cross().major_span(rect)
|
||||
}
|
||||
|
||||
/// Extract the coordinate locating the argument with respect to this axis.
|
||||
pub fn major_pos(self, pos: Point) -> usize {
|
||||
match self {
|
||||
Axis::Horizontal => pos.x,
|
||||
Axis::Vertical => pos.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the coordinate locating the argument with respect to this axis.
|
||||
pub fn major_vec(self, vec: Vec2) -> usize {
|
||||
match self {
|
||||
Axis::Horizontal => vec.x,
|
||||
Axis::Vertical => vec.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the coordinate locating the argument with respect to the perpendicular axis.
|
||||
pub fn minor_pos(self, pos: Point) -> usize {
|
||||
self.cross().major_pos(pos)
|
||||
}
|
||||
|
||||
/// Extract the coordinate locating the argument with respect to the perpendicular axis.
|
||||
pub fn minor_vec(self, vec: Vec2) -> usize {
|
||||
self.cross().major_vec(vec)
|
||||
}
|
||||
|
||||
/// Arrange the major and minor measurements with respect to this axis such that it forms
|
||||
/// an (x, y) pair.
|
||||
pub fn pack(self, major: usize, minor: usize) -> (usize, usize) {
|
||||
match self {
|
||||
Axis::Horizontal => (major, minor),
|
||||
Axis::Vertical => (minor, major),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate constraints with new values on the major axis.
|
||||
pub(crate) fn constraints(
|
||||
self,
|
||||
bc: &BoxConstraints,
|
||||
min_major: usize,
|
||||
major: usize,
|
||||
) -> BoxConstraints {
|
||||
match self {
|
||||
Axis::Horizontal => BoxConstraints::new(
|
||||
Size::new(min_major, bc.min().height),
|
||||
Size::new(major, bc.max().height),
|
||||
),
|
||||
Axis::Vertical => BoxConstraints::new(
|
||||
Size::new(bc.min().width, min_major),
|
||||
Size::new(bc.max().width, major),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data for Axis {}
|
|
@ -0,0 +1,36 @@
|
|||
use crate::widget::{Widget, WidgetPod};
|
||||
|
||||
use super::cross_axis_alignment::CrossAxisAlignment;
|
||||
|
||||
pub enum Child<T> {
|
||||
Fixed {
|
||||
widget: WidgetPod<T, Box<dyn Widget<T>>>,
|
||||
alignment: Option<CrossAxisAlignment>,
|
||||
},
|
||||
Flex {
|
||||
widget: WidgetPod<T, Box<dyn Widget<T>>>,
|
||||
alignment: Option<CrossAxisAlignment>,
|
||||
flex: f64,
|
||||
},
|
||||
FixedSpacer(
|
||||
// KeyOrValue<f64>,
|
||||
usize,
|
||||
usize,
|
||||
),
|
||||
FlexedSpacer(f64, f64),
|
||||
}
|
||||
|
||||
impl<T> Child<T> {
|
||||
fn widget_mut(&mut self) -> Option<&mut WidgetPod<T, Box<dyn Widget<T>>>> {
|
||||
match self {
|
||||
Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn widget(&self) -> Option<&WidgetPod<T, Box<dyn Widget<T>>>> {
|
||||
match self {
|
||||
Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
use crate::Data;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CrossAxisAlignment {
|
||||
/// Top or leading.
|
||||
///
|
||||
/// In a vertical container, widgets are top aligned. In a horiziontal
|
||||
/// container, their leading edges are aligned.
|
||||
Start,
|
||||
/// Widgets are centered in the container.
|
||||
Center,
|
||||
/// Bottom or trailing.
|
||||
///
|
||||
/// In a vertical container, widgets are bottom aligned. In a horiziontal
|
||||
/// container, their trailing edges are aligned.
|
||||
End,
|
||||
/// Align on the baseline.
|
||||
///
|
||||
/// In a horizontal container, widgets are aligned along the calculated
|
||||
/// baseline. In a vertical container, this is equivalent to `End`.
|
||||
///
|
||||
/// The calculated baseline is the maximum baseline offset of the children.
|
||||
Baseline,
|
||||
/// Fill the available space.
|
||||
///
|
||||
/// The size on this axis is the size of the largest widget;
|
||||
/// other widgets must fill that space.
|
||||
Fill,
|
||||
}
|
||||
|
||||
impl CrossAxisAlignment {
|
||||
/// Given the difference between the size of the container and the size
|
||||
/// of the child (on their minor axis) return the necessary offset for
|
||||
/// this alignment.
|
||||
fn align(self, val: f64) -> f64 {
|
||||
match self {
|
||||
CrossAxisAlignment::Start => 0.0,
|
||||
// in vertical layout, baseline is equivalent to center
|
||||
CrossAxisAlignment::Center | CrossAxisAlignment::Baseline => (val / 2.0).round(),
|
||||
CrossAxisAlignment::End => val,
|
||||
CrossAxisAlignment::Fill => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data for CrossAxisAlignment {}
|
|
@ -0,0 +1,37 @@
|
|||
use super::cross_axis_alignment::CrossAxisAlignment;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct FlexParams {
|
||||
pub(super) flex: f64,
|
||||
pub(super) alignment: Option<CrossAxisAlignment>,
|
||||
}
|
||||
|
||||
impl FlexParams {
|
||||
/// Create custom `FlexParams` with a specific `flex_factor` and an optional
|
||||
/// [`CrossAxisAlignment`].
|
||||
///
|
||||
/// You likely only need to create these manually if you need to specify
|
||||
/// a custom alignment; if you only need to use a custom `flex_factor` you
|
||||
/// can pass an `f64` to any of the functions that take `FlexParams`.
|
||||
///
|
||||
/// By default, the widget uses the alignment of its parent [`Flex`] container.
|
||||
pub fn new(flex: f64, alignment: impl Into<Option<CrossAxisAlignment>>) -> Self {
|
||||
#[cfg(debug)]
|
||||
if flex <= 0.0 {
|
||||
panic!("Flex value should be > 0.0. Flex given was: {}", flex);
|
||||
}
|
||||
|
||||
let flex = flex.max(0.0);
|
||||
|
||||
FlexParams {
|
||||
flex,
|
||||
alignment: alignment.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for FlexParams {
|
||||
fn from(flex: f64) -> FlexParams {
|
||||
FlexParams::new(flex, None)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
use crate::Data;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MainAxisAlignment {
|
||||
/// Top or leading.
|
||||
///
|
||||
/// Children are aligned with the top or leading edge, without padding.
|
||||
Start,
|
||||
/// Children are centered, without padding.
|
||||
Center,
|
||||
/// Bottom or trailing.
|
||||
///
|
||||
/// Children are aligned with the bottom or trailing edge, without padding.
|
||||
End,
|
||||
/// Extra space is divided evenly between each child.
|
||||
SpaceBetween,
|
||||
/// Extra space is divided evenly between each child, as well as at the ends.
|
||||
SpaceEvenly,
|
||||
/// Space between each child, with less at the start and end.
|
||||
///
|
||||
/// This divides space such that each child is separated by `n` units,
|
||||
/// and the start and end have `n/2` units of padding.
|
||||
SpaceAround,
|
||||
}
|
||||
|
||||
impl Data for MainAxisAlignment {}
|
|
@ -0,0 +1,530 @@
|
|||
// This is pretty much all stolen from linebender/druid.
|
||||
// They have the license on their github repo.
|
||||
// I didn't include it here because it might change-
|
||||
// just go ask them what the license says and don't bother me.
|
||||
|
||||
use crate::{box_constraints::BoxConstraints, Data, Size, Widget};
|
||||
|
||||
use super::WidgetPod;
|
||||
|
||||
mod axis;
|
||||
mod child;
|
||||
mod cross_axis_alignment;
|
||||
mod flex_params;
|
||||
mod main_axis_alignment;
|
||||
mod spacing;
|
||||
use axis::*;
|
||||
use child::*;
|
||||
use cross_axis_alignment::*;
|
||||
use flex_params::*;
|
||||
use main_axis_alignment::*;
|
||||
use spacing::*;
|
||||
|
||||
pub struct Flex<T> {
|
||||
direction: Axis,
|
||||
cross_alignment: CrossAxisAlignment,
|
||||
main_alignment: MainAxisAlignment,
|
||||
fill_major_axis: bool,
|
||||
children: Vec<Child<T>>,
|
||||
}
|
||||
|
||||
impl<T: Data> Flex<T> {
|
||||
/// Create a new Flex oriented along the provided axis.
|
||||
pub fn for_axis(axis: Axis) -> Self {
|
||||
Flex {
|
||||
direction: axis,
|
||||
children: Vec::new(),
|
||||
cross_alignment: CrossAxisAlignment::Center,
|
||||
main_alignment: MainAxisAlignment::Start,
|
||||
fill_major_axis: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new horizontal stack.
|
||||
///
|
||||
/// The child widgets are laid out horizontally, from left to right.
|
||||
///
|
||||
pub fn row() -> Self {
|
||||
Self::for_axis(Axis::Horizontal)
|
||||
}
|
||||
|
||||
/// Create a new vertical stack.
|
||||
///
|
||||
/// The child widgets are laid out vertically, from top to bottom.
|
||||
pub fn column() -> Self {
|
||||
Self::for_axis(Axis::Vertical)
|
||||
}
|
||||
|
||||
/// Builder-style method for specifying the childrens' [`CrossAxisAlignment`].
|
||||
///
|
||||
/// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html
|
||||
pub fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
|
||||
self.cross_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method for specifying the childrens' [`MainAxisAlignment`].
|
||||
///
|
||||
/// [`MainAxisAlignment`]: enum.MainAxisAlignment.html
|
||||
pub fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
|
||||
self.main_alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method for setting whether the container must expand
|
||||
/// to fill the available space on its main axis.
|
||||
///
|
||||
/// If any children have flex then this container will expand to fill all
|
||||
/// available space on its main axis; But if no children are flex,
|
||||
/// this flag determines whether or not the container should shrink to fit,
|
||||
/// or must expand to fill.
|
||||
///
|
||||
/// If it expands, and there is extra space left over, that space is
|
||||
/// distributed in accordance with the [`MainAxisAlignment`].
|
||||
///
|
||||
/// The default value is `false`.
|
||||
///
|
||||
/// [`MainAxisAlignment`]: enum.MainAxisAlignment.html
|
||||
pub fn must_fill_main_axis(mut self, fill: bool) -> Self {
|
||||
self.fill_major_axis = fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style variant of `add_child`.
|
||||
///
|
||||
/// Convenient for assembling a group of widgets in a single expression.
|
||||
pub fn with_child(mut self, child: impl Widget<T> + 'static) -> Self {
|
||||
self.add_child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method to add a flexible child to the container.
|
||||
///
|
||||
/// This method is used when you need more control over the behaviour
|
||||
/// of the widget you are adding. In the general case, this likely
|
||||
/// means giving that child a 'flex factor', but it could also mean
|
||||
/// giving the child a custom [`CrossAxisAlignment`], or a combination
|
||||
/// of the two.
|
||||
///
|
||||
/// This function takes a child widget and [`FlexParams`]; importantly
|
||||
/// you can pass in a float as your [`FlexParams`] in most cases.
|
||||
///
|
||||
/// For the non-builder varient, see [`add_flex_child`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use druid::widget::{Flex, FlexParams, Label, Slider, CrossAxisAlignment};
|
||||
///
|
||||
/// let my_row = Flex::row()
|
||||
/// .with_flex_child(Slider::new(), 1.0)
|
||||
/// .with_flex_child(Slider::new(), FlexParams::new(1.0, CrossAxisAlignment::End));
|
||||
/// ```
|
||||
///
|
||||
/// [`FlexParams`]: struct.FlexParams.html
|
||||
/// [`add_flex_child`]: #method.add_flex_child
|
||||
/// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html
|
||||
pub fn with_flex_child(
|
||||
mut self,
|
||||
child: impl Widget<T> + 'static,
|
||||
params: impl Into<FlexParams>,
|
||||
) -> Self {
|
||||
self.add_flex_child(child, params);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method to add a spacer widget with a standard size.
|
||||
///
|
||||
/// The actual value of this spacer depends on whether this container is
|
||||
/// a row or column, as well as theme settings.
|
||||
pub fn with_default_spacer(mut self) -> Self {
|
||||
self.add_default_spacer();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method for adding a fixed-size spacer to the container.
|
||||
///
|
||||
/// If you are laying out standard controls in this container, you should
|
||||
/// generally prefer to use [`add_default_spacer`].
|
||||
///
|
||||
/// [`add_default_spacer`]: #method.add_default_spacer
|
||||
pub fn with_spacer(mut self, len: usize) -> Self {
|
||||
self.add_spacer(len);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder-style method for adding a `flex` spacer to the container.
|
||||
pub fn with_flex_spacer(mut self, flex: f64) -> Self {
|
||||
self.add_flex_spacer(flex);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the childrens' [`CrossAxisAlignment`].
|
||||
///
|
||||
/// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html
|
||||
pub fn set_cross_axis_alignment(&mut self, alignment: CrossAxisAlignment) {
|
||||
self.cross_alignment = alignment;
|
||||
}
|
||||
|
||||
/// Set the childrens' [`MainAxisAlignment`].
|
||||
///
|
||||
/// [`MainAxisAlignment`]: enum.MainAxisAlignment.html
|
||||
pub fn set_main_axis_alignment(&mut self, alignment: MainAxisAlignment) {
|
||||
self.main_alignment = alignment;
|
||||
}
|
||||
|
||||
/// Set whether the container must expand to fill the available space on
|
||||
/// its main axis.
|
||||
pub fn set_must_fill_main_axis(&mut self, fill: bool) {
|
||||
self.fill_major_axis = fill;
|
||||
}
|
||||
|
||||
/// Add a non-flex child widget.
|
||||
///
|
||||
/// See also [`with_child`].
|
||||
///
|
||||
/// [`with_child`]: Flex::with_child
|
||||
pub fn add_child(&mut self, child: impl Widget<T> + 'static) {
|
||||
let child = Child::Fixed {
|
||||
widget: WidgetPod::new(Box::new(child)),
|
||||
alignment: None,
|
||||
};
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
/// Add a flexible child widget.
|
||||
///
|
||||
/// This method is used when you need more control over the behaviour
|
||||
/// of the widget you are adding. In the general case, this likely
|
||||
/// means giving that child a 'flex factor', but it could also mean
|
||||
/// giving the child a custom [`CrossAxisAlignment`], or a combination
|
||||
/// of the two.
|
||||
///
|
||||
/// This function takes a child widget and [`FlexParams`]; importantly
|
||||
/// you can pass in a float as your [`FlexParams`] in most cases.
|
||||
///
|
||||
/// For the builder-style varient, see [`with_flex_child`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use druid::widget::{Flex, FlexParams, Label, Slider, CrossAxisAlignment};
|
||||
///
|
||||
/// let mut my_row = Flex::row();
|
||||
/// my_row.add_flex_child(Slider::new(), 1.0);
|
||||
/// my_row.add_flex_child(Slider::new(), FlexParams::new(1.0, CrossAxisAlignment::End));
|
||||
/// ```
|
||||
///
|
||||
/// [`with_flex_child`]: Flex::with_flex_child
|
||||
pub fn add_flex_child(
|
||||
&mut self,
|
||||
child: impl Widget<T> + 'static,
|
||||
params: impl Into<FlexParams>,
|
||||
) {
|
||||
let params = params.into();
|
||||
let child = if params.flex > 0.0 {
|
||||
Child::Flex {
|
||||
widget: WidgetPod::new(Box::new(child)),
|
||||
alignment: params.alignment,
|
||||
flex: params.flex,
|
||||
}
|
||||
} else {
|
||||
// tracing::warn!("Flex value should be > 0.0. To add a non-flex child use the add_child or with_child methods.\nSee the docs for more information: https://docs.rs/druid/0.7.0/druid/widget/struct.Flex.html");
|
||||
Child::Fixed {
|
||||
widget: WidgetPod::new(Box::new(child)),
|
||||
alignment: None,
|
||||
}
|
||||
};
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
/// Add a spacer widget with a standard size.
|
||||
///
|
||||
/// The actual value of this spacer depends on whether this container is
|
||||
/// a row or column, as well as theme settings.
|
||||
pub fn add_default_spacer(&mut self) {
|
||||
let key = match self.direction {
|
||||
Axis::Vertical => crate::theme::WIDGET_PADDING_VERTICAL,
|
||||
Axis::Horizontal => crate::theme::WIDGET_PADDING_HORIZONTAL,
|
||||
};
|
||||
self.add_spacer(key);
|
||||
}
|
||||
|
||||
/// Add an empty spacer widget with the given size.
|
||||
///
|
||||
/// If you are laying out standard controls in this container, you should
|
||||
/// generally prefer to use [`add_default_spacer`].
|
||||
///
|
||||
/// [`add_default_spacer`]: Flex::add_default_spacer
|
||||
pub fn add_spacer(&mut self, len: usize) {
|
||||
let new_child = Child::FixedSpacer(len, 0);
|
||||
self.children.push(new_child);
|
||||
}
|
||||
|
||||
/// Add an empty spacer widget with a specific `flex` factor.
|
||||
pub fn add_flex_spacer(&mut self, flex: f64) {
|
||||
let flex = if flex >= 0.0 {
|
||||
flex
|
||||
} else {
|
||||
debug_assert!(
|
||||
flex >= 0.0,
|
||||
"flex value for space should be greater than equal to 0, received: {}",
|
||||
flex
|
||||
);
|
||||
// tracing::warn!("Provided flex value was less than 0: {}", flex);
|
||||
0.0
|
||||
};
|
||||
let new_child = Child::FlexedSpacer(flex, 0.0);
|
||||
self.children.push(new_child);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Data> Widget<T> for Flex<T> {
|
||||
fn event(&mut self, data: ) {
|
||||
for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
|
||||
child.event(ctx, event, data, env);
|
||||
}
|
||||
}
|
||||
|
||||
// #[instrument(name = "Flex", level = "trace", skip(self, ctx, event, data, env))]
|
||||
// fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
|
||||
// for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
|
||||
// child.lifecycle(ctx, event, data, env);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
|
||||
for child in self.children.iter_mut() {
|
||||
match child {
|
||||
Child::Fixed { widget, .. } | Child::Flex { widget, .. } => {
|
||||
widget.update(ctx, data, env)
|
||||
}
|
||||
Child::FixedSpacer(key_or_val, _) if ctx.env_key_changed(key_or_val) => {
|
||||
ctx.request_layout()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self, bc: &BoxConstraints) -> Size {
|
||||
// bc.debug_check("Flex");
|
||||
// we loosen our constraints when passing to children.
|
||||
let loosened_bc = bc.loosen();
|
||||
|
||||
// minor-axis values for all children
|
||||
let mut minor = self.direction.minor(bc.min());
|
||||
// these two are calculated but only used if we're baseline aligned
|
||||
let mut max_above_baseline = 0;
|
||||
let mut max_below_baseline = 0;
|
||||
let mut any_use_baseline = self.cross_alignment == CrossAxisAlignment::Baseline;
|
||||
|
||||
// Measure non-flex children.
|
||||
let mut major_non_flex = 0;
|
||||
let mut flex_sum = 0.0;
|
||||
for child in &mut self.children {
|
||||
match child {
|
||||
Child::Fixed { widget, alignment } => {
|
||||
any_use_baseline &= *alignment == Some(CrossAxisAlignment::Baseline);
|
||||
|
||||
let child_bc = self.direction.constraints(&loosened_bc, 0, usize::MAX);
|
||||
let child_size = widget.layout(&child_bc);
|
||||
let baseline_offset = 0;
|
||||
|
||||
major_non_flex += self.direction.major(child_size);
|
||||
minor = minor.max(self.direction.minor(child_size));
|
||||
max_above_baseline = max_above_baseline.max(child_size.height - baseline_offset);
|
||||
max_below_baseline = max_below_baseline.max(baseline_offset);
|
||||
}
|
||||
Child::FixedSpacer(kv, calculated_siz) => {
|
||||
*calculated_siz = *kv;
|
||||
major_non_flex += *calculated_siz;
|
||||
}
|
||||
Child::Flex { flex, .. } | Child::FlexedSpacer(flex, _) => flex_sum += *flex,
|
||||
}
|
||||
}
|
||||
|
||||
let total_major = self.direction.major(bc.max());
|
||||
let remaining = (total_major - major_non_flex).max(0.0);
|
||||
let mut remainder: f64 = 0.0;
|
||||
|
||||
let mut major_flex: f64 = 0.0;
|
||||
let px_per_flex = remaining / flex_sum;
|
||||
// Measure flex children.
|
||||
for child in &mut self.children {
|
||||
match child {
|
||||
Child::Flex { widget, flex, .. } => {
|
||||
let desired_major = (*flex) * px_per_flex + remainder;
|
||||
let actual_major = desired_major.round();
|
||||
remainder = desired_major - actual_major;
|
||||
|
||||
let child_bc = self.direction.constraints(&loosened_bc, 0.0, actual_major);
|
||||
let child_size = widget.layout(ctx, &child_bc, data, env);
|
||||
let baseline_offset = widget.baseline_offset();
|
||||
|
||||
major_flex += self.direction.major(child_size).expand();
|
||||
minor = minor.max(self.direction.minor(child_size).expand());
|
||||
max_above_baseline = max_above_baseline.max(child_size.height - baseline_offset);
|
||||
max_below_baseline = max_below_baseline.max(baseline_offset);
|
||||
}
|
||||
Child::FlexedSpacer(flex, calculated_size) => {
|
||||
let desired_major = (*flex) * px_per_flex + remainder;
|
||||
*calculated_size = desired_major.round();
|
||||
remainder = desired_major - *calculated_size;
|
||||
major_flex += *calculated_size;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// figure out if we have extra space on major axis, and if so how to use it
|
||||
let extra = if self.fill_major_axis {
|
||||
(remaining - major_flex).max(0.0)
|
||||
} else {
|
||||
// if we are *not* expected to fill our available space this usually
|
||||
// means we don't have any extra, unless dictated by our constraints.
|
||||
(self.direction.major(bc.min()) - (major_non_flex + major_flex)).max(0.0)
|
||||
};
|
||||
|
||||
let mut spacing = Spacing::new(self.main_alignment, extra, self.children.len());
|
||||
|
||||
// the actual size needed to tightly fit the children on the minor axis.
|
||||
// Unlike the 'minor' var, this ignores the incoming constraints.
|
||||
let minor_dim = match self.direction {
|
||||
Axis::Horizontal if any_use_baseline => max_below_baseline + max_above_baseline,
|
||||
_ => minor,
|
||||
};
|
||||
|
||||
let extra_height = minor - minor_dim.min(minor);
|
||||
|
||||
let mut major = spacing.next().unwrap_or(0.);
|
||||
let mut child_paint_rect = Rect::ZERO;
|
||||
|
||||
for child in &mut self.children {
|
||||
match child {
|
||||
Child::Fixed { widget, alignment }
|
||||
| Child::Flex {
|
||||
widget, alignment, ..
|
||||
} => {
|
||||
let child_size = widget.layout_rect().size();
|
||||
let alignment = alignment.unwrap_or(self.cross_alignment);
|
||||
let child_minor_offset = match alignment {
|
||||
// This will ignore baseline alignment if it is overridden on children,
|
||||
// but is not the default for the container. Is this okay?
|
||||
CrossAxisAlignment::Baseline if matches!(self.direction, Axis::Horizontal) => {
|
||||
let child_baseline = widget.baseline_offset();
|
||||
let child_above_baseline = child_size.height - child_baseline;
|
||||
extra_height + (max_above_baseline - child_above_baseline)
|
||||
}
|
||||
CrossAxisAlignment::Fill => {
|
||||
let fill_size: Size = self
|
||||
.direction
|
||||
.pack(self.direction.major(child_size), minor_dim)
|
||||
.into();
|
||||
let child_bc = BoxConstraints::tight(fill_size);
|
||||
widget.layout(ctx, &child_bc, data, env);
|
||||
0.0
|
||||
}
|
||||
_ => {
|
||||
let extra_minor = minor_dim - self.direction.minor(child_size);
|
||||
alignment.align(extra_minor)
|
||||
}
|
||||
};
|
||||
|
||||
let child_pos: Point = self.direction.pack(major, child_minor_offset).into();
|
||||
widget.set_origin(ctx, data, env, child_pos);
|
||||
child_paint_rect = child_paint_rect.union(widget.paint_rect());
|
||||
major += self.direction.major(child_size).expand();
|
||||
major += spacing.next().unwrap_or(0.);
|
||||
}
|
||||
Child::FlexedSpacer(_, calculated_size) | Child::FixedSpacer(_, calculated_size) => {
|
||||
major += *calculated_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flex_sum > 0.0 && total_major.is_infinite() {
|
||||
tracing::warn!("A child of Flex is flex, but Flex is unbounded.")
|
||||
}
|
||||
|
||||
if flex_sum > 0.0 {
|
||||
major = total_major;
|
||||
}
|
||||
|
||||
let my_size: Size = self.direction.pack(major, minor_dim).into();
|
||||
|
||||
// if we don't have to fill the main axis, we loosen that axis before constraining
|
||||
let my_size = if !self.fill_major_axis {
|
||||
let max_major = self.direction.major(bc.max());
|
||||
self
|
||||
.direction
|
||||
.constraints(bc, 0.0, max_major)
|
||||
.constrain(my_size)
|
||||
} else {
|
||||
bc.constrain(my_size)
|
||||
};
|
||||
|
||||
let my_bounds = Rect::ZERO.with_size(my_size);
|
||||
let insets = child_paint_rect - my_bounds;
|
||||
ctx.set_paint_insets(insets);
|
||||
|
||||
let baseline_offset = match self.direction {
|
||||
Axis::Horizontal => max_below_baseline,
|
||||
Axis::Vertical => (&self.children)
|
||||
.last()
|
||||
.map(|last| {
|
||||
let child = last.widget();
|
||||
if let Some(widget) = child {
|
||||
let child_bl = widget.baseline_offset();
|
||||
let child_max_y = widget.layout_rect().max_y();
|
||||
let extra_bottom_padding = my_size.height - child_max_y;
|
||||
child_bl + extra_bottom_padding
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.unwrap_or(0.0),
|
||||
};
|
||||
|
||||
ctx.set_baseline_offset(baseline_offset);
|
||||
trace!(
|
||||
"Computed layout: size={}, baseline_offset={}",
|
||||
my_size,
|
||||
baseline_offset
|
||||
);
|
||||
my_size
|
||||
}
|
||||
|
||||
// #[instrument(name = "Flex", level = "trace", skip(self, ctx, data, env))]
|
||||
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
|
||||
for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
|
||||
child.paint(ctx, data, env);
|
||||
}
|
||||
|
||||
// paint the baseline if we're debugging layout
|
||||
if env.get(Env::DEBUG_PAINT) && ctx.widget_state.baseline_offset != 0.0 {
|
||||
let color = env.get_debug_color(ctx.widget_id().to_raw());
|
||||
let my_baseline = ctx.size().height - ctx.widget_state.baseline_offset;
|
||||
let line = crate::kurbo::Line::new((0.0, my_baseline), (ctx.size().width, my_baseline));
|
||||
let stroke_style = crate::piet::StrokeStyle::new().dash_pattern(&[4.0, 4.0]);
|
||||
ctx.stroke_styled(line, &color, 1.0, &stroke_style);
|
||||
}
|
||||
}
|
||||
|
||||
// fn debug_state(&self, data: &T) -> DebugState {
|
||||
// let children_state = self
|
||||
// .children
|
||||
// .iter()
|
||||
// .map(|child| {
|
||||
// let child_widget_pod = child.widget()?;
|
||||
// Some(child_widget_pod.widget().debug_state(data))
|
||||
// })
|
||||
// .flatten()
|
||||
// .collect();
|
||||
// DebugState {
|
||||
// display_name: self.short_type_name().to_string(),
|
||||
// children: children_state,
|
||||
// ..Default::default()
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
use super::main_axis_alignment::MainAxisAlignment;
|
||||
|
||||
pub struct Spacing {
|
||||
alignment: MainAxisAlignment,
|
||||
extra: f64,
|
||||
n_children: usize,
|
||||
index: usize,
|
||||
equal_space: f64,
|
||||
remainder: f64,
|
||||
}
|
||||
|
||||
impl Spacing {
|
||||
/// Given the provided extra space and children count,
|
||||
/// this returns an iterator of `f64` spacing,
|
||||
/// where the first element is the spacing before any children
|
||||
/// and all subsequent elements are the spacing after children.
|
||||
fn new(alignment: MainAxisAlignment, extra: f64, n_children: usize) -> Spacing {
|
||||
let extra = if extra.is_finite() { extra } else { 0. };
|
||||
let equal_space = if n_children > 0 {
|
||||
match alignment {
|
||||
MainAxisAlignment::Center => extra / 2.,
|
||||
MainAxisAlignment::SpaceBetween => extra / (n_children - 1).max(1) as f64,
|
||||
MainAxisAlignment::SpaceEvenly => extra / (n_children + 1) as f64,
|
||||
MainAxisAlignment::SpaceAround => extra / (2 * n_children) as f64,
|
||||
_ => 0.,
|
||||
}
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
Spacing {
|
||||
alignment,
|
||||
extra,
|
||||
n_children,
|
||||
index: 0,
|
||||
equal_space,
|
||||
remainder: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_space(&mut self) -> f64 {
|
||||
let desired_space = self.equal_space + self.remainder;
|
||||
let actual_space = desired_space.round();
|
||||
self.remainder = desired_space - actual_space;
|
||||
actual_space
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Spacing {
|
||||
type Item = f64;
|
||||
|
||||
fn next(&mut self) -> Option<f64> {
|
||||
if self.index > self.n_children {
|
||||
return None;
|
||||
}
|
||||
let result = {
|
||||
if self.n_children == 0 {
|
||||
self.extra
|
||||
} else {
|
||||
#[allow(clippy::match_bool)]
|
||||
match self.alignment {
|
||||
MainAxisAlignment::Start => match self.index == self.n_children {
|
||||
true => self.extra,
|
||||
false => 0.,
|
||||
},
|
||||
MainAxisAlignment::End => match self.index == 0 {
|
||||
true => self.extra,
|
||||
false => 0.,
|
||||
},
|
||||
MainAxisAlignment::Center => match self.index {
|
||||
0 => self.next_space(),
|
||||
i if i == self.n_children => self.next_space(),
|
||||
_ => 0.,
|
||||
},
|
||||
MainAxisAlignment::SpaceBetween => match self.index {
|
||||
0 => 0.,
|
||||
i if i != self.n_children => self.next_space(),
|
||||
_ => match self.n_children {
|
||||
1 => self.next_space(),
|
||||
_ => 0.,
|
||||
},
|
||||
},
|
||||
MainAxisAlignment::SpaceEvenly => self.next_space(),
|
||||
MainAxisAlignment::SpaceAround => {
|
||||
if self.index == 0 || self.index == self.n_children {
|
||||
self.next_space()
|
||||
} else {
|
||||
self.next_space() + self.next_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
self.index += 1;
|
||||
Some(result)
|
||||
}
|
||||
}
|
|
@ -1,39 +1,58 @@
|
|||
use crate::{Data, Size, Widget};
|
||||
use crate::{box_constraints::BoxConstraints, Data, DataWrapper, Size, Widget};
|
||||
|
||||
pub struct Text<T: Data> {
|
||||
data: T,
|
||||
text: Box<dyn Fn(&T) -> String>,
|
||||
text: Box<dyn Fn(&DataWrapper<T>) -> String>,
|
||||
needs_repaint: bool,
|
||||
buf: String,
|
||||
}
|
||||
|
||||
impl<T: Data> Text<T> {
|
||||
pub fn new(data: T, text: Box<dyn Fn(&T) -> String>) -> Self {
|
||||
Self { data, text }
|
||||
}
|
||||
fn text(&self) -> String {
|
||||
(self.text)(&self.data)
|
||||
}
|
||||
pub fn new(text: Box<dyn Fn(&DataWrapper<T>) -> String>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
needs_repaint: true,
|
||||
buf: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Data> Widget<T> for Text<T> {
|
||||
fn layout(&mut self, _bounds: &Size) -> Size {
|
||||
Size {
|
||||
a: self.text().chars().count(),
|
||||
b: self.text().chars().filter(|ch| *ch == '\n').count(),
|
||||
}
|
||||
}
|
||||
fn paint(&self, buf: &mut [&mut [char]]) {
|
||||
let the_text = self.text();
|
||||
let mut the_chars = the_text.chars();
|
||||
for line in buf.iter_mut() {
|
||||
for spot in line.iter_mut() {
|
||||
if let Some(ch) = the_chars.next() {
|
||||
if ch == '\n' {
|
||||
break;
|
||||
} else {
|
||||
*spot = ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn update(&mut self, data: &DataWrapper<T>) {
|
||||
self.buf = (*self.text)(data);
|
||||
}
|
||||
fn layout(&mut self, bc: &BoxConstraints) -> Size {
|
||||
let mut width = 0;
|
||||
let mut height = 1;
|
||||
let mut x = 0;
|
||||
for ch in self.buf.chars() {
|
||||
if ch == '\n' {
|
||||
height += 1;
|
||||
x = 0;
|
||||
}
|
||||
x += 1;
|
||||
if x > bc.max().width - 1 {
|
||||
x = 0;
|
||||
width = bc.max().width;
|
||||
}
|
||||
if x > width {
|
||||
width = x;
|
||||
}
|
||||
}
|
||||
Size::new(width, height).clamp(bc.min(), bc.max())
|
||||
}
|
||||
fn paint(&self, buf: &mut [char], size: &Size) {
|
||||
let mut the_chars = self.buf.chars();
|
||||
for y in 0..size.height {
|
||||
for x in 0..size.width {
|
||||
if let (Some(ch), Some(spot)) = (the_chars.next(), buf.get_mut(x + y * size.width)) {
|
||||
if ch == '\n' {
|
||||
break;
|
||||
} else {
|
||||
*spot = ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn event(&mut self, data: &mut DataWrapper<T>) {}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
use std::ops::{DerefMut, Deref};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::Size;
|
||||
use crate::{box_constraints::BoxConstraints, DataWrapper, Size};
|
||||
|
||||
pub trait Widget<T> {
|
||||
fn layout(&mut self, bounds: &Size) -> Size;
|
||||
fn paint(&self, buf: &mut [&mut [char]]);
|
||||
fn update(&mut self, data: &DataWrapper<T>);
|
||||
fn layout(&mut self, bc: &BoxConstraints) -> Size;
|
||||
fn paint(&self, buf: &mut [char], size: &Size);
|
||||
fn event(&mut self, data: &mut DataWrapper<T>);
|
||||
}
|
||||
|
||||
impl<T> Widget<T> for Box<dyn Widget<T>> {
|
||||
fn layout(&mut self, bounds: &Size) -> Size {
|
||||
self.deref_mut().layout(bounds)
|
||||
}
|
||||
|
||||
fn paint(&self, buf: &mut [&mut [char]]) {
|
||||
self.deref().paint(buf)
|
||||
}
|
||||
fn update(&mut self, data: &DataWrapper<T>) {
|
||||
self.deref_mut().update(data)
|
||||
}
|
||||
fn layout(&mut self, bounds: &BoxConstraints) -> Size {
|
||||
self.deref_mut().layout(bounds)
|
||||
}
|
||||
fn paint(&self, buf: &mut [char], size: &Size) {
|
||||
self.deref().paint(buf, size)
|
||||
}
|
||||
fn event(&mut self, data: &mut DataWrapper<T>) {
|
||||
self.deref_mut().event(data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,40 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{Point, Widget, Size, Data};
|
||||
|
||||
|
||||
use crate::{box_constraints::BoxConstraints, Data, DataWrapper, Point, Size, Widget};
|
||||
|
||||
pub struct WidgetPod<T, W> {
|
||||
data: PhantomData<T>,
|
||||
inner: W,
|
||||
origin: Point,
|
||||
data: PhantomData<T>,
|
||||
inner: W,
|
||||
origin: Point,
|
||||
}
|
||||
|
||||
impl<T, W: Widget<T>> WidgetPod<T, W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
data: PhantomData,
|
||||
inner,
|
||||
origin: Point { a: 0, b: 0 },
|
||||
}
|
||||
}
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
data: PhantomData,
|
||||
inner,
|
||||
origin: Point { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_origin(&mut self, p: Point) {
|
||||
self.origin = p;
|
||||
}
|
||||
pub fn set_origin(&mut self, p: Point) {
|
||||
self.origin = p;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Data, W: Widget<T>> Widget<T> for WidgetPod<T, W> {
|
||||
fn layout(&mut self, bounds: &Size) -> Size {
|
||||
self.inner.layout(bounds)
|
||||
}
|
||||
fn update(&mut self, data: &DataWrapper<T>) {
|
||||
self.inner.update(data)
|
||||
}
|
||||
|
||||
fn paint(&self, buf: &mut [&mut [char]]) {
|
||||
self.inner.paint(buf)
|
||||
}
|
||||
fn layout(&mut self, bounds: &BoxConstraints) -> Size {
|
||||
self.inner.layout(bounds)
|
||||
}
|
||||
|
||||
fn paint(&self, buf: &mut [char], size: &Size) {
|
||||
self.inner.paint(buf, size)
|
||||
}
|
||||
fn event(&mut self, data: &mut DataWrapper<T>) {
|
||||
self.inner.event(data)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue