Add justified and/or center- and right-aligned text
Label text will now be centered, right-aligned and/or justified based on the layout. Galleys are no longer always pivoted in the left top corner, so now have a Rect rather than just a size.
This commit is contained in:
parent
cbafd10ee4
commit
acb5501fe4
23 changed files with 316 additions and 112 deletions
|
@ -13,6 +13,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
||||||
* `Fonts::layout_job*`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
|
* `Fonts::layout_job*`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
|
* Label text will now be centered, right-aligned and/or justified based on the layout.
|
||||||
* `Hyperlink` will now word-wrap just like a `Label`.
|
* `Hyperlink` will now word-wrap just like a `Label`.
|
||||||
* All `Ui`:s must now have a finite `max_rect`.
|
* All `Ui`:s must now have a finite `max_rect`.
|
||||||
* Deprecated: `max_rect_finite`, `available_size_before_wrap_finite` and `available_rect_before_wrap_finite`.
|
* Deprecated: `max_rect_finite`, `available_size_before_wrap_finite` and `available_rect_before_wrap_finite`.
|
||||||
|
|
|
@ -270,21 +270,21 @@ impl CollapsingHeader {
|
||||||
let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
|
let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
|
||||||
let galley =
|
let galley =
|
||||||
label.layout_width(ui, available.right() - text_pos.x, Color32::TEMPORARY_COLOR);
|
label.layout_width(ui, available.right() - text_pos.x, Color32::TEMPORARY_COLOR);
|
||||||
let text_max_x = text_pos.x + galley.size.x;
|
let text_max_x = text_pos.x + galley.size().x;
|
||||||
|
|
||||||
let mut desired_width = text_max_x + button_padding.x - available.left();
|
let mut desired_width = text_max_x + button_padding.x - available.left();
|
||||||
if ui.visuals().collapsing_header_frame {
|
if ui.visuals().collapsing_header_frame {
|
||||||
desired_width = desired_width.max(available.width()); // fill full width
|
desired_width = desired_width.max(available.width()); // fill full width
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut desired_size = vec2(desired_width, galley.size.y + 2.0 * button_padding.y);
|
let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y);
|
||||||
desired_size = desired_size.at_least(ui.spacing().interact_size);
|
desired_size = desired_size.at_least(ui.spacing().interact_size);
|
||||||
let (_, rect) = ui.allocate_space(desired_size);
|
let (_, rect) = ui.allocate_space(desired_size);
|
||||||
|
|
||||||
let mut header_response = ui.interact(rect, id, Sense::click());
|
let mut header_response = ui.interact(rect, id, Sense::click());
|
||||||
let text_pos = pos2(
|
let text_pos = pos2(
|
||||||
text_pos.x,
|
text_pos.x,
|
||||||
header_response.rect.center().y - galley.size.y / 2.0,
|
header_response.rect.center().y - galley.size().y / 2.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open);
|
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open);
|
||||||
|
|
|
@ -162,9 +162,9 @@ fn combo_box<R>(
|
||||||
ui.fonts()
|
ui.fonts()
|
||||||
.layout_delayed_color(selected.to_string(), TextStyle::Button, f32::INFINITY);
|
.layout_delayed_color(selected.to_string(), TextStyle::Button, f32::INFINITY);
|
||||||
|
|
||||||
let width = galley.size.x + ui.spacing().item_spacing.x + icon_size.x;
|
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
|
||||||
let width = width.at_least(full_minimum_width);
|
let width = width.at_least(full_minimum_width);
|
||||||
let height = galley.size.y.max(icon_size.y);
|
let height = galley.size().y.max(icon_size.y);
|
||||||
|
|
||||||
let (_, rect) = ui.allocate_space(Vec2::new(width, height));
|
let (_, rect) = ui.allocate_space(Vec2::new(width, height));
|
||||||
let button_rect = ui.min_rect().expand2(ui.spacing().button_padding);
|
let button_rect = ui.min_rect().expand2(ui.spacing().button_padding);
|
||||||
|
@ -179,7 +179,7 @@ fn combo_box<R>(
|
||||||
};
|
};
|
||||||
paint_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
paint_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
|
||||||
|
|
||||||
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size, rect);
|
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.galley_with_color(text_rect.min, galley, visuals.text_color());
|
.galley_with_color(text_rect.min, galley, visuals.text_color());
|
||||||
});
|
});
|
||||||
|
|
|
@ -795,9 +795,9 @@ fn show_title_bar(
|
||||||
|
|
||||||
let minimum_width = if collapsible || show_close_button {
|
let minimum_width = if collapsible || show_close_button {
|
||||||
// If at least one button is shown we make room for both buttons (since title is centered):
|
// If at least one button is shown we make room for both buttons (since title is centered):
|
||||||
2.0 * (pad + button_size.x + item_spacing.x) + title_galley.size.x
|
2.0 * (pad + button_size.x + item_spacing.x) + title_galley.size().x
|
||||||
} else {
|
} else {
|
||||||
pad + title_galley.size.x + pad
|
pad + title_galley.size().x + pad
|
||||||
};
|
};
|
||||||
let min_rect = Rect::from_min_size(ui.min_rect().min, vec2(minimum_width, height));
|
let min_rect = Rect::from_min_size(ui.min_rect().min, vec2(minimum_width, height));
|
||||||
let id = ui.advance_cursor_after_rect(min_rect);
|
let id = ui.advance_cursor_after_rect(min_rect);
|
||||||
|
@ -846,8 +846,10 @@ impl TitleBar {
|
||||||
self.title_label = self.title_label.text_color(style.fg_stroke.color);
|
self.title_label = self.title_label.text_color(style.fg_stroke.color);
|
||||||
|
|
||||||
let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
|
let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
|
||||||
let text_pos = emath::align::center_size_in_rect(self.title_galley.size, full_top_rect);
|
let text_pos =
|
||||||
let text_pos = text_pos.left_top() - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better)
|
emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top();
|
||||||
|
let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
|
||||||
|
let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better)
|
||||||
let text_color = ui.visuals().text_color();
|
let text_color = ui.visuals().text_color();
|
||||||
self.title_label
|
self.title_label
|
||||||
.paint_galley(ui, text_pos, self.title_galley, false, text_color);
|
.paint_galley(ui, text_pos, self.title_galley, false, text_color);
|
||||||
|
|
|
@ -221,7 +221,7 @@ impl Painter {
|
||||||
text: impl ToString,
|
text: impl ToString,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color);
|
let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color);
|
||||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size));
|
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||||
let frame_rect = rect.expand(2.0);
|
let frame_rect = rect.expand(2.0);
|
||||||
self.add(Shape::Rect {
|
self.add(Shape::Rect {
|
||||||
rect: frame_rect,
|
rect: frame_rect,
|
||||||
|
@ -343,7 +343,7 @@ impl Painter {
|
||||||
text_color: Color32,
|
text_color: Color32,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
let galley = self.layout_no_wrap(text.to_string(), text_style, text_color);
|
let galley = self.layout_no_wrap(text.to_string(), text_style, text_color);
|
||||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size));
|
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||||
self.galley(rect.min, galley);
|
self.galley(rect.min, galley);
|
||||||
rect
|
rect
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,7 @@ impl Button {
|
||||||
.fonts()
|
.fonts()
|
||||||
.layout_delayed_color(text, text_style, wrap_width);
|
.layout_delayed_color(text, text_style, wrap_width);
|
||||||
|
|
||||||
let mut desired_size = galley.size + 2.0 * button_padding;
|
let mut desired_size = galley.size() + 2.0 * button_padding;
|
||||||
if !small {
|
if !small {
|
||||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ impl Button {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
let text_pos = ui
|
let text_pos = ui
|
||||||
.layout()
|
.layout()
|
||||||
.align_size_within_rect(galley.size, rect.shrink2(button_padding))
|
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||||
.min;
|
.min;
|
||||||
|
|
||||||
if frame {
|
if frame {
|
||||||
|
@ -290,7 +290,7 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
.fonts()
|
.fonts()
|
||||||
.layout_delayed_color(text, text_style, wrap_width);
|
.layout_delayed_color(text, text_style, wrap_width);
|
||||||
|
|
||||||
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, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
@ -306,7 +306,7 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
let text_pos = pos2(
|
let text_pos = pos2(
|
||||||
rect.min.x + button_padding.x + icon_width + icon_spacing,
|
rect.min.x + button_padding.x + icon_width + icon_spacing,
|
||||||
rect.center().y - 0.5 * galley.size.y,
|
rect.center().y - 0.5 * galley.size().y,
|
||||||
);
|
);
|
||||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||||
ui.painter().add(Shape::Rect {
|
ui.painter().add(Shape::Rect {
|
||||||
|
@ -414,7 +414,7 @@ impl Widget for RadioButton {
|
||||||
.fonts()
|
.fonts()
|
||||||
.layout_delayed_color(text, text_style, wrap_width);
|
.layout_delayed_color(text, text_style, wrap_width);
|
||||||
|
|
||||||
let mut desired_size = total_extra + galley.size;
|
let mut desired_size = total_extra + galley.size();
|
||||||
desired_size = desired_size.at_least(ui.spacing().interact_size);
|
desired_size = desired_size.at_least(ui.spacing().interact_size);
|
||||||
desired_size.y = desired_size.y.max(icon_width);
|
desired_size.y = desired_size.y.max(icon_width);
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
@ -423,7 +423,7 @@ impl Widget for RadioButton {
|
||||||
|
|
||||||
let text_pos = pos2(
|
let text_pos = pos2(
|
||||||
rect.min.x + button_padding.x + icon_width + icon_spacing,
|
rect.min.x + button_padding.x + icon_width + icon_spacing,
|
||||||
rect.center().y - 0.5 * galley.size.y,
|
rect.center().y - 0.5 * galley.size().y,
|
||||||
);
|
);
|
||||||
|
|
||||||
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
|
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
|
||||||
|
|
|
@ -171,9 +171,18 @@ impl Label {
|
||||||
|
|
||||||
/// `line_color`: used for underline and strikethrough, if any.
|
/// `line_color`: used for underline and strikethrough, if any.
|
||||||
pub fn layout_width(&self, ui: &Ui, max_width: f32, line_color: Color32) -> Arc<Galley> {
|
pub fn layout_width(&self, ui: &Ui, max_width: f32, line_color: Color32) -> Arc<Galley> {
|
||||||
self.layout_impl(ui, 0.0, max_width, 0.0, line_color)
|
let (halign, justify) = if ui.is_grid() {
|
||||||
|
(Align::LEFT, false) // TODO: remove special Grid hacks like these
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
ui.layout().horizontal_align(),
|
||||||
|
ui.layout().horizontal_justify(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
self.layout_impl(ui, 0.0, max_width, 0.0, line_color, halign, justify)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_impl(
|
fn layout_impl(
|
||||||
&self,
|
&self,
|
||||||
ui: &Ui,
|
ui: &Ui,
|
||||||
|
@ -181,6 +190,8 @@ impl Label {
|
||||||
max_width: f32,
|
max_width: f32,
|
||||||
first_row_min_height: f32,
|
first_row_min_height: f32,
|
||||||
line_color: Color32,
|
line_color: Color32,
|
||||||
|
halign: Align,
|
||||||
|
justify: bool,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
let text_style = self.text_style_or_default(ui.style());
|
let text_style = self.text_style_or_default(ui.style());
|
||||||
let wrap_width = if self.should_wrap(ui) {
|
let wrap_width = if self.should_wrap(ui) {
|
||||||
|
@ -227,6 +238,8 @@ impl Label {
|
||||||
}],
|
}],
|
||||||
wrap_width,
|
wrap_width,
|
||||||
first_row_min_height,
|
first_row_min_height,
|
||||||
|
halign,
|
||||||
|
justify,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -323,12 +336,16 @@ impl Label {
|
||||||
|
|
||||||
let first_row_min_height = cursor.height();
|
let first_row_min_height = cursor.height();
|
||||||
let default_color = self.get_text_color(ui, ui.visuals().text_color());
|
let default_color = self.get_text_color(ui, ui.visuals().text_color());
|
||||||
|
let halign = Align::Min;
|
||||||
|
let justify = false;
|
||||||
let galley = self.layout_impl(
|
let galley = self.layout_impl(
|
||||||
ui,
|
ui,
|
||||||
first_row_indentation,
|
first_row_indentation,
|
||||||
max_width,
|
max_width,
|
||||||
first_row_min_height,
|
first_row_min_height,
|
||||||
default_color,
|
default_color,
|
||||||
|
halign,
|
||||||
|
justify,
|
||||||
);
|
);
|
||||||
|
|
||||||
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
||||||
|
@ -343,8 +360,13 @@ impl Label {
|
||||||
(pos, galley, response)
|
(pos, galley, response)
|
||||||
} else {
|
} else {
|
||||||
let galley = self.layout(ui);
|
let galley = self.layout(ui);
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size, sense);
|
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
||||||
(rect.min, galley, response)
|
let pos = match galley.job.halign {
|
||||||
|
Align::LEFT => rect.left_top(),
|
||||||
|
Align::Center => rect.center_top(),
|
||||||
|
Align::RIGHT => rect.right_top(),
|
||||||
|
};
|
||||||
|
(pos, galley, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -915,7 +915,7 @@ impl PlotItem for Text {
|
||||||
.layout_no_wrap(self.text.clone(), self.style, color);
|
.layout_no_wrap(self.text.clone(), self.style, color);
|
||||||
let rect = self
|
let rect = self
|
||||||
.anchor
|
.anchor
|
||||||
.anchor_rect(Rect::from_min_size(pos, galley.size));
|
.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||||
shapes.push(Shape::galley(rect.min, galley));
|
shapes.push(Shape::galley(rect.min, galley));
|
||||||
if self.highlight {
|
if self.highlight {
|
||||||
shapes.push(Shape::rect_stroke(
|
shapes.push(Shape::rect_stroke(
|
||||||
|
|
|
@ -94,11 +94,11 @@ impl LegendEntry {
|
||||||
ui.fonts()
|
ui.fonts()
|
||||||
.layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY);
|
.layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY);
|
||||||
|
|
||||||
let icon_size = galley.size.y;
|
let icon_size = galley.size().y;
|
||||||
let icon_spacing = icon_size / 5.0;
|
let icon_spacing = icon_size / 5.0;
|
||||||
let total_extra = vec2(icon_size + icon_spacing, 0.0);
|
let total_extra = vec2(icon_size + icon_spacing, 0.0);
|
||||||
|
|
||||||
let desired_size = total_extra + galley.size;
|
let desired_size = total_extra + galley.size();
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -139,12 +139,12 @@ impl LegendEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_position_x = if label_on_the_left {
|
let text_position_x = if label_on_the_left {
|
||||||
rect.right() - icon_size - icon_spacing - galley.size.x
|
rect.right() - icon_size - icon_spacing - galley.size().x
|
||||||
} else {
|
} else {
|
||||||
rect.left() + icon_size + icon_spacing
|
rect.left() + icon_size + icon_spacing
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size.y);
|
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y);
|
||||||
painter.galley_with_color(text_position, galley, visuals.text_color());
|
painter.galley_with_color(text_position, galley, visuals.text_color());
|
||||||
|
|
||||||
*checked ^= response.clicked_by(PointerButton::Primary);
|
*checked ^= response.clicked_by(PointerButton::Primary);
|
||||||
|
|
|
@ -628,11 +628,11 @@ impl Prepared {
|
||||||
|
|
||||||
let galley = ui.painter().layout_no_wrap(text, text_style, color);
|
let galley = ui.painter().layout_no_wrap(text, text_style, color);
|
||||||
|
|
||||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size.y);
|
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||||
|
|
||||||
// Make sure we see the labels, even if the axis is off-screen:
|
// Make sure we see the labels, even if the axis is off-screen:
|
||||||
text_pos[1 - axis] = text_pos[1 - axis]
|
text_pos[1 - axis] = text_pos[1 - axis]
|
||||||
.at_most(transform.frame().max[1 - axis] - galley.size[1 - axis] - 2.0)
|
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
.at_least(transform.frame().min[1 - axis] + 1.0);
|
||||||
|
|
||||||
shapes.push(Shape::galley(text_pos, galley));
|
shapes.push(Shape::galley(text_pos, galley));
|
||||||
|
|
|
@ -69,7 +69,7 @@ impl Widget for SelectableLabel {
|
||||||
.fonts()
|
.fonts()
|
||||||
.layout_delayed_color(text, text_style, wrap_width);
|
.layout_delayed_color(text, text_style, wrap_width);
|
||||||
|
|
||||||
let mut desired_size = total_extra + galley.size;
|
let mut desired_size = total_extra + galley.size();
|
||||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
|
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
|
@ -78,7 +78,7 @@ impl Widget for SelectableLabel {
|
||||||
|
|
||||||
let text_pos = ui
|
let text_pos = ui
|
||||||
.layout()
|
.layout()
|
||||||
.align_size_within_rect(galley.size, rect.shrink2(button_padding))
|
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
|
||||||
.min;
|
.min;
|
||||||
|
|
||||||
let visuals = ui.style().interact_selectable(&response, selected);
|
let visuals = ui.style().interact_selectable(&response, selected);
|
||||||
|
|
|
@ -521,7 +521,7 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
let mut galley = layouter(ui, text.as_ref(), wrap_width);
|
let mut galley = layouter(ui, text.as_ref(), wrap_width);
|
||||||
|
|
||||||
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
||||||
let desired_size = vec2(wrap_width, galley.size.y.max(desired_height));
|
let desired_size = vec2(wrap_width, galley.size().y.max(desired_height));
|
||||||
let (auto_id, rect) = ui.allocate_space(desired_size);
|
let (auto_id, rect) = ui.allocate_space(desired_size);
|
||||||
|
|
||||||
let id = id.unwrap_or_else(|| {
|
let id = id.unwrap_or_else(|| {
|
||||||
|
@ -804,7 +804,7 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
}
|
}
|
||||||
|
|
||||||
offset_x = offset_x
|
offset_x = offset_x
|
||||||
.at_most(galley.size.x - desired_size.x)
|
.at_most(galley.size().x - desired_size.x)
|
||||||
.at_least(0.0);
|
.at_least(0.0);
|
||||||
|
|
||||||
state.singleline_offset = offset_x;
|
state.singleline_offset = offset_x;
|
||||||
|
|
|
@ -163,14 +163,14 @@ impl DemoWindows {
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS};
|
use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||||
|
|
||||||
ui.label("egui is an immediate mode GUI library written in Rust.");
|
|
||||||
|
|
||||||
ui.label(format!(
|
|
||||||
"egui runs on the web, or natively on {}{}{}",
|
|
||||||
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
|
||||||
));
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label("egui is an immediate mode GUI library written in Rust.");
|
||||||
|
|
||||||
|
ui.label(format!(
|
||||||
|
"egui runs on the web, or natively on {}{}{}",
|
||||||
|
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
||||||
|
));
|
||||||
|
|
||||||
ui.hyperlink_to(
|
ui.hyperlink_to(
|
||||||
format!("{} egui home page", GITHUB),
|
format!("{} egui home page", GITHUB),
|
||||||
"https://github.com/emilk/egui",
|
"https://github.com/emilk/egui",
|
||||||
|
|
|
@ -4,10 +4,7 @@ use egui::*;
|
||||||
#[cfg_attr(feature = "persistence", serde(default))]
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
pub struct LayoutTest {
|
pub struct LayoutTest {
|
||||||
// Identical to contents of `egui::Layout`
|
// Identical to contents of `egui::Layout`
|
||||||
main_dir: Direction,
|
layout: LayoutSettings,
|
||||||
main_wrap: bool,
|
|
||||||
cross_align: Align,
|
|
||||||
cross_justify: bool,
|
|
||||||
|
|
||||||
// Extra for testing wrapping:
|
// Extra for testing wrapping:
|
||||||
wrap_column_width: f32,
|
wrap_column_width: f32,
|
||||||
|
@ -16,15 +13,62 @@ pub struct LayoutTest {
|
||||||
|
|
||||||
impl Default for LayoutTest {
|
impl Default for LayoutTest {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
layout: LayoutSettings::top_down(),
|
||||||
|
wrap_column_width: 150.0,
|
||||||
|
wrap_row_height: 20.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
|
pub struct LayoutSettings {
|
||||||
|
// Similar to the contents of `egui::Layout`
|
||||||
|
main_dir: Direction,
|
||||||
|
main_wrap: bool,
|
||||||
|
cross_align: Align,
|
||||||
|
cross_justify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LayoutSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::top_down()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutSettings {
|
||||||
|
fn top_down() -> Self {
|
||||||
Self {
|
Self {
|
||||||
main_dir: Direction::TopDown,
|
main_dir: Direction::TopDown,
|
||||||
main_wrap: false,
|
main_wrap: false,
|
||||||
cross_align: Align::Min,
|
cross_align: Align::Min,
|
||||||
cross_justify: false,
|
cross_justify: false,
|
||||||
wrap_column_width: 150.0,
|
|
||||||
wrap_row_height: 20.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn top_down_justified_centered() -> Self {
|
||||||
|
Self {
|
||||||
|
main_dir: Direction::TopDown,
|
||||||
|
main_wrap: false,
|
||||||
|
cross_align: Align::Center,
|
||||||
|
cross_justify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn horizontal_wrapped() -> Self {
|
||||||
|
Self {
|
||||||
|
main_dir: Direction::LeftToRight,
|
||||||
|
main_wrap: true,
|
||||||
|
cross_align: Align::Center,
|
||||||
|
cross_justify: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&self) -> Layout {
|
||||||
|
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
||||||
|
.with_main_wrap(self.main_wrap)
|
||||||
|
.with_cross_justify(self.cross_justify)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Demo for LayoutTest {
|
impl super::Demo for LayoutTest {
|
||||||
|
@ -48,22 +92,22 @@ impl super::View for LayoutTest {
|
||||||
ui.label("Tests and demonstrates the limits of the egui layouts");
|
ui.label("Tests and demonstrates the limits of the egui layouts");
|
||||||
self.content_ui(ui);
|
self.content_ui(ui);
|
||||||
Resize::default()
|
Resize::default()
|
||||||
.default_size([300.0, 200.0])
|
.default_size([150.0, 200.0])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if self.main_wrap {
|
if self.layout.main_wrap {
|
||||||
if self.main_dir.is_horizontal() {
|
if self.layout.main_dir.is_horizontal() {
|
||||||
ui.allocate_ui(
|
ui.allocate_ui(
|
||||||
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
||||||
|ui| ui.with_layout(self.layout(), demo_ui),
|
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.allocate_ui(
|
ui.allocate_ui(
|
||||||
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
||||||
|ui| ui.with_layout(self.layout(), demo_ui),
|
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.with_layout(self.layout(), demo_ui);
|
ui.with_layout(self.layout.layout(), demo_ui);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.label("Resize to see effect");
|
ui.label("Resize to see effect");
|
||||||
|
@ -71,28 +115,19 @@ impl super::View for LayoutTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutTest {
|
impl LayoutTest {
|
||||||
fn layout(&self) -> Layout {
|
|
||||||
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
|
||||||
.with_main_wrap(self.main_wrap)
|
|
||||||
.with_cross_justify(self.cross_justify)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Top-down").clicked() {
|
ui.selectable_value(&mut self.layout, LayoutSettings::top_down(), "Top-down");
|
||||||
*self = Default::default();
|
ui.selectable_value(
|
||||||
}
|
&mut self.layout,
|
||||||
if ui.button("Top-down, centered and justified").clicked() {
|
LayoutSettings::top_down_justified_centered(),
|
||||||
*self = Default::default();
|
"Top-down, centered and justified",
|
||||||
self.cross_align = Align::Center;
|
);
|
||||||
self.cross_justify = true;
|
ui.selectable_value(
|
||||||
}
|
&mut self.layout,
|
||||||
if ui.button("Horizontal wrapped").clicked() {
|
LayoutSettings::horizontal_wrapped(),
|
||||||
*self = Default::default();
|
"Horizontal wrapped",
|
||||||
self.main_dir = Direction::LeftToRight;
|
);
|
||||||
self.cross_align = Align::Center;
|
|
||||||
self.main_wrap = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
@ -103,16 +138,16 @@ impl LayoutTest {
|
||||||
Direction::TopDown,
|
Direction::TopDown,
|
||||||
Direction::BottomUp,
|
Direction::BottomUp,
|
||||||
] {
|
] {
|
||||||
ui.radio_value(&mut self.main_dir, dir, format!("{:?}", dir));
|
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.checkbox(&mut self.main_wrap, "Main wrap")
|
ui.checkbox(&mut self.layout.main_wrap, "Main wrap")
|
||||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||||
|
|
||||||
if self.main_wrap {
|
if self.layout.main_wrap {
|
||||||
if self.main_dir.is_horizontal() {
|
if self.layout.main_dir.is_horizontal() {
|
||||||
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||||
} else {
|
} else {
|
||||||
ui.add(
|
ui.add(
|
||||||
|
@ -125,25 +160,19 @@ impl LayoutTest {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Cross Align:");
|
ui.label("Cross Align:");
|
||||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||||
ui.radio_value(&mut self.cross_align, align, format!("{:?}", align));
|
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.checkbox(&mut self.cross_justify, "Cross Justified")
|
ui.checkbox(&mut self.layout.cross_justify, "Cross Justified")
|
||||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn demo_ui(ui: &mut Ui) {
|
fn demo_ui(ui: &mut Ui) {
|
||||||
ui.monospace("Example widgets:");
|
ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true));
|
||||||
for _ in 0..3 {
|
let mut dummy = false;
|
||||||
ui.label("label");
|
ui.checkbox(&mut dummy, "checkbox");
|
||||||
}
|
ui.radio_value(&mut dummy, false, "radio");
|
||||||
for _ in 0..3 {
|
let _ = ui.button("button");
|
||||||
let mut dummy = false;
|
|
||||||
ui.checkbox(&mut dummy, "checkbox");
|
|
||||||
}
|
|
||||||
for _ in 0..3 {
|
|
||||||
let _ = ui.button("button");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -569,7 +569,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
|
||||||
|
|
||||||
let galley = ui.fonts().layout_job(job);
|
let galley = ui.fonts().layout_job(job);
|
||||||
|
|
||||||
let (response, painter) = ui.allocate_painter(galley.size, Sense::hover());
|
let (response, painter) = ui.allocate_painter(galley.size(), Sense::hover());
|
||||||
painter.add(Shape::galley(response.rect.min, galley));
|
painter.add(Shape::galley(response.rect.min, galley));
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
|
|
@ -373,7 +373,7 @@ pub struct WindowResizeTest {
|
||||||
impl Default for WindowResizeTest {
|
impl Default for WindowResizeTest {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: crate::LOREM_IPSUM.to_owned(),
|
text: crate::LOREM_IPSUM_LONG.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,7 @@ impl super::Demo for WindowResizeTest {
|
||||||
ui.label("This window will auto-size based on its contents.");
|
ui.label("This window will auto-size based on its contents.");
|
||||||
ui.heading("Resize this area:");
|
ui.heading("Resize this area:");
|
||||||
Resize::default().show(ui, |ui| {
|
Resize::default().show(ui, |ui| {
|
||||||
ui.code(crate::LOREM_IPSUM);
|
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||||
});
|
});
|
||||||
ui.heading("Resize the above area!");
|
ui.heading("Resize the above area!");
|
||||||
});
|
});
|
||||||
|
@ -405,10 +405,10 @@ impl super::Demo for WindowResizeTest {
|
||||||
.default_height(300.0)
|
.default_height(300.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
"This window is resizable and has a scroll area. You can shrink it to any size",
|
"This window is resizable and has a scroll area. You can shrink it to any size.",
|
||||||
);
|
);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.code(crate::LOREM_IPSUM_LONG);
|
lorem_ipsum(ui, crate::LOREM_IPSUM_LONG);
|
||||||
});
|
});
|
||||||
|
|
||||||
Window::new("↔ resizable + embedded scroll")
|
Window::new("↔ resizable + embedded scroll")
|
||||||
|
@ -421,8 +421,9 @@ impl super::Demo for WindowResizeTest {
|
||||||
ui.label("However, we have a sub-region with a scroll bar:");
|
ui.label("However, we have a sub-region with a scroll bar:");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.code(crate::LOREM_IPSUM_LONG);
|
let lorem_ipsum_extra_long =
|
||||||
ui.code(crate::LOREM_IPSUM_LONG);
|
format!("{}\n\n{}", crate::LOREM_IPSUM_LONG, crate::LOREM_IPSUM_LONG);
|
||||||
|
lorem_ipsum(ui, &lorem_ipsum_extra_long);
|
||||||
});
|
});
|
||||||
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
||||||
});
|
});
|
||||||
|
@ -435,7 +436,7 @@ impl super::Demo for WindowResizeTest {
|
||||||
ui.label("This window is resizable but has no scroll area. This means it can only be resized to a size where all the contents is visible.");
|
ui.label("This window is resizable but has no scroll area. This means it can only be resized to a size where all the contents is visible.");
|
||||||
ui.label("egui will not clip the contents of a window, nor add whitespace to it.");
|
ui.label("egui will not clip the contents of a window, nor add whitespace to it.");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.code(crate::LOREM_IPSUM);
|
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||||
});
|
});
|
||||||
|
|
||||||
Window::new("↔ resizable with TextEdit")
|
Window::new("↔ resizable with TextEdit")
|
||||||
|
@ -459,3 +460,12 @@ impl super::Demo for WindowResizeTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lorem_ipsum(ui: &mut egui::Ui, text: &str) {
|
||||||
|
ui.with_layout(
|
||||||
|
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||||
|
|ui| {
|
||||||
|
ui.add(egui::Label::new(text).weak());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl super::View for WindowWithPanels {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.heading("Expandable Upper Panel");
|
ui.heading("Expandable Upper Panel");
|
||||||
});
|
});
|
||||||
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
|
lorem_ipsum(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ impl super::View for WindowWithPanels {
|
||||||
ui.heading("Left Panel");
|
ui.heading("Left Panel");
|
||||||
});
|
});
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
|
lorem_ipsum(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ impl super::View for WindowWithPanels {
|
||||||
ui.heading("Right Panel");
|
ui.heading("Right Panel");
|
||||||
});
|
});
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
|
lorem_ipsum(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,8 +74,17 @@ impl super::View for WindowWithPanels {
|
||||||
ui.heading("Central Panel");
|
ui.heading("Central Panel");
|
||||||
});
|
});
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
|
lorem_ipsum(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lorem_ipsum(ui: &mut egui::Ui) {
|
||||||
|
ui.with_layout(
|
||||||
|
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||||
|
|ui| {
|
||||||
|
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -361,7 +361,7 @@ impl ColoredText {
|
||||||
let mut job = self.0.clone();
|
let mut job = self.0.clone();
|
||||||
job.wrap_width = ui.available_width();
|
job.wrap_width = ui.available_width();
|
||||||
let galley = ui.fonts().layout_job(job);
|
let galley = ui.fonts().layout_job(job);
|
||||||
let (response, painter) = ui.allocate_painter(galley.size, egui::Sense::hover());
|
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ impl Shape {
|
||||||
color: Color32,
|
color: Color32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let galley = fonts.layout_no_wrap(text.to_string(), text_style, color);
|
let galley = fonts.layout_no_wrap(text.to_string(), text_style, color);
|
||||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size));
|
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||||
Self::galley(rect.min, galley)
|
Self::galley(rect.min, galley)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -621,7 +621,7 @@ impl Tessellator {
|
||||||
if options.debug_paint_text_rects {
|
if options.debug_paint_text_rects {
|
||||||
self.tessellate_rect(
|
self.tessellate_rect(
|
||||||
&PaintRect {
|
&PaintRect {
|
||||||
rect: Rect::from_min_size(text_shape.pos, text_shape.galley.size)
|
rect: Rect::from_min_size(text_shape.pos, text_shape.galley.size())
|
||||||
.expand(0.5),
|
.expand(0.5),
|
||||||
corner_radius: 2.0,
|
corner_radius: 2.0,
|
||||||
fill: Default::default(),
|
fill: Default::default(),
|
||||||
|
|
|
@ -292,6 +292,11 @@ impl Fonts {
|
||||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn floor_to_pixel(&self, point: f32) -> f32 {
|
||||||
|
(point * self.pixels_per_point).floor() / self.pixels_per_point
|
||||||
|
}
|
||||||
|
|
||||||
/// Call each frame to get the latest available font texture data.
|
/// Call each frame to get the latest available font texture data.
|
||||||
pub fn texture(&self) -> Arc<Texture> {
|
pub fn texture(&self) -> Arc<Texture> {
|
||||||
let atlas = self.atlas.lock();
|
let atlas = self.atlas.lock();
|
||||||
|
|
|
@ -25,7 +25,18 @@ pub fn layout(fonts: &Fonts, job: Arc<LayoutJob>) -> Galley {
|
||||||
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
|
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = rows_from_paragraphs(paragraphs, job.wrap_width);
|
let mut rows = rows_from_paragraphs(paragraphs, job.wrap_width);
|
||||||
|
|
||||||
|
let justify = job.justify && job.wrap_width.is_finite();
|
||||||
|
|
||||||
|
if justify || job.halign != Align::LEFT {
|
||||||
|
let num_rows = rows.len();
|
||||||
|
for (i, row) in rows.iter_mut().enumerate() {
|
||||||
|
let is_last_row = i + 1 == num_rows;
|
||||||
|
let justify_row = justify && !row.ends_with_newline && !is_last_row;
|
||||||
|
halign_and_jusitfy_row(fonts, row, job.halign, job.wrap_width, justify_row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
galley_from_rows(fonts, job, rows)
|
galley_from_rows(fonts, job, rows)
|
||||||
}
|
}
|
||||||
|
@ -133,7 +144,7 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
let mut row_start_idx = 0;
|
let mut row_start_idx = 0;
|
||||||
|
|
||||||
for (i, glyph) in paragraph.glyphs.iter().enumerate() {
|
for (i, glyph) in paragraph.glyphs.iter().enumerate() {
|
||||||
let potential_row_width = glyph.pos.x - row_start_x;
|
let potential_row_width = glyph.max_x() - row_start_x;
|
||||||
|
|
||||||
if potential_row_width > wrap_width {
|
if potential_row_width > wrap_width {
|
||||||
if first_row_indentation > 0.0 && !row_break_candidates.has_word_boundary() {
|
if first_row_indentation > 0.0 && !row_break_candidates.has_word_boundary() {
|
||||||
|
@ -200,10 +211,101 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn halign_and_jusitfy_row(
|
||||||
|
fonts: &Fonts,
|
||||||
|
row: &mut Row,
|
||||||
|
halign: Align,
|
||||||
|
wrap_width: f32,
|
||||||
|
justify: bool,
|
||||||
|
) {
|
||||||
|
if row.glyphs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_leading_spaces = row
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.take_while(|glyph| glyph.chr.is_whitespace())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let glyph_range = if num_leading_spaces == row.glyphs.len() {
|
||||||
|
// There is only whitespace
|
||||||
|
(0, row.glyphs.len())
|
||||||
|
} else {
|
||||||
|
let num_trailing_spaces = row
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take_while(|glyph| glyph.chr.is_whitespace())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
(num_leading_spaces, row.glyphs.len() - num_trailing_spaces)
|
||||||
|
};
|
||||||
|
let num_glyphs_in_range = glyph_range.1 - glyph_range.0;
|
||||||
|
assert!(num_glyphs_in_range > 0);
|
||||||
|
|
||||||
|
let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x;
|
||||||
|
let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x;
|
||||||
|
let original_width = original_max_x - original_min_x;
|
||||||
|
|
||||||
|
let target_width = if justify && num_glyphs_in_range > 1 {
|
||||||
|
wrap_width
|
||||||
|
} else {
|
||||||
|
original_width
|
||||||
|
};
|
||||||
|
|
||||||
|
let (target_min_x, target_max_x) = match halign {
|
||||||
|
Align::LEFT => (0.0, target_width),
|
||||||
|
Align::Center => (-target_width / 2.0, target_width / 2.0),
|
||||||
|
Align::RIGHT => (-target_width, 0.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let num_spaces_in_range = row.glyphs[glyph_range.0..glyph_range.1]
|
||||||
|
.iter()
|
||||||
|
.filter(|glyph| glyph.chr.is_whitespace())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let mut extra_x_per_glyph = if num_glyphs_in_range == 1 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(target_width - original_width) / (num_glyphs_in_range as f32 - 1.0)
|
||||||
|
};
|
||||||
|
extra_x_per_glyph = extra_x_per_glyph.at_least(0.0); // Don't contract
|
||||||
|
|
||||||
|
let mut extra_x_per_space = 0.0;
|
||||||
|
if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
|
||||||
|
// Add an integral number of pixels between each glyph,
|
||||||
|
// and add the balance to the spaces:
|
||||||
|
|
||||||
|
extra_x_per_glyph = fonts.floor_to_pixel(extra_x_per_glyph);
|
||||||
|
|
||||||
|
extra_x_per_space = (target_width
|
||||||
|
- original_width
|
||||||
|
- extra_x_per_glyph * (num_glyphs_in_range as f32 - 1.0))
|
||||||
|
/ (num_spaces_in_range as f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut translate_x = target_min_x - original_min_x - extra_x_per_glyph * glyph_range.0 as f32;
|
||||||
|
|
||||||
|
for glyph in &mut row.glyphs {
|
||||||
|
glyph.pos.x += translate_x;
|
||||||
|
glyph.pos.x = fonts.round_to_pixel(glyph.pos.x);
|
||||||
|
translate_x += extra_x_per_glyph;
|
||||||
|
if glyph.chr.is_whitespace() {
|
||||||
|
translate_x += extra_x_per_space;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note we ignore the leading/trailing whitespace here!
|
||||||
|
row.rect.min.x = target_min_x;
|
||||||
|
row.rect.max.x = target_max_x;
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the Y positions and tessellate the text.
|
/// Calculate the Y positions and tessellate the text.
|
||||||
fn galley_from_rows(fonts: &Fonts, job: Arc<LayoutJob>, mut rows: Vec<Row>) -> Galley {
|
fn galley_from_rows(fonts: &Fonts, job: Arc<LayoutJob>, mut rows: Vec<Row>) -> Galley {
|
||||||
let mut first_row_min_height = job.first_row_min_height;
|
let mut first_row_min_height = job.first_row_min_height;
|
||||||
let mut cursor_y = 0.0;
|
let mut cursor_y = 0.0;
|
||||||
|
let mut min_x: f32 = 0.0;
|
||||||
let mut max_x: f32 = 0.0;
|
let mut max_x: f32 = 0.0;
|
||||||
for row in &mut rows {
|
for row in &mut rows {
|
||||||
let mut row_height = first_row_min_height.max(row.rect.height());
|
let mut row_height = first_row_min_height.max(row.rect.height());
|
||||||
|
@ -223,7 +325,8 @@ fn galley_from_rows(fonts: &Fonts, job: Arc<LayoutJob>, mut rows: Vec<Row>) -> G
|
||||||
row.rect.min.y = cursor_y;
|
row.rect.min.y = cursor_y;
|
||||||
row.rect.max.y = cursor_y + row_height;
|
row.rect.max.y = cursor_y + row_height;
|
||||||
|
|
||||||
max_x = max_x.max(row.rect.right());
|
min_x = min_x.min(row.rect.min.x);
|
||||||
|
max_x = max_x.max(row.rect.max.x);
|
||||||
cursor_y += row_height;
|
cursor_y += row_height;
|
||||||
cursor_y = fonts.round_to_pixel(cursor_y);
|
cursor_y = fonts.round_to_pixel(cursor_y);
|
||||||
}
|
}
|
||||||
|
@ -239,12 +342,12 @@ fn galley_from_rows(fonts: &Fonts, job: Arc<LayoutJob>, mut rows: Vec<Row>) -> G
|
||||||
num_indices += row.visuals.mesh.indices.len();
|
num_indices += row.visuals.mesh.indices.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = vec2(max_x, cursor_y);
|
let rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y));
|
||||||
|
|
||||||
Galley {
|
Galley {
|
||||||
job,
|
job,
|
||||||
rows,
|
rows,
|
||||||
size,
|
rect,
|
||||||
num_vertices,
|
num_vertices,
|
||||||
num_indices,
|
num_indices,
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,12 @@ pub struct LayoutJob {
|
||||||
/// and show up as the replacement character.
|
/// and show up as the replacement character.
|
||||||
/// Default: `true`.
|
/// Default: `true`.
|
||||||
pub break_on_newline: bool,
|
pub break_on_newline: bool,
|
||||||
// TODO: option to show whitespace characters
|
|
||||||
|
/// How to horizontally align the text (`Align::LEFT`, `Align::Center`, `Align::RIGHT`).
|
||||||
|
pub halign: Align,
|
||||||
|
|
||||||
|
/// Justify text so that word-wrapped rows fill the whole [`Self::wrap_width`]
|
||||||
|
pub justify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LayoutJob {
|
impl Default for LayoutJob {
|
||||||
|
@ -48,6 +53,8 @@ impl Default for LayoutJob {
|
||||||
wrap_width: f32::INFINITY,
|
wrap_width: f32::INFINITY,
|
||||||
first_row_min_height: 0.0,
|
first_row_min_height: 0.0,
|
||||||
break_on_newline: true,
|
break_on_newline: true,
|
||||||
|
halign: Align::LEFT,
|
||||||
|
justify: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +119,8 @@ impl std::hash::Hash for LayoutJob {
|
||||||
wrap_width,
|
wrap_width,
|
||||||
first_row_min_height,
|
first_row_min_height,
|
||||||
break_on_newline,
|
break_on_newline,
|
||||||
|
halign,
|
||||||
|
justify,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
text.hash(state);
|
text.hash(state);
|
||||||
|
@ -119,6 +128,8 @@ impl std::hash::Hash for LayoutJob {
|
||||||
crate::f32_hash(state, *wrap_width);
|
crate::f32_hash(state, *wrap_width);
|
||||||
crate::f32_hash(state, *first_row_min_height);
|
crate::f32_hash(state, *first_row_min_height);
|
||||||
break_on_newline.hash(state);
|
break_on_newline.hash(state);
|
||||||
|
halign.hash(state);
|
||||||
|
justify.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,16 +210,24 @@ pub struct Galley {
|
||||||
pub job: Arc<LayoutJob>,
|
pub job: Arc<LayoutJob>,
|
||||||
|
|
||||||
/// Rows of text, from top to bottom.
|
/// Rows of text, from top to bottom.
|
||||||
/// The number of chars in all rows sum up to text.chars().count().
|
/// The number of characters in all rows sum up to `job.text.chars().count()`.
|
||||||
/// Note that each paragraph (pieces of text separated with `\n`)
|
/// Note that each paragraph (pieces of text separated with `\n`)
|
||||||
/// can be split up into multiple rows.
|
/// can be split up into multiple rows.
|
||||||
pub rows: Vec<Row>,
|
pub rows: Vec<Row>,
|
||||||
|
|
||||||
/// Bounding size (min is always `[0,0]`)
|
/// Bounding rect.
|
||||||
pub size: Vec2,
|
///
|
||||||
|
/// `rect.top()` is always 0.0.
|
||||||
|
///
|
||||||
|
/// With [`LayoutJob::halign`]:
|
||||||
|
/// * [`Align::LEFT`]: rect.left() == 0.0
|
||||||
|
/// * [`Align::Center`]: rect.center() == 0.0
|
||||||
|
/// * [`Align::RIGHT`]: rect.right() == 0.0
|
||||||
|
pub rect: Rect,
|
||||||
|
|
||||||
/// Total number of vertices in all the row meshes.
|
/// Total number of vertices in all the row meshes.
|
||||||
pub num_vertices: usize,
|
pub num_vertices: usize,
|
||||||
|
|
||||||
/// Total number of indices in all the row meshes.
|
/// Total number of indices in all the row meshes.
|
||||||
pub num_indices: usize,
|
pub num_indices: usize,
|
||||||
}
|
}
|
||||||
|
@ -346,6 +365,10 @@ impl Galley {
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
&self.job.text
|
&self.job.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Vec2 {
|
||||||
|
self.rect.size()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in a new issue