Merge pull request #60 from emilk/layout-wrapping

Wrapping layouts
This commit is contained in:
Emil Ernerfeldt 2020-12-10 23:38:54 +01:00 committed by GitHub
commit 884558ac48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1031 additions and 555 deletions

View file

@ -8,12 +8,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ⭐ ### Added ⭐
* `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): a text-button that can be selected * Wrapping layouts:
* `ui.horizontal_wrapped(|ui| ...)`: Add widgets on a row but wrap at `max_size`.
* `ui.horizontal_wrapped_for_text`: Like `horizontal_wrapped`, but with spacing made for embedding text.
* `egui::Layout` now supports justified layouts where contents is _also_ centered, right-aligned, etc.
* `ui.allocate_ui(size, |ui| ...)`: Easily created a sized child-`Ui`.
* `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): A text-button that can be selected.
* `ui.small_button`: A smaller button that looks good embedded in text.
* Add `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes. * Add `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes.
### Changed 🔧 ### Changed 🔧
* Changed default font to [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu). * Changed default font to [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu).
* Refactored `egui::Layout` substantially, changing its interface.
### Removed 🔥 ### Removed 🔥

View file

@ -16,6 +16,21 @@ pub enum Align {
Max, Max,
} }
impl Align {
pub fn left() -> Self {
Self::Min
}
pub fn right() -> Self {
Self::Max
}
pub fn top() -> Self {
Self::Min
}
pub fn bottom() -> Self {
Self::Max
}
}
impl Default for Align { impl Default for Align {
fn default() -> Align { fn default() -> Align {
Align::Min Align::Min

View file

@ -1,7 +1,6 @@
use std::hash::Hash; use std::hash::Hash;
use crate::{ use crate::{
layout::Direction,
paint::{PaintCmd, TextStyle}, paint::{PaintCmd, TextStyle},
widgets::Label, widgets::Label,
*, *,
@ -168,7 +167,7 @@ struct Prepared {
impl CollapsingHeader { impl CollapsingHeader {
fn begin(self, ui: &mut Ui) -> Prepared { fn begin(self, ui: &mut Ui) -> Prepared {
assert!( assert!(
ui.layout().dir() == Direction::Vertical, ui.layout().main_dir().is_vertical(),
"Horizontal collapsing is unimplemented" "Horizontal collapsing is unimplemented"
); );
let Self { let Self {
@ -181,7 +180,7 @@ impl CollapsingHeader {
let id = ui.make_persistent_id(id_source); let id = ui.make_persistent_id(id_source);
let available = ui.available_finite(); let available = ui.available_rect_before_wrap_finite();
let text_pos = available.min + vec2(ui.style().spacing.indent, 0.0); let text_pos = available.min + vec2(ui.style().spacing.indent, 0.0);
let galley = label.layout_width(ui, available.right() - text_pos.x); let galley = label.layout_width(ui, available.right() - text_pos.x);
let text_max_x = text_pos.x + galley.size.x; let text_max_x = text_pos.x + galley.size.x;

View file

@ -60,10 +60,13 @@ pub fn combo_box(
let frame = Frame::popup(ui.style()); let frame = Frame::popup(ui.style());
let frame_margin = frame.margin; let frame_margin = frame.margin;
frame.show(ui, |ui| { frame.show(ui, |ui| {
ui.with_layout(Layout::justified(Direction::Vertical), |ui| { ui.with_layout(
ui.set_min_width(button_response.rect.width() - 2.0 * frame_margin.x); Layout::top_down(Align::left()).with_cross_justify(true),
menu_contents(ui); |ui| {
}); ui.set_min_width(button_response.rect.width() - 2.0 * frame_margin.x);
menu_contents(ui);
},
);
}) })
}); });
@ -84,7 +87,7 @@ fn button_frame(
add_contents: impl FnOnce(&mut Ui), add_contents: impl FnOnce(&mut Ui),
) -> Response { ) -> Response {
let margin = ui.style().spacing.button_padding; let margin = ui.style().spacing.button_padding;
let outer_rect_bounds = ui.available(); let outer_rect_bounds = ui.available_rect_before_wrap();
let inner_rect = outer_rect_bounds.shrink2(margin); let inner_rect = outer_rect_bounds.shrink2(margin);
let where_to_put_background = ui.painter().add(PaintCmd::Noop); let where_to_put_background = ui.painter().add(PaintCmd::Noop);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); let mut content_ui = ui.child_ui(inner_rect, *ui.layout());

View file

@ -99,7 +99,7 @@ pub struct Prepared {
impl Frame { impl Frame {
pub fn begin(self, ui: &mut Ui) -> Prepared { pub fn begin(self, ui: &mut Ui) -> Prepared {
let where_to_put_background = ui.painter().add(PaintCmd::Noop); let where_to_put_background = ui.painter().add(PaintCmd::Noop);
let outer_rect_bounds = ui.available(); let outer_rect_bounds = ui.available_rect_before_wrap();
let inner_rect = outer_rect_bounds.shrink2(self.margin); let inner_rect = outer_rect_bounds.shrink2(self.margin);
let content_ui = ui.child_ui(inner_rect, *ui.layout()); let content_ui = ui.child_ui(inner_rect, *ui.layout());

View file

@ -153,7 +153,7 @@ struct Prepared {
impl Resize { impl Resize {
fn begin(&mut self, ui: &mut Ui) -> Prepared { fn begin(&mut self, ui: &mut Ui) -> Prepared {
let position = ui.available().min; let position = ui.available_rect_before_wrap().min;
let id = self.id.unwrap_or_else(|| { let id = self.id.unwrap_or_else(|| {
let id_source = self.id_source.unwrap_or_else(|| Id::new("resize")); let id_source = self.id_source.unwrap_or_else(|| Id::new("resize"));
ui.make_persistent_id(id_source) ui.make_persistent_id(id_source)

View file

@ -103,12 +103,12 @@ impl ScrollArea {
}; };
let outer_size = vec2( let outer_size = vec2(
ui.available().width(), ui.available_width(),
ui.available().height().at_most(max_height), ui.available_size_before_wrap().y.at_most(max_height),
); );
let inner_size = outer_size - vec2(current_scroll_bar_width, 0.0); let inner_size = outer_size - vec2(current_scroll_bar_width, 0.0);
let inner_rect = Rect::from_min_size(ui.available().min, inner_size); let inner_rect = Rect::from_min_size(ui.available_rect_before_wrap().min, inner_size);
let mut content_ui = ui.child_ui( let mut content_ui = ui.child_ui(
Rect::from_min_size( Rect::from_min_size(

View file

@ -106,7 +106,7 @@ impl FrameHistory {
// TODO: we should not use `slider_width` as default graph width. // TODO: we should not use `slider_width` as default graph width.
let height = ui.style().spacing.slider_width; let height = ui.style().spacing.slider_width;
let rect = ui.allocate_space(vec2(ui.available_finite().width(), height)); let rect = ui.allocate_space(vec2(ui.available_size_before_wrap_finite().x, height));
let style = ui.style().noninteractive(); let style = ui.style().noninteractive();
let mut cmds = vec![PaintCmd::Rect { let mut cmds = vec![PaintCmd::Rect {

View file

@ -32,7 +32,7 @@ impl View for DancingStrings {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
let time = ui.input().time; let time = ui.input().time;
let desired_size = ui.available().width() * vec2(1.0, 0.35); let desired_size = ui.available_width() * vec2(1.0, 0.35);
let rect = ui.allocate_space(desired_size); let rect = ui.allocate_space(desired_size);
let mut cmds = vec![]; let mut cmds = vec![];

View file

@ -265,7 +265,7 @@ impl Painting {
} }
fn content(&mut self, ui: &mut Ui) { fn content(&mut self, ui: &mut Ui) {
let rect = ui.allocate_space(ui.available_finite().size()); let rect = ui.allocate_space(ui.available_size_before_wrap_finite());
let response = ui.interact(rect, ui.id(), Sense::drag()); let response = ui.interact(rect, ui.id(), Sense::drag());
let rect = response.rect; let rect = response.rect;
let clip_rect = ui.clip_rect().intersect(rect); // Make sure we don't paint out of bounds let clip_rect = ui.clip_rect().intersect(rect); // Make sure we don't paint out of bounds
@ -304,81 +304,134 @@ use crate::layout::*;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
struct LayoutDemo { struct LayoutDemo {
dir: Direction, // Identical to contents of `egui::Layout`
align: Option<Align>, // None == justified main_dir: Direction,
reversed: bool, main_wrap: bool,
cross_align: Align,
cross_justify: bool,
// Extra for testing wrapping:
wrap_column_width: f32,
wrap_row_height: f32,
} }
impl Default for LayoutDemo { impl Default for LayoutDemo {
fn default() -> Self { fn default() -> Self {
Self { Self {
dir: Direction::Vertical, main_dir: Direction::TopDown,
align: Some(Align::Center), main_wrap: false,
reversed: false, cross_align: Align::Min,
cross_justify: false,
wrap_column_width: 150.0,
wrap_row_height: 20.0,
} }
} }
} }
impl LayoutDemo { impl LayoutDemo {
fn layout(&self) -> Layout { fn layout(&self) -> Layout {
let layout = Layout::from_dir_align(self.dir, self.align); Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
if self.reversed { .with_main_wrap(self.main_wrap)
layout.reverse() .with_cross_justify(self.cross_justify)
} else {
layout
}
} }
pub fn ui(&mut self, ui: &mut Ui) { pub fn ui(&mut self, ui: &mut Ui) {
self.content_ui(ui);
Resize::default() Resize::default()
.default_size([200.0, 100.0]) .default_size([300.0, 200.0])
.show(ui, |ui| { .show(ui, |ui| {
ui.with_layout(self.layout(), |ui| self.content_ui(ui)) if self.main_wrap {
if self.main_dir.is_horizontal() {
ui.allocate_ui(
vec2(
ui.available_size_before_wrap_finite().x,
self.wrap_row_height,
),
|ui| ui.with_layout(self.layout(), |ui| self.demo_ui(ui)),
);
} else {
ui.allocate_ui(
vec2(
self.wrap_column_width,
ui.available_size_before_wrap_finite().y,
),
|ui| ui.with_layout(self.layout(), |ui| self.demo_ui(ui)),
);
}
} else {
ui.with_layout(self.layout(), |ui| self.demo_ui(ui));
}
}); });
ui.label("Resize to see effect");
} }
pub fn content_ui(&mut self, ui: &mut Ui) { pub fn content_ui(&mut self, ui: &mut Ui) {
// ui.label(format!("Available space: {:?}", ui.available().size())); ui.horizontal(|ui| {
if ui.button("Reset").clicked { if ui.button("Top-down").clicked {
*self = Default::default(); *self = Default::default();
}
ui.separator();
ui.label("Direction:");
// TODO: enum iter
for &dir in &[Direction::Horizontal, Direction::Vertical] {
if ui
.add(RadioButton::new(self.dir == dir, format!("{:?}", dir)))
.clicked
{
self.dir = dir;
} }
} if ui.button("Top-down, centered and justified").clicked {
*self = Default::default();
ui.checkbox(&mut self.reversed, "Reversed"); self.cross_align = Align::Center;
self.cross_justify = true;
ui.separator();
ui.label("Align:");
for &align in &[Align::Min, Align::Center, Align::Max] {
if ui
.add(RadioButton::new(
self.align == Some(align),
format!("{:?}", align),
))
.clicked
{
self.align = Some(align);
} }
if ui.button("Horizontal wrapped").clicked {
*self = Default::default();
self.main_dir = Direction::LeftToRight;
self.cross_align = Align::Center;
self.main_wrap = true;
}
});
ui.horizontal(|ui| {
ui.label("Main Direction:");
for &dir in &[
Direction::LeftToRight,
Direction::RightToLeft,
Direction::TopDown,
Direction::BottomUp,
] {
ui.radio_value(&mut self.main_dir, dir, format!("{:?}", dir));
}
});
ui.horizontal(|ui| {
ui.checkbox(&mut self.main_wrap, "Main wrap")
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
if self.main_wrap {
if self.main_dir.is_horizontal() {
ui.add(Slider::f32(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
} else {
ui.add(
Slider::f32(&mut self.wrap_column_width, 0.0..=200.0).text("Column width"),
);
}
}
});
ui.horizontal(|ui| {
ui.label("Cross Align:");
for &align in &[Align::Min, Align::Center, Align::Max] {
ui.radio_value(&mut self.cross_align, align, format!("{:?}", align));
}
});
ui.checkbox(&mut self.cross_justify, "Cross Justified")
.on_hover_text("Try to fill full width/height (e.g. buttons)");
}
pub fn demo_ui(&mut self, ui: &mut Ui) {
ui.monospace("Example widgets:");
for _ in 0..3 {
ui.label("label");
} }
if ui for _ in 0..3 {
.add(RadioButton::new(self.align == None, "Justified")) let mut dummy = false;
.on_hover_text("Try to fill full width/height (e.g. buttons)") ui.checkbox(&mut dummy, "checkbox");
.clicked }
{ for _ in 0..3 {
self.align = None; let _ = ui.button("button");
} }
} }
} }

View file

@ -345,7 +345,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight:
(time % 1.0 * 100.0).floor() (time % 1.0 * 100.0).floor()
); );
ui.with_layout(Layout::horizontal(Align::Center).reverse(), |ui| { ui.with_layout(Layout::right_to_left(), |ui| {
if ui if ui
.add(Button::new(time).text_style(TextStyle::Monospace)) .add(Button::new(time).text_style(TextStyle::Monospace))
.clicked .clicked

View file

@ -44,7 +44,7 @@ pub fn drop_target<R>(
let margin = Vec2::splat(4.0); let margin = Vec2::splat(4.0);
let outer_rect_bounds = ui.available(); let outer_rect_bounds = ui.available_rect_before_wrap();
let inner_rect = outer_rect_bounds.shrink2(margin); let inner_rect = outer_rect_bounds.shrink2(margin);
let where_to_put_background = ui.painter().add(PaintCmd::Noop); let where_to_put_background = ui.painter().add(PaintCmd::Noop);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); let mut content_ui = ui.child_ui(inner_rect, *ui.layout());

View file

@ -52,8 +52,12 @@ impl FractalClock {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
let painter = Painter::new(ui.ctx().clone(), ui.layer_id(), ui.available_finite()); let painter = Painter::new(
self.fractal_ui(&painter); ui.ctx().clone(),
ui.layer_id(),
ui.available_rect_before_wrap_finite(),
);
self.paint(&painter);
Frame::popup(ui.style()) Frame::popup(ui.style())
.fill(Rgba::luminance_alpha(0.02, 0.5).into()) .fill(Rgba::luminance_alpha(0.02, 0.5).into())
@ -97,7 +101,7 @@ impl FractalClock {
); );
} }
fn fractal_ui(&mut self, painter: &Painter) { fn paint(&mut self, painter: &Painter) {
let rect = painter.clip_rect(); let rect = painter.clip_rect();
struct Hand { struct Hand {

View file

@ -46,28 +46,34 @@ impl Default for Widgets {
impl Widgets { impl Widgets {
pub fn ui(&mut self, ui: &mut Ui) { pub fn ui(&mut self, ui: &mut Ui) {
ui.add(crate::__egui_github_link_file_line!()); ui.add(__egui_github_link_file_line!());
ui.horizontal(|ui| { ui.horizontal_wrapped_for_text(TextStyle::Body, |ui| {
ui.style_mut().spacing.item_spacing.x = 0.0; ui.label("Long text will wrap, just as you would expect.");
ui.add(Label::new("Text can have ").text_color(srgba(110, 255, 110, 255))); ui.add(Label::new("Text can have").text_color(srgba(110, 255, 110, 255)));
ui.add(Label::new("color ").text_color(srgba(128, 140, 255, 255))); ui.add(Label::new("color").text_color(srgba(128, 140, 255, 255)));
ui.add(Label::new("and tooltips.")).on_hover_text( ui.add(Label::new("and tooltips.")).on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.", "This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
); );
});
ui.label("Tooltips can be more than just simple text.")
.on_hover_ui(|ui| {
ui.heading("The name of the tooltip");
ui.horizontal(|ui| {
ui.label("This tooltip was created with");
ui.monospace(".on_hover_ui(...)");
});
let _ = ui.button("A button you can never press");
});
ui.label("Ευρηκα! τ = 2×π") ui.label("You can mix in other widgets into text, like this");
.on_hover_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text."); let _ = ui.small_button("button");
ui.label(".");
ui.label("There is also (limited) non-ASCII support: Ευρηκα! τ = 2×π")
.on_hover_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text.");
});
let tooltip_ui = |ui: &mut Ui| {
ui.heading("The name of the tooltip");
ui.horizontal(|ui| {
ui.label("This tooltip was created with");
ui.monospace(".on_hover_ui(...)");
});
let _ = ui.button("A button you can never press");
};
ui.label("Tooltips can be more than just simple text.")
.on_hover_ui(tooltip_ui);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.radio_value(&mut self.radio, Enum::First, "First"); ui.radio_value(&mut self.radio, Enum::First, "First");

View file

@ -16,8 +16,8 @@ impl Texture {
return; return;
} }
let mut size = vec2(self.width as f32, self.height as f32); let mut size = vec2(self.width as f32, self.height as f32);
if size.x > ui.available().width() { if size.x > ui.available_width() {
size *= ui.available().width() / size.x; size *= ui.available_width() / size.x;
} }
let rect = ui.allocate_space(size); let rect = ui.allocate_space(size);
let mut triangles = Triangles::default(); let mut triangles = Triangles::default();

View file

@ -2,18 +2,92 @@ use crate::{math::*, Align};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// `Layout` direction (horizontal or vertical). /// This describes the bounds and existing contents of an `Ui`.
/// It is what is used and updated by `Layout` when adding new widgets.
#[derive(Clone, Copy, Debug)]
pub 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);
}
}
// ----------------------------------------------------------------------------
/// Main layout direction
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Direction { pub enum Direction {
Horizontal, LeftToRight,
Vertical, RightToLeft,
TopDown,
BottomUp,
} }
impl Default for Direction { impl Direction {
fn default() -> Direction { pub fn is_horizontal(self) -> bool {
Direction::Vertical 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,
}
} }
} }
@ -23,256 +97,393 @@ impl Default for Direction {
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Layout { pub struct Layout {
/// Lay out things horizontally or vertically? Main axis. /// Main axis direction
dir: 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. /// How to align things on the cross axis.
/// For vertical layouts: put things to left, center or right? /// For vertical layouts: put things to left, center or right?
/// For horizontal layouts: put things to top, center or bottom? /// For horizontal layouts: put things to top, center or bottom?
/// `None` means justified, which means full width (vertical layout) or height (horizontal layouts). cross_align: Align,
align: Option<Align>,
/// Lay out things in reversed order, i.e. from the right or bottom-up. /// Justify the cross axis?
reversed: bool, /// 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 { impl Default for Layout {
fn default() -> Self { fn default() -> Self {
// TODO: Get from `Style` instead.
// This is a very euro-centric default.
Self { Self {
dir: Direction::Vertical, main_dir: Direction::TopDown,
align: Some(Align::Min), main_wrap: false,
reversed: false, cross_align: Align::left(),
cross_justify: false,
} }
} }
} }
impl Layout { impl Layout {
/// None align means justified, e.g. fill full width/height. pub(crate) fn from_main_dir_and_cross_align(main_dir: Direction, cross_align: Align) -> Self {
pub fn from_dir_align(dir: Direction, align: Option<Align>) -> Self {
Self { Self {
dir, main_dir,
align, main_wrap: false,
reversed: false, cross_align,
cross_justify: false,
} }
} }
pub fn vertical(align: Align) -> Self { pub fn left_to_right() -> Self {
Self { Self {
dir: Direction::Vertical, main_dir: Direction::LeftToRight,
align: Some(align), main_wrap: false,
reversed: false, cross_align: Align::Center,
cross_justify: false,
} }
} }
pub fn horizontal(align: Align) -> Self { pub fn right_to_left() -> Self {
Self { Self {
dir: Direction::Horizontal, main_dir: Direction::RightToLeft,
align: Some(align), main_wrap: false,
reversed: false, cross_align: Align::Center,
cross_justify: false,
} }
} }
/// Full-width layout. pub fn top_down(cross_align: Align) -> Self {
/// Nice for menus etc where each button is full width.
pub fn justified(dir: Direction) -> Self {
Self { Self {
dir, main_dir: Direction::TopDown,
align: None, main_wrap: false,
reversed: false, cross_align,
cross_justify: false,
} }
} }
#[must_use] pub fn bottom_up(cross_align: Align) -> Self {
pub fn reverse(self) -> Self {
Self { Self {
dir: self.dir, main_dir: Direction::BottomUp,
align: self.align, main_wrap: false,
reversed: !self.reversed, cross_align,
cross_justify: false,
} }
} }
#[must_use] #[deprecated = "Use `top_down`"]
pub fn with_reversed(self, reversed: bool) -> Self { pub fn vertical(cross_align: Align) -> Self {
if reversed { Self::top_down(cross_align)
self.reverse() }
#[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
}
}
// ------------------------------------------------------------------------
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,
}
}
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 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)
}
pub(crate) 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())
}
pub(crate) 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.
/// Wor wrapping layouts, this is the maximum (after wrap)
pub 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 { } else {
self self.available_from_cursor_max_rect(r.cursor, r.max_rect)
} .size()
}
pub fn dir(self) -> Direction {
self.dir
}
pub fn align(self) -> Option<Align> {
self.align
}
pub fn is_reversed(self) -> bool {
self.reversed
}
pub fn initial_cursor(self, max_rect: Rect) -> Pos2 {
match self.dir {
Direction::Horizontal => {
if self.reversed {
max_rect.right_top()
} else {
max_rect.left_top()
}
}
Direction::Vertical => {
if self.reversed {
max_rect.left_bottom()
} else {
max_rect.left_top()
}
}
} }
} }
/// Given the cursor in the region, how much space is available /// Given the cursor in the region, how much space is available
/// for the next widget? /// for the next widget?
pub fn available(self, cursor: Pos2, max_rect: Rect) -> Rect { fn available_from_cursor_max_rect(self, cursor: Pos2, max_rect: Rect) -> Rect {
let mut rect = max_rect; let mut rect = max_rect;
match self.dir {
Direction::Horizontal => { match self.main_dir {
rect.min.y = cursor.y; Direction::LeftToRight => {
if self.reversed {
rect.max.x = cursor.x;
} else {
rect.min.x = cursor.x;
}
}
Direction::Vertical => {
rect.min.x = cursor.x; rect.min.x = cursor.x;
if self.reversed { rect.min.y = cursor.y;
rect.max.y = cursor.y; }
} else { Direction::RightToLeft => {
rect.min.y = cursor.y; 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 rect
} }
/// Returns where to put the next widget that is of the given size.
/// The returned "outer" `Rect` will always be justified along the cross axis.
/// This is what you then pass to `advance_after_outer_rect`.
/// Use `justify_or_align` to get the inner `Rect`.
#[allow(clippy::collapsible_if)]
pub 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 fn justify_or_align(self, mut rect: Rect, child_size: Vec2) -> Rect {
if self.main_dir.is_horizontal() {
debug_assert!((rect.width() - child_size.x).abs() < 0.1);
if self.cross_justify {
rect // fill full height
} else {
rect.min.y += match self.cross_align {
Align::Min => 0.0,
Align::Center => 0.5 * (rect.size().y - child_size.y),
Align::Max => rect.size().y - child_size.y,
};
rect.max.y = rect.min.y + child_size.y;
rect
}
} else {
debug_assert!((rect.height() - child_size.y).abs() < 0.1);
if self.cross_justify {
rect // justified: fill full width
} else {
rect.min.x += match self.cross_align {
Align::Min => 0.0,
Align::Center => 0.5 * (rect.size().x - child_size.x),
Align::Max => rect.size().x - child_size.x,
};
rect.max.x = rect.min.x + child_size.x;
rect
}
}
}
/// Advance the cursor by this many points. /// Advance the cursor by this many points.
pub fn advance_cursor(self, cursor: &mut Pos2, amount: f32) { pub fn advance_cursor(self, region: &mut Region, amount: f32) {
match self.dir() { match self.main_dir {
Direction::Horizontal => { Direction::LeftToRight => region.cursor.x += amount,
if self.is_reversed() { Direction::RightToLeft => region.cursor.x -= amount,
cursor.x -= amount; Direction::TopDown => region.cursor.y += amount,
} else { Direction::BottomUp => region.cursor.y -= amount,
cursor.x += amount;
}
}
Direction::Vertical => {
if self.is_reversed() {
cursor.y -= amount;
} else {
cursor.y += amount;
}
}
} }
} }
/// Advance the cursor by this spacing /// Advance the cursor by this spacing
pub fn advance_cursor2(self, cursor: &mut Pos2, amount: Vec2) { pub fn advance_cursor2(self, region: &mut Region, amount: Vec2) {
match self.dir() { if self.main_dir.is_horizontal() {
Direction::Horizontal => self.advance_cursor(cursor, amount.x), self.advance_cursor(region, amount.x)
Direction::Vertical => self.advance_cursor(cursor, amount.y),
}
}
pub fn rect_from_cursor_size(self, cursor: Pos2, size: Vec2) -> Rect {
let mut rect = Rect::from_min_size(cursor, size);
match self.dir {
Direction::Horizontal => {
if self.reversed {
rect.min.x = cursor.x - size.x;
rect.max.x = rect.min.x - size.x
}
}
Direction::Vertical => {
if self.reversed {
rect.min.y = cursor.y - size.y;
rect.max.y = rect.min.y - size.y
}
}
}
rect
}
/// Reserve this much space and move the cursor.
/// Returns where to put the widget.
///
/// ## How sizes are negotiated
/// Each widget should have a *minimum desired size* and a *desired size*.
/// When asking for space, ask AT LEAST for you minimum, and don't ask for more than you need.
/// If you want to fill the space, ask about `available().size()` and use that.
///
/// You may get MORE space than you asked for, for instance
/// for `Justified` aligned layouts, like in menus.
///
/// You may get LESS space than you asked for if the current layout won't fit what you asked for.
pub fn allocate_space(
self,
cursor: &mut Pos2,
available_size: Vec2,
minimum_child_size: Vec2,
) -> Rect {
let available_size = available_size.at_least(minimum_child_size);
let mut child_size = minimum_child_size;
let mut child_move = Vec2::default();
let mut cursor_change = Vec2::default();
match self.dir {
Direction::Horizontal => {
if let Some(align) = self.align {
child_move.y += match align {
Align::Min => 0.0,
Align::Center => 0.5 * (available_size.y - child_size.y),
Align::Max => available_size.y - child_size.y,
};
} else {
// justified: fill full height
child_size.y = child_size.y.max(available_size.y);
}
cursor_change.x += child_size.x;
}
Direction::Vertical => {
if let Some(align) = self.align {
child_move.x += match align {
Align::Min => 0.0,
Align::Center => 0.5 * (available_size.x - child_size.x),
Align::Max => available_size.x - child_size.x,
};
} else {
// justified: fill full width
child_size.x = child_size.x.max(available_size.x);
};
cursor_change.y += child_size.y;
}
}
if self.is_reversed() {
let child_pos = *cursor + child_move;
let child_pos = match self.dir {
Direction::Horizontal => child_pos + vec2(-child_size.x, 0.0),
Direction::Vertical => child_pos + vec2(0.0, -child_size.y),
};
*cursor -= cursor_change;
Rect::from_min_size(child_pos, child_size)
} else { } else {
let child_pos = *cursor + child_move; self.advance_cursor(region, amount.y)
*cursor += cursor_change;
Rect::from_min_size(child_pos, child_size)
} }
} }
/// Advance cursor after a widget was added to a specific rectangle.
/// `outer_rect` is a hack needed because the Vec2 cursor is not quite sufficient to keep track
/// of what is happening when we are doing wrapping layouts.
pub fn advance_after_outer_rect(
self,
region: &mut Region,
outer_rect: Rect,
inner_rect: Rect,
item_spacing: Vec2,
) {
region.cursor = match self.main_dir {
Direction::LeftToRight => pos2(inner_rect.right() + item_spacing.x, outer_rect.top()),
Direction::RightToLeft => pos2(inner_rect.left() - item_spacing.x, outer_rect.top()),
Direction::TopDown => pos2(outer_rect.left(), inner_rect.bottom() + item_spacing.y),
Direction::BottomUp => pos2(outer_rect.left(), inner_rect.top() - item_spacing.y),
};
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -280,31 +491,32 @@ impl Layout {
/// ## Debug stuff /// ## Debug stuff
impl Layout { impl Layout {
/// Shows where the next widget is going to be placed /// Shows where the next widget is going to be placed
pub fn debug_paint_cursor(&self, cursor: Pos2, painter: &crate::Painter) { pub fn debug_paint_cursor(&self, region: &Region, painter: &crate::Painter) {
use crate::paint::*; use crate::paint::*;
let cursor = region.cursor;
let color = color::GREEN; let color = color::GREEN;
let stroke = Stroke::new(2.0, color); let stroke = Stroke::new(2.0, color);
let align; let align;
match self.dir { match self.main_dir {
Direction::Horizontal => { Direction::LeftToRight => {
if self.reversed { painter.debug_arrow(cursor, vec2(1.0, 0.0), stroke);
painter.debug_arrow(cursor, vec2(-1.0, 0.0), stroke); align = (Align::Min, Align::Min);
align = (Align::Max, Align::Min);
} else {
painter.debug_arrow(cursor, vec2(1.0, 0.0), stroke);
align = (Align::Min, Align::Min);
}
} }
Direction::Vertical => { Direction::RightToLeft => {
if self.reversed { painter.debug_arrow(cursor, vec2(-1.0, 0.0), stroke);
painter.debug_arrow(cursor, vec2(0.0, -1.0), stroke); align = (Align::Max, Align::Min);
align = (Align::Min, Align::Max); }
} else { Direction::TopDown => {
painter.debug_arrow(cursor, vec2(0.0, 1.0), stroke); painter.debug_arrow(cursor, vec2(0.0, 1.0), stroke);
align = (Align::Min, Align::Min); align = (Align::Min, Align::Min);
} }
Direction::BottomUp => {
painter.debug_arrow(cursor, vec2(0.0, -1.0), stroke);
align = (Align::Min, Align::Max);
} }
} }

View file

@ -54,7 +54,7 @@ pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Respo
// Take full width and fixed height: // Take full width and fixed height:
let height = ui.style().spacing.interact_size.y; let height = ui.style().spacing.interact_size.y;
ui.set_min_size(vec2(ui.available().width(), height)); ui.set_min_size(vec2(ui.available_width(), height));
add_contents(ui) add_contents(ui)
}) })
@ -111,7 +111,10 @@ fn menu_impl<'c>(
style.visuals.widgets.inactive.bg_fill = TRANSPARENT; style.visuals.widgets.inactive.bg_fill = TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::none(); style.visuals.widgets.inactive.bg_stroke = Stroke::none();
ui.set_style(style); ui.set_style(style);
ui.with_layout(Layout::justified(Direction::Vertical), add_contents); ui.with_layout(
Layout::top_down(Align::left()).with_cross_justify(true),
add_contents,
);
}) })
}); });

View file

@ -119,6 +119,10 @@ impl Font {
self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect) self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect)
} }
pub fn glyph_width(&self, c: char) -> f32 {
self.glyph_info(c).advance_width
}
/// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`) /// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`)
fn glyph_info(&self, c: char) -> GlyphInfo { fn glyph_info(&self, c: char) -> GlyphInfo {
{ {
@ -163,7 +167,20 @@ impl Font {
galley galley
} }
/// Always returns at least one row.
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley { pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
self.layout_multiline_with_indentation_and_max_width(text, 0.0, max_width_in_points)
}
/// * `first_row_indentation`: extra space before the very first character (in points).
/// * `max_width_in_points`: wrapping width.
/// Always returns at least one row.
pub fn layout_multiline_with_indentation_and_max_width(
&self,
text: String,
first_row_indentation: f32,
max_width_in_points: f32,
) -> Galley {
let row_height = self.row_height(); let row_height = self.row_height();
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut rows = Vec::new(); let mut rows = Vec::new();
@ -178,8 +195,16 @@ impl Font {
assert!(paragraph_start <= paragraph_end); assert!(paragraph_start <= paragraph_end);
let paragraph_text = &text[paragraph_start..paragraph_end]; let paragraph_text = &text[paragraph_start..paragraph_end];
let mut paragraph_rows = let line_indentation = if rows.is_empty() {
self.layout_paragraph_max_width(paragraph_text, max_width_in_points); first_row_indentation
} else {
0.0
};
let mut paragraph_rows = self.layout_paragraph_max_width(
paragraph_text,
line_indentation,
max_width_in_points,
);
assert!(!paragraph_rows.is_empty()); assert!(!paragraph_rows.is_empty());
paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some(); paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some();
@ -248,10 +273,16 @@ impl Font {
/// A paragraph is text with no line break character in it. /// A paragraph is text with no line break character in it.
/// The text will be wrapped by the given `max_width_in_points`. /// The text will be wrapped by the given `max_width_in_points`.
fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Row> { /// Always returns at least one row.
fn layout_paragraph_max_width(
&self,
text: &str,
mut first_row_indentation: f32,
max_width_in_points: f32,
) -> Vec<Row> {
if text == "" { if text == "" {
return vec![Row { return vec![Row {
x_offsets: vec![0.0], x_offsets: vec![first_row_indentation],
y_min: 0.0, y_min: 0.0,
y_max: self.row_height(), y_max: self.row_height(),
ends_with_newline: false, ends_with_newline: false,
@ -260,12 +291,7 @@ impl Font {
let full_x_offsets = self.layout_single_row_fragment(text); let full_x_offsets = self.layout_single_row_fragment(text);
let mut row_start_x = full_x_offsets[0]; let mut row_start_x = 0.0; // NOTE: BEFORE the `first_row_indentation`.
{
#![allow(clippy::float_cmp)]
assert_eq!(row_start_x, 0.0);
}
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut row_start_idx = 0; let mut row_start_idx = 0;
@ -277,40 +303,40 @@ impl Font {
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() { for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
debug_assert!(chr != '\n'); debug_assert!(chr != '\n');
let potential_row_width = x - row_start_x; let potential_row_width = first_row_indentation + x - row_start_x;
if potential_row_width > max_width_in_points { if potential_row_width > max_width_in_points {
if let Some(last_space_idx) = last_space { if let Some(last_space_idx) = last_space {
let include_trailing_space = true; // We include the trailing space in the row:
let row = if include_trailing_space { let row = Row {
Row { x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1] .iter()
.iter() .map(|x| first_row_indentation + x - row_start_x)
.map(|x| x - row_start_x) .collect(),
.collect(), y_min: cursor_y,
y_min: cursor_y, y_max: cursor_y + self.row_height(),
y_max: cursor_y + self.row_height(), ends_with_newline: false,
ends_with_newline: false,
}
} else {
Row {
x_offsets: full_x_offsets[row_start_idx..=last_space_idx]
.iter()
.map(|x| x - row_start_x)
.collect(),
y_min: cursor_y,
y_max: cursor_y + self.row_height(),
ends_with_newline: false,
}
}; };
row.sanity_check(); row.sanity_check();
out_rows.push(row); out_rows.push(row);
row_start_idx = last_space_idx + 1; row_start_idx = last_space_idx + 1;
row_start_x = full_x_offsets[row_start_idx]; row_start_x = first_row_indentation + full_x_offsets[row_start_idx];
last_space = None; last_space = None;
cursor_y += self.row_height(); cursor_y = self.round_to_pixel(cursor_y + self.row_height());
cursor_y = self.round_to_pixel(cursor_y); } else if out_rows.is_empty() && first_row_indentation > 0.0 {
assert_eq!(row_start_idx, 0);
// Allow the first row to be completely empty, because we know there will be more space on the next row:
let row = Row {
x_offsets: vec![first_row_indentation],
y_min: cursor_y,
y_max: cursor_y + self.row_height(),
ends_with_newline: false,
};
row.sanity_check();
out_rows.push(row);
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
first_row_indentation = 0.0; // Continue all other rows as if there is no indentation
} }
} }
@ -324,7 +350,7 @@ impl Font {
let row = Row { let row = Row {
x_offsets: full_x_offsets[row_start_idx..] x_offsets: full_x_offsets[row_start_idx..]
.iter() .iter()
.map(|x| x - row_start_x) .map(|x| first_row_indentation + x - row_start_x)
.collect(), .collect(),
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.row_height(), y_max: cursor_y + self.row_height(),

View file

@ -196,6 +196,13 @@ impl Row {
self.y_max - self.y_min self.y_max - self.y_min
} }
pub fn rect(&self) -> Rect {
Rect::from_min_max(
pos2(self.min_x(), self.y_min),
pos2(self.max_x(), self.y_max),
)
}
/// Closest char at the desired x coordinate. /// Closest char at the desired x coordinate.
/// Returns something in the range `[0, char_count_excluding_newline()]`. /// Returns something in the range `[0, char_count_excluding_newline()]`.
pub fn char_at(&self, desired_x: f32) -> usize { pub fn char_at(&self, desired_x: f32) -> usize {

View file

@ -22,41 +22,19 @@ pub struct Ui {
/// They are therefore only good for Id:s that has no state. /// They are therefore only good for Id:s that has no state.
next_auto_id: u64, next_auto_id: u64,
/// Specifies paint layer, clip rectangle and a reference to `Context`.
painter: Painter, painter: Painter,
/// This is the minimal size of the `Ui`. /// The `Style` (visuals, spacing, etc) of this ui.
/// When adding new widgets, this will generally expand. /// Commonly many `Ui`:s share the same `Style`.
/// /// The `Ui` implements copy-on-write for this.
/// 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.
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.
max_rect: Rect,
/// Override default style in this ui
style: Arc<Style>, style: Arc<Style>,
/// The strategy for where to put the next widget.
layout: Layout, layout: Layout,
/// Where the next widget will be put. /// Sizes/bounds and cursor used by `Layout`.
/// Progresses along self.dir. region: Region,
/// Initially set to rect.min
/// 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.
cursor: Pos2, // TODO: move into Layout?
} }
impl Ui { impl Ui {
@ -72,36 +50,28 @@ impl Ui {
) -> Self { ) -> Self {
let style = ctx.style(); let style = ctx.style();
let layout = Layout::default(); let layout = Layout::default();
let cursor = layout.initial_cursor(max_rect); let region = layout.region_from_max_rect(max_rect);
let min_size = Vec2::zero(); // TODO: From Style
let min_rect = layout.rect_from_cursor_size(cursor, min_size);
Ui { Ui {
id, id,
next_auto_id: id.with("auto").value(), next_auto_id: id.with("auto").value(),
painter: Painter::new(ctx, layer_id, clip_rect), painter: Painter::new(ctx, layer_id, clip_rect),
min_rect,
max_rect,
style, style,
layout, layout,
cursor, region,
} }
} }
pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self { pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self {
self.next_auto_id = self.next_auto_id.wrapping_add(1); self.next_auto_id = self.next_auto_id.wrapping_add(1);
let cursor = layout.initial_cursor(max_rect); let region = layout.region_from_max_rect(max_rect);
let min_size = Vec2::zero(); // TODO: From Style
let min_rect = layout.rect_from_cursor_size(cursor, min_size);
Ui { Ui {
id: self.id.with("child"), id: self.id.with("child"),
next_auto_id: Id::new(self.next_auto_id).with("child").value(), next_auto_id: Id::new(self.next_auto_id).with("child").value(),
painter: self.painter.clone(), painter: self.painter.clone(),
min_rect,
max_rect,
style: self.style.clone(), style: self.style.clone(),
layout, layout,
cursor, region,
} }
} }
@ -210,12 +180,12 @@ impl Ui {
/// ///
/// This will grow as new widgets are added, but never shrink. /// This will grow as new widgets are added, but never shrink.
pub fn min_rect(&self) -> Rect { pub fn min_rect(&self) -> Rect {
self.min_rect self.region.min_rect
} }
/// Size of content; same as `min_rect().size()` /// Size of content; same as `min_rect().size()`
pub fn min_size(&self) -> Vec2 { pub fn min_size(&self) -> Vec2 {
self.min_rect.size() self.min_rect().size()
} }
/// New widgets will *try* to fit within this rectangle. /// New widgets will *try* to fit within this rectangle.
@ -226,32 +196,19 @@ impl Ui {
/// If a new widget doesn't fit within the `max_rect` then the /// If a new widget doesn't fit within the `max_rect` then the
/// `Ui` will make room for it by expanding both `min_rect` and `max_rect`. /// `Ui` will make room for it by expanding both `min_rect` and `max_rect`.
pub fn max_rect(&self) -> Rect { pub fn max_rect(&self) -> Rect {
self.max_rect self.region.max_rect
} }
/// Used for animation, kind of hacky /// Used for animation, kind of hacky
pub(crate) fn force_set_min_rect(&mut self, min_rect: Rect) { pub(crate) fn force_set_min_rect(&mut self, min_rect: Rect) {
self.min_rect = min_rect; self.region.min_rect = min_rect;
} }
/// This is like `max_rect()`, but will never be infinite. /// This is like `max_rect()`, but will never be infinite.
/// If the desired rect is infinite ("be as big as you want") /// If the desired rect is infinite ("be as big as you want")
/// this will be bounded by `min_rect` instead. /// this will be bounded by `min_rect` instead.
pub fn max_rect_finite(&self) -> Rect { pub fn max_rect_finite(&self) -> Rect {
let mut result = self.max_rect; self.region.max_rect_finite()
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
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -266,24 +223,28 @@ impl Ui {
/// Set the maximum width of the ui. /// Set the maximum width of the ui.
/// You won't be able to shrink it below the current minimum size. /// You won't be able to shrink it below the current minimum size.
pub fn set_max_width(&mut self, width: f32) { pub fn set_max_width(&mut self, width: f32) {
if self.layout.dir() == Direction::Horizontal && self.layout.is_reversed() { if self.layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(self.min_rect.max.x, self.max_rect.max.x); debug_assert_eq!(self.min_rect().max.x, self.max_rect().max.x);
self.max_rect.min.x = self.max_rect.max.x - width.at_least(self.min_rect.width()); self.region.max_rect.min.x =
self.region.max_rect.max.x - width.at_least(self.min_rect().width());
} else { } else {
debug_assert_eq!(self.min_rect.min.x, self.max_rect.min.x); debug_assert_eq!(self.min_rect().min.x, self.region.max_rect.min.x);
self.max_rect.max.x = self.max_rect.min.x + width.at_least(self.min_rect.width()); self.region.max_rect.max.x =
self.region.max_rect.min.x + width.at_least(self.min_rect().width());
} }
} }
/// Set the maximum height of the ui. /// Set the maximum height of the ui.
/// You won't be able to shrink it below the current minimum size. /// You won't be able to shrink it below the current minimum size.
pub fn set_max_height(&mut self, height: f32) { pub fn set_max_height(&mut self, height: f32) {
if self.layout.dir() == Direction::Vertical && self.layout.is_reversed() { if self.layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(self.min_rect.max.y, self.max_rect.max.y); debug_assert_eq!(self.min_rect().max.y, self.region.max_rect.max.y);
self.max_rect.min.y = self.max_rect.max.y - height.at_least(self.min_rect.height()); self.region.max_rect.min.y =
self.region.max_rect.max.y - height.at_least(self.min_rect().height());
} else { } else {
debug_assert_eq!(self.min_rect.min.y, self.max_rect.min.y); debug_assert_eq!(self.min_rect().min.y, self.region.max_rect.min.y);
self.max_rect.max.y = self.max_rect.min.y + height.at_least(self.min_rect.height()); self.region.max_rect.max.y =
self.region.max_rect.min.y + height.at_least(self.min_rect().height());
} }
} }
@ -299,27 +260,31 @@ impl Ui {
/// Set the minimum width of the ui. /// Set the minimum width of the ui.
/// This can't shrink the ui, only make it larger. /// This can't shrink the ui, only make it larger.
pub fn set_min_width(&mut self, width: f32) { pub fn set_min_width(&mut self, width: f32) {
if self.layout.dir() == Direction::Horizontal && self.layout.is_reversed() { if self.layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(self.min_rect.max.x, self.max_rect.max.x); debug_assert_eq!(self.region.min_rect.max.x, self.region.max_rect.max.x);
self.min_rect.min.x = self.min_rect.min.x.min(self.min_rect.max.x - width); let min_rect = &mut self.region.min_rect;
min_rect.min.x = min_rect.min.x.min(min_rect.max.x - width);
} else { } else {
debug_assert_eq!(self.min_rect.min.x, self.max_rect.min.x); debug_assert_eq!(self.region.min_rect.min.x, self.region.max_rect.min.x);
self.min_rect.max.x = self.min_rect.max.x.max(self.min_rect.min.x + width); let min_rect = &mut self.region.min_rect;
min_rect.max.x = min_rect.max.x.max(min_rect.min.x + width);
} }
self.max_rect = self.max_rect.union(self.min_rect); self.region.max_rect = self.region.max_rect.union(self.min_rect());
} }
/// Set the minimum height of the ui. /// Set the minimum height of the ui.
/// This can't shrink the ui, only make it larger. /// This can't shrink the ui, only make it larger.
pub fn set_min_height(&mut self, height: f32) { pub fn set_min_height(&mut self, height: f32) {
if self.layout.dir() == Direction::Vertical && self.layout.is_reversed() { if self.layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(self.min_rect.max.y, self.max_rect.max.y); debug_assert_eq!(self.region.min_rect.max.y, self.region.max_rect.max.y);
self.min_rect.min.y = self.min_rect.min.y.min(self.min_rect.max.y - height); let min_rect = &mut self.region.min_rect;
min_rect.min.y = min_rect.min.y.min(min_rect.max.y - height);
} else { } else {
debug_assert_eq!(self.min_rect.min.y, self.max_rect.min.y); debug_assert_eq!(self.region.min_rect.min.y, self.region.max_rect.min.y);
self.min_rect.max.y = self.min_rect.max.y.max(self.min_rect.min.y + height); let min_rect = &mut self.region.min_rect;
min_rect.max.y = min_rect.max.y.max(min_rect.min.y + height);
} }
self.max_rect = self.max_rect.union(self.min_rect); self.region.max_rect = self.region.max_rect.union(self.min_rect());
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -339,8 +304,7 @@ impl Ui {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect. /// 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) { pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.min_rect = self.min_rect.union(rect); self.region.expand_to_include_rect(rect);
self.max_rect = self.max_rect.union(rect);
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -349,18 +313,39 @@ impl Ui {
/// The available space at the moment, given the current cursor. /// The available space at the moment, given the current cursor.
/// This how much more space we can take up without overflowing our parent. /// This how much more space we can take up without overflowing our parent.
/// Shrinks as widgets allocate space and the cursor moves. /// Shrinks as widgets allocate space and the cursor moves.
/// A small rectangle should be interpreted as "as little as possible". /// A small size should be interpreted as "as little as possible".
/// An infinite rectangle should be interpreted as "as much as you want". /// An infinite size should be interpreted as "as much as you want".
/// In most layouts the next widget will be put in the top left corner of this `Rect`. pub fn available_size(&self) -> Vec2 {
pub fn available(&self) -> Rect { self.layout.available_size(&self.region)
self.layout.available(self.cursor, self.max_rect())
} }
/// This is like `available()`, but will never be infinite. pub fn available_width(&self) -> f32 {
self.available_size().x
}
/// In case of a wrapping layout, how much space is left on this row/column?
pub fn available_size_before_wrap(&self) -> Vec2 {
self.layout.available_size_before_wrap(&self.region)
}
/// This is like `available_size_before_wrap()`, but will never be infinite.
/// Use this for components that want to grow without bounds (but shouldn't). /// Use this for components that want to grow without bounds (but shouldn't).
/// In most layouts the next widget will be put in the top left corner of this `Rect`. /// In most layouts the next widget will be put in the top left corner of this `Rect`.
pub fn available_finite(&self) -> Rect { pub fn available_size_before_wrap_finite(&self) -> Vec2 {
self.layout.available(self.cursor, self.max_rect_finite()) self.layout
.available_rect_before_wrap_finite(&self.region)
.size()
}
pub fn available_rect_before_wrap(&self) -> Rect {
self.layout.available_rect_before_wrap(&self.region)
}
/// This is like `available_rect_before_wrap()`, but will never be infinite.
/// Use this for components that want to grow without bounds (but shouldn't).
/// In most layouts the next widget will be put in the top left corner of this `Rect`.
pub fn available_rect_before_wrap_finite(&self) -> Rect {
self.layout.available_rect_before_wrap_finite(&self.region)
} }
} }
@ -417,7 +402,7 @@ impl Ui {
/// The direction is dependent on the layout. /// The direction is dependent on the layout.
/// This is useful for creating some extra space between widgets. /// This is useful for creating some extra space between widgets.
pub fn advance_cursor(&mut self, amount: f32) { pub fn advance_cursor(&mut self, amount: f32) {
self.layout.advance_cursor(&mut self.cursor, amount); self.layout.advance_cursor(&mut self.region, amount);
} }
/// Reserve this much space and move the cursor. /// Reserve this much space and move the cursor.
@ -429,16 +414,16 @@ impl Ui {
/// If you want to fill the space, ask about `available().size()` and use that. /// If you want to fill the space, ask about `available().size()` and use that.
/// ///
/// You may get MORE space than you asked for, for instance /// You may get MORE space than you asked for, for instance
/// for `Justified` aligned layouts, like in menus. /// for justified layouts, like in menus.
/// ///
/// You may get LESS space than you asked for if the current layout won't fit what you asked for. /// You may get LESS space than you asked for if the current layout won't fit what you asked for.
pub fn allocate_space(&mut self, desired_size: Vec2) -> Rect { pub fn allocate_space(&mut self, desired_size: Vec2) -> Rect {
// For debug rendering // For debug rendering
let original_size = self.available().size(); let original_available = self.available_size_before_wrap();
let too_wide = desired_size.x > original_size.x; let too_wide = desired_size.x > original_available.x;
let too_high = desired_size.y > original_size.y; let too_high = desired_size.y > original_available.y;
let rect = self.reserve_space_impl(desired_size); let rect = self.allocate_space_impl(desired_size);
let debug_expand_width = self.style().visuals.debug_expand_width; let debug_expand_width = self.style().visuals.debug_expand_width;
let debug_expand_height = self.style().visuals.debug_expand_height; let debug_expand_height = self.style().visuals.debug_expand_height;
@ -455,8 +440,8 @@ impl Ui {
paint_line_seg(rect.left_top(), rect.left_bottom()); paint_line_seg(rect.left_top(), rect.left_bottom());
paint_line_seg(rect.left_center(), rect.right_center()); paint_line_seg(rect.left_center(), rect.right_center());
paint_line_seg( paint_line_seg(
pos2(rect.left() + original_size.x, rect.top()), pos2(rect.left() + original_available.x, rect.top()),
pos2(rect.left() + original_size.x, rect.bottom()), pos2(rect.left() + original_available.x, rect.bottom()),
); );
paint_line_seg(rect.right_top(), rect.right_bottom()); paint_line_seg(rect.right_top(), rect.right_bottom());
} }
@ -473,16 +458,65 @@ impl Ui {
/// Reserve this much space and move the cursor. /// Reserve this much space and move the cursor.
/// Returns where to put the widget. /// Returns where to put the widget.
fn reserve_space_impl(&mut self, child_size: Vec2) -> Rect { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
let available_size = self.available_finite().size();
let child_rect = self
.layout
.allocate_space(&mut self.cursor, available_size, child_size);
let item_spacing = self.style().spacing.item_spacing; let item_spacing = self.style().spacing.item_spacing;
self.layout.advance_cursor2(&mut self.cursor, item_spacing); let outer_child_rect = self
self.expand_to_include_rect(child_rect); .layout
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
self.layout.advance_after_outer_rect(
&mut self.region,
outer_child_rect,
inner_child_rect,
item_spacing,
);
self.region.expand_to_include_rect(inner_child_rect);
self.next_auto_id = self.next_auto_id.wrapping_add(1); self.next_auto_id = self.next_auto_id.wrapping_add(1);
child_rect inner_child_rect
}
pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) {
let item_spacing = self.style().spacing.item_spacing;
self.layout
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing);
self.region.expand_to_include_rect(rect);
}
pub(crate) fn cursor(&self) -> Pos2 {
self.region.cursor
}
/// Allocated the given space and then adds content to that space.
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated.
/// So you can request a lot of space and then use less.
pub fn allocate_ui<R>(
&mut self,
desired_size: Vec2,
add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) {
let item_spacing = self.style().spacing.item_spacing;
let outer_child_rect = self
.layout
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
let mut child_ui = self.child_ui(inner_child_rect, self.layout);
let ret = add_contents(&mut child_ui);
let final_child_rect = child_ui.region.min_rect;
self.layout.advance_after_outer_rect(
&mut self.region,
outer_child_rect.union(final_child_rect),
final_child_rect,
item_spacing,
);
self.region.expand_to_include_rect(final_child_rect);
let response = self.interact_hover(final_child_rect);
(ret, response)
} }
} }
@ -532,12 +566,24 @@ impl Ui {
self.add(TextEdit::multiline(text)) self.add(TextEdit::multiline(text))
} }
/// Usage: `if ui.button("Click me").clicked { ... }`
///
/// Shortcut for `add(Button::new(text))` /// Shortcut for `add(Button::new(text))`
#[must_use = "You should check if the user clicked this with `if ui.button(...).clicked { ... } "] #[must_use = "You should check if the user clicked this with `if ui.button(...).clicked { ... } "]
pub fn button(&mut self, text: impl Into<String>) -> Response { pub fn button(&mut self, text: impl Into<String>) -> Response {
self.add(Button::new(text)) self.add(Button::new(text))
} }
/// A button as small as normal body text.
///
/// Usage: `if ui.small_button("Click me").clicked { ... }`
///
/// Shortcut for `add(Button::new(text).small())`
#[must_use = "You should check if the user clicked this with `if ui.small_button(...).clicked { ... } "]
pub fn small_button(&mut self, text: impl Into<String>) -> Response {
self.add(Button::new(text).small())
}
/// Show a checkbox. /// Show a checkbox.
pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<String>) -> Response { pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<String>) -> Response {
self.add(Checkbox::new(checked, text)) self.add(Checkbox::new(checked, text))
@ -676,7 +722,7 @@ impl Ui {
impl Ui { impl Ui {
/// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance. /// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance.
pub fn wrap<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { pub fn wrap<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) {
let child_rect = self.available(); let child_rect = self.available_rect_before_wrap();
let mut child_ui = self.child_ui(child_rect, self.layout); let mut child_ui = self.child_ui(child_rect, self.layout);
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let size = child_ui.min_size(); let size = child_ui.min_size();
@ -696,19 +742,13 @@ impl Ui {
}) })
} }
/// Create a child ui at the current cursor. #[deprecated = "Use `ui.allocate_ui` instead"]
/// `size` is the desired size. pub fn add_custom_contents(
/// Actual size may be much smaller if `available_size()` is not enough. &mut self,
/// Set `size` to `Vec::infinity()` to get as much space as possible. desired_size: Vec2,
/// Just because you ask for a lot of space does not mean you have to use it! add_contents: impl FnOnce(&mut Ui),
/// After `add_contents` is called the contents of `min_size` ) -> Rect {
/// will decide how much space will be used in the parent ui. self.allocate_ui(desired_size, add_contents).1.rect
pub fn add_custom_contents(&mut self, size: Vec2, add_contents: impl FnOnce(&mut Ui)) -> Rect {
let size = size.at_most(self.available().size());
let child_rect = self.layout.rect_from_cursor_size(self.cursor, size);
let mut child_ui = self.child_ui(child_rect, self.layout);
add_contents(&mut child_ui);
self.allocate_space(child_ui.min_size())
} }
/// A `CollapsingHeader` that starts out collapsed. /// A `CollapsingHeader` that starts out collapsed.
@ -727,11 +767,13 @@ impl Ui {
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) { ) -> (R, Response) {
assert!( assert!(
self.layout().dir() == Direction::Vertical, self.layout.is_vertical(),
"You can only indent vertical layouts" "You can only indent vertical layouts, found {:?}",
self.layout
); );
let indent = vec2(self.style().spacing.indent, 0.0); let indent = vec2(self.style().spacing.indent, 0.0);
let child_rect = Rect::from_min_max(self.cursor + indent, self.max_rect.right_bottom()); // TODO: wrong for reversed layouts let child_rect =
Rect::from_min_max(self.region.cursor + indent, self.max_rect().right_bottom()); // TODO: wrong for reversed layouts
let mut child_ui = Self { let mut child_ui = Self {
id: self.id.with(id_source), id: self.id.with(id_source),
..self.child_ui(child_rect, self.layout) ..self.child_ui(child_rect, self.layout)
@ -768,64 +810,102 @@ impl Ui {
pub fn column(&mut self, column_position: Align, width: f32) -> Self { pub fn column(&mut self, column_position: Align, width: f32) -> Self {
let x = match column_position { let x = match column_position {
Align::Min => 0.0, Align::Min => 0.0,
Align::Center => self.available().width() / 2.0 - width / 2.0, Align::Center => self.available_width() / 2.0 - width / 2.0,
Align::Max => self.available().width() - width, Align::Max => self.available_width() - width,
}; };
self.child_ui( self.child_ui(
Rect::from_min_size( Rect::from_min_size(
self.cursor + vec2(x, 0.0), self.region.cursor + vec2(x, 0.0),
vec2(width, self.available().height()), vec2(width, self.available_size_before_wrap().y),
), ),
self.layout, self.layout,
) )
} }
/// Start a ui with horizontal layout. /// Start a ui with horizontal layout.
/// After you have called this, the registers the contents as any other widget. /// After you have called this, the function registers the contents as any other widget.
/// ///
/// Elements will be centered on the Y axis, i.e. /// Elements will be centered on the Y axis, i.e.
/// adjusted up and down to lie in the center of the horizontal layout. /// adjusted up and down to lie in the center of the horizontal layout.
/// The initial height is `style.spacing.interact_size.y`. /// The initial height is `style.spacing.interact_size.y`.
/// Centering is almost always what you want if you are /// Centering is almost always what you want if you are
/// planning to to mix widgets or just different types of text. /// planning to to mix widgets or use different types of text.
/// ///
/// The returned `Response` will only have checked for mouse hover /// The returned `Response` will only have checked for mouse hover
/// but can be used for tooltips (`on_hover_text`). /// but can be used for tooltips (`on_hover_text`).
/// It also contains the `Rect` used by the horizontal layout. /// It also contains the `Rect` used by the horizontal layout.
pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { pub fn horizontal<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) {
self.horizontal_with_main_wrap(false, add_contents)
}
/// Start a ui with horizontal layout that wraps to a new row
/// when it reaches the right edge of the `max_size`.
/// After you have called this, the function registers the contents as any other widget.
///
/// Elements will be centered on the Y axis, i.e.
/// adjusted up and down to lie in the center of the horizontal layout.
/// The initial height is `style.spacing.interact_size.y`.
/// Centering is almost always what you want if you are
/// planning to to mix widgets or use different types of text.
///
/// The returned `Response` will only have checked for mouse hover
/// but can be used for tooltips (`on_hover_text`).
/// It also contains the `Rect` used by the horizontal layout.
pub fn horizontal_wrapped<R>(
&mut self,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
self.horizontal_with_main_wrap(true, add_contents)
}
/// Like `horizontal_wrapped`, but will set up spacing so that
/// the line size and matches that for a normal label.
///
/// In particular, the space between widgets is the same with as the space character
/// and the line spacing is the same as that for text.
///
/// You can still add any widgets to the layout (not only Labels).
pub fn horizontal_wrapped_for_text<R>(
&mut self,
text_style: TextStyle,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
self.wrap(|ui| {
let font = &ui.fonts()[text_style];
let row_height = font.row_height();
let space_width = font.glyph_width(' ');
let style = ui.style_mut();
style.spacing.interact_size.y = row_height;
style.spacing.item_spacing.x = space_width;
style.spacing.item_spacing.y = 0.0;
ui.horizontal_wrapped(add_contents).0
})
}
fn horizontal_with_main_wrap<R>(
&mut self,
main_wrap: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
let initial_size = vec2( let initial_size = vec2(
self.available().width(), self.available_size_before_wrap_finite().x,
self.style().spacing.interact_size.y, // Assume there will be something interactive on the horizontal layout self.style().spacing.interact_size.y, // Assume there will be something interactive on the horizontal layout
); );
let right_to_left = let layout = if self.layout.prefer_right_to_left() {
(self.layout.dir(), self.layout.align()) == (Direction::Vertical, Some(Align::Max)); Layout::right_to_left()
} else {
Layout::left_to_right()
}
.with_main_wrap(main_wrap);
self.inner_layout( self.allocate_ui(initial_size, |ui| ui.with_layout(layout, add_contents).0)
Layout::horizontal(Align::Center).with_reversed(right_to_left),
initial_size,
add_contents,
)
} }
/// Start a ui with vertical layout. /// Start a ui with vertical layout.
/// Widgets will be left-justified. /// Widgets will be left-justified.
pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { pub fn vertical<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) {
self.with_layout(Layout::vertical(Align::Min), add_contents) self.with_layout(Layout::top_down(Align::Min), add_contents)
}
fn inner_layout<R>(
&mut self,
layout: Layout,
initial_size: Vec2,
add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) {
let child_rect = self.layout.rect_from_cursor_size(self.cursor, initial_size);
let mut child_ui = self.child_ui(child_rect, layout);
let ret = add_contents(&mut child_ui);
let size = child_ui.min_size();
let rect = self.allocate_space(size);
(ret, self.interact_hover(rect))
} }
pub fn with_layout<R>( pub fn with_layout<R>(
@ -833,10 +913,13 @@ impl Ui {
layout: Layout, layout: Layout,
add_contents: impl FnOnce(&mut Self) -> R, add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) { ) -> (R, Response) {
let mut child_ui = self.child_ui(self.available(), layout); let mut child_ui = self.child_ui(self.available_rect_before_wrap(), layout);
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let size = child_ui.min_size(); let rect = child_ui.min_rect();
let rect = self.allocate_space(size); let item_spacing = self.style().spacing.item_spacing;
self.layout
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing);
self.region.expand_to_include_rect(rect);
(ret, self.interact_hover(rect)) (ret, self.interact_hover(rect))
} }
@ -856,14 +939,15 @@ impl Ui {
// TODO: ensure there is space // TODO: ensure there is space
let spacing = self.style().spacing.item_spacing.x; let spacing = self.style().spacing.item_spacing.x;
let total_spacing = spacing * (num_columns as f32 - 1.0); let total_spacing = spacing * (num_columns as f32 - 1.0);
let column_width = (self.available().width() - total_spacing) / (num_columns as f32); let column_width = (self.available_width() - total_spacing) / (num_columns as f32);
let mut columns: Vec<Self> = (0..num_columns) let mut columns: Vec<Self> = (0..num_columns)
.map(|col_idx| { .map(|col_idx| {
let pos = self.cursor + vec2((col_idx as f32) * (column_width + spacing), 0.0); let pos =
self.region.cursor + vec2((col_idx as f32) * (column_width + spacing), 0.0);
let child_rect = Rect::from_min_max( let child_rect = Rect::from_min_max(
pos, pos,
pos2(pos.x + column_width, self.max_rect.right_bottom().y), pos2(pos.x + column_width, self.max_rect().right_bottom().y),
); );
self.child_ui(child_rect, self.layout) self.child_ui(child_rect, self.layout)
}) })
@ -873,7 +957,7 @@ impl Ui {
let mut sum_width = total_spacing; let mut sum_width = total_spacing;
for column in &columns { for column in &columns {
sum_width += column.min_rect.width(); sum_width += column.min_rect().width();
} }
let mut max_height = 0.0; let mut max_height = 0.0;
@ -882,7 +966,7 @@ impl Ui {
max_height = size.y.max(max_height); max_height = size.y.max(max_height);
} }
let size = vec2(self.available().width().max(sum_width), max_height); let size = vec2(self.available_width().max(sum_width), max_height);
self.allocate_space(size); self.allocate_space(size);
result result
} }
@ -894,6 +978,6 @@ impl Ui {
impl Ui { impl Ui {
/// Shows where the next widget is going to be placed /// Shows where the next widget is going to be placed
pub fn debug_paint_cursor(&self) { pub fn debug_paint_cursor(&self) {
self.layout.debug_paint_cursor(self.cursor, &self.painter); self.layout.debug_paint_cursor(&self.region, &self.painter);
} }
} }

View file

@ -6,7 +6,7 @@
#![allow(clippy::new_without_default)] #![allow(clippy::new_without_default)]
use crate::{layout::Direction, *}; use crate::*;
pub mod color_picker; pub mod color_picker;
mod drag_value; mod drag_value;
@ -79,10 +79,7 @@ impl Label {
} }
pub fn layout(&self, ui: &Ui) -> Galley { pub fn layout(&self, ui: &Ui) -> Galley {
let max_width = ui.available().width(); let max_width = ui.available_width();
// Prevent word-wrapping after a single letter, and other silly shit:
// TODO: general "don't force labels and similar to wrap so early"
// TODO: max_width = max_width.at_least(ui.spacing.first_wrap_width);
self.layout_width(ui, max_width) self.layout_width(ui, max_width)
} }
@ -125,10 +122,57 @@ impl Label {
impl Widget for Label { impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let galley = self.layout(ui); if self.multiline
let rect = ui.allocate_space(galley.size); && ui.layout().main_dir() == Direction::LeftToRight
self.paint_galley(ui, rect.min, galley); && ui.layout().main_wrap()
ui.interact_hover(rect) {
// On a wrapping horizontal layout we want text to start after the last widget,
// then continue on the line below! This will take some extra work:
let max_width = ui.available_width();
let first_row_indentation = max_width - ui.available_size_before_wrap().x;
let text_style = self.text_style_or_default(ui.style());
let font = &ui.fonts()[text_style];
let mut galley = font.layout_multiline_with_indentation_and_max_width(
self.text.clone(),
first_row_indentation,
max_width,
);
let pos = pos2(ui.min_rect().left(), ui.cursor().y);
assert!(!galley.rows.is_empty(), "Gallyes are never empty");
let rect = galley.rows[0].rect().translate(vec2(pos.x, pos.y));
ui.advance_cursor_after_rect(rect);
let mut total_response = ui.interact_hover(rect);
let mut y_translation = 0.0;
if let Some(row) = galley.rows.get(1) {
// We could be sharing the first row with e.g. a button, that is higher than text.
// So we need to compensate for that:
if pos.y + row.y_min < ui.min_rect().bottom() {
y_translation = ui.min_rect().bottom() - row.y_min - pos.y;
}
}
for row in galley.rows.iter_mut().skip(1) {
row.y_min += y_translation;
row.y_max += y_translation;
let rect = row.rect().translate(vec2(pos.x, pos.y));
ui.advance_cursor_after_rect(rect);
total_response |= ui.interact_hover(rect);
}
self.paint_galley(ui, pos, galley);
total_response
} else {
let galley = self.layout(ui);
let rect = ui.allocate_space(galley.size);
let rect = ui.layout().align_size_within_rect(galley.size, rect);
self.paint_galley(ui, rect.min, galley);
ui.interact_hover(rect)
}
} }
} }
@ -197,7 +241,7 @@ impl Widget for Hyperlink {
let color = color::LIGHT_BLUE; let color = color::LIGHT_BLUE;
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
let font = &ui.fonts()[text_style]; let font = &ui.fonts()[text_style];
let galley = font.layout_multiline(text, ui.available().width()); let galley = font.layout_multiline(text, ui.available_width());
let rect = ui.allocate_space(galley.size); let rect = ui.allocate_space(galley.size);
let id = ui.make_position_id(); let id = ui.make_position_id();
@ -243,6 +287,7 @@ pub struct Button {
/// None means default for interact /// None means default for interact
fill: Option<Srgba>, fill: Option<Srgba>,
sense: Sense, sense: Sense,
small: bool,
} }
impl Button { impl Button {
@ -253,6 +298,7 @@ impl Button {
text_style: TextStyle::Button, text_style: TextStyle::Button,
fill: Default::default(), fill: Default::default(),
sense: Sense::click(), sense: Sense::click(),
small: false,
} }
} }
@ -276,6 +322,13 @@ impl Button {
self self
} }
/// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self {
self.text_style = TextStyle::Body;
self.small = true;
self
}
/// By default, buttons senses clicks. /// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`. /// Change this to a drag-button with `Sense::drag()`.
pub fn sense(mut self, sense: Sense) -> Self { pub fn sense(mut self, sense: Sense) -> Self {
@ -301,24 +354,29 @@ impl Widget for Button {
text_style, text_style,
fill, fill,
sense, sense,
small,
} = self; } = self;
let button_padding = ui.style().spacing.button_padding; let mut button_padding = ui.style().spacing.button_padding;
if small {
button_padding.y = 0.0;
}
let font = &ui.fonts()[text_style]; let font = &ui.fonts()[text_style];
let galley = font.layout_multiline(text, ui.available().width()); let galley = font.layout_multiline(text, ui.available_width());
let mut desired_size = galley.size + 2.0 * button_padding; let mut desired_size = galley.size + 2.0 * button_padding;
desired_size = desired_size.at_least(ui.style().spacing.interact_size); if !small {
desired_size = desired_size.at_least(ui.style().spacing.interact_size);
}
let rect = ui.allocate_space(desired_size); let rect = ui.allocate_space(desired_size);
let id = ui.make_position_id(); let id = ui.make_position_id();
let response = ui.interact(rect, id, sense); let response = ui.interact(rect, id, sense);
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
// let text_cursor = response.rect.center() - 0.5 * galley.size; // centered-centered (looks bad for justified drop-down menus let text_cursor = ui
let text_cursor = pos2( .layout()
response.rect.left() + button_padding.x, .align_size_within_rect(galley.size, response.rect.shrink2(button_padding))
response.rect.center().y - 0.5 * galley.size.y, .min;
); // left-centered
let fill = fill.unwrap_or(visuals.bg_fill); let fill = fill.unwrap_or(visuals.bg_fill);
ui.painter().rect( ui.painter().rect(
response.rect, response.rect,
@ -379,12 +437,13 @@ impl<'a> Widget for Checkbox<'a> {
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = font.layout_single_line(text); let galley = font.layout_single_line(text);
// let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); // let galley = font.layout_multiline(text, ui.available_width() - total_extra.x);
let mut desired_size = total_extra + galley.size; let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(spacing.interact_size); desired_size = desired_size.at_least(spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width); desired_size.y = desired_size.y.max(icon_width);
let rect = ui.allocate_space(desired_size); let rect = ui.allocate_space(desired_size);
let rect = ui.layout().align_size_within_rect(desired_size, rect);
let id = ui.make_position_id(); let id = ui.make_position_id();
let response = ui.interact(rect, id, Sense::click()); let response = ui.interact(rect, id, Sense::click());
@ -467,12 +526,13 @@ impl Widget for RadioButton {
let button_padding = ui.style().spacing.button_padding; let button_padding = ui.style().spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); let galley = font.layout_multiline(text, ui.available_width() - total_extra.x);
let mut desired_size = total_extra + galley.size; let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(ui.style().spacing.interact_size); desired_size = desired_size.at_least(ui.style().spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width); desired_size.y = desired_size.y.max(icon_width);
let rect = ui.allocate_space(desired_size); let rect = ui.allocate_space(desired_size);
let rect = ui.layout().align_size_within_rect(desired_size, rect);
let id = ui.make_position_id(); let id = ui.make_position_id();
let response = ui.interact(rect, id, Sense::click()); let response = ui.interact(rect, id, Sense::click());
@ -544,7 +604,7 @@ impl Widget for SelectableLabel {
let button_padding = ui.style().spacing.button_padding; let button_padding = ui.style().spacing.button_padding;
let total_extra = button_padding + button_padding; let total_extra = button_padding + button_padding;
let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); let galley = font.layout_multiline(text, ui.available_width() - total_extra.x);
let mut desired_size = total_extra + galley.size; let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(ui.style().spacing.interact_size); desired_size = desired_size.at_least(ui.style().spacing.interact_size);
@ -604,29 +664,26 @@ impl Widget for Separator {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Separator { spacing } = self; let Separator { spacing } = self;
let available_space = ui.available_finite().size(); let available_space = ui.available_size_before_wrap_finite();
let (points, rect) = match ui.layout().dir() { let (points, rect) = if ui.layout().main_dir().is_horizontal() {
Direction::Horizontal => { let rect = ui.allocate_space(vec2(spacing, available_space.y));
let rect = ui.allocate_space(vec2(spacing, available_space.y)); (
( [
[ pos2(rect.center().x, rect.top()),
pos2(rect.center().x, rect.top()), pos2(rect.center().x, rect.bottom()),
pos2(rect.center().x, rect.bottom()), ],
], rect,
rect, )
) } else {
} let rect = ui.allocate_space(vec2(available_space.x, spacing));
Direction::Vertical => { (
let rect = ui.allocate_space(vec2(available_space.x, spacing)); [
( pos2(rect.left(), rect.center().y),
[ pos2(rect.right(), rect.center().y),
pos2(rect.left(), rect.center().y), ],
pos2(rect.right(), rect.center().y), rect,
], )
rect,
)
}
}; };
let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke;
ui.painter().line_segment(points, stroke); ui.painter().line_segment(points, stroke);

View file

@ -228,7 +228,7 @@ impl<'t> Widget for TextEdit<'t> {
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
let font = &ui.fonts()[text_style]; let font = &ui.fonts()[text_style];
let line_spacing = font.row_height(); let line_spacing = font.row_height();
let available_width = ui.available().width(); let available_width = ui.available_width();
let mut galley = if multiline { let mut galley = if multiline {
font.layout_multiline(text.clone(), available_width) font.layout_multiline(text.clone(), available_width)
} else { } else {