[epaint] Add more text wrapping options (#1291)
This commit is contained in:
parent
d09fa63d9c
commit
901b7c7994
11 changed files with 256 additions and 57 deletions
|
@ -563,14 +563,14 @@ impl WidgetText {
|
||||||
Self::RichText(text) => {
|
Self::RichText(text) => {
|
||||||
let valign = ui.layout().vertical_align();
|
let valign = ui.layout().vertical_align();
|
||||||
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
||||||
text_job.job.wrap_width = wrap_width;
|
text_job.job.wrap.max_width = wrap_width;
|
||||||
WidgetTextGalley {
|
WidgetTextGalley {
|
||||||
galley: ui.fonts().layout_job(text_job.job),
|
galley: ui.fonts().layout_job(text_job.job),
|
||||||
galley_has_color: text_job.job_has_color,
|
galley_has_color: text_job.job_has_color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::LayoutJob(mut job) => {
|
Self::LayoutJob(mut job) => {
|
||||||
job.wrap_width = wrap_width;
|
job.wrap.max_width = wrap_width;
|
||||||
WidgetTextGalley {
|
WidgetTextGalley {
|
||||||
galley: ui.fonts().layout_job(job),
|
galley: ui.fonts().layout_job(job),
|
||||||
galley_has_color: true,
|
galley_has_color: true,
|
||||||
|
|
|
@ -103,7 +103,7 @@ impl Label {
|
||||||
let first_row_indentation = available_width - ui.available_size_before_wrap().x;
|
let first_row_indentation = available_width - ui.available_size_before_wrap().x;
|
||||||
egui_assert!(first_row_indentation.is_finite());
|
egui_assert!(first_row_indentation.is_finite());
|
||||||
|
|
||||||
text_job.job.wrap_width = available_width;
|
text_job.job.wrap.max_width = available_width;
|
||||||
text_job.job.first_row_min_height = cursor.height();
|
text_job.job.first_row_min_height = cursor.height();
|
||||||
text_job.job.halign = Align::Min;
|
text_job.job.halign = Align::Min;
|
||||||
text_job.job.justify = false;
|
text_job.job.justify = false;
|
||||||
|
@ -129,9 +129,9 @@ impl Label {
|
||||||
(pos, text_galley, response)
|
(pos, text_galley, response)
|
||||||
} else {
|
} else {
|
||||||
if should_wrap {
|
if should_wrap {
|
||||||
text_job.job.wrap_width = available_width;
|
text_job.job.wrap.max_width = available_width;
|
||||||
} else {
|
} else {
|
||||||
text_job.job.wrap_width = f32::INFINITY;
|
text_job.job.wrap.max_width = f32::INFINITY;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ui.is_grid() {
|
if ui.is_grid() {
|
||||||
|
|
|
@ -182,7 +182,7 @@ impl<'t> TextEdit<'t> {
|
||||||
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
||||||
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
||||||
/// layout_job.wrap_width = wrap_width;
|
/// layout_job.wrap.max_width = wrap_width;
|
||||||
/// ui.fonts().layout_job(layout_job)
|
/// ui.fonts().layout_job(layout_job)
|
||||||
/// };
|
/// };
|
||||||
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
||||||
|
|
|
@ -78,7 +78,7 @@ impl super::View for CodeEditor {
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
let mut layout_job =
|
let mut layout_job =
|
||||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||||
layout_job.wrap_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts().layout_job(layout_job)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use egui::{color::*, *};
|
use crate::LOREM_IPSUM;
|
||||||
|
use egui::{color::*, epaint::text::TextWrapping, *};
|
||||||
|
|
||||||
/// Showcase some ui code
|
/// Showcase some ui code
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
@ -7,6 +8,10 @@ use egui::{color::*, *};
|
||||||
pub struct MiscDemoWindow {
|
pub struct MiscDemoWindow {
|
||||||
num_columns: usize,
|
num_columns: usize,
|
||||||
|
|
||||||
|
break_anywhere: bool,
|
||||||
|
max_rows: usize,
|
||||||
|
overflow_character: Option<char>,
|
||||||
|
|
||||||
widgets: Widgets,
|
widgets: Widgets,
|
||||||
colors: ColorWidgets,
|
colors: ColorWidgets,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
|
@ -18,6 +23,10 @@ impl Default for MiscDemoWindow {
|
||||||
MiscDemoWindow {
|
MiscDemoWindow {
|
||||||
num_columns: 2,
|
num_columns: 2,
|
||||||
|
|
||||||
|
max_rows: 2,
|
||||||
|
break_anywhere: false,
|
||||||
|
overflow_character: Some('…'),
|
||||||
|
|
||||||
widgets: Default::default(),
|
widgets: Default::default(),
|
||||||
colors: Default::default(),
|
colors: Default::default(),
|
||||||
tree: Tree::demo(),
|
tree: Tree::demo(),
|
||||||
|
@ -53,7 +62,12 @@ impl View for MiscDemoWindow {
|
||||||
CollapsingHeader::new("Text layout")
|
CollapsingHeader::new("Text layout")
|
||||||
.default_open(false)
|
.default_open(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
text_layout_ui(ui);
|
text_layout_ui(
|
||||||
|
ui,
|
||||||
|
&mut self.max_rows,
|
||||||
|
&mut self.break_anywhere,
|
||||||
|
&mut self.overflow_character,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CollapsingHeader::new("Colors")
|
CollapsingHeader::new("Colors")
|
||||||
|
@ -401,7 +415,12 @@ impl SubTree {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn text_layout_ui(ui: &mut egui::Ui) {
|
fn text_layout_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
max_rows: &mut usize,
|
||||||
|
break_anywhere: &mut bool,
|
||||||
|
overflow_character: &mut Option<char>,
|
||||||
|
) {
|
||||||
use egui::text::LayoutJob;
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
@ -556,6 +575,30 @@ fn text_layout_ui(ui: &mut egui::Ui) {
|
||||||
|
|
||||||
ui.label(job);
|
ui.label(job);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(DragValue::new(max_rows));
|
||||||
|
ui.label("Max rows");
|
||||||
|
});
|
||||||
|
ui.checkbox(break_anywhere, "Break anywhere");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.selectable_value(overflow_character, None, "None");
|
||||||
|
ui.selectable_value(overflow_character, Some('…'), "…");
|
||||||
|
ui.selectable_value(overflow_character, Some('—'), "—");
|
||||||
|
ui.selectable_value(overflow_character, Some('-'), " - ");
|
||||||
|
ui.label("Overflow character");
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut job = LayoutJob::single_section(LOREM_IPSUM.to_string(), TextFormat::default());
|
||||||
|
job.wrap = TextWrapping {
|
||||||
|
max_rows: *max_rows,
|
||||||
|
break_anywhere: *break_anywhere,
|
||||||
|
overflow_character: *overflow_character,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
ui.label(job);
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add(crate::__egui_github_link_file_line!());
|
ui.add(crate::__egui_github_link_file_line!());
|
||||||
});
|
});
|
||||||
|
|
|
@ -244,7 +244,7 @@ impl ColoredText {
|
||||||
// Selectable text:
|
// Selectable text:
|
||||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
||||||
let mut layout_job = self.0.clone();
|
let mut layout_job = self.0.clone();
|
||||||
layout_job.wrap_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts().layout_job(layout_job)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ impl ColoredText {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let mut job = self.0.clone();
|
let mut job = self.0.clone();
|
||||||
job.wrap_width = ui.available_width();
|
job.wrap.max_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));
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl EasyMarkEditor {
|
||||||
let response = if self.highlight_editor {
|
let response = if self.highlight_editor {
|
||||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||||
layout_job.wrap_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts().layout_job(layout_job)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||||
|
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||||
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
||||||
// layout_job.wrap_width = wrap_width; // no wrapping
|
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts().layout_job(layout_job)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,13 @@ All notable changes to the epaint crate will be documented in this file.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
* Add `Shape::Callback` for backend-specific painting ([#1351](https://github.com/emilk/egui/pull/1351)).
|
* Add `Shape::Callback` for backend-specific painting ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||||
|
* Added more text wrapping options ([#1291](https://github.com/emilk/egui/pull/1291)):
|
||||||
|
* Added `TextWrapping` struct containing all wrapping options.
|
||||||
|
* Added `LayoutJob::wrap` field containing these options.
|
||||||
|
* Moved `LayoutJob::wrap_width` to `TextWrapping::max_width`.
|
||||||
|
* Added `TextWrapping::max_rows` to limit amount of rows the text should have.
|
||||||
|
* Added `TextWrapping::break_anywhere` to control should the text break at appropriate places or not.
|
||||||
|
* Added `TextWrapping::overflow_character` to specify what character should be used to represent clipped text.
|
||||||
* Removed the `single_threaded/multi_threaded` flags - epaint is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)).
|
* Removed the `single_threaded/multi_threaded` flags - epaint is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)).
|
||||||
* `Tessellator::from_options` is now `Tessellator::new` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
* `Tessellator::from_options` is now `Tessellator::new` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
||||||
* Renamed `TessellationOptions::anti_alias` to `feathering` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
* Renamed `TessellationOptions::anti_alias` to `feathering` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
||||||
|
|
|
@ -58,16 +58,22 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||||
|
|
||||||
let point_scale = PointScale::new(fonts.pixels_per_point());
|
let point_scale = PointScale::new(fonts.pixels_per_point());
|
||||||
|
|
||||||
let mut rows = rows_from_paragraphs(paragraphs, job.wrap_width);
|
let mut rows = rows_from_paragraphs(fonts, paragraphs, &job);
|
||||||
|
|
||||||
let justify = job.justify && job.wrap_width.is_finite();
|
let justify = job.justify && job.wrap.max_width.is_finite();
|
||||||
|
|
||||||
if justify || job.halign != Align::LEFT {
|
if justify || job.halign != Align::LEFT {
|
||||||
let num_rows = rows.len();
|
let num_rows = rows.len();
|
||||||
for (i, row) in rows.iter_mut().enumerate() {
|
for (i, row) in rows.iter_mut().enumerate() {
|
||||||
let is_last_row = i + 1 == num_rows;
|
let is_last_row = i + 1 == num_rows;
|
||||||
let justify_row = justify && !row.ends_with_newline && !is_last_row;
|
let justify_row = justify && !row.ends_with_newline && !is_last_row;
|
||||||
halign_and_jusitfy_row(point_scale, row, job.halign, job.wrap_width, justify_row);
|
halign_and_jusitfy_row(
|
||||||
|
point_scale,
|
||||||
|
row,
|
||||||
|
job.halign,
|
||||||
|
job.wrap.max_width,
|
||||||
|
justify_row,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +137,11 @@ fn rect_from_x_range(x_range: RangeInclusive<f32>) -> Rect {
|
||||||
Rect::from_x_y_ranges(x_range, 0.0..=0.0)
|
Rect::from_x_y_ranges(x_range, 0.0..=0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rows_from_paragraphs(paragraphs: Vec<Paragraph>, wrap_width: f32) -> Vec<Row> {
|
fn rows_from_paragraphs(
|
||||||
|
fonts: &mut FontsImpl,
|
||||||
|
paragraphs: Vec<Paragraph>,
|
||||||
|
job: &LayoutJob,
|
||||||
|
) -> Vec<Row> {
|
||||||
let num_paragraphs = paragraphs.len();
|
let num_paragraphs = paragraphs.len();
|
||||||
|
|
||||||
let mut rows = vec![];
|
let mut rows = vec![];
|
||||||
|
@ -151,7 +161,7 @@ fn rows_from_paragraphs(paragraphs: Vec<Paragraph>, wrap_width: f32) -> Vec<Row>
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x();
|
let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x();
|
||||||
if paragraph_max_x <= wrap_width {
|
if paragraph_max_x <= job.wrap.max_width {
|
||||||
// early-out optimization
|
// early-out optimization
|
||||||
let paragraph_min_x = paragraph.glyphs[0].pos.x;
|
let paragraph_min_x = paragraph.glyphs[0].pos.x;
|
||||||
rows.push(Row {
|
rows.push(Row {
|
||||||
|
@ -161,7 +171,7 @@ fn rows_from_paragraphs(paragraphs: Vec<Paragraph>, wrap_width: f32) -> Vec<Row>
|
||||||
ends_with_newline: !is_last_paragraph,
|
ends_with_newline: !is_last_paragraph,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
line_break(¶graph, wrap_width, &mut rows);
|
line_break(fonts, ¶graph, job, &mut rows);
|
||||||
rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph;
|
rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,19 +180,31 @@ fn rows_from_paragraphs(paragraphs: Vec<Paragraph>, wrap_width: f32) -> Vec<Row>
|
||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
fn line_break(
|
||||||
|
fonts: &mut FontsImpl,
|
||||||
|
paragraph: &Paragraph,
|
||||||
|
job: &LayoutJob,
|
||||||
|
out_rows: &mut Vec<Row>,
|
||||||
|
) {
|
||||||
// Keeps track of good places to insert row break if we exceed `wrap_width`.
|
// Keeps track of good places to insert row break if we exceed `wrap_width`.
|
||||||
let mut row_break_candidates = RowBreakCandidates::default();
|
let mut row_break_candidates = RowBreakCandidates::default();
|
||||||
|
|
||||||
let mut first_row_indentation = paragraph.glyphs[0].pos.x;
|
let mut first_row_indentation = paragraph.glyphs[0].pos.x;
|
||||||
let mut row_start_x = 0.0;
|
let mut row_start_x = 0.0;
|
||||||
let mut row_start_idx = 0;
|
let mut row_start_idx = 0;
|
||||||
|
let mut non_empty_rows = 0;
|
||||||
|
|
||||||
for (i, glyph) in paragraph.glyphs.iter().enumerate() {
|
for (i, glyph) in paragraph.glyphs.iter().enumerate() {
|
||||||
let potential_row_width = glyph.max_x() - row_start_x;
|
let potential_row_width = glyph.max_x() - row_start_x;
|
||||||
|
|
||||||
if potential_row_width > wrap_width {
|
if job.wrap.max_rows > 0 && non_empty_rows >= job.wrap.max_rows {
|
||||||
if first_row_indentation > 0.0 && !row_break_candidates.has_word_boundary() {
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if potential_row_width > job.wrap.max_width {
|
||||||
|
if first_row_indentation > 0.0
|
||||||
|
&& !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
|
||||||
|
{
|
||||||
// Allow the first row to be completely empty, because we know there will be more space on the next row:
|
// Allow the first row to be completely empty, because we know there will be more space on the next row:
|
||||||
// TODO: this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height.
|
// TODO: this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height.
|
||||||
out_rows.push(Row {
|
out_rows.push(Row {
|
||||||
|
@ -193,7 +215,8 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
});
|
});
|
||||||
row_start_x += first_row_indentation;
|
row_start_x += first_row_indentation;
|
||||||
first_row_indentation = 0.0;
|
first_row_indentation = 0.0;
|
||||||
} else if let Some(last_kept_index) = row_break_candidates.get() {
|
} else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere)
|
||||||
|
{
|
||||||
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
|
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
|
@ -216,6 +239,7 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
row_start_idx = last_kept_index + 1;
|
row_start_idx = last_kept_index + 1;
|
||||||
row_start_x = paragraph.glyphs[row_start_idx].pos.x;
|
row_start_x = paragraph.glyphs[row_start_idx].pos.x;
|
||||||
row_break_candidates = Default::default();
|
row_break_candidates = Default::default();
|
||||||
|
non_empty_rows += 1;
|
||||||
} else {
|
} else {
|
||||||
// Found no place to break, so we have to overrun wrap_width.
|
// Found no place to break, so we have to overrun wrap_width.
|
||||||
}
|
}
|
||||||
|
@ -225,6 +249,9 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if row_start_idx < paragraph.glyphs.len() {
|
if row_start_idx < paragraph.glyphs.len() {
|
||||||
|
if non_empty_rows == job.wrap.max_rows {
|
||||||
|
replace_last_glyph_with_overflow_character(fonts, job, out_rows);
|
||||||
|
} else {
|
||||||
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
|
@ -244,6 +271,69 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
ends_with_newline: false,
|
ends_with_newline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_last_glyph_with_overflow_character(
|
||||||
|
fonts: &mut FontsImpl,
|
||||||
|
job: &LayoutJob,
|
||||||
|
out_rows: &mut Vec<Row>,
|
||||||
|
) {
|
||||||
|
let overflow_character = match job.wrap.overflow_character {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row = match out_rows.last_mut() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (prev_glyph, last_glyph) = match row.glyphs.as_mut_slice() {
|
||||||
|
[.., prev, last] => (Some(prev), last),
|
||||||
|
[.., last] => (None, last),
|
||||||
|
_ => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
let section = &job.sections[last_glyph.section_index as usize];
|
||||||
|
let font = fonts.font(§ion.format.font_id);
|
||||||
|
let font_height = font.row_height();
|
||||||
|
|
||||||
|
let prev_glyph_id = prev_glyph.map(|prev_glyph| {
|
||||||
|
let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr);
|
||||||
|
prev_glyph_info.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// undo kerning with previous glyph
|
||||||
|
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
|
||||||
|
last_glyph.pos.x -= font_impl
|
||||||
|
.zip(prev_glyph_id)
|
||||||
|
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// replace the glyph
|
||||||
|
last_glyph.chr = overflow_character;
|
||||||
|
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
|
||||||
|
last_glyph.size = vec2(glyph_info.advance_width, font_height);
|
||||||
|
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||||
|
|
||||||
|
// reapply kerning
|
||||||
|
last_glyph.pos.x += font_impl
|
||||||
|
.zip(prev_glyph_id)
|
||||||
|
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// check if we're still within width budget
|
||||||
|
let row_end_x = last_glyph.max_x();
|
||||||
|
let row_start_x = row.glyphs.first().unwrap().pos.x; // if `last_mut()` returned `Some`, then so will `first()`
|
||||||
|
let row_width = row_end_x - row_start_x;
|
||||||
|
if row_width <= job.wrap.max_width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.glyphs.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn halign_and_jusitfy_row(
|
fn halign_and_jusitfy_row(
|
||||||
|
@ -651,22 +741,33 @@ impl RowBreakCandidates {
|
||||||
self.dash = Some(index);
|
self.dash = Some(index);
|
||||||
} else if chr.is_ascii_punctuation() {
|
} else if chr.is_ascii_punctuation() {
|
||||||
self.punctuation = Some(index);
|
self.punctuation = Some(index);
|
||||||
} else {
|
|
||||||
self.any = Some(index);
|
|
||||||
}
|
}
|
||||||
|
self.any = Some(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_word_boundary(&self) -> bool {
|
fn has_word_boundary(&self) -> bool {
|
||||||
self.space.is_some() || self.logogram.is_some()
|
self.space.is_some() || self.logogram.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self) -> Option<usize> {
|
fn has_good_candidate(&self, break_anywhere: bool) -> bool {
|
||||||
|
if break_anywhere {
|
||||||
|
self.any.is_some()
|
||||||
|
} else {
|
||||||
|
self.has_word_boundary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, break_anywhere: bool) -> Option<usize> {
|
||||||
|
if break_anywhere {
|
||||||
|
self.any
|
||||||
|
} else {
|
||||||
self.space
|
self.space
|
||||||
.or(self.logogram)
|
.or(self.logogram)
|
||||||
.or(self.dash)
|
.or(self.dash)
|
||||||
.or(self.punctuation)
|
.or(self.punctuation)
|
||||||
.or(self.any)
|
.or(self.any)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -48,10 +48,7 @@ pub struct LayoutJob {
|
||||||
/// The different section, which can have different fonts, colors, etc.
|
/// The different section, which can have different fonts, colors, etc.
|
||||||
pub sections: Vec<LayoutSection>,
|
pub sections: Vec<LayoutSection>,
|
||||||
|
|
||||||
/// Try to break text so that no row is wider than this.
|
pub wrap: TextWrapping,
|
||||||
/// Set to [`f32::INFINITY`] to turn off wrapping.
|
|
||||||
/// Note that `\n` always produces a new line.
|
|
||||||
pub wrap_width: f32,
|
|
||||||
|
|
||||||
/// The first row must be at least this high.
|
/// The first row must be at least this high.
|
||||||
/// This is in case we lay out text that is the continuation
|
/// This is in case we lay out text that is the continuation
|
||||||
|
@ -78,7 +75,7 @@ impl Default for LayoutJob {
|
||||||
Self {
|
Self {
|
||||||
text: Default::default(),
|
text: Default::default(),
|
||||||
sections: Default::default(),
|
sections: Default::default(),
|
||||||
wrap_width: f32::INFINITY,
|
wrap: Default::default(),
|
||||||
first_row_min_height: 0.0,
|
first_row_min_height: 0.0,
|
||||||
break_on_newline: true,
|
break_on_newline: true,
|
||||||
halign: Align::LEFT,
|
halign: Align::LEFT,
|
||||||
|
@ -98,7 +95,10 @@ impl LayoutJob {
|
||||||
format: TextFormat::simple(font_id, color),
|
format: TextFormat::simple(font_id, color),
|
||||||
}],
|
}],
|
||||||
text,
|
text,
|
||||||
wrap_width,
|
wrap: TextWrapping {
|
||||||
|
max_width: wrap_width,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
break_on_newline: true,
|
break_on_newline: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ impl LayoutJob {
|
||||||
format: TextFormat::simple(font_id, color),
|
format: TextFormat::simple(font_id, color),
|
||||||
}],
|
}],
|
||||||
text,
|
text,
|
||||||
wrap_width: f32::INFINITY,
|
wrap: Default::default(),
|
||||||
break_on_newline: false,
|
break_on_newline: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ impl LayoutJob {
|
||||||
format,
|
format,
|
||||||
}],
|
}],
|
||||||
text,
|
text,
|
||||||
wrap_width: f32::INFINITY,
|
wrap: Default::default(),
|
||||||
break_on_newline: true,
|
break_on_newline: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,7 @@ impl std::hash::Hash for LayoutJob {
|
||||||
let Self {
|
let Self {
|
||||||
text,
|
text,
|
||||||
sections,
|
sections,
|
||||||
wrap_width,
|
wrap,
|
||||||
first_row_min_height,
|
first_row_min_height,
|
||||||
break_on_newline,
|
break_on_newline,
|
||||||
halign,
|
halign,
|
||||||
|
@ -177,7 +177,7 @@ impl std::hash::Hash for LayoutJob {
|
||||||
|
|
||||||
text.hash(state);
|
text.hash(state);
|
||||||
sections.hash(state);
|
sections.hash(state);
|
||||||
crate::f32_hash(state, *wrap_width);
|
wrap.hash(state);
|
||||||
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);
|
halign.hash(state);
|
||||||
|
@ -257,6 +257,54 @@ impl TextFormat {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct TextWrapping {
|
||||||
|
/// Try to break text so that no row is wider than this.
|
||||||
|
/// Set to [`f32::INFINITY`] to turn off wrapping.
|
||||||
|
/// Note that `\n` always produces a new line.
|
||||||
|
pub max_width: f32,
|
||||||
|
|
||||||
|
/// Maximum amount of rows the text should have.
|
||||||
|
/// Set to `0` to disable this.
|
||||||
|
pub max_rows: usize,
|
||||||
|
|
||||||
|
/// Don't try to break text at an appropriate place.
|
||||||
|
pub break_anywhere: bool,
|
||||||
|
|
||||||
|
/// Character to use to represent clipped text, `…` for example, which is the default.
|
||||||
|
pub overflow_character: Option<char>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for TextWrapping {
|
||||||
|
#[inline]
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
let Self {
|
||||||
|
max_width,
|
||||||
|
max_rows,
|
||||||
|
break_anywhere,
|
||||||
|
overflow_character,
|
||||||
|
} = self;
|
||||||
|
crate::f32_hash(state, *max_width);
|
||||||
|
max_rows.hash(state);
|
||||||
|
break_anywhere.hash(state);
|
||||||
|
overflow_character.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextWrapping {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_width: f32::INFINITY,
|
||||||
|
max_rows: 0,
|
||||||
|
break_anywhere: false,
|
||||||
|
overflow_character: Some('…'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Text that has been layed out, ready for painting.
|
/// Text that has been layed out, ready for painting.
|
||||||
///
|
///
|
||||||
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
|
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
|
||||||
|
|
Loading…
Reference in a new issue