530 lines
18 KiB
Rust
530 lines
18 KiB
Rust
use crate::{math::*, Align};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// This describes the bounds and existing contents of an [`Ui`][`crate::Ui`].
|
|
/// It is what is used and updated by [`Layout`] when adding new widgets.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct Region {
|
|
/// This is the minimal size of the `Ui`.
|
|
/// When adding new widgets, this will generally expand.
|
|
///
|
|
/// Always finite.
|
|
///
|
|
/// The bounding box of all child widgets, but not necessarily a tight bounding box
|
|
/// since `Ui` can start with a non-zero min_rect size.
|
|
pub min_rect: Rect,
|
|
|
|
/// The maximum size of this `Ui`. This is a *soft max*
|
|
/// meaning new widgets will *try* not to expand beyond it,
|
|
/// but if they have to, they will.
|
|
///
|
|
/// Text will wrap at `max_rect.right()`.
|
|
/// Some widgets (like separator lines) will try to fill the full `max_rect` width of the ui.
|
|
///
|
|
/// `max_rect` will always be at least the size of `min_rect`.
|
|
///
|
|
/// If the `max_rect` size is zero, it is a signal that child widgets should be as small as possible.
|
|
/// If the `max_rect` size is infinite, it is a signal that child widgets should take up as much room as they want.
|
|
pub max_rect: Rect,
|
|
|
|
/// Where the next widget will be put.
|
|
/// If something has already been added, this will point ot `style.spacing.item_spacing` beyond the latest child.
|
|
/// The cursor can thus be `style.spacing.item_spacing` pixels outside of the min_rect.
|
|
pub(crate) cursor: Pos2,
|
|
}
|
|
|
|
impl Region {
|
|
/// This is like `max_rect`, but will never be infinite.
|
|
/// If the desired rect is infinite ("be as big as you want")
|
|
/// this will be bounded by `min_rect` instead.
|
|
pub fn max_rect_finite(&self) -> Rect {
|
|
let mut result = self.max_rect;
|
|
if !result.min.x.is_finite() {
|
|
result.min.x = self.min_rect.min.x;
|
|
}
|
|
if !result.min.y.is_finite() {
|
|
result.min.y = self.min_rect.min.y;
|
|
}
|
|
if !result.max.x.is_finite() {
|
|
result.max.x = self.min_rect.max.x;
|
|
}
|
|
if !result.max.y.is_finite() {
|
|
result.max.y = self.min_rect.max.y;
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
|
|
pub fn expand_to_include_rect(&mut self, rect: Rect) {
|
|
self.min_rect = self.min_rect.union(rect);
|
|
self.max_rect = self.max_rect.union(rect);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// Layout direction, one of `LeftToRight`, `RightToLeft`, `TopDown`, `BottomUp`.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
|
#[cfg_attr(feature = "persistence", serde(rename_all = "snake_case"))]
|
|
pub enum Direction {
|
|
LeftToRight,
|
|
RightToLeft,
|
|
TopDown,
|
|
BottomUp,
|
|
}
|
|
|
|
impl Direction {
|
|
pub fn is_horizontal(self) -> bool {
|
|
match self {
|
|
Direction::LeftToRight | Direction::RightToLeft => true,
|
|
Direction::TopDown | Direction::BottomUp => false,
|
|
}
|
|
}
|
|
|
|
pub fn is_vertical(self) -> bool {
|
|
match self {
|
|
Direction::LeftToRight | Direction::RightToLeft => false,
|
|
Direction::TopDown | Direction::BottomUp => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
// #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct Layout {
|
|
/// Main axis direction
|
|
main_dir: Direction,
|
|
|
|
/// If true, wrap around when reading the end of the main direction.
|
|
/// For instance, for `main_dir == Direction::LeftToRight` this will
|
|
/// wrap to a new row when we reach the right side of the `max_rect`.
|
|
main_wrap: bool,
|
|
|
|
/// How to align things on the cross axis.
|
|
/// For vertical layouts: put things to left, center or right?
|
|
/// For horizontal layouts: put things to top, center or bottom?
|
|
cross_align: Align,
|
|
|
|
/// Justify the cross axis?
|
|
/// For vertical layouts justify mean all widgets get maximum width.
|
|
/// For horizontal layouts justify mean all widgets get maximum height.
|
|
cross_justify: bool,
|
|
}
|
|
|
|
impl Default for Layout {
|
|
fn default() -> Self {
|
|
// TODO: Get from `Style` instead.
|
|
// This is a very euro-centric default.
|
|
Self {
|
|
main_dir: Direction::TopDown,
|
|
main_wrap: false,
|
|
cross_align: Align::left(),
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ## Constructors
|
|
impl Layout {
|
|
pub fn left_to_right() -> Self {
|
|
Self {
|
|
main_dir: Direction::LeftToRight,
|
|
main_wrap: false,
|
|
cross_align: Align::Center,
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
|
|
pub fn right_to_left() -> Self {
|
|
Self {
|
|
main_dir: Direction::RightToLeft,
|
|
main_wrap: false,
|
|
cross_align: Align::Center,
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
|
|
pub fn top_down(cross_align: Align) -> Self {
|
|
Self {
|
|
main_dir: Direction::TopDown,
|
|
main_wrap: false,
|
|
cross_align,
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
|
|
/// Top-down layout justifed so that buttons etc fill the full available width.
|
|
pub fn top_down_justified(cross_align: Align) -> Self {
|
|
Self::top_down(cross_align).with_cross_justify(true)
|
|
}
|
|
|
|
pub fn bottom_up(cross_align: Align) -> Self {
|
|
Self {
|
|
main_dir: Direction::BottomUp,
|
|
main_wrap: false,
|
|
cross_align,
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
|
|
pub fn from_main_dir_and_cross_align(main_dir: Direction, cross_align: Align) -> Self {
|
|
Self {
|
|
main_dir,
|
|
main_wrap: false,
|
|
cross_align,
|
|
cross_justify: false,
|
|
}
|
|
}
|
|
|
|
#[deprecated = "Use `top_down`"]
|
|
pub fn vertical(cross_align: Align) -> Self {
|
|
Self::top_down(cross_align)
|
|
}
|
|
|
|
#[deprecated = "Use `left_to_right`"]
|
|
pub fn horizontal(cross_align: Align) -> Self {
|
|
Self::left_to_right().with_cross_align(cross_align)
|
|
}
|
|
|
|
pub fn with_main_wrap(self, main_wrap: bool) -> Self {
|
|
Self { main_wrap, ..self }
|
|
}
|
|
|
|
pub fn with_cross_align(self, cross_align: Align) -> Self {
|
|
Self {
|
|
cross_align,
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn with_cross_justify(self, cross_justify: bool) -> Self {
|
|
Self {
|
|
cross_justify,
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ## Inspectors
|
|
impl Layout {
|
|
pub fn main_dir(&self) -> Direction {
|
|
self.main_dir
|
|
}
|
|
|
|
pub fn main_wrap(&self) -> bool {
|
|
self.main_wrap
|
|
}
|
|
|
|
pub fn cross_align(&self) -> Align {
|
|
self.cross_align
|
|
}
|
|
|
|
pub fn cross_justify(&self) -> bool {
|
|
self.cross_justify
|
|
}
|
|
|
|
pub fn is_horizontal(&self) -> bool {
|
|
self.main_dir().is_horizontal()
|
|
}
|
|
|
|
pub fn is_vertical(&self) -> bool {
|
|
self.main_dir().is_vertical()
|
|
}
|
|
|
|
pub fn prefer_right_to_left(&self) -> bool {
|
|
self.main_dir == Direction::RightToLeft
|
|
|| self.main_dir.is_vertical() && self.cross_align == Align::Max
|
|
}
|
|
|
|
fn horizontal_align(&self) -> Align {
|
|
match self.main_dir {
|
|
// Direction::LeftToRight => Align::left(),
|
|
// Direction::RightToLeft => Align::right(),
|
|
Direction::LeftToRight | Direction::RightToLeft => Align::Center, // looks better to e.g. center text within a button
|
|
|
|
Direction::TopDown | Direction::BottomUp => self.cross_align,
|
|
}
|
|
}
|
|
|
|
fn vertical_align(&self) -> Align {
|
|
match self.main_dir {
|
|
// Direction::TopDown => Align::top(),
|
|
// Direction::BottomUp => Align::bottom(),
|
|
Direction::TopDown | Direction::BottomUp => Align::Center, // looks better to e.g. center text within a button
|
|
|
|
Direction::LeftToRight | Direction::RightToLeft => self.cross_align,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ## Doing layout
|
|
impl Layout {
|
|
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
|
|
let x = match self.horizontal_align() {
|
|
Align::Min => outer.left(),
|
|
Align::Center => outer.center().x - size.x / 2.0,
|
|
Align::Max => outer.right() - size.x,
|
|
};
|
|
let y = match self.vertical_align() {
|
|
Align::Min => outer.top(),
|
|
Align::Center => outer.center().y - size.y / 2.0,
|
|
Align::Max => outer.bottom() - size.y,
|
|
};
|
|
|
|
Rect::from_min_size(Pos2::new(x, y), size)
|
|
}
|
|
|
|
fn initial_cursor(&self, max_rect: Rect) -> Pos2 {
|
|
match self.main_dir {
|
|
Direction::LeftToRight => max_rect.left_top(),
|
|
Direction::RightToLeft => max_rect.right_top(),
|
|
Direction::TopDown => max_rect.left_top(),
|
|
Direction::BottomUp => max_rect.left_bottom(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region {
|
|
let cursor = self.initial_cursor(max_rect);
|
|
let min_rect = Rect::from_min_size(cursor, Vec2::zero());
|
|
Region {
|
|
min_rect,
|
|
max_rect,
|
|
cursor,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn available_rect_before_wrap(&self, region: &Region) -> Rect {
|
|
self.available_from_cursor_max_rect(region.cursor, region.max_rect)
|
|
}
|
|
|
|
fn available_size_before_wrap(&self, region: &Region) -> Vec2 {
|
|
self.available_rect_before_wrap(region).size()
|
|
}
|
|
|
|
pub(crate) fn available_rect_before_wrap_finite(&self, region: &Region) -> Rect {
|
|
self.available_from_cursor_max_rect(region.cursor, region.max_rect_finite())
|
|
}
|
|
|
|
fn available_size_before_wrap_finite(&self, region: &Region) -> Vec2 {
|
|
self.available_rect_before_wrap_finite(region).size()
|
|
}
|
|
|
|
/// Amount of space available for a widget.
|
|
/// For wrapping layouts, this is the maximum (after wrap).
|
|
pub(crate) fn available_size(&self, r: &Region) -> Vec2 {
|
|
if self.main_wrap {
|
|
if self.main_dir.is_horizontal() {
|
|
vec2(r.max_rect.width(), r.max_rect.bottom() - r.cursor.y)
|
|
} else {
|
|
vec2(r.max_rect.right() - r.cursor.x, r.max_rect.height())
|
|
}
|
|
} else {
|
|
self.available_from_cursor_max_rect(r.cursor, r.max_rect)
|
|
.size()
|
|
}
|
|
}
|
|
|
|
/// Given the cursor in the region, how much space is available
|
|
/// for the next widget?
|
|
fn available_from_cursor_max_rect(&self, cursor: Pos2, max_rect: Rect) -> Rect {
|
|
let mut rect = max_rect;
|
|
|
|
match self.main_dir {
|
|
Direction::LeftToRight => {
|
|
rect.min.x = cursor.x;
|
|
rect.min.y = cursor.y;
|
|
}
|
|
Direction::RightToLeft => {
|
|
rect.max.x = cursor.x;
|
|
rect.min.y = cursor.y;
|
|
}
|
|
Direction::TopDown => {
|
|
rect.min.x = cursor.x;
|
|
rect.min.y = cursor.y;
|
|
}
|
|
Direction::BottomUp => {
|
|
rect.min.x = cursor.x;
|
|
rect.max.y = cursor.y;
|
|
}
|
|
}
|
|
|
|
rect
|
|
}
|
|
|
|
/// Returns where to put the next widget that is of the given size.
|
|
/// The returned `frame_rect` `Rect` will always be justified along the cross axis.
|
|
/// This is what you then pass to `advance_after_rects`.
|
|
/// Use `justify_or_align` to get the inner `widget_rect`.
|
|
#[allow(clippy::collapsible_if)]
|
|
pub(crate) fn next_space(
|
|
&self,
|
|
region: &Region,
|
|
mut child_size: Vec2,
|
|
item_spacing: Vec2,
|
|
) -> Rect {
|
|
let mut cursor = region.cursor;
|
|
|
|
if self.main_wrap {
|
|
let available_size = self.available_size_before_wrap(region);
|
|
match self.main_dir {
|
|
Direction::LeftToRight => {
|
|
if available_size.x < child_size.x && region.max_rect.left() < cursor.x {
|
|
// New row
|
|
cursor = pos2(
|
|
region.max_rect.left(),
|
|
region.max_rect.bottom() + item_spacing.y,
|
|
);
|
|
}
|
|
}
|
|
Direction::RightToLeft => {
|
|
if available_size.x < child_size.x && cursor.x < region.max_rect.right() {
|
|
// New row
|
|
cursor = pos2(
|
|
region.max_rect.right(),
|
|
region.max_rect.bottom() + item_spacing.y,
|
|
);
|
|
}
|
|
}
|
|
Direction::TopDown => {
|
|
if available_size.y < child_size.y && region.max_rect.top() < cursor.y {
|
|
// New column
|
|
cursor = pos2(
|
|
region.max_rect.right() + item_spacing.x,
|
|
region.max_rect.top(),
|
|
);
|
|
}
|
|
}
|
|
Direction::BottomUp => {
|
|
if available_size.y < child_size.y && cursor.y < region.max_rect.bottom() {
|
|
// New column
|
|
cursor = pos2(
|
|
region.max_rect.right() + item_spacing.x,
|
|
region.max_rect.bottom(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let available_size = self.available_size_before_wrap_finite(region);
|
|
if self.main_dir.is_horizontal() {
|
|
// Fill full height
|
|
child_size.y = child_size.y.max(available_size.y);
|
|
} else {
|
|
// Fill full width
|
|
child_size.x = child_size.x.max(available_size.x);
|
|
}
|
|
|
|
let child_pos = match self.main_dir {
|
|
Direction::LeftToRight => cursor,
|
|
Direction::RightToLeft => cursor + vec2(-child_size.x, 0.0),
|
|
Direction::TopDown => cursor,
|
|
Direction::BottomUp => cursor + vec2(0.0, -child_size.y),
|
|
};
|
|
|
|
Rect::from_min_size(child_pos, child_size)
|
|
}
|
|
|
|
/// Apply justify or alignment after calling `next_space`.
|
|
pub(crate) fn justify_or_align(&self, rect: Rect, mut child_size: Vec2) -> Rect {
|
|
if self.main_dir.is_horizontal() {
|
|
if self.cross_justify {
|
|
child_size.y = rect.height(); // fill full height
|
|
}
|
|
Align2([Align::Center, self.cross_align]).align_size_within_rect(child_size, rect)
|
|
} else {
|
|
if self.cross_justify {
|
|
child_size.x = rect.width(); // fill full width
|
|
}
|
|
Align2([self.cross_align, Align::Center]).align_size_within_rect(child_size, rect)
|
|
}
|
|
}
|
|
|
|
/// Advance the cursor by this many points.
|
|
pub(crate) fn advance_cursor(&self, region: &mut Region, amount: f32) {
|
|
match self.main_dir {
|
|
Direction::LeftToRight => region.cursor.x += amount,
|
|
Direction::RightToLeft => region.cursor.x -= amount,
|
|
Direction::TopDown => region.cursor.y += amount,
|
|
Direction::BottomUp => region.cursor.y -= amount,
|
|
}
|
|
}
|
|
|
|
/// Advance cursor after a widget was added to a specific rectangle.
|
|
///
|
|
/// * `frame_rect`: the frame inside which a widget was e.g. centered
|
|
/// * `widget_rect`: the actual rect used by the widget
|
|
pub(crate) fn advance_after_rects(
|
|
&self,
|
|
region: &mut Region,
|
|
frame_rect: Rect,
|
|
widget_rect: Rect,
|
|
item_spacing: Vec2,
|
|
) {
|
|
region.cursor = match self.main_dir {
|
|
Direction::LeftToRight => pos2(widget_rect.right() + item_spacing.x, frame_rect.top()),
|
|
Direction::RightToLeft => pos2(widget_rect.left() - item_spacing.x, frame_rect.top()),
|
|
Direction::TopDown => pos2(frame_rect.left(), widget_rect.bottom() + item_spacing.y),
|
|
Direction::BottomUp => pos2(frame_rect.left(), widget_rect.top() - item_spacing.y),
|
|
};
|
|
}
|
|
|
|
/// Move to the next row in a wrapping layout.
|
|
/// Otherwise does nothing.
|
|
pub(crate) fn end_row(&mut self, region: &mut Region, item_spacing: Vec2) {
|
|
if self.main_wrap && self.is_horizontal() {
|
|
// New row
|
|
region.cursor = pos2(
|
|
region.max_rect.left(),
|
|
region.max_rect.bottom() + item_spacing.y,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// ## Debug stuff
|
|
impl Layout {
|
|
/// Shows where the next widget is going to be placed
|
|
pub(crate) fn debug_paint_cursor(
|
|
&self,
|
|
region: &Region,
|
|
stroke: epaint::Stroke,
|
|
painter: &crate::Painter,
|
|
) {
|
|
use crate::paint::*;
|
|
|
|
let cursor = region.cursor;
|
|
|
|
let align;
|
|
|
|
let l = 64.0;
|
|
|
|
match self.main_dir {
|
|
Direction::LeftToRight => {
|
|
painter.arrow(cursor, vec2(l, 0.0), stroke);
|
|
align = Align2::LEFT_TOP;
|
|
}
|
|
Direction::RightToLeft => {
|
|
painter.arrow(cursor, vec2(-l, 0.0), stroke);
|
|
align = Align2::RIGHT_TOP;
|
|
}
|
|
Direction::TopDown => {
|
|
painter.arrow(cursor, vec2(0.0, l), stroke);
|
|
align = Align2::LEFT_TOP;
|
|
}
|
|
Direction::BottomUp => {
|
|
painter.arrow(cursor, vec2(0.0, -l), stroke);
|
|
align = Align2::LEFT_BOTTOM;
|
|
}
|
|
}
|
|
|
|
painter.text(cursor, align, "cursor", TextStyle::Monospace, stroke.color);
|
|
}
|
|
}
|