1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

Choose your own font and size (#1154)

* Refactor text layout: don't need &Fonts in all functions
* Replace indexing in Fonts with member function
* Wrap Fonts in a Mutex
* Remove mutex for Font::glyph_info_cache
* Remove RwLock around Font::characters
* Put FontsImpl and GalleyCache behind the same Mutex
* Round font sizes to whole pixels before deduplicating them
* Make TextStyle !Copy
* Implement user-named TextStyle:s
* round font size earlier
* Cache fonts based on family and size
* Move TextStyle into egui and Style
* Remove body_text_style
* Query graphics about max texture size and use that as font atlas size
* Recreate texture atlas when it is getting full
This commit is contained in:
Emil Ernerfeldt
2022-01-24 14:32:36 +01:00
committed by GitHub
parent bb407e9b00
commit fa43d16c41
67 changed files with 1231 additions and 640 deletions

View File

@@ -1,7 +1,7 @@
use std::hash::Hash;
use crate::*;
use epaint::{Shape, TextStyle};
use epaint::Shape;
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]

View File

@@ -374,7 +374,7 @@ impl ScrollArea {
/// ```
/// # egui::__run_test_ui(|ui| {
/// let text_style = egui::TextStyle::Body;
/// let row_height = ui.fonts()[text_style].row_height();
/// let row_height = ui.text_style_height(&text_style);
/// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
/// let total_rows = 10_000;
/// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| {

View File

@@ -296,7 +296,8 @@ impl<'open> Window<'open> {
.and_then(|window_interaction| {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
title.font_height(ctx) + title_content_spacing
let style = ctx.style();
title.font_height(&ctx.fonts(), &style) + title_content_spacing
} else {
0.0
};
@@ -764,7 +765,7 @@ fn show_title_bar(
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = title
.font_height(ui.ctx())
.font_height(&ui.fonts(), ui.style())
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);

View File

@@ -62,7 +62,7 @@ impl ContextImpl {
self.input = input.begin_frame(new_raw_input);
self.frame_state.begin_frame(&self.input);
self.update_fonts_mut(self.input.pixels_per_point());
self.update_fonts_mut();
// Ensure we register the background area so panels and background ui can catch clicks:
let screen_rect = self.input.screen_rect();
@@ -77,27 +77,21 @@ impl ContextImpl {
}
/// Load fonts unless already loaded.
fn update_fonts_mut(&mut self, pixels_per_point: f32) {
let new_font_definitions = self.memory.new_font_definitions.take();
fn update_fonts_mut(&mut self) {
let pixels_per_point = self.input.pixels_per_point();
let max_texture_side = self.input.raw.max_texture_side;
let pixels_per_point_changed = match &self.fonts {
None => true,
Some(current_fonts) => {
(current_fonts.pixels_per_point() - pixels_per_point).abs() > 1e-3
}
};
if self.fonts.is_none() || new_font_definitions.is_some() || pixels_per_point_changed {
self.fonts = Some(Fonts::new(
pixels_per_point,
new_font_definitions.unwrap_or_else(|| {
self.fonts
.as_ref()
.map(|font| font.definitions().clone())
.unwrap_or_default()
}),
));
if let Some(font_definitions) = self.memory.new_font_definitions.take() {
let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions);
self.fonts = Some(fonts);
}
let fonts = self.fonts.get_or_insert_with(|| {
let font_definitions = FontDefinitions::default();
Fonts::new(pixels_per_point, max_texture_side, font_definitions)
});
fonts.begin_frame(pixels_per_point, max_texture_side);
}
}
@@ -521,7 +515,7 @@ impl Context {
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
if let Some(current_fonts) = &*self.fonts_mut() {
// NOTE: this comparison is expensive since it checks TTF data for equality
if current_fonts.definitions() == &font_definitions {
if current_fonts.lock().fonts.definitions() == &font_definitions {
return; // no change - save us from reloading font textures
}
}
@@ -700,8 +694,6 @@ impl Context {
self.request_repaint();
}
self.fonts().end_frame();
{
let ctx_impl = &mut *self.write();
ctx_impl
@@ -953,16 +945,6 @@ impl Context {
self.style_ui(ui);
});
CollapsingHeader::new("🔠 Fonts")
.default_open(false)
.show(ui, |ui| {
let mut font_definitions = self.fonts().definitions().clone();
font_definitions.ui(ui);
let font_image_size = self.fonts().font_image_size();
crate::introspection::font_texture_ui(ui, font_image_size);
self.set_fonts(font_definitions);
});
CollapsingHeader::new("✒ Painting")
.default_open(true)
.show(ui, |ui| {
@@ -1039,6 +1021,13 @@ impl Context {
.show(ui, |ui| {
self.texture_ui(ui);
});
CollapsingHeader::new("🔠 Font texture")
.default_open(false)
.show(ui, |ui| {
let font_image_size = self.fonts().font_image_size();
crate::introspection::font_texture_ui(ui, font_image_size);
});
}
/// Show stats about the allocated textures.
@@ -1080,8 +1069,12 @@ impl Context {
size *= (max_preview_size.x / size.x).min(1.0);
size *= (max_preview_size.y / size.y).min(1.0);
ui.image(texture_id, size).on_hover_ui(|ui| {
// show full size on hover
ui.image(texture_id, Vec2::new(w as f32, h as f32));
// show larger on hover
let max_size = 0.5 * ui.ctx().input().screen_rect().size();
let mut size = Vec2::new(w as f32, h as f32);
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.image(texture_id, size);
});
ui.label(format!("{} x {}", w, h));

View File

@@ -28,6 +28,13 @@ pub struct RawInput {
/// Set this the first frame, whenever it changes, or just on every frame.
pub pixels_per_point: Option<f32>,
/// Maximum size of one side of the font texture.
///
/// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`.
///
/// The default is a very small (but very portable) 2048.
pub max_texture_side: usize,
/// Monotonically increasing time, in seconds. Relative to whatever. Used for animations.
/// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds).
pub time: Option<f64>,
@@ -62,6 +69,7 @@ impl Default for RawInput {
Self {
screen_rect: None,
pixels_per_point: None,
max_texture_side: 2048,
time: None,
predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(),
@@ -81,6 +89,7 @@ impl RawInput {
RawInput {
screen_rect: self.screen_rect.take(),
pixels_per_point: self.pixels_per_point.take(),
max_texture_side: self.max_texture_side,
time: self.time.take(),
predicted_dt: self.predicted_dt,
modifiers: self.modifiers,
@@ -95,6 +104,7 @@ impl RawInput {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
@@ -105,6 +115,7 @@ impl RawInput {
self.screen_rect = screen_rect.or(self.screen_rect);
self.pixels_per_point = pixels_per_point.or(self.pixels_per_point);
self.max_texture_side = max_texture_side; // use latest
self.time = time; // use latest time
self.predicted_dt = predicted_dt; // use latest dt
self.modifiers = modifiers; // use latest
@@ -357,6 +368,7 @@ impl RawInput {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
@@ -370,6 +382,7 @@ impl RawInput {
.on_hover_text(
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
);
ui.label(format!("max_texture_side: {}", max_texture_side));
if let Some(time) = time {
ui.label(format!("time: {:.3} s", time));
} else {

View File

@@ -700,7 +700,12 @@ impl InputState {
events,
} = self;
ui.style_mut().body_text_style = epaint::TextStyle::Monospace;
ui.style_mut()
.text_styles
.get_mut(&crate::TextStyle::Body)
.unwrap()
.family = crate::FontFamily::Monospace;
ui.collapsing("Raw Input", |ui| raw.ui(ui));
crate::containers::CollapsingHeader::new("🖱 Pointer")

View File

@@ -1,6 +1,27 @@
//! uis for egui types.
//! Showing UI:s for egui/epaint types.
use crate::*;
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
let families = ui.fonts().families();
ui.horizontal(|ui| {
for alternative in families {
let text = alternative.to_string();
ui.radio_value(font_family, alternative, text);
}
});
}
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
let families = ui.fonts().families();
ui.horizontal(|ui| {
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(0));
for alternative in families {
let text = alternative.to_string();
ui.radio_value(&mut font_id.family, alternative, text);
}
});
}
// Show font texture in demo Ui
pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response {
use epaint::Mesh;
@@ -55,33 +76,16 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
.response
}
impl Widget for &mut epaint::text::FontDefinitions {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
for (text_style, (_family, size)) in self.family_and_size.iter_mut() {
// TODO: radio button for family
ui.add(
Slider::new(size, 4.0..=40.0)
.max_decimals(0)
.text(format!("{:?}", text_style)),
);
}
crate::reset_button(ui, self);
})
.response
}
}
impl Widget for &epaint::stats::PaintStats {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
ui.label(
"egui generates intermediate level shapes like circles and text. \
These are later tessellated into triangles.",
These are later tessellated into triangles.",
);
ui.add_space(10.0);
ui.style_mut().body_text_style = TextStyle::Monospace;
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
let epaint::stats::PaintStats {
shapes,
@@ -124,7 +128,7 @@ impl Widget for &epaint::stats::PaintStats {
}
}
pub fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response {
fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response {
ui.add(Label::new(alloc_info.format(what)).wrap(false))
}

View File

@@ -77,7 +77,6 @@ impl Region {
/// Layout direction, one of `LeftToRight`, `RightToLeft`, `TopDown`, `BottomUp`.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Direction {
LeftToRight,
RightToLeft,

View File

@@ -364,7 +364,7 @@ mod frame_state;
pub(crate) mod grid;
mod id;
mod input_state;
mod introspection;
pub mod introspection;
pub mod layers;
mod layout;
mod memory;
@@ -385,7 +385,7 @@ pub use epaint::emath;
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
pub use epaint::{
color, mutex,
text::{FontData, FontDefinitions, FontFamily, TextStyle},
text::{FontData, FontDefinitions, FontFamily, FontId},
textures::TexturesDelta,
AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Rgba, Shape, Stroke, TextureHandle,
TextureId,
@@ -394,7 +394,7 @@ pub use epaint::{
pub mod text {
pub use epaint::text::{
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat,
TextStyle, TAB_SIZE,
TAB_SIZE,
};
}
@@ -414,7 +414,7 @@ pub use {
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,
style::{Style, Visuals},
style::{FontSelection, Style, TextStyle, Visuals},
text::{Galley, TextFormat},
ui::Ui,
widget_text::{RichText, WidgetText},
@@ -511,7 +511,7 @@ macro_rules! egui_assert {
// ----------------------------------------------------------------------------
/// egui supports around 1216 emojis in total.
/// The default egui fonts supports around 1216 emojis in total.
/// Here are some of the most useful:
/// ∞⊗⎗⎘⎙⏏⏴⏵⏶⏷
/// ⏩⏪⏭⏮⏸⏹⏺■▶📾🔀🔁🔃

View File

@@ -414,7 +414,8 @@ impl SubMenuButton {
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = text.into_galley(ui, Some(true), text_available_width, text_style);
let text_galley =
text.into_galley(ui, Some(true), text_available_width, text_style.clone());
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style);

View File

@@ -1,11 +1,11 @@
use crate::{
emath::{Align2, Pos2, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context,
Color32, Context, FontId,
};
use epaint::{
mutex::{Arc, RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley, TextStyle},
text::{Fonts, Galley},
CircleShape, RectShape, Shape, Stroke, TextShape,
};
@@ -30,6 +30,7 @@ pub struct Painter {
}
impl Painter {
/// Create a painter to a specific layer within a certain clip rectangle.
pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
Self {
ctx,
@@ -39,6 +40,7 @@ impl Painter {
}
}
/// Redirect where you are painting.
#[must_use]
pub fn with_layer_id(self, layer_id: LayerId) -> Self {
Self {
@@ -49,7 +51,7 @@ impl Painter {
}
}
/// redirect
/// Redirect where you are painting.
pub fn set_layer_id(&mut self, layer_id: LayerId) {
self.layer_id = layer_id;
}
@@ -194,12 +196,11 @@ impl Painter {
#[allow(clippy::needless_pass_by_value)]
pub fn debug_rect(&mut self, rect: Rect, color: Color32, text: impl ToString) {
self.rect_stroke(rect, 0.0, (1.0, color));
let text_style = TextStyle::Monospace;
self.text(
rect.min,
Align2::LEFT_TOP,
text.to_string(),
text_style,
FontId::monospace(14.0),
color,
);
}
@@ -217,7 +218,7 @@ impl Painter {
color: Color32,
text: impl ToString,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color);
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(14.0), color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
let frame_rect = rect.expand(2.0);
self.add(Shape::rect_filled(
@@ -324,7 +325,7 @@ impl Painter {
impl Painter {
/// Lay out and paint some text.
///
/// To center the text at the given position, use `anchor: (Center, Center)`.
/// To center the text at the given position, use `Align2::CENTER_CENTER`.
///
/// To find out the size of text before painting it, use
/// [`Self::layout`] or [`Self::layout_no_wrap`].
@@ -336,10 +337,10 @@ impl Painter {
pos: Pos2,
anchor: Align2,
text: impl ToString,
text_style: TextStyle,
font_id: FontId,
text_color: Color32,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), text_style, text_color);
let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
self.galley(rect.min, galley);
rect
@@ -352,11 +353,11 @@ impl Painter {
pub fn layout(
&self,
text: String,
text_style: TextStyle,
font_id: FontId,
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts().layout(text, text_style, color, wrap_width)
self.fonts().layout(text, font_id, color, wrap_width)
}
/// Will line break at `\n`.
@@ -366,10 +367,10 @@ impl Painter {
pub fn layout_no_wrap(
&self,
text: String,
text_style: TextStyle,
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts().layout(text, text_style, color, f32::INFINITY)
self.fonts().layout(text, font_id, color, f32::INFINITY)
}
/// Paint text that has already been layed out in a [`Galley`].

View File

@@ -2,8 +2,123 @@
#![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, Response, RichText, WidgetText};
use epaint::{Shadow, Stroke, TextStyle};
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
use epaint::{mutex::Arc, Shadow, Stroke};
use std::collections::BTreeMap;
// ----------------------------------------------------------------------------
/// Alias for a [`FontId`] (font of a certain size).
///
/// The font is found via look-up in [`Style::text_styles`].
/// You can use [`TextStyle::resolve`] to do this lookup.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextStyle {
/// Used when small text is needed.
Small,
/// Normal labels. Easily readable, doesn't take up too much space.
Body,
/// Same size as [`Self::Body]`, but used when monospace is important (for aligning number, code snippets, etc).
Monospace,
/// Buttons. Maybe slightly bigger than [`Self::Body]`.
/// Signifies that he item is interactive.
Button,
/// Heading. Probably larger than [`Self::Body]`.
Heading,
/// A user-chosen style, found in [`Style::text_styles`].
/// ```
/// egui::TextStyle::Name("footing".into());
/// ````
Name(Arc<str>),
}
impl std::fmt::Display for TextStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Small => "Small".fmt(f),
Self::Body => "Body".fmt(f),
Self::Monospace => "Monospace".fmt(f),
Self::Button => "Button".fmt(f),
Self::Heading => "Heading".fmt(f),
Self::Name(name) => (*name).fmt(f),
}
}
}
impl TextStyle {
/// Look up this [`TextStyle`] in [`Style::text_styles`].
pub fn resolve(&self, style: &Style) -> FontId {
style.text_styles.get(self).cloned().unwrap_or_else(|| {
panic!(
"Failed to find {:?} in Style::text_styles. Available styles:\n{:#?}",
self,
style.text_styles()
)
})
}
}
// ----------------------------------------------------------------------------
/// A way to select [`FontId`], either by picking one directly or by using a [`TextStyle`].
pub enum FontSelection {
/// Default text style - will use [`TextStyle::Body`], unless
/// [`Style::override_font_id`] or [`Style::override_text_style`] is set.
Default,
/// Directly select size and font family
FontId(FontId),
/// Use a [`TextStyle`] to look up the [`FontId`] in [`Style::text_styles`].
Style(TextStyle),
}
impl Default for FontSelection {
#[inline]
fn default() -> Self {
Self::Default
}
}
impl FontSelection {
pub fn resolve(self, style: &Style) -> FontId {
match self {
Self::Default => {
if let Some(override_font_id) = &style.override_font_id {
override_font_id.clone()
} else if let Some(text_style) = &style.override_text_style {
text_style.resolve(style)
} else {
TextStyle::Body.resolve(style)
}
}
Self::FontId(font_id) => font_id,
Self::Style(text_style) => text_style.resolve(style),
}
}
}
impl From<FontId> for FontSelection {
#[inline(always)]
fn from(font_id: FontId) -> Self {
Self::FontId(font_id)
}
}
impl From<TextStyle> for FontSelection {
#[inline(always)]
fn from(text_style: TextStyle) -> Self {
Self::Style(text_style)
}
}
// ----------------------------------------------------------------------------
/// Specifies the look and feel of egui.
///
@@ -15,15 +130,23 @@ use epaint::{Shadow, Stroke, TextStyle};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Style {
/// Default `TextStyle` for normal text (i.e. for `Label` and `TextEdit`).
pub body_text_style: TextStyle,
/// If set this will change the default [`TextStyle`] for all widgets.
///
/// On most widgets you can also set an explicit text style,
/// which will take precedence over this.
pub override_text_style: Option<TextStyle>,
/// If set this will change the font family and size for all widgets.
///
/// On most widgets you can also set an explicit text style,
/// which will take precedence over this.
pub override_font_id: Option<FontId>,
/// The [`FontFamily`] and size you want to use for a specific [`TextStyle`].
///
/// The most convenient way to look something up in this is to use [`TextStyle::resolve`].
pub text_styles: BTreeMap<TextStyle, FontId>,
/// If set, labels buttons wtc will use this to determine whether or not
/// to wrap the text at the right edge of the `Ui` they are in.
/// By default this is `None`.
@@ -77,6 +200,11 @@ impl Style {
pub fn noninteractive(&self) -> &WidgetVisuals {
&self.visuals.widgets.noninteractive
}
/// All known text styles.
pub fn text_styles(&self) -> Vec<TextStyle> {
self.text_styles.keys().cloned().collect()
}
}
/// Controls the sizes and distances between widgets.
@@ -356,11 +484,35 @@ pub struct DebugOptions {
// ----------------------------------------------------------------------------
/// The default text styles of the default egui theme.
pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
let mut text_styles = BTreeMap::new();
text_styles.insert(
TextStyle::Small,
FontId::new(10.0, FontFamily::Proportional),
);
text_styles.insert(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional));
text_styles.insert(
TextStyle::Button,
FontId::new(14.0, FontFamily::Proportional),
);
text_styles.insert(
TextStyle::Heading,
FontId::new(20.0, FontFamily::Proportional),
);
text_styles.insert(
TextStyle::Monospace,
FontId::new(14.0, FontFamily::Monospace),
);
text_styles
}
impl Default for Style {
fn default() -> Self {
Self {
body_text_style: TextStyle::Body,
override_font_id: None,
override_text_style: None,
text_styles: default_text_styles(),
wrap: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
@@ -565,8 +717,9 @@ use crate::{widgets::*, Ui};
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
body_text_style,
override_font_id,
override_text_style,
text_styles,
wrap: _,
spacing,
interaction,
@@ -579,11 +732,14 @@ impl Style {
visuals.light_dark_radio_buttons(ui);
crate::Grid::new("_options").show(ui, |ui| {
ui.label("Default body text style:");
ui.label("Override font id:");
ui.horizontal(|ui| {
for &style in &[TextStyle::Body, TextStyle::Monospace] {
let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
ui.radio_value(body_text_style, style, text);
ui.radio_value(override_font_id, None, "None");
if ui.radio(override_font_id.is_some(), "override").clicked() {
*override_font_id = Some(FontId::default());
}
if let Some(override_font_id) = override_font_id {
crate::introspection::font_id_ui(ui, override_font_id);
}
});
ui.end_row();
@@ -592,12 +748,14 @@ impl Style {
crate::ComboBox::from_id_source("Override text style")
.selected_text(match override_text_style {
None => "None".to_owned(),
Some(override_text_style) => format!("{:?}", override_text_style),
Some(override_text_style) => override_text_style.to_string(),
})
.show_ui(ui, |ui| {
ui.selectable_value(override_text_style, None, "None");
for style in TextStyle::all() {
let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
let all_text_styles = ui.style().text_styles();
for style in all_text_styles {
let text =
crate::RichText::new(style.to_string()).text_style(style.clone());
ui.selectable_value(override_text_style, Some(style), text);
}
});
@@ -612,6 +770,7 @@ impl Style {
ui.end_row();
});
ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
@@ -626,6 +785,20 @@ impl Style {
}
}
fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap<TextStyle, FontId>) -> Response {
ui.vertical(|ui| {
crate::Grid::new("text_styles").show(ui, |ui| {
for (text_style, font_id) in text_styles.iter_mut() {
ui.label(RichText::new(text_style.to_string()).font(font_id.clone()));
crate::introspection::font_id_ui(ui, font_id);
ui.end_row();
}
});
crate::reset_button_with(ui, text_styles, default_text_styles());
})
.response
}
impl Spacing {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {

View File

@@ -133,7 +133,7 @@ impl Ui {
/// Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.style_mut().body_text_style = egui::TextStyle::Heading;
/// ui.style_mut().override_text_style = Some(egui::TextStyle::Heading);
/// # });
/// ```
pub fn style_mut(&mut self) -> &mut Style {
@@ -359,6 +359,11 @@ impl Ui {
self.ctx().fonts()
}
/// The height of text of this text style
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts().row_height(&style.resolve(self.style()))
}
/// Screen-space rectangle for clipping what we paint in this ui.
/// This is used, for instance, to avoid painting outside a window that is smaller than its contents.
#[inline]
@@ -1086,6 +1091,16 @@ impl Ui {
/// Shortcut for `add(Label::new(text))`
///
/// See also [`Label`].
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// use egui::{RichText, FontId, Color32};
/// ui.label("Normal text");
/// ui.label(RichText::new("Large text").font(FontId::proportional(40.0)));
/// ui.label(RichText::new("Red text").color(Color32::RED));
/// # });
/// ```
#[inline]
pub fn label(&mut self, text: impl Into<WidgetText>) -> Response {
Label::new(text).ui(self)
@@ -1256,12 +1271,12 @@ impl Ui {
pub fn radio_value<Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
alternative: Value,
text: impl Into<WidgetText>,
) -> Response {
let mut response = self.radio(*current_value == selected_value, text);
let mut response = self.radio(*current_value == alternative, text);
if response.clicked() {
*current_value = selected_value;
*current_value = alternative;
response.mark_changed();
}
response

View File

@@ -1,17 +1,30 @@
use epaint::mutex::Arc;
use crate::{
style::WidgetVisuals, text::LayoutJob, Align, Color32, Context, Galley, Pos2, Style, TextStyle,
Ui, Visuals,
style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2,
Style, TextStyle, Ui, Visuals,
};
/// Text and optional style choices for it.
///
/// The style choices (font, color) are applied to the entire text.
/// For more detailed control, use [`crate::text::LayoutJob`] instead.
///
/// A `RichText` can be used in most widgets and helper functions, e.g. [`Ui::label`] and [`Ui::button`].
///
/// ### Example
/// ```
/// use egui::{RichText, Color32};
///
/// RichText::new("Plain");
/// RichText::new("colored").color(Color32::RED);
/// RichText::new("Large and underlined").size(20.0).underline();
/// ```
#[derive(Clone, Default, PartialEq)]
pub struct RichText {
text: String,
size: Option<f32>,
family: Option<FontFamily>,
text_style: Option<TextStyle>,
background_color: Color32,
text_color: Option<Color32>,
@@ -64,6 +77,35 @@ impl RichText {
&self.text
}
/// Select the font size (in points).
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Select the font family.
///
/// This overrides the value from [`Self::text_style`].
///
/// Only the families available in [`crate::FontDefinitions::families`] may be used.
#[inline]
pub fn family(mut self, family: FontFamily) -> Self {
self.family = Some(family);
self
}
/// Select the font and size.
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn font(mut self, font_id: crate::FontId) -> Self {
let crate::FontId { size, family } = font_id;
self.size = Some(size);
self.family = Some(family);
self
}
/// Override the [`TextStyle`].
#[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self {
@@ -170,24 +212,33 @@ impl RichText {
}
/// Read the font height of the selected text style.
pub fn font_height(&self, ctx: &Context) -> f32 {
let text_style = self
.text_style
.or(ctx.style().override_text_style)
.unwrap_or(ctx.style().body_text_style);
ctx.fonts().row_height(text_style)
pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = self.size {
font_id.size = size;
}
if let Some(family) = &self.family {
font_id.family = family.clone();
}
fonts.row_height(&font_id)
}
fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
let text_color = self.get_text_color(&style.visuals);
let Self {
text,
size,
family,
text_style,
background_color,
text_color: _, // already used by `get_text_color`
@@ -204,9 +255,21 @@ impl RichText {
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
let text_style = text_style
.or(style.override_text_style)
.unwrap_or(default_text_style);
let font_id = {
let mut font_id = text_style
.or_else(|| style.override_text_style.clone())
.map_or_else(
|| fallback_font.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = size {
font_id.size = size;
}
if let Some(family) = family {
font_id.family = family;
}
font_id
};
let mut background_color = background_color;
if code {
@@ -230,7 +293,7 @@ impl RichText {
};
let text_format = crate::text::TextFormat {
style: text_style,
font_id,
color: text_color,
background: background_color,
italics,
@@ -270,6 +333,7 @@ impl RichText {
#[derive(Clone)]
pub enum WidgetText {
RichText(RichText),
/// Use this [`LayoutJob`] when laying out the text.
///
/// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected.
@@ -280,6 +344,7 @@ pub enum WidgetText {
/// If you want all parts of the `LayoutJob` respected, then convert it to a
/// [`Galley`] and use [`Self::Galley`] instead.
LayoutJob(LayoutJob),
/// Use exactly this galley when painting the text.
Galley(Arc<Galley>),
}
@@ -438,10 +503,10 @@ impl WidgetText {
}
}
pub(crate) fn font_height(&self, ctx: &Context) -> f32 {
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
match self {
Self::RichText(text) => text.font_height(ctx),
Self::LayoutJob(job) => job.font_height(&*ctx.fonts()),
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
Self::Galley(galley) => {
if let Some(row) = galley.rows.first() {
row.height()
@@ -455,11 +520,11 @@ impl WidgetText {
pub fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
match self {
Self::RichText(text) => text.into_text_job(style, default_text_style, default_valign),
Self::RichText(text) => text.into_text_job(style, fallback_font, default_valign),
Self::LayoutJob(job) => WidgetTextJob {
job,
job_has_color: true,
@@ -482,7 +547,7 @@ impl WidgetText {
ui: &Ui,
wrap: Option<bool>,
available_width: f32,
default_text_style: TextStyle,
fallback_font: impl Into<FontSelection>,
) -> WidgetTextGalley {
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
let wrap_width = if wrap { available_width } else { f32::INFINITY };
@@ -490,7 +555,7 @@ impl WidgetText {
match self {
Self::RichText(text) => {
let valign = ui.layout().vertical_align();
let mut text_job = text.into_text_job(ui.style(), default_text_style, valign);
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(text_job.job),

View File

@@ -195,7 +195,7 @@ impl<'a> Widget for DragValue<'a> {
TextEdit::singleline(&mut value_text)
.id(kb_edit_id)
.desired_width(button_width)
.text_style(TextStyle::Monospace),
.font(TextStyle::Monospace),
);
if let Ok(parsed_value) = value_text.parse() {
let parsed_value = clamp_to_range(parsed_value, clamp_range);

View File

@@ -2,6 +2,8 @@ use crate::{widget_text::WidgetTextGalley, *};
/// Static text.
///
/// Usually it is more convenient to use [`Ui::label`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.label("Equivalent");
@@ -84,7 +86,7 @@ impl Label {
let valign = ui.layout().vertical_align();
let mut text_job = self
.text
.into_text_job(ui.style(), ui.style().body_text_style, valign);
.into_text_job(ui.style(), FontSelection::Default, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width();

View File

@@ -1621,13 +1621,15 @@ fn add_rulers_and_text(
text
});
let font_id = TextStyle::Body.resolve(plot.ui.style());
let corner_value = elem.corner_value();
shapes.push(Shape::text(
&*plot.ui.fonts(),
plot.transform.position_from_value(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
TextStyle::Body,
font_id,
plot.ui.visuals().text_color(),
));
}
@@ -1677,12 +1679,14 @@ pub(super) fn rulers_at_value(
}
};
let font_id = TextStyle::Body.resolve(plot.ui.style());
shapes.push(Shape::text(
&*plot.ui.fonts(),
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
TextStyle::Body,
font_id,
plot.ui.visuals().text_color(),
));
}

View File

@@ -29,7 +29,7 @@ impl Corner {
}
/// The configuration for a plot legend.
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, PartialEq)]
pub struct Legend {
pub text_style: TextStyle,
pub background_alpha: f32,
@@ -82,16 +82,18 @@ impl LegendEntry {
}
}
fn ui(&mut self, ui: &mut Ui, text: String) -> Response {
fn ui(&mut self, ui: &mut Ui, text: String, text_style: &TextStyle) -> Response {
let Self {
color,
checked,
hovered,
} = self;
let galley =
ui.fonts()
.layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY);
let font_id = text_style.resolve(ui.style());
let galley = ui
.fonts()
.layout_delayed_color(text, font_id, f32::INFINITY);
let icon_size = galley.size().y;
let icon_spacing = icon_size / 5.0;
@@ -236,7 +238,6 @@ impl Widget for &mut LegendWidget {
let mut legend_ui = ui.child_ui(legend_rect, layout);
legend_ui
.scope(|ui| {
ui.style_mut().body_text_style = config.text_style;
let background_frame = Frame {
margin: vec2(8.0, 4.0),
corner_radius: ui.style().visuals.window_corner_radius,
@@ -249,7 +250,7 @@ impl Widget for &mut LegendWidget {
.show(ui, |ui| {
entries
.iter_mut()
.map(|(name, entry)| entry.ui(ui, name.clone()))
.map(|(name, entry)| entry.ui(ui, name.clone(), &config.text_style))
.reduce(|r1, r2| r1.union(r2))
.unwrap()
})

View File

@@ -683,7 +683,8 @@ impl PreparedPlot {
let Self { transform, .. } = self;
let bounds = transform.bounds();
let text_style = TextStyle::Body;
let font_id = TextStyle::Body.resolve(ui.style());
let base: i64 = 10;
let basef = base as f64;
@@ -741,7 +742,7 @@ impl PreparedPlot {
let color = color_from_alpha(ui, text_alpha);
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
let galley = ui.painter().layout_no_wrap(text, text_style, color);
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);

View File

@@ -466,10 +466,8 @@ impl<'a> Slider<'a> {
}
fn add_contents(&mut self, ui: &mut Ui) -> Response {
let text_style = TextStyle::Button;
let perpendicular = ui
.fonts()
.row_height(text_style)
.text_style_height(&TextStyle::Body)
.at_least(ui.spacing().interact_size.y);
let slider_response = self.allocate_slider_space(ui, perpendicular);
self.slider_ui(ui, &slider_response);

View File

@@ -52,7 +52,7 @@ pub struct TextEdit<'t> {
hint_text: WidgetText,
id: Option<Id>,
id_source: Option<Id>,
text_style: Option<TextStyle>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
password: bool,
@@ -97,7 +97,7 @@ impl<'t> TextEdit<'t> {
hint_text: Default::default(),
id: None,
id_source: None,
text_style: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
@@ -117,7 +117,7 @@ impl<'t> TextEdit<'t> {
/// - monospaced font
/// - focus lock
pub fn code_editor(self) -> Self {
self.text_style(TextStyle::Monospace).lock_focus(true)
self.font(TextStyle::Monospace).lock_focus(true)
}
/// Use if you want to set an explicit `Id` for this widget.
@@ -144,11 +144,17 @@ impl<'t> TextEdit<'t> {
self
}
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style);
/// Pick a [`FontId`] or [`TextStyle`].
pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
self.font_selection = font_selection.into();
self
}
#[deprecated = "Use .font(…) instead"]
pub fn text_style(self, text_style: TextStyle) -> Self {
self.font(text_style)
}
pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color);
self
@@ -330,7 +336,7 @@ impl<'t> TextEdit<'t> {
hint_text,
id,
id_source,
text_style,
font_selection,
text_color,
layouter,
password,
@@ -350,10 +356,9 @@ impl<'t> TextEdit<'t> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_ref().to_owned();
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or_else(|| ui.style().body_text_style);
let row_height = ui.fonts().row_height(text_style);
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
const MIN_WIDTH: f32 = 24.0; // Never make a `TextEdit` more narrow than this.
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
@@ -363,12 +368,13 @@ impl<'t> TextEdit<'t> {
desired_width.min(available_width)
};
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
let text = mask_if_password(password, text);
ui.fonts().layout_job(if multiline {
LayoutJob::simple(text, text_style, text_color, wrap_width)
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, text_style, text_color)
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
})
};
@@ -543,9 +549,9 @@ impl<'t> TextEdit<'t> {
if text.as_ref().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let galley = if multiline {
hint_text.into_galley(ui, Some(true), desired_size.x, text_style)
hint_text.into_galley(ui, Some(true), desired_size.x, font_id)
} else {
hint_text.into_galley(ui, Some(false), f32::INFINITY, text_style)
hint_text.into_galley(ui, Some(false), f32::INFINITY, font_id)
};
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
}