New text layout (#682)
This PR introduces a completely rewritten text layout engine which is simpler and more powerful. It allows mixing different text styles (heading, body, etc) and formats (color, underlining, strikethrough, …) in the same layout pass, and baked into the same `Galley`. This opens up the door to having a syntax-highlighed code editor, or a WYSIWYG markdown editor. One major change is the color is now baked in at layout time. However, many widgets changes text color on hovered. But we need to do the text layout before we know if it is hovered. Therefor the painter has an option to override the text color of a galley. ## Performance Text layout alone is about 20% slower, but a lot of that is because more tessellation is done upfront. Text tessellation is now a lot faster, but text layout + tessellation still lands at a net loss of 5-10% in performance. There are however a few tricks to speed it up (like using `smallvec`) which I am saving for later. Text layout is also cached, meaning that in most cases (when all text isn't changing each frame) text tessellation is actually more important (and that's more than 2x faster!). Sadly, the actual text cache lookup is significantly slower (300ns -> 600ns). That's because the `TextLayoutJob` is a lot bigger (it has more options, like underlining, fonts etc), so it is slower to hash and compare. I have an idea how to speed this up, but I need to do some other work before I can implement that. All in all, the performance impact on `demo_with_tesselate__realistic` is about 5-6% in the red. Not great; not terrible. The benefits are worth it, but I also think with some work I can get that down significantly, hopefully down to the old levels.
This commit is contained in:
parent
36cffd7b84
commit
de1a1ba9b2
43 changed files with 2204 additions and 1295 deletions
|
@ -9,10 +9,17 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
|
* Add horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
|
||||||
|
* `TextEdit::layouter`: Add custom text layout for e.g. syntax highlighting or WYSIWYG.
|
||||||
|
* `Fonts::layout_job*`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
|
* `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`.
|
||||||
|
* `Painter`/`Fonts`: text layout now expect color when creating a `Galley`. You may override that color with `Painter::galley_with_color`.
|
||||||
|
|
||||||
|
### Fixed 🐛
|
||||||
|
* Fix wrongly sized multiline `TextEdit` in justified layouts.
|
||||||
|
|
||||||
|
|
||||||
## 0.14.2 - 2021-08-28 - Window resize fix
|
## 0.14.2 - 2021-08-28 - Window resize fix
|
||||||
|
|
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -885,7 +885,6 @@ dependencies = [
|
||||||
"atomic_refcell",
|
"atomic_refcell",
|
||||||
"cint",
|
"cint",
|
||||||
"emath",
|
"emath",
|
||||||
"ordered-float",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -1631,15 +1630,6 @@ version = "11.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ordered-float"
|
|
||||||
version = "2.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "osmesa-sys"
|
name = "osmesa-sys"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
@ -81,7 +81,7 @@ ui.label(format!("Hello '{}', age {}", name, age));
|
||||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
||||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||||
* Safe: there is no `unsafe` code in egui
|
* Safe: there is no `unsafe` code in egui
|
||||||
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ordered-float`](https://crates.io/crates/ordered-float).
|
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell)
|
||||||
|
|
||||||
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
||||||
|
|
||||||
|
|
|
@ -268,7 +268,8 @@ impl CollapsingHeader {
|
||||||
|
|
||||||
let available = ui.available_rect_before_wrap();
|
let available = ui.available_rect_before_wrap();
|
||||||
let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
|
let text_pos = available.min + vec2(ui.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, 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();
|
||||||
|
@ -292,7 +293,7 @@ impl CollapsingHeader {
|
||||||
header_response.mark_changed();
|
header_response.mark_changed();
|
||||||
}
|
}
|
||||||
header_response
|
header_response
|
||||||
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, &galley.text));
|
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text()));
|
||||||
|
|
||||||
let visuals = ui
|
let visuals = ui
|
||||||
.style()
|
.style()
|
||||||
|
@ -337,7 +338,7 @@ impl CollapsingHeader {
|
||||||
paint_icon(ui, openness, &icon_response);
|
paint_icon(ui, openness, &icon_response);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.painter().galley(text_pos, galley, text_color);
|
ui.painter().galley_with_color(text_pos, galley, text_color);
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -158,9 +158,9 @@ fn combo_box<R>(
|
||||||
let full_minimum_width = ui.spacing().slider_width;
|
let full_minimum_width = ui.spacing().slider_width;
|
||||||
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
||||||
|
|
||||||
let galley = ui
|
let galley =
|
||||||
.fonts()
|
ui.fonts()
|
||||||
.layout_no_wrap(TextStyle::Button, selected.to_string());
|
.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);
|
||||||
|
@ -181,7 +181,7 @@ fn combo_box<R>(
|
||||||
|
|
||||||
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(text_rect.min, galley, visuals.text_color());
|
.galley_with_color(text_rect.min, galley, visuals.text_color());
|
||||||
});
|
});
|
||||||
|
|
||||||
if button_response.clicked() {
|
if button_response.clicked() {
|
||||||
|
|
|
@ -848,8 +848,9 @@ impl TitleBar {
|
||||||
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 = emath::align::center_size_in_rect(self.title_galley.size, full_top_rect);
|
||||||
let text_pos = text_pos.left_top() - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better)
|
let text_pos = text_pos.left_top() - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better)
|
||||||
|
let text_color = ui.visuals().text_color();
|
||||||
self.title_label
|
self.title_label
|
||||||
.paint_galley(ui, text_pos, self.title_galley);
|
.paint_galley(ui, text_pos, self.title_galley, false, text_color);
|
||||||
|
|
||||||
if let Some(content_response) = &content_response {
|
if let Some(content_response) = &content_response {
|
||||||
// paint separator between title and content:
|
// paint separator between title and content:
|
||||||
|
|
|
@ -377,6 +377,10 @@ pub use {
|
||||||
widgets::*,
|
widgets::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod text {
|
||||||
|
pub use epaint::text::{Galley, LayoutJob, LayoutSection, TextFormat, TAB_SIZE};
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Helper function that adds a label when compiling with debug assertions enabled.
|
/// Helper function that adds a label when compiling with debug assertions enabled.
|
||||||
|
|
|
@ -219,9 +219,7 @@ impl Painter {
|
||||||
color: Color32,
|
color: Color32,
|
||||||
text: impl ToString,
|
text: impl ToString,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
let galley = self
|
let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color);
|
||||||
.fonts()
|
|
||||||
.layout_no_wrap(TextStyle::Monospace, text.to_string());
|
|
||||||
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 {
|
||||||
|
@ -231,7 +229,7 @@ impl Painter {
|
||||||
// stroke: Stroke::new(1.0, color),
|
// stroke: Stroke::new(1.0, color),
|
||||||
stroke: Default::default(),
|
stroke: Default::default(),
|
||||||
});
|
});
|
||||||
self.galley(rect.min, galley, color);
|
self.galley(rect.min, galley);
|
||||||
frame_rect
|
frame_rect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,7 +329,7 @@ impl Painter {
|
||||||
/// To center the text at the given position, use `anchor: (Center, Center)`.
|
/// To center the text at the given position, use `anchor: (Center, Center)`.
|
||||||
///
|
///
|
||||||
/// To find out the size of text before painting it, use
|
/// To find out the size of text before painting it, use
|
||||||
/// [`Self::layout_no_wrap`] or [`Self::layout_multiline`].
|
/// [`Self::layout`] or [`Self::layout_no_wrap`].
|
||||||
///
|
///
|
||||||
/// Returns where the text ended up.
|
/// Returns where the text ended up.
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
@ -343,57 +341,69 @@ impl Painter {
|
||||||
text_style: TextStyle,
|
text_style: TextStyle,
|
||||||
text_color: Color32,
|
text_color: Color32,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
let galley = self.layout_no_wrap(text_style, text.to_string());
|
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, text_color);
|
self.galley(rect.min, galley);
|
||||||
rect
|
rect
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will line break at `\n`.
|
|
||||||
///
|
|
||||||
/// Paint the results with [`Self::galley`].
|
|
||||||
/// Always returns at least one row.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn layout_no_wrap(&self, text_style: TextStyle, text: String) -> std::sync::Arc<Galley> {
|
|
||||||
self.layout_multiline(text_style, text, f32::INFINITY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will wrap text at the given width and line break at `\n`.
|
/// Will wrap text at the given width and line break at `\n`.
|
||||||
///
|
///
|
||||||
/// Paint the results with [`Self::galley`].
|
/// Paint the results with [`Self::galley`].
|
||||||
/// Always returns at least one row.
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn layout_multiline(
|
pub fn layout(
|
||||||
&self,
|
&self,
|
||||||
text_style: TextStyle,
|
|
||||||
text: String,
|
text: String,
|
||||||
max_width_in_points: f32,
|
text_style: TextStyle,
|
||||||
|
color: crate::Color32,
|
||||||
|
wrap_width: f32,
|
||||||
) -> std::sync::Arc<Galley> {
|
) -> std::sync::Arc<Galley> {
|
||||||
self.fonts()
|
self.fonts().layout(text, text_style, color, wrap_width)
|
||||||
.layout_multiline(text_style, text, max_width_in_points)
|
}
|
||||||
|
|
||||||
|
/// Will line break at `\n`.
|
||||||
|
///
|
||||||
|
/// Paint the results with [`Self::galley`].
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn layout_no_wrap(
|
||||||
|
&self,
|
||||||
|
text: String,
|
||||||
|
text_style: TextStyle,
|
||||||
|
color: crate::Color32,
|
||||||
|
) -> std::sync::Arc<Galley> {
|
||||||
|
self.fonts().layout(text, text_style, color, f32::INFINITY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint text that has already been layed out in a [`Galley`].
|
/// Paint text that has already been layed out in a [`Galley`].
|
||||||
///
|
///
|
||||||
/// You can create the `Galley` with [`Self::layout_no_wrap`] or [`Self::layout_multiline`].
|
/// You can create the `Galley` with [`Self::layout`].
|
||||||
|
///
|
||||||
|
/// If you want to change the color of the text, use [`Self::galley_with_color`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn galley(&self, pos: Pos2, galley: std::sync::Arc<Galley>, color: Color32) {
|
pub fn galley(&self, pos: Pos2, galley: std::sync::Arc<Galley>) {
|
||||||
self.galley_with_italics(pos, galley, color, false)
|
if !galley.is_empty() {
|
||||||
|
self.add(Shape::galley(pos, galley));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn galley_with_italics(
|
/// Paint text that has already been layed out in a [`Galley`].
|
||||||
|
///
|
||||||
|
/// You can create the `Galley` with [`Self::layout`].
|
||||||
|
///
|
||||||
|
/// The text color in the [`Galley`] will be replaced with the given color.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn galley_with_color(
|
||||||
&self,
|
&self,
|
||||||
pos: Pos2,
|
pos: Pos2,
|
||||||
galley: std::sync::Arc<Galley>,
|
galley: std::sync::Arc<Galley>,
|
||||||
color: Color32,
|
text_color: Color32,
|
||||||
fake_italics: bool,
|
|
||||||
) {
|
) {
|
||||||
if !galley.is_empty() {
|
if !galley.is_empty() {
|
||||||
self.add(Shape::Text {
|
self.add(Shape::Text {
|
||||||
pos,
|
pos,
|
||||||
galley,
|
galley,
|
||||||
color,
|
underline: Stroke::none(),
|
||||||
fake_italics,
|
override_text_color: Some(text_color),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,6 +233,7 @@ pub struct Visuals {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visuals {
|
impl Visuals {
|
||||||
|
#[inline(always)]
|
||||||
pub fn noninteractive(&self) -> &WidgetVisuals {
|
pub fn noninteractive(&self) -> &WidgetVisuals {
|
||||||
&self.widgets.noninteractive
|
&self.widgets.noninteractive
|
||||||
}
|
}
|
||||||
|
@ -246,14 +247,17 @@ impl Visuals {
|
||||||
crate::color::tint_color_towards(self.text_color(), self.window_fill())
|
crate::color::tint_color_towards(self.text_color(), self.window_fill())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn strong_text_color(&self) -> Color32 {
|
pub fn strong_text_color(&self) -> Color32 {
|
||||||
self.widgets.active.text_color()
|
self.widgets.active.text_color()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn window_fill(&self) -> Color32 {
|
pub fn window_fill(&self) -> Color32 {
|
||||||
self.widgets.noninteractive.bg_fill
|
self.widgets.noninteractive.bg_fill
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn window_stroke(&self) -> Stroke {
|
pub fn window_stroke(&self) -> Stroke {
|
||||||
self.widgets.noninteractive.bg_stroke
|
self.widgets.noninteractive.bg_stroke
|
||||||
}
|
}
|
||||||
|
@ -325,6 +329,7 @@ pub struct WidgetVisuals {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetVisuals {
|
impl WidgetVisuals {
|
||||||
|
#[inline(always)]
|
||||||
pub fn text_color(&self) -> Color32 {
|
pub fn text_color(&self) -> Color32 {
|
||||||
self.fg_stroke.color
|
self.fg_stroke.color
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// For those of us who miss `a ? yes : no`.
|
||||||
|
fn select<T>(b: bool, if_true: T, if_false: T) -> T {
|
||||||
|
if b {
|
||||||
|
if_true
|
||||||
|
} else {
|
||||||
|
if_false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Clickable button with text.
|
/// Clickable button with text.
|
||||||
///
|
///
|
||||||
/// See also [`Ui::button`].
|
/// See also [`Ui::button`].
|
||||||
|
@ -150,12 +159,10 @@ impl Button {
|
||||||
let total_extra = button_padding + button_padding;
|
let total_extra = button_padding + button_padding;
|
||||||
|
|
||||||
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
|
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
|
||||||
let galley = if wrap {
|
let wrap_width = select(wrap, ui.available_width() - total_extra.x, f32::INFINITY);
|
||||||
ui.fonts()
|
let galley = ui
|
||||||
.layout_multiline(text_style, text, ui.available_width() - total_extra.x)
|
.fonts()
|
||||||
} else {
|
.layout_delayed_color(text, text_style, wrap_width);
|
||||||
ui.fonts().layout_no_wrap(text_style, text)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut desired_size = galley.size + 2.0 * button_padding;
|
let mut desired_size = galley.size + 2.0 * button_padding;
|
||||||
if !small {
|
if !small {
|
||||||
|
@ -164,7 +171,7 @@ impl Button {
|
||||||
desired_size = desired_size.at_least(min_size);
|
desired_size = desired_size.at_least(min_size);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, &galley.text));
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, galley.text()));
|
||||||
|
|
||||||
if ui.clip_rect().intersects(rect) {
|
if ui.clip_rect().intersects(rect) {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
@ -187,7 +194,7 @@ impl Button {
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or(ui.visuals().override_text_color)
|
.or(ui.visuals().override_text_color)
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
.unwrap_or_else(|| visuals.text_color());
|
||||||
ui.painter().galley(text_pos, galley, text_color);
|
ui.painter().galley_with_color(text_pos, galley, text_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -274,12 +281,14 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
let button_padding = spacing.button_padding;
|
let button_padding = 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 = if ui.wrap_text() {
|
let wrap_width = select(
|
||||||
ui.fonts()
|
ui.wrap_text(),
|
||||||
.layout_multiline(text_style, text, ui.available_width() - total_extra.x)
|
ui.available_width() - total_extra.x,
|
||||||
} else {
|
f32::INFINITY,
|
||||||
ui.fonts().layout_no_wrap(text_style, text)
|
);
|
||||||
};
|
let galley = ui
|
||||||
|
.fonts()
|
||||||
|
.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);
|
||||||
|
@ -290,7 +299,8 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
*checked = !*checked;
|
*checked = !*checked;
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text));
|
response
|
||||||
|
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
|
||||||
|
|
||||||
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
@ -321,7 +331,7 @@ impl<'a> Widget for Checkbox<'a> {
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or(ui.visuals().override_text_color)
|
.or(ui.visuals().override_text_color)
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
.unwrap_or_else(|| visuals.text_color());
|
||||||
ui.painter().galley(text_pos, galley, text_color);
|
ui.painter().galley_with_color(text_pos, galley, text_color);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -395,19 +405,21 @@ impl Widget for RadioButton {
|
||||||
let button_padding = ui.spacing().button_padding;
|
let button_padding = ui.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 = if ui.wrap_text() {
|
let wrap_width = select(
|
||||||
ui.fonts()
|
ui.wrap_text(),
|
||||||
.layout_multiline(text_style, text, ui.available_width() - total_extra.x)
|
ui.available_width() - total_extra.x,
|
||||||
} else {
|
f32::INFINITY,
|
||||||
ui.fonts().layout_no_wrap(text_style, text)
|
);
|
||||||
};
|
let galley = ui
|
||||||
|
.fonts()
|
||||||
|
.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());
|
||||||
response
|
response
|
||||||
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, &galley.text));
|
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, galley.text()));
|
||||||
|
|
||||||
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,
|
||||||
|
@ -441,7 +453,7 @@ impl Widget for RadioButton {
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or(ui.visuals().override_text_color)
|
.or(ui.visuals().override_text_color)
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
.unwrap_or_else(|| visuals.text_color());
|
||||||
painter.galley(text_pos, galley, text_color);
|
painter.galley_with_color(text_pos, galley, text_color);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ impl Hyperlink {
|
||||||
let url = url.to_string();
|
let url = url.to_string();
|
||||||
Self {
|
Self {
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
label: Label::new(url),
|
label: Label::new(url).sense(Sense::click()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,9 +54,8 @@ impl Hyperlink {
|
||||||
impl Widget for Hyperlink {
|
impl Widget for Hyperlink {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let Hyperlink { url, label } = self;
|
let Hyperlink { url, label } = self;
|
||||||
let galley = label.layout(ui);
|
let (pos, galley, response) = label.layout_in_ui(ui);
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size, Sense::click());
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, galley.text()));
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, &galley.text));
|
|
||||||
|
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
||||||
|
@ -78,19 +77,18 @@ impl Widget for Hyperlink {
|
||||||
let color = ui.visuals().hyperlink_color;
|
let color = ui.visuals().hyperlink_color;
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
if response.hovered() || response.has_focus() {
|
let underline = if response.hovered() || response.has_focus() {
|
||||||
// Underline:
|
Stroke::new(visuals.fg_stroke.width, color)
|
||||||
for row in &galley.rows {
|
} else {
|
||||||
let rect = row.rect().translate(rect.min.to_vec2());
|
Stroke::none()
|
||||||
ui.painter().line_segment(
|
};
|
||||||
[rect.left_bottom(), rect.right_bottom()],
|
|
||||||
(visuals.fg_stroke.width, color),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = label.text_color(color);
|
ui.painter().add(Shape::Text {
|
||||||
label.paint_galley(ui, rect.min, galley);
|
pos,
|
||||||
|
galley,
|
||||||
|
override_text_color: Some(color),
|
||||||
|
underline,
|
||||||
|
});
|
||||||
|
|
||||||
response.on_hover_text(url)
|
response.on_hover_text(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use epaint::Galley;
|
use epaint::{
|
||||||
|
text::{LayoutJob, LayoutSection, TextFormat},
|
||||||
|
Galley,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Static text.
|
/// Static text.
|
||||||
|
@ -162,20 +165,111 @@ impl Label {
|
||||||
impl Label {
|
impl Label {
|
||||||
pub fn layout(&self, ui: &Ui) -> Arc<Galley> {
|
pub fn layout(&self, ui: &Ui) -> Arc<Galley> {
|
||||||
let max_width = ui.available_width();
|
let max_width = ui.available_width();
|
||||||
self.layout_width(ui, max_width)
|
let line_color = self.get_text_color(ui, ui.visuals().text_color());
|
||||||
|
self.layout_width(ui, max_width, line_color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_width(&self, ui: &Ui, max_width: f32) -> Arc<Galley> {
|
/// `line_color`: used for underline and strikethrough, if any.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_impl(
|
||||||
|
&self,
|
||||||
|
ui: &Ui,
|
||||||
|
leading_space: f32,
|
||||||
|
max_width: f32,
|
||||||
|
first_row_min_height: f32,
|
||||||
|
line_color: Color32,
|
||||||
|
) -> 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) {
|
||||||
max_width
|
max_width
|
||||||
} else {
|
} else {
|
||||||
f32::INFINITY
|
f32::INFINITY
|
||||||
};
|
};
|
||||||
let galley = ui
|
|
||||||
.fonts()
|
let mut background_color = self.background_color;
|
||||||
.layout_multiline(text_style, self.text.clone(), wrap_width); // TODO: avoid clone
|
if self.code {
|
||||||
self.valign_galley(ui, text_style, galley)
|
background_color = ui.visuals().code_bg_color;
|
||||||
|
}
|
||||||
|
let underline = if self.underline {
|
||||||
|
Stroke::new(1.0, line_color)
|
||||||
|
} else {
|
||||||
|
Stroke::none()
|
||||||
|
};
|
||||||
|
let strikethrough = if self.strikethrough {
|
||||||
|
Stroke::new(1.0, line_color)
|
||||||
|
} else {
|
||||||
|
Stroke::none()
|
||||||
|
};
|
||||||
|
|
||||||
|
let valign = if self.raised {
|
||||||
|
Align::TOP
|
||||||
|
} else {
|
||||||
|
ui.layout().vertical_align()
|
||||||
|
};
|
||||||
|
|
||||||
|
let job = LayoutJob {
|
||||||
|
text: self.text.clone(), // TODO: avoid clone
|
||||||
|
sections: vec![LayoutSection {
|
||||||
|
leading_space,
|
||||||
|
byte_range: 0..self.text.len(),
|
||||||
|
format: TextFormat {
|
||||||
|
style: text_style,
|
||||||
|
color: Color32::TEMPORARY_COLOR,
|
||||||
|
background: background_color,
|
||||||
|
italics: self.italics,
|
||||||
|
underline,
|
||||||
|
strikethrough,
|
||||||
|
valign,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
wrap_width,
|
||||||
|
first_row_min_height,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.fonts().layout_job(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `has_focus`: the item is selected with the keyboard, so highlight with underline.
|
||||||
|
/// `response_color`: Unless we have a special color set, use this.
|
||||||
|
pub(crate) fn paint_galley(
|
||||||
|
&self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
pos: Pos2,
|
||||||
|
galley: Arc<Galley>,
|
||||||
|
has_focus: bool,
|
||||||
|
response_color: Color32,
|
||||||
|
) {
|
||||||
|
let text_color = self.get_text_color(ui, response_color);
|
||||||
|
|
||||||
|
let underline = if has_focus {
|
||||||
|
Stroke::new(1.0, text_color)
|
||||||
|
} else {
|
||||||
|
Stroke::none()
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().add(Shape::Text {
|
||||||
|
pos,
|
||||||
|
galley,
|
||||||
|
override_text_color: Some(text_color),
|
||||||
|
underline,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `response_color`: Unless we have a special color set, use this.
|
||||||
|
fn get_text_color(&self, ui: &Ui, response_color: Color32) -> Color32 {
|
||||||
|
if let Some(text_color) = self.text_color {
|
||||||
|
text_color
|
||||||
|
} else if self.strong {
|
||||||
|
ui.visuals().strong_text_color()
|
||||||
|
} else if self.weak {
|
||||||
|
ui.visuals().weak_text_color()
|
||||||
|
} else {
|
||||||
|
response_color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_height(&self, fonts: &epaint::text::Fonts, style: &Style) -> f32 {
|
pub fn font_height(&self, fonts: &epaint::text::Fonts, style: &Style) -> f32 {
|
||||||
|
@ -191,79 +285,6 @@ impl Label {
|
||||||
// TODO: a paint method for painting anywhere in a ui.
|
// TODO: a paint method for painting anywhere in a ui.
|
||||||
// This should be the easiest method of putting text anywhere.
|
// This should be the easiest method of putting text anywhere.
|
||||||
|
|
||||||
pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: Arc<Galley>) {
|
|
||||||
self.paint_galley_impl(ui, pos, galley, false, ui.visuals().text_color())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint_galley_impl(
|
|
||||||
&self,
|
|
||||||
ui: &mut Ui,
|
|
||||||
pos: Pos2,
|
|
||||||
galley: Arc<Galley>,
|
|
||||||
has_focus: bool,
|
|
||||||
response_color: Color32,
|
|
||||||
) {
|
|
||||||
let Self {
|
|
||||||
mut background_color,
|
|
||||||
code,
|
|
||||||
strong,
|
|
||||||
weak,
|
|
||||||
strikethrough,
|
|
||||||
underline,
|
|
||||||
italics,
|
|
||||||
raised: _,
|
|
||||||
..
|
|
||||||
} = *self;
|
|
||||||
|
|
||||||
let underline = underline || has_focus;
|
|
||||||
|
|
||||||
let text_color = if let Some(text_color) = self.text_color {
|
|
||||||
text_color
|
|
||||||
} else if strong {
|
|
||||||
ui.visuals().strong_text_color()
|
|
||||||
} else if weak {
|
|
||||||
ui.visuals().weak_text_color()
|
|
||||||
} else {
|
|
||||||
response_color
|
|
||||||
};
|
|
||||||
|
|
||||||
if code {
|
|
||||||
background_color = ui.visuals().code_bg_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut lines = vec![];
|
|
||||||
|
|
||||||
if strikethrough || underline || background_color != Color32::TRANSPARENT {
|
|
||||||
for row in &galley.rows {
|
|
||||||
let rect = row.rect().translate(pos.to_vec2());
|
|
||||||
|
|
||||||
if background_color != Color32::TRANSPARENT {
|
|
||||||
let rect = rect.expand(1.0); // looks better
|
|
||||||
ui.painter().rect_filled(rect, 0.0, background_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stroke_width = 1.0;
|
|
||||||
if strikethrough {
|
|
||||||
lines.push(Shape::line_segment(
|
|
||||||
[rect.left_center(), rect.right_center()],
|
|
||||||
(stroke_width, text_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if underline {
|
|
||||||
lines.push(Shape::line_segment(
|
|
||||||
[rect.left_bottom(), rect.right_bottom()],
|
|
||||||
(stroke_width, text_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.painter()
|
|
||||||
.galley_with_italics(pos, galley, text_color, italics);
|
|
||||||
|
|
||||||
ui.painter().extend(lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the text style, or get the default for the current style
|
/// Read the text style, or get the default for the current style
|
||||||
pub fn text_style_or_default(&self, style: &Style) -> TextStyle {
|
pub fn text_style_or_default(&self, style: &Style) -> TextStyle {
|
||||||
self.text_style
|
self.text_style
|
||||||
|
@ -282,38 +303,9 @@ impl Label {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valign_galley(
|
/// Do layout and place the galley in the ui, without painting it or adding widget info.
|
||||||
&self,
|
pub(crate) fn layout_in_ui(&self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
|
||||||
ui: &Ui,
|
|
||||||
text_style: TextStyle,
|
|
||||||
mut galley: Arc<Galley>,
|
|
||||||
) -> Arc<Galley> {
|
|
||||||
if text_style == TextStyle::Small {
|
|
||||||
// Hacky McHackface strikes again:
|
|
||||||
let dy = if self.raised {
|
|
||||||
-2.0
|
|
||||||
} else {
|
|
||||||
let normal_text_height = ui.fonts()[TextStyle::Body].row_height();
|
|
||||||
let font_height = ui.fonts().row_height(text_style);
|
|
||||||
(normal_text_height - font_height) / 2.0 - 1.0 // center
|
|
||||||
|
|
||||||
// normal_text_height - font_height // align bottom
|
|
||||||
};
|
|
||||||
|
|
||||||
if dy != 0.0 {
|
|
||||||
for row in &mut Arc::make_mut(&mut galley).rows {
|
|
||||||
row.translate_y(dy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
galley
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Label {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let sense = self.sense;
|
let sense = self.sense;
|
||||||
|
|
||||||
let max_width = ui.available_width();
|
let max_width = ui.available_width();
|
||||||
|
|
||||||
if self.should_wrap(ui)
|
if self.should_wrap(ui)
|
||||||
|
@ -328,59 +320,44 @@ impl Widget for Label {
|
||||||
let first_row_indentation = max_width - ui.available_size_before_wrap().x;
|
let first_row_indentation = max_width - ui.available_size_before_wrap().x;
|
||||||
egui_assert!(first_row_indentation.is_finite());
|
egui_assert!(first_row_indentation.is_finite());
|
||||||
|
|
||||||
let text_style = self.text_style_or_default(ui.style());
|
let first_row_min_height = cursor.height();
|
||||||
let galley = ui.fonts().layout_multiline_with_indentation_and_max_width(
|
let default_color = self.get_text_color(ui, ui.visuals().text_color());
|
||||||
text_style,
|
let galley = self.layout_impl(
|
||||||
self.text.clone(),
|
ui,
|
||||||
first_row_indentation,
|
first_row_indentation,
|
||||||
max_width,
|
max_width,
|
||||||
|
first_row_min_height,
|
||||||
|
default_color,
|
||||||
);
|
);
|
||||||
let mut galley: Galley = (*galley).clone();
|
|
||||||
|
|
||||||
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
||||||
|
|
||||||
assert!(!galley.rows.is_empty(), "Galleys are never empty");
|
assert!(!galley.rows.is_empty(), "Galleys are never empty");
|
||||||
|
// collect a response from many rows:
|
||||||
// Center first row within the cursor:
|
let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y));
|
||||||
let dy = 0.5 * (cursor.height() - galley.rows[0].height());
|
|
||||||
galley.rows[0].translate_y(dy);
|
|
||||||
|
|
||||||
// We could be sharing the first row with e.g. a button which is higher than text.
|
|
||||||
// So we need to compensate for that:
|
|
||||||
if let Some(row) = galley.rows.get_mut(1) {
|
|
||||||
if pos.y + row.y_min < cursor.bottom() {
|
|
||||||
let y_translation = cursor.bottom() - row.y_min - pos.y;
|
|
||||||
if y_translation != 0.0 {
|
|
||||||
for row in galley.rows.iter_mut().skip(1) {
|
|
||||||
row.translate_y(y_translation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let galley = self.valign_galley(ui, text_style, Arc::new(galley));
|
|
||||||
|
|
||||||
let rect = galley.rows[0].rect().translate(vec2(pos.x, pos.y));
|
|
||||||
let mut response = ui.allocate_rect(rect, sense);
|
let mut response = ui.allocate_rect(rect, sense);
|
||||||
for row in galley.rows.iter().skip(1) {
|
for row in galley.rows.iter().skip(1) {
|
||||||
let rect = row.rect().translate(vec2(pos.x, pos.y));
|
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
||||||
response |= ui.allocate_rect(rect, sense);
|
response |= ui.allocate_rect(rect, sense);
|
||||||
}
|
}
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, &galley.text));
|
(pos, galley, response)
|
||||||
let response_color = ui.style().interact(&response).text_color();
|
|
||||||
self.paint_galley_impl(ui, pos, galley, response.has_focus(), response_color);
|
|
||||||
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);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, &galley.text));
|
(rect.min, galley, response)
|
||||||
let response_color = ui.style().interact(&response).text_color();
|
|
||||||
self.paint_galley_impl(ui, rect.min, galley, response.has_focus(), response_color);
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Widget for Label {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let (pos, galley, response) = self.layout_in_ui(ui);
|
||||||
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text()));
|
||||||
|
let response_color = ui.style().interact(&response).text_color();
|
||||||
|
self.paint_galley(ui, pos, galley, response.has_focus(), response_color);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&str> for Label {
|
impl From<&str> for Label {
|
||||||
fn from(s: &str) -> Label {
|
fn from(s: &str) -> Label {
|
||||||
Label::new(s)
|
Label::new(s)
|
||||||
|
|
|
@ -912,16 +912,11 @@ impl PlotItem for Text {
|
||||||
let pos = transform.position_from_value(&self.position);
|
let pos = transform.position_from_value(&self.position);
|
||||||
let galley = ui
|
let galley = ui
|
||||||
.fonts()
|
.fonts()
|
||||||
.layout_multiline(self.style, self.text.clone(), f32::INFINITY);
|
.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::Text {
|
shapes.push(Shape::galley(rect.min, galley));
|
||||||
pos: rect.min,
|
|
||||||
galley,
|
|
||||||
color,
|
|
||||||
fake_italics: false,
|
|
||||||
});
|
|
||||||
if self.highlight {
|
if self.highlight {
|
||||||
shapes.push(Shape::rect_stroke(
|
shapes.push(Shape::rect_stroke(
|
||||||
rect.expand(2.0),
|
rect.expand(2.0),
|
||||||
|
|
|
@ -90,7 +90,9 @@ impl LegendEntry {
|
||||||
hovered,
|
hovered,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let galley = ui.fonts().layout_no_wrap(ui.style().body_text_style, text);
|
let galley =
|
||||||
|
ui.fonts()
|
||||||
|
.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;
|
||||||
|
@ -99,7 +101,8 @@ impl LegendEntry {
|
||||||
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.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text));
|
response
|
||||||
|
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
|
||||||
|
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
let label_on_the_left = ui.layout().horizontal_align() == Align::RIGHT;
|
let label_on_the_left = ui.layout().horizontal_align() == Align::RIGHT;
|
||||||
|
@ -142,7 +145,7 @@ impl LegendEntry {
|
||||||
};
|
};
|
||||||
|
|
||||||
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(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);
|
||||||
*hovered = response.hovered();
|
*hovered = response.hovered();
|
||||||
|
|
|
@ -626,7 +626,7 @@ impl Prepared {
|
||||||
let color = color_from_alpha(ui, text_alpha);
|
let color = color_from_alpha(ui, text_alpha);
|
||||||
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
|
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
|
||||||
|
|
||||||
let galley = ui.fonts().layout_single_line(text_style, text);
|
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);
|
||||||
|
|
||||||
|
@ -635,12 +635,7 @@ impl Prepared {
|
||||||
.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::Text {
|
shapes.push(Shape::galley(text_pos, galley));
|
||||||
pos: text_pos,
|
|
||||||
galley,
|
|
||||||
color,
|
|
||||||
fake_italics: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,18 +59,21 @@ impl Widget for SelectableLabel {
|
||||||
let button_padding = ui.spacing().button_padding;
|
let button_padding = ui.spacing().button_padding;
|
||||||
let total_extra = button_padding + button_padding;
|
let total_extra = button_padding + button_padding;
|
||||||
|
|
||||||
let galley = if ui.wrap_text() {
|
let wrap_width = if ui.wrap_text() {
|
||||||
ui.fonts()
|
ui.available_width() - total_extra.x
|
||||||
.layout_multiline(text_style, text, ui.available_width() - total_extra.x)
|
|
||||||
} else {
|
} else {
|
||||||
ui.fonts().layout_no_wrap(text_style, text)
|
f32::INFINITY
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let galley = ui
|
||||||
|
.fonts()
|
||||||
|
.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(|| {
|
||||||
WidgetInfo::selected(WidgetType::SelectableLabel, selected, &galley.text)
|
WidgetInfo::selected(WidgetType::SelectableLabel, selected, galley.text())
|
||||||
});
|
});
|
||||||
|
|
||||||
let text_pos = ui
|
let text_pos = ui
|
||||||
|
@ -93,7 +96,7 @@ impl Widget for SelectableLabel {
|
||||||
.visuals
|
.visuals
|
||||||
.override_text_color
|
.override_text_color
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
.unwrap_or_else(|| visuals.text_color());
|
||||||
ui.painter().galley(text_pos, galley, text_color);
|
ui.painter().galley_with_color(text_pos, galley, text_color);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{output::OutputEvent, util::undoer::Undoer, *};
|
use crate::{output::OutputEvent, util::undoer::Undoer, *};
|
||||||
use epaint::{text::cursor::*, *};
|
use epaint::text::{cursor::*, Galley, LayoutJob};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
@ -222,7 +223,6 @@ impl TextBuffer for String {
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TextEdit<'t, S: TextBuffer = String> {
|
pub struct TextEdit<'t, S: TextBuffer = String> {
|
||||||
text: &'t mut S,
|
text: &'t mut S,
|
||||||
hint_text: String,
|
hint_text: String,
|
||||||
|
@ -230,6 +230,7 @@ pub struct TextEdit<'t, S: TextBuffer = String> {
|
||||||
id_source: Option<Id>,
|
id_source: Option<Id>,
|
||||||
text_style: Option<TextStyle>,
|
text_style: Option<TextStyle>,
|
||||||
text_color: Option<Color32>,
|
text_color: Option<Color32>,
|
||||||
|
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
|
||||||
password: bool,
|
password: bool,
|
||||||
frame: bool,
|
frame: bool,
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
|
@ -239,6 +240,7 @@ pub struct TextEdit<'t, S: TextBuffer = String> {
|
||||||
lock_focus: bool,
|
lock_focus: bool,
|
||||||
cursor_at_end: bool,
|
cursor_at_end: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
|
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
|
||||||
ui.memory()
|
ui.memory()
|
||||||
|
@ -251,33 +253,23 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
/// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`).
|
/// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`).
|
||||||
pub fn singleline(text: &'t mut S) -> Self {
|
pub fn singleline(text: &'t mut S) -> Self {
|
||||||
TextEdit {
|
Self {
|
||||||
text,
|
|
||||||
hint_text: Default::default(),
|
|
||||||
id: None,
|
|
||||||
id_source: None,
|
|
||||||
text_style: None,
|
|
||||||
text_color: None,
|
|
||||||
password: false,
|
|
||||||
frame: true,
|
|
||||||
multiline: false,
|
|
||||||
enabled: true,
|
|
||||||
desired_width: None,
|
|
||||||
desired_height_rows: 1,
|
desired_height_rows: 1,
|
||||||
lock_focus: false,
|
multiline: false,
|
||||||
cursor_at_end: true,
|
..Self::multiline(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `TextEdit` for multiple lines. Pressing enter key will create a new line.
|
/// A `TextEdit` for multiple lines. Pressing enter key will create a new line.
|
||||||
pub fn multiline(text: &'t mut S) -> Self {
|
pub fn multiline(text: &'t mut S) -> Self {
|
||||||
TextEdit {
|
Self {
|
||||||
text,
|
text,
|
||||||
hint_text: Default::default(),
|
hint_text: Default::default(),
|
||||||
id: None,
|
id: None,
|
||||||
id_source: None,
|
id_source: None,
|
||||||
text_style: None,
|
text_style: None,
|
||||||
text_color: None,
|
text_color: None,
|
||||||
|
layouter: None,
|
||||||
password: false,
|
password: false,
|
||||||
frame: true,
|
frame: true,
|
||||||
multiline: true,
|
multiline: true,
|
||||||
|
@ -337,6 +329,34 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override how text is being shown inside the `TextEdit`.
|
||||||
|
///
|
||||||
|
/// This can be used to implement things like syntax highlighting.
|
||||||
|
///
|
||||||
|
/// This function will be called at least once per frame,
|
||||||
|
/// so it is strongly suggested that you cache the results of any syntax highlighter
|
||||||
|
/// so as not to waste CPU highlighting the same string every frame.
|
||||||
|
///
|
||||||
|
/// The arguments is the enclosing [`Ui`] (so you can access e.g. [`Ui::fonts`]),
|
||||||
|
/// the text and the wrap width.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # let ui = &mut egui::Ui::__test();
|
||||||
|
/// # let mut my_code = String::new();
|
||||||
|
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
||||||
|
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
|
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
||||||
|
/// layout_job.wrap_width = wrap_width;
|
||||||
|
/// ui.fonts().layout_job(layout_job)
|
||||||
|
/// };
|
||||||
|
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
||||||
|
/// ```
|
||||||
|
pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>) -> Self {
|
||||||
|
self.layouter = Some(layouter);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Default is `true`. If set to `false` then you cannot edit the text.
|
/// Default is `true`. If set to `false` then you cannot edit the text.
|
||||||
pub fn enabled(mut self, enabled: bool) -> Self {
|
pub fn enabled(mut self, enabled: bool) -> Self {
|
||||||
self.enabled = enabled;
|
self.enabled = enabled;
|
||||||
|
@ -349,7 +369,8 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set to 0.0 to keep as small as possible
|
/// Set to 0.0 to keep as small as possible.
|
||||||
|
/// Set to [`f32::INFINITY`] to take up all available space.
|
||||||
pub fn desired_width(mut self, desired_width: f32) -> Self {
|
pub fn desired_width(mut self, desired_width: f32) -> Self {
|
||||||
self.desired_width = Some(desired_width);
|
self.desired_width = Some(desired_width);
|
||||||
self
|
self
|
||||||
|
@ -433,6 +454,14 @@ fn mask_massword(text: &str) -> String {
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mask_if_password(is_password: bool, text: &str) -> String {
|
||||||
|
if is_password {
|
||||||
|
mask_massword(text)
|
||||||
|
} else {
|
||||||
|
text.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
fn content_ui(self, ui: &mut Ui) -> Response {
|
fn content_ui(self, ui: &mut Ui) -> Response {
|
||||||
let TextEdit {
|
let TextEdit {
|
||||||
|
@ -442,6 +471,7 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
id_source,
|
id_source,
|
||||||
text_style,
|
text_style,
|
||||||
text_color,
|
text_color,
|
||||||
|
layouter,
|
||||||
password,
|
password,
|
||||||
frame: _,
|
frame: _,
|
||||||
multiline,
|
multiline,
|
||||||
|
@ -452,19 +482,16 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
cursor_at_end,
|
cursor_at_end,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mask_if_password = |text: &str| {
|
let text_color = text_color
|
||||||
if password {
|
.or(ui.visuals().override_text_color)
|
||||||
mask_massword(text)
|
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
||||||
} else {
|
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
|
||||||
text.to_owned()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let prev_text = text.as_ref().to_owned();
|
let prev_text = text.as_ref().to_owned();
|
||||||
let text_style = text_style
|
let text_style = text_style
|
||||||
.or(ui.style().override_text_style)
|
.or(ui.style().override_text_style)
|
||||||
.unwrap_or_else(|| ui.style().body_text_style);
|
.unwrap_or_else(|| ui.style().body_text_style);
|
||||||
let line_spacing = ui.fonts().row_height(text_style);
|
let row_height = ui.fonts().row_height(text_style);
|
||||||
const MIN_WIDTH: f32 = 24.0; // Never make a `TextEdit` more narrow than this.
|
const MIN_WIDTH: f32 = 24.0; // Never make a `TextEdit` more narrow than this.
|
||||||
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
||||||
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
||||||
|
@ -474,24 +501,26 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
desired_width.min(available_width)
|
desired_width.min(available_width)
|
||||||
};
|
};
|
||||||
|
|
||||||
let make_galley = |ui: &Ui, wrap_width: f32, text: &str| {
|
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
||||||
let text = mask_if_password(text);
|
let text = mask_if_password(password, text);
|
||||||
if multiline {
|
ui.fonts().layout_job(if multiline {
|
||||||
ui.fonts().layout_multiline(text_style, text, wrap_width)
|
LayoutJob::simple(text, text_style, text_color, wrap_width)
|
||||||
} else {
|
} else {
|
||||||
ui.fonts().layout_single_line(text_style, text)
|
LayoutJob::simple_singleline(text, text_style, text_color)
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||||
|
|
||||||
let copy_if_not_password = |ui: &Ui, text: String| {
|
let copy_if_not_password = |ui: &Ui, text: String| {
|
||||||
if !password {
|
if !password {
|
||||||
ui.ctx().output().copied_text = text;
|
ui.ctx().output().copied_text = text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut galley = make_galley(ui, wrap_width, text.as_ref());
|
let mut galley = layouter(ui, text.as_ref(), wrap_width);
|
||||||
|
|
||||||
let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing;
|
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);
|
||||||
|
|
||||||
|
@ -525,7 +554,14 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
&& ui.input().pointer.is_moving()
|
&& ui.input().pointer.is_moving()
|
||||||
{
|
{
|
||||||
// preview:
|
// preview:
|
||||||
paint_cursor_end(ui, &painter, response.rect.min, &galley, &cursor_at_pointer);
|
paint_cursor_end(
|
||||||
|
ui,
|
||||||
|
row_height,
|
||||||
|
&painter,
|
||||||
|
response.rect.min,
|
||||||
|
&galley,
|
||||||
|
&cursor_at_pointer,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.double_clicked() {
|
if response.double_clicked() {
|
||||||
|
@ -729,7 +765,7 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
|
|
||||||
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
||||||
galley = make_galley(ui, wrap_width, text.as_ref());
|
galley = layouter(ui, text.as_ref(), wrap_width);
|
||||||
|
|
||||||
// Set cursorp using new galley:
|
// Set cursorp using new galley:
|
||||||
cursorp = CursorPair {
|
cursorp = CursorPair {
|
||||||
|
@ -778,7 +814,14 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
if ui.memory().has_focus(id) {
|
if ui.memory().has_focus(id) {
|
||||||
if let Some(cursorp) = state.cursorp {
|
if let Some(cursorp) = state.cursorp {
|
||||||
paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursorp);
|
paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursorp);
|
||||||
paint_cursor_end(ui, &painter, text_draw_pos, &galley, &cursorp.primary);
|
paint_cursor_end(
|
||||||
|
ui,
|
||||||
|
row_height,
|
||||||
|
&painter,
|
||||||
|
text_draw_pos,
|
||||||
|
&galley,
|
||||||
|
&cursorp.primary,
|
||||||
|
);
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
ui.ctx().output().text_cursor_pos = Some(
|
ui.ctx().output().text_cursor_pos = Some(
|
||||||
|
@ -791,21 +834,15 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_color = text_color
|
painter.galley(text_draw_pos, galley);
|
||||||
.or(ui.visuals().override_text_color)
|
|
||||||
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
|
||||||
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
|
|
||||||
|
|
||||||
painter.galley(text_draw_pos, galley, text_color);
|
|
||||||
if text.as_ref().is_empty() && !hint_text.is_empty() {
|
if text.as_ref().is_empty() && !hint_text.is_empty() {
|
||||||
let galley = if multiline {
|
|
||||||
ui.fonts()
|
|
||||||
.layout_multiline(text_style, hint_text, desired_size.x)
|
|
||||||
} else {
|
|
||||||
ui.fonts().layout_single_line(text_style, hint_text)
|
|
||||||
};
|
|
||||||
let hint_text_color = ui.visuals().weak_text_color();
|
let hint_text_color = ui.visuals().weak_text_color();
|
||||||
painter.galley(response.rect.min, galley, hint_text_color);
|
let galley = ui.fonts().layout_job(if multiline {
|
||||||
|
LayoutJob::simple(hint_text, text_style, hint_text_color, desired_size.x)
|
||||||
|
} else {
|
||||||
|
LayoutJob::simple_singleline(hint_text, text_style, hint_text_color)
|
||||||
|
});
|
||||||
|
painter.galley(response.rect.min, galley);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.memory().id_data.insert(id, state);
|
ui.memory().id_data.insert(id, state);
|
||||||
|
@ -822,16 +859,18 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
if response.changed {
|
if response.changed {
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
WidgetInfo::text_edit(
|
WidgetInfo::text_edit(
|
||||||
mask_if_password(prev_text.as_str()),
|
mask_if_password(password, prev_text.as_str()),
|
||||||
mask_if_password(text.as_str()),
|
mask_if_password(password, text.as_str()),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
} else if selection_changed {
|
} else if selection_changed {
|
||||||
let text_cursor = text_cursor.unwrap();
|
let text_cursor = text_cursor.unwrap();
|
||||||
let char_range =
|
let char_range =
|
||||||
text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index;
|
text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index;
|
||||||
let info =
|
let info = WidgetInfo::text_selection_changed(
|
||||||
WidgetInfo::text_selection_changed(char_range, mask_if_password(text.as_str()));
|
char_range,
|
||||||
|
mask_if_password(password, text.as_str()),
|
||||||
|
);
|
||||||
response
|
response
|
||||||
.ctx
|
.ctx
|
||||||
.output()
|
.output()
|
||||||
|
@ -840,8 +879,8 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> {
|
||||||
} else {
|
} else {
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
WidgetInfo::text_edit(
|
WidgetInfo::text_edit(
|
||||||
mask_if_password(prev_text.as_str()),
|
mask_if_password(password, prev_text.as_str()),
|
||||||
mask_if_password(text.as_str()),
|
mask_if_password(password, text.as_str()),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -871,7 +910,7 @@ fn paint_cursor_selection(
|
||||||
let left = if ri == min.row {
|
let left = if ri == min.row {
|
||||||
row.x_offset(min.column)
|
row.x_offset(min.column)
|
||||||
} else {
|
} else {
|
||||||
row.min_x()
|
row.rect.left()
|
||||||
};
|
};
|
||||||
let right = if ri == max.row {
|
let right = if ri == max.row {
|
||||||
row.x_offset(max.column)
|
row.x_offset(max.column)
|
||||||
|
@ -881,18 +920,29 @@ fn paint_cursor_selection(
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
row.max_x() + newline_size
|
row.rect.right() + newline_size
|
||||||
};
|
};
|
||||||
let rect = Rect::from_min_max(pos + vec2(left, row.y_min), pos + vec2(right, row.y_max));
|
let rect = Rect::from_min_max(
|
||||||
|
pos + vec2(left, row.min_y()),
|
||||||
|
pos + vec2(right, row.max_y()),
|
||||||
|
);
|
||||||
painter.rect_filled(rect, 0.0, color);
|
painter.rect_filled(rect, 0.0, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_cursor_end(ui: &mut Ui, painter: &Painter, pos: Pos2, galley: &Galley, cursor: &Cursor) {
|
fn paint_cursor_end(
|
||||||
|
ui: &mut Ui,
|
||||||
|
row_height: f32,
|
||||||
|
painter: &Painter,
|
||||||
|
pos: Pos2,
|
||||||
|
galley: &Galley,
|
||||||
|
cursor: &Cursor,
|
||||||
|
) {
|
||||||
let stroke = ui.visuals().selection.stroke;
|
let stroke = ui.visuals().selection.stroke;
|
||||||
|
|
||||||
let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
||||||
let cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys
|
||||||
|
cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
||||||
|
|
||||||
let top = cursor_pos.center_top();
|
let top = cursor_pos.center_top();
|
||||||
let bottom = cursor_pos.center_bottom();
|
let bottom = cursor_pos.center_bottom();
|
||||||
|
@ -1102,7 +1152,7 @@ fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers:
|
||||||
Key::ArrowLeft => {
|
Key::ArrowLeft => {
|
||||||
if modifiers.alt || modifiers.ctrl {
|
if modifiers.alt || modifiers.ctrl {
|
||||||
// alt on mac, ctrl on windows
|
// alt on mac, ctrl on windows
|
||||||
*cursor = galley.from_ccursor(ccursor_previous_word(&galley.text, cursor.ccursor));
|
*cursor = galley.from_ccursor(ccursor_previous_word(galley.text(), cursor.ccursor));
|
||||||
} else if modifiers.mac_cmd {
|
} else if modifiers.mac_cmd {
|
||||||
*cursor = galley.cursor_begin_of_row(cursor);
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1112,7 +1162,7 @@ fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers:
|
||||||
Key::ArrowRight => {
|
Key::ArrowRight => {
|
||||||
if modifiers.alt || modifiers.ctrl {
|
if modifiers.alt || modifiers.ctrl {
|
||||||
// alt on mac, ctrl on windows
|
// alt on mac, ctrl on windows
|
||||||
*cursor = galley.from_ccursor(ccursor_next_word(&galley.text, cursor.ccursor));
|
*cursor = galley.from_ccursor(ccursor_next_word(galley.text(), cursor.ccursor));
|
||||||
} else if modifiers.mac_cmd {
|
} else if modifiers.mac_cmd {
|
||||||
*cursor = galley.cursor_end_of_row(cursor);
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -68,30 +68,39 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
let pixels_per_point = 1.0;
|
let pixels_per_point = 1.0;
|
||||||
let wrap_width = 512.0;
|
let wrap_width = 512.0;
|
||||||
let text_style = egui::TextStyle::Body;
|
let text_style = egui::TextStyle::Body;
|
||||||
|
let color = egui::Color32::WHITE;
|
||||||
let fonts = egui::epaint::text::Fonts::from_definitions(
|
let fonts = egui::epaint::text::Fonts::from_definitions(
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
egui::FontDefinitions::default(),
|
egui::FontDefinitions::default(),
|
||||||
);
|
);
|
||||||
let font = &fonts[text_style];
|
|
||||||
c.bench_function("text_layout_uncached", |b| {
|
c.bench_function("text_layout_uncached", |b| {
|
||||||
b.iter(|| font.layout_multiline(LOREM_IPSUM_LONG.to_owned(), wrap_width))
|
b.iter(|| {
|
||||||
|
use egui::epaint::text::{layout, LayoutJob};
|
||||||
|
|
||||||
|
let job = LayoutJob::simple(
|
||||||
|
LOREM_IPSUM_LONG.to_owned(),
|
||||||
|
egui::TextStyle::Body,
|
||||||
|
color,
|
||||||
|
wrap_width,
|
||||||
|
);
|
||||||
|
layout(&fonts, job.into())
|
||||||
|
})
|
||||||
});
|
});
|
||||||
c.bench_function("text_layout_cached", |b| {
|
c.bench_function("text_layout_cached", |b| {
|
||||||
b.iter(|| fonts.layout_multiline(text_style, LOREM_IPSUM_LONG.to_owned(), wrap_width))
|
b.iter(|| fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width))
|
||||||
});
|
});
|
||||||
|
|
||||||
let galley = font.layout_multiline(LOREM_IPSUM_LONG.to_owned(), wrap_width);
|
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width);
|
||||||
let mut tessellator = egui::epaint::Tessellator::from_options(Default::default());
|
let mut tessellator = egui::epaint::Tessellator::from_options(Default::default());
|
||||||
let mut mesh = egui::epaint::Mesh::default();
|
let mut mesh = egui::epaint::Mesh::default();
|
||||||
c.bench_function("tessellate_text", |b| {
|
c.bench_function("tessellate_text", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let fake_italics = false;
|
|
||||||
tessellator.tessellate_text(
|
tessellator.tessellate_text(
|
||||||
fonts.texture().size(),
|
fonts.texture().size(),
|
||||||
egui::Pos2::ZERO,
|
egui::Pos2::ZERO,
|
||||||
&galley,
|
&galley,
|
||||||
egui::Color32::WHITE,
|
Default::default(),
|
||||||
fake_italics,
|
None,
|
||||||
&mut mesh,
|
&mut mesh,
|
||||||
);
|
);
|
||||||
mesh.clear();
|
mesh.clear();
|
||||||
|
|
357
egui_demo_lib/src/apps/demo/code_editor.rs
Normal file
357
egui_demo_lib/src/apps/demo/code_editor.rs
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "persistence", serde(default))]
|
||||||
|
pub struct CodeEditor {
|
||||||
|
code: String,
|
||||||
|
language: String,
|
||||||
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
|
highlighter: MemoizedSyntaxHighlighter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeEditor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
code: "// A very simple example\n\
|
||||||
|
fn main() {\n\
|
||||||
|
\tprintln!(\"Hello world!\");\n\
|
||||||
|
}\n\
|
||||||
|
"
|
||||||
|
.into(),
|
||||||
|
language: "rs".into(),
|
||||||
|
highlighter: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::Demo for CodeEditor {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"🖮 Code Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||||
|
use super::View;
|
||||||
|
egui::Window::new(self.name())
|
||||||
|
.open(open)
|
||||||
|
.show(ctx, |ui| self.ui(ui));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for CodeEditor {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let Self {
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
highlighter,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.set_height(0.0);
|
||||||
|
ui.label("An example of syntax highlighting in a TextEdit.");
|
||||||
|
ui.add(crate::__egui_github_link_file!());
|
||||||
|
});
|
||||||
|
|
||||||
|
if cfg!(feature = "syntect") {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Language:");
|
||||||
|
ui.text_edit_singleline(language);
|
||||||
|
});
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
ui.label("Syntax highlighting powered by ");
|
||||||
|
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||||
|
ui.label(".");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.horizontal_wrapped(|ui|{
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
ui.label("Compile the demo with the 'syntect' feature to enable much nicer syntax highlighting using ");
|
||||||
|
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||||
|
ui.label(".");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
|
let mut layout_job = highlighter.highlight(ui.visuals().dark_mode, string, language);
|
||||||
|
layout_job.wrap_width = wrap_width;
|
||||||
|
ui.fonts().layout_job(layout_job)
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(code)
|
||||||
|
.text_style(egui::TextStyle::Monospace) // for cursor height
|
||||||
|
.code_editor()
|
||||||
|
.lock_focus(true)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.layouter(&mut layouter),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MemoizedSyntaxHighlighter {
|
||||||
|
is_dark_mode: bool,
|
||||||
|
code: String,
|
||||||
|
language: String,
|
||||||
|
output: LayoutJob,
|
||||||
|
highligher: Highligher,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoizedSyntaxHighlighter {
|
||||||
|
fn highlight(&mut self, is_dark_mode: bool, code: &str, language: &str) -> LayoutJob {
|
||||||
|
if (
|
||||||
|
self.is_dark_mode,
|
||||||
|
self.code.as_str(),
|
||||||
|
self.language.as_str(),
|
||||||
|
) != (is_dark_mode, code, language)
|
||||||
|
{
|
||||||
|
self.is_dark_mode = is_dark_mode;
|
||||||
|
self.code = code.to_owned();
|
||||||
|
self.language = language.to_owned();
|
||||||
|
self.output = self
|
||||||
|
.highligher
|
||||||
|
.highlight(is_dark_mode, code, language)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
LayoutJob::simple(
|
||||||
|
code.into(),
|
||||||
|
egui::TextStyle::Monospace,
|
||||||
|
if is_dark_mode {
|
||||||
|
egui::Color32::LIGHT_GRAY
|
||||||
|
} else {
|
||||||
|
egui::Color32::DARK_GRAY
|
||||||
|
},
|
||||||
|
f32::INFINITY,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.output.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
struct Highligher {
|
||||||
|
ps: syntect::parsing::SyntaxSet,
|
||||||
|
ts: syntect::highlighting::ThemeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
impl Default for Highligher {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||||
|
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
impl Highligher {
|
||||||
|
fn highlight(&self, is_dark_mode: bool, text: &str, language: &str) -> Option<LayoutJob> {
|
||||||
|
use syntect::easy::HighlightLines;
|
||||||
|
use syntect::highlighting::FontStyle;
|
||||||
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
|
let syntax = self
|
||||||
|
.ps
|
||||||
|
.find_syntax_by_name(language)
|
||||||
|
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||||
|
|
||||||
|
let theme = if is_dark_mode {
|
||||||
|
"base16-mocha.dark"
|
||||||
|
} else {
|
||||||
|
"base16-ocean.light"
|
||||||
|
};
|
||||||
|
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
||||||
|
|
||||||
|
use egui::text::{LayoutSection, TextFormat};
|
||||||
|
|
||||||
|
let mut job = LayoutJob {
|
||||||
|
text: text.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in LinesWithEndings::from(text) {
|
||||||
|
for (style, range) in h.highlight(line, &self.ps) {
|
||||||
|
let fg = style.foreground;
|
||||||
|
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||||
|
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = if underline {
|
||||||
|
egui::Stroke::new(1.0, text_color)
|
||||||
|
} else {
|
||||||
|
egui::Stroke::none()
|
||||||
|
};
|
||||||
|
job.sections.push(LayoutSection {
|
||||||
|
leading_space: 0.0,
|
||||||
|
byte_range: as_byte_range(text, range),
|
||||||
|
format: TextFormat {
|
||||||
|
style: egui::TextStyle::Monospace,
|
||||||
|
color: text_color,
|
||||||
|
italics,
|
||||||
|
underline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||||
|
let whole_start = whole.as_ptr() as usize;
|
||||||
|
let range_start = range.as_ptr() as usize;
|
||||||
|
assert!(whole_start <= range_start);
|
||||||
|
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||||
|
let offset = range_start - whole_start;
|
||||||
|
offset..(offset + range.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Highligher {}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
impl Highligher {
|
||||||
|
fn highlight(&self, is_dark_mode: bool, mut text: &str, _language: &str) -> Option<LayoutJob> {
|
||||||
|
// Extremely simple syntax highlighter for when we compile without syntect
|
||||||
|
|
||||||
|
use egui::text::TextFormat;
|
||||||
|
use egui::Color32;
|
||||||
|
let monospace = egui::TextStyle::Monospace;
|
||||||
|
|
||||||
|
let comment_format = TextFormat::simple(monospace, Color32::GRAY);
|
||||||
|
let quoted_string_format = TextFormat::simple(
|
||||||
|
monospace,
|
||||||
|
if is_dark_mode {
|
||||||
|
Color32::KHAKI
|
||||||
|
} else {
|
||||||
|
Color32::BROWN
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let keyword_format = TextFormat::simple(
|
||||||
|
monospace,
|
||||||
|
if is_dark_mode {
|
||||||
|
Color32::LIGHT_RED
|
||||||
|
} else {
|
||||||
|
Color32::DARK_RED
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let literal_format = TextFormat::simple(
|
||||||
|
monospace,
|
||||||
|
if is_dark_mode {
|
||||||
|
Color32::LIGHT_GREEN
|
||||||
|
} else {
|
||||||
|
Color32::DARK_GREEN
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let whitespace_format = TextFormat::simple(monospace, Color32::WHITE);
|
||||||
|
let punctuation_format = TextFormat::simple(
|
||||||
|
monospace,
|
||||||
|
if is_dark_mode {
|
||||||
|
Color32::LIGHT_GRAY
|
||||||
|
} else {
|
||||||
|
Color32::DARK_GRAY
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
|
||||||
|
while !text.is_empty() {
|
||||||
|
if text.starts_with("//") {
|
||||||
|
let end = text.find('\n').unwrap_or(text.len());
|
||||||
|
job.append(&text[..end], 0.0, comment_format);
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with('"') {
|
||||||
|
let end = text[1..]
|
||||||
|
.find('"')
|
||||||
|
.map(|i| i + 2)
|
||||||
|
.or_else(|| text.find('\n'))
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
job.append(&text[..end], 0.0, quoted_string_format);
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||||
|
let end = text[1..]
|
||||||
|
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
let word = &text[..end];
|
||||||
|
if is_keyword(word) {
|
||||||
|
job.append(word, 0.0, keyword_format);
|
||||||
|
} else {
|
||||||
|
job.append(word, 0.0, literal_format);
|
||||||
|
};
|
||||||
|
text = &text[end..];
|
||||||
|
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
||||||
|
let end = text[1..]
|
||||||
|
.find(|c: char| !c.is_ascii_whitespace())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
job.append(&text[..end], 0.0, whitespace_format);
|
||||||
|
text = &text[end..];
|
||||||
|
} else {
|
||||||
|
let mut it = text.char_indices();
|
||||||
|
it.next();
|
||||||
|
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
||||||
|
job.append(&text[..end], 0.0, punctuation_format);
|
||||||
|
text = &text[end..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "syntect"))]
|
||||||
|
fn is_keyword(word: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
word,
|
||||||
|
"as" | "async"
|
||||||
|
| "await"
|
||||||
|
| "break"
|
||||||
|
| "const"
|
||||||
|
| "continue"
|
||||||
|
| "crate"
|
||||||
|
| "dyn"
|
||||||
|
| "else"
|
||||||
|
| "enum"
|
||||||
|
| "extern"
|
||||||
|
| "false"
|
||||||
|
| "fn"
|
||||||
|
| "for"
|
||||||
|
| "if"
|
||||||
|
| "impl"
|
||||||
|
| "in"
|
||||||
|
| "let"
|
||||||
|
| "loop"
|
||||||
|
| "match"
|
||||||
|
| "mod"
|
||||||
|
| "move"
|
||||||
|
| "mut"
|
||||||
|
| "pub"
|
||||||
|
| "ref"
|
||||||
|
| "return"
|
||||||
|
| "self"
|
||||||
|
| "Self"
|
||||||
|
| "static"
|
||||||
|
| "struct"
|
||||||
|
| "super"
|
||||||
|
| "trait"
|
||||||
|
| "true"
|
||||||
|
| "type"
|
||||||
|
| "unsafe"
|
||||||
|
| "use"
|
||||||
|
| "where"
|
||||||
|
| "while"
|
||||||
|
)
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ struct Demos {
|
||||||
impl Default for Demos {
|
impl Default for Demos {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::from_demos(vec![
|
Self::from_demos(vec![
|
||||||
|
Box::new(super::code_editor::CodeEditor::default()),
|
||||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||||
Box::new(super::font_book::FontBook::default()),
|
Box::new(super::font_book::FontBook::default()),
|
||||||
|
|
|
@ -50,6 +50,12 @@ impl View for MiscDemoWindow {
|
||||||
self.widgets.ui(ui);
|
self.widgets.ui(ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CollapsingHeader::new("Text layout")
|
||||||
|
.default_open(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
text_layout_ui(ui);
|
||||||
|
});
|
||||||
|
|
||||||
CollapsingHeader::new("Colors")
|
CollapsingHeader::new("Colors")
|
||||||
.default_open(false)
|
.default_open(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
@ -114,8 +120,6 @@ impl View for MiscDemoWindow {
|
||||||
pub struct Widgets {
|
pub struct Widgets {
|
||||||
angle: f32,
|
angle: f32,
|
||||||
password: String,
|
password: String,
|
||||||
lock_focus: bool,
|
|
||||||
code_snippet: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Widgets {
|
impl Default for Widgets {
|
||||||
|
@ -123,25 +127,13 @@ impl Default for Widgets {
|
||||||
Self {
|
Self {
|
||||||
angle: std::f32::consts::TAU / 3.0,
|
angle: std::f32::consts::TAU / 3.0,
|
||||||
password: "hunter2".to_owned(),
|
password: "hunter2".to_owned(),
|
||||||
lock_focus: true,
|
|
||||||
code_snippet: "\
|
|
||||||
fn main() {
|
|
||||||
\tprintln!(\"Hello world!\");
|
|
||||||
}
|
|
||||||
"
|
|
||||||
.to_owned(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widgets {
|
impl Widgets {
|
||||||
pub fn ui(&mut self, ui: &mut Ui) {
|
pub fn ui(&mut self, ui: &mut Ui) {
|
||||||
let Self {
|
let Self { angle, password } = self;
|
||||||
angle,
|
|
||||||
password,
|
|
||||||
lock_focus,
|
|
||||||
code_snippet,
|
|
||||||
} = self;
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add(crate::__egui_github_link_file_line!());
|
ui.add(crate::__egui_github_link_file_line!());
|
||||||
});
|
});
|
||||||
|
@ -196,24 +188,6 @@ impl Widgets {
|
||||||
.on_hover_text("See the example code for how to use egui to store UI state");
|
.on_hover_text("See the example code for how to use egui to store UI state");
|
||||||
ui.add(super::password::password(password));
|
ui.add(super::password::password(password));
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Code editor:");
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.checkbox(lock_focus, "Lock focus").on_hover_text(
|
|
||||||
"When checked, pressing TAB will insert a tab instead of moving focus",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
TextEdit::multiline(code_snippet)
|
|
||||||
.code_editor()
|
|
||||||
.lock_focus(*lock_focus),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,3 +397,182 @@ impl SubTree {
|
||||||
Action::Keep
|
Action::Keep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn text_layout_ui(ui: &mut egui::Ui) {
|
||||||
|
use egui::epaint::text::{LayoutJob, TextFormat};
|
||||||
|
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
|
||||||
|
let first_row_indentation = 10.0;
|
||||||
|
|
||||||
|
let (default_color, strong_color) = if ui.visuals().dark_mode {
|
||||||
|
(Color32::LIGHT_GRAY, Color32::WHITE)
|
||||||
|
} else {
|
||||||
|
(Color32::DARK_GRAY, Color32::BLACK)
|
||||||
|
};
|
||||||
|
|
||||||
|
job.append(
|
||||||
|
"This is a demonstration of ",
|
||||||
|
first_row_indentation,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"the egui text layout engine. ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: strong_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"It supports ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"different ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: Color32::from_rgb(110, 255, 110),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"colors, ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: Color32::from_rgb(128, 140, 255),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"backgrounds, ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
background: Color32::from_rgb(128, 32, 32),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"mixing ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Heading,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"fonts, ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Monospace,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"raised text, ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Small,
|
||||||
|
color: default_color,
|
||||||
|
valign: Align::TOP,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"with ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"underlining",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
underline: Stroke::new(1.0, Color32::LIGHT_BLUE),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
" and ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"strikethrough",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
strikethrough: Stroke::new(2.0, Color32::RED.linear_multiply(0.5)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
". Of course, ",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
"you can",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: default_color,
|
||||||
|
strikethrough: Stroke::new(1.0, strong_color),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
job.append(
|
||||||
|
" mix these!",
|
||||||
|
0.0,
|
||||||
|
TextFormat {
|
||||||
|
style: TextStyle::Small,
|
||||||
|
color: Color32::LIGHT_BLUE,
|
||||||
|
background: Color32::from_rgb(128, 0, 0),
|
||||||
|
underline: Stroke::new(1.0, strong_color),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
job.wrap_width = ui.available_width();
|
||||||
|
|
||||||
|
let galley = ui.fonts().layout_job(job);
|
||||||
|
|
||||||
|
let (response, painter) = ui.allocate_painter(galley.size, Sense::hover());
|
||||||
|
painter.add(Shape::galley(response.rect.min, galley));
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add(crate::__egui_github_link_file_line!());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
pub mod code_editor;
|
||||||
pub mod dancing_strings;
|
pub mod dancing_strings;
|
||||||
pub mod demo_app_windows;
|
pub mod demo_app_windows;
|
||||||
pub mod drag_and_drop;
|
pub mod drag_and_drop;
|
||||||
|
|
|
@ -292,13 +292,14 @@ fn syntax_highlighting(response: &Response, text: &str) -> Option<ColoredText> {
|
||||||
|
|
||||||
/// Lines of text fragments
|
/// Lines of text fragments
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
struct ColoredText(Vec<Vec<(syntect::highlighting::Style, String)>>);
|
struct ColoredText(egui::text::LayoutJob);
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
impl ColoredText {
|
impl ColoredText {
|
||||||
/// e.g. `text_with_extension("fn foo() {}", "rs")`
|
/// e.g. `text_with_extension("fn foo() {}", "rs")`
|
||||||
pub fn text_with_extension(text: &str, extension: &str) -> Option<ColoredText> {
|
pub fn text_with_extension(text: &str, extension: &str) -> Option<ColoredText> {
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
|
use syntect::highlighting::FontStyle;
|
||||||
use syntect::highlighting::ThemeSet;
|
use syntect::highlighting::ThemeSet;
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
|
@ -308,36 +309,67 @@ impl ColoredText {
|
||||||
|
|
||||||
let syntax = ps.find_syntax_by_extension(extension)?;
|
let syntax = ps.find_syntax_by_extension(extension)?;
|
||||||
|
|
||||||
let mut h = HighlightLines::new(syntax, &ts.themes["base16-mocha.dark"]);
|
let dark_mode = true;
|
||||||
|
let theme = if dark_mode {
|
||||||
|
"base16-mocha.dark"
|
||||||
|
} else {
|
||||||
|
"base16-ocean.light"
|
||||||
|
};
|
||||||
|
let mut h = HighlightLines::new(syntax, &ts.themes[theme]);
|
||||||
|
|
||||||
let lines = LinesWithEndings::from(text)
|
use egui::text::{LayoutJob, LayoutSection, TextFormat};
|
||||||
.map(|line| {
|
|
||||||
h.highlight(line, &ps)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(style, range)| (style, range.trim_end_matches('\n').to_owned()))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(ColoredText(lines))
|
let mut job = LayoutJob {
|
||||||
|
text: text.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in LinesWithEndings::from(text) {
|
||||||
|
for (style, range) in h.highlight(line, &ps) {
|
||||||
|
let fg = style.foreground;
|
||||||
|
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||||
|
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = if underline {
|
||||||
|
egui::Stroke::new(1.0, text_color)
|
||||||
|
} else {
|
||||||
|
egui::Stroke::none()
|
||||||
|
};
|
||||||
|
job.sections.push(LayoutSection {
|
||||||
|
leading_space: 0.0,
|
||||||
|
byte_range: as_byte_range(text, range),
|
||||||
|
format: TextFormat {
|
||||||
|
style: egui::TextStyle::Monospace,
|
||||||
|
color: text_color,
|
||||||
|
italics,
|
||||||
|
underline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ColoredText(job))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||||
for line in &self.0 {
|
let mut job = self.0.clone();
|
||||||
ui.horizontal_wrapped(|ui| {
|
job.wrap_width = ui.available_width();
|
||||||
ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
|
let galley = ui.fonts().layout_job(job);
|
||||||
ui.set_row_height(ui.fonts()[egui::TextStyle::Body].row_height());
|
let (response, painter) = ui.allocate_painter(galley.size, egui::Sense::hover());
|
||||||
|
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||||
for (style, range) in line {
|
|
||||||
let fg = style.foreground;
|
|
||||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
|
||||||
ui.add(egui::Label::new(range).monospace().text_color(text_color));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||||
|
let whole_start = whole.as_ptr() as usize;
|
||||||
|
let range_start = range.as_ptr() as usize;
|
||||||
|
assert!(whole_start <= range_start);
|
||||||
|
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||||
|
let offset = range_start - whole_start;
|
||||||
|
offset..(offset + range.len())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
fn syntax_highlighting(_: &Response, _: &str) -> Option<ColoredText> {
|
fn syntax_highlighting(_: &Response, _: &str) -> Option<ColoredText> {
|
||||||
None
|
None
|
||||||
|
|
|
@ -45,7 +45,7 @@ pub struct Style {
|
||||||
pub strong: bool,
|
pub strong: bool,
|
||||||
/// _underline_
|
/// _underline_
|
||||||
pub underline: bool,
|
pub underline: bool,
|
||||||
/// -strikethrough-
|
/// ~strikethrough~
|
||||||
pub strikethrough: bool,
|
pub strikethrough: bool,
|
||||||
/// /italics/
|
/// /italics/
|
||||||
pub italics: bool,
|
pub italics: bool,
|
||||||
|
@ -67,7 +67,7 @@ pub struct Style {
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Parser<'a> {
|
pub struct Parser<'a> {
|
||||||
/// The remainer of the input text
|
/// The remainder of the input text
|
||||||
s: &'a str,
|
s: &'a str,
|
||||||
/// Are we at the start of a line?
|
/// Are we at the start of a line?
|
||||||
start_of_line: bool,
|
start_of_line: bool,
|
||||||
|
|
|
@ -7,7 +7,16 @@ pub fn easy_mark(ui: &mut Ui, easy_mark: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator<Item = easy_mark::Item<'em>>) {
|
pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator<Item = easy_mark::Item<'em>>) {
|
||||||
ui.horizontal_wrapped(|ui| {
|
let initial_size = vec2(
|
||||||
|
ui.available_width(),
|
||||||
|
ui.spacing().interact_size.y, // Assume there will be
|
||||||
|
);
|
||||||
|
|
||||||
|
let layout = Layout::left_to_right()
|
||||||
|
.with_main_wrap(true)
|
||||||
|
.with_cross_align(Align::BOTTOM);
|
||||||
|
|
||||||
|
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.set_row_height(ui.fonts()[TextStyle::Body].row_height());
|
ui.set_row_height(ui.fonts()[TextStyle::Body].row_height());
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// left/center/right or top/center/bottom alignment for e.g. anchors and layouts.
|
/// left/center/right or top/center/bottom alignment for e.g. anchors and layouts.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
#[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 Align {
|
pub enum Align {
|
||||||
|
@ -100,7 +100,7 @@ impl Default for Align {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`].
|
/// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`].
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
#[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 struct Align2(pub [Align; 2]);
|
pub struct Align2(pub [Align; 2]);
|
||||||
|
|
|
@ -109,6 +109,7 @@ impl Pos2 {
|
||||||
/// Same as `Pos2::default()`.
|
/// Same as `Pos2::default()`.
|
||||||
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
|
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub const fn new(x: f32, y: f32) -> Self {
|
pub const fn new(x: f32, y: f32) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ impl Rect {
|
||||||
Rect { min, max }
|
Rect { min, max }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn from_min_size(min: Pos2, size: Vec2) -> Self {
|
pub fn from_min_size(min: Pos2, size: Vec2) -> Self {
|
||||||
Rect {
|
Rect {
|
||||||
min,
|
min,
|
||||||
|
@ -63,6 +64,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn from_center_size(center: Pos2, size: Vec2) -> Self {
|
pub fn from_center_size(center: Pos2, size: Vec2) -> Self {
|
||||||
Rect {
|
Rect {
|
||||||
min: center - size * 0.5,
|
min: center - size * 0.5,
|
||||||
|
@ -70,6 +72,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn from_x_y_ranges(x_range: RangeInclusive<f32>, y_range: RangeInclusive<f32>) -> Self {
|
pub fn from_x_y_ranges(x_range: RangeInclusive<f32>, y_range: RangeInclusive<f32>) -> Self {
|
||||||
Rect {
|
Rect {
|
||||||
min: pos2(*x_range.start(), *y_range.start()),
|
min: pos2(*x_range.start(), *y_range.start()),
|
||||||
|
@ -77,6 +80,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn from_two_pos(a: Pos2, b: Pos2) -> Self {
|
pub fn from_two_pos(a: Pos2, b: Pos2) -> Self {
|
||||||
Rect {
|
Rect {
|
||||||
min: pos2(a.x.min(b.x), a.y.min(b.y)),
|
min: pos2(a.x.min(b.x), a.y.min(b.y)),
|
||||||
|
@ -85,6 +89,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Rect` that contains every point to the right of the given X coordinate.
|
/// A `Rect` that contains every point to the right of the given X coordinate.
|
||||||
|
#[inline]
|
||||||
pub fn everything_right_of(left_x: f32) -> Self {
|
pub fn everything_right_of(left_x: f32) -> Self {
|
||||||
let mut rect = Self::EVERYTHING;
|
let mut rect = Self::EVERYTHING;
|
||||||
rect.set_left(left_x);
|
rect.set_left(left_x);
|
||||||
|
@ -92,6 +97,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Rect` that contains every point to the left of the given X coordinate.
|
/// A `Rect` that contains every point to the left of the given X coordinate.
|
||||||
|
#[inline]
|
||||||
pub fn everything_left_of(right_x: f32) -> Self {
|
pub fn everything_left_of(right_x: f32) -> Self {
|
||||||
let mut rect = Self::EVERYTHING;
|
let mut rect = Self::EVERYTHING;
|
||||||
rect.set_right(right_x);
|
rect.set_right(right_x);
|
||||||
|
@ -99,6 +105,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Rect` that contains every point below a certain y coordinate
|
/// A `Rect` that contains every point below a certain y coordinate
|
||||||
|
#[inline]
|
||||||
pub fn everything_below(top_y: f32) -> Self {
|
pub fn everything_below(top_y: f32) -> Self {
|
||||||
let mut rect = Self::EVERYTHING;
|
let mut rect = Self::EVERYTHING;
|
||||||
rect.set_top(top_y);
|
rect.set_top(top_y);
|
||||||
|
@ -106,6 +113,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Rect` that contains every point above a certain y coordinate
|
/// A `Rect` that contains every point above a certain y coordinate
|
||||||
|
#[inline]
|
||||||
pub fn everything_above(bottom_y: f32) -> Self {
|
pub fn everything_above(bottom_y: f32) -> Self {
|
||||||
let mut rect = Self::EVERYTHING;
|
let mut rect = Self::EVERYTHING;
|
||||||
rect.set_bottom(bottom_y);
|
rect.set_bottom(bottom_y);
|
||||||
|
@ -137,6 +145,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
pub fn translate(self, amnt: Vec2) -> Self {
|
pub fn translate(self, amnt: Vec2) -> Self {
|
||||||
Rect::from_min_size(self.min + amnt, self.size())
|
Rect::from_min_size(self.min + amnt, self.size())
|
||||||
}
|
}
|
||||||
|
@ -151,6 +160,7 @@ impl Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
pub fn intersects(self, other: Rect) -> bool {
|
pub fn intersects(self, other: Rect) -> bool {
|
||||||
self.min.x <= other.max.x
|
self.min.x <= other.max.x
|
||||||
&& other.min.x <= self.max.x
|
&& other.min.x <= self.max.x
|
||||||
|
@ -191,23 +201,27 @@ impl Rect {
|
||||||
p.clamp(self.min, self.max)
|
p.clamp(self.min, self.max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn extend_with(&mut self, p: Pos2) {
|
pub fn extend_with(&mut self, p: Pos2) {
|
||||||
self.min = self.min.min(p);
|
self.min = self.min.min(p);
|
||||||
self.max = self.max.max(p);
|
self.max = self.max.max(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
/// Expand to include the given x coordinate
|
/// Expand to include the given x coordinate
|
||||||
pub fn extend_with_x(&mut self, x: f32) {
|
pub fn extend_with_x(&mut self, x: f32) {
|
||||||
self.min.x = self.min.x.min(x);
|
self.min.x = self.min.x.min(x);
|
||||||
self.max.x = self.max.x.max(x);
|
self.max.x = self.max.x.max(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
/// Expand to include the given y coordinate
|
/// Expand to include the given y coordinate
|
||||||
pub fn extend_with_y(&mut self, y: f32) {
|
pub fn extend_with_y(&mut self, y: f32) {
|
||||||
self.min.y = self.min.y.min(y);
|
self.min.y = self.min.y.min(y);
|
||||||
self.max.y = self.max.y.max(y);
|
self.max.y = self.max.y.max(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn union(self, other: Rect) -> Rect {
|
pub fn union(self, other: Rect) -> Rect {
|
||||||
Rect {
|
Rect {
|
||||||
min: self.min.min(other.min),
|
min: self.min.min(other.min),
|
||||||
|
@ -260,16 +274,22 @@ impl Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn area(&self) -> f32 {
|
pub fn area(&self) -> f32 {
|
||||||
self.width() * self.height()
|
self.width() * self.height()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn x_range(&self) -> RangeInclusive<f32> {
|
pub fn x_range(&self) -> RangeInclusive<f32> {
|
||||||
self.min.x..=self.max.x
|
self.min.x..=self.max.x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn y_range(&self) -> RangeInclusive<f32> {
|
pub fn y_range(&self) -> RangeInclusive<f32> {
|
||||||
self.min.y..=self.max.y
|
self.min.y..=self.max.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
pub fn bottom_up_range(&self) -> RangeInclusive<f32> {
|
pub fn bottom_up_range(&self) -> RangeInclusive<f32> {
|
||||||
self.max.y..=self.min.y
|
self.max.y..=self.min.y
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,10 @@ impl Vec2 {
|
||||||
/// `v.to_pos2()` is equivalent to `Pos2::default() + v`.
|
/// `v.to_pos2()` is equivalent to `Pos2::default() + v`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_pos2(self) -> crate::Pos2 {
|
pub fn to_pos2(self) -> crate::Pos2 {
|
||||||
crate::Pos2::new(self.x, self.y)
|
crate::Pos2 {
|
||||||
|
x: self.x,
|
||||||
|
y: self.y,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Safe normalize: returns zero if input is zero.
|
/// Safe normalize: returns zero if input is zero.
|
||||||
|
|
|
@ -28,7 +28,6 @@ ab_glyph = "0.2.11"
|
||||||
ahash = { version = "0.7", features = ["std"], default-features = false }
|
ahash = { version = "0.7", features = ["std"], default-features = false }
|
||||||
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
||||||
cint = { version = "^0.2.2", optional = true }
|
cint = { version = "^0.2.2", optional = true }
|
||||||
ordered-float = { version = "2", default-features = false }
|
|
||||||
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
|
|
|
@ -34,20 +34,39 @@ impl std::ops::IndexMut<usize> for Color32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color32 {
|
impl Color32 {
|
||||||
|
// Mostly follows CSS names:
|
||||||
|
|
||||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||||
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
||||||
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
|
||||||
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
||||||
|
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
||||||
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
||||||
|
|
||||||
|
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
|
||||||
|
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||||
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
||||||
|
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
|
||||||
|
|
||||||
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
||||||
|
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
|
||||||
|
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
|
||||||
|
|
||||||
|
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||||
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
||||||
|
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
|
||||||
|
|
||||||
|
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
|
||||||
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
||||||
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(140, 160, 255);
|
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
|
||||||
|
|
||||||
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
||||||
|
|
||||||
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
|
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
|
||||||
|
|
||||||
|
/// An ugly color that is planned to be replaced before making it to the screen.
|
||||||
|
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self([r, g, b, 255])
|
Self([r, g, b, 255])
|
||||||
|
|
|
@ -175,3 +175,26 @@ macro_rules! epaint_assert {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||||
|
if f == 0.0 {
|
||||||
|
state.write_u8(0)
|
||||||
|
} else if f.is_nan() {
|
||||||
|
state.write_u8(1)
|
||||||
|
} else {
|
||||||
|
use std::hash::Hash;
|
||||||
|
f.to_bits().hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn f32_eq(a: f32, b: f32) -> bool {
|
||||||
|
if a.is_nan() && b.is_nan() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
a == b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ pub struct Mesh {
|
||||||
|
|
||||||
/// The texture to use when drawing these triangles.
|
/// The texture to use when drawing these triangles.
|
||||||
pub texture_id: TextureId,
|
pub texture_id: TextureId,
|
||||||
|
// TODO: bounding rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mesh {
|
impl Mesh {
|
||||||
|
@ -72,6 +73,15 @@ impl Mesh {
|
||||||
self.indices.is_empty() && self.vertices.is_empty()
|
self.indices.is_empty() && self.vertices.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate a bounding rectangle.
|
||||||
|
pub fn calc_bounds(&self) -> Rect {
|
||||||
|
let mut bounds = Rect::NOTHING;
|
||||||
|
for v in &self.vertices {
|
||||||
|
bounds.extend_with(v.pos);
|
||||||
|
}
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
/// Append all the indices and vertices of `other` to `self`.
|
/// Append all the indices and vertices of `other` to `self`.
|
||||||
pub fn append(&mut self, other: Mesh) {
|
pub fn append(&mut self, other: Mesh) {
|
||||||
crate::epaint_assert!(other.is_valid());
|
crate::epaint_assert!(other.is_valid());
|
||||||
|
@ -85,9 +95,8 @@ impl Mesh {
|
||||||
);
|
);
|
||||||
|
|
||||||
let index_offset = self.vertices.len() as u32;
|
let index_offset = self.vertices.len() as u32;
|
||||||
for index in &other.indices {
|
self.indices
|
||||||
self.indices.push(index_offset + index);
|
.extend(other.indices.iter().map(|index| index + index_offset));
|
||||||
}
|
|
||||||
self.vertices.extend(other.vertices.iter());
|
self.vertices.extend(other.vertices.iter());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,12 +43,18 @@ pub enum Shape {
|
||||||
Text {
|
Text {
|
||||||
/// Top left corner of the first character..
|
/// Top left corner of the first character..
|
||||||
pos: Pos2,
|
pos: Pos2,
|
||||||
|
|
||||||
/// The layed out text.
|
/// The layed out text.
|
||||||
galley: std::sync::Arc<Galley>,
|
galley: std::sync::Arc<Galley>,
|
||||||
/// Text color (foreground).
|
|
||||||
color: Color32,
|
/// Add this underline to the whole text.
|
||||||
/// If true, tilt the letters for a hacky italics effect.
|
/// You can also set an underline when creating the galley.
|
||||||
fake_italics: bool,
|
underline: Stroke,
|
||||||
|
|
||||||
|
/// If set, the text color in the galley will be ignored and replaced
|
||||||
|
/// with the given color.
|
||||||
|
/// This will NOT replace background color nor strikethrough/underline color.
|
||||||
|
override_text_color: Option<Color32>,
|
||||||
},
|
},
|
||||||
Mesh(Mesh),
|
Mesh(Mesh),
|
||||||
}
|
}
|
||||||
|
@ -169,13 +175,17 @@ impl Shape {
|
||||||
text_style: TextStyle,
|
text_style: TextStyle,
|
||||||
color: Color32,
|
color: Color32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let galley = fonts.layout_multiline(text_style, text.to_string(), f32::INFINITY);
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn galley(pos: Pos2, galley: std::sync::Arc<Galley>) -> Self {
|
||||||
Self::Text {
|
Self::Text {
|
||||||
pos: rect.min,
|
pos,
|
||||||
galley,
|
galley,
|
||||||
color,
|
override_text_color: None,
|
||||||
fake_italics: false,
|
underline: Stroke::none(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,23 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
|
||||||
adjust_color(fill);
|
adjust_color(fill);
|
||||||
adjust_color(&mut stroke.color);
|
adjust_color(&mut stroke.color);
|
||||||
}
|
}
|
||||||
Shape::Text { color, .. } => {
|
Shape::Text {
|
||||||
adjust_color(color);
|
galley,
|
||||||
|
override_text_color,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(override_text_color) = override_text_color {
|
||||||
|
adjust_color(override_text_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !galley.is_empty() {
|
||||||
|
let galley = std::sync::Arc::make_mut(galley);
|
||||||
|
for row in &mut galley.rows {
|
||||||
|
for vertex in &mut row.visuals.mesh.vertices {
|
||||||
|
adjust_color(&mut vertex.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Shape::Mesh(mesh) => {
|
Shape::Mesh(mesh) => {
|
||||||
for v in &mut mesh.vertices {
|
for v in &mut mesh.vertices {
|
||||||
|
|
|
@ -85,13 +85,13 @@ impl AllocInfo {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn from_galley(galley: &Galley) -> Self {
|
pub fn from_galley(galley: &Galley) -> Self {
|
||||||
Self::from_slice(galley.text.as_bytes())
|
Self::from_slice(galley.text().as_bytes())
|
||||||
+ Self::from_slice(&galley.rows)
|
+ Self::from_slice(&galley.rows)
|
||||||
+ galley.rows.iter().map(Self::from_galley_row).sum()
|
+ galley.rows.iter().map(Self::from_galley_row).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_galley_row(row: &crate::text::Row) -> Self {
|
fn from_galley_row(row: &crate::text::Row) -> Self {
|
||||||
Self::from_slice(&row.x_offsets) + Self::from_slice(&row.uv_rects)
|
Self::from_mesh(&row.visuals.mesh) + Self::from_slice(&row.glyphs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_mesh(mesh: &Mesh) -> Self {
|
pub fn from_mesh(mesh: &Mesh) -> Self {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use super::*;
|
||||||
/// Describes the width and color of a line.
|
/// Describes the width and color of a line.
|
||||||
///
|
///
|
||||||
/// The default stroke is the same as [`Stroke::none`].
|
/// The default stroke is the same as [`Stroke::none`].
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
|
@ -12,10 +12,12 @@ pub struct Stroke {
|
||||||
|
|
||||||
impl Stroke {
|
impl Stroke {
|
||||||
/// Same as [`Stroke::default`].
|
/// Same as [`Stroke::default`].
|
||||||
|
#[inline(always)]
|
||||||
pub fn none() -> Self {
|
pub fn none() -> Self {
|
||||||
Self::new(0.0, Color32::TRANSPARENT)
|
Self::new(0.0, Color32::TRANSPARENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
width: width.into(),
|
width: width.into(),
|
||||||
|
@ -28,7 +30,26 @@ impl<Color> From<(f32, Color)> for Stroke
|
||||||
where
|
where
|
||||||
Color: Into<Color32>,
|
Color: Into<Color32>,
|
||||||
{
|
{
|
||||||
|
#[inline(always)]
|
||||||
fn from((width, color): (f32, Color)) -> Stroke {
|
fn from((width, color): (f32, Color)) -> Stroke {
|
||||||
Stroke::new(width, color)
|
Stroke::new(width, color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for Stroke {
|
||||||
|
#[inline(always)]
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
let Self { width, color } = *self;
|
||||||
|
crate::f32_hash(state, width);
|
||||||
|
color.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Stroke {
|
||||||
|
#[inline(always)]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.color == other.color && crate::f32_eq(self.width, other.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::Eq for Stroke {}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use std::f32::consts::TAU;
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct PathPoint {
|
struct PathPoint {
|
||||||
pos: Pos2,
|
pos: Pos2,
|
||||||
|
|
||||||
/// For filled paths the normal is used for anti-aliasing (both strokes and filled areas).
|
/// For filled paths the normal is used for anti-aliasing (both strokes and filled areas).
|
||||||
|
@ -31,7 +31,7 @@ pub struct PathPoint {
|
||||||
/// to either to a stroke (with thickness) or a filled convex area.
|
/// to either to a stroke (with thickness) or a filled convex area.
|
||||||
/// Used as a scratch-pad during tessellation.
|
/// Used as a scratch-pad during tessellation.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct Path(Vec<PathPoint>);
|
pub struct Path(Vec<PathPoint>);
|
||||||
|
|
||||||
impl Path {
|
impl Path {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
@ -150,6 +150,31 @@ impl Path {
|
||||||
n0 = n1;
|
n0 = n1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open-ended.
|
||||||
|
pub fn stroke_open(&self, stroke: Stroke, options: TessellationOptions, out: &mut Mesh) {
|
||||||
|
stroke_path(&self.0, PathType::Open, stroke, options, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A closed path (returning to the first point).
|
||||||
|
pub fn stroke_closed(&self, stroke: Stroke, options: TessellationOptions, out: &mut Mesh) {
|
||||||
|
stroke_path(&self.0, PathType::Closed, stroke, options, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stroke(
|
||||||
|
&self,
|
||||||
|
path_type: PathType,
|
||||||
|
stroke: Stroke,
|
||||||
|
options: TessellationOptions,
|
||||||
|
out: &mut Mesh,
|
||||||
|
) {
|
||||||
|
stroke_path(&self.0, path_type, stroke, options, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path is taken to be closed (i.e. returning to the start again).
|
||||||
|
pub fn fill(&self, color: Color32, options: TessellationOptions, out: &mut Mesh) {
|
||||||
|
fill_closed_path(&self.0, color, options, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod path {
|
pub mod path {
|
||||||
|
@ -226,7 +251,6 @@ pub enum PathType {
|
||||||
Open,
|
Open,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
use self::PathType::{Closed, Open};
|
|
||||||
|
|
||||||
/// Tessellation quality options
|
/// Tessellation quality options
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
@ -265,6 +289,16 @@ impl Default for TessellationOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TessellationOptions {
|
||||||
|
pub fn from_pixels_per_point(pixels_per_point: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
pixels_per_point,
|
||||||
|
aa_size: 1.0 / pixels_per_point,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TessellationOptions {
|
impl TessellationOptions {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||||
|
@ -420,7 +454,11 @@ fn stroke_path(
|
||||||
out.reserve_triangles(2 * n as usize);
|
out.reserve_triangles(2 * n as usize);
|
||||||
out.reserve_vertices(2 * n as usize);
|
out.reserve_vertices(2 * n as usize);
|
||||||
|
|
||||||
let last_index = if path_type == Closed { n } else { n - 1 };
|
let last_index = if path_type == PathType::Closed {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
n - 1
|
||||||
|
};
|
||||||
for i in 0..last_index {
|
for i in 0..last_index {
|
||||||
out.add_triangle(
|
out.add_triangle(
|
||||||
idx + (2 * i + 0) % (2 * n),
|
idx + (2 * i + 0) % (2 * n),
|
||||||
|
@ -519,11 +557,10 @@ impl Tessellator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = &mut self.scratchpad_path;
|
self.scratchpad_path.clear();
|
||||||
path.clear();
|
self.scratchpad_path.add_circle(center, radius);
|
||||||
path.add_circle(center, radius);
|
self.scratchpad_path.fill(fill, options, out);
|
||||||
fill_closed_path(&path.0, fill, options, out);
|
self.scratchpad_path.stroke_closed(stroke, options, out);
|
||||||
stroke_path(&path.0, Closed, stroke, options, out);
|
|
||||||
}
|
}
|
||||||
Shape::Mesh(mesh) => {
|
Shape::Mesh(mesh) => {
|
||||||
if mesh.is_valid() {
|
if mesh.is_valid() {
|
||||||
|
@ -533,10 +570,9 @@ impl Tessellator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shape::LineSegment { points, stroke } => {
|
Shape::LineSegment { points, stroke } => {
|
||||||
let path = &mut self.scratchpad_path;
|
self.scratchpad_path.clear();
|
||||||
path.clear();
|
self.scratchpad_path.add_line_segment(points);
|
||||||
path.add_line_segment(points);
|
self.scratchpad_path.stroke_open(stroke, options, out);
|
||||||
stroke_path(&path.0, Open, stroke, options, out);
|
|
||||||
}
|
}
|
||||||
Shape::Path {
|
Shape::Path {
|
||||||
points,
|
points,
|
||||||
|
@ -545,12 +581,11 @@ impl Tessellator {
|
||||||
stroke,
|
stroke,
|
||||||
} => {
|
} => {
|
||||||
if points.len() >= 2 {
|
if points.len() >= 2 {
|
||||||
let path = &mut self.scratchpad_path;
|
self.scratchpad_path.clear();
|
||||||
path.clear();
|
|
||||||
if closed {
|
if closed {
|
||||||
path.add_line_loop(&points);
|
self.scratchpad_path.add_line_loop(&points);
|
||||||
} else {
|
} else {
|
||||||
path.add_open_points(&points);
|
self.scratchpad_path.add_open_points(&points);
|
||||||
}
|
}
|
||||||
|
|
||||||
if fill != Color32::TRANSPARENT {
|
if fill != Color32::TRANSPARENT {
|
||||||
|
@ -558,10 +593,14 @@ impl Tessellator {
|
||||||
closed,
|
closed,
|
||||||
"You asked to fill a path that is not closed. That makes no sense."
|
"You asked to fill a path that is not closed. That makes no sense."
|
||||||
);
|
);
|
||||||
fill_closed_path(&path.0, fill, options, out);
|
self.scratchpad_path.fill(fill, options, out);
|
||||||
}
|
}
|
||||||
let typ = if closed { Closed } else { Open };
|
let typ = if closed {
|
||||||
stroke_path(&path.0, typ, stroke, options, out);
|
PathType::Closed
|
||||||
|
} else {
|
||||||
|
PathType::Open
|
||||||
|
};
|
||||||
|
self.scratchpad_path.stroke(typ, stroke, options, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shape::Rect {
|
Shape::Rect {
|
||||||
|
@ -581,8 +620,8 @@ impl Tessellator {
|
||||||
Shape::Text {
|
Shape::Text {
|
||||||
pos,
|
pos,
|
||||||
galley,
|
galley,
|
||||||
color,
|
underline,
|
||||||
fake_italics,
|
override_text_color,
|
||||||
} => {
|
} => {
|
||||||
if options.debug_paint_text_rects {
|
if options.debug_paint_text_rects {
|
||||||
self.tessellate_rect(
|
self.tessellate_rect(
|
||||||
|
@ -590,12 +629,12 @@ impl Tessellator {
|
||||||
rect: Rect::from_min_size(pos, galley.size).expand(0.5),
|
rect: Rect::from_min_size(pos, galley.size).expand(0.5),
|
||||||
corner_radius: 2.0,
|
corner_radius: 2.0,
|
||||||
fill: Default::default(),
|
fill: Default::default(),
|
||||||
stroke: (0.5, color).into(),
|
stroke: (0.5, Color32::GREEN).into(),
|
||||||
},
|
},
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.tessellate_text(tex_size, pos, &galley, color, fake_italics, out);
|
self.tessellate_text(tex_size, pos, &galley, underline, override_text_color, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -626,107 +665,87 @@ impl Tessellator {
|
||||||
path.clear();
|
path.clear();
|
||||||
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
|
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
|
||||||
path.add_line_loop(&self.scratchpad_points);
|
path.add_line_loop(&self.scratchpad_points);
|
||||||
fill_closed_path(&path.0, fill, self.options, out);
|
path.fill(fill, self.options, out);
|
||||||
stroke_path(&path.0, Closed, stroke, self.options, out);
|
path.stroke_closed(stroke, self.options, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tessellate_text(
|
pub fn tessellate_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
tex_size: [usize; 2],
|
tex_size: [usize; 2],
|
||||||
pos: Pos2,
|
galley_pos: Pos2,
|
||||||
galley: &super::Galley,
|
galley: &super::Galley,
|
||||||
color: Color32,
|
underline: Stroke,
|
||||||
fake_italics: bool,
|
override_text_color: Option<Color32>,
|
||||||
out: &mut Mesh,
|
out: &mut Mesh,
|
||||||
) {
|
) {
|
||||||
if color == Color32::TRANSPARENT || galley.is_empty() {
|
if galley.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if cfg!(any(
|
|
||||||
feature = "extra_asserts",
|
out.vertices.reserve(galley.num_vertices);
|
||||||
all(feature = "extra_debug_asserts", debug_assertions),
|
out.indices.reserve(galley.num_indices);
|
||||||
)) {
|
|
||||||
galley.sanity_check();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The contents of the galley is already snapped to pixel coordinates,
|
// The contents of the galley is already snapped to pixel coordinates,
|
||||||
// but we need to make sure the galley ends up on the start of a physical pixel:
|
// but we need to make sure the galley ends up on the start of a physical pixel:
|
||||||
let pos = pos2(
|
let galley_pos = pos2(
|
||||||
self.options.round_to_pixel(pos.x),
|
self.options.round_to_pixel(galley_pos.x),
|
||||||
self.options.round_to_pixel(pos.y),
|
self.options.round_to_pixel(galley_pos.y),
|
||||||
);
|
);
|
||||||
|
|
||||||
let num_chars = galley.char_count_excluding_newlines();
|
let uv_normalizer = vec2(1.0 / tex_size[0] as f32, 1.0 / tex_size[1] as f32);
|
||||||
out.reserve_triangles(num_chars * 2);
|
|
||||||
out.reserve_vertices(num_chars * 4);
|
|
||||||
|
|
||||||
let inv_tex_w = 1.0 / tex_size[0] as f32;
|
|
||||||
let inv_tex_h = 1.0 / tex_size[1] as f32;
|
|
||||||
|
|
||||||
let clip_slack = 2.0; // Some fudge to handle letters that are slightly larger than expected.
|
|
||||||
let clip_rect_min_y = self.clip_rect.min.y - clip_slack;
|
|
||||||
let clip_rect_max_y = self.clip_rect.max.y + clip_slack;
|
|
||||||
|
|
||||||
for row in &galley.rows {
|
for row in &galley.rows {
|
||||||
let row_min_y = pos.y + row.y_min;
|
if row.visuals.mesh.is_empty() {
|
||||||
let row_max_y = pos.y + row.y_max;
|
continue;
|
||||||
let is_line_visible = clip_rect_min_y <= row_max_y && row_min_y <= clip_rect_max_y;
|
}
|
||||||
|
|
||||||
if self.options.coarse_tessellation_culling && !is_line_visible {
|
let row_rect = row.visuals.mesh_bounds.translate(galley_pos.to_vec2());
|
||||||
|
|
||||||
|
if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) {
|
||||||
// culling individual lines of text is important, since a single `Shape::Text`
|
// culling individual lines of text is important, since a single `Shape::Text`
|
||||||
// can span hundreds of lines.
|
// can span hundreds of lines.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (x_offset, uv_rect) in row.x_offsets.iter().zip(&row.uv_rects) {
|
let index_offset = out.vertices.len() as u32;
|
||||||
if let Some(glyph) = uv_rect {
|
|
||||||
let mut left_top = pos + glyph.offset + vec2(*x_offset, row.y_min);
|
|
||||||
left_top.x = self.options.round_to_pixel(left_top.x); // Pixel-perfection.
|
|
||||||
left_top.y = self.options.round_to_pixel(left_top.y); // Pixel-perfection.
|
|
||||||
|
|
||||||
let rect = Rect::from_min_max(left_top, left_top + glyph.size);
|
out.indices.extend(
|
||||||
let uv = Rect::from_min_max(
|
row.visuals
|
||||||
pos2(
|
.mesh
|
||||||
glyph.min.0 as f32 * inv_tex_w,
|
.indices
|
||||||
glyph.min.1 as f32 * inv_tex_h,
|
.iter()
|
||||||
),
|
.map(|index| index + index_offset),
|
||||||
pos2(
|
);
|
||||||
glyph.max.0 as f32 * inv_tex_w,
|
|
||||||
glyph.max.1 as f32 * inv_tex_h,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if fake_italics {
|
out.vertices.extend(
|
||||||
let idx = out.vertices.len() as u32;
|
row.visuals
|
||||||
out.add_triangle(idx, idx + 1, idx + 2);
|
.mesh
|
||||||
out.add_triangle(idx + 2, idx + 1, idx + 3);
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, vertex)| {
|
||||||
|
let mut color = vertex.color;
|
||||||
|
|
||||||
let top_offset = rect.height() * 0.25 * Vec2::X;
|
if let Some(override_text_color) = override_text_color {
|
||||||
|
if row.visuals.glyph_vertex_range.contains(&i) {
|
||||||
|
color = override_text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out.vertices.push(Vertex {
|
Vertex {
|
||||||
pos: rect.left_top() + top_offset,
|
pos: galley_pos + vertex.pos.to_vec2(),
|
||||||
uv: uv.left_top(),
|
uv: (vertex.uv.to_vec2() * uv_normalizer).to_pos2(),
|
||||||
color,
|
color,
|
||||||
});
|
}
|
||||||
out.vertices.push(Vertex {
|
}),
|
||||||
pos: rect.right_top() + top_offset,
|
);
|
||||||
uv: uv.right_top(),
|
|
||||||
color,
|
if underline != Stroke::none() {
|
||||||
});
|
self.scratchpad_path.clear();
|
||||||
out.vertices.push(Vertex {
|
self.scratchpad_path
|
||||||
pos: rect.left_bottom(),
|
.add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]);
|
||||||
uv: uv.left_bottom(),
|
self.scratchpad_path
|
||||||
color,
|
.stroke_open(underline, self.options, out);
|
||||||
});
|
|
||||||
out.vertices.push(Vertex {
|
|
||||||
pos: rect.right_bottom(),
|
|
||||||
uv: uv.right_bottom(),
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
out.add_rect_with_uv(rect, uv, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
mutex::{Mutex, RwLock},
|
mutex::{Mutex, RwLock},
|
||||||
text::{
|
text::TextStyle,
|
||||||
galley::{Galley, Row},
|
|
||||||
TextStyle,
|
|
||||||
},
|
|
||||||
TextureAtlas,
|
TextureAtlas,
|
||||||
};
|
};
|
||||||
use ahash::AHashMap;
|
use ahash::AHashMap;
|
||||||
|
@ -13,28 +10,37 @@ use std::sync::Arc;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
pub struct UvRect {
|
pub struct UvRect {
|
||||||
/// X/Y offset for nice rendering (unit: points).
|
/// X/Y offset for nice rendering (unit: points).
|
||||||
pub offset: Vec2,
|
pub offset: Vec2,
|
||||||
|
|
||||||
|
/// Screen size (in points) of this glyph.
|
||||||
|
/// Note that the height is different from the font height.
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
|
||||||
/// Top left corner UV in texture.
|
/// Top left corner UV in texture.
|
||||||
pub min: (u16, u16),
|
pub min: [u16; 2],
|
||||||
|
|
||||||
/// Bottom right corner (exclusive).
|
/// Bottom right corner (exclusive).
|
||||||
pub max: (u16, u16),
|
pub max: [u16; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UvRect {
|
||||||
|
pub fn is_nothing(&self) -> bool {
|
||||||
|
self.min == self.max
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct GlyphInfo {
|
pub struct GlyphInfo {
|
||||||
id: ab_glyph::GlyphId,
|
pub(crate) id: ab_glyph::GlyphId,
|
||||||
|
|
||||||
/// Unit: points.
|
/// Unit: points.
|
||||||
pub advance_width: f32,
|
pub advance_width: f32,
|
||||||
|
|
||||||
/// Texture coordinates. None for space.
|
/// Texture coordinates. None for space.
|
||||||
pub uv_rect: Option<UvRect>,
|
pub uv_rect: UvRect,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GlyphInfo {
|
impl Default for GlyphInfo {
|
||||||
|
@ -42,7 +48,7 @@ impl Default for GlyphInfo {
|
||||||
Self {
|
Self {
|
||||||
id: ab_glyph::GlyphId(0),
|
id: ab_glyph::GlyphId(0),
|
||||||
advance_width: 0.0,
|
advance_width: 0.0,
|
||||||
uv_rect: None,
|
uv_rect: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,6 +167,7 @@ impl FontImpl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn pair_kerning(
|
pub fn pair_kerning(
|
||||||
&self,
|
&self,
|
||||||
last_glyph_id: ab_glyph::GlyphId,
|
last_glyph_id: ab_glyph::GlyphId,
|
||||||
|
@ -281,11 +288,12 @@ impl Font {
|
||||||
self.row_height
|
self.row_height
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uv_rect(&self, c: char) -> Option<UvRect> {
|
pub fn uv_rect(&self, c: char) -> UvRect {
|
||||||
self.glyph_info_cache
|
self.glyph_info_cache
|
||||||
.read()
|
.read()
|
||||||
.get(&c)
|
.get(&c)
|
||||||
.and_then(|gi| gi.1.uv_rect)
|
.map(|gi| gi.1.uv_rect)
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width of this character in points.
|
/// Width of this character in points.
|
||||||
|
@ -309,6 +317,13 @@ impl Font {
|
||||||
font_index_glyph_info
|
font_index_glyph_info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn glyph_info_and_font_impl(&self, c: char) -> (&FontImpl, GlyphInfo) {
|
||||||
|
let (font_index, glyph_info) = self.glyph_info(c);
|
||||||
|
let font_impl = &self.fonts[font_index];
|
||||||
|
(font_impl, glyph_info)
|
||||||
|
}
|
||||||
|
|
||||||
fn glyph_info_no_cache_or_fallback(&self, c: char) -> Option<(FontIndex, GlyphInfo)> {
|
fn glyph_info_no_cache_or_fallback(&self, c: char) -> Option<(FontIndex, GlyphInfo)> {
|
||||||
for (font_index, font_impl) in self.fonts.iter().enumerate() {
|
for (font_index, font_impl) in self.fonts.iter().enumerate() {
|
||||||
if let Some(glyph_info) = font_impl.glyph_info(c) {
|
if let Some(glyph_info) = font_impl.glyph_info(c) {
|
||||||
|
@ -320,325 +335,6 @@ impl Font {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typeset the given text onto one row.
|
|
||||||
/// Assumes there are no `\n` in the text.
|
|
||||||
/// Return `x_offsets`, one longer than the number of characters in the text.
|
|
||||||
fn layout_single_row_fragment(&self, text: &str) -> Vec<f32> {
|
|
||||||
let mut x_offsets = Vec::with_capacity(text.chars().count() + 1);
|
|
||||||
x_offsets.push(0.0);
|
|
||||||
|
|
||||||
let mut cursor_x_in_points = 0.0f32;
|
|
||||||
let mut last_glyph_id = None;
|
|
||||||
|
|
||||||
for c in text.chars() {
|
|
||||||
if !self.fonts.is_empty() {
|
|
||||||
let (font_index, glyph_info) = self.glyph_info(c);
|
|
||||||
|
|
||||||
let font_impl = &self.fonts[font_index];
|
|
||||||
|
|
||||||
if let Some(last_glyph_id) = last_glyph_id {
|
|
||||||
cursor_x_in_points += font_impl.pair_kerning(last_glyph_id, glyph_info.id)
|
|
||||||
}
|
|
||||||
cursor_x_in_points += glyph_info.advance_width;
|
|
||||||
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
|
|
||||||
last_glyph_id = Some(glyph_info.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
x_offsets.push(cursor_x_in_points);
|
|
||||||
}
|
|
||||||
|
|
||||||
x_offsets
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Typeset the given text onto one row.
|
|
||||||
/// Any `\n` will show up as the replacement character.
|
|
||||||
/// Always returns exactly one `Row` in the `Galley`.
|
|
||||||
///
|
|
||||||
/// Most often you probably want `\n` to produce a new row,
|
|
||||||
/// and so [`Self::layout_no_wrap`] may be a better choice.
|
|
||||||
pub fn layout_single_line(&self, text: String) -> Galley {
|
|
||||||
let x_offsets = self.layout_single_row_fragment(&text);
|
|
||||||
let row = Row {
|
|
||||||
x_offsets,
|
|
||||||
uv_rects: vec![], // will be filled in later
|
|
||||||
y_min: 0.0,
|
|
||||||
y_max: self.row_height(),
|
|
||||||
ends_with_newline: false,
|
|
||||||
};
|
|
||||||
let width = row.max_x();
|
|
||||||
let size = vec2(width, self.row_height());
|
|
||||||
let galley = Galley {
|
|
||||||
text_style: self.text_style,
|
|
||||||
text,
|
|
||||||
rows: vec![row],
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
self.finalize_galley(galley)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will line break at `\n`.
|
|
||||||
///
|
|
||||||
/// Always returns at least one row.
|
|
||||||
pub fn layout_no_wrap(&self, text: String) -> Galley {
|
|
||||||
self.layout_multiline(text, f32::INFINITY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Will wrap text at the given width and line break at `\n`.
|
|
||||||
///
|
|
||||||
/// Always returns at least one row.
|
|
||||||
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 mut cursor_y = 0.0;
|
|
||||||
let mut rows = Vec::new();
|
|
||||||
|
|
||||||
let mut paragraph_start = 0;
|
|
||||||
|
|
||||||
while paragraph_start < text.len() {
|
|
||||||
let next_newline = text[paragraph_start..].find('\n');
|
|
||||||
let paragraph_end = next_newline
|
|
||||||
.map(|newline| paragraph_start + newline)
|
|
||||||
.unwrap_or_else(|| text.len());
|
|
||||||
|
|
||||||
assert!(paragraph_start <= paragraph_end);
|
|
||||||
let paragraph_text = &text[paragraph_start..paragraph_end];
|
|
||||||
let line_indentation = if rows.is_empty() {
|
|
||||||
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());
|
|
||||||
paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some();
|
|
||||||
|
|
||||||
for row in &mut paragraph_rows {
|
|
||||||
row.y_min += cursor_y;
|
|
||||||
row.y_max += cursor_y;
|
|
||||||
}
|
|
||||||
cursor_y = paragraph_rows.last().unwrap().y_max;
|
|
||||||
|
|
||||||
// cursor_y += row_height * 0.2; // Extra spacing between paragraphs.
|
|
||||||
|
|
||||||
rows.append(&mut paragraph_rows);
|
|
||||||
|
|
||||||
paragraph_start = paragraph_end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if text.is_empty() {
|
|
||||||
rows.push(Row {
|
|
||||||
x_offsets: vec![first_row_indentation],
|
|
||||||
uv_rects: vec![],
|
|
||||||
y_min: cursor_y,
|
|
||||||
y_max: cursor_y + row_height,
|
|
||||||
ends_with_newline: false,
|
|
||||||
});
|
|
||||||
} else if text.ends_with('\n') {
|
|
||||||
rows.push(Row {
|
|
||||||
x_offsets: vec![0.0],
|
|
||||||
uv_rects: vec![],
|
|
||||||
y_min: cursor_y,
|
|
||||||
y_max: cursor_y + row_height,
|
|
||||||
ends_with_newline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut widest_row = 0.0;
|
|
||||||
for row in &rows {
|
|
||||||
widest_row = row.max_x().max(widest_row);
|
|
||||||
}
|
|
||||||
let size = vec2(widest_row, rows.last().unwrap().y_max);
|
|
||||||
|
|
||||||
let text_style = self.text_style;
|
|
||||||
let galley = Galley {
|
|
||||||
text_style,
|
|
||||||
text,
|
|
||||||
rows,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
self.finalize_galley(galley)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A paragraph is text with no line break character in it.
|
|
||||||
/// The text will be wrapped by the given `max_width_in_points`.
|
|
||||||
/// 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.is_empty() {
|
|
||||||
return vec![Row {
|
|
||||||
x_offsets: vec![first_row_indentation],
|
|
||||||
uv_rects: vec![],
|
|
||||||
y_min: 0.0,
|
|
||||||
y_max: self.row_height(),
|
|
||||||
ends_with_newline: false,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_x_offsets = self.layout_single_row_fragment(text);
|
|
||||||
|
|
||||||
let mut row_start_x = 0.0; // NOTE: BEFORE the `first_row_indentation`.
|
|
||||||
|
|
||||||
let mut cursor_y = 0.0;
|
|
||||||
let mut row_start_idx = 0;
|
|
||||||
|
|
||||||
// Keeps track of good places to insert row break if we exceed `max_width_in_points`.
|
|
||||||
let mut row_break_candidates = RowBreakCandidates::default();
|
|
||||||
|
|
||||||
let mut out_rows = vec![];
|
|
||||||
|
|
||||||
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
|
|
||||||
crate::epaint_assert!(chr != '\n');
|
|
||||||
let potential_row_width = first_row_indentation + x - row_start_x;
|
|
||||||
|
|
||||||
if potential_row_width > max_width_in_points {
|
|
||||||
let is_first_row = out_rows.is_empty();
|
|
||||||
if is_first_row
|
|
||||||
&& first_row_indentation > 0.0
|
|
||||||
&& !row_break_candidates.has_word_boundary()
|
|
||||||
{
|
|
||||||
// Allow the first row to be completely empty, because we know there will be more space on the next row:
|
|
||||||
assert_eq!(row_start_idx, 0);
|
|
||||||
out_rows.push(Row {
|
|
||||||
x_offsets: vec![first_row_indentation],
|
|
||||||
uv_rects: vec![],
|
|
||||||
y_min: cursor_y,
|
|
||||||
y_max: cursor_y + self.row_height(),
|
|
||||||
ends_with_newline: false,
|
|
||||||
});
|
|
||||||
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
|
|
||||||
} else if let Some(last_kept_index) = row_break_candidates.get() {
|
|
||||||
out_rows.push(Row {
|
|
||||||
x_offsets: full_x_offsets[row_start_idx..=last_kept_index + 1]
|
|
||||||
.iter()
|
|
||||||
.map(|x| first_row_indentation + x - row_start_x)
|
|
||||||
.collect(),
|
|
||||||
uv_rects: vec![], // Will be filled in later!
|
|
||||||
y_min: cursor_y,
|
|
||||||
y_max: cursor_y + self.row_height(),
|
|
||||||
ends_with_newline: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
row_start_idx = last_kept_index + 1;
|
|
||||||
row_start_x = first_row_indentation + full_x_offsets[row_start_idx];
|
|
||||||
row_break_candidates = Default::default();
|
|
||||||
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row_break_candidates.add(i, chr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if row_start_idx + 1 < full_x_offsets.len() {
|
|
||||||
out_rows.push(Row {
|
|
||||||
x_offsets: full_x_offsets[row_start_idx..]
|
|
||||||
.iter()
|
|
||||||
.map(|x| first_row_indentation + x - row_start_x)
|
|
||||||
.collect(),
|
|
||||||
uv_rects: vec![], // Will be filled in later!
|
|
||||||
y_min: cursor_y,
|
|
||||||
y_max: cursor_y + self.row_height(),
|
|
||||||
ends_with_newline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
out_rows
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finalize_galley(&self, mut galley: Galley) -> Galley {
|
|
||||||
let mut chars = galley.text.chars();
|
|
||||||
for row in &mut galley.rows {
|
|
||||||
row.uv_rects.clear();
|
|
||||||
row.uv_rects.reserve(row.char_count_excluding_newline());
|
|
||||||
for _ in 0..row.char_count_excluding_newline() {
|
|
||||||
let c = chars.next().unwrap();
|
|
||||||
row.uv_rects.push(self.uv_rect(c));
|
|
||||||
}
|
|
||||||
if row.ends_with_newline {
|
|
||||||
let newline = chars.next().unwrap();
|
|
||||||
assert_eq!(newline, '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(chars.next(), None);
|
|
||||||
galley.sanity_check();
|
|
||||||
galley
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Keeps track of good places to break a long row of text.
|
|
||||||
/// Will focus primarily on spaces, secondarily on things like `-`
|
|
||||||
#[derive(Clone, Copy, Default)]
|
|
||||||
struct RowBreakCandidates {
|
|
||||||
/// Breaking at ` ` or other whitespace
|
|
||||||
/// is always the primary candidate.
|
|
||||||
space: Option<usize>,
|
|
||||||
/// Logogram (single character representing a whole word) are good candidates for line break.
|
|
||||||
logogram: Option<usize>,
|
|
||||||
/// Breaking at a dash is super-
|
|
||||||
/// good idea.
|
|
||||||
dash: Option<usize>,
|
|
||||||
/// This is nicer for things like URLs, e.g. www.
|
|
||||||
/// example.com.
|
|
||||||
punctuation: Option<usize>,
|
|
||||||
/// Breaking after just random character is some
|
|
||||||
/// times necessary.
|
|
||||||
any: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RowBreakCandidates {
|
|
||||||
fn add(&mut self, index: usize, chr: char) {
|
|
||||||
const NON_BREAKING_SPACE: char = '\u{A0}';
|
|
||||||
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
|
|
||||||
self.space = Some(index);
|
|
||||||
} else if is_chinese(chr) {
|
|
||||||
self.logogram = Some(index);
|
|
||||||
} else if chr == '-' {
|
|
||||||
self.dash = Some(index);
|
|
||||||
} else if chr.is_ascii_punctuation() {
|
|
||||||
self.punctuation = Some(index);
|
|
||||||
} else {
|
|
||||||
self.any = Some(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_word_boundary(&self) -> bool {
|
|
||||||
self.space.is_some() || self.logogram.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self) -> Option<usize> {
|
|
||||||
self.space
|
|
||||||
.or(self.logogram)
|
|
||||||
.or(self.dash)
|
|
||||||
.or(self.punctuation)
|
|
||||||
.or(self.any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn is_chinese(c: char) -> bool {
|
|
||||||
('\u{4E00}' <= c && c <= '\u{9FFF}')
|
|
||||||
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|
|
||||||
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -663,12 +359,12 @@ fn allocate_glyph(
|
||||||
let glyph =
|
let glyph =
|
||||||
glyph_id.with_scale_and_position(scale_in_pixels, ab_glyph::Point { x: 0.0, y: 0.0 });
|
glyph_id.with_scale_and_position(scale_in_pixels, ab_glyph::Point { x: 0.0, y: 0.0 });
|
||||||
|
|
||||||
let uv_rect = font.outline_glyph(glyph).and_then(|glyph| {
|
let uv_rect = font.outline_glyph(glyph).map(|glyph| {
|
||||||
let bb = glyph.px_bounds();
|
let bb = glyph.px_bounds();
|
||||||
let glyph_width = bb.width() as usize;
|
let glyph_width = bb.width() as usize;
|
||||||
let glyph_height = bb.height() as usize;
|
let glyph_height = bb.height() as usize;
|
||||||
if glyph_width == 0 || glyph_height == 0 {
|
if glyph_width == 0 || glyph_height == 0 {
|
||||||
None
|
UvRect::default()
|
||||||
} else {
|
} else {
|
||||||
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
|
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
|
||||||
|
|
||||||
|
@ -683,17 +379,18 @@ fn allocate_glyph(
|
||||||
|
|
||||||
let offset_in_pixels = vec2(bb.min.x as f32, scale_in_pixels as f32 + bb.min.y as f32);
|
let offset_in_pixels = vec2(bb.min.x as f32, scale_in_pixels as f32 + bb.min.y as f32);
|
||||||
let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y;
|
let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y;
|
||||||
Some(UvRect {
|
UvRect {
|
||||||
offset,
|
offset,
|
||||||
size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point,
|
size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point,
|
||||||
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
|
min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
|
||||||
max: (
|
max: [
|
||||||
(glyph_pos.0 + glyph_width) as u16,
|
(glyph_pos.0 + glyph_width) as u16,
|
||||||
(glyph_pos.1 + glyph_height) as u16,
|
(glyph_pos.1 + glyph_height) as u16,
|
||||||
),
|
],
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let uv_rect = uv_rect.unwrap_or_default();
|
||||||
|
|
||||||
let advance_width_in_points =
|
let advance_width_in_points =
|
||||||
font.as_scaled(scale_in_pixels).h_advance(glyph_id) / pixels_per_point;
|
font.as_scaled(scale_in_pixels).h_advance(glyph_id) / pixels_per_point;
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
text::{
|
text::{
|
||||||
font::{Font, FontImpl},
|
font::{Font, FontImpl},
|
||||||
Galley,
|
Galley, LayoutJob,
|
||||||
},
|
},
|
||||||
Texture, TextureAtlas,
|
Texture, TextureAtlas,
|
||||||
};
|
};
|
||||||
|
@ -315,69 +315,58 @@ impl Fonts {
|
||||||
self.fonts[&text_style].row_height()
|
self.fonts[&text_style].row_height()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will line break at `\n`.
|
/// Layout some text.
|
||||||
|
/// This is the most advanced layout function.
|
||||||
|
/// See also [`Self::layout`], [`Self::layout_no_wrap`] and
|
||||||
|
/// [`Self::layout_delayed_color`].
|
||||||
///
|
///
|
||||||
/// Always returns at least one row.
|
/// The implementation uses memoization so repeated calls are cheap.
|
||||||
pub fn layout_no_wrap(&self, text_style: TextStyle, text: String) -> Arc<Galley> {
|
pub fn layout_job(&self, job: impl Into<Arc<LayoutJob>>) -> Arc<Galley> {
|
||||||
self.layout_multiline(text_style, text, f32::INFINITY)
|
self.galley_cache.lock().layout(self, job.into())
|
||||||
}
|
|
||||||
|
|
||||||
/// Typeset the given text onto one row.
|
|
||||||
/// Any `\n` will show up as the replacement character.
|
|
||||||
/// Always returns exactly one `Row` in the `Galley`.
|
|
||||||
///
|
|
||||||
/// Most often you probably want `\n` to produce a new row,
|
|
||||||
/// and so [`Self::layout_no_wrap`] may be a better choice.
|
|
||||||
pub fn layout_single_line(&self, text_style: TextStyle, text: String) -> Arc<Galley> {
|
|
||||||
self.galley_cache.lock().layout(
|
|
||||||
&self.fonts,
|
|
||||||
LayoutJob {
|
|
||||||
text_style,
|
|
||||||
text,
|
|
||||||
layout_params: LayoutParams::SingleLine,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will wrap text at the given width and line break at `\n`.
|
/// Will wrap text at the given width and line break at `\n`.
|
||||||
///
|
///
|
||||||
/// Always returns at least one row.
|
/// The implementation uses memoization so repeated calls are cheap.
|
||||||
pub fn layout_multiline(
|
pub fn layout(
|
||||||
&self,
|
&self,
|
||||||
text_style: TextStyle,
|
|
||||||
text: String,
|
text: String,
|
||||||
max_width_in_points: f32,
|
text_style: TextStyle,
|
||||||
|
color: crate::Color32,
|
||||||
|
wrap_width: f32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.layout_multiline_with_indentation_and_max_width(
|
let job = LayoutJob::simple(text, text_style, color, wrap_width);
|
||||||
text_style,
|
self.layout_job(job)
|
||||||
text,
|
|
||||||
0.0,
|
|
||||||
max_width_in_points,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// * `first_row_indentation`: extra space before the very first character (in points).
|
/// Will line break at `\n`.
|
||||||
/// * `max_width_in_points`: wrapping width.
|
|
||||||
///
|
///
|
||||||
/// Always returns at least one row.
|
/// The implementation uses memoization so repeated calls are cheap.
|
||||||
pub fn layout_multiline_with_indentation_and_max_width(
|
pub fn layout_no_wrap(
|
||||||
&self,
|
&self,
|
||||||
text_style: TextStyle,
|
|
||||||
text: String,
|
text: String,
|
||||||
first_row_indentation: f32,
|
text_style: TextStyle,
|
||||||
max_width_in_points: f32,
|
color: crate::Color32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.galley_cache.lock().layout(
|
let job = LayoutJob::simple(text, text_style, color, f32::INFINITY);
|
||||||
&self.fonts,
|
self.layout_job(job)
|
||||||
LayoutJob {
|
}
|
||||||
text_style,
|
|
||||||
text,
|
/// Like [`Self::layout`], made for when you want to pick a color for the text later.
|
||||||
layout_params: LayoutParams::Multiline {
|
///
|
||||||
first_row_indentation: first_row_indentation.into(),
|
/// The implementation uses memoization so repeated calls are cheap.
|
||||||
max_width_in_points: max_width_in_points.into(),
|
pub fn layout_delayed_color(
|
||||||
},
|
&self,
|
||||||
},
|
text: String,
|
||||||
)
|
text_style: TextStyle,
|
||||||
|
wrap_width: f32,
|
||||||
|
) -> Arc<Galley> {
|
||||||
|
self.layout_job(LayoutJob::simple(
|
||||||
|
text,
|
||||||
|
text_style,
|
||||||
|
crate::Color32::TEMPORARY_COLOR,
|
||||||
|
wrap_width,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn num_galleys_in_cache(&self) -> usize {
|
pub fn num_galleys_in_cache(&self) -> usize {
|
||||||
|
@ -386,7 +375,7 @@ impl Fonts {
|
||||||
|
|
||||||
/// Must be called once per frame to clear the [`Galley`] cache.
|
/// Must be called once per frame to clear the [`Galley`] cache.
|
||||||
pub fn end_frame(&self) {
|
pub fn end_frame(&self) {
|
||||||
self.galley_cache.lock().end_frame()
|
self.galley_cache.lock().end_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,22 +390,6 @@ impl std::ops::Index<TextStyle> for Fonts {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
|
||||||
enum LayoutParams {
|
|
||||||
SingleLine,
|
|
||||||
Multiline {
|
|
||||||
first_row_indentation: ordered_float::OrderedFloat<f32>,
|
|
||||||
max_width_in_points: ordered_float::OrderedFloat<f32>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
|
||||||
struct LayoutJob {
|
|
||||||
text_style: TextStyle,
|
|
||||||
layout_params: LayoutParams,
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CachedGalley {
|
struct CachedGalley {
|
||||||
/// When it was last used
|
/// When it was last used
|
||||||
last_used: u32,
|
last_used: u32,
|
||||||
|
@ -427,41 +400,26 @@ struct CachedGalley {
|
||||||
struct GalleyCache {
|
struct GalleyCache {
|
||||||
/// Frame counter used to do garbage collection on the cache
|
/// Frame counter used to do garbage collection on the cache
|
||||||
generation: u32,
|
generation: u32,
|
||||||
cache: AHashMap<LayoutJob, CachedGalley>,
|
cache: AHashMap<Arc<LayoutJob>, CachedGalley>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GalleyCache {
|
impl GalleyCache {
|
||||||
fn layout(&mut self, fonts: &BTreeMap<TextStyle, Font>, job: LayoutJob) -> Arc<Galley> {
|
fn layout(&mut self, fonts: &Fonts, job: Arc<LayoutJob>) -> Arc<Galley> {
|
||||||
if let Some(cached) = self.cache.get_mut(&job) {
|
match self.cache.entry(job.clone()) {
|
||||||
cached.last_used = self.generation;
|
std::collections::hash_map::Entry::Occupied(entry) => {
|
||||||
cached.galley.clone()
|
let cached = entry.into_mut();
|
||||||
} else {
|
cached.last_used = self.generation;
|
||||||
let LayoutJob {
|
cached.galley.clone()
|
||||||
text_style,
|
}
|
||||||
layout_params,
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||||
text,
|
let galley = super::layout(fonts, job);
|
||||||
} = job.clone();
|
let galley = Arc::new(galley);
|
||||||
let font = &fonts[&text_style];
|
entry.insert(CachedGalley {
|
||||||
let galley = match layout_params {
|
|
||||||
LayoutParams::SingleLine => font.layout_single_line(text),
|
|
||||||
LayoutParams::Multiline {
|
|
||||||
first_row_indentation,
|
|
||||||
max_width_in_points,
|
|
||||||
} => font.layout_multiline_with_indentation_and_max_width(
|
|
||||||
text,
|
|
||||||
first_row_indentation.into_inner(),
|
|
||||||
max_width_in_points.into_inner(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let galley = Arc::new(galley);
|
|
||||||
self.cache.insert(
|
|
||||||
job,
|
|
||||||
CachedGalley {
|
|
||||||
last_used: self.generation,
|
last_used: self.generation,
|
||||||
galley: galley.clone(),
|
galley: galley.clone(),
|
||||||
},
|
});
|
||||||
);
|
galley
|
||||||
galley
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,16 @@
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
mod font;
|
mod font;
|
||||||
mod fonts;
|
mod fonts;
|
||||||
mod galley;
|
mod text_layout;
|
||||||
|
mod text_layout_types;
|
||||||
|
|
||||||
/// One `\t` character is this many spaces wide.
|
/// One `\t` character is this many spaces wide.
|
||||||
pub const TAB_SIZE: usize = 4;
|
pub const TAB_SIZE: usize = 4;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
||||||
galley::{Galley, Row},
|
text_layout::layout,
|
||||||
|
text_layout_types::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Suggested character to use to replace those in password text fields.
|
/// Suggested character to use to replace those in password text fields.
|
||||||
|
|
538
epaint/src/text/text_layout.rs
Normal file
538
epaint/src/text/text_layout.rs
Normal file
|
@ -0,0 +1,538 @@
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::{Fonts, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals};
|
||||||
|
use crate::{Color32, Mesh, Stroke, Vertex};
|
||||||
|
use emath::*;
|
||||||
|
|
||||||
|
/// Temporary storage before line-wrapping.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Paragraph {
|
||||||
|
/// Start of the next glyph to be added.
|
||||||
|
pub cursor_x: f32,
|
||||||
|
pub glyphs: Vec<Glyph>,
|
||||||
|
/// In case of an empty paragraph ("\n"), use this as height.
|
||||||
|
pub empty_paragraph_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout text into a [`Galley`].
|
||||||
|
///
|
||||||
|
/// In most cases you should use [`Fonts::layout_job`] instead
|
||||||
|
/// since that memoizes the input, making subsequent layouting of the same text much faster.
|
||||||
|
pub fn layout(fonts: &Fonts, job: Arc<LayoutJob>) -> Galley {
|
||||||
|
let mut paragraphs = vec![Paragraph::default()];
|
||||||
|
for (section_index, section) in job.sections.iter().enumerate() {
|
||||||
|
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = rows_from_paragraphs(paragraphs, job.wrap_width);
|
||||||
|
|
||||||
|
galley_from_rows(fonts, job, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_section(
|
||||||
|
fonts: &Fonts,
|
||||||
|
job: &LayoutJob,
|
||||||
|
section_index: u32,
|
||||||
|
section: &LayoutSection,
|
||||||
|
out_paragraphs: &mut Vec<Paragraph>,
|
||||||
|
) {
|
||||||
|
let LayoutSection {
|
||||||
|
leading_space,
|
||||||
|
byte_range,
|
||||||
|
format,
|
||||||
|
} = section;
|
||||||
|
let font = &fonts[format.style];
|
||||||
|
let font_height = font.row_height();
|
||||||
|
|
||||||
|
let mut paragraph = out_paragraphs.last_mut().unwrap();
|
||||||
|
if paragraph.glyphs.is_empty() {
|
||||||
|
paragraph.empty_paragraph_height = font_height; // TODO: replace this hack with actually including `\n` in the glyphs?
|
||||||
|
}
|
||||||
|
|
||||||
|
paragraph.cursor_x += leading_space;
|
||||||
|
|
||||||
|
let mut last_glyph_id = None;
|
||||||
|
|
||||||
|
for chr in job.text[byte_range.clone()].chars() {
|
||||||
|
if job.break_on_newline && chr == '\n' {
|
||||||
|
out_paragraphs.push(Paragraph::default());
|
||||||
|
paragraph = out_paragraphs.last_mut().unwrap();
|
||||||
|
paragraph.empty_paragraph_height = font_height; // TODO: replace this hack with actually including `\n` in the glyphs?
|
||||||
|
} else {
|
||||||
|
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(chr);
|
||||||
|
if let Some(last_glyph_id) = last_glyph_id {
|
||||||
|
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
paragraph.glyphs.push(Glyph {
|
||||||
|
chr,
|
||||||
|
pos: pos2(paragraph.cursor_x, f32::NAN),
|
||||||
|
size: vec2(glyph_info.advance_width, font_height),
|
||||||
|
uv_rect: glyph_info.uv_rect,
|
||||||
|
section_index,
|
||||||
|
});
|
||||||
|
|
||||||
|
paragraph.cursor_x += glyph_info.advance_width;
|
||||||
|
paragraph.cursor_x = font.round_to_pixel(paragraph.cursor_x);
|
||||||
|
last_glyph_id = Some(glyph_info.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We ignore y at this stage
|
||||||
|
fn rect_from_x_range(x_range: RangeInclusive<f32>) -> Rect {
|
||||||
|
Rect::from_x_y_ranges(x_range, 0.0..=0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rows_from_paragraphs(paragraphs: Vec<Paragraph>, wrap_width: f32) -> Vec<Row> {
|
||||||
|
let num_paragraphs = paragraphs.len();
|
||||||
|
|
||||||
|
let mut rows = vec![];
|
||||||
|
|
||||||
|
for (i, paragraph) in paragraphs.into_iter().enumerate() {
|
||||||
|
let is_last_paragraph = (i + 1) == num_paragraphs;
|
||||||
|
|
||||||
|
if paragraph.glyphs.is_empty() {
|
||||||
|
rows.push(Row {
|
||||||
|
glyphs: vec![],
|
||||||
|
visuals: Default::default(),
|
||||||
|
rect: Rect::from_min_size(
|
||||||
|
pos2(paragraph.cursor_x, 0.0),
|
||||||
|
vec2(0.0, paragraph.empty_paragraph_height),
|
||||||
|
),
|
||||||
|
ends_with_newline: !is_last_paragraph,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x();
|
||||||
|
if paragraph_max_x <= wrap_width {
|
||||||
|
// early-out optimization
|
||||||
|
let paragraph_min_x = paragraph.glyphs[0].pos.x;
|
||||||
|
rows.push(Row {
|
||||||
|
glyphs: paragraph.glyphs,
|
||||||
|
visuals: Default::default(),
|
||||||
|
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||||
|
ends_with_newline: !is_last_paragraph,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
line_break(¶graph, wrap_width, &mut rows);
|
||||||
|
rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec<Row>) {
|
||||||
|
// Keeps track of good places to insert row break if we exceed `wrap_width`.
|
||||||
|
let mut row_break_candidates = RowBreakCandidates::default();
|
||||||
|
|
||||||
|
let mut first_row_indentation = paragraph.glyphs[0].pos.x;
|
||||||
|
let mut row_start_x = 0.0;
|
||||||
|
let mut row_start_idx = 0;
|
||||||
|
|
||||||
|
for (i, glyph) in paragraph.glyphs.iter().enumerate() {
|
||||||
|
let potential_row_width = glyph.pos.x - row_start_x;
|
||||||
|
|
||||||
|
if potential_row_width > wrap_width {
|
||||||
|
if first_row_indentation > 0.0 && !row_break_candidates.has_word_boundary() {
|
||||||
|
// 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.
|
||||||
|
out_rows.push(Row {
|
||||||
|
glyphs: vec![],
|
||||||
|
visuals: Default::default(),
|
||||||
|
rect: rect_from_x_range(first_row_indentation..=first_row_indentation),
|
||||||
|
ends_with_newline: false,
|
||||||
|
});
|
||||||
|
row_start_x += first_row_indentation;
|
||||||
|
first_row_indentation = 0.0;
|
||||||
|
} else if let Some(last_kept_index) = row_break_candidates.get() {
|
||||||
|
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|mut glyph| {
|
||||||
|
glyph.pos.x -= row_start_x;
|
||||||
|
glyph
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let paragraph_min_x = glyphs[0].pos.x;
|
||||||
|
let paragraph_max_x = glyphs.last().unwrap().max_x();
|
||||||
|
|
||||||
|
out_rows.push(Row {
|
||||||
|
glyphs,
|
||||||
|
visuals: Default::default(),
|
||||||
|
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||||
|
ends_with_newline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
row_start_idx = last_kept_index + 1;
|
||||||
|
row_start_x = paragraph.glyphs[row_start_idx].pos.x;
|
||||||
|
row_break_candidates = Default::default();
|
||||||
|
} else {
|
||||||
|
// Found no place to break, so we have to overrun wrap_width.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row_break_candidates.add(i, glyph.chr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if row_start_idx < paragraph.glyphs.len() {
|
||||||
|
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|mut glyph| {
|
||||||
|
glyph.pos.x -= row_start_x;
|
||||||
|
glyph
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let paragraph_min_x = glyphs[0].pos.x;
|
||||||
|
let paragraph_max_x = glyphs.last().unwrap().max_x();
|
||||||
|
|
||||||
|
out_rows.push(Row {
|
||||||
|
glyphs,
|
||||||
|
visuals: Default::default(),
|
||||||
|
rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x),
|
||||||
|
ends_with_newline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the Y positions and tessellate the text.
|
||||||
|
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 cursor_y = 0.0;
|
||||||
|
let mut max_x: f32 = 0.0;
|
||||||
|
for row in &mut rows {
|
||||||
|
let mut row_height = first_row_min_height.max(row.rect.height());
|
||||||
|
first_row_min_height = 0.0;
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
row_height = row_height.max(glyph.size.y);
|
||||||
|
}
|
||||||
|
row_height = fonts.round_to_pixel(row_height);
|
||||||
|
|
||||||
|
// Now positions each glyph:
|
||||||
|
for glyph in &mut row.glyphs {
|
||||||
|
let format = &job.sections[glyph.section_index as usize].format;
|
||||||
|
glyph.pos.y = cursor_y + format.valign.to_factor() * (row_height - glyph.size.y);
|
||||||
|
glyph.pos.y = fonts.round_to_pixel(glyph.pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.rect.min.y = cursor_y;
|
||||||
|
row.rect.max.y = cursor_y + row_height;
|
||||||
|
|
||||||
|
max_x = max_x.max(row.rect.right());
|
||||||
|
cursor_y += row_height;
|
||||||
|
cursor_y = fonts.round_to_pixel(cursor_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let format_summary = format_summary(&job);
|
||||||
|
|
||||||
|
let mut num_vertices = 0;
|
||||||
|
let mut num_indices = 0;
|
||||||
|
|
||||||
|
for row in &mut rows {
|
||||||
|
row.visuals = tessellate_row(fonts, &job, &format_summary, row);
|
||||||
|
num_vertices += row.visuals.mesh.vertices.len();
|
||||||
|
num_indices += row.visuals.mesh.indices.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = vec2(max_x, cursor_y);
|
||||||
|
|
||||||
|
Galley {
|
||||||
|
job,
|
||||||
|
rows,
|
||||||
|
size,
|
||||||
|
num_vertices,
|
||||||
|
num_indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FormatSummary {
|
||||||
|
any_background: bool,
|
||||||
|
any_underline: bool,
|
||||||
|
any_strikethrough: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_summary(job: &LayoutJob) -> FormatSummary {
|
||||||
|
let mut format_summary = FormatSummary::default();
|
||||||
|
for section in &job.sections {
|
||||||
|
format_summary.any_background |= section.format.background != Color32::TRANSPARENT;
|
||||||
|
format_summary.any_underline |= section.format.underline != Stroke::none();
|
||||||
|
format_summary.any_strikethrough |= section.format.strikethrough != Stroke::none();
|
||||||
|
}
|
||||||
|
format_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tessellate_row(
|
||||||
|
fonts: &Fonts,
|
||||||
|
job: &LayoutJob,
|
||||||
|
format_summary: &FormatSummary,
|
||||||
|
row: &mut Row,
|
||||||
|
) -> RowVisuals {
|
||||||
|
if row.glyphs.is_empty() {
|
||||||
|
return Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mesh = Mesh::default();
|
||||||
|
|
||||||
|
mesh.reserve_triangles(row.glyphs.len() * 2);
|
||||||
|
mesh.reserve_vertices(row.glyphs.len() * 4);
|
||||||
|
|
||||||
|
if format_summary.any_background {
|
||||||
|
add_row_backgrounds(job, row, &mut mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
let glyph_vertex_start = mesh.vertices.len();
|
||||||
|
tessellate_glyphs(fonts, job, row, &mut mesh);
|
||||||
|
let glyph_vertex_end = mesh.vertices.len();
|
||||||
|
|
||||||
|
if format_summary.any_underline {
|
||||||
|
add_row_hline(fonts, row, &mut mesh, |glyph| {
|
||||||
|
let format = &job.sections[glyph.section_index as usize].format;
|
||||||
|
let stroke = format.underline;
|
||||||
|
let y = glyph.logical_rect().bottom();
|
||||||
|
(stroke, y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if format_summary.any_strikethrough {
|
||||||
|
add_row_hline(fonts, row, &mut mesh, |glyph| {
|
||||||
|
let format = &job.sections[glyph.section_index as usize].format;
|
||||||
|
let stroke = format.strikethrough;
|
||||||
|
let y = glyph.logical_rect().center().y;
|
||||||
|
(stroke, y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mesh_bounds = mesh.calc_bounds();
|
||||||
|
|
||||||
|
RowVisuals {
|
||||||
|
mesh,
|
||||||
|
mesh_bounds,
|
||||||
|
glyph_vertex_range: glyph_vertex_start..glyph_vertex_end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create background for glyphs that have them.
|
||||||
|
/// Creates as few rectangular regions as possible.
|
||||||
|
fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
|
||||||
|
if row.glyphs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut end_run = |start: Option<(Color32, Rect)>, stop_x: f32| {
|
||||||
|
if let Some((color, start_rect)) = start {
|
||||||
|
let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom()));
|
||||||
|
let rect = rect.expand(1.0); // looks better
|
||||||
|
mesh.add_colored_rect(rect, color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut run_start = None;
|
||||||
|
let mut last_rect = Rect::NAN;
|
||||||
|
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let format = &job.sections[glyph.section_index as usize].format;
|
||||||
|
let color = format.background;
|
||||||
|
let rect = glyph.logical_rect();
|
||||||
|
|
||||||
|
if color == Color32::TRANSPARENT {
|
||||||
|
end_run(run_start.take(), last_rect.right());
|
||||||
|
} else if let Some((existing_color, start)) = run_start {
|
||||||
|
if existing_color == color
|
||||||
|
&& start.top() == rect.top()
|
||||||
|
&& start.bottom() == rect.bottom()
|
||||||
|
{
|
||||||
|
// continue the same background rectangle
|
||||||
|
} else {
|
||||||
|
end_run(run_start.take(), last_rect.right());
|
||||||
|
run_start = Some((color, rect));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
run_start = Some((color, rect));
|
||||||
|
}
|
||||||
|
|
||||||
|
last_rect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
end_run(run_start.take(), last_rect.right());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tessellate_glyphs(fonts: &Fonts, job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let uv_rect = glyph.uv_rect;
|
||||||
|
if !uv_rect.is_nothing() {
|
||||||
|
let mut left_top = glyph.pos + uv_rect.offset;
|
||||||
|
left_top.x = fonts.round_to_pixel(left_top.x);
|
||||||
|
left_top.y = fonts.round_to_pixel(left_top.y);
|
||||||
|
|
||||||
|
let rect = Rect::from_min_max(left_top, left_top + uv_rect.size);
|
||||||
|
let uv = Rect::from_min_max(
|
||||||
|
pos2(uv_rect.min[0] as f32, uv_rect.min[1] as f32),
|
||||||
|
pos2(uv_rect.max[0] as f32, uv_rect.max[1] as f32),
|
||||||
|
);
|
||||||
|
|
||||||
|
let format = &job.sections[glyph.section_index as usize].format;
|
||||||
|
|
||||||
|
let color = format.color;
|
||||||
|
|
||||||
|
if format.italics {
|
||||||
|
let idx = mesh.vertices.len() as u32;
|
||||||
|
mesh.add_triangle(idx, idx + 1, idx + 2);
|
||||||
|
mesh.add_triangle(idx + 2, idx + 1, idx + 3);
|
||||||
|
|
||||||
|
let top_offset = rect.height() * 0.25 * Vec2::X;
|
||||||
|
|
||||||
|
mesh.vertices.push(Vertex {
|
||||||
|
pos: rect.left_top() + top_offset,
|
||||||
|
uv: uv.left_top(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
mesh.vertices.push(Vertex {
|
||||||
|
pos: rect.right_top() + top_offset,
|
||||||
|
uv: uv.right_top(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
mesh.vertices.push(Vertex {
|
||||||
|
pos: rect.left_bottom(),
|
||||||
|
uv: uv.left_bottom(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
mesh.vertices.push(Vertex {
|
||||||
|
pos: rect.right_bottom(),
|
||||||
|
uv: uv.right_bottom(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mesh.add_rect_with_uv(rect, uv, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a horizontal line over a row of glyphs with a stroke and y decided by a callback.
|
||||||
|
fn add_row_hline(
|
||||||
|
fonts: &Fonts,
|
||||||
|
row: &Row,
|
||||||
|
mesh: &mut Mesh,
|
||||||
|
stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32),
|
||||||
|
) {
|
||||||
|
let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| {
|
||||||
|
if let Some((stroke, start)) = start {
|
||||||
|
add_hline(fonts, [start, pos2(stop_x, start.y)], stroke, mesh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut line_start = None;
|
||||||
|
let mut last_right_x = f32::NAN;
|
||||||
|
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let (stroke, y) = stroke_and_y(glyph);
|
||||||
|
|
||||||
|
if stroke == Stroke::none() {
|
||||||
|
end_line(line_start.take(), last_right_x);
|
||||||
|
} else if let Some((existing_stroke, start)) = line_start {
|
||||||
|
if existing_stroke == stroke && start.y == y {
|
||||||
|
// continue the same line
|
||||||
|
} else {
|
||||||
|
end_line(line_start.take(), last_right_x);
|
||||||
|
line_start = Some((stroke, pos2(glyph.pos.x, y)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line_start = Some((stroke, pos2(glyph.pos.x, y)));
|
||||||
|
}
|
||||||
|
|
||||||
|
last_right_x = glyph.max_x();
|
||||||
|
}
|
||||||
|
|
||||||
|
end_line(line_start.take(), last_right_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_hline(fonts: &Fonts, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut Mesh) {
|
||||||
|
let antialiased = true;
|
||||||
|
|
||||||
|
if antialiased {
|
||||||
|
let mut path = crate::tessellator::Path::default(); // TODO: reuse this to avoid re-allocations.
|
||||||
|
path.add_line_segment([start, stop]);
|
||||||
|
let options = crate::tessellator::TessellationOptions::from_pixels_per_point(
|
||||||
|
fonts.pixels_per_point(),
|
||||||
|
);
|
||||||
|
path.stroke_open(stroke, options, mesh);
|
||||||
|
} else {
|
||||||
|
// Thin lines often lost, so this is a bad idea
|
||||||
|
|
||||||
|
assert_eq!(start.y, stop.y);
|
||||||
|
|
||||||
|
let min_y = fonts.round_to_pixel(start.y - 0.5 * stroke.width);
|
||||||
|
let max_y = fonts.round_to_pixel(min_y + stroke.width);
|
||||||
|
|
||||||
|
let rect = Rect::from_min_max(
|
||||||
|
pos2(fonts.round_to_pixel(start.x), min_y),
|
||||||
|
pos2(fonts.round_to_pixel(stop.x), max_y),
|
||||||
|
);
|
||||||
|
|
||||||
|
mesh.add_colored_rect(rect, stroke.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Keeps track of good places to break a long row of text.
|
||||||
|
/// Will focus primarily on spaces, secondarily on things like `-`
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct RowBreakCandidates {
|
||||||
|
/// Breaking at ` ` or other whitespace
|
||||||
|
/// is always the primary candidate.
|
||||||
|
space: Option<usize>,
|
||||||
|
/// Logograms (single character representing a whole word) are good candidates for line break.
|
||||||
|
logogram: Option<usize>,
|
||||||
|
/// Breaking at a dash is a super-
|
||||||
|
/// good idea.
|
||||||
|
dash: Option<usize>,
|
||||||
|
/// This is nicer for things like URLs, e.g. www.
|
||||||
|
/// example.com.
|
||||||
|
punctuation: Option<usize>,
|
||||||
|
/// Breaking after just random character is some
|
||||||
|
/// times necessary.
|
||||||
|
any: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RowBreakCandidates {
|
||||||
|
fn add(&mut self, index: usize, chr: char) {
|
||||||
|
const NON_BREAKING_SPACE: char = '\u{A0}';
|
||||||
|
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
|
||||||
|
self.space = Some(index);
|
||||||
|
} else if is_chinese(chr) {
|
||||||
|
self.logogram = Some(index);
|
||||||
|
} else if chr == '-' {
|
||||||
|
self.dash = Some(index);
|
||||||
|
} else if chr.is_ascii_punctuation() {
|
||||||
|
self.punctuation = Some(index);
|
||||||
|
} else {
|
||||||
|
self.any = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_word_boundary(&self) -> bool {
|
||||||
|
self.space.is_some() || self.logogram.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self) -> Option<usize> {
|
||||||
|
self.space
|
||||||
|
.or(self.logogram)
|
||||||
|
.or(self.dash)
|
||||||
|
.or(self.punctuation)
|
||||||
|
.or(self.any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_chinese(c: char) -> bool {
|
||||||
|
('\u{4E00}' <= c && c <= '\u{9FFF}')
|
||||||
|
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|
||||||
|
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
|
||||||
|
}
|
|
@ -1,35 +1,224 @@
|
||||||
//! A [`Galley`] is a piece of text after layout, i.e. where each character has been assigned a position.
|
use std::ops::Range;
|
||||||
//!
|
use std::sync::Arc;
|
||||||
//! ## How it works
|
|
||||||
//! This is going to get complicated.
|
|
||||||
//!
|
|
||||||
//! To avoid confusion, we never use the word "line".
|
|
||||||
//! The `\n` character demarcates the split of text into "paragraphs".
|
|
||||||
//! Each paragraph is wrapped at some width onto one or more "rows".
|
|
||||||
//!
|
|
||||||
//! If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
|
||||||
//! do we prefer the next row?
|
|
||||||
//! For instance, consider this single paragraph, word wrapped:
|
|
||||||
//! ``` text
|
|
||||||
//! Hello_
|
|
||||||
//! world!
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! The offset `6` is both the end of the first row
|
|
||||||
//! and the start of the second row.
|
|
||||||
//! [`CCursor::prefer_next_row`] etc selects which.
|
|
||||||
|
|
||||||
use super::{cursor::*, font::UvRect};
|
use super::{cursor::*, font::UvRect};
|
||||||
use emath::{pos2, NumExt, Rect, Vec2};
|
use crate::{Color32, Mesh, Stroke, TextStyle};
|
||||||
|
use emath::*;
|
||||||
|
|
||||||
|
/// Describes the task of laying out text.
|
||||||
|
///
|
||||||
|
/// This supports mixing different fonts, color and formats (underline etc).
|
||||||
|
///
|
||||||
|
/// Pass this to [`Fonts::layout_job]` or [`crate::text::layout`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LayoutJob {
|
||||||
|
/// The complete text of this job, referenced by `LayoutSection`.
|
||||||
|
pub text: String, // TODO: Cow<'static, str>
|
||||||
|
|
||||||
|
/// The different section, which can have different fonts, colors, etc.
|
||||||
|
pub sections: Vec<LayoutSection>,
|
||||||
|
|
||||||
|
/// 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 wrap_width: f32,
|
||||||
|
|
||||||
|
/// The first row must be at least this high.
|
||||||
|
/// This is in case we lay out text that is the continuation
|
||||||
|
/// of some earlier text (sharing the same row),
|
||||||
|
/// in which case this will be the height of the earlier text.
|
||||||
|
/// In other cases, set this to `0.0`.
|
||||||
|
pub first_row_min_height: f32,
|
||||||
|
|
||||||
|
/// If `false`, all newlines characters will be ignored
|
||||||
|
/// and show up as the replacement character.
|
||||||
|
/// Default: `true`.
|
||||||
|
pub break_on_newline: bool,
|
||||||
|
// TODO: option to show whitespace characters
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LayoutJob {
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
text: Default::default(),
|
||||||
|
sections: Default::default(),
|
||||||
|
wrap_width: f32::INFINITY,
|
||||||
|
first_row_min_height: 0.0,
|
||||||
|
break_on_newline: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutJob {
|
||||||
|
/// Break on `\n` and at the given wrap width.
|
||||||
|
#[inline]
|
||||||
|
pub fn simple(text: String, text_style: TextStyle, color: Color32, wrap_width: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
sections: vec![LayoutSection {
|
||||||
|
leading_space: 0.0,
|
||||||
|
byte_range: 0..text.len(),
|
||||||
|
format: TextFormat::simple(text_style, color),
|
||||||
|
}],
|
||||||
|
text,
|
||||||
|
wrap_width,
|
||||||
|
break_on_newline: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does not break on `\n`, but shows the replacement character instead.
|
||||||
|
#[inline]
|
||||||
|
pub fn simple_singleline(text: String, text_style: TextStyle, color: Color32) -> Self {
|
||||||
|
Self {
|
||||||
|
sections: vec![LayoutSection {
|
||||||
|
leading_space: 0.0,
|
||||||
|
byte_range: 0..text.len(),
|
||||||
|
format: TextFormat::simple(text_style, color),
|
||||||
|
}],
|
||||||
|
text,
|
||||||
|
wrap_width: f32::INFINITY,
|
||||||
|
break_on_newline: false,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.sections.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for adding a new section when building a `LayoutJob`.
|
||||||
|
pub fn append(&mut self, text: &str, leading_space: f32, format: TextFormat) {
|
||||||
|
let start = self.text.len();
|
||||||
|
self.text += text;
|
||||||
|
let byte_range = start..self.text.len();
|
||||||
|
self.sections.push(LayoutSection {
|
||||||
|
leading_space,
|
||||||
|
byte_range,
|
||||||
|
format,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for LayoutJob {
|
||||||
|
#[inline]
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
let Self {
|
||||||
|
text,
|
||||||
|
sections,
|
||||||
|
wrap_width,
|
||||||
|
first_row_min_height,
|
||||||
|
break_on_newline,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
text.hash(state);
|
||||||
|
sections.hash(state);
|
||||||
|
crate::f32_hash(state, *wrap_width);
|
||||||
|
crate::f32_hash(state, *first_row_min_height);
|
||||||
|
break_on_newline.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for LayoutJob {
|
||||||
|
#[inline(always)]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.text == other.text
|
||||||
|
&& self.sections == other.sections
|
||||||
|
&& crate::f32_eq(self.wrap_width, other.wrap_width)
|
||||||
|
&& crate::f32_eq(self.first_row_min_height, other.first_row_min_height)
|
||||||
|
&& self.break_on_newline == other.break_on_newline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::Eq for LayoutJob {}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LayoutSection {
|
||||||
|
/// Can be used for first row indentation.
|
||||||
|
pub leading_space: f32,
|
||||||
|
/// Range into the galley text
|
||||||
|
pub byte_range: Range<usize>,
|
||||||
|
pub format: TextFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for LayoutSection {
|
||||||
|
#[inline]
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
let Self {
|
||||||
|
leading_space,
|
||||||
|
byte_range,
|
||||||
|
format,
|
||||||
|
} = self;
|
||||||
|
crate::f32_hash(state, *leading_space);
|
||||||
|
byte_range.hash(state);
|
||||||
|
format.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for LayoutSection {
|
||||||
|
#[inline(always)]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
crate::f32_eq(self.leading_space, other.leading_space)
|
||||||
|
&& self.byte_range == other.byte_range
|
||||||
|
&& self.format == other.format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::Eq for LayoutSection {}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
pub struct TextFormat {
|
||||||
|
pub style: TextStyle,
|
||||||
|
/// Text color
|
||||||
|
pub color: Color32,
|
||||||
|
pub background: Color32,
|
||||||
|
pub italics: bool,
|
||||||
|
pub underline: Stroke,
|
||||||
|
pub strikethrough: Stroke,
|
||||||
|
/// If you use a small font and [`Align::TOP`] you
|
||||||
|
/// can get the effect of raised text.
|
||||||
|
pub valign: Align,
|
||||||
|
// TODO: lowered
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextFormat {
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
style: TextStyle::Body,
|
||||||
|
color: Color32::GRAY,
|
||||||
|
background: Color32::TRANSPARENT,
|
||||||
|
italics: false,
|
||||||
|
underline: Stroke::none(),
|
||||||
|
strikethrough: Stroke::none(),
|
||||||
|
valign: Align::BOTTOM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextFormat {
|
||||||
|
#[inline]
|
||||||
|
pub fn simple(style: TextStyle, color: Color32) -> Self {
|
||||||
|
Self {
|
||||||
|
style,
|
||||||
|
color,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// A collection of text locked into place.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Galley {
|
pub struct Galley {
|
||||||
/// The [`crate::TextStyle`] (font) used.
|
/// The job that this galley is the result of.
|
||||||
pub text_style: crate::TextStyle,
|
/// Contains the original string and style sections.
|
||||||
|
pub job: Arc<LayoutJob>,
|
||||||
/// The full text, including any an all `\n`.
|
|
||||||
pub text: String,
|
|
||||||
|
|
||||||
/// 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 chars in all rows sum up to text.chars().count().
|
||||||
|
@ -37,88 +226,123 @@ pub struct Galley {
|
||||||
/// can be split up into multiple rows.
|
/// can be split up into multiple rows.
|
||||||
pub rows: Vec<Row>,
|
pub rows: Vec<Row>,
|
||||||
|
|
||||||
// Optimization: calculated once and reused.
|
/// Bounding size (min is always `[0,0]`)
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
|
||||||
|
/// Total number of vertices in all the row meshes.
|
||||||
|
pub num_vertices: usize,
|
||||||
|
/// Total number of indices in all the row meshes.
|
||||||
|
pub num_indices: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A typeset piece of text on a single row.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Row {
|
pub struct Row {
|
||||||
/// The start of each character, probably starting at zero.
|
/// One for each `char`.
|
||||||
/// The last element is the end of the last character.
|
pub glyphs: Vec<Glyph>,
|
||||||
/// This is never empty.
|
|
||||||
/// Unit: points.
|
|
||||||
///
|
|
||||||
/// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1`
|
|
||||||
pub x_offsets: Vec<f32>,
|
|
||||||
|
|
||||||
/// Per-character. Used when rendering.
|
/// Logical bounding rectangle based on font heights etc.
|
||||||
pub uv_rects: Vec<Option<UvRect>>,
|
/// Use this when drawing a selection or similar!
|
||||||
|
/// Includes leading and trailing whitespace.
|
||||||
|
pub rect: Rect,
|
||||||
|
|
||||||
/// Top of the row, offset within the Galley.
|
/// The mesh, ready to be rendered.
|
||||||
/// Unit: points.
|
pub visuals: RowVisuals,
|
||||||
pub y_min: f32,
|
|
||||||
|
|
||||||
/// Bottom of the row, offset within the Galley.
|
|
||||||
/// Unit: points.
|
|
||||||
pub y_max: f32,
|
|
||||||
|
|
||||||
/// If true, this `Row` came from a paragraph ending with a `\n`.
|
/// If true, this `Row` came from a paragraph ending with a `\n`.
|
||||||
/// The `\n` itself is omitted from `x_offsets`.
|
/// The `\n` itself is omitted from [`Self::glyphs`].
|
||||||
/// A `\n` in the input text always creates a new `Row` below it,
|
/// A `\n` in the input text always creates a new `Row` below it,
|
||||||
/// so that text that ends with `\n` has an empty `Row` last.
|
/// so that text that ends with `\n` has an empty `Row` last.
|
||||||
/// This also implies that the last `Row` in a `Galley` always has `ends_with_newline == false`.
|
/// This also implies that the last `Row` in a `Galley` always has `ends_with_newline == false`.
|
||||||
pub ends_with_newline: bool,
|
pub ends_with_newline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Row {
|
/// The tessellated output of a row.
|
||||||
#[inline]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub fn sanity_check(&self) {
|
pub struct RowVisuals {
|
||||||
assert!(!self.x_offsets.is_empty());
|
/// The tessellated text, using non-normalized (texel) UV coordinates.
|
||||||
assert!(self.x_offsets.len() == self.uv_rects.len() + 1);
|
/// That is, you need to divide the uv coordinates by the texture size.
|
||||||
|
pub mesh: Mesh,
|
||||||
|
|
||||||
|
/// Bounds of the mesh, and can be used for culling.
|
||||||
|
/// Does NOT include leading or trailing whitespace glyphs!!
|
||||||
|
pub mesh_bounds: Rect,
|
||||||
|
|
||||||
|
/// The range of vertices in the mesh the contain glyphs.
|
||||||
|
/// Before comes backgrounds (if any), and after any underlines and strikethrough.
|
||||||
|
pub glyph_vertex_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RowVisuals {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mesh: Default::default(),
|
||||||
|
mesh_bounds: Rect::NOTHING,
|
||||||
|
glyph_vertex_range: 0..0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub struct Glyph {
|
||||||
|
pub chr: char,
|
||||||
|
/// Relative to the galley position.
|
||||||
|
/// Logical position: pos.y is the same for all chars of the same [`TextFormat`].
|
||||||
|
pub pos: Pos2,
|
||||||
|
/// Advance width and font row height.
|
||||||
|
pub size: Vec2,
|
||||||
|
/// Position of the glyph in the font texture.
|
||||||
|
pub uv_rect: UvRect,
|
||||||
|
/// Index into [`LayoutJob::sections`]. Decides color etc.
|
||||||
|
pub section_index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Glyph {
|
||||||
|
pub fn max_x(&self) -> f32 {
|
||||||
|
self.pos.x + self.size.x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same y range for all characters with the same [`TextFormat`].
|
||||||
|
#[inline]
|
||||||
|
pub fn logical_rect(&self) -> Rect {
|
||||||
|
Rect::from_min_size(self.pos, self.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl Row {
|
||||||
/// Excludes the implicit `\n` after the `Row`, if any.
|
/// Excludes the implicit `\n` after the `Row`, if any.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn char_count_excluding_newline(&self) -> usize {
|
pub fn char_count_excluding_newline(&self) -> usize {
|
||||||
assert!(!self.x_offsets.is_empty());
|
self.glyphs.len()
|
||||||
self.x_offsets.len() - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Includes the implicit `\n` after the `Row`, if any.
|
/// Includes the implicit `\n` after the `Row`, if any.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn char_count_including_newline(&self) -> usize {
|
pub fn char_count_including_newline(&self) -> usize {
|
||||||
self.char_count_excluding_newline() + (self.ends_with_newline as usize)
|
self.glyphs.len() + (self.ends_with_newline as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn min_x(&self) -> f32 {
|
pub fn min_y(&self) -> f32 {
|
||||||
*self.x_offsets.first().unwrap()
|
self.rect.top()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn max_x(&self) -> f32 {
|
pub fn max_y(&self) -> f32 {
|
||||||
*self.x_offsets.last().unwrap()
|
self.rect.bottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn height(&self) -> f32 {
|
pub fn height(&self) -> f32 {
|
||||||
self.y_max - self.y_min
|
self.rect.height()
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() {
|
for (i, glyph) in self.glyphs.iter().enumerate() {
|
||||||
let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]);
|
if desired_x < glyph.logical_rect().center().x {
|
||||||
if desired_x < char_center_x {
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,56 +350,35 @@ impl Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn x_offset(&self, column: usize) -> f32 {
|
pub fn x_offset(&self, column: usize) -> f32 {
|
||||||
self.x_offsets[column.min(self.x_offsets.len() - 1)]
|
if let Some(glyph) = self.glyphs.get(column) {
|
||||||
}
|
glyph.pos.x
|
||||||
|
} else {
|
||||||
// Move down this much
|
self.rect.right()
|
||||||
#[inline(always)]
|
}
|
||||||
pub fn translate_y(&mut self, dy: f32) {
|
|
||||||
self.y_min += dy;
|
|
||||||
self.y_max += dy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Galley {
|
impl Galley {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.text.is_empty()
|
self.job.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub(crate) fn char_count_excluding_newlines(&self) -> usize {
|
pub fn text(&self) -> &str {
|
||||||
let mut char_count = 0;
|
&self.job.text
|
||||||
for row in &self.rows {
|
|
||||||
char_count += row.char_count_excluding_newline();
|
|
||||||
}
|
|
||||||
char_count
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sanity_check(&self) {
|
|
||||||
let mut char_count = 0;
|
|
||||||
for row in &self.rows {
|
|
||||||
row.sanity_check();
|
|
||||||
char_count += row.char_count_including_newline();
|
|
||||||
}
|
|
||||||
crate::epaint_assert!(char_count == self.text.chars().count());
|
|
||||||
if let Some(last_row) = self.rows.last() {
|
|
||||||
crate::epaint_assert!(
|
|
||||||
!last_row.ends_with_newline,
|
|
||||||
"If the text ends with '\\n', there would be an empty row last.\n\
|
|
||||||
Galley: {:#?}",
|
|
||||||
self
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// ## Physical positions
|
/// ## Physical positions
|
||||||
impl Galley {
|
impl Galley {
|
||||||
|
/// Zero-width rect past the last character.
|
||||||
fn end_pos(&self) -> Rect {
|
fn end_pos(&self) -> Rect {
|
||||||
if let Some(row) = self.rows.last() {
|
if let Some(row) = self.rows.last() {
|
||||||
let x = row.max_x();
|
let x = row.rect.right();
|
||||||
Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max))
|
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
|
||||||
} else {
|
} else {
|
||||||
// Empty galley
|
// Empty galley
|
||||||
Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
|
Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
|
||||||
|
@ -201,7 +404,7 @@ impl Galley {
|
||||||
&& column >= row.char_count_excluding_newline();
|
&& column >= row.char_count_excluding_newline();
|
||||||
if !select_next_row_instead {
|
if !select_next_row_instead {
|
||||||
let x = row.x_offset(column);
|
let x = row.x_offset(column);
|
||||||
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +422,7 @@ impl Galley {
|
||||||
|
|
||||||
/// Returns a 0-width Rect.
|
/// Returns a 0-width Rect.
|
||||||
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
||||||
self.pos_from_pcursor(cursor.pcursor) // The one TextEdit stores
|
self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cursor at the given position within the galley
|
/// Cursor at the given position within the galley
|
||||||
|
@ -231,8 +434,8 @@ impl Galley {
|
||||||
let mut pcursor_it = PCursor::default();
|
let mut pcursor_it = PCursor::default();
|
||||||
|
|
||||||
for (row_nr, row) in self.rows.iter().enumerate() {
|
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||||
let is_pos_within_row = pos.y >= row.y_min && pos.y <= row.y_max;
|
let is_pos_within_row = pos.y >= row.min_y() && pos.y <= row.max_y();
|
||||||
let y_dist = (row.y_min - pos.y).abs().min((row.y_max - pos.y).abs());
|
let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs());
|
||||||
if is_pos_within_row || y_dist < best_y_dist {
|
if is_pos_within_row || y_dist < best_y_dist {
|
||||||
best_y_dist = y_dist;
|
best_y_dist = y_dist;
|
||||||
let column = row.char_at(pos.x);
|
let column = row.char_at(pos.x);
|
||||||
|
@ -515,7 +718,7 @@ impl Galley {
|
||||||
} else {
|
} else {
|
||||||
// keep same X coord
|
// keep same X coord
|
||||||
let x = self.pos_from_cursor(cursor).center().x;
|
let x = self.pos_from_cursor(cursor).center().x;
|
||||||
let column = if x > self.rows[new_row].max_x() {
|
let column = if x > self.rows[new_row].rect.right() {
|
||||||
// beyond the end of this row - keep same colum
|
// beyond the end of this row - keep same colum
|
||||||
cursor.rcursor.column
|
cursor.rcursor.column
|
||||||
} else {
|
} else {
|
||||||
|
@ -546,7 +749,7 @@ impl Galley {
|
||||||
} else {
|
} else {
|
||||||
// keep same X coord
|
// keep same X coord
|
||||||
let x = self.pos_from_cursor(cursor).center().x;
|
let x = self.pos_from_cursor(cursor).center().x;
|
||||||
let column = if x > self.rows[new_row].max_x() {
|
let column = if x > self.rows[new_row].rect.right() {
|
||||||
// beyond the end of the next row - keep same column
|
// beyond the end of the next row - keep same column
|
||||||
cursor.rcursor.column
|
cursor.rcursor.column
|
||||||
} else {
|
} else {
|
||||||
|
@ -578,244 +781,3 @@ impl Galley {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_text_layout() {
|
|
||||||
impl PartialEq for Cursor {
|
|
||||||
fn eq(&self, other: &Cursor) -> bool {
|
|
||||||
(self.ccursor, self.rcursor, self.pcursor)
|
|
||||||
== (other.ccursor, other.rcursor, other.pcursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
let pixels_per_point = 1.0;
|
|
||||||
let fonts = text::Fonts::from_definitions(pixels_per_point, text::FontDefinitions::default());
|
|
||||||
let font = &fonts[TextStyle::Monospace];
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 1);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[0].x_offsets, vec![0.0]);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("\n".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 2);
|
|
||||||
assert!(galley.rows[0].ends_with_newline);
|
|
||||||
assert!(!galley.rows[1].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("\n\n".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 3);
|
|
||||||
assert!(galley.rows[0].ends_with_newline);
|
|
||||||
assert!(galley.rows[1].ends_with_newline);
|
|
||||||
assert!(!galley.rows[2].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[2].x_offsets, vec![0.0]);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline(" ".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 1);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("One row!".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 1);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("First row!\n".to_owned(), 1024.0);
|
|
||||||
assert_eq!(galley.rows.len(), 2);
|
|
||||||
assert!(galley.rows[0].ends_with_newline);
|
|
||||||
assert!(!galley.rows[1].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
|
||||||
|
|
||||||
let galley = font.layout_multiline("line\nbreak".to_owned(), 40.0);
|
|
||||||
assert_eq!(galley.rows.len(), 2);
|
|
||||||
assert!(galley.rows[0].ends_with_newline);
|
|
||||||
assert!(!galley.rows[1].ends_with_newline);
|
|
||||||
|
|
||||||
// Test wrapping:
|
|
||||||
let galley = font.layout_multiline("word wrap".to_owned(), 40.0);
|
|
||||||
assert_eq!(galley.rows.len(), 2);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
assert!(!galley.rows[1].ends_with_newline);
|
|
||||||
|
|
||||||
{
|
|
||||||
// Test wrapping:
|
|
||||||
let galley = font.layout_multiline("word wrap.\nNew para.".to_owned(), 40.0);
|
|
||||||
assert_eq!(galley.rows.len(), 4);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[0].char_count_excluding_newline(), "word ".len());
|
|
||||||
assert_eq!(galley.rows[0].char_count_including_newline(), "word ".len());
|
|
||||||
assert!(galley.rows[1].ends_with_newline);
|
|
||||||
assert_eq!(galley.rows[1].char_count_excluding_newline(), "wrap.".len());
|
|
||||||
assert_eq!(
|
|
||||||
galley.rows[1].char_count_including_newline(),
|
|
||||||
"wrap.\n".len()
|
|
||||||
);
|
|
||||||
assert_eq!(galley.rows[2].char_count_excluding_newline(), "New ".len());
|
|
||||||
assert_eq!(galley.rows[3].char_count_excluding_newline(), "para.".len());
|
|
||||||
assert!(!galley.rows[2].ends_with_newline);
|
|
||||||
assert!(!galley.rows[3].ends_with_newline);
|
|
||||||
|
|
||||||
let cursor = Cursor::default();
|
|
||||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
|
||||||
|
|
||||||
let cursor = galley.end();
|
|
||||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
|
||||||
assert_eq!(
|
|
||||||
cursor,
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(20),
|
|
||||||
rcursor: RCursor { row: 3, column: 5 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 1,
|
|
||||||
offset: 9,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let cursor = galley.from_ccursor(CCursor::new(1));
|
|
||||||
assert_eq!(cursor.rcursor, RCursor { row: 0, column: 1 });
|
|
||||||
assert_eq!(
|
|
||||||
cursor.pcursor,
|
|
||||||
PCursor {
|
|
||||||
paragraph: 0,
|
|
||||||
offset: 1,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
|
||||||
|
|
||||||
let cursor = galley.from_pcursor(PCursor {
|
|
||||||
paragraph: 1,
|
|
||||||
offset: 2,
|
|
||||||
prefer_next_row: false,
|
|
||||||
});
|
|
||||||
assert_eq!(cursor.rcursor, RCursor { row: 2, column: 2 });
|
|
||||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
|
||||||
|
|
||||||
let cursor = galley.from_pcursor(PCursor {
|
|
||||||
paragraph: 1,
|
|
||||||
offset: 6,
|
|
||||||
prefer_next_row: false,
|
|
||||||
});
|
|
||||||
assert_eq!(cursor.rcursor, RCursor { row: 3, column: 2 });
|
|
||||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
|
||||||
|
|
||||||
// On the border between two rows within the same paragraph:
|
|
||||||
let cursor = galley.from_rcursor(RCursor { row: 0, column: 5 });
|
|
||||||
assert_eq!(
|
|
||||||
cursor,
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(5),
|
|
||||||
rcursor: RCursor { row: 0, column: 5 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 0,
|
|
||||||
offset: 5,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
|
|
||||||
let cursor = galley.from_rcursor(RCursor { row: 1, column: 0 });
|
|
||||||
assert_eq!(
|
|
||||||
cursor,
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(5),
|
|
||||||
rcursor: RCursor { row: 1, column: 0 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 0,
|
|
||||||
offset: 5,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Test cursor movement:
|
|
||||||
let galley = font.layout_multiline("word wrap.\nNew para.".to_owned(), 40.0);
|
|
||||||
assert_eq!(galley.rows.len(), 4);
|
|
||||||
assert!(!galley.rows[0].ends_with_newline);
|
|
||||||
assert!(galley.rows[1].ends_with_newline);
|
|
||||||
assert!(!galley.rows[2].ends_with_newline);
|
|
||||||
assert!(!galley.rows[3].ends_with_newline);
|
|
||||||
|
|
||||||
let cursor = Cursor::default();
|
|
||||||
|
|
||||||
assert_eq!(galley.cursor_up_one_row(&cursor), cursor);
|
|
||||||
assert_eq!(galley.cursor_begin_of_row(&cursor), cursor);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
galley.cursor_end_of_row(&cursor),
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(5),
|
|
||||||
rcursor: RCursor { row: 0, column: 5 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 0,
|
|
||||||
offset: 5,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
galley.cursor_down_one_row(&cursor),
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(5),
|
|
||||||
rcursor: RCursor { row: 1, column: 0 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 0,
|
|
||||||
offset: 5,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let cursor = Cursor::default();
|
|
||||||
assert_eq!(
|
|
||||||
galley.cursor_down_one_row(&galley.cursor_down_one_row(&cursor)),
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(11),
|
|
||||||
rcursor: RCursor { row: 2, column: 0 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 1,
|
|
||||||
offset: 0,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let cursor = galley.end();
|
|
||||||
assert_eq!(galley.cursor_down_one_row(&cursor), cursor);
|
|
||||||
|
|
||||||
let cursor = galley.end();
|
|
||||||
assert!(galley.cursor_up_one_row(&galley.end()) != cursor);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
galley.cursor_up_one_row(&galley.end()),
|
|
||||||
Cursor {
|
|
||||||
ccursor: CCursor::new(15),
|
|
||||||
rcursor: RCursor { row: 2, column: 5 },
|
|
||||||
pcursor: PCursor {
|
|
||||||
paragraph: 1,
|
|
||||||
offset: 4,
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue