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 ⭐ ### 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: * Wrapping layouts:
* `ui.horizontal_wrapped(|ui| ...)`: Add widgets on a row but wrap at `max_size`. * `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. * `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 🔧
* Changed default font to [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu). * Changed default font to [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu).
* Remove minimum button width
* Refactored `egui::Layout` substantially, changing its interface. * Refactored `egui::Layout` substantially, changing its interface.
### Removed 🔥 ### Removed 🔥

View file

@ -209,5 +209,7 @@ Egui is under MIT OR Apache-2.0 license.
Fonts: Fonts:
* ProggyClean.ttf, Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. <http://www.proggyfonts.net/> * `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License
* Ubuntu-Light.ttf by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence) * `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 * [/] Unicode
* [/] Text editing of unicode (needs more testing) * [/] Text editing of unicode (needs more testing)
* [ ] Font with some more unicode characters * [ ] 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) * Menu bar (File, Edit, etc)
* [ ] Sub-menus * [ ] Sub-menus
* [ ] Keyboard shortcuts * [ ] Keyboard shortcuts
@ -117,6 +115,8 @@ Ability to do a search for any widget. The search works even for collapsed regio
* Widgets * Widgets
* [x] Label * [x] Label
* [x] Emoji support (great for things like ▶️⏸⏹⚠︎)
* [x] Change text style/color and continue in same layout
* [x] Button * [x] Button
* [x] Checkbox * [x] Checkbox
* [x] Radiobutton * [x] Radiobutton

Binary file not shown.

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

@ -1,20 +1,19 @@
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 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: This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
----------------------------------------------------------- -----------------------------------------------------------
PREAMBLE PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation development of collaborative font projects, to support the font
efforts of academic and linguistic communities, and to provide a free and creation efforts of academic and linguistic communities, and to
open framework in which fonts may be shared and improved in partnership provide a free and open framework in which fonts may be shared and
with others. improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The redistributed freely as long as they are not sold by themselves. The
@ -22,8 +21,8 @@ fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives, names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply requirement for fonts to remain under this license does not apply to
to any document created using the fonts or their derivatives. any document created using the fonts or their derivatives.
DEFINITIONS DEFINITIONS
"Font Software" refers to the set of files released by the Copyright "Font Software" refers to the set of files released by the Copyright
@ -33,25 +32,25 @@ include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the "Reserved Font Name" refers to any names specified as such after the
copyright statement(s). copyright statement(s).
"Original Version" refers to the collection of Font Software components as "Original Version" refers to the collection of Font Software
distributed by the Copyright Holder(s). components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, "Modified Version" refers to any derivative made by adding to,
or substituting -- in part or in whole -- any of the components of the deleting, or substituting -- in part or in whole -- any of the
Original Version, by changing formats or by porting the Font Software to a components of the Original Version, by changing formats or by porting
new environment. the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical "Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software. writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify, a copy of the Font Software, to use, study, copy, merge, embed,
redistribute, and sell modified and unmodified copies of the Font modify, redistribute, and sell modified and unmodified copies of the
Software, subject to the following conditions: Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, 1) Neither the Font Software nor any of its individual components, in
in Original or Modified Versions, may be sold by itself. Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, 2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy redistributed and/or sold with any software, provided that each copy
@ -61,9 +60,9 @@ in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user. 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 3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding Name(s) unless explicit written permission is granted by the
Copyright Holder. This restriction only applies to the primary font name as corresponding Copyright Holder. This restriction only applies to the
presented to the users. primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 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 Software shall not be used to promote, endorse or advertise any
@ -74,8 +73,8 @@ permission.
5) The Font Software, modified or unmodified, in part or in whole, 5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created remain under this license does not apply to any document created using
using the Font Software. the Font Software.
TERMINATION TERMINATION
This license becomes null and void if any of the above conditions are This license becomes null and void if any of the above conditions are

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

View file

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

View file

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

View file

@ -37,6 +37,7 @@ impl Default for Demos {
fn default() -> Self { fn default() -> Self {
Self { Self {
demos: vec![ demos: vec![
(false, Box::new(crate::demos::FontBook::default())),
(false, Box::new(crate::demos::DancingStrings::default())), (false, Box::new(crate::demos::DancingStrings::default())),
(false, Box::new(crate::demos::DragAndDropDemo::default())), (false, Box::new(crate::demos::DragAndDropDemo::default())),
(false, Box::new(crate::demos::Tests::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| { 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); crate::demos::warn_if_debug_build(ui);
ui.label("Egui is an immediate mode GUI library written in Rust."); 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.separator();
ui.label( ui.label(
@ -117,7 +120,7 @@ impl DemoWindows {
} }
ui.separator(); ui.separator();
ui.heading("Windows:"); ui.heading(" Windows:");
ui.indent("windows", |ui| { ui.indent("windows", |ui| {
self.open_windows.checkboxes(ui); self.open_windows.checkboxes(ui);
self.demos.checkboxes(ui); self.demos.checkboxes(ui);
@ -147,34 +150,34 @@ impl DemoWindows {
.. ..
} = self; } = self;
Window::new("Demo") Window::new("Demo")
.open(&mut open_windows.demo) .open(&mut open_windows.demo)
.scroll(true) .scroll(true)
.show(ctx, |ui| { .show(ctx, |ui| {
demo_window.ui(ui); demo_window.ui(ui);
}); });
Window::new("Settings") Window::new("🔧 Settings")
.open(&mut open_windows.settings) .open(&mut open_windows.settings)
.show(ctx, |ui| { .show(ctx, |ui| {
ctx.settings_ui(ui); ctx.settings_ui(ui);
}); });
Window::new("Inspection") Window::new("🔍 Inspection")
.open(&mut open_windows.inspection) .open(&mut open_windows.inspection)
.scroll(true) .scroll(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ctx.inspection_ui(ui); ctx.inspection_ui(ui);
}); });
Window::new("Memory") Window::new("📝 Memory")
.open(&mut open_windows.memory) .open(&mut open_windows.memory)
.resizable(false) .resizable(false)
.show(ctx, |ui| { .show(ctx, |ui| {
ctx.memory_ui(ui); ctx.memory_ui(ui);
}); });
Window::new("Color Test") Window::new("🎨 Color Test")
.default_size([800.0, 1024.0]) .default_size([800.0, 1024.0])
.scroll(true) .scroll(true)
.open(&mut open_windows.color_test) .open(&mut open_windows.color_test)
@ -299,18 +302,18 @@ impl OpenWindows {
color_test, color_test,
} = self; } = self;
ui.label("Egui:"); ui.label("Egui:");
ui.checkbox(settings, "Settings"); ui.checkbox(settings, "🔧 Settings");
ui.checkbox(inspection, "Inspection"); ui.checkbox(inspection, "🔍 Inspection");
ui.checkbox(memory, "Memory"); ui.checkbox(memory, "📝 Memory");
ui.separator(); ui.separator();
ui.checkbox(demo, "Demo"); ui.checkbox(demo, "Demo");
ui.separator(); ui.separator();
ui.checkbox(resize, "Resize examples"); ui.checkbox(resize, "Resize examples");
ui.checkbox(color_test, "Color test") ui.checkbox(color_test, "🎨 Color test")
.on_hover_text("For testing the integrations painter"); .on_hover_text("For testing the integrations painter");
ui.separator(); ui.separator();
ui.label("Misc:"); 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, "Windows", |ui| windows.checkboxes(ui));
menu::menu(ui, "About", |ui| { menu::menu(ui, "About", |ui| {
ui.label("This is Egui"); 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 { if let Some(time) = seconds_since_midnight {

View file

@ -97,7 +97,7 @@ impl Default for DragAndDropDemo {
impl Demo for DragAndDropDemo { impl Demo for DragAndDropDemo {
fn name(&self) -> &str { fn name(&self) -> &str {
"Drag and Drop" "Drag and Drop"
} }
fn show(&mut self, ctx: &std::sync::Arc<Context>, open: &mut bool) { 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, open: &mut bool,
seconds_since_midnight: Option<f64>, seconds_since_midnight: Option<f64>,
) { ) {
Window::new("FractalClock") Window::new("🕑 Fractal Clock")
.open(open) .open(open)
.default_size(vec2(512.0, 512.0)) .default_size(vec2(512.0, 512.0))
.scroll(false) .scroll(false)

View file

@ -7,6 +7,9 @@ mod dancing_strings;
pub mod demo_window; pub mod demo_window;
mod demo_windows; mod demo_windows;
mod drag_and_drop; mod drag_and_drop;
mod font_book;
pub mod font_contents_emoji;
pub mod font_contents_ubuntu;
mod fractal_clock; mod fractal_clock;
mod sliders; mod sliders;
mod tests; mod tests;
@ -15,8 +18,8 @@ mod widgets;
pub use { pub use {
app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow, app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow,
demo_windows::*, drag_and_drop::*, fractal_clock::FractalClock, sliders::Sliders, tests::Tests, demo_windows::*, drag_and_drop::*, font_book::FontBook, fractal_clock::FractalClock,
widgets::Widgets, 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."; 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) { pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if crate::has_debug_assertions() { if crate::has_debug_assertions() {
ui.label( ui.label(
crate::Label::new("[Debug build]") crate::Label::new("‼ Debug build ‼")
.small() .small()
.text_color(crate::color::RED), .text_color(crate::color::RED),
) )

View file

@ -5,7 +5,7 @@ pub struct Tests {}
impl demos::Demo for Tests { impl demos::Demo for Tests {
fn name(&self) -> &str { fn name(&self) -> &str {
"Tests" "📋 Tests"
} }
fn show(&mut self, ctx: &std::sync::Arc<crate::Context>, open: &mut bool) { 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.style_mut().body_text_style = crate::paint::TextStyle::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui)); ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("mouse") crate::containers::CollapsingHeader::new("🖱 Mouse")
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
mouse.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 = '\u{FFFD}'; // <20> REPLACEMENT CHARACTER
const REPLACEMENT_CHAR: char = '?'; // const REPLACEMENT_CHAR: char = '?';
const REPLACEMENT_CHAR: char = '◻'; // white medium square
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct UvRect { pub struct UvRect {
@ -44,57 +44,49 @@ pub struct GlyphInfo {
pub uv_rect: Option<UvRect>, 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. /// The interface uses points as the unit for everything.
pub struct Font { pub struct FontImpl {
font: rusttype::Font<'static>, rusttype_font: Arc<rusttype::Font<'static>>,
/// Maximum character height /// Maximum character height
scale_in_pixels: f32, scale_in_pixels: f32,
pixels_per_point: f32, pixels_per_point: f32,
replacement_glyph_info: GlyphInfo, glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>, // TODO: standard Mutex
glyph_infos: RwLock<AHashMap<char, GlyphInfo>>,
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
} }
impl Font { impl FontImpl {
pub fn new( pub fn new(
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
font_data: &'static [u8],
scale_in_points: f32,
pixels_per_point: f32, pixels_per_point: f32,
) -> Font { rusttype_font: Arc<rusttype::Font<'static>>,
scale_in_points: f32,
) -> FontImpl {
assert!(scale_in_points > 0.0); assert!(scale_in_points > 0.0);
assert!(pixels_per_point > 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 scale_in_pixels = pixels_per_point * scale_in_points;
let replacement_glyph_info = allocate_glyph( let font = Self {
&mut atlas.lock(), rusttype_font,
REPLACEMENT_CHAR,
&font,
scale_in_pixels, scale_in_pixels,
pixels_per_point, pixels_per_point,
) glyph_info_cache: Default::default(),
.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(),
atlas, atlas,
}; };
font.glyph_infos
.write()
.insert(REPLACEMENT_CHAR, font.replacement_glyph_info);
// Preload the printable ASCII characters [32, 126] (which excludes control codes): // Preload the printable ASCII characters [32, 126] (which excludes control codes):
const FIRST_ASCII: usize = 32; // 32 == space const FIRST_ASCII: usize = 32; // 32 == space
const LAST_ASCII: usize = 126; const LAST_ASCII: usize = 126;
@ -106,8 +98,39 @@ impl Font {
font font
} }
pub fn round_to_pixel(&self, point: f32) -> f32 { /// `\n` will result in `None`
(point * self.pixels_per_point).round() / self.pixels_per_point 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 /// Height of one row of text. In points
@ -115,34 +138,121 @@ impl Font {
self.scale_in_pixels / self.pixels_per_point 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> { 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 { 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`) /// `\n` will (intentionally) show up as `REPLACEMENT_CHAR`
fn glyph_info(&self, c: char) -> GlyphInfo { 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; return *glyph_info;
} }
} }
// Add new character: let font_index_glyph_info = self.glyph_info_no_cache(c);
let glyph_info = allocate_glyph( let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph);
&mut self.atlas.lock(), self.glyph_info_cache
c, .write()
&self.font, .insert(c, font_index_glyph_info);
self.scale_in_pixels, font_index_glyph_info
self.pixels_per_point, }
);
// debug_assert!(glyph_info.is_some(), "Failed to find {:?}", c); fn glyph_info_no_cache(&self, c: char) -> Option<(FontIndex, GlyphInfo)> {
let glyph_info = glyph_info.unwrap_or(self.replacement_glyph_info); for (font_index, font_impl) in self.fonts.iter().enumerate() {
self.glyph_infos.write().insert(c, glyph_info); if let Some(glyph_info) = font_impl.glyph_info(c) {
glyph_info 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. /// Typeset the given text onto one row.
@ -240,37 +350,6 @@ impl Font {
galley 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. /// A paragraph is text with no line break character in it.
/// The text will be wrapped by the given `max_width_in_points`. /// The text will be wrapped by the given `max_width_in_points`.
/// Always returns at least one row. /// Always returns at least one row.
@ -366,15 +445,11 @@ impl Font {
fn allocate_glyph( fn allocate_glyph(
atlas: &mut TextureAtlas, atlas: &mut TextureAtlas,
c: char, glyph: rusttype::Glyph<'static>,
font: &rusttype::Font<'static>,
scale_in_pixels: f32, scale_in_pixels: f32,
pixels_per_point: f32, pixels_per_point: f32,
) -> Option<GlyphInfo> { ) -> GlyphInfo {
let glyph = font.glyph(c); assert!(glyph.id().0 != 0);
if glyph.id().0 == 0 {
return None; // Failed to find a glyph for the character
}
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels)); let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
let glyph = glyph.positioned(point(0.0, 0.0)); let glyph = glyph.positioned(point(0.0, 0.0));
@ -382,9 +457,10 @@ fn allocate_glyph(
let uv_rect = if let Some(bb) = glyph.pixel_bounding_box() { let uv_rect = if let Some(bb) = glyph.pixel_bounding_box() {
let glyph_width = bb.width() as usize; let glyph_width = bb.width() as usize;
let glyph_height = bb.height() as usize; let glyph_height = bb.height() as usize;
assert!(glyph_width >= 1);
assert!(glyph_height >= 1);
if glyph_width == 0 || glyph_height == 0 {
None
} else {
let glyph_pos = atlas.allocate((glyph_width, glyph_height)); let glyph_pos = atlas.allocate((glyph_width, glyph_height));
let texture = atlas.texture_mut(); let texture = atlas.texture_mut();
@ -396,7 +472,8 @@ fn allocate_glyph(
} }
}); });
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 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 { Some(UvRect {
offset: vec2( offset: vec2(
bb.min.x as f32 / pixels_per_point, bb.min.x as f32 / pixels_per_point,
@ -409,6 +486,7 @@ fn allocate_glyph(
(glyph_pos.1 + glyph_height) as u16, (glyph_pos.1 + glyph_height) as u16,
), ),
}) })
}
} else { } else {
// No bounding box. Maybe a space? // No bounding box. Maybe a space?
None None
@ -416,9 +494,9 @@ fn allocate_glyph(
let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point; let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
Some(GlyphInfo { GlyphInfo {
id: glyph.id(), id: glyph.id(),
advance_width: advance_width_in_points, advance_width: advance_width_in_points,
uv_rect, uv_rect,
}) }
} }

View file

@ -7,7 +7,7 @@ use std::{
use crate::mutex::Mutex; use crate::mutex::Mutex;
use super::{ use super::{
font::Font, font::{Font, FontImpl},
texture_atlas::{Texture, TextureAtlas}, texture_atlas::{Texture, TextureAtlas},
}; };
@ -23,6 +23,20 @@ pub enum TextStyle {
Monospace, 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)] #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum FontFamily { pub enum FontFamily {
@ -41,6 +55,9 @@ pub struct FontDefinitions {
/// Egui has built-in-default for these, /// Egui has built-in-default for these,
/// but you can override them if you like. /// but you can override them if you like.
pub ttf_data: BTreeMap<FontFamily, &'static [u8]>, 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 { impl Default for FontDefinitions {
@ -70,6 +87,10 @@ impl FontDefinitions {
pixels_per_point, pixels_per_point,
fonts, fonts,
ttf_data, 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; 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: // Make the top left pixel fully white:
@ -112,21 +135,33 @@ impl Fonts {
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));
self.definitions = definitions.clone(); self.definitions = definitions;
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);
(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(); .collect();
@ -162,3 +197,79 @@ impl std::ops::Index<TextStyle> for Fonts {
&self.fonts[&text_style] &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::*;
use crate::paint::{font::Font, *};
let pixels_per_point = 1.0; let pixels_per_point = 1.0;
let typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); let fonts = Fonts::from_definitions(FontDefinitions::with_pixels_per_point(pixels_per_point));
let atlas = TextureAtlas::new(512, 16); let font = &fonts[TextStyle::Monospace];
let atlas = std::sync::Arc::new(Mutex::new(atlas));
let font = Font::new(atlas, typeface_data, 13.0, pixels_per_point);
let galley = font.layout_multiline("".to_owned(), 1024.0); let galley = font.layout_multiline("".to_owned(), 1024.0);
assert_eq!(galley.rows.len(), 1); 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); self.text(rect.min, LEFT_TOP, text.into(), text_style, color);
} }
pub fn error(&self, pos: Pos2, text: impl Into<String>) { pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) {
let text = text.into();
let text_style = TextStyle::Monospace; let text_style = TextStyle::Monospace;
let font = &self.fonts()[text_style]; 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); let rect = anchor_rect(Rect::from_min_size(pos, galley.size), LEFT_TOP);
self.add(PaintCmd::Rect { self.add(PaintCmd::Rect {
rect: rect.expand(2.0), rect: rect.expand(2.0),

View file

@ -355,9 +355,9 @@ impl Style {
ui.radio_value(body_text_style, value, format!("{:?}", value)); ui.radio_value(body_text_style, value, format!("{:?}", value));
} }
}); });
ui.collapsing("Spacing", |ui| spacing.ui(ui)); ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("Interaction", |ui| interaction.ui(ui)); ui.collapsing("Interaction", |ui| interaction.ui(ui));
ui.collapsing("Visuals", |ui| visuals.ui(ui)); ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.add(Slider::f32(animation_time, 0.0..=1.0).text("animation_time")); 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>, fill: Option<Srgba>,
sense: Sense, sense: Sense,
small: bool, small: bool,
frame: bool,
} }
impl Button { impl Button {
@ -299,6 +300,7 @@ impl Button {
fill: Default::default(), fill: Default::default(),
sense: Sense::click(), sense: Sense::click(),
small: false, small: false,
frame: true,
} }
} }
@ -329,6 +331,12 @@ impl Button {
self self
} }
/// Turn off the frame
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
/// By default, buttons senses clicks. /// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`. /// Change this to a drag-button with `Sense::drag()`.
pub fn sense(mut self, sense: Sense) -> Self { pub fn sense(mut self, sense: Sense) -> Self {
@ -355,6 +363,7 @@ impl Widget for Button {
fill, fill,
sense, sense,
small, small,
frame,
} = self; } = self;
let mut button_padding = ui.style().spacing.button_padding; let mut button_padding = ui.style().spacing.button_padding;
@ -363,20 +372,31 @@ impl Widget for Button {
} }
let font = &ui.fonts()[text_style]; 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; let mut desired_size = galley.size + 2.0 * button_padding;
if !small { 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 rect = ui.allocate_space(desired_size);
let id = ui.make_position_id(); let id = ui.make_position_id();
let response = ui.interact(rect, id, sense); let response = ui.interact(rect, id, sense);
if ui.clip_rect().intersects(rect) {
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
let text_cursor = ui let text_cursor = ui
.layout() .layout()
.align_size_within_rect(galley.size, response.rect.shrink2(button_padding)) .align_size_within_rect(galley.size, response.rect.shrink2(button_padding))
.min; .min;
if frame {
let fill = fill.unwrap_or(visuals.bg_fill); let fill = fill.unwrap_or(visuals.bg_fill);
ui.painter().rect( ui.painter().rect(
response.rect, response.rect,
@ -384,11 +404,15 @@ impl Widget for Button {
fill, fill,
visuals.bg_stroke, visuals.bg_stroke,
); );
}
let text_color = text_color let text_color = text_color
.or(ui.style().visuals.override_text_color) .or(ui.style().visuals.override_text_color)
.unwrap_or_else(|| visuals.text_color()); .unwrap_or_else(|| visuals.text_color());
ui.painter() ui.painter()
.galley(text_cursor, galley, text_style, text_color); .galley(text_cursor, galley, text_style, text_color);
}
response response
} }
} }
@ -436,8 +460,12 @@ impl<'a> Widget for Checkbox<'a> {
let button_padding = spacing.button_padding; let button_padding = spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = font.layout_single_line(text); let single_line = ui.layout().is_horizontal();
// let galley = font.layout_multiline(text, ui.available_width() - total_extra.x); 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; let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(spacing.interact_size); desired_size = desired_size.at_least(spacing.interact_size);
@ -526,7 +554,12 @@ impl Widget for RadioButton {
let button_padding = ui.style().spacing.button_padding; let button_padding = ui.style().spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let galley = 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; let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(ui.style().spacing.interact_size); desired_size = desired_size.at_least(ui.style().spacing.interact_size);