mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Break out mod paint into new crate epaint
This commit is contained in:
41
epaint/Cargo.toml
Normal file
41
epaint/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "epaint"
|
||||
version = "0.7.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Minimal 2D graphics library for GUI work"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["gui", "graphics"]
|
||||
keywords = ["gui", "graphics"]
|
||||
include = [
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
"fonts/*.ttf",
|
||||
]
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
emath = { path = "../emath" }
|
||||
|
||||
ahash = { version = "0.6", features = ["std"], default-features = false }
|
||||
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
||||
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||
rusttype = "0.9"
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["multi_threaded", "default_fonts"]
|
||||
persistence = ["serde", "emath/serde"]
|
||||
|
||||
# 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 = []
|
||||
|
||||
single_threaded = ["atomic_refcell"]
|
||||
|
||||
# Only needed if you plan to use the same fonts from multiple threads.
|
||||
multi_threaded = ["parking_lot"]
|
||||
5
epaint/README.md
Normal file
5
epaint/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# epaint - Egui Paint Library
|
||||
|
||||
A bare-bones 2D graphics library for turning simple 2D shapes and text into textured triangles.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
||||
BIN
epaint/fonts/NotoEmoji-Regular.ttf
Normal file
BIN
epaint/fonts/NotoEmoji-Regular.ttf
Normal file
Binary file not shown.
92
epaint/fonts/OFL.txt
Normal file
92
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.
|
||||
BIN
epaint/fonts/ProggyClean.ttf
Normal file
BIN
epaint/fonts/ProggyClean.ttf
Normal file
Binary file not shown.
96
epaint/fonts/UFL.txt
Executable file
96
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
epaint/fonts/Ubuntu-Light.ttf
Executable file
BIN
epaint/fonts/Ubuntu-Light.ttf
Executable file
Binary file not shown.
9
epaint/fonts/emoji-icon-font-mit-license.txt
Normal file
9
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
epaint/fonts/emoji-icon-font.ttf
Normal file
BIN
epaint/fonts/emoji-icon-font.ttf
Normal file
Binary file not shown.
32
epaint/fonts/list_fonts.py
Normal file
32
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()
|
||||
570
epaint/src/color.rs
Normal file
570
epaint/src/color.rs
Normal file
@@ -0,0 +1,570 @@
|
||||
use emath::clamp;
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
/// Instead of manipulating this directly it is often better
|
||||
/// to first convert it to either [`Rgba`] or [`Hsva`].
|
||||
///
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
||||
/// Alpha channel is in linear space.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Color32(pub(crate) [u8; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Color32 {
|
||||
type Output = u8;
|
||||
fn index(&self, index: usize) -> &u8 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Color32 {
|
||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Replaced by Color32::from_rgb… family of functions."]
|
||||
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Color32 {
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
|
||||
impl Color32 {
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
||||
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
||||
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
||||
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
||||
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
||||
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
||||
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
||||
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
||||
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(140, 160, 255);
|
||||
|
||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 255])
|
||||
}
|
||||
|
||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha.
|
||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
if a == 255 {
|
||||
Self::from_rgba_premultiplied(r, g, b, 255) // common-case optimization
|
||||
} else if a == 0 {
|
||||
Self::TRANSPARENT // common-case optimization
|
||||
} else {
|
||||
let r_lin = linear_from_gamma_byte(r);
|
||||
let g_lin = linear_from_gamma_byte(g);
|
||||
let b_lin = linear_from_gamma_byte(b);
|
||||
let a_lin = linear_from_alpha_byte(a);
|
||||
|
||||
let r = gamma_byte_from_linear(r_lin * a_lin);
|
||||
let g = gamma_byte_from_linear(g_lin * a_lin);
|
||||
let b = gamma_byte_from_linear(b_lin * a_lin);
|
||||
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Use from_rgb(..), from_rgba_premultiplied(..) or from_srgba_unmultiplied(..)"]
|
||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
pub const fn from_gray(l: u8) -> Self {
|
||||
Self([l, l, l, 255])
|
||||
}
|
||||
|
||||
pub const fn from_black_alpha(a: u8) -> Self {
|
||||
Self([0, 0, 0, a])
|
||||
}
|
||||
|
||||
pub fn from_white_alpha(a: u8) -> Self {
|
||||
Rgba::from_white_alpha(linear_from_alpha_byte(a)).into()
|
||||
}
|
||||
|
||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||
Self([l, l, l, 0])
|
||||
}
|
||||
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.a() == 255
|
||||
}
|
||||
|
||||
pub fn r(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
pub fn g(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
pub fn b(&self) -> u8 {
|
||||
self.0[2]
|
||||
}
|
||||
pub fn a(&self) -> u8 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(self) -> Self {
|
||||
Rgba::from(self).to_opaque().into()
|
||||
}
|
||||
|
||||
pub fn to_array(&self) -> [u8; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
pub fn to_tuple(&self) -> (u8, u8, u8, u8) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Rgba(pub(crate) [f32; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Rgba {
|
||||
type Output = f32;
|
||||
fn index(&self, index: usize) -> &f32 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Rgba {
|
||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Rgba {
|
||||
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
|
||||
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
|
||||
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
|
||||
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
|
||||
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
|
||||
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
|
||||
|
||||
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
|
||||
Self([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
pub const fn from_gray(l: f32) -> Self {
|
||||
Self([l, l, l, 1.0])
|
||||
}
|
||||
|
||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||
debug_assert!(0.0 <= l && l <= 1.0);
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([l * a, l * a, l * a, a])
|
||||
}
|
||||
|
||||
/// Transparent black
|
||||
pub fn from_black_alpha(a: f32) -> Self {
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([0.0, 0.0, 0.0, a])
|
||||
}
|
||||
|
||||
/// Transparent white
|
||||
pub fn from_white_alpha(a: f32) -> Self {
|
||||
debug_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([a, a, a, a])
|
||||
}
|
||||
|
||||
/// Return an additive version of this color (alpha = 0)
|
||||
pub fn additive(self) -> Self {
|
||||
let [r, g, b, _] = self.0;
|
||||
Self([r, g, b, 0.0])
|
||||
}
|
||||
|
||||
/// Multiply with e.g. 0.5 to make us half transparent
|
||||
pub fn multiply(self, alpha: f32) -> Self {
|
||||
Self([
|
||||
alpha * self[0],
|
||||
alpha * self[1],
|
||||
alpha * self[2],
|
||||
alpha * self[3],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn r(&self) -> f32 {
|
||||
self.0[0]
|
||||
}
|
||||
pub fn g(&self) -> f32 {
|
||||
self.0[1]
|
||||
}
|
||||
pub fn b(&self) -> f32 {
|
||||
self.0[2]
|
||||
}
|
||||
pub fn a(&self) -> f32 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// How perceptually intense (bright) is the color?
|
||||
pub fn intensity(&self) -> f32 {
|
||||
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(&self) -> Self {
|
||||
if self.a() == 0.0 {
|
||||
// Additive or fully transparent black.
|
||||
Self::from_rgba_premultiplied(self.r(), self.g(), self.b(), 1.0)
|
||||
} else {
|
||||
// un-multiply alpha:
|
||||
Self::from_rgba_premultiplied(
|
||||
self.r() / self.a(),
|
||||
self.g() / self.a(),
|
||||
self.b() / self.a(),
|
||||
1.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Rgba {
|
||||
type Output = Rgba;
|
||||
fn add(self, rhs: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] + rhs[0],
|
||||
self[1] + rhs[1],
|
||||
self[2] + rhs[2],
|
||||
self[3] + rhs[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for Rgba {
|
||||
type Output = Rgba;
|
||||
fn mul(self, other: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * other[0],
|
||||
self[1] * other[1],
|
||||
self[2] * other[2],
|
||||
self[3] * other[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Rgba {
|
||||
type Output = Rgba;
|
||||
fn mul(self, factor: f32) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * factor,
|
||||
self[1] * factor,
|
||||
self[2] * factor,
|
||||
self[3] * factor,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for f32 {
|
||||
type Output = Rgba;
|
||||
fn mul(self, rgba: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self * rgba[0],
|
||||
self * rgba[1],
|
||||
self * rgba[2],
|
||||
self * rgba[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Color conversion:
|
||||
|
||||
impl From<Color32> for Rgba {
|
||||
fn from(srgba: Color32) -> Rgba {
|
||||
Rgba([
|
||||
linear_from_gamma_byte(srgba[0]),
|
||||
linear_from_gamma_byte(srgba[1]),
|
||||
linear_from_gamma_byte(srgba[2]),
|
||||
linear_from_alpha_byte(srgba[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color32 {
|
||||
fn from(rgba: Rgba) -> Color32 {
|
||||
Color32([
|
||||
gamma_byte_from_linear(rgba[0]),
|
||||
gamma_byte_from_linear(rgba[1]),
|
||||
gamma_byte_from_linear(rgba[2]),
|
||||
alpha_byte_from_linear(rgba[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// [0, 255] -> [0, 1]
|
||||
fn linear_from_gamma_byte(s: u8) -> f32 {
|
||||
if s <= 10 {
|
||||
s as f32 / 3294.6
|
||||
} else {
|
||||
((s as f32 + 14.025) / 269.025).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_from_alpha_byte(a: u8) -> f32 {
|
||||
a as f32 / 255.0
|
||||
}
|
||||
|
||||
/// [0, 1] -> [0, 255]
|
||||
fn gamma_byte_from_linear(l: f32) -> u8 {
|
||||
if l <= 0.0 {
|
||||
0
|
||||
} else if l <= 0.0031308 {
|
||||
(3294.6 * l).round() as u8
|
||||
} else if l <= 1.0 {
|
||||
(269.025 * l.powf(1.0 / 2.4) - 14.025).round() as u8
|
||||
} else {
|
||||
255
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha_byte_from_linear(a: f32) -> u8 {
|
||||
clamp(a * 255.0, 0.0..=255.0).round() as u8
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_srgba_conversion() {
|
||||
#![allow(clippy::float_cmp)]
|
||||
for b in 0..=255 {
|
||||
let l = linear_from_gamma_byte(b);
|
||||
assert!(0.0 <= l && l <= 1.0);
|
||||
assert_eq!(gamma_byte_from_linear(l), b);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||
/// No premultiplied alpha.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Hsva {
|
||||
/// hue 0-1
|
||||
pub h: f32,
|
||||
/// saturation 0-1
|
||||
pub s: f32,
|
||||
/// value 0-1
|
||||
pub v: f32,
|
||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Hsva {
|
||||
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
|
||||
Self { h, s, v, a }
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha
|
||||
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_premultiplied([
|
||||
linear_from_gamma_byte(srgba[0]),
|
||||
linear_from_gamma_byte(srgba[1]),
|
||||
linear_from_gamma_byte(srgba[2]),
|
||||
linear_from_alpha_byte(srgba[3]),
|
||||
])
|
||||
}
|
||||
|
||||
/// From `sRGBA` without premultiplied alpha
|
||||
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_unmultiplied([
|
||||
linear_from_gamma_byte(srgba[0]),
|
||||
linear_from_gamma_byte(srgba[1]),
|
||||
linear_from_gamma_byte(srgba[2]),
|
||||
linear_from_alpha_byte(srgba[3]),
|
||||
])
|
||||
}
|
||||
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
pub fn from_rgba_premultiplied([r, g, b, a]: [f32; 4]) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
if a == 0.0 {
|
||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Hsva::default()
|
||||
} else {
|
||||
Hsva::from_additive_rgb([r, g, b])
|
||||
}
|
||||
} else {
|
||||
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
}
|
||||
|
||||
/// From linear RGBA without premultiplied alpha
|
||||
pub fn from_rgba_unmultiplied([r, g, b, a]: [f32; 4]) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
|
||||
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva {
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
a: -0.5, // anything negative is treated as additive
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva { h, s, v, a: 1.0 }
|
||||
}
|
||||
|
||||
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
|
||||
Self::from_rgb([
|
||||
linear_from_gamma_byte(r),
|
||||
linear_from_gamma_byte(g),
|
||||
linear_from_gamma_byte(b),
|
||||
])
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub fn to_rgb(&self) -> [f32; 3] {
|
||||
rgb_from_hsv((self.h, self.s, self.v))
|
||||
}
|
||||
|
||||
pub fn to_srgb(&self) -> [u8; 3] {
|
||||
let [r, g, b] = self.to_rgb();
|
||||
[
|
||||
gamma_byte_from_linear(r),
|
||||
gamma_byte_from_linear(g),
|
||||
gamma_byte_from_linear(b),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
let additive = a < 0.0;
|
||||
if additive {
|
||||
[r, g, b, 0.0]
|
||||
} else {
|
||||
[a * r, a * g, a * b, a]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents additive colors using a negative alpha.
|
||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
||||
let Hsva { h, s, v, a } = *self;
|
||||
let [r, g, b] = rgb_from_hsv((h, s, v));
|
||||
[r, g, b, a]
|
||||
}
|
||||
|
||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
||||
[
|
||||
gamma_byte_from_linear(r),
|
||||
gamma_byte_from_linear(g),
|
||||
gamma_byte_from_linear(b),
|
||||
alpha_byte_from_linear(a),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_byte_from_linear(r),
|
||||
gamma_byte_from_linear(g),
|
||||
gamma_byte_from_linear(b),
|
||||
alpha_byte_from_linear(a.abs()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Rgba {
|
||||
fn from(hsva: Hsva) -> Rgba {
|
||||
Rgba(hsva.to_rgba_premultiplied())
|
||||
}
|
||||
}
|
||||
impl From<Rgba> for Hsva {
|
||||
fn from(rgba: Rgba) -> Hsva {
|
||||
Self::from_rgba_premultiplied(rgba.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Color32 {
|
||||
fn from(hsva: Hsva) -> Color32 {
|
||||
Color32::from(Rgba::from(hsva))
|
||||
}
|
||||
}
|
||||
impl From<Color32> for Hsva {
|
||||
fn from(srgba: Color32) -> Hsva {
|
||||
Hsva::from(Rgba::from(srgba))
|
||||
}
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let min = r.min(g.min(b));
|
||||
let max = r.max(g.max(b)); // value
|
||||
|
||||
let range = max - min;
|
||||
|
||||
let h = if max == min {
|
||||
0.0 // hue is undefined
|
||||
} else if max == r {
|
||||
(g - b) / (6.0 * range)
|
||||
} else if max == g {
|
||||
(b - r) / (6.0 * range) + 1.0 / 3.0
|
||||
} else {
|
||||
// max == b
|
||||
(r - g) / (6.0 * range) + 2.0 / 3.0
|
||||
};
|
||||
let h = (h + 1.0).fract(); // wrap
|
||||
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
|
||||
(h, s, max)
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let h = (h.fract() + 1.0).fract(); // wrap
|
||||
let s = clamp(s, 0.0..=1.0);
|
||||
|
||||
let f = h * 6.0 - (h * 6.0).floor();
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - f * s);
|
||||
let t = v * (1.0 - (1.0 - f) * s);
|
||||
|
||||
match (h * 6.0).floor() as i32 % 6 {
|
||||
0 => [v, t, p],
|
||||
1 => [q, v, p],
|
||||
2 => [p, v, t],
|
||||
3 => [p, q, v],
|
||||
4 => [t, p, v],
|
||||
5 => [v, p, q],
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // a bit expensive
|
||||
fn test_hsv_roundtrip() {
|
||||
for r in 0..=255 {
|
||||
for g in 0..=255 {
|
||||
for b in 0..=255 {
|
||||
let srgba = Color32::from_rgb(r, g, b);
|
||||
let hsva = Hsva::from(srgba);
|
||||
assert_eq!(srgba, Color32::from(hsva));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
epaint/src/lib.rs
Normal file
103
epaint/src/lib.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! 2D graphics/rendering. Fonts, textures, color, geometry, tessellation etc.
|
||||
|
||||
#![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::await_holding_lock,
|
||||
clippy::dbg_macro,
|
||||
clippy::doc_markdown,
|
||||
clippy::empty_enum,
|
||||
clippy::enum_glob_use,
|
||||
clippy::exit,
|
||||
clippy::filter_map_next,
|
||||
clippy::fn_params_excessive_bools,
|
||||
clippy::if_let_mutex,
|
||||
clippy::imprecise_flops,
|
||||
clippy::inefficient_to_string,
|
||||
clippy::linkedlist,
|
||||
clippy::lossy_float_literal,
|
||||
clippy::macro_use_imports,
|
||||
clippy::match_on_vec_items,
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::mem_forget,
|
||||
clippy::mismatched_target_os,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::needless_borrow,
|
||||
clippy::needless_continue,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::option_option,
|
||||
clippy::pub_enum_variant_names,
|
||||
clippy::rest_pat_in_fully_bound_structs,
|
||||
clippy::todo,
|
||||
clippy::unimplemented,
|
||||
clippy::unnested_or_patterns,
|
||||
clippy::verbose_file_reads,
|
||||
future_incompatible,
|
||||
missing_crate_level_docs,
|
||||
missing_doc_code_examples,
|
||||
// missing_docs,
|
||||
nonstandard_style,
|
||||
rust_2018_idioms,
|
||||
unused_doc_comments,
|
||||
)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
pub mod color;
|
||||
pub mod mutex;
|
||||
mod shadow;
|
||||
pub mod shape;
|
||||
pub mod stats;
|
||||
mod stroke;
|
||||
pub mod tessellator;
|
||||
pub mod text;
|
||||
mod texture_atlas;
|
||||
mod triangles;
|
||||
|
||||
pub use {
|
||||
color::{Color32, Rgba},
|
||||
shadow::Shadow,
|
||||
shape::Shape,
|
||||
stats::PaintStats,
|
||||
stroke::Stroke,
|
||||
tessellator::{PaintJob, PaintJobs, TessellationOptions},
|
||||
text::{Galley, TextStyle},
|
||||
texture_atlas::{Texture, TextureAtlas},
|
||||
triangles::{Triangles, Vertex},
|
||||
};
|
||||
|
||||
pub use ahash;
|
||||
pub use emath;
|
||||
|
||||
/// 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 [`Triangles`] mesh.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum TextureId {
|
||||
/// The Egui font texture.
|
||||
/// If you don't want to use a texture, pick this and the [`WHITE_UV`] for uv-coord.
|
||||
Egui,
|
||||
|
||||
/// Your own texture, defined in any which way you want.
|
||||
/// Egui won't care. The backend renderer will presumably use this to look up what texture to use.
|
||||
User(u64),
|
||||
}
|
||||
|
||||
impl Default for TextureId {
|
||||
fn default() -> Self {
|
||||
Self::Egui
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PaintRect {
|
||||
pub rect: emath::Rect,
|
||||
/// How rounded the corners are. Use `0.0` for no rounding.
|
||||
pub corner_radius: f32,
|
||||
pub fill: Color32,
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
142
epaint/src/mutex.rs
Normal file
142
epaint/src/mutex.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! Helper module that wraps some Mutex types with different implementations.
|
||||
//!
|
||||
//! When the `single_threaded` feature is on the mutexes will panic when locked from different threads.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The lock you get from [`Mutex`].
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
pub use parking_lot::MutexGuard;
|
||||
|
||||
/// Provides interior mutability. Only thread-safe if the `multi_threaded` feature is enabled.
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T>(parking_lot::Mutex<T>);
|
||||
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
impl<T> Mutex<T> {
|
||||
#[inline(always)]
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(parking_lot::Mutex::new(val))
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
// TODO: detect if we are trying to lock the same mutex from the same thread (bad)
|
||||
// vs locking it from another thread (fine).
|
||||
// At the moment we just panic on any double-locking of a mutex (so no multithreaded support in debug builds)
|
||||
self.0
|
||||
.try_lock()
|
||||
.expect("The Mutex is already locked. Probably a bug")
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||
self.0.lock()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
/// The lock you get from [`RwLock::read`].
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
pub use parking_lot::RwLockReadGuard;
|
||||
|
||||
/// The lock you get from [`RwLock::write`].
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
pub use parking_lot::RwLockWriteGuard;
|
||||
|
||||
/// Provides interior mutability. Only thread-safe if the `multi_threaded` feature is enabled.
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
#[derive(Default)]
|
||||
pub struct RwLock<T>(parking_lot::RwLock<T>);
|
||||
|
||||
#[cfg(feature = "multi_threaded")]
|
||||
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> {
|
||||
self.0.read()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, T> {
|
||||
self.0.write()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// `atomic_refcell` will panic if multiple threads try to access the same value
|
||||
|
||||
/// The lock you get from [`Mutex`].
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
pub use atomic_refcell::AtomicRefMut as MutexGuard;
|
||||
|
||||
/// Provides interior mutability. Only thread-safe if the `multi_threaded` feature is enabled.
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
#[derive(Default)]
|
||||
pub struct Mutex<T>(atomic_refcell::AtomicRefCell<T>);
|
||||
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
/// The lock you get from [`RwLock::read`].
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
pub use atomic_refcell::AtomicRef as RwLockReadGuard;
|
||||
|
||||
/// The lock you get from [`RwLock::write`].
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
pub use atomic_refcell::AtomicRefMut as RwLockWriteGuard;
|
||||
|
||||
/// Provides interior mutability. Only thread-safe if the `multi_threaded` feature is enabled.
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
#[derive(Default)]
|
||||
pub struct RwLock<T>(atomic_refcell::AtomicRefCell<T>);
|
||||
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
impl<T> Clone for Mutex<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(self.lock().clone())
|
||||
}
|
||||
}
|
||||
49
epaint/src/shadow.rs
Normal file
49
epaint/src/shadow.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Shadow {
|
||||
// The shadow extends this much outside the rect.
|
||||
pub extrusion: f32,
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
impl Shadow {
|
||||
/// Tooltips, menus, ...
|
||||
pub fn small() -> Self {
|
||||
Self {
|
||||
extrusion: 8.0,
|
||||
color: Color32::from_black_alpha(64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows
|
||||
pub fn big() -> Self {
|
||||
Self {
|
||||
extrusion: 32.0,
|
||||
color: Color32::from_black_alpha(96),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tessellate(&self, rect: emath::Rect, corner_radius: f32) -> Triangles {
|
||||
// tessellator.clip_rect = clip_rect; // TODO: culling
|
||||
|
||||
let Self { extrusion, color } = *self;
|
||||
|
||||
use crate::tessellator::*;
|
||||
let rect = PaintRect {
|
||||
rect: rect.expand(0.5 * extrusion),
|
||||
corner_radius: corner_radius + 0.5 * extrusion,
|
||||
fill: color,
|
||||
stroke: Default::default(),
|
||||
};
|
||||
let mut tessellator = Tessellator::from_options(TessellationOptions {
|
||||
aa_size: extrusion,
|
||||
anti_alias: true,
|
||||
..Default::default()
|
||||
});
|
||||
let mut triangles = Triangles::default();
|
||||
tessellator.tessellate_rect(&rect, &mut triangles);
|
||||
triangles
|
||||
}
|
||||
}
|
||||
192
epaint/src/shape.rs
Normal file
192
epaint/src/shape.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use crate::{
|
||||
text::{Fonts, Galley, TextStyle},
|
||||
Color32, Stroke, Triangles,
|
||||
};
|
||||
use emath::*;
|
||||
|
||||
/// A paint primitive such as a circle or a piece of text.
|
||||
/// Coordinates are all screen space points (not physical pixels).
|
||||
#[derive(Clone, Debug)]
|
||||
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 {
|
||||
center: Pos2,
|
||||
radius: f32,
|
||||
fill: Color32,
|
||||
stroke: Stroke,
|
||||
},
|
||||
LineSegment {
|
||||
points: [Pos2; 2],
|
||||
stroke: Stroke,
|
||||
},
|
||||
Path {
|
||||
points: Vec<Pos2>,
|
||||
/// If true, connect the first and last of the points together.
|
||||
/// This is required if `fill != TRANSPARENT`.
|
||||
closed: bool,
|
||||
fill: Color32,
|
||||
stroke: Stroke,
|
||||
},
|
||||
Rect {
|
||||
rect: Rect,
|
||||
/// How rounded the corners are. Use `0.0` for no rounding.
|
||||
corner_radius: f32,
|
||||
fill: Color32,
|
||||
stroke: Stroke,
|
||||
},
|
||||
Text {
|
||||
/// Top left corner of the first character.
|
||||
pos: Pos2,
|
||||
/// The layed out text
|
||||
galley: Galley,
|
||||
text_style: TextStyle, // TODO: Font?
|
||||
color: Color32,
|
||||
},
|
||||
Triangles(Triangles),
|
||||
}
|
||||
|
||||
/// ## Constructors
|
||||
impl Shape {
|
||||
pub fn line_segment(points: [Pos2; 2], stroke: impl Into<Stroke>) -> Self {
|
||||
Self::LineSegment {
|
||||
points,
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Path {
|
||||
points,
|
||||
closed: false,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closed_line(points: Vec<Pos2>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Path {
|
||||
points,
|
||||
closed: true,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn polygon(points: Vec<Pos2>, fill: impl Into<Color32>, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Path {
|
||||
points,
|
||||
closed: true,
|
||||
fill: fill.into(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into<Color32>) -> Self {
|
||||
Self::Circle {
|
||||
center,
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Circle {
|
||||
center,
|
||||
radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rect_filled(rect: Rect, corner_radius: f32, fill_color: impl Into<Color32>) -> Self {
|
||||
Self::Rect {
|
||||
rect,
|
||||
corner_radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rect_stroke(rect: Rect, corner_radius: f32, stroke: impl Into<Stroke>) -> Self {
|
||||
Self::Rect {
|
||||
rect,
|
||||
corner_radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(
|
||||
fonts: &Fonts,
|
||||
pos: Pos2,
|
||||
anchor: Align2,
|
||||
text: impl Into<String>,
|
||||
text_style: TextStyle,
|
||||
color: Color32,
|
||||
) -> Self {
|
||||
let font = &fonts[text_style];
|
||||
let galley = font.layout_multiline(text.into(), f32::INFINITY);
|
||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size));
|
||||
Self::Text {
|
||||
pos: rect.min,
|
||||
galley,
|
||||
text_style,
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Operations
|
||||
impl Shape {
|
||||
pub fn triangles(triangles: Triangles) -> Self {
|
||||
debug_assert!(triangles.is_valid());
|
||||
Self::Triangles(triangles)
|
||||
}
|
||||
|
||||
pub fn texture_id(&self) -> super::TextureId {
|
||||
if let Shape::Triangles(triangles) = self {
|
||||
triangles.texture_id
|
||||
} else {
|
||||
super::TextureId::Egui
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate location by this much, 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 { center, .. } => {
|
||||
*center += delta;
|
||||
}
|
||||
Shape::LineSegment { points, .. } => {
|
||||
for p in points {
|
||||
*p += delta;
|
||||
}
|
||||
}
|
||||
Shape::Path { points, .. } => {
|
||||
for p in points {
|
||||
*p += delta;
|
||||
}
|
||||
}
|
||||
Shape::Rect { rect, .. } => {
|
||||
*rect = rect.translate(delta);
|
||||
}
|
||||
Shape::Text { pos, .. } => {
|
||||
*pos += delta;
|
||||
}
|
||||
Shape::Triangles(triangles) => {
|
||||
triangles.translate(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
208
epaint/src/stats.rs
Normal file
208
epaint/src/stats.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use {crate::*, emath::*};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ElementSize {
|
||||
Unknown,
|
||||
Homogeneous(usize),
|
||||
Heterogenous,
|
||||
}
|
||||
|
||||
impl Default for ElementSize {
|
||||
fn default() -> Self {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[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 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::Triangles(triangles) => Self::from_triangles(triangles),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn from_galley(galley: &Galley) -> Self {
|
||||
Self::from_slice(galley.text.as_bytes()) + Self::from_slice(&galley.rows)
|
||||
}
|
||||
|
||||
pub fn from_triangles(triangles: &Triangles) -> Self {
|
||||
Self::from_slice(&triangles.indices) + Self::from_slice(&triangles.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} {:12}", 0, what)
|
||||
} else if self.num_allocs() == 1 {
|
||||
format!(
|
||||
"{:6} {:12} {} 1 allocation",
|
||||
self.num_elements,
|
||||
what,
|
||||
self.megabytes()
|
||||
)
|
||||
} else if self.element_size != ElementSize::Heterogenous {
|
||||
format!(
|
||||
"{:6} {:12} {} {:3} allocations",
|
||||
self.num_elements(),
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:6} {:12} {} {:3} allocations",
|
||||
"",
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
/// Number of separate clip rectangles
|
||||
pub jobs: AllocInfo,
|
||||
pub vertices: AllocInfo,
|
||||
pub indices: AllocInfo,
|
||||
}
|
||||
|
||||
impl PaintStats {
|
||||
pub fn from_shapes(shapes: &[(Rect, Shape)]) -> 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 (_, 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 { .. } => {
|
||||
Default::default()
|
||||
}
|
||||
Shape::Path { points, .. } => {
|
||||
self.shape_path += AllocInfo::from_slice(points);
|
||||
}
|
||||
Shape::Text { galley, .. } => {
|
||||
self.shape_text += AllocInfo::from_galley(galley);
|
||||
}
|
||||
Shape::Triangles(triangles) => {
|
||||
self.shape_mesh += AllocInfo::from_triangles(triangles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_paint_jobs(mut self, paint_jobs: &[crate::PaintJob]) -> Self {
|
||||
self.jobs += AllocInfo::from_slice(paint_jobs);
|
||||
for (_, indices) in paint_jobs {
|
||||
self.vertices += AllocInfo::from_slice(&indices.vertices);
|
||||
self.indices += AllocInfo::from_slice(&indices.indices);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn total(&self) -> AllocInfo {
|
||||
// self.shapes
|
||||
// + self.shape_text
|
||||
// + self.shape_path
|
||||
// + self.shape_mesh
|
||||
// + self.jobs
|
||||
// + self.vertices
|
||||
// + self.indices
|
||||
// }
|
||||
}
|
||||
|
||||
fn megabytes(size: usize) -> String {
|
||||
format!("{:.2} MB", size as f64 / 1e6)
|
||||
}
|
||||
31
epaint/src/stroke.rs
Normal file
31
epaint/src/stroke.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
|
||||
/// Describes the width and color of a line.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Stroke {
|
||||
pub width: f32,
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub fn none() -> Self {
|
||||
Self::new(0.0, Color32::TRANSPARENT)
|
||||
}
|
||||
|
||||
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
color: color.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Color> From<(f32, Color)> for Stroke
|
||||
where
|
||||
Color: Into<Color32>,
|
||||
{
|
||||
fn from((width, color): (f32, Color)) -> Stroke {
|
||||
Stroke::new(width, color)
|
||||
}
|
||||
}
|
||||
725
epaint/src/tessellator.rs
Normal file
725
epaint/src/tessellator.rs
Normal file
@@ -0,0 +1,725 @@
|
||||
//! Converts graphics primitives into textured triangles.
|
||||
//!
|
||||
//! This module converts lines, circles, text and more represented by [`Shape`]
|
||||
//! into textured triangles represented by [`Triangles`].
|
||||
|
||||
#![allow(clippy::identity_op)]
|
||||
|
||||
use crate::{text::Fonts, *};
|
||||
use emath::*;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
/// A clip triangle and some textured triangles.
|
||||
pub type PaintJob = (Rect, Triangles);
|
||||
|
||||
/// Grouped by clip rectangles, in pixel coordinates
|
||||
pub type PaintJobs = Vec<PaintJob>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathPoint {
|
||||
pos: Pos2,
|
||||
|
||||
/// For filled paths the normal is used for anti-aliasing (both strokes and filled areas).
|
||||
///
|
||||
/// For strokes the normal is also used for giving thickness to the path
|
||||
/// (i.e. in what direction to expand).
|
||||
///
|
||||
/// The normal could be estimated by differences between successive points,
|
||||
/// but that would be less accurate (and in some cases slower).
|
||||
///
|
||||
/// Normals are normally unit-length.
|
||||
normal: Vec2,
|
||||
}
|
||||
|
||||
/// A connected line (without thickness or gaps) which can be tessellated
|
||||
/// to either to a stroke (with thickness) or a filled convex area.
|
||||
/// Used as a scratch-pad during tessellation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Path(Vec<PathPoint>);
|
||||
|
||||
impl Path {
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.0.reserve(additional)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_point(&mut self, pos: Pos2, normal: Vec2) {
|
||||
self.0.push(PathPoint { pos, normal });
|
||||
}
|
||||
|
||||
pub fn add_circle(&mut self, center: Pos2, radius: f32) {
|
||||
let n = (radius * 4.0).round() as i32; // TODO: tweak a bit more
|
||||
let n = clamp(n, 4..=64);
|
||||
self.reserve(n as usize);
|
||||
for i in 0..n {
|
||||
let angle = remap(i as f32, 0.0..=n as f32, 0.0..=TAU);
|
||||
let normal = vec2(angle.cos(), angle.sin());
|
||||
self.add_point(center + radius * normal, normal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_line_segment(&mut self, points: [Pos2; 2]) {
|
||||
self.reserve(2);
|
||||
let normal = (points[1] - points[0]).normalized().rot90();
|
||||
self.add_point(points[0], normal);
|
||||
self.add_point(points[1], normal);
|
||||
}
|
||||
|
||||
pub fn add_open_points(&mut self, points: &[Pos2]) {
|
||||
let n = points.len();
|
||||
assert!(n >= 2);
|
||||
|
||||
if n == 2 {
|
||||
// Common case optimization:
|
||||
self.add_line_segment([points[0], points[1]]);
|
||||
} else {
|
||||
// TODO: optimize
|
||||
self.reserve(n);
|
||||
self.add_point(points[0], (points[1] - points[0]).normalized().rot90());
|
||||
for i in 1..n - 1 {
|
||||
let mut n0 = (points[i] - points[i - 1]).normalized().rot90();
|
||||
let mut n1 = (points[i + 1] - points[i]).normalized().rot90();
|
||||
|
||||
// Handle duplicated points (but not triplicated...):
|
||||
if n0 == Vec2::zero() {
|
||||
n0 = n1;
|
||||
} else if n1 == Vec2::zero() {
|
||||
n1 = n0;
|
||||
}
|
||||
|
||||
let v = (n0 + n1) / 2.0;
|
||||
let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better
|
||||
self.add_point(points[i], normal);
|
||||
}
|
||||
self.add_point(
|
||||
points[n - 1],
|
||||
(points[n - 1] - points[n - 2]).normalized().rot90(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_line_loop(&mut self, points: &[Pos2]) {
|
||||
let n = points.len();
|
||||
assert!(n >= 2);
|
||||
self.reserve(n);
|
||||
|
||||
// TODO: optimize
|
||||
for i in 0..n {
|
||||
let mut n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90();
|
||||
let mut n1 = (points[(i + 1) % n] - points[i]).normalized().rot90();
|
||||
|
||||
// Handle duplicated points (but not triplicated...):
|
||||
if n0 == Vec2::zero() {
|
||||
n0 = n1;
|
||||
} else if n1 == Vec2::zero() {
|
||||
n1 = n0;
|
||||
}
|
||||
|
||||
// if n1 == Vec2::zero() {
|
||||
// continue
|
||||
// }
|
||||
let v = (n0 + n1) / 2.0;
|
||||
let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better
|
||||
self.add_point(points[i], normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod path {
|
||||
//! Helpers for constructing paths
|
||||
use super::*;
|
||||
|
||||
/// overwrites existing points
|
||||
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, corner_radius: f32) {
|
||||
path.clear();
|
||||
|
||||
let min = rect.min;
|
||||
let max = rect.max;
|
||||
|
||||
let cr = corner_radius
|
||||
.min(rect.width() * 0.5)
|
||||
.min(rect.height() * 0.5);
|
||||
|
||||
if cr <= 0.0 {
|
||||
let min = rect.min;
|
||||
let max = rect.max;
|
||||
path.reserve(4);
|
||||
path.push(pos2(min.x, min.y));
|
||||
path.push(pos2(max.x, min.y));
|
||||
path.push(pos2(max.x, max.y));
|
||||
path.push(pos2(min.x, max.y));
|
||||
} else {
|
||||
add_circle_quadrant(path, pos2(max.x - cr, max.y - cr), cr, 0.0);
|
||||
add_circle_quadrant(path, pos2(min.x + cr, max.y - cr), cr, 1.0);
|
||||
add_circle_quadrant(path, pos2(min.x + cr, min.y + cr), cr, 2.0);
|
||||
add_circle_quadrant(path, pos2(max.x - cr, min.y + cr), cr, 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add one quadrant of a circle
|
||||
///
|
||||
/// * quadrant 0: right bottom
|
||||
/// * quadrant 1: left bottom
|
||||
/// * quadrant 2: left top
|
||||
/// * quadrant 3: right top
|
||||
//
|
||||
// Derivation:
|
||||
//
|
||||
// * angle 0 * TAU / 4 = right
|
||||
// - quadrant 0: right bottom
|
||||
// * angle 1 * TAU / 4 = bottom
|
||||
// - quadrant 1: left bottom
|
||||
// * angle 2 * TAU / 4 = left
|
||||
// - quadrant 2: left top
|
||||
// * angle 3 * TAU / 4 = top
|
||||
// - quadrant 3: right top
|
||||
// * angle 4 * TAU / 4 = right
|
||||
pub fn add_circle_quadrant(path: &mut Vec<Pos2>, center: Pos2, radius: f32, quadrant: f32) {
|
||||
// TODO: optimize with precalculated vertices for some radii ranges
|
||||
|
||||
let n = (radius * 0.75).round() as i32; // TODO: tweak a bit more
|
||||
let n = clamp(n, 2..=32);
|
||||
const RIGHT_ANGLE: f32 = TAU / 4.0;
|
||||
path.reserve(n as usize + 1);
|
||||
for i in 0..=n {
|
||||
let angle = remap(
|
||||
i as f32,
|
||||
0.0..=n as f32,
|
||||
quadrant * RIGHT_ANGLE..=(quadrant + 1.0) * RIGHT_ANGLE,
|
||||
);
|
||||
path.push(center + radius * Vec2::angled(angle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PathType {
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
use self::PathType::{Closed, Open};
|
||||
|
||||
/// Tessellation quality options
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "persistence", serde(default))]
|
||||
pub struct TessellationOptions {
|
||||
/// Size of a pixel in points, e.g. 0.5
|
||||
pub aa_size: f32,
|
||||
/// Anti-aliasing makes shapes appear smoother, but requires more triangles and is therefore slower.
|
||||
/// By default this is enabled in release builds and disabled in debug builds.
|
||||
pub anti_alias: bool,
|
||||
/// If `true` (default) cull certain primitives before tessellating them
|
||||
pub coarse_tessellation_culling: bool,
|
||||
/// Output the clip rectangles to be painted?
|
||||
pub debug_paint_clip_rects: bool,
|
||||
/// Output the text-containing rectangles
|
||||
pub debug_paint_text_rects: bool,
|
||||
/// If true, no clipping will be done
|
||||
pub debug_ignore_clip_rects: bool,
|
||||
}
|
||||
|
||||
impl Default for TessellationOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
aa_size: 1.0,
|
||||
anti_alias: true,
|
||||
coarse_tessellation_culling: true,
|
||||
debug_paint_text_rects: false,
|
||||
debug_paint_clip_rects: false,
|
||||
debug_ignore_clip_rects: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate the given convex area into a polygon.
|
||||
fn fill_closed_path(
|
||||
path: &[PathPoint],
|
||||
color: Color32,
|
||||
options: TessellationOptions,
|
||||
out: &mut Triangles,
|
||||
) {
|
||||
if color == Color32::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
let n = path.len() as u32;
|
||||
if options.anti_alias {
|
||||
out.reserve_triangles(3 * n as usize);
|
||||
out.reserve_vertices(2 * n as usize);
|
||||
let color_outer = Color32::TRANSPARENT;
|
||||
let idx_inner = out.vertices.len() as u32;
|
||||
let idx_outer = idx_inner + 1;
|
||||
for i in 2..n {
|
||||
out.add_triangle(idx_inner + 2 * (i - 1), idx_inner, idx_inner + 2 * i);
|
||||
}
|
||||
let mut i0 = n - 1;
|
||||
for i1 in 0..n {
|
||||
let p1 = &path[i1 as usize];
|
||||
let dm = p1.normal * options.aa_size * 0.5;
|
||||
out.colored_vertex(p1.pos - dm, color);
|
||||
out.colored_vertex(p1.pos + dm, color_outer);
|
||||
out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0);
|
||||
out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1);
|
||||
i0 = i1;
|
||||
}
|
||||
} else {
|
||||
out.reserve_triangles(n as usize);
|
||||
let idx = out.vertices.len() as u32;
|
||||
out.vertices.extend(path.iter().map(|p| Vertex {
|
||||
pos: p.pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
}));
|
||||
for i in 2..n {
|
||||
out.add_triangle(idx, idx + i - 1, idx + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate the given path as a stroke with thickness.
|
||||
fn stroke_path(
|
||||
path: &[PathPoint],
|
||||
path_type: PathType,
|
||||
stroke: Stroke,
|
||||
options: TessellationOptions,
|
||||
out: &mut Triangles,
|
||||
) {
|
||||
if stroke.width <= 0.0 || stroke.color == Color32::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
let n = path.len() as u32;
|
||||
let idx = out.vertices.len() as u32;
|
||||
|
||||
if options.anti_alias {
|
||||
let color_inner = stroke.color;
|
||||
let color_outer = Color32::TRANSPARENT;
|
||||
|
||||
let thin_line = stroke.width <= options.aa_size;
|
||||
if thin_line {
|
||||
/*
|
||||
We paint the line using three edges: outer, inner, outer.
|
||||
|
||||
. o i o outer, inner, outer
|
||||
. |---| aa_size (pixel width)
|
||||
*/
|
||||
|
||||
// Fade out as it gets thinner:
|
||||
let color_inner = mul_color(color_inner, stroke.width / options.aa_size);
|
||||
if color_inner == Color32::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
out.reserve_triangles(4 * n as usize);
|
||||
out.reserve_vertices(3 * n as usize);
|
||||
|
||||
let mut i0 = n - 1;
|
||||
for i1 in 0..n {
|
||||
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||
let p1 = &path[i1 as usize];
|
||||
let p = p1.pos;
|
||||
let n = p1.normal;
|
||||
out.colored_vertex(p + n * options.aa_size, color_outer);
|
||||
out.colored_vertex(p, color_inner);
|
||||
out.colored_vertex(p - n * options.aa_size, color_outer);
|
||||
|
||||
if connect_with_previous {
|
||||
out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0);
|
||||
out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i1 + 0, idx + 3 * i1 + 1);
|
||||
|
||||
out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
|
||||
out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
|
||||
}
|
||||
i0 = i1;
|
||||
}
|
||||
} else {
|
||||
// thick line
|
||||
// TODO: line caps for really thick lines?
|
||||
|
||||
/*
|
||||
We paint the line using four edges: outer, inner, inner, outer
|
||||
|
||||
. o i p i o outer, inner, point, inner, outer
|
||||
. |---| aa_size (pixel width)
|
||||
. |--------------| width
|
||||
. |---------| outer_rad
|
||||
. |-----| inner_rad
|
||||
*/
|
||||
|
||||
out.reserve_triangles(6 * n as usize);
|
||||
out.reserve_vertices(4 * n as usize);
|
||||
|
||||
let mut i0 = n - 1;
|
||||
for i1 in 0..n {
|
||||
let connect_with_previous = path_type == PathType::Closed || i1 > 0;
|
||||
let inner_rad = 0.5 * (stroke.width - options.aa_size);
|
||||
let outer_rad = 0.5 * (stroke.width + options.aa_size);
|
||||
let p1 = &path[i1 as usize];
|
||||
let p = p1.pos;
|
||||
let n = p1.normal;
|
||||
out.colored_vertex(p + n * outer_rad, color_outer);
|
||||
out.colored_vertex(p + n * inner_rad, color_inner);
|
||||
out.colored_vertex(p - n * inner_rad, color_inner);
|
||||
out.colored_vertex(p - n * outer_rad, color_outer);
|
||||
|
||||
if connect_with_previous {
|
||||
out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
|
||||
out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1);
|
||||
|
||||
out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i0 + 2, idx + 4 * i1 + 1);
|
||||
out.add_triangle(idx + 4 * i0 + 2, idx + 4 * i1 + 1, idx + 4 * i1 + 2);
|
||||
|
||||
out.add_triangle(idx + 4 * i0 + 2, idx + 4 * i0 + 3, idx + 4 * i1 + 2);
|
||||
out.add_triangle(idx + 4 * i0 + 3, idx + 4 * i1 + 2, idx + 4 * i1 + 3);
|
||||
}
|
||||
i0 = i1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.reserve_triangles(2 * n as usize);
|
||||
out.reserve_vertices(2 * n as usize);
|
||||
|
||||
let last_index = if path_type == Closed { n } else { n - 1 };
|
||||
for i in 0..last_index {
|
||||
out.add_triangle(
|
||||
idx + (2 * i + 0) % (2 * n),
|
||||
idx + (2 * i + 1) % (2 * n),
|
||||
idx + (2 * i + 2) % (2 * n),
|
||||
);
|
||||
out.add_triangle(
|
||||
idx + (2 * i + 2) % (2 * n),
|
||||
idx + (2 * i + 1) % (2 * n),
|
||||
idx + (2 * i + 3) % (2 * n),
|
||||
);
|
||||
}
|
||||
|
||||
let thin_line = stroke.width <= options.aa_size;
|
||||
if thin_line {
|
||||
// Fade out thin lines rather than making them thinner
|
||||
let radius = options.aa_size / 2.0;
|
||||
let color = mul_color(stroke.color, stroke.width / options.aa_size);
|
||||
if color == Color32::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
for p in path {
|
||||
out.colored_vertex(p.pos + radius * p.normal, color);
|
||||
out.colored_vertex(p.pos - radius * p.normal, color);
|
||||
}
|
||||
} else {
|
||||
let radius = stroke.width / 2.0;
|
||||
for p in path {
|
||||
out.colored_vertex(p.pos + radius * p.normal, stroke.color);
|
||||
out.colored_vertex(p.pos - radius * p.normal, stroke.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mul_color(color: Color32, factor: f32) -> Color32 {
|
||||
debug_assert!(0.0 <= factor && factor <= 1.0);
|
||||
// sRGBA correct fading requires conversion to linear space and back again because of premultiplied alpha
|
||||
Rgba::from(color).multiply(factor).into()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Converts [`Shape`]s into [`Triangles`].
|
||||
pub struct Tessellator {
|
||||
options: TessellationOptions,
|
||||
/// Only used for culling
|
||||
clip_rect: Rect,
|
||||
scratchpad_points: Vec<Pos2>,
|
||||
scratchpad_path: Path,
|
||||
}
|
||||
|
||||
impl Tessellator {
|
||||
pub fn from_options(options: TessellationOptions) -> Self {
|
||||
Self {
|
||||
options,
|
||||
clip_rect: Rect::everything(),
|
||||
scratchpad_points: Default::default(),
|
||||
scratchpad_path: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate a single [`Shape`] into a [`Triangles`].
|
||||
///
|
||||
/// * `shape`: the shape to tessellate
|
||||
/// * `options`: tessellation quality
|
||||
/// * `fonts`: font source when tessellating text
|
||||
/// * `out`: where the triangles are put
|
||||
/// * `scratchpad_path`: if you plan to run `tessellate_shape`
|
||||
/// many times, pass it a reference to the same `Path` to avoid excessive allocations.
|
||||
pub fn tessellate_shape(&mut self, fonts: &Fonts, shape: Shape, out: &mut Triangles) {
|
||||
let clip_rect = self.clip_rect;
|
||||
let options = self.options;
|
||||
|
||||
match shape {
|
||||
Shape::Noop => {}
|
||||
Shape::Vec(vec) => {
|
||||
for shape in vec {
|
||||
self.tessellate_shape(fonts, shape, out)
|
||||
}
|
||||
}
|
||||
Shape::Circle {
|
||||
center,
|
||||
radius,
|
||||
fill,
|
||||
stroke,
|
||||
} => {
|
||||
if radius <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if options.coarse_tessellation_culling
|
||||
&& !clip_rect.expand(radius + stroke.width).contains(center)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
path.add_circle(center, radius);
|
||||
fill_closed_path(&path.0, fill, options, out);
|
||||
stroke_path(&path.0, Closed, stroke, options, out);
|
||||
}
|
||||
Shape::Triangles(triangles) => {
|
||||
if triangles.is_valid() {
|
||||
out.append(triangles);
|
||||
} else {
|
||||
debug_assert!(false, "Invalid Triangles in Shape::Triangles");
|
||||
}
|
||||
}
|
||||
Shape::LineSegment { points, stroke } => {
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
path.add_line_segment(points);
|
||||
stroke_path(&path.0, Open, stroke, options, out);
|
||||
}
|
||||
Shape::Path {
|
||||
points,
|
||||
closed,
|
||||
fill,
|
||||
stroke,
|
||||
} => {
|
||||
if points.len() >= 2 {
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
if closed {
|
||||
path.add_line_loop(&points);
|
||||
} else {
|
||||
path.add_open_points(&points);
|
||||
}
|
||||
|
||||
if fill != Color32::TRANSPARENT {
|
||||
debug_assert!(
|
||||
closed,
|
||||
"You asked to fill a path that is not closed. That makes no sense."
|
||||
);
|
||||
fill_closed_path(&path.0, fill, options, out);
|
||||
}
|
||||
let typ = if closed { Closed } else { Open };
|
||||
stroke_path(&path.0, typ, stroke, options, out);
|
||||
}
|
||||
}
|
||||
Shape::Rect {
|
||||
rect,
|
||||
corner_radius,
|
||||
fill,
|
||||
stroke,
|
||||
} => {
|
||||
let rect = PaintRect {
|
||||
rect,
|
||||
corner_radius,
|
||||
fill,
|
||||
stroke,
|
||||
};
|
||||
self.tessellate_rect(&rect, out);
|
||||
}
|
||||
Shape::Text {
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
color,
|
||||
} => {
|
||||
if options.debug_paint_text_rects {
|
||||
self.tessellate_rect(
|
||||
&PaintRect {
|
||||
rect: Rect::from_min_size(pos, galley.size).expand(0.5),
|
||||
corner_radius: 2.0,
|
||||
fill: Default::default(),
|
||||
stroke: (0.5, color).into(),
|
||||
},
|
||||
out,
|
||||
);
|
||||
}
|
||||
self.tessellate_text(fonts, pos, &galley, text_style, color, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tessellate_rect(&mut self, rect: &PaintRect, out: &mut Triangles) {
|
||||
let PaintRect {
|
||||
mut rect,
|
||||
corner_radius,
|
||||
fill,
|
||||
stroke,
|
||||
} = *rect;
|
||||
|
||||
if self.options.coarse_tessellation_culling
|
||||
&& !rect.expand(stroke.width).intersects(self.clip_rect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if rect.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// It is common to (sometimes accidentally) create an infinitely sized rectangle.
|
||||
// Make sure we can handle that:
|
||||
rect.min = rect.min.at_least(pos2(-1e7, -1e7));
|
||||
rect.max = rect.max.at_most(pos2(1e7, 1e7));
|
||||
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
|
||||
path.add_line_loop(&self.scratchpad_points);
|
||||
fill_closed_path(&path.0, fill, self.options, out);
|
||||
stroke_path(&path.0, Closed, stroke, self.options, out);
|
||||
}
|
||||
|
||||
pub fn tessellate_text(
|
||||
&mut self,
|
||||
fonts: &Fonts,
|
||||
pos: Pos2,
|
||||
galley: &super::Galley,
|
||||
text_style: super::TextStyle,
|
||||
color: Color32,
|
||||
out: &mut Triangles,
|
||||
) {
|
||||
if color == Color32::TRANSPARENT {
|
||||
return;
|
||||
}
|
||||
galley.sanity_check();
|
||||
|
||||
let num_chars = galley.text.chars().count();
|
||||
out.reserve_triangles(num_chars * 2);
|
||||
out.reserve_vertices(num_chars * 4);
|
||||
|
||||
let tex_w = fonts.texture().width as f32;
|
||||
let tex_h = fonts.texture().height as f32;
|
||||
|
||||
let clip_rect = self.clip_rect.expand(2.0); // Some fudge to handle letters that are slightly larger than expected.
|
||||
|
||||
let font = &fonts[text_style];
|
||||
let mut chars = galley.text.chars();
|
||||
for line in &galley.rows {
|
||||
let line_min_y = pos.y + line.y_min;
|
||||
let line_max_y = line_min_y + font.row_height();
|
||||
let is_line_visible = line_max_y >= clip_rect.min.y && line_min_y <= clip_rect.max.y;
|
||||
|
||||
for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) {
|
||||
let c = chars.next().unwrap();
|
||||
|
||||
if self.options.coarse_tessellation_culling && !is_line_visible {
|
||||
// culling individual lines of text is important, since a single `Shape::Text`
|
||||
// can span hundreds of lines.
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(glyph) = font.uv_rect(c) {
|
||||
let mut left_top = pos + glyph.offset + vec2(*x_offset, line.y_min);
|
||||
left_top.x = font.round_to_pixel(left_top.x); // Pixel-perfection.
|
||||
left_top.y = font.round_to_pixel(left_top.y); // Pixel-perfection.
|
||||
|
||||
let pos = Rect::from_min_max(left_top, left_top + glyph.size);
|
||||
let uv = Rect::from_min_max(
|
||||
pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h),
|
||||
pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h),
|
||||
);
|
||||
out.add_rect_with_uv(pos, uv, color);
|
||||
}
|
||||
}
|
||||
if line.ends_with_newline {
|
||||
let newline = chars.next().unwrap();
|
||||
debug_assert_eq!(newline, '\n');
|
||||
}
|
||||
}
|
||||
assert_eq!(chars.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns [`Shape`]:s into sets of triangles.
|
||||
///
|
||||
/// The given shapes will be painted back-to-front (painters algorithm).
|
||||
/// They will be batched together by clip rectangle.
|
||||
///
|
||||
/// * `shapes`: the shape to tessellate
|
||||
/// * `options`: tessellation quality
|
||||
/// * `fonts`: font source when tessellating text
|
||||
///
|
||||
/// ## Returns
|
||||
/// A list of clip rectangles with matching [`Triangles`].
|
||||
pub fn tessellate_shapes(
|
||||
shapes: Vec<(Rect, Shape)>,
|
||||
options: TessellationOptions,
|
||||
fonts: &Fonts,
|
||||
) -> Vec<(Rect, Triangles)> {
|
||||
let mut tessellator = Tessellator::from_options(options);
|
||||
|
||||
let mut jobs = PaintJobs::default();
|
||||
for (clip_rect, shape) in shapes {
|
||||
let start_new_job = match jobs.last() {
|
||||
None => true,
|
||||
Some(job) => job.0 != clip_rect || job.1.texture_id != shape.texture_id(),
|
||||
};
|
||||
|
||||
if start_new_job {
|
||||
jobs.push((clip_rect, Triangles::default()));
|
||||
}
|
||||
|
||||
let out = &mut jobs.last_mut().unwrap().1;
|
||||
tessellator.clip_rect = clip_rect;
|
||||
tessellator.tessellate_shape(fonts, shape, out);
|
||||
}
|
||||
|
||||
if options.debug_paint_clip_rects {
|
||||
for (clip_rect, triangles) in &mut jobs {
|
||||
tessellator.clip_rect = Rect::everything();
|
||||
tessellator.tessellate_shape(
|
||||
fonts,
|
||||
Shape::Rect {
|
||||
rect: *clip_rect,
|
||||
corner_radius: 0.0,
|
||||
fill: Default::default(),
|
||||
stroke: Stroke::new(2.0, Color32::from_rgb(150, 255, 150)),
|
||||
},
|
||||
triangles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if options.debug_ignore_clip_rects {
|
||||
for (clip_rect, _) in &mut jobs {
|
||||
*clip_rect = Rect::everything();
|
||||
}
|
||||
}
|
||||
|
||||
for (_, triangles) in &jobs {
|
||||
debug_assert!(
|
||||
triangles.is_valid(),
|
||||
"Tessellator generated invalid Triangles"
|
||||
);
|
||||
}
|
||||
|
||||
jobs
|
||||
}
|
||||
108
epaint/src/text/cursor.rs
Normal file
108
epaint/src/text/cursor.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! Different types of text cursors, i.e. ways to point into a [`super::Galley`].
|
||||
|
||||
/// Character cursor
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "persistence", 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Row Cursor
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", 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 = "persistence", 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)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Cursor {
|
||||
pub ccursor: CCursor,
|
||||
pub rcursor: RCursor,
|
||||
pub pcursor: PCursor,
|
||||
}
|
||||
517
epaint/src/text/font.rs
Normal file
517
epaint/src/text/font.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use {
|
||||
ahash::AHashMap,
|
||||
rusttype::{point, Scale},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mutex::{Mutex, RwLock},
|
||||
text::galley::{Galley, Row},
|
||||
TextureAtlas,
|
||||
};
|
||||
use emath::{vec2, Vec2};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct UvRect {
|
||||
/// X/Y offset for nice rendering (unit: points).
|
||||
pub offset: Vec2,
|
||||
pub size: Vec2,
|
||||
|
||||
/// Top left corner UV in texture.
|
||||
pub min: (u16, u16),
|
||||
|
||||
/// Bottom right corner (exclusive).
|
||||
pub max: (u16, u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GlyphInfo {
|
||||
id: rusttype::GlyphId,
|
||||
|
||||
/// Unit: points.
|
||||
pub advance_width: f32,
|
||||
|
||||
/// Texture coordinates. None for space.
|
||||
pub uv_rect: Option<UvRect>,
|
||||
}
|
||||
|
||||
impl Default for GlyphInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: rusttype::GlyphId(0),
|
||||
advance_width: 0.0,
|
||||
uv_rect: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A specific font with a size.
|
||||
/// The interface uses points as the unit for everything.
|
||||
pub struct FontImpl {
|
||||
rusttype_font: Arc<rusttype::Font<'static>>,
|
||||
/// Maximum character height
|
||||
scale_in_pixels: f32,
|
||||
height_in_points: f32,
|
||||
// move each character by this much (hack)
|
||||
y_offset: f32,
|
||||
pixels_per_point: f32,
|
||||
glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>, // TODO: standard Mutex
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
}
|
||||
|
||||
impl FontImpl {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
rusttype_font: Arc<rusttype::Font<'static>>,
|
||||
scale_in_points: f32,
|
||||
y_offset: f32,
|
||||
) -> FontImpl {
|
||||
assert!(scale_in_points > 0.0);
|
||||
assert!(pixels_per_point > 0.0);
|
||||
|
||||
let scale_in_pixels = pixels_per_point * scale_in_points;
|
||||
|
||||
let height_in_points = scale_in_points;
|
||||
// TODO: use v_metrics for line spacing ?
|
||||
// let v = rusttype_font.v_metrics(Scale::uniform(scale_in_pixels));
|
||||
// let height_in_pixels = v.ascent - v.descent + v.line_gap;
|
||||
// let height_in_points = height_in_pixels / pixels_per_point;
|
||||
|
||||
Self {
|
||||
rusttype_font,
|
||||
scale_in_pixels,
|
||||
height_in_points,
|
||||
y_offset,
|
||||
pixels_per_point,
|
||||
glyph_info_cache: Default::default(),
|
||||
atlas,
|
||||
}
|
||||
}
|
||||
|
||||
/// `\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);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new character:
|
||||
let glyph = self.rusttype_font.glyph(c);
|
||||
if glyph.id().0 == 0 {
|
||||
None
|
||||
} else {
|
||||
let glyph_info = allocate_glyph(
|
||||
&mut self.atlas.lock(),
|
||||
glyph,
|
||||
self.scale_in_pixels,
|
||||
self.y_offset,
|
||||
self.pixels_per_point,
|
||||
);
|
||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||
Some(glyph_info)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pair_kerning(
|
||||
&self,
|
||||
last_glyph_id: rusttype::GlyphId,
|
||||
glyph_id: rusttype::GlyphId,
|
||||
) -> f32 {
|
||||
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
|
||||
self.rusttype_font
|
||||
.pair_kerning(scale_in_pixels, last_glyph_id, glyph_id)
|
||||
/ self.pixels_per_point
|
||||
}
|
||||
|
||||
/// Height of one row of text. In points
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.height_in_points
|
||||
}
|
||||
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.pixels_per_point
|
||||
}
|
||||
}
|
||||
|
||||
type FontIndex = usize;
|
||||
|
||||
// TODO: rename?
|
||||
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
|
||||
#[derive(Default)]
|
||||
pub struct Font {
|
||||
fonts: Vec<Arc<FontImpl>>,
|
||||
replacement_glyph: (FontIndex, GlyphInfo),
|
||||
pixels_per_point: f32,
|
||||
row_height: f32,
|
||||
glyph_info_cache: RwLock<AHashMap<char, (FontIndex, GlyphInfo)>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
|
||||
if fonts.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let pixels_per_point = fonts[0].pixels_per_point();
|
||||
let row_height = fonts[0].row_height();
|
||||
|
||||
let mut slf = Self {
|
||||
fonts,
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
slf.glyph_info(c);
|
||||
}
|
||||
slf.glyph_info('°');
|
||||
|
||||
slf
|
||||
}
|
||||
|
||||
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
|
||||
pub fn row_height(&self) -> f32 {
|
||||
self.row_height
|
||||
}
|
||||
|
||||
pub fn uv_rect(&self, c: char) -> Option<UvRect> {
|
||||
self.glyph_info_cache
|
||||
.read()
|
||||
.get(&c)
|
||||
.and_then(|gi| gi.1.uv_rect)
|
||||
}
|
||||
|
||||
pub fn glyph_width(&self, c: char) -> f32 {
|
||||
self.glyph_info(c).1.advance_width
|
||||
}
|
||||
|
||||
/// `\n` will (intentionally) show up as the replacement character.
|
||||
fn glyph_info(&self, c: char) -> (FontIndex, GlyphInfo) {
|
||||
{
|
||||
if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
|
||||
return *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
|
||||
.write()
|
||||
.insert(c, font_index_glyph_info);
|
||||
font_index_glyph_info
|
||||
}
|
||||
|
||||
fn glyph_info_no_cache_or_fallback(&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
|
||||
.write()
|
||||
.insert(c, (font_index, glyph_info));
|
||||
return Some((font_index, glyph_info));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Typeset the given text onto one row.
|
||||
/// Assumes there are no `\n` in the text.
|
||||
/// Return `x_offsets`, one longer than the number of characters in the text.
|
||||
fn layout_single_row_fragment(&self, text: &str) -> Vec<f32> {
|
||||
let mut x_offsets = Vec::with_capacity(text.chars().count() + 1);
|
||||
x_offsets.push(0.0);
|
||||
|
||||
let mut cursor_x_in_points = 0.0f32;
|
||||
let mut last_glyph_id = None;
|
||||
|
||||
for c in text.chars() {
|
||||
if !self.fonts.is_empty() {
|
||||
let (font_index, glyph_info) = self.glyph_info(c);
|
||||
let font_impl = &self.fonts[font_index];
|
||||
|
||||
if let Some(last_glyph_id) = last_glyph_id {
|
||||
cursor_x_in_points += font_impl.pair_kerning(last_glyph_id, glyph_info.id)
|
||||
}
|
||||
cursor_x_in_points += glyph_info.advance_width;
|
||||
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
|
||||
last_glyph_id = Some(glyph_info.id);
|
||||
}
|
||||
|
||||
x_offsets.push(cursor_x_in_points);
|
||||
}
|
||||
|
||||
x_offsets
|
||||
}
|
||||
|
||||
/// Typeset the given text onto one row.
|
||||
/// Any `\n` will show up as the replacement character.
|
||||
/// Always returns exactly one `Row` in the `Galley`.
|
||||
pub fn layout_single_line(&self, text: String) -> Galley {
|
||||
let x_offsets = self.layout_single_row_fragment(&text);
|
||||
let row = Row {
|
||||
x_offsets,
|
||||
y_min: 0.0,
|
||||
y_max: self.row_height(),
|
||||
ends_with_newline: false,
|
||||
};
|
||||
let width = row.max_x();
|
||||
let size = vec2(width, self.row_height());
|
||||
let galley = Galley {
|
||||
text,
|
||||
rows: vec![row],
|
||||
size,
|
||||
};
|
||||
galley.sanity_check();
|
||||
galley
|
||||
}
|
||||
|
||||
/// Always returns at least one row.
|
||||
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
|
||||
self.layout_multiline_with_indentation_and_max_width(text, 0.0, max_width_in_points)
|
||||
}
|
||||
|
||||
/// * `first_row_indentation`: extra space before the very first character (in points).
|
||||
/// * `max_width_in_points`: wrapping width.
|
||||
/// Always returns at least one row.
|
||||
pub fn layout_multiline_with_indentation_and_max_width(
|
||||
&self,
|
||||
text: String,
|
||||
first_row_indentation: f32,
|
||||
max_width_in_points: f32,
|
||||
) -> Galley {
|
||||
let row_height = self.row_height();
|
||||
let mut cursor_y = 0.0;
|
||||
let mut rows = Vec::new();
|
||||
|
||||
let mut paragraph_start = 0;
|
||||
|
||||
while paragraph_start < text.len() {
|
||||
let next_newline = text[paragraph_start..].find('\n');
|
||||
let paragraph_end = next_newline
|
||||
.map(|newline| paragraph_start + newline)
|
||||
.unwrap_or_else(|| text.len());
|
||||
|
||||
assert!(paragraph_start <= paragraph_end);
|
||||
let paragraph_text = &text[paragraph_start..paragraph_end];
|
||||
let line_indentation = if rows.is_empty() {
|
||||
first_row_indentation
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let mut paragraph_rows = self.layout_paragraph_max_width(
|
||||
paragraph_text,
|
||||
line_indentation,
|
||||
max_width_in_points,
|
||||
);
|
||||
assert!(!paragraph_rows.is_empty());
|
||||
paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some();
|
||||
|
||||
for row in &mut paragraph_rows {
|
||||
row.y_min += cursor_y;
|
||||
row.y_max += cursor_y;
|
||||
}
|
||||
cursor_y = paragraph_rows.last().unwrap().y_max;
|
||||
cursor_y += row_height * 0.4; // Extra spacing between paragraphs. TODO: less hacky
|
||||
|
||||
rows.append(&mut paragraph_rows);
|
||||
|
||||
paragraph_start = paragraph_end + 1;
|
||||
}
|
||||
|
||||
if text.is_empty() || text.ends_with('\n') {
|
||||
rows.push(Row {
|
||||
x_offsets: vec![0.0],
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + row_height,
|
||||
ends_with_newline: false,
|
||||
});
|
||||
}
|
||||
|
||||
let mut widest_row = 0.0;
|
||||
for row in &rows {
|
||||
widest_row = row.max_x().max(widest_row);
|
||||
}
|
||||
let size = vec2(widest_row, rows.last().unwrap().y_max);
|
||||
|
||||
let galley = Galley { text, rows, size };
|
||||
galley.sanity_check();
|
||||
galley
|
||||
}
|
||||
|
||||
/// A paragraph is text with no line break character in it.
|
||||
/// The text will be wrapped by the given `max_width_in_points`.
|
||||
/// Always returns at least one row.
|
||||
fn layout_paragraph_max_width(
|
||||
&self,
|
||||
text: &str,
|
||||
mut first_row_indentation: f32,
|
||||
max_width_in_points: f32,
|
||||
) -> Vec<Row> {
|
||||
if text.is_empty() {
|
||||
return vec![Row {
|
||||
x_offsets: vec![first_row_indentation],
|
||||
y_min: 0.0,
|
||||
y_max: self.row_height(),
|
||||
ends_with_newline: false,
|
||||
}];
|
||||
}
|
||||
|
||||
let full_x_offsets = self.layout_single_row_fragment(text);
|
||||
|
||||
let mut row_start_x = 0.0; // NOTE: BEFORE the `first_row_indentation`.
|
||||
|
||||
let mut cursor_y = 0.0;
|
||||
let mut row_start_idx = 0;
|
||||
|
||||
// start index of the last space. A candidate for a new row.
|
||||
let mut last_space = None;
|
||||
|
||||
let mut out_rows = vec![];
|
||||
|
||||
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
|
||||
debug_assert!(chr != '\n');
|
||||
let potential_row_width = first_row_indentation + x - row_start_x;
|
||||
|
||||
if potential_row_width > max_width_in_points {
|
||||
if let Some(last_space_idx) = last_space {
|
||||
// We include the trailing space in the row:
|
||||
let row = Row {
|
||||
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
|
||||
.iter()
|
||||
.map(|x| first_row_indentation + x - row_start_x)
|
||||
.collect(),
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.row_height(),
|
||||
ends_with_newline: false,
|
||||
};
|
||||
row.sanity_check();
|
||||
out_rows.push(row);
|
||||
|
||||
row_start_idx = last_space_idx + 1;
|
||||
row_start_x = first_row_indentation + full_x_offsets[row_start_idx];
|
||||
last_space = None;
|
||||
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
|
||||
} else if out_rows.is_empty() && first_row_indentation > 0.0 {
|
||||
assert_eq!(row_start_idx, 0);
|
||||
// Allow the first row to be completely empty, because we know there will be more space on the next row:
|
||||
let row = Row {
|
||||
x_offsets: vec![first_row_indentation],
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.row_height(),
|
||||
ends_with_newline: false,
|
||||
};
|
||||
row.sanity_check();
|
||||
out_rows.push(row);
|
||||
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
|
||||
first_row_indentation = 0.0; // Continue all other rows as if there is no indentation
|
||||
}
|
||||
}
|
||||
|
||||
const NON_BREAKING_SPACE: char = '\u{A0}';
|
||||
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
|
||||
last_space = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
if row_start_idx + 1 < full_x_offsets.len() {
|
||||
let row = Row {
|
||||
x_offsets: full_x_offsets[row_start_idx..]
|
||||
.iter()
|
||||
.map(|x| first_row_indentation + x - row_start_x)
|
||||
.collect(),
|
||||
y_min: cursor_y,
|
||||
y_max: cursor_y + self.row_height(),
|
||||
ends_with_newline: false,
|
||||
};
|
||||
row.sanity_check();
|
||||
out_rows.push(row);
|
||||
}
|
||||
|
||||
out_rows
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_glyph(
|
||||
atlas: &mut TextureAtlas,
|
||||
glyph: rusttype::Glyph<'static>,
|
||||
scale_in_pixels: f32,
|
||||
y_offset: f32,
|
||||
pixels_per_point: f32,
|
||||
) -> GlyphInfo {
|
||||
assert!(glyph.id().0 != 0);
|
||||
|
||||
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
|
||||
let glyph = glyph.positioned(point(0.0, 0.0));
|
||||
|
||||
let uv_rect = if let Some(bb) = glyph.pixel_bounding_box() {
|
||||
let glyph_width = bb.width() as usize;
|
||||
let glyph_height = bb.height() as usize;
|
||||
|
||||
if glyph_width == 0 || glyph_height == 0 {
|
||||
None
|
||||
} else {
|
||||
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
|
||||
|
||||
let texture = atlas.texture_mut();
|
||||
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;
|
||||
texture[(px, py)] = (v * 255.0).round() as u8;
|
||||
}
|
||||
});
|
||||
|
||||
let offset_in_pixels = vec2(bb.min.x as f32, scale_in_pixels as f32 + bb.min.y as f32);
|
||||
let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y;
|
||||
Some(UvRect {
|
||||
offset,
|
||||
size: vec2(glyph_width as f32, glyph_height as f32) / 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,
|
||||
),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// No bounding box. Maybe a space?
|
||||
None
|
||||
};
|
||||
|
||||
let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
|
||||
|
||||
GlyphInfo {
|
||||
id: glyph.id(),
|
||||
advance_width: advance_width_in_points,
|
||||
uv_rect,
|
||||
}
|
||||
}
|
||||
347
epaint/src/text/fonts.rs
Normal file
347
epaint/src/text/fonts.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
mutex::Mutex,
|
||||
text::font::{Font, FontImpl},
|
||||
Texture, TextureAtlas,
|
||||
};
|
||||
|
||||
// TODO: rename
|
||||
/// One of a few categories of styles of text, e.g. body, button or heading.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "persistence", serde(rename_all = "snake_case"))]
|
||||
pub enum TextStyle {
|
||||
/// Used when small text is needed.
|
||||
Small,
|
||||
/// Normal labels. Easily readable, doesn't take up too much space.
|
||||
Body,
|
||||
/// Buttons. Maybe slightly bigger than `Body`.
|
||||
Button,
|
||||
/// Heading. Probably larger than `Body`.
|
||||
Heading,
|
||||
/// Same size as `Body`, but used when monospace is important (for aligning number, code snippets, etc).
|
||||
Monospace,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
pub fn all() -> impl Iterator<Item = TextStyle> {
|
||||
[
|
||||
TextStyle::Small,
|
||||
TextStyle::Body,
|
||||
TextStyle::Button,
|
||||
TextStyle::Heading,
|
||||
TextStyle::Monospace,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which style of font: [`Monospace`][`FontFamily::Monospace`] or [`Proportional`][`FontFamily::Proportional`].
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "persistence", serde(rename_all = "snake_case"))]
|
||||
pub enum FontFamily {
|
||||
/// A font where each character is the same width (`w` is the same width as `i`).
|
||||
Monospace,
|
||||
/// A font where some characters are wider than other (e.g. 'w' is wider than 'i').
|
||||
Proportional,
|
||||
}
|
||||
|
||||
/// The data of a `.ttf` or `.otf` file.
|
||||
pub type FontData = std::borrow::Cow<'static, [u8]>;
|
||||
|
||||
fn rusttype_font_from_font_data(name: &str, data: &FontData) -> rusttype::Font<'static> {
|
||||
match data {
|
||||
std::borrow::Cow::Borrowed(bytes) => rusttype::Font::try_from_bytes(bytes),
|
||||
std::borrow::Cow::Owned(bytes) => rusttype::Font::try_from_vec(bytes.clone()),
|
||||
}
|
||||
.unwrap_or_else(|| panic!("Error parsing {:?} TTF/OTF font file", name))
|
||||
}
|
||||
|
||||
/// Describes the font data and the sizes to use.
|
||||
///
|
||||
/// This is how you can tell Egui which fonts and font sizes to use.
|
||||
///
|
||||
/// Often you would start with [`FontDefinitions::default()`] and then add/change the contents.
|
||||
///
|
||||
/// ``` ignore
|
||||
/// # let mut ctx = egui::CtxRef::default();
|
||||
/// let mut fonts = egui::FontDefinitions::default();
|
||||
/// // Large button text:
|
||||
/// fonts.family_and_size.insert(
|
||||
/// egui::TextStyle::Button,
|
||||
/// (egui::FontFamily::Proportional, 32.0));
|
||||
/// ctx.set_fonts(fonts);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "persistence", serde(default))]
|
||||
pub struct FontDefinitions {
|
||||
/// List of font names and their definitions.
|
||||
/// The definition must be the contents of either a `.ttf` or `.otf` font file.
|
||||
///
|
||||
/// Egui has built-in-default for these,
|
||||
/// but you can override them if you like.
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
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 Egui 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 fonts_for_family: BTreeMap<FontFamily, Vec<String>>,
|
||||
|
||||
/// The [`FontFamily`] and size you want to use for a specific [`TextStyle`].
|
||||
pub family_and_size: BTreeMap<TextStyle, (FontFamily, f32)>,
|
||||
}
|
||||
|
||||
impl Default for FontDefinitions {
|
||||
fn default() -> Self {
|
||||
#[allow(unused)]
|
||||
let mut font_data: BTreeMap<String, FontData> = BTreeMap::new();
|
||||
|
||||
let mut fonts_for_family = BTreeMap::new();
|
||||
|
||||
#[cfg(feature = "default_fonts")]
|
||||
{
|
||||
// TODO: figure out a way to make the WASM smaller despite including fonts. Zip them?
|
||||
|
||||
// Use size 13 for this. NOTHING ELSE:
|
||||
font_data.insert(
|
||||
"ProggyClean".to_owned(),
|
||||
std::borrow::Cow::Borrowed(include_bytes!("../../fonts/ProggyClean.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"Ubuntu-Light".to_owned(),
|
||||
std::borrow::Cow::Borrowed(include_bytes!("../../fonts/Ubuntu-Light.ttf")),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
std::borrow::Cow::Borrowed(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")),
|
||||
);
|
||||
// Bigger emojis, and more. <http://jslegers.github.io/emoji-icon-font/>:
|
||||
font_data.insert(
|
||||
"emoji-icon-font".to_owned(),
|
||||
std::borrow::Cow::Borrowed(include_bytes!("../../fonts/emoji-icon-font.ttf")),
|
||||
);
|
||||
|
||||
fonts_for_family.insert(
|
||||
FontFamily::Monospace,
|
||||
vec![
|
||||
"ProggyClean".to_owned(),
|
||||
"Ubuntu-Light".to_owned(), // fallback for √ etc
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
"emoji-icon-font".to_owned(),
|
||||
],
|
||||
);
|
||||
fonts_for_family.insert(
|
||||
FontFamily::Proportional,
|
||||
vec![
|
||||
"Ubuntu-Light".to_owned(),
|
||||
"NotoEmoji-Regular".to_owned(),
|
||||
"emoji-icon-font".to_owned(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "default_fonts"))]
|
||||
{
|
||||
fonts_for_family.insert(FontFamily::Monospace, vec![]);
|
||||
fonts_for_family.insert(FontFamily::Proportional, vec![]);
|
||||
}
|
||||
|
||||
let mut family_and_size = BTreeMap::new();
|
||||
family_and_size.insert(TextStyle::Small, (FontFamily::Proportional, 10.0));
|
||||
family_and_size.insert(TextStyle::Body, (FontFamily::Proportional, 14.0));
|
||||
family_and_size.insert(TextStyle::Button, (FontFamily::Proportional, 16.0));
|
||||
family_and_size.insert(TextStyle::Heading, (FontFamily::Proportional, 20.0));
|
||||
family_and_size.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0)); // 13 for `ProggyClean`
|
||||
|
||||
Self {
|
||||
font_data,
|
||||
fonts_for_family,
|
||||
family_and_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The collection of fonts used by Egui.
|
||||
///
|
||||
/// Note: `Fonts::default()` is invalid (missing `pixels_per_point`).
|
||||
#[derive(Default)]
|
||||
pub struct Fonts {
|
||||
pixels_per_point: f32,
|
||||
definitions: FontDefinitions,
|
||||
fonts: BTreeMap<TextStyle, Font>,
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
/// Copy of the texture in the texture atlas.
|
||||
/// This is so we can return a reference to it (the texture atlas is behind a lock).
|
||||
buffered_texture: Mutex<Arc<Texture>>,
|
||||
}
|
||||
|
||||
impl Fonts {
|
||||
pub fn from_definitions(pixels_per_point: f32, definitions: FontDefinitions) -> Self {
|
||||
// We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`,
|
||||
// so we can show the Emoji picker demo window.
|
||||
let mut atlas = TextureAtlas::new(2048, 64);
|
||||
|
||||
{
|
||||
// Make the top left pixel fully white:
|
||||
let pos = atlas.allocate((1, 1));
|
||||
assert_eq!(pos, (0, 0));
|
||||
atlas.texture_mut()[pos] = 255;
|
||||
}
|
||||
|
||||
let atlas = Arc::new(Mutex::new(atlas));
|
||||
|
||||
let mut font_impl_cache = FontImplCache::new(atlas.clone(), pixels_per_point, &definitions);
|
||||
|
||||
let fonts = definitions
|
||||
.family_and_size
|
||||
.iter()
|
||||
.map(|(&text_style, &(family, scale_in_points))| {
|
||||
let fonts = &definitions.fonts_for_family.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| font_impl_cache.font_impl(font_name, scale_in_points))
|
||||
.collect();
|
||||
|
||||
(text_style, Font::new(fonts))
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let mut atlas = atlas.lock();
|
||||
let texture = atlas.texture_mut();
|
||||
// Make sure we seed the texture version with something unique based on the default characters:
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
let mut hasher = DefaultHasher::default();
|
||||
texture.pixels.hash(&mut hasher);
|
||||
texture.version = hasher.finish();
|
||||
}
|
||||
|
||||
Self {
|
||||
pixels_per_point,
|
||||
definitions,
|
||||
fonts,
|
||||
atlas,
|
||||
buffered_texture: Default::default(), //atlas.lock().texture().clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixels_per_point(&self) -> f32 {
|
||||
self.pixels_per_point
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> &FontDefinitions {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
/// Call each frame to get the latest available font texture data.
|
||||
pub fn texture(&self) -> Arc<Texture> {
|
||||
let atlas = self.atlas.lock();
|
||||
let mut buffered_texture = self.buffered_texture.lock();
|
||||
if buffered_texture.version != atlas.texture().version {
|
||||
*buffered_texture = Arc::new(atlas.texture().clone());
|
||||
}
|
||||
|
||||
buffered_texture.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<TextStyle> for Fonts {
|
||||
type Output = Font;
|
||||
|
||||
fn index(&self, text_style: TextStyle) -> &Font {
|
||||
&self.fonts[&text_style]
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct FontImplCache {
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
rusttype_fonts: std::collections::BTreeMap<String, Arc<rusttype::Font<'static>>>,
|
||||
|
||||
/// Map font names and size to the cached `FontImpl`.
|
||||
/// Can't have f32 in a HashMap or BTreeMap, so let's do a linear search
|
||||
cache: Vec<(String, f32, Arc<FontImpl>)>,
|
||||
}
|
||||
|
||||
impl FontImplCache {
|
||||
pub fn new(
|
||||
atlas: Arc<Mutex<TextureAtlas>>,
|
||||
pixels_per_point: f32,
|
||||
definitions: &super::FontDefinitions,
|
||||
) -> Self {
|
||||
let rusttype_fonts = definitions
|
||||
.font_data
|
||||
.iter()
|
||||
.map(|(name, font_data)| {
|
||||
(
|
||||
name.clone(),
|
||||
Arc::new(rusttype_font_from_font_data(name, font_data)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
atlas,
|
||||
pixels_per_point,
|
||||
rusttype_fonts,
|
||||
cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rusttype_font(&self, font_name: &str) -> Arc<rusttype::Font<'static>> {
|
||||
self.rusttype_fonts
|
||||
.get(font_name)
|
||||
.unwrap_or_else(|| panic!("No font data found for {:?}", font_name))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn font_impl(&mut self, font_name: &str, scale_in_points: f32) -> Arc<FontImpl> {
|
||||
for entry in &self.cache {
|
||||
if (entry.0.as_str(), entry.1) == (font_name, scale_in_points) {
|
||||
return entry.2.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let y_offset = if font_name == "emoji-icon-font" {
|
||||
1.0 // TODO: remove font alignment hack
|
||||
} else {
|
||||
-3.0 // TODO: remove font alignment hack
|
||||
};
|
||||
|
||||
let scale_in_points = if font_name == "emoji-icon-font" {
|
||||
scale_in_points - 2.0 // TODO: remove HACK!
|
||||
} else {
|
||||
scale_in_points
|
||||
};
|
||||
|
||||
let font_impl = Arc::new(FontImpl::new(
|
||||
self.atlas.clone(),
|
||||
self.pixels_per_point,
|
||||
self.rusttype_font(font_name),
|
||||
scale_in_points,
|
||||
y_offset,
|
||||
));
|
||||
self.cache
|
||||
.push((font_name.to_owned(), scale_in_points, font_impl.clone()));
|
||||
font_impl
|
||||
}
|
||||
}
|
||||
780
epaint/src/text/galley.rs
Normal file
780
epaint/src/text/galley.rs
Normal file
@@ -0,0 +1,780 @@
|
||||
//! A [`Galley`] is a piece of text after layout, i.e. where each character has been assigned a position.
|
||||
//!
|
||||
//! ## How it works
|
||||
//! This is going to get complicated.
|
||||
//!
|
||||
//! To avoid confusion, we never use the word "line".
|
||||
//! The `\n` character demarcates the split of text into "paragraphs".
|
||||
//! Each paragraph is wrapped at some width onto one or more "rows".
|
||||
//!
|
||||
//! If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
|
||||
//! do we prefer the next row?
|
||||
//! For instance, consider this single paragraph, word wrapped:
|
||||
//! ``` text
|
||||
//! Hello_
|
||||
//! world!
|
||||
//! ```
|
||||
//!
|
||||
//! The offset `6` is both the end of the first row
|
||||
//! and the start of the second row.
|
||||
//! [`CCursor::prefer_next_row`] etc selects which.
|
||||
|
||||
use super::cursor::*;
|
||||
use emath::{pos2, NumExt, Rect, Vec2};
|
||||
|
||||
/// A collection of text locked into place.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Galley {
|
||||
/// The full text, including any an all `\n`.
|
||||
pub text: String,
|
||||
|
||||
/// Rows of text, from top to bottom.
|
||||
/// The number of chars in all rows sum up to text.chars().count().
|
||||
/// Note that each paragraph (pieces of text separated with `\n`)
|
||||
/// can be split up into multiple rows.
|
||||
pub rows: Vec<Row>,
|
||||
|
||||
// Optimization: calculated once and reused.
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
/// A typeset piece of text on a single row.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Row {
|
||||
/// The start of each character, probably starting at zero.
|
||||
/// The last element is the end of the last character.
|
||||
/// This is never empty.
|
||||
/// Unit: points.
|
||||
///
|
||||
/// `x_offsets.len() + (ends_with_newline as usize) == text.chars().count() + 1`
|
||||
pub x_offsets: Vec<f32>,
|
||||
|
||||
/// Top of the row, offset within the Galley.
|
||||
/// Unit: points.
|
||||
pub y_min: f32,
|
||||
|
||||
/// Bottom of the row, offset within the Galley.
|
||||
/// Unit: points.
|
||||
pub y_max: f32,
|
||||
|
||||
/// If true, this `Row` came from a paragraph ending with a `\n`.
|
||||
/// The `\n` itself is omitted from `x_offsets`.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn sanity_check(&self) {
|
||||
assert!(!self.x_offsets.is_empty());
|
||||
}
|
||||
|
||||
/// Excludes the implicit `\n` after the `Row`, if any.
|
||||
pub fn char_count_excluding_newline(&self) -> usize {
|
||||
assert!(!self.x_offsets.is_empty());
|
||||
self.x_offsets.len() - 1
|
||||
}
|
||||
|
||||
/// Includes the implicit `\n` after the `Row`, if any.
|
||||
pub fn char_count_including_newline(&self) -> usize {
|
||||
self.char_count_excluding_newline() + (self.ends_with_newline as usize)
|
||||
}
|
||||
|
||||
pub fn min_x(&self) -> f32 {
|
||||
*self.x_offsets.first().unwrap()
|
||||
}
|
||||
|
||||
pub fn max_x(&self) -> f32 {
|
||||
*self.x_offsets.last().unwrap()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.y_max - self.y_min
|
||||
}
|
||||
|
||||
pub fn rect(&self) -> Rect {
|
||||
Rect::from_min_max(
|
||||
pos2(self.min_x(), self.y_min),
|
||||
pos2(self.max_x(), self.y_max),
|
||||
)
|
||||
}
|
||||
|
||||
/// Closest char at the desired x coordinate.
|
||||
/// Returns something in the range `[0, char_count_excluding_newline()]`.
|
||||
pub fn char_at(&self, desired_x: f32) -> usize {
|
||||
for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() {
|
||||
let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]);
|
||||
if desired_x < char_center_x {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
self.char_count_excluding_newline()
|
||||
}
|
||||
|
||||
pub fn x_offset(&self, column: usize) -> f32 {
|
||||
self.x_offsets[column.min(self.x_offsets.len() - 1)]
|
||||
}
|
||||
}
|
||||
|
||||
impl Galley {
|
||||
pub fn sanity_check(&self) {
|
||||
let mut char_count = 0;
|
||||
for row in &self.rows {
|
||||
row.sanity_check();
|
||||
char_count += row.char_count_including_newline();
|
||||
}
|
||||
assert_eq!(char_count, self.text.chars().count());
|
||||
if let Some(last_row) = self.rows.last() {
|
||||
debug_assert!(
|
||||
!last_row.ends_with_newline,
|
||||
"If the text ends with '\\n', there would be an empty row last.\n\
|
||||
Galley: {:#?}",
|
||||
self
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Physical positions
|
||||
impl Galley {
|
||||
fn end_pos(&self) -> Rect {
|
||||
if let Some(row) = self.rows.last() {
|
||||
let x = row.max_x();
|
||||
Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max))
|
||||
} 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.y_min), pos2(x, row.y_max));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) // The one 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 y_dist = (row.y_min - pos.y).abs().min((row.y_max - pos.y).abs());
|
||||
if 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
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() {
|
||||
debug_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();
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(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: 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].max_x() {
|
||||
// beyond the end of this row - keep same colum
|
||||
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].max_x() {
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_text_layout() {
|
||||
impl PartialEq for Cursor {
|
||||
fn eq(&self, other: &Cursor) -> bool {
|
||||
(self.ccursor, self.rcursor, self.pcursor)
|
||||
== (other.ccursor, other.rcursor, other.pcursor)
|
||||
}
|
||||
}
|
||||
|
||||
use crate::*;
|
||||
|
||||
let pixels_per_point = 1.0;
|
||||
let fonts = text::Fonts::from_definitions(pixels_per_point, text::FontDefinitions::default());
|
||||
let font = &fonts[TextStyle::Monospace];
|
||||
|
||||
let galley = font.layout_multiline("".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[0].x_offsets, vec![0.0]);
|
||||
|
||||
let galley = font.layout_multiline("\n".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 2);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
||||
|
||||
let galley = font.layout_multiline("\n\n".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 3);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[2].x_offsets, vec![0.0]);
|
||||
|
||||
let galley = font.layout_multiline(" ".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
|
||||
let galley = font.layout_multiline("One row!".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
|
||||
let galley = font.layout_multiline("First row!\n".to_owned(), 1024.0);
|
||||
assert_eq!(galley.rows.len(), 2);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
||||
|
||||
let galley = font.layout_multiline("line\nbreak".to_owned(), 10.0);
|
||||
assert_eq!(galley.rows.len(), 2);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||
|
||||
// Test wrapping:
|
||||
let galley = font.layout_multiline("word wrap".to_owned(), 10.0);
|
||||
assert_eq!(galley.rows.len(), 2);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||
|
||||
{
|
||||
// Test wrapping:
|
||||
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
|
||||
assert_eq!(galley.rows.len(), 4);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[0].char_count_excluding_newline(), "word ".len());
|
||||
assert_eq!(galley.rows[0].char_count_including_newline(), "word ".len());
|
||||
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[1].char_count_excluding_newline(), "wrap.".len());
|
||||
assert_eq!(
|
||||
galley.rows[1].char_count_including_newline(),
|
||||
"wrap.\n".len()
|
||||
);
|
||||
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[3].ends_with_newline, false);
|
||||
|
||||
let cursor = Cursor::default();
|
||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||
|
||||
let cursor = galley.end();
|
||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||
assert_eq!(
|
||||
cursor,
|
||||
Cursor {
|
||||
ccursor: CCursor::new(25),
|
||||
rcursor: RCursor { row: 3, column: 10 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 1,
|
||||
offset: 14,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let cursor = galley.from_ccursor(CCursor::new(1));
|
||||
assert_eq!(cursor.rcursor, RCursor { row: 0, column: 1 });
|
||||
assert_eq!(
|
||||
cursor.pcursor,
|
||||
PCursor {
|
||||
paragraph: 0,
|
||||
offset: 1,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||
|
||||
let cursor = galley.from_pcursor(PCursor {
|
||||
paragraph: 1,
|
||||
offset: 2,
|
||||
prefer_next_row: false,
|
||||
});
|
||||
assert_eq!(cursor.rcursor, RCursor { row: 2, column: 2 });
|
||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||
|
||||
let cursor = galley.from_pcursor(PCursor {
|
||||
paragraph: 1,
|
||||
offset: 6,
|
||||
prefer_next_row: false,
|
||||
});
|
||||
assert_eq!(cursor.rcursor, RCursor { row: 3, column: 2 });
|
||||
assert_eq!(cursor, galley.from_ccursor(cursor.ccursor));
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
assert_eq!(cursor, galley.from_pcursor(cursor.pcursor));
|
||||
|
||||
// On the border between two rows within the same paragraph:
|
||||
let cursor = galley.from_rcursor(RCursor { row: 0, column: 5 });
|
||||
assert_eq!(
|
||||
cursor,
|
||||
Cursor {
|
||||
ccursor: CCursor::new(5),
|
||||
rcursor: RCursor { row: 0, column: 5 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 0,
|
||||
offset: 5,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
|
||||
let cursor = galley.from_rcursor(RCursor { row: 1, column: 0 });
|
||||
assert_eq!(
|
||||
cursor,
|
||||
Cursor {
|
||||
ccursor: CCursor::new(5),
|
||||
rcursor: RCursor { row: 1, column: 0 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 0,
|
||||
offset: 5,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(cursor, galley.from_rcursor(cursor.rcursor));
|
||||
}
|
||||
|
||||
{
|
||||
// Test cursor movement:
|
||||
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
|
||||
assert_eq!(galley.rows.len(), 4);
|
||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||
assert_eq!(galley.rows[2].ends_with_newline, false);
|
||||
assert_eq!(galley.rows[3].ends_with_newline, false);
|
||||
|
||||
let cursor = Cursor::default();
|
||||
|
||||
assert_eq!(galley.cursor_up_one_row(&cursor), cursor);
|
||||
assert_eq!(galley.cursor_begin_of_row(&cursor), cursor);
|
||||
|
||||
assert_eq!(
|
||||
galley.cursor_end_of_row(&cursor),
|
||||
Cursor {
|
||||
ccursor: CCursor::new(5),
|
||||
rcursor: RCursor { row: 0, column: 5 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 0,
|
||||
offset: 5,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galley.cursor_down_one_row(&cursor),
|
||||
Cursor {
|
||||
ccursor: CCursor::new(5),
|
||||
rcursor: RCursor { row: 1, column: 0 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 0,
|
||||
offset: 5,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let cursor = Cursor::default();
|
||||
assert_eq!(
|
||||
galley.cursor_down_one_row(&galley.cursor_down_one_row(&cursor)),
|
||||
Cursor {
|
||||
ccursor: CCursor::new(11),
|
||||
rcursor: RCursor { row: 2, column: 0 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 1,
|
||||
offset: 0,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let cursor = galley.end();
|
||||
assert_eq!(galley.cursor_down_one_row(&cursor), cursor);
|
||||
|
||||
let cursor = galley.end();
|
||||
assert!(galley.cursor_up_one_row(&galley.end()) != cursor);
|
||||
|
||||
assert_eq!(
|
||||
galley.cursor_up_one_row(&galley.end()),
|
||||
Cursor {
|
||||
ccursor: CCursor::new(15),
|
||||
rcursor: RCursor { row: 2, column: 10 },
|
||||
pcursor: PCursor {
|
||||
paragraph: 1,
|
||||
offset: 4,
|
||||
prefer_next_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
9
epaint/src/text/mod.rs
Normal file
9
epaint/src/text/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod cursor;
|
||||
mod font;
|
||||
mod fonts;
|
||||
mod galley;
|
||||
|
||||
pub use {
|
||||
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
||||
galley::{Galley, Row},
|
||||
};
|
||||
109
epaint/src/texture_atlas.rs
Normal file
109
epaint/src/texture_atlas.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
// TODO: `TextureData` or similar?
|
||||
/// An 8-bit texture containing font data.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Texture {
|
||||
/// e.g. a hash of the data. Use this to detect changes!
|
||||
/// If the texture changes, this too will change.
|
||||
pub version: u64,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
/// White color with the given alpha (linear space 0-255).
|
||||
pub pixels: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
|
||||
pub fn srgba_pixels(&'_ self) -> impl Iterator<Item = super::Color32> + '_ {
|
||||
use super::Color32;
|
||||
let srgba_from_luminance_lut: Vec<Color32> =
|
||||
(0..=255).map(Color32::from_white_alpha).collect();
|
||||
self.pixels
|
||||
.iter()
|
||||
.map(move |&l| srgba_from_luminance_lut[l as usize])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<(usize, usize)> for Texture {
|
||||
type Output = u8;
|
||||
|
||||
fn index(&self, (x, y): (usize, usize)) -> &u8 {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
&self.pixels[y * self.width + x]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<(usize, usize)> for Texture {
|
||||
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 {
|
||||
assert!(x < self.width);
|
||||
assert!(y < self.height);
|
||||
&mut self.pixels[y * self.width + x]
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
||||
///
|
||||
/// More characters can be added, possibly expanding the texture.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TextureAtlas {
|
||||
texture: Texture,
|
||||
|
||||
/// Used for when allocating new rectangles.
|
||||
cursor: (usize, usize),
|
||||
row_height: usize,
|
||||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
texture: Texture {
|
||||
version: 0,
|
||||
width,
|
||||
height,
|
||||
pixels: vec![0; width * height],
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn texture(&self) -> &Texture {
|
||||
&self.texture
|
||||
}
|
||||
|
||||
pub fn texture_mut(&mut self) -> &mut Texture {
|
||||
self.texture.version += 1;
|
||||
&mut self.texture
|
||||
}
|
||||
|
||||
/// Returns the coordinates of where the rect ended up.
|
||||
pub fn allocate(&mut self, (w, h): (usize, usize)) -> (usize, usize) {
|
||||
/// 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.texture.width);
|
||||
if self.cursor.0 + w > self.texture.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);
|
||||
while self.cursor.1 + self.row_height >= self.texture.height {
|
||||
self.texture.height *= 2;
|
||||
}
|
||||
|
||||
if self.texture.width * self.texture.height > self.texture.pixels.len() {
|
||||
self.texture
|
||||
.pixels
|
||||
.resize(self.texture.width * self.texture.height, 0);
|
||||
}
|
||||
|
||||
let pos = self.cursor;
|
||||
self.cursor.0 += w + PADDING;
|
||||
self.texture.version += 1;
|
||||
(pos.0 as usize, pos.1 as usize)
|
||||
}
|
||||
}
|
||||
209
epaint/src/triangles.rs
Normal file
209
epaint/src/triangles.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
use crate::*;
|
||||
use emath::*;
|
||||
|
||||
/// The vertex type.
|
||||
///
|
||||
/// Should be friendly to send to GPU as is.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
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
|
||||
}
|
||||
|
||||
/// Textured triangles.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Triangles {
|
||||
/// Draw as triangles (i.e. the length is always multiple of three).
|
||||
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,
|
||||
}
|
||||
|
||||
impl Triangles {
|
||||
pub fn with_texture(texture_id: TextureId) -> Self {
|
||||
Self {
|
||||
texture_id,
|
||||
..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 {
|
||||
let n = self.vertices.len() as u32;
|
||||
self.indices.iter().all(|&i| i < n)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.indices.is_empty() && self.vertices.is_empty()
|
||||
}
|
||||
|
||||
/// Append all the indices and vertices of `other` to `self`.
|
||||
pub fn append(&mut self, other: Triangles) {
|
||||
debug_assert!(other.is_valid());
|
||||
|
||||
if self.is_empty() {
|
||||
*self = other;
|
||||
} else {
|
||||
assert_eq!(
|
||||
self.texture_id, other.texture_id,
|
||||
"Can't merge Triangles using different textures"
|
||||
);
|
||||
|
||||
let index_offset = self.vertices.len() as u32;
|
||||
for index in &other.indices {
|
||||
self.indices.push(index_offset + index);
|
||||
}
|
||||
self.vertices.extend(other.vertices.iter());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) {
|
||||
debug_assert!(self.texture_id == TextureId::Egui);
|
||||
self.vertices.push(Vertex {
|
||||
pos,
|
||||
uv: WHITE_UV,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
/// Add a triangle.
|
||||
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`.
|
||||
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`.
|
||||
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, pos: 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);
|
||||
|
||||
let right_top = Vertex {
|
||||
pos: pos.right_top(),
|
||||
uv: uv.right_top(),
|
||||
color,
|
||||
};
|
||||
let left_top = Vertex {
|
||||
pos: pos.left_top(),
|
||||
uv: uv.left_top(),
|
||||
color,
|
||||
};
|
||||
let left_bottom = Vertex {
|
||||
pos: pos.left_bottom(),
|
||||
uv: uv.left_bottom(),
|
||||
color,
|
||||
};
|
||||
let right_bottom = Vertex {
|
||||
pos: pos.right_bottom(),
|
||||
uv: uv.right_bottom(),
|
||||
color,
|
||||
};
|
||||
self.vertices.push(left_top);
|
||||
self.vertices.push(right_top);
|
||||
self.vertices.push(left_bottom);
|
||||
self.vertices.push(right_bottom);
|
||||
}
|
||||
|
||||
/// Uniformly colored rectangle.
|
||||
pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) {
|
||||
debug_assert!(self.texture_id == TextureId::Egui);
|
||||
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).
|
||||
/// All the returned meshes will have indices that fit into a `u16`.
|
||||
pub fn split_to_u16(self) -> Vec<Triangles> {
|
||||
const MAX_SIZE: u32 = 1 << 16;
|
||||
|
||||
if self.vertices.len() < MAX_SIZE as usize {
|
||||
return vec![self]; // Common-case optimization
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if new_max - new_min < 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
|
||||
);
|
||||
|
||||
output.push(Triangles {
|
||||
indices: self.indices[span_start..index_cursor]
|
||||
.iter()
|
||||
.map(|vi| vi - min_vindex)
|
||||
.collect(),
|
||||
vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(),
|
||||
texture_id: self.texture_id,
|
||||
});
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Translate location by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
for v in &mut self.vertices {
|
||||
v.pos += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user