Merge pull request #63 from emilk/emoji

Emoji Support
This commit is contained in:
Emil Ernerfeldt 2020-12-12 20:11:45 +01:00 committed by GitHub
commit 4c9a4896af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 4584 additions and 294 deletions

View file

@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ⭐
* Emoji support:
* 1216 different emojis.
* Works in any text.
* Great for button icons.
* The Demo app comes with a Font Book to explore the available glyphs.
* Wrapping layouts:
* `ui.horizontal_wrapped(|ui| ...)`: Add widgets on a row but wrap at `max_size`.
* `ui.horizontal_wrapped_for_text`: Like `horizontal_wrapped`, but with spacing made for embedding text.
@ -22,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed 🔧
* Changed default font to [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu).
* Remove minimum button width
* Refactored `egui::Layout` substantially, changing its interface.
### Removed 🔥

View file

@ -209,5 +209,7 @@ Egui is under MIT OR Apache-2.0 license.
Fonts:
* ProggyClean.ttf, Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. <http://www.proggyfonts.net/>
* Ubuntu-Light.ttf by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
* `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
* `ProggyClean.ttf`: Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. <http://www.proggyfonts.net/>
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)

View file

@ -21,8 +21,6 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [/] Unicode
* [/] Text editing of unicode (needs more testing)
* [ ] Font with some more unicode characters
* [ ] Emoji support (great for things like ▶️⏸⏹⚠︎)
* [ ] Change text style/color and continue in same layout
* Menu bar (File, Edit, etc)
* [ ] Sub-menus
* [ ] Keyboard shortcuts
@ -117,6 +115,8 @@ Ability to do a search for any widget. The search works even for collapsed regio
* Widgets
* [x] Label
* [x] Emoji support (great for things like ▶️⏸⏹⚠︎)
* [x] Change text style/color and continue in same layout
* [x] Button
* [x] Checkbox
* [x] Radiobutton

Binary file not shown.

185
OFL.txt → egui/fonts/OFL.txt Executable file → Normal file
View file

@ -1,93 +1,92 @@
Copyright 2011 The Comfortaa Project Authors (https://github.com/alexeiva/comfortaa), with Reserved Font Name "Comfortaa".
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.
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.

View 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.

Binary file not shown.

View file

@ -587,13 +587,13 @@ impl Context {
pub fn settings_ui(&self, ui: &mut Ui) {
use crate::containers::*;
CollapsingHeader::new("Style")
CollapsingHeader::new("🎑 Style")
.default_open(true)
.show(ui, |ui| {
self.style_ui(ui);
});
CollapsingHeader::new("Fonts")
CollapsingHeader::new("🔠 Fonts")
.default_open(false)
.show(ui, |ui| {
let mut font_definitions = self.fonts().definitions().clone();
@ -602,7 +602,7 @@ impl Context {
self.set_fonts(font_definitions);
});
CollapsingHeader::new("Painting")
CollapsingHeader::new("Painting")
.default_open(true)
.show(ui, |ui| {
let mut tesselation_options = self.options.lock().tesselation_options;
@ -625,11 +625,11 @@ impl Context {
.on_hover_text("Is Egui currently listening for text input");
ui.advance_cursor(16.0);
CollapsingHeader::new("Input")
CollapsingHeader::new("📥 Input")
.default_open(false)
.show(ui, |ui| ui.input().clone().ui(ui));
CollapsingHeader::new("Paint stats")
CollapsingHeader::new("📊 Paint stats")
.default_open(true)
.show(ui, |ui| {
self.paint_stats.lock().ui(ui);

View file

@ -89,7 +89,7 @@ impl FrameHistory {
);
crate::demos::warn_if_debug_build(ui);
crate::CollapsingHeader::new("CPU usage history")
crate::CollapsingHeader::new("📊 CPU usage history")
.default_open(false)
.show(ui, |ui| {
self.graph(ui);
@ -304,7 +304,7 @@ impl app::App for DemoApp {
&mut integration_context.tex_allocator,
);
crate::Window::new("Backend")
crate::Window::new("💻 Backend")
.min_width(360.0)
.scroll(false)
.show(ctx, |ui| {

View file

@ -14,7 +14,7 @@ impl Default for DancingStrings {
impl Demo for DancingStrings {
fn name(&self) -> &str {
"Dancing Strings"
"Dancing Strings"
}
fn show(&mut self, ctx: &Arc<Context>, open: &mut bool) {

View file

@ -37,6 +37,7 @@ impl Default for Demos {
fn default() -> Self {
Self {
demos: vec![
(false, Box::new(crate::demos::FontBook::default())),
(false, Box::new(crate::demos::DancingStrings::default())),
(false, Box::new(crate::demos::DragAndDropDemo::default())),
(false, Box::new(crate::demos::Tests::default())),
@ -103,10 +104,12 @@ impl DemoWindows {
}
crate::SidePanel::left(Id::new("side_panel"), 200.0).show(ctx, |ui| {
ui.heading("Egui Demo");
ui.heading("Egui Demo");
crate::demos::warn_if_debug_build(ui);
ui.label("Egui is an immediate mode GUI library written in Rust.");
ui.add(crate::Hyperlink::new("https://github.com/emilk/egui").text("Egui home page"));
ui.add(crate::Hyperlink::new("https://github.com/emilk/egui").text(" Egui home page"));
ui.label("Egui can be run on the web, or natively on 🐧");
ui.separator();
ui.label(
@ -117,7 +120,7 @@ impl DemoWindows {
}
ui.separator();
ui.heading("Windows:");
ui.heading(" Windows:");
ui.indent("windows", |ui| {
self.open_windows.checkboxes(ui);
self.demos.checkboxes(ui);
@ -147,34 +150,34 @@ impl DemoWindows {
..
} = self;
Window::new("Demo")
Window::new("Demo")
.open(&mut open_windows.demo)
.scroll(true)
.show(ctx, |ui| {
demo_window.ui(ui);
});
Window::new("Settings")
Window::new("🔧 Settings")
.open(&mut open_windows.settings)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
Window::new("Inspection")
Window::new("🔍 Inspection")
.open(&mut open_windows.inspection)
.scroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
Window::new("Memory")
Window::new("📝 Memory")
.open(&mut open_windows.memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
Window::new("Color Test")
Window::new("🎨 Color Test")
.default_size([800.0, 1024.0])
.scroll(true)
.open(&mut open_windows.color_test)
@ -299,18 +302,18 @@ impl OpenWindows {
color_test,
} = self;
ui.label("Egui:");
ui.checkbox(settings, "Settings");
ui.checkbox(inspection, "Inspection");
ui.checkbox(memory, "Memory");
ui.checkbox(settings, "🔧 Settings");
ui.checkbox(inspection, "🔍 Inspection");
ui.checkbox(memory, "📝 Memory");
ui.separator();
ui.checkbox(demo, "Demo");
ui.checkbox(demo, "Demo");
ui.separator();
ui.checkbox(resize, "Resize examples");
ui.checkbox(color_test, "Color test")
ui.checkbox(resize, "Resize examples");
ui.checkbox(color_test, "🎨 Color test")
.on_hover_text("For testing the integrations painter");
ui.separator();
ui.label("Misc:");
ui.checkbox(fractal_clock, "Fractal Clock");
ui.checkbox(fractal_clock, "🕑 Fractal Clock");
}
}
@ -333,7 +336,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight:
menu::menu(ui, "Windows", |ui| windows.checkboxes(ui));
menu::menu(ui, "About", |ui| {
ui.label("This is Egui");
ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page"));
ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page"));
});
if let Some(time) = seconds_since_midnight {

View file

@ -97,7 +97,7 @@ impl Default for DragAndDropDemo {
impl Demo for DragAndDropDemo {
fn name(&self) -> &str {
"Drag and Drop"
"Drag and Drop"
}
fn show(&mut self, ctx: &std::sync::Arc<Context>, open: &mut bool) {

105
egui/src/demos/font_book.rs Normal file
View file

@ -0,0 +1,105 @@
use crate::*;
pub struct FontBook {
standard: bool,
emojis: bool,
filter: String,
text_style: TextStyle,
}
impl Default for FontBook {
fn default() -> Self {
Self {
standard: false,
emojis: true,
filter: Default::default(),
text_style: TextStyle::Heading,
}
}
}
impl FontBook {
fn characters_ui(&self, ui: &mut Ui, characters: &[(u32, char, &str)]) {
for &(_, chr, name) in characters {
if !self.filter.is_empty() && !name.contains(&self.filter) {
continue;
}
let button = Button::new(chr).text_style(self.text_style).frame(false);
let tooltip_ui = |ui: &mut Ui| {
ui.add(Label::new(chr).text_style(self.text_style));
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
};
if ui.add(button).on_hover_ui(tooltip_ui).clicked {
ui.output().copied_text = chr.to_string();
}
}
}
}
impl demos::Demo for FontBook {
fn name(&self) -> &str {
"🔤 Font Book"
}
fn show(&mut self, ctx: &std::sync::Arc<crate::Context>, open: &mut bool) {
Window::new(self.name()).open(open).show(ctx, |ui| {
use demos::View;
self.ui(ui);
});
}
}
impl demos::View for FontBook {
fn ui(&mut self, ui: &mut Ui) {
use crate::demos::font_contents_emoji::FULL_EMOJI_LIST;
use crate::demos::font_contents_ubuntu::UBUNTU_FONT_CHARACTERS;
ui.label(format!(
"Egui supports {} standard characters and {} emojis.\nClick on a character to copy it.",
UBUNTU_FONT_CHARACTERS.len(),
FULL_EMOJI_LIST.len(),
));
ui.separator();
ui.horizontal(|ui| {
ui.label("Text style:");
for style in TextStyle::all() {
ui.radio_value(&mut self.text_style, style, format!("{:?}", style));
}
});
ui.horizontal(|ui| {
ui.label("Show:");
ui.checkbox(&mut self.standard, "Standard");
ui.checkbox(&mut self.emojis, "Emojis");
});
ui.horizontal(|ui| {
ui.label("Filter:");
ui.text_edit_singleline(&mut self.filter);
self.filter = self.filter.to_lowercase();
if ui.button("").clicked {
self.filter.clear();
}
});
ui.separator();
crate::ScrollArea::auto_sized().show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.style_mut().spacing.item_spacing = Vec2::splat(2.0);
if self.standard {
self.characters_ui(ui, UBUNTU_FONT_CHARACTERS);
}
if self.emojis {
self.characters_ui(ui, FULL_EMOJI_LIST);
}
});
});
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,7 @@ impl FractalClock {
open: &mut bool,
seconds_since_midnight: Option<f64>,
) {
Window::new("FractalClock")
Window::new("🕑 Fractal Clock")
.open(open)
.default_size(vec2(512.0, 512.0))
.scroll(false)

View file

@ -7,6 +7,9 @@ mod dancing_strings;
pub mod demo_window;
mod demo_windows;
mod drag_and_drop;
mod font_book;
pub mod font_contents_emoji;
pub mod font_contents_ubuntu;
mod fractal_clock;
mod sliders;
mod tests;
@ -15,8 +18,8 @@ mod widgets;
pub use {
app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow,
demo_windows::*, drag_and_drop::*, fractal_clock::FractalClock, sliders::Sliders, tests::Tests,
widgets::Widgets,
demo_windows::*, drag_and_drop::*, font_book::FontBook, fractal_clock::FractalClock,
sliders::Sliders, tests::Tests, widgets::Widgets,
};
pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
@ -45,7 +48,7 @@ pub trait Demo {
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if crate::has_debug_assertions() {
ui.label(
crate::Label::new("[Debug build]")
crate::Label::new("‼ Debug build ‼")
.small()
.text_color(crate::color::RED),
)

View file

@ -5,7 +5,7 @@ pub struct Tests {}
impl demos::Demo for Tests {
fn name(&self) -> &str {
"Tests"
"📋 Tests"
}
fn show(&mut self, ctx: &std::sync::Arc<crate::Context>, open: &mut bool) {

View file

@ -410,7 +410,7 @@ impl InputState {
ui.style_mut().body_text_style = crate::paint::TextStyle::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("mouse")
crate::containers::CollapsingHeader::new("🖱 Mouse")
.default_open(true)
.show(ui, |ui| {
mouse.ui(ui);

View file

@ -16,9 +16,9 @@ use super::texture_atlas::TextureAtlas;
// ----------------------------------------------------------------------------
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
// const REPLACEMENT_CHAR: char = '\u{FFFD}'; // <20> REPLACEMENT CHARACTER
const REPLACEMENT_CHAR: char = '?';
// const REPLACEMENT_CHAR: char = '?';
const REPLACEMENT_CHAR: char = '◻'; // white medium square
#[derive(Clone, Copy, Debug)]
pub struct UvRect {
@ -44,57 +44,49 @@ pub struct GlyphInfo {
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 Font {
font: rusttype::Font<'static>,
pub struct FontImpl {
rusttype_font: Arc<rusttype::Font<'static>>,
/// Maximum character height
scale_in_pixels: f32,
pixels_per_point: f32,
replacement_glyph_info: GlyphInfo,
glyph_infos: RwLock<AHashMap<char, GlyphInfo>>,
glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>, // TODO: standard Mutex
atlas: Arc<Mutex<TextureAtlas>>,
}
impl Font {
impl FontImpl {
pub fn new(
atlas: Arc<Mutex<TextureAtlas>>,
font_data: &'static [u8],
scale_in_points: f32,
pixels_per_point: f32,
) -> Font {
rusttype_font: Arc<rusttype::Font<'static>>,
scale_in_points: f32,
) -> FontImpl {
assert!(scale_in_points > 0.0);
assert!(pixels_per_point > 0.0);
let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font");
let scale_in_pixels = pixels_per_point * scale_in_points;
let replacement_glyph_info = allocate_glyph(
&mut atlas.lock(),
REPLACEMENT_CHAR,
&font,
let font = Self {
rusttype_font,
scale_in_pixels,
pixels_per_point,
)
.unwrap_or_else(|| {
panic!(
"Failed to find replacement character {:?}",
REPLACEMENT_CHAR
)
});
let font = Font {
font,
scale_in_pixels,
pixels_per_point,
replacement_glyph_info,
glyph_infos: Default::default(),
glyph_info_cache: Default::default(),
atlas,
};
font.glyph_infos
.write()
.insert(REPLACEMENT_CHAR, font.replacement_glyph_info);
// Preload the printable ASCII characters [32, 126] (which excludes control codes):
const FIRST_ASCII: usize = 32; // 32 == space
const LAST_ASCII: usize = 126;
@ -106,8 +98,39 @@ impl Font {
font
}
pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point
/// `\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.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
@ -115,34 +138,121 @@ impl Font {
self.scale_in_pixels / self.pixels_per_point
}
pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point
}
}
type FontIndex = usize;
// TODO: rename Layouter ?
/// Wrapper over multiple `FontImpl` (commonly two: primary + emoji fallback)
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 {
assert!(!fonts.is_empty());
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(),
};
let replacement_glyph = slf
.glyph_info_no_cache(REPLACEMENT_CHAR)
.unwrap_or_else(|| {
panic!(
"Failed to find replacement character {:?}",
REPLACEMENT_CHAR
)
});
slf.replacement_glyph = replacement_glyph;
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_infos.read().get(&c).and_then(|gi| gi.uv_rect)
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).advance_width
self.glyph_info(c).1.advance_width
}
/// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`)
fn glyph_info(&self, c: char) -> GlyphInfo {
/// `\n` will (intentionally) show up as `REPLACEMENT_CHAR`
fn glyph_info(&self, c: char) -> (FontIndex, GlyphInfo) {
{
if let Some(glyph_info) = self.glyph_infos.read().get(&c) {
if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
return *glyph_info;
}
}
// Add new character:
let glyph_info = allocate_glyph(
&mut self.atlas.lock(),
c,
&self.font,
self.scale_in_pixels,
self.pixels_per_point,
);
// debug_assert!(glyph_info.is_some(), "Failed to find {:?}", c);
let glyph_info = glyph_info.unwrap_or(self.replacement_glyph_info);
self.glyph_infos.write().insert(c, glyph_info);
glyph_info
let font_index_glyph_info = self.glyph_info_no_cache(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(&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() {
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.
@ -240,37 +350,6 @@ impl Font {
galley
}
/// 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 scale_in_pixels = Scale::uniform(self.scale_in_pixels);
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() {
let glyph = self.glyph_info(c);
if let Some(last_glyph_id) = last_glyph_id {
cursor_x_in_points +=
self.font
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
/ self.pixels_per_point
}
cursor_x_in_points += glyph.advance_width;
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
last_glyph_id = Some(glyph.id);
x_offsets.push(cursor_x_in_points);
}
x_offsets
}
/// 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.
@ -366,15 +445,11 @@ impl Font {
fn allocate_glyph(
atlas: &mut TextureAtlas,
c: char,
font: &rusttype::Font<'static>,
glyph: rusttype::Glyph<'static>,
scale_in_pixels: f32,
pixels_per_point: f32,
) -> Option<GlyphInfo> {
let glyph = font.glyph(c);
if glyph.id().0 == 0 {
return None; // Failed to find a glyph for the character
}
) -> GlyphInfo {
assert!(glyph.id().0 != 0);
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
let glyph = glyph.positioned(point(0.0, 0.0));
@ -382,33 +457,36 @@ fn allocate_glyph(
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;
assert!(glyph_width >= 1);
assert!(glyph_height >= 1);
let glyph_pos = atlas.allocate((glyph_width, glyph_height));
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 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_y_in_pixels = scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * pixels_per_point; // TODO: use font.v_metrics
Some(UvRect {
offset: vec2(
bb.min.x as f32 / pixels_per_point,
offset_y_in_pixels / pixels_per_point,
),
size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point,
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
max: (
(glyph_pos.0 + glyph_width) as u16,
(glyph_pos.1 + glyph_height) as u16,
),
})
let offset_y_in_pixels =
scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * pixels_per_point; // TODO: use font.v_metrics
Some(UvRect {
offset: vec2(
bb.min.x as f32 / pixels_per_point,
offset_y_in_pixels / pixels_per_point,
),
size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point,
min: (glyph_pos.0 as u16, glyph_pos.1 as u16),
max: (
(glyph_pos.0 + glyph_width) as u16,
(glyph_pos.1 + glyph_height) as u16,
),
})
}
} else {
// No bounding box. Maybe a space?
None
@ -416,9 +494,9 @@ fn allocate_glyph(
let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
Some(GlyphInfo {
GlyphInfo {
id: glyph.id(),
advance_width: advance_width_in_points,
uv_rect,
})
}
}

View file

@ -7,7 +7,7 @@ use std::{
use crate::mutex::Mutex;
use super::{
font::Font,
font::{Font, FontImpl},
texture_atlas::{Texture, TextureAtlas},
};
@ -23,6 +23,20 @@ pub enum TextStyle {
Monospace,
}
impl TextStyle {
pub fn all() -> impl Iterator<Item = TextStyle> {
[
TextStyle::Small,
TextStyle::Body,
TextStyle::Button,
TextStyle::Heading,
TextStyle::Monospace,
]
.iter()
.copied()
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum FontFamily {
@ -41,6 +55,9 @@ pub struct FontDefinitions {
/// Egui has built-in-default for these,
/// but you can override them if you like.
pub ttf_data: BTreeMap<FontFamily, &'static [u8]>,
/// ttf data for emoji font(s), if any, in order of preference
pub emoji_ttf_data: Vec<&'static [u8]>,
}
impl Default for FontDefinitions {
@ -70,6 +87,10 @@ impl FontDefinitions {
pixels_per_point,
fonts,
ttf_data,
emoji_ttf_data: vec![
include_bytes!("../../fonts/NotoEmoji-Regular.ttf"), // few, but good looking. Use as first priority
include_bytes!("../../fonts/emoji-icon-font.ttf"), // bigger and more: http://jslegers.github.io/emoji-icon-font/
],
}
}
}
@ -101,7 +122,9 @@ impl Fonts {
return;
}
let mut atlas = TextureAtlas::new(512, 16); // TODO: better default?
// 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:
@ -112,21 +135,33 @@ impl Fonts {
let atlas = Arc::new(Mutex::new(atlas));
self.definitions = definitions.clone();
let FontDefinitions {
pixels_per_point,
fonts,
ttf_data,
} = definitions;
self.fonts = fonts
.into_iter()
.map(|(text_style, (family, size))| {
let typeface_data = ttf_data
.get(&family)
.unwrap_or_else(|| panic!("Missing TTF data for {:?}", family));
let font = Font::new(atlas.clone(), typeface_data, size, pixels_per_point);
self.definitions = definitions;
(text_style, font)
let mut font_impl_cache = FontImplCache::new(atlas.clone(), &self.definitions);
self.fonts = self
.definitions
.fonts
.iter()
.map(|(&text_style, &(family, size))| {
let mut fonts = vec![];
fonts.push(font_impl_cache.font_impl(FontSource::Family(family), size));
if family == FontFamily::Monospace {
// monospace should have ubuntu as fallback (for √ etc):
fonts.push(
font_impl_cache
.font_impl(FontSource::Family(FontFamily::VariableWidth), size),
);
}
for index in 0..self.definitions.emoji_ttf_data.len() {
let emoji_font_impl = font_impl_cache.font_impl(FontSource::Emoji(index), size);
fonts.push(emoji_font_impl);
}
(text_style, Font::new(fonts))
})
.collect();
@ -162,3 +197,79 @@ impl std::ops::Index<TextStyle> for Fonts {
&self.fonts[&text_style]
}
}
// ----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FontSource {
Family(FontFamily),
/// Emoji fonts are numbered from hight priority (0) and onwards
Emoji(usize),
}
pub struct FontImplCache {
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
font_families: std::collections::BTreeMap<FontFamily, Arc<rusttype::Font<'static>>>,
emoji_fonts: Vec<Arc<rusttype::Font<'static>>>,
/// can't have f32 in a HashMap or BTreeMap,
/// so let's do a linear search
cache: Vec<(FontSource, f32, Arc<FontImpl>)>,
}
impl FontImplCache {
pub fn new(atlas: Arc<Mutex<TextureAtlas>>, definitions: &super::FontDefinitions) -> Self {
let font_families = definitions
.ttf_data
.iter()
.map(|(family, ttf_data)| {
(
*family,
Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF")),
)
})
.collect();
let emoji_fonts = definitions
.emoji_ttf_data
.iter()
.map(|ttf_data| {
Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF"))
})
.collect();
Self {
atlas,
pixels_per_point: definitions.pixels_per_point,
font_families,
emoji_fonts,
cache: Default::default(),
}
}
pub fn rusttype_font(&self, source: FontSource) -> Arc<rusttype::Font<'static>> {
match source {
FontSource::Family(family) => self.font_families.get(&family).unwrap().clone(),
FontSource::Emoji(index) => self.emoji_fonts[index].clone(),
}
}
pub fn font_impl(&mut self, source: FontSource, scale_in_points: f32) -> Arc<FontImpl> {
for entry in &self.cache {
if (entry.0, entry.1) == (source, scale_in_points) {
return entry.2.clone();
}
}
let font_impl = Arc::new(FontImpl::new(
self.atlas.clone(),
self.pixels_per_point,
self.rusttype_font(source),
scale_in_points,
));
self.cache
.push((source, scale_in_points, font_impl.clone()));
font_impl
}
}

View file

@ -654,14 +654,11 @@ fn test_text_layout() {
}
}
use crate::mutex::Mutex;
use crate::paint::{font::Font, *};
use crate::paint::*;
let pixels_per_point = 1.0;
let typeface_data = include_bytes!("../../fonts/ProggyClean.ttf");
let atlas = TextureAtlas::new(512, 16);
let atlas = std::sync::Arc::new(Mutex::new(atlas));
let font = Font::new(atlas, typeface_data, 13.0, pixels_per_point);
let fonts = Fonts::from_definitions(FontDefinitions::with_pixels_per_point(pixels_per_point));
let font = &fonts[TextStyle::Monospace];
let galley = font.layout_multiline("".to_owned(), 1024.0);
assert_eq!(galley.rows.len(), 1);

View file

@ -139,11 +139,10 @@ impl Painter {
self.text(rect.min, LEFT_TOP, text.into(), text_style, color);
}
pub fn error(&self, pos: Pos2, text: impl Into<String>) {
let text = text.into();
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) {
let text_style = TextStyle::Monospace;
let font = &self.fonts()[text_style];
let galley = font.layout_multiline(text, f32::INFINITY);
let galley = font.layout_multiline(format!("🔥 {}", text), f32::INFINITY);
let rect = anchor_rect(Rect::from_min_size(pos, galley.size), LEFT_TOP);
self.add(PaintCmd::Rect {
rect: rect.expand(2.0),

View file

@ -355,9 +355,9 @@ impl Style {
ui.radio_value(body_text_style, value, format!("{:?}", value));
}
});
ui.collapsing("Spacing", |ui| spacing.ui(ui));
ui.collapsing("Interaction", |ui| interaction.ui(ui));
ui.collapsing("Visuals", |ui| visuals.ui(ui));
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.add(Slider::f32(animation_time, 0.0..=1.0).text("animation_time"));
}
}

View file

@ -288,6 +288,7 @@ pub struct Button {
fill: Option<Srgba>,
sense: Sense,
small: bool,
frame: bool,
}
impl Button {
@ -299,6 +300,7 @@ impl Button {
fill: Default::default(),
sense: Sense::click(),
small: false,
frame: true,
}
}
@ -329,6 +331,12 @@ impl Button {
self
}
/// Turn off the frame
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
pub fn sense(mut self, sense: Sense) -> Self {
@ -355,6 +363,7 @@ impl Widget for Button {
fill,
sense,
small,
frame,
} = self;
let mut button_padding = ui.style().spacing.button_padding;
@ -363,32 +372,47 @@ impl Widget for Button {
}
let font = &ui.fonts()[text_style];
let galley = font.layout_multiline(text, ui.available_width());
let single_line = ui.layout().is_horizontal();
let galley = if single_line {
font.layout_single_line(text)
} else {
font.layout_multiline(text, ui.available_width())
};
let mut desired_size = galley.size + 2.0 * button_padding;
if !small {
desired_size = desired_size.at_least(ui.style().spacing.interact_size);
desired_size.y = desired_size.y.at_least(ui.style().spacing.interact_size.y);
}
let rect = ui.allocate_space(desired_size);
let id = ui.make_position_id();
let response = ui.interact(rect, id, sense);
let visuals = ui.style().interact(&response);
let text_cursor = ui
.layout()
.align_size_within_rect(galley.size, response.rect.shrink2(button_padding))
.min;
let fill = fill.unwrap_or(visuals.bg_fill);
ui.painter().rect(
response.rect,
visuals.corner_radius,
fill,
visuals.bg_stroke,
);
let text_color = text_color
.or(ui.style().visuals.override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter()
.galley(text_cursor, galley, text_style, text_color);
if ui.clip_rect().intersects(rect) {
let visuals = ui.style().interact(&response);
let text_cursor = ui
.layout()
.align_size_within_rect(galley.size, response.rect.shrink2(button_padding))
.min;
if frame {
let fill = fill.unwrap_or(visuals.bg_fill);
ui.painter().rect(
response.rect,
visuals.corner_radius,
fill,
visuals.bg_stroke,
);
}
let text_color = text_color
.or(ui.style().visuals.override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter()
.galley(text_cursor, galley, text_style, text_color);
}
response
}
}
@ -436,8 +460,12 @@ impl<'a> Widget for Checkbox<'a> {
let button_padding = spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = font.layout_single_line(text);
// let galley = font.layout_multiline(text, ui.available_width() - total_extra.x);
let single_line = ui.layout().is_horizontal();
let galley = if single_line {
font.layout_single_line(text)
} else {
font.layout_multiline(text, ui.available_width() - total_extra.x)
};
let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(spacing.interact_size);
@ -526,7 +554,12 @@ impl Widget for RadioButton {
let button_padding = ui.style().spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = font.layout_multiline(text, ui.available_width() - total_extra.x);
let single_line = ui.layout().is_horizontal();
let galley = if single_line {
font.layout_single_line(text)
} else {
font.layout_multiline(text, ui.available_width() - total_extra.x)
};
let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(ui.style().spacing.interact_size);