mirror of
https://github.com/Noratrieb/game-wip-dontplay.git
synced 2026-01-16 20:35:02 +01:00
vendor
This commit is contained in:
parent
12163d1338
commit
550b1644cb
363 changed files with 84081 additions and 16 deletions
81
egui/crates/epaint/CHANGELOG.md
Normal file
81
egui/crates/epaint/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# epaint changelog
|
||||
All notable changes to the epaint crate will be documented in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 0.21.0 - 2023-02-08
|
||||
* Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)).
|
||||
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
|
||||
* Fix bug in `Mesh::split_to_u16` ([#2459](https://github.com/emilk/egui/pull/2459)).
|
||||
* Improve rendering of very thin rectangles.
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08
|
||||
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
|
||||
* ⚠️ BREAKING: epaint now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
|
||||
* Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)).
|
||||
* Added `Fonts::has_glyph(s)` for querying if a glyph is supported ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
|
||||
* Split out color into its own crate, `ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)).
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
* Added `epaint::hex_color!` to create `Color32`'s from hex strings under the `color-hex` feature ([#1596](https://github.com/emilk/egui/pull/1596)).
|
||||
* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).
|
||||
* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)).
|
||||
* Texture loading now takes a `TextureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)).
|
||||
|
||||
|
||||
## 0.18.1 - 2022-05-01
|
||||
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
|
||||
|
||||
|
||||
## 0.18.0 - 2022-04-30
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Added `Shape::Callback` for backend-specific painting ([#1351](https://github.com/emilk/egui/pull/1351)).
|
||||
* Added more text wrapping options ([#1291](https://github.com/emilk/egui/pull/1291)):
|
||||
* Added `TextWrapping` struct containing all wrapping options.
|
||||
* Added `LayoutJob::wrap` field containing these options.
|
||||
* Moved `LayoutJob::wrap_width` to `TextWrapping::max_width`.
|
||||
* Added `TextWrapping::max_rows` to limit amount of rows the text should have.
|
||||
* Added `TextWrapping::break_anywhere` to control should the text break at appropriate places or not.
|
||||
* Added `TextWrapping::overflow_character` to specify what character should be used to represent clipped text.
|
||||
* Removed the `single_threaded/multi_threaded` flags - epaint is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)).
|
||||
* `Tessellator::from_options` is now `Tessellator::new` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
||||
* Renamed `TessellationOptions::anti_alias` to `feathering` ([#1408](https://github.com/emilk/egui/pull/1408)).
|
||||
* Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)).
|
||||
* Dark text is darker and more readable on bright backgrounds ([#1412](https://github.com/emilk/egui/pull/1412)).
|
||||
* Fixed panic when tessellating a `Shape::Vec` containing meshes with differing `TextureId`s ([#1445](https://github.com/emilk/egui/pull/1445)).
|
||||
* Added `Shape::galley_with_color` which adds the functionality of `Painter::galley_with_color` into the Shape enum ([#1461](https://github.com/emilk/egui/pull/1461)).
|
||||
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
|
||||
* Added line breaking rules for Japanese text ([#1498](https://github.com/emilk/egui/pull/1498)).
|
||||
* Optimize tessellation of circles and boxes with rounded corners ([#1547](https://github.com/emilk/egui/pull/1547)).
|
||||
|
||||
|
||||
## 0.17.0 - 2022-02-22
|
||||
* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)):
|
||||
* Replaced `TextStyle` with `FontId` which lets you pick any font size and font family.
|
||||
* Replaced `Fonts::font_image` with `font_image_delta` for partial font atlas updates.
|
||||
* Made the v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)).
|
||||
* Added `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)).
|
||||
* Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)).
|
||||
* Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)).
|
||||
* Fixed anti-aliasing of filled paths with counter-clockwise winding order.
|
||||
* Improve the anti-aliasing of filled paths with sharp corners, at the cost of these corners sometimes becoming badly extruded instead (see https://github.com/emilk/egui/issues/1226).
|
||||
|
||||
|
||||
## 0.16.0 - 2021-12-29
|
||||
* Anti-alias path ends ([#893](https://github.com/emilk/egui/pull/893)).
|
||||
* `Rgba` now implements `Hash` ([#886](https://github.com/emilk/egui/pull/886)).
|
||||
* Renamed `Texture` to `FontImage`.
|
||||
|
||||
|
||||
## 0.15.0 - 2021-10-24
|
||||
* `Fonts::layout_job`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
|
||||
* New `CircleShape`, `PathShape`, `RectShape` and `TextShape` used in `enum Shape`.
|
||||
* Added support for rotated text (see `TextShape`).
|
||||
* Added `"convert_bytemuck"` feature.
|
||||
105
egui/crates/epaint/Cargo.toml
Normal file
105
egui/crates/epaint/Cargo.toml
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
[package]
|
||||
name = "epaint"
|
||||
version = "0.21.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Minimal 2D graphics library for GUI work"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/epaint"
|
||||
license = "(MIT OR Apache-2.0) AND OFL-1.1 AND LicenseRef-UFL-1.0" # OFL and UFL used by default_fonts. See https://github.com/emilk/egui/issues/2321
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/master/crates/epaint"
|
||||
categories = ["graphics", "gui"]
|
||||
keywords = ["graphics", "gui", "egui"]
|
||||
include = [
|
||||
"../LICENSE-APACHE",
|
||||
"../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
"fonts/*.ttf",
|
||||
"fonts/*.txt",
|
||||
"fonts/OFL.txt",
|
||||
"fonts/UFL.txt",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["default_fonts"]
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`Vertex`] to `&[u8]`.
|
||||
bytemuck = ["dep:bytemuck", "emath/bytemuck", "ecolor/bytemuck"]
|
||||
|
||||
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
|
||||
cint = ["ecolor/cint"]
|
||||
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = ["ecolor/color-hex"]
|
||||
|
||||
## This will automatically detect deadlocks due to double-locking on the same thread.
|
||||
## If your app freezes, you may want to enable this!
|
||||
## Only affects [`mutex::RwLock`] (which epaint and egui uses a lot).
|
||||
deadlock_detection = ["dep:backtrace"]
|
||||
|
||||
## If set, epaint will use `include_bytes!` to bundle some fonts.
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = []
|
||||
|
||||
## Enable additional checks if debug assertions are enabled (debug builds).
|
||||
extra_debug_asserts = [
|
||||
"emath/extra_debug_asserts",
|
||||
"ecolor/extra_debug_asserts",
|
||||
]
|
||||
## Always enable additional checks.
|
||||
extra_asserts = ["emath/extra_asserts", "ecolor/extra_asserts"]
|
||||
|
||||
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
|
||||
mint = ["emath/mint"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = []
|
||||
|
||||
[dependencies]
|
||||
emath = { version = "0.21.0", path = "../emath" }
|
||||
ecolor = { version = "0.21.0", path = "../ecolor" }
|
||||
|
||||
ab_glyph = "0.2.11"
|
||||
ahash = { version = "0.8.1", default-features = false, features = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
"std",
|
||||
] }
|
||||
nohash-hasher = "0.2"
|
||||
|
||||
#! ### Optional dependencies
|
||||
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde) .
|
||||
serde = { version = "1", optional = true, features = ["derive", "rc"] }
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
parking_lot = "0.12" # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
atomic_refcell = "0.1" # Used instead of parking_lot on on wasm. See https://github.com/emilk/egui/issues/1401
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.4", default-features = false }
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
11
egui/crates/epaint/README.md
Normal file
11
egui/crates/epaint/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# epaint - egui paint library
|
||||
|
||||
[](https://crates.io/crates/epaint)
|
||||
[](https://docs.rs/epaint)
|
||||
[](https://github.com/rust-secure-code/safety-dance/)
|
||||

|
||||

|
||||
|
||||
A bare-bones 2D graphics library for turning simple 2D shapes and text into textured triangles.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
||||
81
egui/crates/epaint/benches/benchmark.rs
Normal file
81
egui/crates/epaint/benches/benchmark.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use epaint::*;
|
||||
|
||||
fn single_dashed_lines(c: &mut Criterion) {
|
||||
c.bench_function("single_dashed_lines", move |b| {
|
||||
b.iter(|| {
|
||||
let mut v = Vec::new();
|
||||
|
||||
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
|
||||
|
||||
for _ in 0..100 {
|
||||
v.extend(Shape::dashed_line(
|
||||
&line,
|
||||
Stroke::new(1.5, Color32::RED),
|
||||
10.0,
|
||||
2.5,
|
||||
));
|
||||
}
|
||||
|
||||
black_box(v);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn many_dashed_lines(c: &mut Criterion) {
|
||||
c.bench_function("many_dashed_lines", move |b| {
|
||||
b.iter(|| {
|
||||
let mut v = Vec::new();
|
||||
|
||||
let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)];
|
||||
|
||||
for _ in 0..100 {
|
||||
Shape::dashed_line_many(&line, Stroke::new(1.5, Color32::RED), 10.0, 2.5, &mut v);
|
||||
}
|
||||
|
||||
black_box(v);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn tessellate_circles(c: &mut Criterion) {
|
||||
c.bench_function("tessellate_circles_100k", move |b| {
|
||||
let radii: [f32; 10] = [1.0, 2.0, 3.6, 4.0, 5.7, 8.0, 10.0, 13.0, 15.0, 17.0];
|
||||
let mut clipped_shapes = vec![];
|
||||
for r in radii {
|
||||
for _ in 0..10_000 {
|
||||
let clip_rect = Rect::from_min_size(Pos2::ZERO, Vec2::splat(1024.0));
|
||||
let shape = Shape::circle_filled(Pos2::new(10.0, 10.0), r, Color32::WHITE);
|
||||
clipped_shapes.push(ClippedShape(clip_rect, shape));
|
||||
}
|
||||
}
|
||||
assert_eq!(clipped_shapes.len(), 100_000);
|
||||
|
||||
let pixels_per_point = 2.0;
|
||||
let options = TessellationOptions::default();
|
||||
|
||||
let atlas = TextureAtlas::new([4096, 256]);
|
||||
let font_tex_size = atlas.size();
|
||||
let prepared_discs = atlas.prepared_discs();
|
||||
|
||||
b.iter(|| {
|
||||
let clipped_primitive = tessellate_shapes(
|
||||
pixels_per_point,
|
||||
options,
|
||||
font_tex_size,
|
||||
prepared_discs.clone(),
|
||||
clipped_shapes.clone(),
|
||||
);
|
||||
black_box(clipped_primitive);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
single_dashed_lines,
|
||||
many_dashed_lines,
|
||||
tessellate_circles
|
||||
);
|
||||
criterion_main!(benches);
|
||||
BIN
egui/crates/epaint/fonts/Hack-Regular.ttf
Normal file
BIN
egui/crates/epaint/fonts/Hack-Regular.ttf
Normal file
Binary file not shown.
31
egui/crates/epaint/fonts/Hack-Regular.txt
Normal file
31
egui/crates/epaint/fonts/Hack-Regular.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License
|
||||
|
||||
The work in the DejaVu project was committed to the public domain.
|
||||
|
||||
Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera"
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Source Foundry Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
BITSTREAM VERA LICENSE
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org.
|
||||
BIN
egui/crates/epaint/fonts/NotoEmoji-Regular.ttf
Normal file
BIN
egui/crates/epaint/fonts/NotoEmoji-Regular.ttf
Normal file
Binary file not shown.
92
egui/crates/epaint/fonts/OFL.txt
Normal file
92
egui/crates/epaint/fonts/OFL.txt
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
96
egui/crates/epaint/fonts/UFL.txt
Executable file
96
egui/crates/epaint/fonts/UFL.txt
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
-------------------------------
|
||||
UBUNTU FONT LICENCE Version 1.0
|
||||
-------------------------------
|
||||
|
||||
PREAMBLE
|
||||
This licence allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely. The fonts, including any derivative works, can be
|
||||
bundled, embedded, and redistributed provided the terms of this licence
|
||||
are met. The fonts and derivatives, however, cannot be released under
|
||||
any other licence. The requirement for fonts to remain under this
|
||||
licence does not require any document created using the fonts or their
|
||||
derivatives to be published under this licence, as long as the primary
|
||||
purpose of the document is not to be a vehicle for the distribution of
|
||||
the fonts.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this licence and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as received under this licence.
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to
|
||||
a new environment.
|
||||
|
||||
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||
copyright ownership of the Font Software.
|
||||
|
||||
"Substantially Changed" refers to Modified Versions which can be easily
|
||||
identified as dissimilar to the Font Software by users of the Font
|
||||
Software comparing the Original Version with the Modified Version.
|
||||
|
||||
To "Propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification and with or without charging
|
||||
a redistribution fee), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
This licence does not grant any rights under trademark law and all such
|
||||
rights are reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to propagate the Font Software, subject to
|
||||
the below conditions:
|
||||
|
||||
1) Each copy of the Font Software must contain the above copyright
|
||||
notice and this licence. These can be included either as stand-alone
|
||||
text files, human-readable headers or in the appropriate machine-
|
||||
readable metadata fields within text or binary files as long as those
|
||||
fields can be easily viewed by the user.
|
||||
|
||||
2) The font name complies with the following:
|
||||
(a) The Original Version must retain its name, unmodified.
|
||||
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||
avoid use of the name of the Original Version or similar names entirely.
|
||||
(c) Modified Versions which are not Substantially Changed must be
|
||||
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||
additional naming elements to distinguish the Modified Version from the
|
||||
Original Version. The name of such Modified Versions must be the name of
|
||||
the Original Version, with "derivative X" where X represents the name of
|
||||
the new work, appended to that name.
|
||||
|
||||
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||
Font Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except (i) as required by this licence, (ii) to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||
their explicit written permission.
|
||||
|
||||
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this licence, and must not be distributed
|
||||
under any other licence. The requirement for fonts to remain under this
|
||||
licence does not affect any document created using the Font Software,
|
||||
except any version of the Font Software extracted from a document
|
||||
created using the Font Software may only be distributed under this
|
||||
licence.
|
||||
|
||||
TERMINATION
|
||||
This licence becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
egui/crates/epaint/fonts/Ubuntu-Light.ttf
Executable file
BIN
egui/crates/epaint/fonts/Ubuntu-Light.ttf
Executable file
Binary file not shown.
9
egui/crates/epaint/fonts/emoji-icon-font-mit-license.txt
Normal file
9
egui/crates/epaint/fonts/emoji-icon-font-mit-license.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2014 John Slegers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
BIN
egui/crates/epaint/fonts/emoji-icon-font.ttf
Normal file
BIN
egui/crates/epaint/fonts/emoji-icon-font.ttf
Normal file
Binary file not shown.
32
egui/crates/epaint/fonts/list_fonts.py
Normal file
32
egui/crates/epaint/fonts/list_fonts.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.unicode import Unicode
|
||||
from itertools import chain
|
||||
import sys
|
||||
|
||||
ttf = TTFont(sys.argv[1], 0, verbose=0, allowVID=0,
|
||||
ignoreDecompileErrors=True,
|
||||
fontNumber=-1)
|
||||
|
||||
chars = chain.from_iterable([y + (Unicode[y[0]],)
|
||||
for y in x.cmap.items()] for x in ttf["cmap"].tables)
|
||||
|
||||
|
||||
all_codepoints = {}
|
||||
|
||||
for entry in chars:
|
||||
codepoint = entry[0]
|
||||
short_name = entry[1]
|
||||
long_name = entry[2].lower()
|
||||
if False:
|
||||
print(f'(0x{codepoint:02X}, "{short_name}", "{long_name}"),')
|
||||
else:
|
||||
name = short_name if long_name == "????" else long_name
|
||||
# print(f'(0x{codepoint:02X}, "{name}"),')
|
||||
all_codepoints[codepoint] = name
|
||||
|
||||
for codepoint in sorted(all_codepoints.keys()):
|
||||
name = all_codepoints[codepoint]
|
||||
print(f'(0x{codepoint:02X}, \'{chr(codepoint)}\', "{name}"),')
|
||||
|
||||
ttf.close()
|
||||
1124
egui/crates/epaint/src/bezier.rs
Normal file
1124
egui/crates/epaint/src/bezier.rs
Normal file
File diff suppressed because it is too large
Load diff
376
egui/crates/epaint/src/image.rs
Normal file
376
egui/crates/epaint/src/image.rs
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
use crate::{textures::TextureOptions, Color32};
|
||||
|
||||
/// An image stored in RAM.
|
||||
///
|
||||
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
|
||||
///
|
||||
/// In order to paint the image on screen, you first need to convert it to
|
||||
///
|
||||
/// See also: [`ColorImage`], [`FontImage`].
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ImageData {
|
||||
/// RGBA image.
|
||||
Color(ColorImage),
|
||||
|
||||
/// Used for the font texture.
|
||||
Font(FontImage),
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
match self {
|
||||
Self::Color(image) => image.size,
|
||||
Self::Font(image) => image.size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.size()[0]
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.size()[1]
|
||||
}
|
||||
|
||||
pub fn bytes_per_pixel(&self) -> usize {
|
||||
match self {
|
||||
Self::Color(_) | Self::Font(_) => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A 2D RGBA color image in RAM.
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ColorImage {
|
||||
/// width, height.
|
||||
pub size: [usize; 2],
|
||||
|
||||
/// The pixels, row by row, from top to bottom.
|
||||
pub pixels: Vec<Color32>,
|
||||
}
|
||||
|
||||
impl ColorImage {
|
||||
/// Create an image filled with the given color.
|
||||
pub fn new(size: [usize; 2], color: Color32) -> Self {
|
||||
Self {
|
||||
size,
|
||||
pixels: vec![color; size[0] * size[1]],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`ColorImage`] from flat un-multiplied RGBA data.
|
||||
///
|
||||
/// This is usually what you want to use after having loaded an image file.
|
||||
///
|
||||
/// Panics if `size[0] * size[1] * 4 != rgba.len()`.
|
||||
///
|
||||
/// ## Example using the [`image`](crates.io/crates/image) crate:
|
||||
/// ``` ignore
|
||||
/// fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> {
|
||||
/// let image = image::io::Reader::open(path)?.decode()?;
|
||||
/// let size = [image.width() as _, image.height() as _];
|
||||
/// let image_buffer = image.to_rgba8();
|
||||
/// let pixels = image_buffer.as_flat_samples();
|
||||
/// Ok(egui::ColorImage::from_rgba_unmultiplied(
|
||||
/// size,
|
||||
/// pixels.as_slice(),
|
||||
/// ))
|
||||
/// }
|
||||
///
|
||||
/// fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
|
||||
/// let image = image::load_from_memory(image_data)?;
|
||||
/// let size = [image.width() as _, image.height() as _];
|
||||
/// let image_buffer = image.to_rgba8();
|
||||
/// let pixels = image_buffer.as_flat_samples();
|
||||
/// Ok(ColorImage::from_rgba_unmultiplied(
|
||||
/// size,
|
||||
/// pixels.as_slice(),
|
||||
/// ))
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
|
||||
assert_eq!(size[0] * size[1] * 4, rgba.len());
|
||||
let pixels = rgba
|
||||
.chunks_exact(4)
|
||||
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
|
||||
.collect();
|
||||
Self { size, pixels }
|
||||
}
|
||||
|
||||
pub fn from_rgba_premultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
|
||||
assert_eq!(size[0] * size[1] * 4, rgba.len());
|
||||
let pixels = rgba
|
||||
.chunks_exact(4)
|
||||
.map(|p| Color32::from_rgba_premultiplied(p[0], p[1], p[2], p[3]))
|
||||
.collect();
|
||||
Self { size, pixels }
|
||||
}
|
||||
|
||||
/// A view of the underlying data as `&[u8]`
|
||||
#[cfg(feature = "bytemuck")]
|
||||
pub fn as_raw(&self) -> &[u8] {
|
||||
bytemuck::cast_slice(&self.pixels)
|
||||
}
|
||||
|
||||
/// A view of the underlying data as `&mut [u8]`
|
||||
#[cfg(feature = "bytemuck")]
|
||||
pub fn as_raw_mut(&mut self) -> &mut [u8] {
|
||||
bytemuck::cast_slice_mut(&mut self.pixels)
|
||||
}
|
||||
|
||||
/// Create a new Image from a patch of the current image. This method is especially convenient for screenshotting a part of the app
|
||||
/// since `region` can be interpreted as screen coordinates of the entire screenshot if `pixels_per_point` is provided for the native application.
|
||||
/// The floats of [`emath::Rect`] are cast to usize, rounding them down in order to interpret them as indices to the image data.
|
||||
///
|
||||
/// Panics if `region.min.x > region.max.x || region.min.y > region.max.y`, or if a region larger than the image is passed.
|
||||
pub fn region(&self, region: &emath::Rect, pixels_per_point: Option<f32>) -> Self {
|
||||
let pixels_per_point = pixels_per_point.unwrap_or(1.0);
|
||||
let min_x = (region.min.x * pixels_per_point) as usize;
|
||||
let max_x = (region.max.x * pixels_per_point) as usize;
|
||||
let min_y = (region.min.y * pixels_per_point) as usize;
|
||||
let max_y = (region.max.y * pixels_per_point) as usize;
|
||||
assert!(min_x <= max_x);
|
||||
assert!(min_y <= max_y);
|
||||
let width = max_x - min_x;
|
||||
let height = max_y - min_y;
|
||||
let mut output = Vec::with_capacity(width * height);
|
||||
let row_stride = self.size[0];
|
||||
|
||||
for row in min_y..max_y {
|
||||
output.extend_from_slice(
|
||||
&self.pixels[row * row_stride + min_x..row * row_stride + max_x],
|
||||
);
|
||||
}
|
||||
Self {
|
||||
size: [width, height],
|
||||
pixels: output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`ColorImage`] from flat RGB data.
|
||||
///
|
||||
/// This is what you want to use after having loaded an image file (and if
|
||||
/// you are ignoring the alpha channel - considering it to always be 0xff)
|
||||
///
|
||||
/// Panics if `size[0] * size[1] * 3 != rgb.len()`.
|
||||
pub fn from_rgb(size: [usize; 2], rgb: &[u8]) -> Self {
|
||||
assert_eq!(size[0] * size[1] * 3, rgb.len());
|
||||
let pixels = rgb
|
||||
.chunks_exact(3)
|
||||
.map(|p| Color32::from_rgb(p[0], p[1], p[2]))
|
||||
.collect();
|
||||
Self { size, pixels }
|
||||
}
|
||||
|
||||
/// An example color image, useful for tests.
|
||||
pub fn example() -> Self {
|
||||
let width = 128;
|
||||
let height = 64;
|
||||
let mut img = Self::new([width, height], Color32::TRANSPARENT);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let h = x as f32 / width as f32;
|
||||
let s = 1.0;
|
||||
let v = 1.0;
|
||||
let a = y as f32 / height as f32;
|
||||
img[(x, y)] = crate::Hsva { h, s, v, a }.into();
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.size[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.size[1]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for ColorImage {
|
||||
type Output = Color32;
|
||||
|
||||
#[inline]
|
||||
fn index(&self, (x, y): (usize, usize)) -> &Color32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h);
|
||||
&self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<(usize, usize)> for ColorImage {
|
||||
#[inline]
|
||||
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h);
|
||||
&mut self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorImage> for ImageData {
|
||||
#[inline(always)]
|
||||
fn from(image: ColorImage) -> Self {
|
||||
Self::Color(image)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A single-channel image designed for the font texture.
|
||||
///
|
||||
/// Each value represents "coverage", i.e. how much a texel is covered by a character.
|
||||
///
|
||||
/// This is roughly interpreted as the opacity of a white image.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontImage {
|
||||
/// width, height
|
||||
pub size: [usize; 2],
|
||||
|
||||
/// The coverage value.
|
||||
///
|
||||
/// Often you want to use [`Self::srgba_pixels`] instead.
|
||||
pub pixels: Vec<f32>,
|
||||
}
|
||||
|
||||
impl FontImage {
|
||||
pub fn new(size: [usize; 2]) -> Self {
|
||||
Self {
|
||||
size,
|
||||
pixels: vec![0.0; size[0] * size[1]],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> usize {
|
||||
self.size[0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> usize {
|
||||
self.size[1]
|
||||
}
|
||||
|
||||
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
||||
///
|
||||
/// `gamma` should normally be set to `None`.
|
||||
///
|
||||
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
|
||||
pub fn srgba_pixels(
|
||||
&'_ self,
|
||||
gamma: Option<f32>,
|
||||
) -> impl ExactSizeIterator<Item = Color32> + '_ {
|
||||
let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it.
|
||||
self.pixels.iter().map(move |coverage| {
|
||||
let alpha = coverage.powf(gamma);
|
||||
// We want to multiply with `vec4(alpha)` in the fragment shader:
|
||||
let a = fast_round(alpha * 255.0);
|
||||
Color32::from_rgba_premultiplied(a, a, a, a)
|
||||
})
|
||||
}
|
||||
|
||||
/// Clone a sub-region as a new image.
|
||||
pub fn region(&self, [x, y]: [usize; 2], [w, h]: [usize; 2]) -> FontImage {
|
||||
assert!(x + w <= self.width());
|
||||
assert!(y + h <= self.height());
|
||||
|
||||
let mut pixels = Vec::with_capacity(w * h);
|
||||
for y in y..y + h {
|
||||
let offset = y * self.width() + x;
|
||||
pixels.extend(&self.pixels[offset..(offset + w)]);
|
||||
}
|
||||
assert_eq!(pixels.len(), w * h);
|
||||
FontImage {
|
||||
size: [w, h],
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for FontImage {
|
||||
type Output = f32;
|
||||
|
||||
#[inline]
|
||||
fn index(&self, (x, y): (usize, usize)) -> &f32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h);
|
||||
&self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<(usize, usize)> for FontImage {
|
||||
#[inline]
|
||||
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut f32 {
|
||||
let [w, h] = self.size;
|
||||
assert!(x < w && y < h);
|
||||
&mut self.pixels[y * w + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontImage> for ImageData {
|
||||
#[inline(always)]
|
||||
fn from(image: FontImage) -> Self {
|
||||
Self::Font(image)
|
||||
}
|
||||
}
|
||||
|
||||
fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A change to an image.
|
||||
///
|
||||
/// Either a whole new image, or an update to a rectangular region of it.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct ImageDelta {
|
||||
/// What to set the texture to.
|
||||
///
|
||||
/// If [`Self::pos`] is `None`, this describes the whole texture.
|
||||
///
|
||||
/// If [`Self::pos`] is `Some`, this describes a patch of the whole image starting at [`Self::pos`].
|
||||
pub image: ImageData,
|
||||
|
||||
pub options: TextureOptions,
|
||||
|
||||
/// If `None`, set the whole texture to [`Self::image`].
|
||||
///
|
||||
/// If `Some(pos)`, update a sub-region of an already allocated texture with the patch in [`Self::image`].
|
||||
pub pos: Option<[usize; 2]>,
|
||||
}
|
||||
|
||||
impl ImageDelta {
|
||||
/// Update the whole texture.
|
||||
pub fn full(image: impl Into<ImageData>, options: TextureOptions) -> Self {
|
||||
Self {
|
||||
image: image.into(),
|
||||
options,
|
||||
pos: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a sub-region of an existing texture.
|
||||
pub fn partial(pos: [usize; 2], image: impl Into<ImageData>, options: TextureOptions) -> Self {
|
||||
Self {
|
||||
image: image.into(),
|
||||
options,
|
||||
pos: Some(pos),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this affecting the whole texture?
|
||||
/// If `false`, this is a partial (sub-region) update.
|
||||
pub fn is_whole(&self) -> bool {
|
||||
self.pos.is_none()
|
||||
}
|
||||
}
|
||||
161
egui/crates/epaint/src/lib.rs
Normal file
161
egui/crates/epaint/src/lib.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
//! A simple 2D graphics library for turning simple 2D shapes and text into textured triangles.
|
||||
//!
|
||||
//! Made for [`egui`](https://github.com/emilk/egui/).
|
||||
//!
|
||||
//! Create some [`Shape`]:s and pass them to [`tessellate_shapes`] to generate [`Mesh`]:es
|
||||
//! that you can then paint using some graphics API of your choice (e.g. OpenGL).
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod bezier;
|
||||
pub mod image;
|
||||
mod mesh;
|
||||
pub mod mutex;
|
||||
mod shadow;
|
||||
mod shape;
|
||||
pub mod shape_transform;
|
||||
pub mod stats;
|
||||
mod stroke;
|
||||
pub mod tessellator;
|
||||
pub mod text;
|
||||
mod texture_atlas;
|
||||
mod texture_handle;
|
||||
pub mod textures;
|
||||
pub mod util;
|
||||
|
||||
pub use {
|
||||
bezier::{CubicBezierShape, QuadraticBezierShape},
|
||||
image::{ColorImage, FontImage, ImageData, ImageDelta},
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
shadow::Shadow,
|
||||
shape::{
|
||||
CircleShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, Rounding, Shape,
|
||||
TextShape,
|
||||
},
|
||||
stats::PaintStats,
|
||||
stroke::Stroke,
|
||||
tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
|
||||
text::{FontFamily, FontId, Fonts, Galley},
|
||||
texture_atlas::TextureAtlas,
|
||||
texture_handle::TextureHandle,
|
||||
textures::TextureManager,
|
||||
};
|
||||
|
||||
pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba};
|
||||
pub use emath::{pos2, vec2, Pos2, Rect, Vec2};
|
||||
|
||||
pub use ahash;
|
||||
pub use ecolor;
|
||||
pub use emath;
|
||||
|
||||
#[cfg(feature = "color-hex")]
|
||||
pub use ecolor::hex_color;
|
||||
|
||||
/// The UV coordinate of a white region of the texture mesh.
|
||||
/// The default egui texture has the top-left corner pixel fully white.
|
||||
/// You need need use a clamping texture sampler for this to work
|
||||
/// (so it doesn't do bilinear blending with bottom right corner).
|
||||
pub const WHITE_UV: emath::Pos2 = emath::pos2(0.0, 0.0);
|
||||
|
||||
/// What texture to use in a [`Mesh`] mesh.
|
||||
///
|
||||
/// If you don't want to use a texture, use `TextureId::Epaint(0)` and the [`WHITE_UV`] for uv-coord.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum TextureId {
|
||||
/// Textures allocated using [`TextureManager`].
|
||||
///
|
||||
/// The first texture (`TextureId::Epaint(0)`) is used for the font data.
|
||||
Managed(u64),
|
||||
|
||||
/// Your own texture, defined in any which way you want.
|
||||
/// The backend renderer will presumably use this to look up what texture to use.
|
||||
User(u64),
|
||||
}
|
||||
|
||||
impl Default for TextureId {
|
||||
/// The epaint font texture.
|
||||
fn default() -> Self {
|
||||
Self::Managed(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Shape`] within a clip rectangle.
|
||||
///
|
||||
/// Everything is using logical points.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ClippedShape(
|
||||
/// Clip / scissor rectangle.
|
||||
/// Only show the part of the [`Shape`] that falls within this.
|
||||
pub emath::Rect,
|
||||
/// The shape
|
||||
pub Shape,
|
||||
);
|
||||
|
||||
/// A [`Mesh`] or [`PaintCallback`] within a clip rectangle.
|
||||
///
|
||||
/// Everything is using logical points.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClippedPrimitive {
|
||||
/// Clip / scissor rectangle.
|
||||
/// Only show the part of the [`Mesh`] that falls within this.
|
||||
pub clip_rect: emath::Rect,
|
||||
|
||||
/// What to paint - either a [`Mesh`] or a [`PaintCallback`].
|
||||
pub primitive: Primitive,
|
||||
}
|
||||
|
||||
/// A rendering primitive - either a [`Mesh`] or a [`PaintCallback`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Primitive {
|
||||
Mesh(Mesh),
|
||||
Callback(PaintCallback),
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
|
||||
/// or with the `extra_debug_asserts` feature in debug builds.
|
||||
#[macro_export]
|
||||
macro_rules! epaint_assert {
|
||||
($($arg: tt)*) => {
|
||||
if cfg!(any(
|
||||
feature = "extra_asserts",
|
||||
all(feature = "extra_debug_asserts", debug_assertions),
|
||||
)) {
|
||||
assert!($($arg)*);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[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 f64_hash<H: std::hash::Hasher>(state: &mut H, f: f64) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
316
egui/crates/epaint/src/mesh.rs
Normal file
316
egui/crates/epaint/src/mesh.rs
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
use crate::*;
|
||||
use emath::*;
|
||||
|
||||
/// The 2D vertex type.
|
||||
///
|
||||
/// Should be friendly to send to GPU as is.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg(not(feature = "unity"))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Vertex {
|
||||
/// Logical pixel coordinates (points).
|
||||
/// (0,0) is the top left corner of the screen.
|
||||
pub pos: Pos2, // 64 bit
|
||||
|
||||
/// Normalized texture coordinates.
|
||||
/// (0, 0) is the top left corner of the texture.
|
||||
/// (1, 1) is the bottom right corner of the texture.
|
||||
pub uv: Pos2, // 64 bit
|
||||
|
||||
/// sRGBA with premultiplied alpha
|
||||
pub color: Color32, // 32 bit
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg(feature = "unity")]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Vertex {
|
||||
/// Logical pixel coordinates (points).
|
||||
/// (0,0) is the top left corner of the screen.
|
||||
pub pos: Pos2, // 64 bit
|
||||
|
||||
/// sRGBA with premultiplied alpha
|
||||
pub color: Color32, // 32 bit
|
||||
|
||||
/// Normalized texture coordinates.
|
||||
/// (0, 0) is the top left corner of the texture.
|
||||
/// (1, 1) is the bottom right corner of the texture.
|
||||
pub uv: Pos2, // 64 bit
|
||||
}
|
||||
|
||||
/// Textured triangles in two dimensions.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Mesh {
|
||||
/// Draw as triangles (i.e. the length is always multiple of three).
|
||||
///
|
||||
/// If you only support 16-bit indices you can use [`Mesh::split_to_u16`].
|
||||
///
|
||||
/// egui is NOT consistent with what winding order it uses, so turn off backface culling.
|
||||
pub indices: Vec<u32>,
|
||||
|
||||
/// The vertex data indexed by `indices`.
|
||||
pub vertices: Vec<Vertex>,
|
||||
|
||||
/// The texture to use when drawing these triangles.
|
||||
pub texture_id: TextureId,
|
||||
// TODO(emilk): bounding rectangle
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
pub fn with_texture(texture_id: TextureId) -> Self {
|
||||
Self {
|
||||
texture_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore to default state, but without freeing memory.
|
||||
pub fn clear(&mut self) {
|
||||
self.indices.clear();
|
||||
self.vertices.clear();
|
||||
self.vertices = Default::default();
|
||||
}
|
||||
|
||||
pub fn bytes_used(&self) -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
+ self.vertices.len() * std::mem::size_of::<Vertex>()
|
||||
+ self.indices.len() * std::mem::size_of::<u32>()
|
||||
}
|
||||
|
||||
/// Are all indices within the bounds of the contained vertices?
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if let Ok(n) = u32::try_from(self.vertices.len()) {
|
||||
self.indices.iter().all(|&i| i < n)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
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`.
|
||||
pub fn append(&mut self, other: Mesh) {
|
||||
crate::epaint_assert!(other.is_valid());
|
||||
|
||||
if self.is_empty() {
|
||||
*self = other;
|
||||
} else {
|
||||
self.append_ref(&other);
|
||||
}
|
||||
}
|
||||
|
||||
/// Append all the indices and vertices of `other` to `self` without
|
||||
/// taking ownership.
|
||||
pub fn append_ref(&mut self, other: &Mesh) {
|
||||
crate::epaint_assert!(other.is_valid());
|
||||
|
||||
if !self.is_empty() {
|
||||
assert_eq!(
|
||||
self.texture_id, other.texture_id,
|
||||
"Can't merge Mesh using different textures"
|
||||
);
|
||||
} else {
|
||||
self.texture_id = other.texture_id;
|
||||
}
|
||||
|
||||
let index_offset = self.vertices.len() as u32;
|
||||
self.indices
|
||||
.extend(other.indices.iter().map(|index| index + index_offset));
|
||||
self.vertices.extend(other.vertices.iter());
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) {
|
||||
crate::epaint_assert!(self.texture_id == TextureId::default());
|
||||
self.vertices.push(Vertex {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
/// Add a triangle.
|
||||
#[inline(always)]
|
||||
pub fn add_triangle(&mut self, a: u32, b: u32, c: u32) {
|
||||
self.indices.push(a);
|
||||
self.indices.push(b);
|
||||
self.indices.push(c);
|
||||
}
|
||||
|
||||
/// Make room for this many additional triangles (will reserve 3x as many indices).
|
||||
/// See also `reserve_vertices`.
|
||||
#[inline(always)]
|
||||
pub fn reserve_triangles(&mut self, additional_triangles: usize) {
|
||||
self.indices.reserve(3 * additional_triangles);
|
||||
}
|
||||
|
||||
/// Make room for this many additional vertices.
|
||||
/// See also `reserve_triangles`.
|
||||
#[inline(always)]
|
||||
pub fn reserve_vertices(&mut self, additional: usize) {
|
||||
self.vertices.reserve(additional);
|
||||
}
|
||||
|
||||
/// Rectangle with a texture and color.
|
||||
pub fn add_rect_with_uv(&mut self, rect: Rect, uv: Rect, color: Color32) {
|
||||
#![allow(clippy::identity_op)]
|
||||
|
||||
let idx = self.vertices.len() as u32;
|
||||
self.add_triangle(idx + 0, idx + 1, idx + 2);
|
||||
self.add_triangle(idx + 2, idx + 1, idx + 3);
|
||||
|
||||
self.vertices.push(Vertex {
|
||||
pos: rect.left_top(),
|
||||
uv: uv.left_top(),
|
||||
color,
|
||||
});
|
||||
self.vertices.push(Vertex {
|
||||
pos: rect.right_top(),
|
||||
uv: uv.right_top(),
|
||||
color,
|
||||
});
|
||||
self.vertices.push(Vertex {
|
||||
pos: rect.left_bottom(),
|
||||
uv: uv.left_bottom(),
|
||||
color,
|
||||
});
|
||||
self.vertices.push(Vertex {
|
||||
pos: rect.right_bottom(),
|
||||
uv: uv.right_bottom(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
/// Uniformly colored rectangle.
|
||||
#[inline(always)]
|
||||
pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) {
|
||||
crate::epaint_assert!(self.texture_id == TextureId::default());
|
||||
self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color);
|
||||
}
|
||||
|
||||
/// This is for platforms that only support 16-bit index buffers.
|
||||
///
|
||||
/// Splits this mesh into many smaller meshes (if needed)
|
||||
/// where the smaller meshes have 16-bit indices.
|
||||
pub fn split_to_u16(self) -> Vec<Mesh16> {
|
||||
crate::epaint_assert!(self.is_valid());
|
||||
|
||||
const MAX_SIZE: u32 = std::u16::MAX as u32;
|
||||
|
||||
if self.vertices.len() <= MAX_SIZE as usize {
|
||||
// Common-case optimization:
|
||||
return vec![Mesh16 {
|
||||
indices: self.indices.iter().map(|&i| i as u16).collect(),
|
||||
vertices: self.vertices,
|
||||
texture_id: self.texture_id,
|
||||
}];
|
||||
}
|
||||
|
||||
let mut output = vec![];
|
||||
let mut index_cursor = 0;
|
||||
|
||||
while index_cursor < self.indices.len() {
|
||||
let span_start = index_cursor;
|
||||
let mut min_vindex = self.indices[index_cursor];
|
||||
let mut max_vindex = self.indices[index_cursor];
|
||||
|
||||
while index_cursor < self.indices.len() {
|
||||
let (mut new_min, mut new_max) = (min_vindex, max_vindex);
|
||||
for i in 0..3 {
|
||||
let idx = self.indices[index_cursor + i];
|
||||
new_min = new_min.min(idx);
|
||||
new_max = new_max.max(idx);
|
||||
}
|
||||
|
||||
let new_span_size = new_max - new_min + 1; // plus one, because it is an inclusive range
|
||||
if new_span_size <= MAX_SIZE {
|
||||
// Triangle fits
|
||||
min_vindex = new_min;
|
||||
max_vindex = new_max;
|
||||
index_cursor += 3;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
index_cursor > span_start,
|
||||
"One triangle spanned more than {} vertices",
|
||||
MAX_SIZE
|
||||
);
|
||||
|
||||
let mesh = Mesh16 {
|
||||
indices: self.indices[span_start..index_cursor]
|
||||
.iter()
|
||||
.map(|vi| u16::try_from(vi - min_vindex).unwrap())
|
||||
.collect(),
|
||||
vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(),
|
||||
texture_id: self.texture_id,
|
||||
};
|
||||
crate::epaint_assert!(mesh.is_valid());
|
||||
output.push(mesh);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Translate location by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
for v in &mut self.vertices {
|
||||
v.pos += delta;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate by some angle about an origin, in-place.
|
||||
///
|
||||
/// Origin is a position in screen space.
|
||||
pub fn rotate(&mut self, rot: Rot2, origin: Pos2) {
|
||||
for v in &mut self.vertices {
|
||||
v.pos = origin + rot * (v.pos - origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A version of [`Mesh`] that uses 16-bit indices.
|
||||
///
|
||||
/// This is produced by [`Mesh::split_to_u16`] and is meant to be used for legacy render backends.
|
||||
pub struct Mesh16 {
|
||||
/// Draw as triangles (i.e. the length is always multiple of three).
|
||||
///
|
||||
/// egui is NOT consistent with what winding order it uses, so turn off backface culling.
|
||||
pub indices: Vec<u16>,
|
||||
|
||||
/// The vertex data indexed by `indices`.
|
||||
pub vertices: Vec<Vertex>,
|
||||
|
||||
/// The texture to use when drawing these triangles.
|
||||
pub texture_id: TextureId,
|
||||
}
|
||||
|
||||
impl Mesh16 {
|
||||
/// Are all indices within the bounds of the contained vertices?
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if let Ok(n) = u16::try_from(self.vertices.len()) {
|
||||
self.indices.iter().all(|&i| i < n)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
579
egui/crates/epaint/src/mutex.rs
Normal file
579
egui/crates/epaint/src/mutex.rs
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
//! Helper module that wraps some Mutex types with different implementations.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod mutex_impl {
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T>(parking_lot::Mutex<T>);
|
||||
|
||||
/// The lock you get from [`Mutex`].
|
||||
pub use parking_lot::MutexGuard;
|
||||
|
||||
impl<T> Mutex<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(parking_lot::Mutex::new(val))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
self.0.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(debug_assertions)]
|
||||
mod mutex_impl {
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T>(parking_lot::Mutex<T>);
|
||||
|
||||
/// The lock you get from [`Mutex`].
|
||||
pub struct MutexGuard<'a, T>(parking_lot::MutexGuard<'a, T>, *const ());
|
||||
|
||||
#[derive(Default)]
|
||||
struct HeldLocks(Vec<*const ()>);
|
||||
|
||||
impl HeldLocks {
|
||||
#[inline(always)]
|
||||
fn insert(&mut self, lock: *const ()) {
|
||||
// Very few locks will ever be held at the same time, so a linear search is fast
|
||||
assert!(
|
||||
!self.0.contains(&lock),
|
||||
"Recursively locking a Mutex in the same thread is not supported"
|
||||
);
|
||||
self.0.push(lock);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn remove(&mut self, lock: *const ()) {
|
||||
self.0.retain(|&ptr| ptr != lock);
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static HELD_LOCKS_TLS: std::cell::RefCell<HeldLocks> = Default::default();
|
||||
}
|
||||
|
||||
impl<T> Mutex<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(parking_lot::Mutex::new(val))
|
||||
}
|
||||
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
// Detect if we are recursively taking out a lock on this mutex.
|
||||
|
||||
// use a pointer to the inner data as an id for this lock
|
||||
let ptr = (&self.0 as *const parking_lot::Mutex<_>).cast::<()>();
|
||||
|
||||
// Store it in thread local storage while we have a lock guard taken out
|
||||
HELD_LOCKS_TLS.with(|held_locks| {
|
||||
held_locks.borrow_mut().insert(ptr);
|
||||
});
|
||||
|
||||
MutexGuard(self.0.lock(), ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for MutexGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
let ptr = self.1;
|
||||
HELD_LOCKS_TLS.with(|held_locks| {
|
||||
held_locks.borrow_mut().remove(ptr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for MutexGuard<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::DerefMut for MutexGuard<'_, T> {
|
||||
#[inline(always)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(feature = "deadlock_detection"))]
|
||||
mod rw_lock_impl {
|
||||
/// The lock you get from [`RwLock::read`].
|
||||
pub use parking_lot::MappedRwLockReadGuard as RwLockReadGuard;
|
||||
|
||||
/// The lock you get from [`RwLock::write`].
|
||||
pub use parking_lot::MappedRwLockWriteGuard as RwLockWriteGuard;
|
||||
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct RwLock<T>(parking_lot::RwLock<T>);
|
||||
|
||||
impl<T> RwLock<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(parking_lot::RwLock::new(val))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read(&self) -> RwLockReadGuard<'_, T> {
|
||||
parking_lot::RwLockReadGuard::map(self.0.read(), |v| v)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, T> {
|
||||
parking_lot::RwLockWriteGuard::map(self.0.write(), |v| v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
mod rw_lock_impl {
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
thread::ThreadId,
|
||||
};
|
||||
|
||||
use ahash::HashMap;
|
||||
use parking_lot::{MappedRwLockReadGuard, MappedRwLockWriteGuard};
|
||||
|
||||
/// The lock you get from [`RwLock::read`].
|
||||
pub struct RwLockReadGuard<'a, T> {
|
||||
// The option is used only because we need to `take()` the guard out of self
|
||||
// when doing remappings (`map()`), i.e. it's used as a safe `ManuallyDrop`.
|
||||
guard: Option<MappedRwLockReadGuard<'a, T>>,
|
||||
holders: Arc<parking_lot::Mutex<HashMap<ThreadId, backtrace::Backtrace>>>,
|
||||
}
|
||||
|
||||
impl<'a, T> RwLockReadGuard<'a, T> {
|
||||
#[inline]
|
||||
pub fn map<U, F>(mut s: Self, f: F) -> RwLockReadGuard<'a, U>
|
||||
where
|
||||
F: FnOnce(&T) -> &U,
|
||||
{
|
||||
RwLockReadGuard {
|
||||
guard: s
|
||||
.guard
|
||||
.take()
|
||||
.map(|g| parking_lot::MappedRwLockReadGuard::map(g, f)),
|
||||
holders: Arc::clone(&s.holders),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for RwLockReadGuard<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.guard.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for RwLockReadGuard<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
let tid = std::thread::current().id();
|
||||
self.holders.lock().remove(&tid);
|
||||
}
|
||||
}
|
||||
|
||||
/// The lock you get from [`RwLock::write`].
|
||||
pub struct RwLockWriteGuard<'a, T> {
|
||||
// The option is used only because we need to `take()` the guard out of self
|
||||
// when doing remappings (`map()`), i.e. it's used as a safe `ManuallyDrop`.
|
||||
guard: Option<MappedRwLockWriteGuard<'a, T>>,
|
||||
holders: Arc<parking_lot::Mutex<HashMap<ThreadId, backtrace::Backtrace>>>,
|
||||
}
|
||||
|
||||
impl<'a, T> RwLockWriteGuard<'a, T> {
|
||||
#[inline]
|
||||
pub fn map<U, F>(mut s: Self, f: F) -> RwLockWriteGuard<'a, U>
|
||||
where
|
||||
F: FnOnce(&mut T) -> &mut U,
|
||||
{
|
||||
RwLockWriteGuard {
|
||||
guard: s
|
||||
.guard
|
||||
.take()
|
||||
.map(|g| parking_lot::MappedRwLockWriteGuard::map(g, f)),
|
||||
holders: Arc::clone(&s.holders),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for RwLockWriteGuard<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.guard.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DerefMut for RwLockWriteGuard<'a, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.guard.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for RwLockWriteGuard<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
let tid = std::thread::current().id();
|
||||
self.holders.lock().remove(&tid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct RwLock<T> {
|
||||
lock: parking_lot::RwLock<T>,
|
||||
// Technically we'd need a list of backtraces per thread-id since parking_lot's
|
||||
// read-locks are reentrant.
|
||||
// In practice it's not that useful to have the whole list though, so we only
|
||||
// keep track of the first backtrace for now.
|
||||
holders: Arc<parking_lot::Mutex<HashMap<ThreadId, backtrace::Backtrace>>>,
|
||||
}
|
||||
|
||||
impl<T> RwLock<T> {
|
||||
pub fn new(val: T) -> Self {
|
||||
Self {
|
||||
lock: parking_lot::RwLock::new(val),
|
||||
holders: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> RwLockReadGuard<'_, T> {
|
||||
let tid = std::thread::current().id();
|
||||
|
||||
// If it is write-locked, and we locked it (reentrancy deadlock)
|
||||
let would_deadlock =
|
||||
self.lock.is_locked_exclusive() && self.holders.lock().contains_key(&tid);
|
||||
assert!(
|
||||
!would_deadlock,
|
||||
"{} DEAD-LOCK DETECTED ({:?})!\n\
|
||||
Trying to grab read-lock at:\n{}\n\
|
||||
which is already exclusively held by current thread at:\n{}\n\n",
|
||||
std::any::type_name::<Self>(),
|
||||
tid,
|
||||
format_backtrace(&mut make_backtrace()),
|
||||
format_backtrace(self.holders.lock().get_mut(&tid).unwrap())
|
||||
);
|
||||
|
||||
self.holders
|
||||
.lock()
|
||||
.entry(tid)
|
||||
.or_insert_with(make_backtrace);
|
||||
|
||||
RwLockReadGuard {
|
||||
guard: parking_lot::RwLockReadGuard::map(self.lock.read(), |v| v).into(),
|
||||
holders: Arc::clone(&self.holders),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, T> {
|
||||
let tid = std::thread::current().id();
|
||||
|
||||
// If it is locked in any way, and we locked it (reentrancy deadlock)
|
||||
let would_deadlock = self.lock.is_locked() && self.holders.lock().contains_key(&tid);
|
||||
assert!(
|
||||
!would_deadlock,
|
||||
"{} DEAD-LOCK DETECTED ({:?})!\n\
|
||||
Trying to grab write-lock at:\n{}\n\
|
||||
which is already held by current thread at:\n{}\n\n",
|
||||
std::any::type_name::<Self>(),
|
||||
tid,
|
||||
format_backtrace(&mut make_backtrace()),
|
||||
format_backtrace(self.holders.lock().get_mut(&tid).unwrap())
|
||||
);
|
||||
|
||||
self.holders
|
||||
.lock()
|
||||
.entry(tid)
|
||||
.or_insert_with(make_backtrace);
|
||||
|
||||
RwLockWriteGuard {
|
||||
guard: parking_lot::RwLockWriteGuard::map(self.lock.write(), |v| v).into(),
|
||||
holders: Arc::clone(&self.holders),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_backtrace() -> backtrace::Backtrace {
|
||||
backtrace::Backtrace::new_unresolved()
|
||||
}
|
||||
|
||||
fn format_backtrace(backtrace: &mut backtrace::Backtrace) -> String {
|
||||
backtrace.resolve();
|
||||
|
||||
let stacktrace = format!("{:?}", backtrace);
|
||||
|
||||
// Remove irrelevant parts of the stacktrace:
|
||||
let end_offset = stacktrace
|
||||
.find("std::sys_common::backtrace::__rust_begin_short_backtrace")
|
||||
.unwrap_or(stacktrace.len());
|
||||
let stacktrace = &stacktrace[..end_offset];
|
||||
|
||||
let first_interesting_function = "epaint::mutex::rw_lock_impl::make_backtrace\n";
|
||||
if let Some(start_offset) = stacktrace.find(first_interesting_function) {
|
||||
stacktrace[start_offset + first_interesting_function.len()..].to_owned()
|
||||
} else {
|
||||
stacktrace.to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod mutex_impl {
|
||||
// `atomic_refcell` will panic if multiple threads try to access the same value
|
||||
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T>(atomic_refcell::AtomicRefCell<T>);
|
||||
|
||||
/// The lock you get from [`Mutex`].
|
||||
pub use atomic_refcell::AtomicRefMut as MutexGuard;
|
||||
|
||||
impl<T> Mutex<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(atomic_refcell::AtomicRefCell::new(val))
|
||||
}
|
||||
|
||||
/// Panics if already locked.
|
||||
#[inline(always)]
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod rw_lock_impl {
|
||||
// `atomic_refcell` will panic if multiple threads try to access the same value
|
||||
|
||||
/// The lock you get from [`RwLock::read`].
|
||||
pub use atomic_refcell::AtomicRef as RwLockReadGuard;
|
||||
|
||||
/// The lock you get from [`RwLock::write`].
|
||||
pub use atomic_refcell::AtomicRefMut as RwLockWriteGuard;
|
||||
|
||||
/// Provides interior mutability.
|
||||
///
|
||||
/// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets.
|
||||
#[derive(Default)]
|
||||
pub struct RwLock<T>(atomic_refcell::AtomicRefCell<T>);
|
||||
|
||||
impl<T> RwLock<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(atomic_refcell::AtomicRefCell::new(val))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read(&self) -> RwLockReadGuard<'_, T> {
|
||||
self.0.borrow()
|
||||
}
|
||||
|
||||
/// Panics if already locked.
|
||||
#[inline(always)]
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, T> {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub use mutex_impl::{Mutex, MutexGuard};
|
||||
pub use rw_lock_impl::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
impl<T> Clone for Mutex<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(self.lock().clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::mutex::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn lock_two_different_mutexes_single_thread() {
|
||||
let one = Mutex::new(());
|
||||
let two = Mutex::new(());
|
||||
let _a = one.lock();
|
||||
let _b = two.lock();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn lock_reentry_single_thread() {
|
||||
let one = Mutex::new(());
|
||||
let _a = one.lock();
|
||||
let _a2 = one.lock(); // panics
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_multiple_threads() {
|
||||
use std::sync::Arc;
|
||||
let one = Arc::new(Mutex::new(()));
|
||||
let our_lock = one.lock();
|
||||
let other_thread = {
|
||||
let one = Arc::clone(&one);
|
||||
std::thread::spawn(move || {
|
||||
let _ = one.lock();
|
||||
})
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
drop(our_lock);
|
||||
other_thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[cfg(test)]
|
||||
mod tests_rwlock {
|
||||
use crate::mutex::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn lock_two_different_rwlocks_single_thread() {
|
||||
let one = RwLock::new(());
|
||||
let two = RwLock::new(());
|
||||
let _a = one.write();
|
||||
let _b = two.write();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rwlock_multiple_threads() {
|
||||
use std::sync::Arc;
|
||||
let one = Arc::new(RwLock::new(()));
|
||||
let our_lock = one.write();
|
||||
let other_thread1 = {
|
||||
let one = Arc::clone(&one);
|
||||
std::thread::spawn(move || {
|
||||
let _ = one.write();
|
||||
})
|
||||
};
|
||||
let other_thread2 = {
|
||||
let one = Arc::clone(&one);
|
||||
std::thread::spawn(move || {
|
||||
let _ = one.read();
|
||||
})
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
drop(our_lock);
|
||||
other_thread1.join().unwrap();
|
||||
other_thread2.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn rwlock_write_write_reentrancy() {
|
||||
let one = RwLock::new(());
|
||||
let _a1 = one.write();
|
||||
let _a2 = one.write(); // panics
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn rwlock_write_read_reentrancy() {
|
||||
let one = RwLock::new(());
|
||||
let _a1 = one.write();
|
||||
let _a2 = one.read(); // panics
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn rwlock_read_write_reentrancy() {
|
||||
let one = RwLock::new(());
|
||||
let _a1 = one.read();
|
||||
let _a2 = one.write(); // panics
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rwlock_read_read_reentrancy() {
|
||||
let one = RwLock::new(());
|
||||
let _a1 = one.read();
|
||||
// This is legal: this test suite specifically targets native, which relies
|
||||
// on parking_lot's rw-locks, which are reentrant.
|
||||
let _a2 = one.read();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rwlock_short_read_foreign_read_write_reentrancy() {
|
||||
use std::sync::Arc;
|
||||
|
||||
let lock = Arc::new(RwLock::new(()));
|
||||
|
||||
// Thread #0 grabs a read lock
|
||||
let t0r0 = lock.read();
|
||||
|
||||
// Thread #1 grabs the same read lock
|
||||
let other_thread = {
|
||||
let lock = Arc::clone(&lock);
|
||||
std::thread::spawn(move || {
|
||||
let _t1r0 = lock.read();
|
||||
})
|
||||
};
|
||||
other_thread.join().unwrap();
|
||||
|
||||
// Thread #0 releases its read lock
|
||||
drop(t0r0);
|
||||
|
||||
// Thread #0 now grabs a write lock, which is legal
|
||||
let _t0w0 = lock.write();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn rwlock_read_foreign_read_write_reentrancy() {
|
||||
use std::sync::Arc;
|
||||
|
||||
let lock = Arc::new(RwLock::new(()));
|
||||
|
||||
// Thread #0 grabs a read lock
|
||||
let _t0r0 = lock.read();
|
||||
|
||||
// Thread #1 grabs the same read lock
|
||||
let other_thread = {
|
||||
let lock = Arc::clone(&lock);
|
||||
std::thread::spawn(move || {
|
||||
let _t1r0 = lock.read();
|
||||
})
|
||||
};
|
||||
other_thread.join().unwrap();
|
||||
|
||||
// Thread #0 now grabs a write lock, which should panic (read-write)
|
||||
let _t0w0 = lock.write(); // panics
|
||||
}
|
||||
}
|
||||
87
egui/crates/epaint/src/shadow.rs
Normal file
87
egui/crates/epaint/src/shadow.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use super::*;
|
||||
|
||||
/// The color and fuzziness of a fuzzy shape.
|
||||
/// Can be used for a rectangular shadow with a soft penumbra.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Shadow {
|
||||
/// The shadow extends this much outside the rect.
|
||||
/// The size of the fuzzy penumbra.
|
||||
pub extrusion: f32,
|
||||
|
||||
/// Color of the opaque center of the shadow.
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
impl Shadow {
|
||||
pub const NONE: Self = Self {
|
||||
extrusion: 0.0,
|
||||
color: Color32::TRANSPARENT,
|
||||
};
|
||||
|
||||
/// Tooltips, menus, …, for dark mode.
|
||||
pub fn small_dark() -> Self {
|
||||
Self {
|
||||
extrusion: 16.0,
|
||||
color: Color32::from_black_alpha(96),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tooltips, menus, …, for light mode.
|
||||
pub fn small_light() -> Self {
|
||||
Self {
|
||||
extrusion: 16.0,
|
||||
color: Color32::from_black_alpha(20),
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for egui windows in dark mode.
|
||||
pub fn big_dark() -> Self {
|
||||
Self {
|
||||
extrusion: 32.0,
|
||||
color: Color32::from_black_alpha(96),
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for egui windows in light mode.
|
||||
pub fn big_light() -> Self {
|
||||
Self {
|
||||
extrusion: 32.0,
|
||||
color: Color32::from_black_alpha(16),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tessellate(&self, rect: Rect, rounding: impl Into<Rounding>) -> Mesh {
|
||||
// tessellator.clip_rect = clip_rect; // TODO(emilk): culling
|
||||
|
||||
let Self { extrusion, color } = *self;
|
||||
|
||||
let rounding: Rounding = rounding.into();
|
||||
let half_ext = 0.5 * extrusion;
|
||||
|
||||
let ext_rounding = Rounding {
|
||||
nw: rounding.nw + half_ext,
|
||||
ne: rounding.ne + half_ext,
|
||||
sw: rounding.sw + half_ext,
|
||||
se: rounding.se + half_ext,
|
||||
};
|
||||
|
||||
use crate::tessellator::*;
|
||||
let rect = RectShape::filled(rect.expand(half_ext), ext_rounding, color);
|
||||
let pixels_per_point = 1.0; // doesn't matter here
|
||||
let font_tex_size = [1; 2]; // unused size we are not tessellating text.
|
||||
let mut tessellator = Tessellator::new(
|
||||
pixels_per_point,
|
||||
TessellationOptions {
|
||||
feathering: true,
|
||||
feathering_size_in_pixels: extrusion * pixels_per_point,
|
||||
..Default::default()
|
||||
},
|
||||
font_tex_size,
|
||||
vec![],
|
||||
);
|
||||
let mut mesh = Mesh::default();
|
||||
tessellator.tessellate_rect(&rect, &mut mesh);
|
||||
mesh
|
||||
}
|
||||
}
|
||||
846
egui/crates/epaint/src/shape.rs
Normal file
846
egui/crates/epaint/src/shape.rs
Normal file
|
|
@ -0,0 +1,846 @@
|
|||
//! The different shapes that can be painted.
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
text::{FontId, Fonts, Galley},
|
||||
Color32, Mesh, Stroke, TextureId,
|
||||
};
|
||||
use emath::*;
|
||||
|
||||
pub use crate::{CubicBezierShape, QuadraticBezierShape};
|
||||
|
||||
/// A paint primitive such as a circle or a piece of text.
|
||||
/// Coordinates are all screen space points (not physical pixels).
|
||||
///
|
||||
/// You should generally recreate your [`Shape`]s each frame,
|
||||
/// but storing them should also be fine with one exception:
|
||||
/// [`Shape::Text`] depends on the current `pixels_per_point` (dpi scale)
|
||||
/// and so must be recreated every time `pixels_per_point` changes.
|
||||
#[must_use = "Add a Shape to a Painter"]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Shape {
|
||||
/// Paint nothing. This can be useful as a placeholder.
|
||||
Noop,
|
||||
|
||||
/// Recursively nest more shapes - sometimes a convenience to be able to do.
|
||||
/// For performance reasons it is better to avoid it.
|
||||
Vec(Vec<Shape>),
|
||||
|
||||
/// Circle with optional outline and fill.
|
||||
Circle(CircleShape),
|
||||
|
||||
/// A line between two points.
|
||||
LineSegment { points: [Pos2; 2], stroke: Stroke },
|
||||
|
||||
/// A series of lines between points.
|
||||
/// The path can have a stroke and/or fill (if closed).
|
||||
Path(PathShape),
|
||||
|
||||
/// Rectangle with optional outline and fill.
|
||||
Rect(RectShape),
|
||||
|
||||
/// Text.
|
||||
///
|
||||
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
|
||||
Text(TextShape),
|
||||
|
||||
/// A general triangle mesh.
|
||||
///
|
||||
/// Can be used to display images.
|
||||
Mesh(Mesh),
|
||||
|
||||
/// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
|
||||
QuadraticBezier(QuadraticBezierShape),
|
||||
|
||||
/// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve).
|
||||
CubicBezier(CubicBezierShape),
|
||||
|
||||
/// Backend-specific painting.
|
||||
Callback(PaintCallback),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Shape>();
|
||||
}
|
||||
|
||||
impl From<Vec<Shape>> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shapes: Vec<Shape>) -> Self {
|
||||
Self::Vec(shapes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mesh> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(mesh: Mesh) -> Self {
|
||||
Self::Mesh(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Constructors
|
||||
impl Shape {
|
||||
/// A line between two points.
|
||||
/// More efficient than calling [`Self::line`].
|
||||
#[inline]
|
||||
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self {
|
||||
Self::LineSegment {
|
||||
points,
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal line.
|
||||
pub fn hline(x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) -> Self {
|
||||
Shape::LineSegment {
|
||||
points: [pos2(*x.start(), y), pos2(*x.end(), y)],
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertical line.
|
||||
pub fn vline(x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) -> Self {
|
||||
Shape::LineSegment {
|
||||
points: [pos2(x, *y.start()), pos2(x, *y.end())],
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A line through many points.
|
||||
///
|
||||
/// Use [`Self::line_segment`] instead if your line only connects two points.
|
||||
#[inline]
|
||||
pub fn line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Path(PathShape::line(points, stroke))
|
||||
}
|
||||
|
||||
/// A line that closes back to the start point again.
|
||||
#[inline]
|
||||
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Path(PathShape::closed_line(points, stroke))
|
||||
}
|
||||
|
||||
/// Turn a line into equally spaced dots.
|
||||
pub fn dotted_line(
|
||||
path: &[Pos2],
|
||||
color: impl Into<Color32>,
|
||||
spacing: f32,
|
||||
radius: f32,
|
||||
) -> Vec<Self> {
|
||||
let mut shapes = Vec::new();
|
||||
points_from_line(path, spacing, radius, color.into(), &mut shapes);
|
||||
shapes
|
||||
}
|
||||
|
||||
/// Turn a line into dashes.
|
||||
pub fn dashed_line(
|
||||
path: &[Pos2],
|
||||
stroke: impl Into<Stroke>,
|
||||
dash_length: f32,
|
||||
gap_length: f32,
|
||||
) -> Vec<Self> {
|
||||
let mut shapes = Vec::new();
|
||||
dashes_from_line(path, stroke.into(), dash_length, gap_length, &mut shapes);
|
||||
shapes
|
||||
}
|
||||
|
||||
/// Turn a line into dashes. If you need to create many dashed lines use this instead of
|
||||
/// [`Self::dashed_line`]
|
||||
pub fn dashed_line_many(
|
||||
points: &[Pos2],
|
||||
stroke: impl Into<Stroke>,
|
||||
dash_length: f32,
|
||||
gap_length: f32,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
dashes_from_line(points, stroke.into(), dash_length, gap_length, shapes);
|
||||
}
|
||||
|
||||
/// A convex polygon with a fill and optional stroke.
|
||||
///
|
||||
/// The most performant winding order is clockwise.
|
||||
#[inline]
|
||||
pub fn convex_polygon(
|
||||
points: Vec<Pos2>,
|
||||
fill: impl Into<Color32>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) -> Self {
|
||||
Self::Path(PathShape::convex_polygon(points, fill, stroke))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into<Color32>) -> Self {
|
||||
Self::Circle(CircleShape::filled(center, radius, fill_color))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Circle(CircleShape::stroke(center, radius, stroke))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rect_filled(
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
fill_color: impl Into<Color32>,
|
||||
) -> Self {
|
||||
Self::Rect(RectShape::filled(rect, rounding, fill_color))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rect_stroke(
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) -> Self {
|
||||
Self::Rect(RectShape::stroke(rect, rounding, stroke))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn text(
|
||||
fonts: &Fonts,
|
||||
pos: Pos2,
|
||||
anchor: Align2,
|
||||
text: impl ToString,
|
||||
font_id: FontId,
|
||||
color: Color32,
|
||||
) -> Self {
|
||||
let galley = fonts.layout_no_wrap(text.to_string(), font_id, color);
|
||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||
Self::galley(rect.min, galley)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn galley(pos: Pos2, galley: Arc<Galley>) -> Self {
|
||||
TextShape::new(pos, galley).into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// The text color in the [`Galley`] will be replaced with the given color.
|
||||
pub fn galley_with_color(pos: Pos2, galley: Arc<Galley>, text_color: Color32) -> Self {
|
||||
TextShape {
|
||||
override_text_color: Some(text_color),
|
||||
..TextShape::new(pos, galley)
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn mesh(mesh: Mesh) -> Self {
|
||||
crate::epaint_assert!(mesh.is_valid());
|
||||
Self::Mesh(mesh)
|
||||
}
|
||||
|
||||
/// An image at the given position.
|
||||
///
|
||||
/// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
|
||||
/// unless you want to crop or flip the image.
|
||||
///
|
||||
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
|
||||
pub fn image(texture_id: TextureId, rect: Rect, uv: Rect, tint: Color32) -> Self {
|
||||
let mut mesh = Mesh::with_texture(texture_id);
|
||||
mesh.add_rect_with_uv(rect, uv, tint);
|
||||
Shape::mesh(mesh)
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke widths)
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
match self {
|
||||
Self::Noop => Rect::NOTHING,
|
||||
Self::Vec(shapes) => {
|
||||
let mut rect = Rect::NOTHING;
|
||||
for shape in shapes {
|
||||
rect = rect.union(shape.visual_bounding_rect());
|
||||
}
|
||||
rect
|
||||
}
|
||||
Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(),
|
||||
Self::LineSegment { points, stroke } => {
|
||||
if stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
Rect::from_two_pos(points[0], points[1]).expand(stroke.width / 2.0)
|
||||
}
|
||||
}
|
||||
Self::Path(path_shape) => path_shape.visual_bounding_rect(),
|
||||
Self::Rect(rect_shape) => rect_shape.visual_bounding_rect(),
|
||||
Self::Text(text_shape) => text_shape.visual_bounding_rect(),
|
||||
Self::Mesh(mesh) => mesh.calc_bounds(),
|
||||
Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(),
|
||||
Self::CubicBezier(bezier) => bezier.visual_bounding_rect(),
|
||||
Self::Callback(custom) => custom.rect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Inspection and transforms
|
||||
impl Shape {
|
||||
#[inline(always)]
|
||||
pub fn texture_id(&self) -> super::TextureId {
|
||||
if let Shape::Mesh(mesh) = self {
|
||||
mesh.texture_id
|
||||
} else {
|
||||
super::TextureId::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the shape by this many points, in-place.
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
match self {
|
||||
Shape::Noop => {}
|
||||
Shape::Vec(shapes) => {
|
||||
for shape in shapes {
|
||||
shape.translate(delta);
|
||||
}
|
||||
}
|
||||
Shape::Circle(circle_shape) => {
|
||||
circle_shape.center += delta;
|
||||
}
|
||||
Shape::LineSegment { points, .. } => {
|
||||
for p in points {
|
||||
*p += delta;
|
||||
}
|
||||
}
|
||||
Shape::Path(path_shape) => {
|
||||
for p in &mut path_shape.points {
|
||||
*p += delta;
|
||||
}
|
||||
}
|
||||
Shape::Rect(rect_shape) => {
|
||||
rect_shape.rect = rect_shape.rect.translate(delta);
|
||||
}
|
||||
Shape::Text(text_shape) => {
|
||||
text_shape.pos += delta;
|
||||
}
|
||||
Shape::Mesh(mesh) => {
|
||||
mesh.translate(delta);
|
||||
}
|
||||
Shape::QuadraticBezier(bezier_shape) => {
|
||||
bezier_shape.points[0] += delta;
|
||||
bezier_shape.points[1] += delta;
|
||||
bezier_shape.points[2] += delta;
|
||||
}
|
||||
Shape::CubicBezier(cubic_curve) => {
|
||||
for p in &mut cubic_curve.points {
|
||||
*p += delta;
|
||||
}
|
||||
}
|
||||
Shape::Callback(shape) => {
|
||||
shape.rect = shape.rect.translate(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to paint a circle.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct CircleShape {
|
||||
pub center: Pos2,
|
||||
pub radius: f32,
|
||||
pub fill: Color32,
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl CircleShape {
|
||||
#[inline]
|
||||
pub fn filled(center: Pos2, radius: f32, fill_color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
center,
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
|
||||
Self {
|
||||
center,
|
||||
radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
Rect::from_center_size(
|
||||
self.center,
|
||||
Vec2::splat(self.radius * 2.0 + self.stroke.width),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CircleShape> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: CircleShape) -> Self {
|
||||
Self::Circle(shape)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A path which can be stroked and/or filled (if closed).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PathShape {
|
||||
/// Filled paths should prefer clockwise order.
|
||||
pub points: Vec<Pos2>,
|
||||
|
||||
/// If true, connect the first and last of the points together.
|
||||
/// This is required if `fill != TRANSPARENT`.
|
||||
pub closed: bool,
|
||||
|
||||
/// Fill is only supported for convex polygons.
|
||||
pub fill: Color32,
|
||||
|
||||
/// Color and thickness of the line.
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl PathShape {
|
||||
/// A line through many points.
|
||||
///
|
||||
/// Use [`Shape::line_segment`] instead if your line only connects two points.
|
||||
#[inline]
|
||||
pub fn line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
PathShape {
|
||||
points,
|
||||
closed: false,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A line that closes back to the start point again.
|
||||
#[inline]
|
||||
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
PathShape {
|
||||
points,
|
||||
closed: true,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A convex polygon with a fill and optional stroke.
|
||||
///
|
||||
/// The most performant winding order is clockwise.
|
||||
#[inline]
|
||||
pub fn convex_polygon(
|
||||
points: Vec<Pos2>,
|
||||
fill: impl Into<Color32>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) -> Self {
|
||||
PathShape {
|
||||
points,
|
||||
closed: true,
|
||||
fill: fill.into(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
#[inline]
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
Rect::from_points(&self.points).expand(self.stroke.width / 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathShape> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: PathShape) -> Self {
|
||||
Self::Path(shape)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to paint a rectangle.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RectShape {
|
||||
pub rect: Rect,
|
||||
|
||||
/// How rounded the corners are. Use `Rounding::none()` for no rounding.
|
||||
pub rounding: Rounding,
|
||||
|
||||
/// How to fill the rectangle.
|
||||
pub fill: Color32,
|
||||
|
||||
/// The thickness and color of the outline.
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl RectShape {
|
||||
#[inline]
|
||||
pub fn filled(
|
||||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
fill_color: impl Into<Color32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
rounding: rounding.into(),
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stroke(rect: Rect, rounding: impl Into<Rounding>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
rounding: rounding.into(),
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle (includes stroke width)
|
||||
#[inline]
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
|
||||
Rect::NOTHING
|
||||
} else {
|
||||
self.rect.expand(self.stroke.width / 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RectShape> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: RectShape) -> Self {
|
||||
Self::Rect(shape)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
/// How rounded the corners of things should be
|
||||
pub struct Rounding {
|
||||
/// Radius of the rounding of the North-West (left top) corner.
|
||||
pub nw: f32,
|
||||
|
||||
/// Radius of the rounding of the North-East (right top) corner.
|
||||
pub ne: f32,
|
||||
|
||||
/// Radius of the rounding of the South-West (left bottom) corner.
|
||||
pub sw: f32,
|
||||
|
||||
/// Radius of the rounding of the South-East (right bottom) corner.
|
||||
pub se: f32,
|
||||
}
|
||||
|
||||
impl Default for Rounding {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Rounding {
|
||||
#[inline]
|
||||
fn from(radius: f32) -> Self {
|
||||
Self {
|
||||
nw: radius,
|
||||
ne: radius,
|
||||
sw: radius,
|
||||
se: radius,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rounding {
|
||||
#[inline]
|
||||
pub fn same(radius: f32) -> Self {
|
||||
Self {
|
||||
nw: radius,
|
||||
ne: radius,
|
||||
sw: radius,
|
||||
se: radius,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
nw: 0.0,
|
||||
ne: 0.0,
|
||||
sw: 0.0,
|
||||
se: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do all corners have the same rounding?
|
||||
#[inline]
|
||||
pub fn is_same(&self) -> bool {
|
||||
self.nw == self.ne && self.nw == self.sw && self.nw == self.se
|
||||
}
|
||||
|
||||
/// Make sure each corner has a rounding of at least this.
|
||||
#[inline]
|
||||
pub fn at_least(&self, min: f32) -> Self {
|
||||
Self {
|
||||
nw: self.nw.max(min),
|
||||
ne: self.ne.max(min),
|
||||
sw: self.sw.max(min),
|
||||
se: self.se.max(min),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure each corner has a rounding of at most this.
|
||||
#[inline]
|
||||
pub fn at_most(&self, max: f32) -> Self {
|
||||
Self {
|
||||
nw: self.nw.min(max),
|
||||
ne: self.ne.min(max),
|
||||
sw: self.sw.min(max),
|
||||
se: self.se.min(max),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to paint some text on screen.
|
||||
///
|
||||
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TextShape {
|
||||
/// Top left corner of the first character.
|
||||
pub pos: Pos2,
|
||||
|
||||
/// The laid out text, from [`Fonts::layout_job`].
|
||||
pub galley: Arc<Galley>,
|
||||
|
||||
/// Add this underline to the whole text.
|
||||
/// You can also set an underline when creating the galley.
|
||||
pub 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.
|
||||
pub override_text_color: Option<Color32>,
|
||||
|
||||
/// Rotate text by this many radians clockwise.
|
||||
/// The pivot is `pos` (the upper left corner of the text).
|
||||
pub angle: f32,
|
||||
}
|
||||
|
||||
impl TextShape {
|
||||
#[inline]
|
||||
pub fn new(pos: Pos2, galley: Arc<Galley>) -> Self {
|
||||
Self {
|
||||
pos,
|
||||
galley,
|
||||
underline: Stroke::NONE,
|
||||
override_text_color: None,
|
||||
angle: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual bounding rectangle
|
||||
#[inline]
|
||||
pub fn visual_bounding_rect(&self) -> Rect {
|
||||
self.galley.mesh_bounds.translate(self.pos.to_vec2())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextShape> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: TextShape) -> Self {
|
||||
Self::Text(shape)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Creates equally spaced filled circles from a line.
|
||||
fn points_from_line(
|
||||
path: &[Pos2],
|
||||
spacing: f32,
|
||||
radius: f32,
|
||||
color: Color32,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
let mut position_on_segment = 0.0;
|
||||
path.windows(2).for_each(|window| {
|
||||
let (start, end) = (window[0], window[1]);
|
||||
let vector = end - start;
|
||||
let segment_length = vector.length();
|
||||
while position_on_segment < segment_length {
|
||||
let new_point = start + vector * (position_on_segment / segment_length);
|
||||
shapes.push(Shape::circle_filled(new_point, radius, color));
|
||||
position_on_segment += spacing;
|
||||
}
|
||||
position_on_segment -= segment_length;
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates dashes from a line.
|
||||
fn dashes_from_line(
|
||||
path: &[Pos2],
|
||||
stroke: Stroke,
|
||||
dash_length: f32,
|
||||
gap_length: f32,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
let mut position_on_segment = 0.0;
|
||||
let mut drawing_dash = false;
|
||||
path.windows(2).for_each(|window| {
|
||||
let (start, end) = (window[0], window[1]);
|
||||
let vector = end - start;
|
||||
let segment_length = vector.length();
|
||||
|
||||
let mut start_point = start;
|
||||
while position_on_segment < segment_length {
|
||||
let new_point = start + vector * (position_on_segment / segment_length);
|
||||
if drawing_dash {
|
||||
// This is the end point.
|
||||
shapes.push(Shape::line_segment([start_point, new_point], stroke));
|
||||
position_on_segment += gap_length;
|
||||
} else {
|
||||
// Start a new dash.
|
||||
start_point = new_point;
|
||||
position_on_segment += dash_length;
|
||||
}
|
||||
drawing_dash = !drawing_dash;
|
||||
}
|
||||
|
||||
// If the segment ends and the dash is not finished, add the segment's end point.
|
||||
if drawing_dash {
|
||||
shapes.push(Shape::line_segment([start_point, end], stroke));
|
||||
}
|
||||
|
||||
position_on_segment -= segment_length;
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Information passed along with [`PaintCallback`] ([`Shape::Callback`]).
|
||||
pub struct PaintCallbackInfo {
|
||||
/// Viewport in points.
|
||||
///
|
||||
/// This specifies where on the screen to paint, and the borders of this
|
||||
/// Rect is the [-1, +1] of the Normalized Device Coordinates.
|
||||
///
|
||||
/// Note than only a portion of this may be visible due to [`Self::clip_rect`].
|
||||
pub viewport: Rect,
|
||||
|
||||
/// Clip rectangle in points.
|
||||
pub clip_rect: Rect,
|
||||
|
||||
/// Pixels per point.
|
||||
pub pixels_per_point: f32,
|
||||
|
||||
/// Full size of the screen, in pixels.
|
||||
pub screen_size_px: [u32; 2],
|
||||
}
|
||||
|
||||
pub struct ViewportInPixels {
|
||||
/// Physical pixel offset for left side of the viewport.
|
||||
pub left_px: f32,
|
||||
|
||||
/// Physical pixel offset for top side of the viewport.
|
||||
pub top_px: f32,
|
||||
|
||||
/// Physical pixel offset for bottom side of the viewport.
|
||||
///
|
||||
/// This is what `glViewport`, `glScissor` etc expects for the y axis.
|
||||
pub from_bottom_px: f32,
|
||||
|
||||
/// Viewport width in physical pixels.
|
||||
pub width_px: f32,
|
||||
|
||||
/// Viewport height in physical pixels.
|
||||
pub height_px: f32,
|
||||
}
|
||||
|
||||
impl PaintCallbackInfo {
|
||||
fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels {
|
||||
ViewportInPixels {
|
||||
left_px: rect.min.x * self.pixels_per_point,
|
||||
top_px: rect.min.y * self.pixels_per_point,
|
||||
from_bottom_px: self.screen_size_px[1] as f32 - rect.max.y * self.pixels_per_point,
|
||||
width_px: rect.width() * self.pixels_per_point,
|
||||
height_px: rect.height() * self.pixels_per_point,
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewport rectangle. This is what you would use in e.g. `glViewport`.
|
||||
pub fn viewport_in_pixels(&self) -> ViewportInPixels {
|
||||
self.points_to_pixels(&self.viewport)
|
||||
}
|
||||
|
||||
/// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`.
|
||||
pub fn clip_rect_in_pixels(&self) -> ViewportInPixels {
|
||||
self.points_to_pixels(&self.clip_rect)
|
||||
}
|
||||
}
|
||||
|
||||
/// If you want to paint some 3D shapes inside an egui region, you can use this.
|
||||
///
|
||||
/// This is advanced usage, and is backend specific.
|
||||
#[derive(Clone)]
|
||||
pub struct PaintCallback {
|
||||
/// Where to paint.
|
||||
pub rect: Rect,
|
||||
|
||||
/// Paint something custom (e.g. 3D stuff).
|
||||
///
|
||||
/// The concrete value of `callback` depends on the rendering backend used. For instance, the
|
||||
/// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu`
|
||||
/// backend requires a `egui_wgpu::CallbackFn`.
|
||||
///
|
||||
/// If the type cannot be downcast to the type expected by the current backend the callback
|
||||
/// will not be drawn.
|
||||
///
|
||||
/// The rendering backend is responsible for first setting the active viewport to
|
||||
/// [`Self::rect`].
|
||||
///
|
||||
/// The rendering backend is also responsible for restoring any state, such as the bound shader
|
||||
/// program, vertex array, etc.
|
||||
pub callback: Arc<dyn Any + Sync + Send>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PaintCallback {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CustomShape")
|
||||
.field("rect", &self.rect)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq for PaintCallback {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// As I understand it, the problem this clippy is trying to protect against
|
||||
// can only happen if we do dynamic casts back and forth on the pointers, and we don't do that.
|
||||
#[allow(clippy::vtable_address_comparisons)]
|
||||
{
|
||||
self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PaintCallback> for Shape {
|
||||
#[inline(always)]
|
||||
fn from(shape: PaintCallback) -> Self {
|
||||
Self::Callback(shape)
|
||||
}
|
||||
}
|
||||
58
egui/crates/epaint/src/shape_transform.rs
Normal file
58
egui/crates/epaint/src/shape_transform.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
|
||||
#![allow(clippy::match_same_arms)]
|
||||
match shape {
|
||||
Shape::Noop => {}
|
||||
Shape::Vec(shapes) => {
|
||||
for shape in shapes {
|
||||
adjust_colors(shape, adjust_color);
|
||||
}
|
||||
}
|
||||
Shape::Circle(circle_shape) => {
|
||||
adjust_color(&mut circle_shape.fill);
|
||||
adjust_color(&mut circle_shape.stroke.color);
|
||||
}
|
||||
Shape::LineSegment { stroke, .. } => {
|
||||
adjust_color(&mut stroke.color);
|
||||
}
|
||||
Shape::Path(path_shape) => {
|
||||
adjust_color(&mut path_shape.fill);
|
||||
adjust_color(&mut path_shape.stroke.color);
|
||||
}
|
||||
Shape::Rect(rect_shape) => {
|
||||
adjust_color(&mut rect_shape.fill);
|
||||
adjust_color(&mut rect_shape.stroke.color);
|
||||
}
|
||||
Shape::Text(text_shape) => {
|
||||
if let Some(override_text_color) = &mut text_shape.override_text_color {
|
||||
adjust_color(override_text_color);
|
||||
}
|
||||
|
||||
if !text_shape.galley.is_empty() {
|
||||
let galley = std::sync::Arc::make_mut(&mut text_shape.galley);
|
||||
for row in &mut galley.rows {
|
||||
for vertex in &mut row.visuals.mesh.vertices {
|
||||
adjust_color(&mut vertex.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Shape::Mesh(mesh) => {
|
||||
for v in &mut mesh.vertices {
|
||||
adjust_color(&mut v.color);
|
||||
}
|
||||
}
|
||||
Shape::QuadraticBezier(quadratic) => {
|
||||
adjust_color(&mut quadratic.fill);
|
||||
adjust_color(&mut quadratic.stroke.color);
|
||||
}
|
||||
Shape::CubicBezier(bezier) => {
|
||||
adjust_color(&mut bezier.fill);
|
||||
adjust_color(&mut bezier.stroke.color);
|
||||
}
|
||||
Shape::Callback(_) => {
|
||||
// Can't tint user callback code
|
||||
}
|
||||
}
|
||||
}
|
||||
245
egui/crates/epaint/src/stats.rs
Normal file
245
egui/crates/epaint/src/stats.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
//! Collect statistics about what is being painted.
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Size of the elements in a vector/array.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ElementSize {
|
||||
Unknown,
|
||||
Homogeneous(usize),
|
||||
Heterogenous,
|
||||
}
|
||||
|
||||
impl Default for ElementSize {
|
||||
fn default() -> Self {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregate information about a bunch of allocations.
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
pub struct AllocInfo {
|
||||
element_size: ElementSize,
|
||||
num_allocs: usize,
|
||||
num_elements: usize,
|
||||
num_bytes: usize,
|
||||
}
|
||||
|
||||
impl<T> From<&[T]> for AllocInfo {
|
||||
fn from(slice: &[T]) -> Self {
|
||||
Self::from_slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for AllocInfo {
|
||||
type Output = AllocInfo;
|
||||
|
||||
fn add(self, rhs: AllocInfo) -> AllocInfo {
|
||||
use ElementSize::{Heterogenous, Homogeneous, Unknown};
|
||||
let element_size = match (self.element_size, rhs.element_size) {
|
||||
(Heterogenous, _) | (_, Heterogenous) => Heterogenous,
|
||||
(Unknown, other) | (other, Unknown) => other,
|
||||
(Homogeneous(lhs), Homogeneous(rhs)) if lhs == rhs => Homogeneous(lhs),
|
||||
_ => Heterogenous,
|
||||
};
|
||||
|
||||
AllocInfo {
|
||||
element_size,
|
||||
num_allocs: self.num_allocs + rhs.num_allocs,
|
||||
num_elements: self.num_elements + rhs.num_elements,
|
||||
num_bytes: self.num_bytes + rhs.num_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign for AllocInfo {
|
||||
fn add_assign(&mut self, rhs: AllocInfo) {
|
||||
*self = *self + rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for AllocInfo {
|
||||
fn sum<I>(iter: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = Self>,
|
||||
{
|
||||
let mut sum = Self::default();
|
||||
for value in iter {
|
||||
sum += value;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
impl AllocInfo {
|
||||
// pub fn from_shape(shape: &Shape) -> Self {
|
||||
// match shape {
|
||||
// Shape::Noop
|
||||
// Shape::Vec(shapes) => Self::from_shapes(shapes)
|
||||
// | Shape::Circle { .. }
|
||||
// | Shape::LineSegment { .. }
|
||||
// | Shape::Rect { .. } => Self::default(),
|
||||
// Shape::Path { points, .. } => Self::from_slice(points),
|
||||
// Shape::Text { galley, .. } => Self::from_galley(galley),
|
||||
// Shape::Mesh(mesh) => Self::from_mesh(mesh),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn from_galley(galley: &Galley) -> Self {
|
||||
Self::from_slice(galley.text().as_bytes())
|
||||
+ Self::from_slice(&galley.rows)
|
||||
+ galley.rows.iter().map(Self::from_galley_row).sum()
|
||||
}
|
||||
|
||||
fn from_galley_row(row: &crate::text::Row) -> Self {
|
||||
Self::from_mesh(&row.visuals.mesh) + Self::from_slice(&row.glyphs)
|
||||
}
|
||||
|
||||
pub fn from_mesh(mesh: &Mesh) -> Self {
|
||||
Self::from_slice(&mesh.indices) + Self::from_slice(&mesh.vertices)
|
||||
}
|
||||
|
||||
pub fn from_slice<T>(slice: &[T]) -> Self {
|
||||
use std::mem::size_of;
|
||||
let element_size = size_of::<T>();
|
||||
Self {
|
||||
element_size: ElementSize::Homogeneous(element_size),
|
||||
num_allocs: 1,
|
||||
num_elements: slice.len(),
|
||||
num_bytes: slice.len() * element_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_elements(&self) -> usize {
|
||||
assert!(self.element_size != ElementSize::Heterogenous);
|
||||
self.num_elements
|
||||
}
|
||||
|
||||
pub fn num_allocs(&self) -> usize {
|
||||
self.num_allocs
|
||||
}
|
||||
|
||||
pub fn num_bytes(&self) -> usize {
|
||||
self.num_bytes
|
||||
}
|
||||
|
||||
pub fn megabytes(&self) -> String {
|
||||
megabytes(self.num_bytes())
|
||||
}
|
||||
|
||||
pub fn format(&self, what: &str) -> String {
|
||||
if self.num_allocs() == 0 {
|
||||
format!("{:6} {:16}", 0, what)
|
||||
} else if self.num_allocs() == 1 {
|
||||
format!(
|
||||
"{:6} {:16} {} 1 allocation",
|
||||
self.num_elements,
|
||||
what,
|
||||
self.megabytes()
|
||||
)
|
||||
} else if self.element_size != ElementSize::Heterogenous {
|
||||
format!(
|
||||
"{:6} {:16} {} {:3} allocations",
|
||||
self.num_elements(),
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:6} {:16} {} {:3} allocations",
|
||||
"",
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collected allocation statistics for shapes and meshes.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PaintStats {
|
||||
pub shapes: AllocInfo,
|
||||
pub shape_text: AllocInfo,
|
||||
pub shape_path: AllocInfo,
|
||||
pub shape_mesh: AllocInfo,
|
||||
pub shape_vec: AllocInfo,
|
||||
pub num_callbacks: usize,
|
||||
|
||||
pub text_shape_vertices: AllocInfo,
|
||||
pub text_shape_indices: AllocInfo,
|
||||
|
||||
/// Number of separate clip rectangles
|
||||
pub clipped_primitives: AllocInfo,
|
||||
pub vertices: AllocInfo,
|
||||
pub indices: AllocInfo,
|
||||
}
|
||||
|
||||
impl PaintStats {
|
||||
pub fn from_shapes(shapes: &[ClippedShape]) -> Self {
|
||||
let mut stats = Self::default();
|
||||
stats.shape_path.element_size = ElementSize::Heterogenous; // nicer display later
|
||||
stats.shape_vec.element_size = ElementSize::Heterogenous; // nicer display later
|
||||
|
||||
stats.shapes = AllocInfo::from_slice(shapes);
|
||||
for ClippedShape(_, shape) in shapes {
|
||||
stats.add(shape);
|
||||
}
|
||||
stats
|
||||
}
|
||||
|
||||
fn add(&mut self, shape: &Shape) {
|
||||
match shape {
|
||||
Shape::Vec(shapes) => {
|
||||
// self += PaintStats::from_shapes(&shapes); // TODO
|
||||
self.shapes += AllocInfo::from_slice(shapes);
|
||||
self.shape_vec += AllocInfo::from_slice(shapes);
|
||||
for shape in shapes {
|
||||
self.add(shape);
|
||||
}
|
||||
}
|
||||
Shape::Noop
|
||||
| Shape::Circle { .. }
|
||||
| Shape::LineSegment { .. }
|
||||
| Shape::Rect { .. }
|
||||
| Shape::CubicBezier(_)
|
||||
| Shape::QuadraticBezier(_) => {}
|
||||
Shape::Path(path_shape) => {
|
||||
self.shape_path += AllocInfo::from_slice(&path_shape.points);
|
||||
}
|
||||
Shape::Text(text_shape) => {
|
||||
self.shape_text += AllocInfo::from_galley(&text_shape.galley);
|
||||
|
||||
for row in &text_shape.galley.rows {
|
||||
self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices);
|
||||
self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices);
|
||||
}
|
||||
}
|
||||
Shape::Mesh(mesh) => {
|
||||
self.shape_mesh += AllocInfo::from_mesh(mesh);
|
||||
}
|
||||
Shape::Callback(_) => {
|
||||
self.num_callbacks += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_clipped_primitives(
|
||||
mut self,
|
||||
clipped_primitives: &[crate::ClippedPrimitive],
|
||||
) -> Self {
|
||||
self.clipped_primitives += AllocInfo::from_slice(clipped_primitives);
|
||||
for clipped_primitive in clipped_primitives {
|
||||
if let Primitive::Mesh(mesh) = &clipped_primitive.primitive {
|
||||
self.vertices += AllocInfo::from_slice(&mesh.vertices);
|
||||
self.indices += AllocInfo::from_slice(&mesh.indices);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn megabytes(size: usize) -> String {
|
||||
format!("{:.2} MB", size as f64 / 1e6)
|
||||
}
|
||||
60
egui/crates/epaint/src/stroke.rs
Normal file
60
egui/crates/epaint/src/stroke.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Describes the width and color of a line.
|
||||
///
|
||||
/// The default stroke is the same as [`Stroke::none`].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Stroke {
|
||||
pub width: f32,
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
/// Same as [`Stroke::default`].
|
||||
pub const NONE: Stroke = Stroke {
|
||||
width: 0.0,
|
||||
color: Color32::TRANSPARENT,
|
||||
};
|
||||
|
||||
#[deprecated = "Use Stroke::NONE instead"]
|
||||
#[inline(always)]
|
||||
pub fn none() -> Self {
|
||||
Self::new(0.0, Color32::TRANSPARENT)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
color: color.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// True if width is zero or color is transparent
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.width <= 0.0 || self.color == Color32::TRANSPARENT
|
||||
}
|
||||
}
|
||||
|
||||
impl<Color> From<(f32, Color)> for Stroke
|
||||
where
|
||||
Color: Into<Color32>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from((width, color): (f32, Color)) -> Stroke {
|
||||
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);
|
||||
}
|
||||
}
|
||||
1657
egui/crates/epaint/src/tessellator.rs
Normal file
1657
egui/crates/epaint/src/tessellator.rs
Normal file
File diff suppressed because it is too large
Load diff
122
egui/crates/epaint/src/text/cursor.rs
Normal file
122
egui/crates/epaint/src/text/cursor.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! Different types of text cursors, i.e. ways to point into a [`super::Galley`].
|
||||
|
||||
/// Character cursor
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct CCursor {
|
||||
/// Character offset (NOT byte offset!).
|
||||
pub index: usize,
|
||||
|
||||
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||
/// do we prefer the next row?
|
||||
/// This is *almost* always what you want, *except* for when
|
||||
/// explicitly clicking the end of a row or pressing the end key.
|
||||
pub prefer_next_row: bool,
|
||||
}
|
||||
|
||||
impl CCursor {
|
||||
pub fn new(index: usize) -> Self {
|
||||
Self {
|
||||
index,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Two `CCursor`s are considered equal if they refer to the same character boundary,
|
||||
/// even if one prefers the start of the next row.
|
||||
impl PartialEq for CCursor {
|
||||
fn eq(&self, other: &CCursor) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<usize> for CCursor {
|
||||
type Output = CCursor;
|
||||
|
||||
fn add(self, rhs: usize) -> Self::Output {
|
||||
CCursor {
|
||||
index: self.index.saturating_add(rhs),
|
||||
prefer_next_row: self.prefer_next_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub<usize> for CCursor {
|
||||
type Output = CCursor;
|
||||
|
||||
fn sub(self, rhs: usize) -> Self::Output {
|
||||
CCursor {
|
||||
index: self.index.saturating_sub(rhs),
|
||||
prefer_next_row: self.prefer_next_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign<usize> for CCursor {
|
||||
fn add_assign(&mut self, rhs: usize) {
|
||||
self.index = self.index.saturating_add(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::SubAssign<usize> for CCursor {
|
||||
fn sub_assign(&mut self, rhs: usize) {
|
||||
self.index = self.index.saturating_sub(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Row Cursor
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RCursor {
|
||||
/// 0 is first row, and so on.
|
||||
/// Note that a single paragraph can span multiple rows.
|
||||
/// (a paragraph is text separated by `\n`).
|
||||
pub row: usize,
|
||||
|
||||
/// Character based (NOT bytes).
|
||||
/// It is fine if this points to something beyond the end of the current row.
|
||||
/// When moving up/down it may again be within the next row.
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
/// Paragraph Cursor
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PCursor {
|
||||
/// 0 is first paragraph, and so on.
|
||||
/// Note that a single paragraph can span multiple rows.
|
||||
/// (a paragraph is text separated by `\n`).
|
||||
pub paragraph: usize,
|
||||
|
||||
/// Character based (NOT bytes).
|
||||
/// It is fine if this points to something beyond the end of the current paragraph.
|
||||
/// When moving up/down it may again be within the next paragraph.
|
||||
pub offset: usize,
|
||||
|
||||
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||
/// do we prefer the next row?
|
||||
/// This is *almost* always what you want, *except* for when
|
||||
/// explicitly clicking the end of a row or pressing the end key.
|
||||
pub prefer_next_row: bool,
|
||||
}
|
||||
|
||||
/// Two `PCursor`s are considered equal if they refer to the same character boundary,
|
||||
/// even if one prefers the start of the next row.
|
||||
impl PartialEq for PCursor {
|
||||
fn eq(&self, other: &PCursor) -> bool {
|
||||
self.paragraph == other.paragraph && self.offset == other.offset
|
||||
}
|
||||
}
|
||||
|
||||
/// All different types of cursors together.
|
||||
/// They all point to the same place, but in their own different ways.
|
||||
/// pcursor/rcursor can also point to after the end of the paragraph/row.
|
||||
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Cursor {
|
||||
pub ccursor: CCursor,
|
||||
pub rcursor: RCursor,
|
||||
pub pcursor: PCursor,
|
||||
}
|
||||
512
egui/crates/epaint/src/text/font.rs
Normal file
512
egui/crates/epaint/src/text/font.rs
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
use crate::{
|
||||
mutex::{Mutex, RwLock},
|
||||
text::FontTweak,
|
||||
TextureAtlas,
|
||||
};
|
||||
use emath::{vec2, Vec2};
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct UvRect {
|
||||
/// X/Y offset for nice rendering (unit: points).
|
||||
pub offset: Vec2,
|
||||
|
||||
/// Screen size (in points) of this glyph.
|
||||
/// Note that the height is different from the font height.
|
||||
pub size: Vec2,
|
||||
|
||||
/// Top left corner UV in texture.
|
||||
pub min: [u16; 2],
|
||||
|
||||
/// Bottom right corner (exclusive).
|
||||
pub max: [u16; 2],
|
||||
}
|
||||
|
||||
impl UvRect {
|
||||
pub fn is_nothing(&self) -> bool {
|
||||
self.min == self.max
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct GlyphInfo {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
/// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
|
||||
pub(crate) id: ab_glyph::GlyphId,
|
||||
|
||||
/// Unit: points.
|
||||
pub advance_width: f32,
|
||||
|
||||
/// `ascent` value from the font metrics.
|
||||
/// this is the distance from the top to the baseline.
|
||||
///
|
||||
/// Unit: points.
|
||||
pub ascent: f32,
|
||||
|
||||
/// row height computed from the font metrics.
|
||||
///
|
||||
/// Unit: points.
|
||||
pub row_height: f32,
|
||||
|
||||
/// Texture coordinates.
|
||||
pub uv_rect: UvRect,
|
||||
}
|
||||
|
||||
impl Default for GlyphInfo {
|
||||
/// Basically a zero-width space.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: ab_glyph::GlyphId(0),
|
||||
advance_width: 0.0,
|
||||
ascent: 0.0,
|
||||
row_height: 0.0,
|
||||
uv_rect: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A specific font with a size.
|
||||
/// The interface uses points as the unit for everything.
|
||||
pub struct FontImpl {
|
||||
name: String,
|
||||
ab_glyph_font: ab_glyph::FontArc,
|
||||
/// Maximum character height
|
||||
scale_in_pixels: u32,
|
||||
height_in_points: f32,
|
||||
// move each character by this much (hack)
|
||||
y_offset: f32,
|
||||
ascent: f32,
|
||||
pixels_per_point: f32,
|
||||
glyph_info_cache: RwLock<ahash::HashMap<char, GlyphInfo>>, // TODO(emilk): standard Mutex
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
}
|
||||
|
||||
impl FontImpl {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
name: String,
|
||||
ab_glyph_font: ab_glyph::FontArc,
|
||||
scale_in_pixels: f32,
|
||||
tweak: FontTweak,
|
||||
) -> FontImpl {
|
||||
assert!(scale_in_pixels > 0.0);
|
||||
assert!(pixels_per_point > 0.0);
|
||||
|
||||
use ab_glyph::*;
|
||||
let scaled = ab_glyph_font.as_scaled(scale_in_pixels);
|
||||
let ascent = scaled.ascent() / pixels_per_point;
|
||||
let descent = scaled.descent() / pixels_per_point;
|
||||
let line_gap = scaled.line_gap() / pixels_per_point;
|
||||
|
||||
// Tweak the scale as the user desired
|
||||
let scale_in_pixels = scale_in_pixels * tweak.scale;
|
||||
|
||||
let baseline_offset = {
|
||||
let scale_in_points = scale_in_pixels / pixels_per_point;
|
||||
scale_in_points * tweak.baseline_offset_factor
|
||||
};
|
||||
|
||||
let y_offset_points = {
|
||||
let scale_in_points = scale_in_pixels / pixels_per_point;
|
||||
scale_in_points * tweak.y_offset_factor
|
||||
} + tweak.y_offset;
|
||||
|
||||
// center scaled glyphs properly
|
||||
let y_offset_points = y_offset_points + (tweak.scale - 1.0) * 0.5 * (ascent + descent);
|
||||
|
||||
// Round to an even number of physical pixels to get even kerning.
|
||||
// See https://github.com/emilk/egui/issues/382
|
||||
let scale_in_pixels = scale_in_pixels.round() as u32;
|
||||
|
||||
// Round to closest pixel:
|
||||
let y_offset = (y_offset_points * pixels_per_point).round() / pixels_per_point;
|
||||
|
||||
Self {
|
||||
name,
|
||||
ab_glyph_font,
|
||||
scale_in_pixels,
|
||||
height_in_points: ascent - descent + line_gap,
|
||||
y_offset,
|
||||
ascent: ascent + baseline_offset,
|
||||
pixels_per_point,
|
||||
glyph_info_cache: Default::default(),
|
||||
atlas,
|
||||
}
|
||||
}
|
||||
|
||||
/// Code points that will always be replaced by the replacement character.
|
||||
///
|
||||
/// See also [`invisible_char`].
|
||||
fn ignore_character(&self, chr: char) -> bool {
|
||||
if self.name == "emoji-icon-font" {
|
||||
// HACK: https://github.com/emilk/egui/issues/1284 https://github.com/jslegers/emoji-icon-font/issues/18
|
||||
// Don't show the wrong fullwidth capital letters:
|
||||
if 'S' <= chr && chr <= 'Y' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
matches!(
|
||||
chr,
|
||||
// Strip out a religious symbol with secondary nefarious interpretation:
|
||||
'\u{534d}' | '\u{5350}' |
|
||||
|
||||
// Ignore ubuntu-specific stuff in `Ubuntu-Light.ttf`:
|
||||
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
|
||||
)
|
||||
}
|
||||
|
||||
/// An un-ordered iterator over all supported characters.
|
||||
fn characters(&self) -> impl Iterator<Item = char> + '_ {
|
||||
use ab_glyph::Font as _;
|
||||
self.ab_glyph_font
|
||||
.codepoint_ids()
|
||||
.map(|(_, chr)| chr)
|
||||
.filter(|&chr| !self.ignore_character(chr))
|
||||
}
|
||||
|
||||
/// `\n` will result in `None`
|
||||
fn glyph_info(&self, c: char) -> Option<GlyphInfo> {
|
||||
{
|
||||
if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
|
||||
return Some(*glyph_info);
|
||||
}
|
||||
}
|
||||
|
||||
if self.ignore_character(c) {
|
||||
return None; // these will result in the replacement character when rendering
|
||||
}
|
||||
|
||||
if c == '\t' {
|
||||
if let Some(space) = self.glyph_info(' ') {
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
|
||||
..GlyphInfo::default()
|
||||
};
|
||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
}
|
||||
|
||||
if c == '\u{2009}' {
|
||||
// Thin space, often used as thousands deliminator: 1 234 567 890
|
||||
// https://www.compart.com/en/unicode/U+2009
|
||||
// https://en.wikipedia.org/wiki/Thin_space
|
||||
|
||||
if let Some(space) = self.glyph_info(' ') {
|
||||
let em = self.height_in_points; // TODO(emilk): is this right?
|
||||
let advance_width = f32::min(em / 6.0, space.advance_width * 0.5);
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width,
|
||||
..GlyphInfo::default()
|
||||
};
|
||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
}
|
||||
|
||||
if invisible_char(c) {
|
||||
let glyph_info = GlyphInfo::default();
|
||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
// Add new character:
|
||||
use ab_glyph::Font as _;
|
||||
let glyph_id = self.ab_glyph_font.glyph_id(c);
|
||||
|
||||
if glyph_id.0 == 0 {
|
||||
None // unsupported character
|
||||
} else {
|
||||
let glyph_info = self.allocate_glyph(glyph_id);
|
||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||
Some(glyph_info)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pair_kerning(
|
||||
&self,
|
||||
last_glyph_id: ab_glyph::GlyphId,
|
||||
glyph_id: ab_glyph::GlyphId,
|
||||
) -> f32 {
|
||||
use ab_glyph::{Font as _, ScaleFont};
|
||||
self.ab_glyph_font
|
||||
.as_scaled(self.scale_in_pixels as f32)
|
||||
.kern(last_glyph_id, glyph_id)
|
||||
/ self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Height of one row of text. In points
|
||||
#[inline(always)]
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.height_in_points
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.pixels_per_point
|
||||
}
|
||||
|
||||
fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
|
||||
assert!(glyph_id.0 != 0);
|
||||
use ab_glyph::{Font as _, ScaleFont};
|
||||
|
||||
let glyph = glyph_id.with_scale_and_position(
|
||||
self.scale_in_pixels as f32,
|
||||
ab_glyph::Point { x: 0.0, y: 0.0 },
|
||||
);
|
||||
|
||||
let uv_rect = self.ab_glyph_font.outline_glyph(glyph).map(|glyph| {
|
||||
let bb = glyph.px_bounds();
|
||||
let glyph_width = bb.width() as usize;
|
||||
let glyph_height = bb.height() as usize;
|
||||
if glyph_width == 0 || glyph_height == 0 {
|
||||
UvRect::default()
|
||||
} else {
|
||||
let atlas = &mut self.atlas.lock();
|
||||
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
|
||||
glyph.draw(|x, y, v| {
|
||||
if v > 0.0 {
|
||||
let px = glyph_pos.0 + x as usize;
|
||||
let py = glyph_pos.1 + y as usize;
|
||||
image[(px, py)] = v;
|
||||
}
|
||||
});
|
||||
|
||||
let offset_in_pixels = vec2(bb.min.x, bb.min.y);
|
||||
let offset = offset_in_pixels / self.pixels_per_point + self.y_offset * Vec2::Y;
|
||||
UvRect {
|
||||
offset,
|
||||
size: vec2(glyph_width as f32, glyph_height as f32) / self.pixels_per_point,
|
||||
min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
|
||||
max: [
|
||||
(glyph_pos.0 + glyph_width) as u16,
|
||||
(glyph_pos.1 + glyph_height) as u16,
|
||||
],
|
||||
}
|
||||
}
|
||||
});
|
||||
let uv_rect = uv_rect.unwrap_or_default();
|
||||
|
||||
let advance_width_in_points = self
|
||||
.ab_glyph_font
|
||||
.as_scaled(self.scale_in_pixels as f32)
|
||||
.h_advance(glyph_id)
|
||||
/ self.pixels_per_point;
|
||||
|
||||
GlyphInfo {
|
||||
id: glyph_id,
|
||||
advance_width: advance_width_in_points,
|
||||
ascent: self.ascent,
|
||||
row_height: self.row_height(),
|
||||
uv_rect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FontIndex = usize;
|
||||
|
||||
// TODO(emilk): rename?
|
||||
/// Wrapper over multiple [`FontImpl`] (e.g. a primary + fallbacks for emojis)
|
||||
pub struct Font {
|
||||
fonts: Vec<Arc<FontImpl>>,
|
||||
/// Lazily calculated.
|
||||
characters: Option<BTreeSet<char>>,
|
||||
replacement_glyph: (FontIndex, GlyphInfo),
|
||||
pixels_per_point: f32,
|
||||
row_height: f32,
|
||||
glyph_info_cache: ahash::HashMap<char, (FontIndex, GlyphInfo)>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
|
||||
if fonts.is_empty() {
|
||||
return Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: Default::default(),
|
||||
pixels_per_point: 1.0,
|
||||
row_height: 0.0,
|
||||
glyph_info_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
let pixels_per_point = fonts[0].pixels_per_point();
|
||||
let row_height = fonts[0].row_height();
|
||||
|
||||
let mut slf = Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: Default::default(),
|
||||
pixels_per_point,
|
||||
row_height,
|
||||
glyph_info_cache: Default::default(),
|
||||
};
|
||||
|
||||
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
|
||||
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
|
||||
|
||||
let replacement_glyph = slf
|
||||
.glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR)
|
||||
.or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR))
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Failed to find replacement characters {:?} or {:?}",
|
||||
PRIMARY_REPLACEMENT_CHAR, FALLBACK_REPLACEMENT_CHAR
|
||||
)
|
||||
});
|
||||
slf.replacement_glyph = replacement_glyph;
|
||||
|
||||
slf
|
||||
}
|
||||
|
||||
pub fn preload_characters(&mut self, s: &str) {
|
||||
for c in s.chars() {
|
||||
self.glyph_info(c);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preload_common_characters(&mut self) {
|
||||
// Preload the printable ASCII characters [32, 126] (which excludes control codes):
|
||||
const FIRST_ASCII: usize = 32; // 32 == space
|
||||
const LAST_ASCII: usize = 126;
|
||||
for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
|
||||
self.glyph_info(c);
|
||||
}
|
||||
self.glyph_info('°');
|
||||
self.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR);
|
||||
}
|
||||
|
||||
/// All supported characters.
|
||||
pub fn characters(&mut self) -> &BTreeSet<char> {
|
||||
self.characters.get_or_insert_with(|| {
|
||||
let mut characters = BTreeSet::new();
|
||||
for font in &self.fonts {
|
||||
characters.extend(font.characters());
|
||||
}
|
||||
characters
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Height of one row of text. In points
|
||||
#[inline(always)]
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.row_height
|
||||
}
|
||||
|
||||
pub fn uv_rect(&self, c: char) -> UvRect {
|
||||
self.glyph_info_cache
|
||||
.get(&c)
|
||||
.map(|gi| gi.1.uv_rect)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
pub fn glyph_width(&mut self, c: char) -> f32 {
|
||||
self.glyph_info(c).1.advance_width
|
||||
}
|
||||
|
||||
/// Can we display this glyph?
|
||||
pub fn has_glyph(&mut self, c: char) -> bool {
|
||||
self.glyph_info(c) != self.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦♂️
|
||||
}
|
||||
|
||||
/// Can we display all the glyphs in this text?
|
||||
pub fn has_glyphs(&mut self, s: &str) -> bool {
|
||||
s.chars().all(|c| self.has_glyph(c))
|
||||
}
|
||||
|
||||
/// `\n` will (intentionally) show up as the replacement character.
|
||||
fn glyph_info(&mut self, c: char) -> (FontIndex, GlyphInfo) {
|
||||
if let Some(font_index_glyph_info) = self.glyph_info_cache.get(&c) {
|
||||
return *font_index_glyph_info;
|
||||
}
|
||||
|
||||
let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c);
|
||||
let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph);
|
||||
self.glyph_info_cache.insert(c, font_index_glyph_info);
|
||||
font_index_glyph_info
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn glyph_info_and_font_impl(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) {
|
||||
if self.fonts.is_empty() {
|
||||
return (None, self.replacement_glyph.1);
|
||||
}
|
||||
let (font_index, glyph_info) = self.glyph_info(c);
|
||||
let font_impl = &self.fonts[font_index];
|
||||
(Some(font_impl), glyph_info)
|
||||
}
|
||||
|
||||
fn glyph_info_no_cache_or_fallback(&mut self, c: char) -> Option<(FontIndex, GlyphInfo)> {
|
||||
for (font_index, font_impl) in self.fonts.iter().enumerate() {
|
||||
if let Some(glyph_info) = font_impl.glyph_info(c) {
|
||||
self.glyph_info_cache.insert(c, (font_index, glyph_info));
|
||||
return Some((font_index, glyph_info));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Code points that will always be invisible (zero width).
|
||||
///
|
||||
/// See also [`FontImpl::ignore_character`].
|
||||
#[inline]
|
||||
fn invisible_char(c: char) -> bool {
|
||||
if c == '\r' {
|
||||
// A character most vile and pernicious. Don't display it.
|
||||
return true;
|
||||
}
|
||||
|
||||
// See https://github.com/emilk/egui/issues/336
|
||||
|
||||
// From https://www.fileformat.info/info/unicode/category/Cf/list.htm
|
||||
|
||||
// TODO(emilk): heed bidi characters
|
||||
|
||||
matches!(
|
||||
c,
|
||||
'\u{200B}' // ZERO WIDTH SPACE
|
||||
| '\u{200C}' // ZERO WIDTH NON-JOINER
|
||||
| '\u{200D}' // ZERO WIDTH JOINER
|
||||
| '\u{200E}' // LEFT-TO-RIGHT MARK
|
||||
| '\u{200F}' // RIGHT-TO-LEFT MARK
|
||||
| '\u{202A}' // LEFT-TO-RIGHT EMBEDDING
|
||||
| '\u{202B}' // RIGHT-TO-LEFT EMBEDDING
|
||||
| '\u{202C}' // POP DIRECTIONAL FORMATTING
|
||||
| '\u{202D}' // LEFT-TO-RIGHT OVERRIDE
|
||||
| '\u{202E}' // RIGHT-TO-LEFT OVERRIDE
|
||||
| '\u{2060}' // WORD JOINER
|
||||
| '\u{2061}' // FUNCTION APPLICATION
|
||||
| '\u{2062}' // INVISIBLE TIMES
|
||||
| '\u{2063}' // INVISIBLE SEPARATOR
|
||||
| '\u{2064}' // INVISIBLE PLUS
|
||||
| '\u{2066}' // LEFT-TO-RIGHT ISOLATE
|
||||
| '\u{2067}' // RIGHT-TO-LEFT ISOLATE
|
||||
| '\u{2068}' // FIRST STRONG ISOLATE
|
||||
| '\u{2069}' // POP DIRECTIONAL ISOLATE
|
||||
| '\u{206A}' // INHIBIT SYMMETRIC SWAPPING
|
||||
| '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING
|
||||
| '\u{206C}' // INHIBIT ARABIC FORM SHAPING
|
||||
| '\u{206D}' // ACTIVATE ARABIC FORM SHAPING
|
||||
| '\u{206E}' // NATIONAL DIGIT SHAPES
|
||||
| '\u{206F}' // NOMINAL DIGIT SHAPES
|
||||
| '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
|
||||
)
|
||||
}
|
||||
793
egui/crates/epaint/src/text/fonts.rs
Normal file
793
egui/crates/epaint/src/text/fonts.rs
Normal file
|
|
@ -0,0 +1,793 @@
|
|||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
mutex::{Mutex, MutexGuard},
|
||||
text::{
|
||||
font::{Font, FontImpl},
|
||||
Galley, LayoutJob,
|
||||
},
|
||||
TextureAtlas,
|
||||
};
|
||||
use emath::NumExt as _;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to select a sized font.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontId {
|
||||
/// Height in points.
|
||||
pub size: f32,
|
||||
|
||||
/// What font family to use.
|
||||
pub family: FontFamily,
|
||||
// TODO(emilk): weight (bold), italics, …
|
||||
}
|
||||
|
||||
impl Default for FontId {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: 14.0,
|
||||
family: FontFamily::Proportional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontId {
|
||||
#[inline]
|
||||
pub const fn new(size: f32, family: FontFamily) -> Self {
|
||||
Self { size, family }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn proportional(size: f32) -> Self {
|
||||
Self::new(size, FontFamily::Proportional)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn monospace(size: f32) -> Self {
|
||||
Self::new(size, FontFamily::Monospace)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl std::hash::Hash for FontId {
|
||||
#[inline(always)]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let Self { size, family } = self;
|
||||
crate::f32_hash(state, *size);
|
||||
family.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Font of unknown size.
|
||||
///
|
||||
/// Which style of font: [`Monospace`][`FontFamily::Monospace`], [`Proportional`][`FontFamily::Proportional`],
|
||||
/// or by user-chosen name.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum FontFamily {
|
||||
/// A font where some characters are wider than other (e.g. 'w' is wider than 'i').
|
||||
///
|
||||
/// Proportional fonts are easier to read and should be the preferred choice in most situations.
|
||||
Proportional,
|
||||
|
||||
/// A font where each character is the same width (`w` is the same width as `i`).
|
||||
///
|
||||
/// Useful for code snippets, or when you need to align numbers or text.
|
||||
Monospace,
|
||||
|
||||
/// One of the names in [`FontDefinitions::families`].
|
||||
///
|
||||
/// ```
|
||||
/// # use epaint::FontFamily;
|
||||
/// // User-chosen names:
|
||||
/// FontFamily::Name("arial".into());
|
||||
/// FontFamily::Name("serif".into());
|
||||
/// ```
|
||||
Name(Arc<str>),
|
||||
}
|
||||
|
||||
impl Default for FontFamily {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
FontFamily::Proportional
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FontFamily {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Monospace => "Monospace".fmt(f),
|
||||
Self::Proportional => "Proportional".fmt(f),
|
||||
Self::Name(name) => (*name).fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A `.ttf` or `.otf` file and a font face index.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontData {
|
||||
/// The content of a `.ttf` or `.otf` file.
|
||||
pub font: std::borrow::Cow<'static, [u8]>,
|
||||
|
||||
/// Which font face in the file to use.
|
||||
/// When in doubt, use `0`.
|
||||
pub index: u32,
|
||||
|
||||
/// Extra scale and vertical tweak to apply to all text of this font.
|
||||
pub tweak: FontTweak,
|
||||
}
|
||||
|
||||
impl FontData {
|
||||
pub fn from_static(font: &'static [u8]) -> Self {
|
||||
Self {
|
||||
font: std::borrow::Cow::Borrowed(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_owned(font: Vec<u8>) -> Self {
|
||||
Self {
|
||||
font: std::borrow::Cow::Owned(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tweak(self, tweak: FontTweak) -> Self {
|
||||
Self { tweak, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Extra scale and vertical tweak to apply to all text of a certain font.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontTweak {
|
||||
/// Scale the font's glyphs by this much.
|
||||
/// this is only a visual effect and does not affect the text layout.
|
||||
///
|
||||
/// Default: `1.0` (no scaling).
|
||||
pub scale: f32,
|
||||
|
||||
/// Shift font's glyphs downwards by this fraction of the font size (in points).
|
||||
/// this is only a visual effect and does not affect the text layout.
|
||||
///
|
||||
/// A positive value shifts the text downwards.
|
||||
/// A negative value shifts it upwards.
|
||||
///
|
||||
/// Example value: `-0.2`.
|
||||
pub y_offset_factor: f32,
|
||||
|
||||
/// Shift font's glyphs downwards by this amount of logical points.
|
||||
/// this is only a visual effect and does not affect the text layout.
|
||||
///
|
||||
/// Example value: `2.0`.
|
||||
pub y_offset: f32,
|
||||
|
||||
/// When using this font's metrics to layout a row,
|
||||
/// shift the entire row downwards by this fraction of the font size (in points).
|
||||
///
|
||||
/// A positive value shifts the text downwards.
|
||||
/// A negative value shifts it upwards.
|
||||
pub baseline_offset_factor: f32,
|
||||
}
|
||||
|
||||
impl Default for FontTweak {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: 1.0,
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: -0.0333, // makes the default fonts look more centered in buttons and such
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc {
|
||||
match &data.font {
|
||||
std::borrow::Cow::Borrowed(bytes) => {
|
||||
ab_glyph::FontRef::try_from_slice_and_index(bytes, data.index)
|
||||
.map(ab_glyph::FontArc::from)
|
||||
}
|
||||
std::borrow::Cow::Owned(bytes) => {
|
||||
ab_glyph::FontVec::try_from_vec_and_index(bytes.clone(), data.index)
|
||||
.map(ab_glyph::FontArc::from)
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|err| panic!("Error parsing {:?} TTF/OTF font file: {}", name, err))
|
||||
}
|
||||
|
||||
/// Describes the font data and the sizes to use.
|
||||
///
|
||||
/// Often you would start with [`FontDefinitions::default()`] and then add/change the contents.
|
||||
///
|
||||
/// This is how you install your own custom fonts:
|
||||
/// ```
|
||||
/// # use {epaint::text::{FontDefinitions, FontFamily, FontData}};
|
||||
/// # struct FakeEguiCtx {};
|
||||
/// # impl FakeEguiCtx { fn set_fonts(&self, _: FontDefinitions) {} }
|
||||
/// # let egui_ctx = FakeEguiCtx {};
|
||||
/// let mut fonts = FontDefinitions::default();
|
||||
///
|
||||
/// // Install my own font (maybe supporting non-latin characters):
|
||||
/// fonts.font_data.insert("my_font".to_owned(),
|
||||
/// FontData::from_static(include_bytes!("../../fonts/Ubuntu-Light.ttf"))); // .ttf and .otf supported
|
||||
///
|
||||
/// // Put my font first (highest priority):
|
||||
/// fonts.families.get_mut(&FontFamily::Proportional).unwrap()
|
||||
/// .insert(0, "my_font".to_owned());
|
||||
///
|
||||
/// // Put my font as last fallback for monospace:
|
||||
/// fonts.families.get_mut(&FontFamily::Monospace).unwrap()
|
||||
/// .push("my_font".to_owned());
|
||||
///
|
||||
/// egui_ctx.set_fonts(fonts);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct FontDefinitions {
|
||||
/// List of font names and their definitions.
|
||||
///
|
||||
/// `epaint` has built-in-default for these, but you can override them if you like.
|
||||
pub font_data: BTreeMap<String, FontData>,
|
||||
|
||||
/// Which fonts (names) to use for each [`FontFamily`].
|
||||
///
|
||||
/// The list should be a list of keys into [`Self::font_data`].
|
||||
/// When looking for a character glyph `epaint` will start with
|
||||
/// the first font and then move to the second, and so on.
|
||||
/// So the first font is the primary, and then comes a list of fallbacks in order of priority.
|
||||
pub families: BTreeMap<FontFamily, Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for FontDefinitions {
|
||||
/// Specifies the default fonts if the feature `default_fonts` is enabled,
|
||||
/// otherwise this is the same as [`Self::empty`].
|
||||
#[cfg(not(feature = "default_fonts"))]
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
|
||||
/// Specifies the default fonts if the feature `default_fonts` is enabled,
|
||||
/// otherwise this is the same as [`Self::empty`].
|
||||
#[cfg(feature = "default_fonts")]
|
||||
fn default() -> Self {
|
||||
let mut font_data: BTreeMap<String, FontData> = BTreeMap::new();
|
||||
|
||||
let mut families = BTreeMap::new();
|
||||
|
||||
font_data.insert(
|
||||
"Hack".to_owned(),
|
||||
FontData::from_static(include_bytes!("../../fonts/Hack-Regular.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"Ubuntu-Light".to_owned(),
|
||||
FontData::from_static(include_bytes!("../../fonts/Ubuntu-Light.ttf")),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
FontData::from_static(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")).tweak(
|
||||
FontTweak {
|
||||
scale: 0.81, // make it smaller
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>:
|
||||
font_data.insert(
|
||||
"emoji-icon-font".to_owned(),
|
||||
FontData::from_static(include_bytes!("../../fonts/emoji-icon-font.ttf")).tweak(
|
||||
FontTweak {
|
||||
scale: 0.88, // make it smaller
|
||||
|
||||
// probably not correct, but this does make texts look better (#2724 for details)
|
||||
y_offset_factor: 0.11, // move glyphs down to better align with common fonts
|
||||
baseline_offset_factor: -0.11, // ...now the entire row is a bit down so shift it back
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
families.insert(
|
||||
FontFamily::Monospace,
|
||||
vec![
|
||||
"Hack".to_owned(),
|
||||
"Ubuntu-Light".to_owned(), // fallback for √ etc
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
"emoji-icon-font".to_owned(),
|
||||
],
|
||||
);
|
||||
families.insert(
|
||||
FontFamily::Proportional,
|
||||
vec![
|
||||
"Ubuntu-Light".to_owned(),
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
"emoji-icon-font".to_owned(),
|
||||
],
|
||||
);
|
||||
|
||||
Self {
|
||||
font_data,
|
||||
families,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontDefinitions {
|
||||
/// No fonts.
|
||||
pub fn empty() -> Self {
|
||||
let mut families = BTreeMap::new();
|
||||
families.insert(FontFamily::Monospace, vec![]);
|
||||
families.insert(FontFamily::Proportional, vec![]);
|
||||
|
||||
Self {
|
||||
font_data: Default::default(),
|
||||
families,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The collection of fonts used by `epaint`.
|
||||
///
|
||||
/// Required in order to paint text. Create one and reuse. Cheap to clone.
|
||||
///
|
||||
/// Each [`Fonts`] comes with a font atlas textures that needs to be used when painting.
|
||||
///
|
||||
/// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`.
|
||||
///
|
||||
/// You need to call [`Self::begin_frame`] and [`Self::font_image_delta`] once every frame.
|
||||
pub struct Fonts(Arc<Mutex<FontsAndCache>>);
|
||||
|
||||
impl Fonts {
|
||||
/// Create a new [`Fonts`] for text layout.
|
||||
/// This call is expensive, so only create one [`Fonts`] and then reuse it.
|
||||
///
|
||||
/// * `pixels_per_point`: how many physical pixels per logical "point".
|
||||
/// * `max_texture_side`: largest supported texture size (one side).
|
||||
pub fn new(
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
let fonts_and_cache = FontsAndCache {
|
||||
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions),
|
||||
galley_cache: Default::default(),
|
||||
};
|
||||
Self(Arc::new(Mutex::new(fonts_and_cache)))
|
||||
}
|
||||
|
||||
/// Call at the start of each frame with the latest known
|
||||
/// `pixels_per_point` and `max_texture_side`.
|
||||
///
|
||||
/// Call after painting the previous frame, but before using [`Fonts`] for the new frame.
|
||||
///
|
||||
/// This function will react to changes in `pixels_per_point` and `max_texture_side`,
|
||||
/// as well as notice when the font atlas is getting full, and handle that.
|
||||
pub fn begin_frame(&self, pixels_per_point: f32, max_texture_side: usize) {
|
||||
let mut fonts_and_cache = self.0.lock();
|
||||
|
||||
let pixels_per_point_changed =
|
||||
(fonts_and_cache.fonts.pixels_per_point - pixels_per_point).abs() > 1e-3;
|
||||
let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
|
||||
let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
|
||||
let needs_recreate =
|
||||
pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full;
|
||||
|
||||
if needs_recreate {
|
||||
let definitions = fonts_and_cache.fonts.definitions.clone();
|
||||
|
||||
*fonts_and_cache = FontsAndCache {
|
||||
fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions),
|
||||
galley_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
fonts_and_cache.galley_cache.flush_cache();
|
||||
}
|
||||
|
||||
/// Call at the end of each frame (before painting) to get the change to the font texture since last call.
|
||||
pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
|
||||
self.lock().fonts.atlas.lock().take_delta()
|
||||
}
|
||||
|
||||
/// Access the underlying [`FontsAndCache`].
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
pub fn lock(&self) -> MutexGuard<'_, FontsAndCache> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.lock().fonts.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn max_texture_side(&self) -> usize {
|
||||
self.lock().fonts.max_texture_side
|
||||
}
|
||||
|
||||
/// The font atlas.
|
||||
/// Pass this to [`crate::Tessellator`].
|
||||
pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
|
||||
self.lock().fonts.atlas.clone()
|
||||
}
|
||||
|
||||
/// Current size of the font image.
|
||||
/// Pass this to [`crate::Tessellator`].
|
||||
pub fn font_image_size(&self) -> [usize; 2] {
|
||||
self.lock().fonts.atlas.lock().size()
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
#[inline]
|
||||
pub fn glyph_width(&self, font_id: &FontId, c: char) -> f32 {
|
||||
self.lock().fonts.glyph_width(font_id, c)
|
||||
}
|
||||
|
||||
/// Can we display this glyph?
|
||||
#[inline]
|
||||
pub fn has_glyph(&self, font_id: &FontId, c: char) -> bool {
|
||||
self.lock().fonts.has_glyph(font_id, c)
|
||||
}
|
||||
|
||||
/// Can we display all the glyphs in this text?
|
||||
pub fn has_glyphs(&self, font_id: &FontId, s: &str) -> bool {
|
||||
self.lock().fonts.has_glyphs(font_id, s)
|
||||
}
|
||||
|
||||
/// Height of one row of text in points
|
||||
#[inline]
|
||||
pub fn row_height(&self, font_id: &FontId) -> f32 {
|
||||
self.lock().fonts.row_height(font_id)
|
||||
}
|
||||
|
||||
/// List of all known font families.
|
||||
pub fn families(&self) -> Vec<FontFamily> {
|
||||
self.lock()
|
||||
.fonts
|
||||
.definitions
|
||||
.families
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Layout some text.
|
||||
///
|
||||
/// This is the most advanced layout function.
|
||||
/// See also [`Self::layout`], [`Self::layout_no_wrap`] and
|
||||
/// [`Self::layout_delayed_color`].
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
#[inline]
|
||||
pub fn layout_job(&self, job: LayoutJob) -> Arc<Galley> {
|
||||
self.lock().layout_job(job)
|
||||
}
|
||||
|
||||
pub fn num_galleys_in_cache(&self) -> usize {
|
||||
self.lock().galley_cache.num_galleys_in_cache()
|
||||
}
|
||||
|
||||
/// How full is the font atlas?
|
||||
///
|
||||
/// This increases as new fonts and/or glyphs are used,
|
||||
/// but can also decrease in a call to [`Self::begin_frame`].
|
||||
pub fn font_atlas_fill_ratio(&self) -> f32 {
|
||||
self.lock().fonts.atlas.lock().fill_ratio()
|
||||
}
|
||||
|
||||
/// Will wrap text at the given width and line break at `\n`.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
pub fn layout(
|
||||
&self,
|
||||
text: String,
|
||||
font_id: FontId,
|
||||
color: crate::Color32,
|
||||
wrap_width: f32,
|
||||
) -> Arc<Galley> {
|
||||
let job = LayoutJob::simple(text, font_id, color, wrap_width);
|
||||
self.layout_job(job)
|
||||
}
|
||||
|
||||
/// Will line break at `\n`.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
pub fn layout_no_wrap(
|
||||
&self,
|
||||
text: String,
|
||||
font_id: FontId,
|
||||
color: crate::Color32,
|
||||
) -> Arc<Galley> {
|
||||
let job = LayoutJob::simple(text, font_id, color, f32::INFINITY);
|
||||
self.layout_job(job)
|
||||
}
|
||||
|
||||
/// Like [`Self::layout`], made for when you want to pick a color for the text later.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
pub fn layout_delayed_color(
|
||||
&self,
|
||||
text: String,
|
||||
font_id: FontId,
|
||||
wrap_width: f32,
|
||||
) -> Arc<Galley> {
|
||||
self.layout_job(LayoutJob::simple(
|
||||
text,
|
||||
font_id,
|
||||
crate::Color32::TEMPORARY_COLOR,
|
||||
wrap_width,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct FontsAndCache {
|
||||
pub fonts: FontsImpl,
|
||||
galley_cache: GalleyCache,
|
||||
}
|
||||
|
||||
impl FontsAndCache {
|
||||
fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
|
||||
self.galley_cache.layout(&mut self.fonts, job)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct HashableF32(f32);
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl std::hash::Hash for HashableF32 {
|
||||
#[inline(always)]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
crate::f32_hash(state, self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for HashableF32 {}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The collection of fonts used by `epaint`.
|
||||
///
|
||||
/// Required in order to paint text.
|
||||
pub struct FontsImpl {
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
definitions: FontDefinitions,
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
font_impl_cache: FontImplCache,
|
||||
sized_family: ahash::HashMap<(HashableF32, FontFamily), Font>,
|
||||
}
|
||||
|
||||
impl FontsImpl {
|
||||
/// Create a new [`FontsImpl`] for text layout.
|
||||
/// This call is expensive, so only create one [`FontsImpl`] and then reuse it.
|
||||
pub fn new(
|
||||
pixels_per_point: f32,
|
||||
max_texture_side: usize,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
assert!(
|
||||
0.0 < pixels_per_point && pixels_per_point < 100.0,
|
||||
"pixels_per_point out of range: {}",
|
||||
pixels_per_point
|
||||
);
|
||||
|
||||
let texture_width = max_texture_side.at_most(8 * 1024);
|
||||
let initial_height = 64;
|
||||
let atlas = TextureAtlas::new([texture_width, initial_height]);
|
||||
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
|
||||
let font_impl_cache =
|
||||
FontImplCache::new(atlas.clone(), pixels_per_point, &definitions.font_data);
|
||||
|
||||
Self {
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
definitions,
|
||||
atlas,
|
||||
font_impl_cache,
|
||||
sized_family: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn definitions(&self) -> &FontDefinitions {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
/// Get the right font implementation from size and [`FontFamily`].
|
||||
pub fn font(&mut self, font_id: &FontId) -> &mut Font {
|
||||
let FontId { size, family } = font_id;
|
||||
|
||||
self.sized_family
|
||||
.entry((HashableF32(*size), family.clone()))
|
||||
.or_insert_with(|| {
|
||||
let fonts = &self.definitions.families.get(family);
|
||||
let fonts = fonts.unwrap_or_else(|| {
|
||||
panic!("FontFamily::{:?} is not bound to any fonts", family)
|
||||
});
|
||||
|
||||
let fonts: Vec<Arc<FontImpl>> = fonts
|
||||
.iter()
|
||||
.map(|font_name| self.font_impl_cache.font_impl(*size, font_name))
|
||||
.collect();
|
||||
|
||||
Font::new(fonts)
|
||||
})
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
|
||||
self.font(font_id).glyph_width(c)
|
||||
}
|
||||
|
||||
/// Can we display this glyph?
|
||||
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
|
||||
self.font(font_id).has_glyph(c)
|
||||
}
|
||||
|
||||
/// Can we display all the glyphs in this text?
|
||||
pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
|
||||
self.font(font_id).has_glyphs(s)
|
||||
}
|
||||
|
||||
/// Height of one row of text. In points
|
||||
fn row_height(&mut self, font_id: &FontId) -> f32 {
|
||||
self.font(font_id).row_height()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct CachedGalley {
|
||||
/// When it was last used
|
||||
last_used: u32,
|
||||
galley: Arc<Galley>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GalleyCache {
|
||||
/// Frame counter used to do garbage collection on the cache
|
||||
generation: u32,
|
||||
cache: nohash_hasher::IntMap<u64, CachedGalley>,
|
||||
}
|
||||
|
||||
impl GalleyCache {
|
||||
fn layout(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc<Galley> {
|
||||
let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher?
|
||||
|
||||
match self.cache.entry(hash) {
|
||||
std::collections::hash_map::Entry::Occupied(entry) => {
|
||||
let cached = entry.into_mut();
|
||||
cached.last_used = self.generation;
|
||||
cached.galley.clone()
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
let galley = super::layout(fonts, job.into());
|
||||
let galley = Arc::new(galley);
|
||||
entry.insert(CachedGalley {
|
||||
last_used: self.generation,
|
||||
galley: galley.clone(),
|
||||
});
|
||||
galley
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_galleys_in_cache(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
/// Must be called once per frame to clear the [`Galley`] cache.
|
||||
pub fn flush_cache(&mut self) {
|
||||
let current_generation = self.generation;
|
||||
self.cache.retain(|_key, cached| {
|
||||
cached.last_used == current_generation // only keep those that were used this frame
|
||||
});
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct FontImplCache {
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>,
|
||||
|
||||
/// Map font pixel sizes and names to the cached [`FontImpl`].
|
||||
cache: ahash::HashMap<(u32, String), Arc<FontImpl>>,
|
||||
}
|
||||
|
||||
impl FontImplCache {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
font_data: &BTreeMap<String, FontData>,
|
||||
) -> Self {
|
||||
let ab_glyph_fonts = font_data
|
||||
.iter()
|
||||
.map(|(name, font_data)| {
|
||||
let tweak = font_data.tweak;
|
||||
let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
|
||||
(name.clone(), (tweak, ab_glyph))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
atlas,
|
||||
pixels_per_point,
|
||||
ab_glyph_fonts,
|
||||
cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_impl(&mut self, scale_in_points: f32, font_name: &str) -> Arc<FontImpl> {
|
||||
use ab_glyph::Font as _;
|
||||
|
||||
let (tweak, ab_glyph_font) = self
|
||||
.ab_glyph_fonts
|
||||
.get(font_name)
|
||||
.unwrap_or_else(|| panic!("No font data found for {:?}", font_name))
|
||||
.clone();
|
||||
|
||||
let scale_in_pixels = self.pixels_per_point * scale_in_points;
|
||||
|
||||
// Scale the font properly (see https://github.com/emilk/egui/issues/2068).
|
||||
let units_per_em = ab_glyph_font.units_per_em().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"The font unit size of {:?} exceeds the expected range (16..=16384)",
|
||||
font_name
|
||||
)
|
||||
});
|
||||
let font_scaling = ab_glyph_font.height_unscaled() / units_per_em;
|
||||
let scale_in_pixels = scale_in_pixels * font_scaling;
|
||||
|
||||
self.cache
|
||||
.entry((
|
||||
(scale_in_pixels * tweak.scale).round() as u32,
|
||||
font_name.to_owned(),
|
||||
))
|
||||
.or_insert_with(|| {
|
||||
Arc::new(FontImpl::new(
|
||||
self.atlas.clone(),
|
||||
self.pixels_per_point,
|
||||
font_name.to_owned(),
|
||||
ab_glyph_font,
|
||||
scale_in_pixels,
|
||||
tweak,
|
||||
))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
19
egui/crates/epaint/src/text/mod.rs
Normal file
19
egui/crates/epaint/src/text/mod.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Everything related to text, fonts, text layout, cursors etc.
|
||||
|
||||
pub mod cursor;
|
||||
mod font;
|
||||
mod fonts;
|
||||
mod text_layout;
|
||||
mod text_layout_types;
|
||||
|
||||
/// One `\t` character is this many spaces wide.
|
||||
pub const TAB_SIZE: usize = 4;
|
||||
|
||||
pub use {
|
||||
fonts::{FontData, FontDefinitions, FontFamily, FontId, FontTweak, Fonts, FontsImpl},
|
||||
text_layout::layout,
|
||||
text_layout_types::*,
|
||||
};
|
||||
|
||||
/// Suggested character to use to replace those in password text fields.
|
||||
pub const PASSWORD_REPLACEMENT_CHAR: char = '•';
|
||||
873
egui/crates/epaint/src/text/text_layout.rs
Normal file
873
egui/crates/epaint/src/text/text_layout.rs
Normal file
|
|
@ -0,0 +1,873 @@
|
|||
use std::ops::RangeInclusive;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals};
|
||||
use crate::{Color32, Mesh, Stroke, Vertex};
|
||||
use emath::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Represents GUI scale and convenience methods for rounding to pixels.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PointScale {
|
||||
pub pixels_per_point: f32,
|
||||
}
|
||||
|
||||
impl PointScale {
|
||||
#[inline(always)]
|
||||
pub fn new(pixels_per_point: f32) -> Self {
|
||||
Self { pixels_per_point }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn floor_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.pixels_per_point).floor() / self.pixels_per_point
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 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 [`crate::Fonts::layout_job`] instead
|
||||
/// since that memoizes the input, making subsequent layouting of the same text much faster.
|
||||
pub fn layout(fonts: &mut FontsImpl, 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 point_scale = PointScale::new(fonts.pixels_per_point());
|
||||
|
||||
let mut rows = rows_from_paragraphs(fonts, paragraphs, &job);
|
||||
|
||||
let justify = job.justify && job.wrap.max_width.is_finite();
|
||||
|
||||
if justify || job.halign != Align::LEFT {
|
||||
let num_rows = rows.len();
|
||||
for (i, row) in rows.iter_mut().enumerate() {
|
||||
let is_last_row = i + 1 == num_rows;
|
||||
let justify_row = justify && !row.ends_with_newline && !is_last_row;
|
||||
halign_and_justify_row(
|
||||
point_scale,
|
||||
row,
|
||||
job.halign,
|
||||
job.wrap.max_width,
|
||||
justify_row,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
galley_from_rows(point_scale, job, rows)
|
||||
}
|
||||
|
||||
fn layout_section(
|
||||
fonts: &mut FontsImpl,
|
||||
job: &LayoutJob,
|
||||
section_index: u32,
|
||||
section: &LayoutSection,
|
||||
out_paragraphs: &mut Vec<Paragraph>,
|
||||
) {
|
||||
let LayoutSection {
|
||||
leading_space,
|
||||
byte_range,
|
||||
format,
|
||||
} = section;
|
||||
let font = fonts.font(&format.font_id);
|
||||
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(emilk): 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(emilk): 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(font_impl) = font_impl {
|
||||
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, glyph_info.row_height),
|
||||
ascent: glyph_info.ascent,
|
||||
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(
|
||||
fonts: &mut FontsImpl,
|
||||
paragraphs: Vec<Paragraph>,
|
||||
job: &LayoutJob,
|
||||
) -> 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 <= job.wrap.max_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(fonts, ¶graph, job, &mut rows);
|
||||
rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn line_break(
|
||||
fonts: &mut FontsImpl,
|
||||
paragraph: &Paragraph,
|
||||
job: &LayoutJob,
|
||||
out_rows: &mut Vec<Row>,
|
||||
) {
|
||||
// Keeps track of good places to insert row break if we exceed `wrap_width`.
|
||||
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;
|
||||
let mut non_empty_rows = 0;
|
||||
|
||||
for i in 0..paragraph.glyphs.len() {
|
||||
let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
|
||||
|
||||
if job.wrap.max_rows > 0 && non_empty_rows >= job.wrap.max_rows {
|
||||
break;
|
||||
}
|
||||
|
||||
if potential_row_width > job.wrap.max_width {
|
||||
if first_row_indentation > 0.0
|
||||
&& !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
|
||||
{
|
||||
// Allow the first row to be completely empty, because we know there will be more space on the next row:
|
||||
// TODO(emilk): 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(job.wrap.break_anywhere)
|
||||
{
|
||||
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();
|
||||
non_empty_rows += 1;
|
||||
} else {
|
||||
// Found no place to break, so we have to overrun wrap_width.
|
||||
}
|
||||
}
|
||||
|
||||
row_break_candidates.add(i, ¶graph.glyphs[i..]);
|
||||
}
|
||||
|
||||
if row_start_idx < paragraph.glyphs.len() {
|
||||
if job.wrap.max_rows > 0 && non_empty_rows == job.wrap.max_rows {
|
||||
if let Some(last_row) = out_rows.last_mut() {
|
||||
replace_last_glyph_with_overflow_character(fonts, job, last_row);
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_last_glyph_with_overflow_character(
|
||||
fonts: &mut FontsImpl,
|
||||
job: &LayoutJob,
|
||||
row: &mut Row,
|
||||
) {
|
||||
let overflow_character = match job.wrap.overflow_character {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
loop {
|
||||
let (prev_glyph, last_glyph) = match row.glyphs.as_mut_slice() {
|
||||
[.., prev, last] => (Some(prev), last),
|
||||
[.., last] => (None, last),
|
||||
_ => break,
|
||||
};
|
||||
|
||||
let section = &job.sections[last_glyph.section_index as usize];
|
||||
let font = fonts.font(§ion.format.font_id);
|
||||
let font_height = font.row_height();
|
||||
|
||||
let prev_glyph_id = prev_glyph.map(|prev_glyph| {
|
||||
let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr);
|
||||
prev_glyph_info.id
|
||||
});
|
||||
|
||||
// undo kerning with previous glyph
|
||||
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
|
||||
last_glyph.pos.x -= font_impl
|
||||
.zip(prev_glyph_id)
|
||||
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id))
|
||||
.unwrap_or_default();
|
||||
|
||||
// replace the glyph
|
||||
last_glyph.chr = overflow_character;
|
||||
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
|
||||
last_glyph.size = vec2(glyph_info.advance_width, font_height);
|
||||
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||
|
||||
// reapply kerning
|
||||
last_glyph.pos.x += font_impl
|
||||
.zip(prev_glyph_id)
|
||||
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id))
|
||||
.unwrap_or_default();
|
||||
|
||||
// check if we're still within width budget
|
||||
let row_end_x = last_glyph.max_x();
|
||||
let row_start_x = row.glyphs.first().unwrap().pos.x; // if `last_mut()` returned `Some`, then so will `first()`
|
||||
let row_width = row_end_x - row_start_x;
|
||||
if row_width <= job.wrap.max_width {
|
||||
break;
|
||||
}
|
||||
|
||||
row.glyphs.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn halign_and_justify_row(
|
||||
point_scale: PointScale,
|
||||
row: &mut Row,
|
||||
halign: Align,
|
||||
wrap_width: f32,
|
||||
justify: bool,
|
||||
) {
|
||||
if row.glyphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let num_leading_spaces = row
|
||||
.glyphs
|
||||
.iter()
|
||||
.take_while(|glyph| glyph.chr.is_whitespace())
|
||||
.count();
|
||||
|
||||
let glyph_range = if num_leading_spaces == row.glyphs.len() {
|
||||
// There is only whitespace
|
||||
(0, row.glyphs.len())
|
||||
} else {
|
||||
let num_trailing_spaces = row
|
||||
.glyphs
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|glyph| glyph.chr.is_whitespace())
|
||||
.count();
|
||||
|
||||
(num_leading_spaces, row.glyphs.len() - num_trailing_spaces)
|
||||
};
|
||||
let num_glyphs_in_range = glyph_range.1 - glyph_range.0;
|
||||
assert!(num_glyphs_in_range > 0);
|
||||
|
||||
let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x;
|
||||
let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x;
|
||||
let original_width = original_max_x - original_min_x;
|
||||
|
||||
let target_width = if justify && num_glyphs_in_range > 1 {
|
||||
wrap_width
|
||||
} else {
|
||||
original_width
|
||||
};
|
||||
|
||||
let (target_min_x, target_max_x) = match halign {
|
||||
Align::LEFT => (0.0, target_width),
|
||||
Align::Center => (-target_width / 2.0, target_width / 2.0),
|
||||
Align::RIGHT => (-target_width, 0.0),
|
||||
};
|
||||
|
||||
let num_spaces_in_range = row.glyphs[glyph_range.0..glyph_range.1]
|
||||
.iter()
|
||||
.filter(|glyph| glyph.chr.is_whitespace())
|
||||
.count();
|
||||
|
||||
let mut extra_x_per_glyph = if num_glyphs_in_range == 1 {
|
||||
0.0
|
||||
} else {
|
||||
(target_width - original_width) / (num_glyphs_in_range as f32 - 1.0)
|
||||
};
|
||||
extra_x_per_glyph = extra_x_per_glyph.at_least(0.0); // Don't contract
|
||||
|
||||
let mut extra_x_per_space = 0.0;
|
||||
if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
|
||||
// Add an integral number of pixels between each glyph,
|
||||
// and add the balance to the spaces:
|
||||
|
||||
extra_x_per_glyph = point_scale.floor_to_pixel(extra_x_per_glyph);
|
||||
|
||||
extra_x_per_space = (target_width
|
||||
- original_width
|
||||
- extra_x_per_glyph * (num_glyphs_in_range as f32 - 1.0))
|
||||
/ (num_spaces_in_range as f32);
|
||||
}
|
||||
|
||||
let mut translate_x = target_min_x - original_min_x - extra_x_per_glyph * glyph_range.0 as f32;
|
||||
|
||||
for glyph in &mut row.glyphs {
|
||||
glyph.pos.x += translate_x;
|
||||
glyph.pos.x = point_scale.round_to_pixel(glyph.pos.x);
|
||||
translate_x += extra_x_per_glyph;
|
||||
if glyph.chr.is_whitespace() {
|
||||
translate_x += extra_x_per_space;
|
||||
}
|
||||
}
|
||||
|
||||
// Note we ignore the leading/trailing whitespace here!
|
||||
row.rect.min.x = target_min_x;
|
||||
row.rect.max.x = target_max_x;
|
||||
}
|
||||
|
||||
/// Calculate the Y positions and tessellate the text.
|
||||
fn galley_from_rows(point_scale: PointScale, 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 min_x: f32 = 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());
|
||||
let mut row_ascent = 0.0f32;
|
||||
first_row_min_height = 0.0;
|
||||
|
||||
// take metrics from the highest font in this row
|
||||
if let Some(glyph) = row
|
||||
.glyphs
|
||||
.iter()
|
||||
.max_by(|a, b| a.size.y.partial_cmp(&b.size.y).unwrap())
|
||||
{
|
||||
row_height = glyph.size.y;
|
||||
row_ascent = glyph.ascent;
|
||||
}
|
||||
row_height = point_scale.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;
|
||||
|
||||
let align_offset = match format.valign {
|
||||
Align::Center | Align::Max => row_ascent,
|
||||
|
||||
// raised text.
|
||||
Align::Min => glyph.ascent,
|
||||
};
|
||||
glyph.pos.y = cursor_y + align_offset;
|
||||
}
|
||||
|
||||
row.rect.min.y = cursor_y;
|
||||
row.rect.max.y = cursor_y + row_height;
|
||||
|
||||
min_x = min_x.min(row.rect.min.x);
|
||||
max_x = max_x.max(row.rect.max.x);
|
||||
cursor_y += row_height;
|
||||
cursor_y = point_scale.round_to_pixel(cursor_y);
|
||||
}
|
||||
|
||||
let format_summary = format_summary(&job);
|
||||
|
||||
let mut mesh_bounds = Rect::NOTHING;
|
||||
let mut num_vertices = 0;
|
||||
let mut num_indices = 0;
|
||||
|
||||
for row in &mut rows {
|
||||
row.visuals = tessellate_row(point_scale, &job, &format_summary, row);
|
||||
mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds);
|
||||
num_vertices += row.visuals.mesh.vertices.len();
|
||||
num_indices += row.visuals.mesh.indices.len();
|
||||
}
|
||||
|
||||
let rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y));
|
||||
|
||||
Galley {
|
||||
job,
|
||||
rows,
|
||||
rect,
|
||||
mesh_bounds,
|
||||
num_vertices,
|
||||
num_indices,
|
||||
pixels_per_point: point_scale.pixels_per_point,
|
||||
}
|
||||
}
|
||||
|
||||
#[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(
|
||||
point_scale: PointScale,
|
||||
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(point_scale, job, row, &mut mesh);
|
||||
let glyph_vertex_end = mesh.vertices.len();
|
||||
|
||||
if format_summary.any_underline {
|
||||
add_row_hline(point_scale, 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(point_scale, 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(point_scale: PointScale, 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 = point_scale.round_to_pixel(left_top.x);
|
||||
left_top.y = point_scale.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(
|
||||
point_scale: PointScale,
|
||||
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(point_scale, [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(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut Mesh) {
|
||||
let antialiased = true;
|
||||
|
||||
if antialiased {
|
||||
let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations.
|
||||
path.add_line_segment([start, stop]);
|
||||
let feathering = 1.0 / point_scale.pixels_per_point();
|
||||
path.stroke_open(feathering, stroke, mesh);
|
||||
} else {
|
||||
// Thin lines often lost, so this is a bad idea
|
||||
|
||||
assert_eq!(start.y, stop.y);
|
||||
|
||||
let min_y = point_scale.round_to_pixel(start.y - 0.5 * stroke.width);
|
||||
let max_y = point_scale.round_to_pixel(min_y + stroke.width);
|
||||
|
||||
let rect = Rect::from_min_max(
|
||||
pos2(point_scale.round_to_pixel(start.x), min_y),
|
||||
pos2(point_scale.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) or kana (Japanese hiragana and katakana) are good candidates for line break.
|
||||
cjk: Option<usize>,
|
||||
|
||||
/// Breaking anywhere before a CJK character is acceptable too.
|
||||
pre_cjk: 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, glyphs: &[Glyph]) {
|
||||
let chr = glyphs[0].chr;
|
||||
const NON_BREAKING_SPACE: char = '\u{A0}';
|
||||
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
|
||||
self.space = Some(index);
|
||||
} else if is_cjk(chr) && (glyphs.len() == 1 || is_cjk_break_allowed(glyphs[1].chr)) {
|
||||
self.cjk = Some(index);
|
||||
} else if chr == '-' {
|
||||
self.dash = Some(index);
|
||||
} else if chr.is_ascii_punctuation() {
|
||||
self.punctuation = Some(index);
|
||||
} else if glyphs.len() > 1 && is_cjk(glyphs[1].chr) {
|
||||
self.pre_cjk = Some(index);
|
||||
}
|
||||
self.any = Some(index);
|
||||
}
|
||||
|
||||
fn word_boundary(&self) -> Option<usize> {
|
||||
[self.space, self.cjk, self.pre_cjk]
|
||||
.into_iter()
|
||||
.max()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn has_good_candidate(&self, break_anywhere: bool) -> bool {
|
||||
if break_anywhere {
|
||||
self.any.is_some()
|
||||
} else {
|
||||
self.word_boundary().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, break_anywhere: bool) -> Option<usize> {
|
||||
if break_anywhere {
|
||||
self.any
|
||||
} else {
|
||||
self.word_boundary()
|
||||
.or(self.dash)
|
||||
.or(self.punctuation)
|
||||
.or(self.any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_cjk_ideograph(c: char) -> bool {
|
||||
('\u{4E00}' <= c && c <= '\u{9FFF}')
|
||||
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|
||||
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_kana(c: char) -> bool {
|
||||
('\u{3040}' <= c && c <= '\u{309F}') // Hiragana block
|
||||
|| ('\u{30A0}' <= c && c <= '\u{30FF}') // Katakana block
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_cjk(c: char) -> bool {
|
||||
// TODO: Add support for Korean Hangul.
|
||||
is_cjk_ideograph(c) || is_kana(c)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_cjk_break_allowed(c: char) -> bool {
|
||||
// See: https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages#Characters_not_permitted_on_the_start_of_a_line.
|
||||
!")]}〕〉》」』】〙〗〟'\"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。.".contains(c)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_zero_max_width() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section("W".into(), super::TextFormat::default());
|
||||
layout_job.wrap.max_width = 0.0;
|
||||
let galley = super::layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
super::TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 90.0;
|
||||
let galley = super::layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(
|
||||
galley
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.glyphs.iter().map(|g| g.chr).collect::<String>())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["日本語と", "Englishの混在", "した文章"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_cjk() {
|
||||
let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
super::TextFormat::default(),
|
||||
);
|
||||
layout_job.wrap.max_width = 110.0;
|
||||
let galley = super::layout(&mut fonts, layout_job.into());
|
||||
assert_eq!(
|
||||
galley
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.glyphs.iter().map(|g| g.chr).collect::<String>())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["日本語とEnglish", "の混在した文章"]
|
||||
);
|
||||
}
|
||||
914
egui/crates/epaint/src/text/text_layout_types.rs
Normal file
914
egui/crates/epaint/src/text/text_layout_types.rs
Normal file
|
|
@ -0,0 +1,914 @@
|
|||
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{cursor::*, font::UvRect};
|
||||
use crate::{Color32, FontId, Mesh, Stroke};
|
||||
use emath::*;
|
||||
|
||||
/// Describes the task of laying out text.
|
||||
///
|
||||
/// This supports mixing different fonts, color and formats (underline etc).
|
||||
///
|
||||
/// Pass this to [`crate::Fonts::layout_job`] or [`crate::text::layout`].
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```
|
||||
/// use epaint::{Color32, text::{LayoutJob, TextFormat}, FontFamily, FontId};
|
||||
///
|
||||
/// let mut job = LayoutJob::default();
|
||||
/// job.append(
|
||||
/// "Hello ",
|
||||
/// 0.0,
|
||||
/// TextFormat {
|
||||
/// font_id: FontId::new(14.0, FontFamily::Proportional),
|
||||
/// color: Color32::WHITE,
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// );
|
||||
/// job.append(
|
||||
/// "World!",
|
||||
/// 0.0,
|
||||
/// TextFormat {
|
||||
/// font_id: FontId::new(14.0, FontFamily::Monospace),
|
||||
/// color: Color32::BLACK,
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// As you can see, constructing a [`LayoutJob`] is currently a lot of work.
|
||||
/// It would be nice to have a helper macro for it!
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct LayoutJob {
|
||||
/// The complete text of this job, referenced by [`LayoutSection`].
|
||||
pub text: String,
|
||||
|
||||
/// The different section, which can have different fonts, colors, etc.
|
||||
pub sections: Vec<LayoutSection>,
|
||||
|
||||
pub wrap: TextWrapping,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// How to horizontally align the text (`Align::LEFT`, `Align::Center`, `Align::RIGHT`).
|
||||
pub halign: Align,
|
||||
|
||||
/// Justify text so that word-wrapped rows fill the whole [`TextWrapping::max_width`]
|
||||
pub justify: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutJob {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: Default::default(),
|
||||
sections: Default::default(),
|
||||
wrap: Default::default(),
|
||||
first_row_min_height: 0.0,
|
||||
break_on_newline: true,
|
||||
halign: Align::LEFT,
|
||||
justify: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutJob {
|
||||
/// Break on `\n` and at the given wrap width.
|
||||
#[inline]
|
||||
pub fn simple(text: String, font_id: FontId, color: Color32, wrap_width: f32) -> Self {
|
||||
Self {
|
||||
sections: vec![LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: 0..text.len(),
|
||||
format: TextFormat::simple(font_id, color),
|
||||
}],
|
||||
text,
|
||||
wrap: TextWrapping {
|
||||
max_width: wrap_width,
|
||||
..Default::default()
|
||||
},
|
||||
break_on_newline: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Does not break on `\n`, but shows the replacement character instead.
|
||||
#[inline]
|
||||
pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self {
|
||||
Self {
|
||||
sections: vec![LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: 0..text.len(),
|
||||
format: TextFormat::simple(font_id, color),
|
||||
}],
|
||||
text,
|
||||
wrap: Default::default(),
|
||||
break_on_newline: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn single_section(text: String, format: TextFormat) -> Self {
|
||||
Self {
|
||||
sections: vec![LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: 0..text.len(),
|
||||
format,
|
||||
}],
|
||||
text,
|
||||
wrap: Default::default(),
|
||||
break_on_newline: true,
|
||||
..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,
|
||||
});
|
||||
}
|
||||
|
||||
/// The height of the tallest used font in the job.
|
||||
pub fn font_height(&self, fonts: &crate::Fonts) -> f32 {
|
||||
let mut max_height = 0.0_f32;
|
||||
for section in &self.sections {
|
||||
max_height = max_height.max(fonts.row_height(§ion.format.font_id));
|
||||
}
|
||||
max_height
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for LayoutJob {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let Self {
|
||||
text,
|
||||
sections,
|
||||
wrap,
|
||||
first_row_min_height,
|
||||
break_on_newline,
|
||||
halign,
|
||||
justify,
|
||||
} = self;
|
||||
|
||||
text.hash(state);
|
||||
sections.hash(state);
|
||||
wrap.hash(state);
|
||||
crate::f32_hash(state, *first_row_min_height);
|
||||
break_on_newline.hash(state);
|
||||
halign.hash(state);
|
||||
justify.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TextFormat {
|
||||
pub font_id: FontId,
|
||||
/// 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(emilk): lowered
|
||||
}
|
||||
|
||||
impl Default for TextFormat {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_id: FontId::default(),
|
||||
color: Color32::GRAY,
|
||||
background: Color32::TRANSPARENT,
|
||||
italics: false,
|
||||
underline: Stroke::NONE,
|
||||
strikethrough: Stroke::NONE,
|
||||
valign: Align::BOTTOM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextFormat {
|
||||
#[inline]
|
||||
pub fn simple(font_id: FontId, color: Color32) -> Self {
|
||||
Self {
|
||||
font_id,
|
||||
color,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TextWrapping {
|
||||
/// Try to break text so that no row is wider than this.
|
||||
/// Set to [`f32::INFINITY`] to turn off wrapping.
|
||||
/// Note that `\n` always produces a new line.
|
||||
pub max_width: f32,
|
||||
|
||||
/// Maximum amount of rows the text should have.
|
||||
/// Set to `0` to disable this.
|
||||
pub max_rows: usize,
|
||||
|
||||
/// Don't try to break text at an appropriate place.
|
||||
pub break_anywhere: bool,
|
||||
|
||||
/// Character to use to represent clipped text, `…` for example, which is the default.
|
||||
pub overflow_character: Option<char>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for TextWrapping {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let Self {
|
||||
max_width,
|
||||
max_rows,
|
||||
break_anywhere,
|
||||
overflow_character,
|
||||
} = self;
|
||||
crate::f32_hash(state, *max_width);
|
||||
max_rows.hash(state);
|
||||
break_anywhere.hash(state);
|
||||
overflow_character.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextWrapping {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_width: f32::INFINITY,
|
||||
max_rows: 0,
|
||||
break_anywhere: false,
|
||||
overflow_character: Some('…'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Text that has been laid out, ready for painting.
|
||||
///
|
||||
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
|
||||
///
|
||||
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Galley {
|
||||
/// The job that this galley is the result of.
|
||||
/// Contains the original string and style sections.
|
||||
pub job: Arc<LayoutJob>,
|
||||
|
||||
/// Rows of text, from top to bottom.
|
||||
/// The number of characters in all rows sum up to `job.text.chars().count()`.
|
||||
/// Note that each paragraph (pieces of text separated with `\n`)
|
||||
/// can be split up into multiple rows.
|
||||
pub rows: Vec<Row>,
|
||||
|
||||
/// Bounding rect.
|
||||
///
|
||||
/// `rect.top()` is always 0.0.
|
||||
///
|
||||
/// With [`LayoutJob::halign`]:
|
||||
/// * [`Align::LEFT`]: rect.left() == 0.0
|
||||
/// * [`Align::Center`]: rect.center() == 0.0
|
||||
/// * [`Align::RIGHT`]: rect.right() == 0.0
|
||||
pub rect: Rect,
|
||||
|
||||
/// Tight bounding box around all the meshes in all the rows.
|
||||
/// Can be used for culling.
|
||||
pub mesh_bounds: Rect,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// The number of physical pixels for each logical point.
|
||||
/// Since this affects the layout, we keep track of it
|
||||
/// so that we can warn if this has changed once we get to
|
||||
/// tessellation.
|
||||
pub pixels_per_point: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Row {
|
||||
/// One for each `char`.
|
||||
pub glyphs: Vec<Glyph>,
|
||||
|
||||
/// Logical bounding rectangle based on font heights etc.
|
||||
/// Use this when drawing a selection or similar!
|
||||
/// Includes leading and trailing whitespace.
|
||||
pub rect: Rect,
|
||||
|
||||
/// The mesh, ready to be rendered.
|
||||
pub visuals: RowVisuals,
|
||||
|
||||
/// If true, this [`Row`] came from a paragraph ending with a `\n`.
|
||||
/// The `\n` itself is omitted from [`Self::glyphs`].
|
||||
/// 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.
|
||||
/// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`.
|
||||
pub ends_with_newline: bool,
|
||||
}
|
||||
|
||||
/// The tessellated output of a row.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RowVisuals {
|
||||
/// The tessellated text, using non-normalized (texel) UV coordinates.
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Glyph {
|
||||
/// The character this glyph represents.
|
||||
pub chr: char,
|
||||
|
||||
/// Baseline position, relative to the galley.
|
||||
/// Logical position: pos.y is the same for all chars of the same [`TextFormat`].
|
||||
pub pos: Pos2,
|
||||
|
||||
/// `ascent` value from the font
|
||||
pub ascent: f32,
|
||||
|
||||
/// Advance width and font row height.
|
||||
pub size: Vec2,
|
||||
|
||||
/// Position of the glyph in the font texture, in texels.
|
||||
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 - vec2(0.0, self.ascent), self.size)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
impl Row {
|
||||
/// Excludes the implicit `\n` after the [`Row`], if any.
|
||||
#[inline]
|
||||
pub fn char_count_excluding_newline(&self) -> usize {
|
||||
self.glyphs.len()
|
||||
}
|
||||
|
||||
/// Includes the implicit `\n` after the [`Row`], if any.
|
||||
#[inline]
|
||||
pub fn char_count_including_newline(&self) -> usize {
|
||||
self.glyphs.len() + (self.ends_with_newline as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn min_y(&self) -> f32 {
|
||||
self.rect.top()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn max_y(&self) -> f32 {
|
||||
self.rect.bottom()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> f32 {
|
||||
self.rect.height()
|
||||
}
|
||||
|
||||
/// Closest char at the desired x coordinate.
|
||||
/// Returns something in the range `[0, char_count_excluding_newline()]`.
|
||||
pub fn char_at(&self, desired_x: f32) -> usize {
|
||||
for (i, glyph) in self.glyphs.iter().enumerate() {
|
||||
if desired_x < glyph.logical_rect().center().x {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
self.char_count_excluding_newline()
|
||||
}
|
||||
|
||||
pub fn x_offset(&self, column: usize) -> f32 {
|
||||
if let Some(glyph) = self.glyphs.get(column) {
|
||||
glyph.pos.x
|
||||
} else {
|
||||
self.rect.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Galley {
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.job.is_empty()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn text(&self) -> &str {
|
||||
&self.job.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.rect.size()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// ## Physical positions
|
||||
impl Galley {
|
||||
/// Zero-width rect past the last character.
|
||||
fn end_pos(&self) -> Rect {
|
||||
if let Some(row) = self.rows.last() {
|
||||
let x = row.rect.right();
|
||||
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
|
||||
} else {
|
||||
// Empty galley
|
||||
Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect {
|
||||
let mut it = PCursor::default();
|
||||
|
||||
for row in &self.rows {
|
||||
if it.paragraph == pcursor.paragraph {
|
||||
// Right paragraph, but is it the right row in the paragraph?
|
||||
|
||||
if it.offset <= pcursor.offset
|
||||
&& (pcursor.offset <= it.offset + row.char_count_excluding_newline()
|
||||
|| row.ends_with_newline)
|
||||
{
|
||||
let column = pcursor.offset - it.offset;
|
||||
|
||||
let select_next_row_instead = pcursor.prefer_next_row
|
||||
&& !row.ends_with_newline
|
||||
&& column >= row.char_count_excluding_newline();
|
||||
if !select_next_row_instead {
|
||||
let x = row.x_offset(column);
|
||||
return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if row.ends_with_newline {
|
||||
it.paragraph += 1;
|
||||
it.offset = 0;
|
||||
} else {
|
||||
it.offset += row.char_count_including_newline();
|
||||
}
|
||||
}
|
||||
|
||||
self.end_pos()
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
||||
self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores
|
||||
}
|
||||
|
||||
/// Cursor at the given position within the galley
|
||||
pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor {
|
||||
let mut best_y_dist = f32::INFINITY;
|
||||
let mut cursor = Cursor::default();
|
||||
|
||||
let mut ccursor_index = 0;
|
||||
let mut pcursor_it = PCursor::default();
|
||||
|
||||
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||
let is_pos_within_row = pos.y >= row.min_y() && pos.y <= row.max_y();
|
||||
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 {
|
||||
best_y_dist = y_dist;
|
||||
let column = row.char_at(pos.x);
|
||||
let prefer_next_row = column < row.char_count_excluding_newline();
|
||||
cursor = Cursor {
|
||||
ccursor: CCursor {
|
||||
index: ccursor_index + column,
|
||||
prefer_next_row,
|
||||
},
|
||||
rcursor: RCursor {
|
||||
row: row_nr,
|
||||
column,
|
||||
},
|
||||
pcursor: PCursor {
|
||||
paragraph: pcursor_it.paragraph,
|
||||
offset: pcursor_it.offset + column,
|
||||
prefer_next_row,
|
||||
},
|
||||
};
|
||||
|
||||
if is_pos_within_row {
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
ccursor_index += row.char_count_including_newline();
|
||||
if row.ends_with_newline {
|
||||
pcursor_it.paragraph += 1;
|
||||
pcursor_it.offset = 0;
|
||||
} else {
|
||||
pcursor_it.offset += row.char_count_including_newline();
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Cursor positions
|
||||
impl Galley {
|
||||
/// Cursor to one-past last character.
|
||||
pub fn end(&self) -> Cursor {
|
||||
if self.rows.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
let mut ccursor = CCursor {
|
||||
index: 0,
|
||||
prefer_next_row: true,
|
||||
};
|
||||
let mut pcursor = PCursor {
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
prefer_next_row: true,
|
||||
};
|
||||
for row in &self.rows {
|
||||
let row_char_count = row.char_count_including_newline();
|
||||
ccursor.index += row_char_count;
|
||||
if row.ends_with_newline {
|
||||
pcursor.paragraph += 1;
|
||||
pcursor.offset = 0;
|
||||
} else {
|
||||
pcursor.offset += row_char_count;
|
||||
}
|
||||
}
|
||||
Cursor {
|
||||
ccursor,
|
||||
rcursor: self.end_rcursor(),
|
||||
pcursor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_rcursor(&self) -> RCursor {
|
||||
if let Some(last_row) = self.rows.last() {
|
||||
crate::epaint_assert!(!last_row.ends_with_newline);
|
||||
RCursor {
|
||||
row: self.rows.len() - 1,
|
||||
column: last_row.char_count_excluding_newline(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Cursor conversions
|
||||
impl Galley {
|
||||
// The returned cursor is clamped.
|
||||
pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor {
|
||||
let prefer_next_row = ccursor.prefer_next_row;
|
||||
let mut ccursor_it = CCursor {
|
||||
index: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
let mut pcursor_it = PCursor {
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
|
||||
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||
let row_char_count = row.char_count_excluding_newline();
|
||||
|
||||
if ccursor_it.index <= ccursor.index
|
||||
&& ccursor.index <= ccursor_it.index + row_char_count
|
||||
{
|
||||
let column = ccursor.index - ccursor_it.index;
|
||||
|
||||
let select_next_row_instead = prefer_next_row
|
||||
&& !row.ends_with_newline
|
||||
&& column >= row.char_count_excluding_newline();
|
||||
if !select_next_row_instead {
|
||||
pcursor_it.offset += column;
|
||||
return Cursor {
|
||||
ccursor,
|
||||
rcursor: RCursor {
|
||||
row: row_nr,
|
||||
column,
|
||||
},
|
||||
pcursor: pcursor_it,
|
||||
};
|
||||
}
|
||||
}
|
||||
ccursor_it.index += row.char_count_including_newline();
|
||||
if row.ends_with_newline {
|
||||
pcursor_it.paragraph += 1;
|
||||
pcursor_it.offset = 0;
|
||||
} else {
|
||||
pcursor_it.offset += row.char_count_including_newline();
|
||||
}
|
||||
}
|
||||
crate::epaint_assert!(ccursor_it == self.end().ccursor);
|
||||
Cursor {
|
||||
ccursor: ccursor_it, // clamp
|
||||
rcursor: self.end_rcursor(),
|
||||
pcursor: pcursor_it,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor {
|
||||
if rcursor.row >= self.rows.len() {
|
||||
return self.end();
|
||||
}
|
||||
|
||||
let prefer_next_row =
|
||||
rcursor.column < self.rows[rcursor.row].char_count_excluding_newline();
|
||||
let mut ccursor_it = CCursor {
|
||||
index: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
let mut pcursor_it = PCursor {
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
|
||||
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||
if row_nr == rcursor.row {
|
||||
ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline());
|
||||
|
||||
if row.ends_with_newline {
|
||||
// Allow offset to go beyond the end of the paragraph
|
||||
pcursor_it.offset += rcursor.column;
|
||||
} else {
|
||||
pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline());
|
||||
}
|
||||
return Cursor {
|
||||
ccursor: ccursor_it,
|
||||
rcursor,
|
||||
pcursor: pcursor_it,
|
||||
};
|
||||
}
|
||||
ccursor_it.index += row.char_count_including_newline();
|
||||
if row.ends_with_newline {
|
||||
pcursor_it.paragraph += 1;
|
||||
pcursor_it.offset = 0;
|
||||
} else {
|
||||
pcursor_it.offset += row.char_count_including_newline();
|
||||
}
|
||||
}
|
||||
Cursor {
|
||||
ccursor: ccursor_it,
|
||||
rcursor: self.end_rcursor(),
|
||||
pcursor: pcursor_it,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(emilk): return identical cursor, or clamp?
|
||||
pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor {
|
||||
let prefer_next_row = pcursor.prefer_next_row;
|
||||
let mut ccursor_it = CCursor {
|
||||
index: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
let mut pcursor_it = PCursor {
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
prefer_next_row,
|
||||
};
|
||||
|
||||
for (row_nr, row) in self.rows.iter().enumerate() {
|
||||
if pcursor_it.paragraph == pcursor.paragraph {
|
||||
// Right paragraph, but is it the right row in the paragraph?
|
||||
|
||||
if pcursor_it.offset <= pcursor.offset
|
||||
&& (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline()
|
||||
|| row.ends_with_newline)
|
||||
{
|
||||
let column = pcursor.offset - pcursor_it.offset;
|
||||
|
||||
let select_next_row_instead = pcursor.prefer_next_row
|
||||
&& !row.ends_with_newline
|
||||
&& column >= row.char_count_excluding_newline();
|
||||
|
||||
if !select_next_row_instead {
|
||||
ccursor_it.index += column.at_most(row.char_count_excluding_newline());
|
||||
|
||||
return Cursor {
|
||||
ccursor: ccursor_it,
|
||||
rcursor: RCursor {
|
||||
row: row_nr,
|
||||
column,
|
||||
},
|
||||
pcursor,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ccursor_it.index += row.char_count_including_newline();
|
||||
if row.ends_with_newline {
|
||||
pcursor_it.paragraph += 1;
|
||||
pcursor_it.offset = 0;
|
||||
} else {
|
||||
pcursor_it.offset += row.char_count_including_newline();
|
||||
}
|
||||
}
|
||||
Cursor {
|
||||
ccursor: ccursor_it,
|
||||
rcursor: self.end_rcursor(),
|
||||
pcursor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Cursor positions
|
||||
impl Galley {
|
||||
pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor {
|
||||
if cursor.ccursor.index == 0 {
|
||||
Default::default()
|
||||
} else {
|
||||
let ccursor = CCursor {
|
||||
index: cursor.ccursor.index,
|
||||
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
|
||||
};
|
||||
self.from_ccursor(ccursor - 1)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor {
|
||||
let ccursor = CCursor {
|
||||
index: cursor.ccursor.index,
|
||||
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
|
||||
};
|
||||
self.from_ccursor(ccursor + 1)
|
||||
}
|
||||
|
||||
pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor {
|
||||
if cursor.rcursor.row == 0 {
|
||||
Cursor::default()
|
||||
} else {
|
||||
let new_row = cursor.rcursor.row - 1;
|
||||
|
||||
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
|
||||
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
|
||||
|
||||
let new_rcursor = if cursor_is_beyond_end_of_current_row {
|
||||
// keep same column
|
||||
RCursor {
|
||||
row: new_row,
|
||||
column: cursor.rcursor.column,
|
||||
}
|
||||
} else {
|
||||
// keep same X coord
|
||||
let x = self.pos_from_cursor(cursor).center().x;
|
||||
let column = if x > self.rows[new_row].rect.right() {
|
||||
// beyond the end of this row - keep same column
|
||||
cursor.rcursor.column
|
||||
} else {
|
||||
self.rows[new_row].char_at(x)
|
||||
};
|
||||
RCursor {
|
||||
row: new_row,
|
||||
column,
|
||||
}
|
||||
};
|
||||
self.from_rcursor(new_rcursor)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor {
|
||||
if cursor.rcursor.row + 1 < self.rows.len() {
|
||||
let new_row = cursor.rcursor.row + 1;
|
||||
|
||||
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
|
||||
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
|
||||
|
||||
let new_rcursor = if cursor_is_beyond_end_of_current_row {
|
||||
// keep same column
|
||||
RCursor {
|
||||
row: new_row,
|
||||
column: cursor.rcursor.column,
|
||||
}
|
||||
} else {
|
||||
// keep same X coord
|
||||
let x = self.pos_from_cursor(cursor).center().x;
|
||||
let column = if x > self.rows[new_row].rect.right() {
|
||||
// beyond the end of the next row - keep same column
|
||||
cursor.rcursor.column
|
||||
} else {
|
||||
self.rows[new_row].char_at(x)
|
||||
};
|
||||
RCursor {
|
||||
row: new_row,
|
||||
column,
|
||||
}
|
||||
};
|
||||
|
||||
self.from_rcursor(new_rcursor)
|
||||
} else {
|
||||
self.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor {
|
||||
self.from_rcursor(RCursor {
|
||||
row: cursor.rcursor.row,
|
||||
column: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor {
|
||||
self.from_rcursor(RCursor {
|
||||
row: cursor.rcursor.row,
|
||||
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
|
||||
})
|
||||
}
|
||||
}
|
||||
252
egui/crates/epaint/src/texture_atlas.rs
Normal file
252
egui/crates/epaint/src/texture_atlas.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
use emath::{remap_clamp, Rect};
|
||||
|
||||
use crate::{FontImage, ImageDelta};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct Rectu {
|
||||
/// inclusive
|
||||
min_x: usize,
|
||||
|
||||
/// inclusive
|
||||
min_y: usize,
|
||||
|
||||
/// exclusive
|
||||
max_x: usize,
|
||||
|
||||
/// exclusive
|
||||
max_y: usize,
|
||||
}
|
||||
|
||||
impl Rectu {
|
||||
const NOTHING: Self = Self {
|
||||
min_x: usize::MAX,
|
||||
min_y: usize::MAX,
|
||||
max_x: 0,
|
||||
max_y: 0,
|
||||
};
|
||||
const EVERYTHING: Self = Self {
|
||||
min_x: 0,
|
||||
min_y: 0,
|
||||
max_x: usize::MAX,
|
||||
max_y: usize::MAX,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct PrerasterizedDisc {
|
||||
r: f32,
|
||||
uv: Rectu,
|
||||
}
|
||||
|
||||
/// A pre-rasterized disc (filled circle), somewhere in the texture atlas.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PreparedDisc {
|
||||
/// The radius of this disc in texels.
|
||||
pub r: f32,
|
||||
|
||||
/// Width in texels.
|
||||
pub w: f32,
|
||||
|
||||
/// Where in the texture atlas the disc is.
|
||||
/// Normalized in 0-1 range.
|
||||
pub uv: Rect,
|
||||
}
|
||||
|
||||
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
||||
///
|
||||
/// More characters can be added, possibly expanding the texture.
|
||||
#[derive(Clone)]
|
||||
pub struct TextureAtlas {
|
||||
image: FontImage,
|
||||
|
||||
/// What part of the image that is dirty
|
||||
dirty: Rectu,
|
||||
|
||||
/// Used for when allocating new rectangles.
|
||||
cursor: (usize, usize),
|
||||
|
||||
row_height: usize,
|
||||
|
||||
/// Set when someone requested more space than was available.
|
||||
overflowed: bool,
|
||||
|
||||
/// pre-rasterized discs of radii `2^i`, where `i` is the index.
|
||||
discs: Vec<PrerasterizedDisc>,
|
||||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
pub fn new(size: [usize; 2]) -> Self {
|
||||
assert!(size[0] >= 1024, "Tiny texture atlas");
|
||||
let mut atlas = Self {
|
||||
image: FontImage::new(size),
|
||||
dirty: Rectu::EVERYTHING,
|
||||
cursor: (0, 0),
|
||||
row_height: 0,
|
||||
overflowed: false,
|
||||
discs: vec![], // will be filled in below
|
||||
};
|
||||
|
||||
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
|
||||
let (pos, image) = atlas.allocate((1, 1));
|
||||
assert_eq!(pos, (0, 0));
|
||||
image[pos] = 1.0;
|
||||
|
||||
// Allocate a series of anti-aliased discs used to render small filled circles:
|
||||
// TODO(emilk): these circles can be packed A LOT better.
|
||||
// In fact, the whole texture atlas could be packed a lot better.
|
||||
// for r in [1, 2, 4, 8, 16, 32, 64] {
|
||||
// let w = 2 * r + 3;
|
||||
// let hw = w as i32 / 2;
|
||||
const LARGEST_CIRCLE_RADIUS: f32 = 64.0;
|
||||
for i in 0.. {
|
||||
let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0);
|
||||
if r > LARGEST_CIRCLE_RADIUS {
|
||||
break;
|
||||
}
|
||||
let hw = (r + 0.5).ceil() as i32;
|
||||
let w = (2 * hw + 1) as usize;
|
||||
let ((x, y), image) = atlas.allocate((w, w));
|
||||
for dx in -hw..=hw {
|
||||
for dy in -hw..=hw {
|
||||
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
||||
let coverage =
|
||||
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
|
||||
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
|
||||
coverage;
|
||||
}
|
||||
}
|
||||
atlas.discs.push(PrerasterizedDisc {
|
||||
r,
|
||||
uv: Rectu {
|
||||
min_x: x,
|
||||
min_y: y,
|
||||
max_x: x + w,
|
||||
max_y: y + w,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
atlas
|
||||
}
|
||||
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.image.size
|
||||
}
|
||||
|
||||
/// Returns the locations and sizes of pre-rasterized discs (filled circles) in this atlas.
|
||||
pub fn prepared_discs(&self) -> Vec<PreparedDisc> {
|
||||
let size = self.size();
|
||||
let inv_w = 1.0 / size[0] as f32;
|
||||
let inv_h = 1.0 / size[1] as f32;
|
||||
self.discs
|
||||
.iter()
|
||||
.map(|disc| {
|
||||
let r = disc.r;
|
||||
let Rectu {
|
||||
min_x,
|
||||
min_y,
|
||||
max_x,
|
||||
max_y,
|
||||
} = disc.uv;
|
||||
let w = max_x - min_x;
|
||||
let uv = Rect::from_min_max(
|
||||
emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h),
|
||||
emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h),
|
||||
);
|
||||
PreparedDisc { r, w: w as f32, uv }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn max_height(&self) -> usize {
|
||||
// the initial width is likely the max texture side size
|
||||
self.image.width()
|
||||
}
|
||||
|
||||
/// When this get high, it might be time to clear and start over!
|
||||
pub fn fill_ratio(&self) -> f32 {
|
||||
if self.overflowed {
|
||||
1.0
|
||||
} else {
|
||||
(self.cursor.1 + self.row_height) as f32 / self.max_height() as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Call to get the change to the image since last call.
|
||||
pub fn take_delta(&mut self) -> Option<ImageDelta> {
|
||||
let texture_options = crate::textures::TextureOptions::LINEAR;
|
||||
|
||||
let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING);
|
||||
if dirty == Rectu::NOTHING {
|
||||
None
|
||||
} else if dirty == Rectu::EVERYTHING {
|
||||
Some(ImageDelta::full(self.image.clone(), texture_options))
|
||||
} else {
|
||||
let pos = [dirty.min_x, dirty.min_y];
|
||||
let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
|
||||
let region = self.image.region(pos, size);
|
||||
Some(ImageDelta::partial(pos, region, texture_options))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the coordinates of where the rect ended up,
|
||||
/// and invalidates the region.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) {
|
||||
/// On some low-precision GPUs (my old iPad) characters get muddled up
|
||||
/// if we don't add some empty pixels between the characters.
|
||||
/// On modern high-precision GPUs this is not needed.
|
||||
const PADDING: usize = 1;
|
||||
|
||||
assert!(
|
||||
w <= self.image.width(),
|
||||
"Tried to allocate a {} wide glyph in a {} wide texture atlas",
|
||||
w,
|
||||
self.image.width()
|
||||
);
|
||||
if self.cursor.0 + w > self.image.width() {
|
||||
// New row:
|
||||
self.cursor.0 = 0;
|
||||
self.cursor.1 += self.row_height + PADDING;
|
||||
self.row_height = 0;
|
||||
}
|
||||
|
||||
self.row_height = self.row_height.max(h);
|
||||
|
||||
let required_height = self.cursor.1 + self.row_height;
|
||||
|
||||
if required_height > self.max_height() {
|
||||
// This is a bad place to be - we need to start reusing space :/
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::wan!("epaint texture atlas overflowed!");
|
||||
|
||||
self.cursor = (0, self.image.height() / 3); // Restart a bit down - the top of the atlas has too many important things in it
|
||||
self.overflowed = true; // this will signal the user that we need to recreate the texture atlas next frame.
|
||||
} else if resize_to_min_height(&mut self.image, required_height) {
|
||||
self.dirty = Rectu::EVERYTHING;
|
||||
}
|
||||
|
||||
let pos = self.cursor;
|
||||
self.cursor.0 += w + PADDING;
|
||||
|
||||
self.dirty.min_x = self.dirty.min_x.min(pos.0);
|
||||
self.dirty.min_y = self.dirty.min_y.min(pos.1);
|
||||
self.dirty.max_x = self.dirty.max_x.max(pos.0 + w);
|
||||
self.dirty.max_y = self.dirty.max_y.max(pos.1 + h);
|
||||
|
||||
(pos, &mut self.image)
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool {
|
||||
while required_height >= image.height() {
|
||||
image.size[1] *= 2; // double the height
|
||||
}
|
||||
|
||||
if image.width() * image.height() > image.pixels.len() {
|
||||
image.pixels.resize(image.width() * image.height(), 0.0);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
122
egui/crates/epaint/src/texture_handle.rs
Normal file
122
egui/crates/epaint/src/texture_handle.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
emath::NumExt, mutex::RwLock, textures::TextureOptions, ImageData, ImageDelta, TextureId,
|
||||
TextureManager,
|
||||
};
|
||||
|
||||
/// Used to paint images.
|
||||
///
|
||||
/// An _image_ is pixels stored in RAM, and represented using [`ImageData`].
|
||||
/// Before you can paint it however, you need to convert it to a _texture_.
|
||||
///
|
||||
/// If you are using egui, use `egui::Context::load_texture`.
|
||||
///
|
||||
/// The [`TextureHandle`] can be cloned cheaply.
|
||||
/// When the last [`TextureHandle`] for specific texture is dropped, the texture is freed.
|
||||
///
|
||||
/// See also [`TextureManager`].
|
||||
#[must_use]
|
||||
pub struct TextureHandle {
|
||||
tex_mngr: Arc<RwLock<TextureManager>>,
|
||||
id: TextureId,
|
||||
}
|
||||
|
||||
impl Drop for TextureHandle {
|
||||
fn drop(&mut self) {
|
||||
self.tex_mngr.write().free(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for TextureHandle {
|
||||
fn clone(&self) -> Self {
|
||||
self.tex_mngr.write().retain(self.id);
|
||||
Self {
|
||||
tex_mngr: self.tex_mngr.clone(),
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TextureHandle {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TextureHandle {}
|
||||
|
||||
impl std::hash::Hash for TextureHandle {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureHandle {
|
||||
/// If you are using egui, use `egui::Context::load_texture` instead.
|
||||
pub fn new(tex_mngr: Arc<RwLock<TextureManager>>, id: TextureId) -> Self {
|
||||
Self { tex_mngr, id }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> TextureId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Assign a new image to an existing texture.
|
||||
pub fn set(&mut self, image: impl Into<ImageData>, options: TextureOptions) {
|
||||
self.tex_mngr
|
||||
.write()
|
||||
.set(self.id, ImageDelta::full(image.into(), options));
|
||||
}
|
||||
|
||||
/// Assign a new image to a subregion of the whole texture.
|
||||
pub fn set_partial(
|
||||
&mut self,
|
||||
pos: [usize; 2],
|
||||
image: impl Into<ImageData>,
|
||||
options: TextureOptions,
|
||||
) {
|
||||
self.tex_mngr
|
||||
.write()
|
||||
.set(self.id, ImageDelta::partial(pos, image.into(), options));
|
||||
}
|
||||
|
||||
/// width x height
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().size
|
||||
}
|
||||
|
||||
/// width x height
|
||||
pub fn size_vec2(&self) -> crate::Vec2 {
|
||||
let [w, h] = self.size();
|
||||
crate::Vec2::new(w as f32, h as f32)
|
||||
}
|
||||
|
||||
/// width / height
|
||||
pub fn aspect_ratio(&self) -> f32 {
|
||||
let [w, h] = self.size();
|
||||
w as f32 / h.at_least(1) as f32
|
||||
}
|
||||
|
||||
/// Debug-name.
|
||||
pub fn name(&self) -> String {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TextureHandle> for TextureId {
|
||||
#[inline(always)]
|
||||
fn from(handle: &TextureHandle) -> Self {
|
||||
handle.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mut TextureHandle> for TextureId {
|
||||
#[inline(always)]
|
||||
fn from(handle: &mut TextureHandle) -> Self {
|
||||
handle.id()
|
||||
}
|
||||
}
|
||||
257
egui/crates/epaint/src/textures.rs
Normal file
257
egui/crates/epaint/src/textures.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
use crate::{ImageData, ImageDelta, TextureId};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Low-level manager for allocating textures.
|
||||
///
|
||||
/// Communicates with the painting subsystem using [`Self::take_delta`].
|
||||
#[derive(Default)]
|
||||
pub struct TextureManager {
|
||||
/// We allocate texture id:s linearly.
|
||||
next_id: u64,
|
||||
/// Information about currently allocated textures.
|
||||
metas: ahash::HashMap<TextureId, TextureMeta>,
|
||||
delta: TexturesDelta,
|
||||
}
|
||||
|
||||
impl TextureManager {
|
||||
/// Allocate a new texture.
|
||||
///
|
||||
/// The given name can be useful for later debugging.
|
||||
///
|
||||
/// The returned [`TextureId`] will be [`TextureId::Managed`], with an index
|
||||
/// starting from zero and increasing with each call to [`Self::alloc`].
|
||||
///
|
||||
/// The first texture you allocate will be `TextureId::Managed(0) == TextureId::default()` and
|
||||
/// MUST have a white pixel at (0,0) ([`crate::WHITE_UV`]).
|
||||
///
|
||||
/// The texture is given a retain-count of `1`, requiring one call to [`Self::free`] to free it.
|
||||
pub fn alloc(&mut self, name: String, image: ImageData, options: TextureOptions) -> TextureId {
|
||||
let id = TextureId::Managed(self.next_id);
|
||||
self.next_id += 1;
|
||||
|
||||
self.metas.entry(id).or_insert_with(|| TextureMeta {
|
||||
name,
|
||||
size: image.size(),
|
||||
bytes_per_pixel: image.bytes_per_pixel(),
|
||||
retain_count: 1,
|
||||
options,
|
||||
});
|
||||
|
||||
self.delta.set.push((id, ImageDelta::full(image, options)));
|
||||
id
|
||||
}
|
||||
|
||||
/// Assign a new image to an existing texture,
|
||||
/// or update a region of it.
|
||||
pub fn set(&mut self, id: TextureId, delta: ImageDelta) {
|
||||
if let Some(meta) = self.metas.get_mut(&id) {
|
||||
if let Some(pos) = delta.pos {
|
||||
crate::epaint_assert!(
|
||||
pos[0] + delta.image.width() <= meta.size[0]
|
||||
&& pos[1] + delta.image.height() <= meta.size[1],
|
||||
"Partial texture update is outside the bounds of texture {id:?}",
|
||||
);
|
||||
} else {
|
||||
// whole update
|
||||
meta.size = delta.image.size();
|
||||
meta.bytes_per_pixel = delta.image.bytes_per_pixel();
|
||||
// since we update the whole image, we can discard all old enqueued deltas
|
||||
self.delta.set.retain(|(x, _)| x != &id);
|
||||
}
|
||||
self.delta.set.push((id, delta));
|
||||
} else {
|
||||
crate::epaint_assert!(false, "Tried setting texture {id:?} which is not allocated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Free an existing texture.
|
||||
pub fn free(&mut self, id: TextureId) {
|
||||
if let std::collections::hash_map::Entry::Occupied(mut entry) = self.metas.entry(id) {
|
||||
let meta = entry.get_mut();
|
||||
meta.retain_count -= 1;
|
||||
if meta.retain_count == 0 {
|
||||
entry.remove();
|
||||
self.delta.free.push(id);
|
||||
}
|
||||
} else {
|
||||
crate::epaint_assert!(false, "Tried freeing texture {id:?} which is not allocated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Increase the retain-count of the given texture.
|
||||
///
|
||||
/// For each time you call [`Self::retain`] you must call [`Self::free`] on additional time.
|
||||
pub fn retain(&mut self, id: TextureId) {
|
||||
if let Some(meta) = self.metas.get_mut(&id) {
|
||||
meta.retain_count += 1;
|
||||
} else {
|
||||
crate::epaint_assert!(
|
||||
false,
|
||||
"Tried retaining texture {id:?} which is not allocated",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Take and reset changes since last frame.
|
||||
///
|
||||
/// These should be applied to the painting subsystem each frame.
|
||||
pub fn take_delta(&mut self) -> TexturesDelta {
|
||||
std::mem::take(&mut self.delta)
|
||||
}
|
||||
|
||||
/// Get meta-data about a specific texture.
|
||||
pub fn meta(&self, id: TextureId) -> Option<&TextureMeta> {
|
||||
self.metas.get(&id)
|
||||
}
|
||||
|
||||
/// Get meta-data about all allocated textures in some arbitrary order.
|
||||
pub fn allocated(&self) -> impl ExactSizeIterator<Item = (&TextureId, &TextureMeta)> {
|
||||
self.metas.iter()
|
||||
}
|
||||
|
||||
/// Total number of allocated textures.
|
||||
pub fn num_allocated(&self) -> usize {
|
||||
self.metas.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Meta-data about an allocated texture.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TextureMeta {
|
||||
/// A human-readable name useful for debugging.
|
||||
pub name: String,
|
||||
|
||||
/// width x height
|
||||
pub size: [usize; 2],
|
||||
|
||||
/// 4 or 1
|
||||
pub bytes_per_pixel: usize,
|
||||
|
||||
/// Free when this reaches zero.
|
||||
pub retain_count: usize,
|
||||
|
||||
/// The texture filtering mode to use when rendering.
|
||||
pub options: TextureOptions,
|
||||
}
|
||||
|
||||
impl TextureMeta {
|
||||
/// Size in bytes.
|
||||
/// width x height x [`Self::bytes_per_pixel`].
|
||||
pub fn bytes_used(&self) -> usize {
|
||||
self.size[0] * self.size[1] * self.bytes_per_pixel
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How the texture texels are filtered.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TextureOptions {
|
||||
/// How to filter when magnifying (when texels are larger than pixels).
|
||||
pub magnification: TextureFilter,
|
||||
|
||||
/// How to filter when minifying (when texels are smaller than pixels).
|
||||
pub minification: TextureFilter,
|
||||
}
|
||||
|
||||
impl TextureOptions {
|
||||
/// Linear magnification and minification.
|
||||
pub const LINEAR: Self = Self {
|
||||
magnification: TextureFilter::Linear,
|
||||
minification: TextureFilter::Linear,
|
||||
};
|
||||
|
||||
/// Nearest magnification and minification.
|
||||
pub const NEAREST: Self = Self {
|
||||
magnification: TextureFilter::Nearest,
|
||||
minification: TextureFilter::Nearest,
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for TextureOptions {
|
||||
/// The default is linear for both magnification and minification.
|
||||
fn default() -> Self {
|
||||
Self::LINEAR
|
||||
}
|
||||
}
|
||||
|
||||
/// How the texture texels are filtered.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum TextureFilter {
|
||||
/// Show the nearest pixel value.
|
||||
///
|
||||
/// When zooming in you will get sharp, square pixels/texels.
|
||||
/// When zooming out you will get a very crisp (and aliased) look.
|
||||
Nearest,
|
||||
|
||||
/// Linearly interpolate the nearest neighbors, creating a smoother look when zooming in and out.
|
||||
Linear,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// What has been allocated and freed during the last period.
|
||||
///
|
||||
/// These are commands given to the integration painter.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[must_use = "The painter must take care of this"]
|
||||
pub struct TexturesDelta {
|
||||
/// New or changed textures. Apply before painting.
|
||||
pub set: Vec<(TextureId, ImageDelta)>,
|
||||
|
||||
/// Textures to free after painting.
|
||||
pub free: Vec<TextureId>,
|
||||
}
|
||||
|
||||
impl TexturesDelta {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.set.is_empty() && self.free.is_empty()
|
||||
}
|
||||
|
||||
pub fn append(&mut self, mut newer: TexturesDelta) {
|
||||
self.set.extend(newer.set.into_iter());
|
||||
self.free.append(&mut newer.free);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
self.free.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TexturesDelta {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let mut debug_struct = f.debug_struct("TexturesDelta");
|
||||
if !self.set.is_empty() {
|
||||
let mut string = String::new();
|
||||
for (tex_id, delta) in &self.set {
|
||||
let size = delta.image.size();
|
||||
if let Some(pos) = delta.pos {
|
||||
write!(
|
||||
string,
|
||||
"{:?} partial ([{} {}] - [{} {}]), ",
|
||||
tex_id,
|
||||
pos[0],
|
||||
pos[1],
|
||||
pos[0] + size[0],
|
||||
pos[1] + size[1]
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok();
|
||||
}
|
||||
}
|
||||
debug_struct.field("set", &string);
|
||||
}
|
||||
if !self.free.is_empty() {
|
||||
debug_struct.field("free", &self.free);
|
||||
}
|
||||
debug_struct.finish()
|
||||
}
|
||||
}
|
||||
16
egui/crates/epaint/src/util/mod.rs
Normal file
16
egui/crates/epaint/src/util/mod.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
mod ordered_float;
|
||||
|
||||
pub use ordered_float::*;
|
||||
|
||||
/// Hash the given value with a predictable hasher.
|
||||
#[inline]
|
||||
pub fn hash(value: impl std::hash::Hash) -> u64 {
|
||||
ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(value)
|
||||
}
|
||||
|
||||
/// Hash the given value with the given hasher.
|
||||
#[inline]
|
||||
pub fn hash_with(value: impl std::hash::Hash, mut hasher: impl std::hash::Hasher) -> u64 {
|
||||
value.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
130
egui/crates/epaint/src/util/ordered_float.rs
Normal file
130
egui/crates/epaint/src/util/ordered_float.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//! Total order on floating point types.
|
||||
//! Can be used for sorting, min/max computation, and other collection algorithms.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Wraps a floating-point value to add total order and hash.
|
||||
/// Possible types for `T` are `f32` and `f64`.
|
||||
///
|
||||
/// See also [`FloatOrd`].
|
||||
pub struct OrderedFloat<T>(T);
|
||||
|
||||
impl<T: Float> Eq for OrderedFloat<T> {}
|
||||
|
||||
impl<T: Float> PartialEq<Self> for OrderedFloat<T> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// NaNs are considered equal (equivalent) when it comes to ordering
|
||||
if self.0.is_nan() {
|
||||
other.0.is_nan()
|
||||
} else {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float> PartialOrd<Self> for OrderedFloat<T> {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.0.partial_cmp(&other.0) {
|
||||
Some(ord) => Some(ord),
|
||||
None => Some(self.0.is_nan().cmp(&other.0.is_nan())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float> Ord for OrderedFloat<T> {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.partial_cmp(other) {
|
||||
Some(ord) => ord,
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float> Hash for OrderedFloat<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Extension trait to provide `ord()` method.
|
||||
///
|
||||
/// Example with `f64`:
|
||||
/// ```
|
||||
/// use epaint::util::FloatOrd;
|
||||
///
|
||||
/// let array = [1.0, 2.5, 2.0];
|
||||
/// let max = array.iter().max_by_key(|val| val.ord());
|
||||
///
|
||||
/// assert_eq!(max, Some(&2.5));
|
||||
/// ```
|
||||
pub trait FloatOrd {
|
||||
/// Type to provide total order, useful as key in sorted contexts.
|
||||
fn ord(self) -> OrderedFloat<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl FloatOrd for f32 {
|
||||
#[inline]
|
||||
fn ord(self) -> OrderedFloat<f32> {
|
||||
OrderedFloat(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatOrd for f64 {
|
||||
#[inline]
|
||||
fn ord(self) -> OrderedFloat<f64> {
|
||||
OrderedFloat(self)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Internal abstraction over floating point types
|
||||
#[doc(hidden)]
|
||||
pub trait Float: PartialOrd + PartialEq + private::FloatImpl {}
|
||||
|
||||
impl Float for f32 {}
|
||||
|
||||
impl Float for f64 {}
|
||||
|
||||
// Keep this trait in private module, to avoid exposing its methods as extensions in user code
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait FloatImpl {
|
||||
fn is_nan(&self) -> bool;
|
||||
|
||||
fn hash<H: Hasher>(&self, state: &mut H);
|
||||
}
|
||||
|
||||
impl FloatImpl for f32 {
|
||||
#[inline]
|
||||
fn is_nan(&self) -> bool {
|
||||
f32::is_nan(*self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
crate::f32_hash(state, *self);
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatImpl for f64 {
|
||||
#[inline]
|
||||
fn is_nan(&self) -> bool {
|
||||
f64::is_nan(*self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
crate::f64_hash(state, *self);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue