Rich text for all widgets (#855)

Introduce `RichText` and `WidgetText`
This commit is contained in:
Emil Ernerfeldt 2021-11-01 21:30:10 +01:00 committed by GitHub
parent 9378cd5c6e
commit 09b8269326
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1121 additions and 712 deletions

View file

@ -10,9 +10,11 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
### Added ⭐ ### Added ⭐
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)). * Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)). * You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
### Changed 🔧 ### Changed 🔧
* Unifiy the four `Memory` data buckets (`data`, `data_temp`, `id_data` and `id_data_temp`) into a single `Memory::data`, with a new interface ([#836](https://github.com/emilk/egui/pull/836)). * Unifiy the four `Memory` data buckets (`data`, `data_temp`, `id_data` and `id_data_temp`) into a single `Memory::data`, with a new interface ([#836](https://github.com/emilk/egui/pull/836)).
* `ui.add(Button::new("…").text_color(…))` is now `ui.button(RichText::new("…").color(…))` (same for `Label` )([#855](https://github.com/emilk/egui/pull/855)).
### Contributors 🙏 ### Contributors 🙏
* [mankinskin](https://github.com/mankinskin) ([#543](https://github.com/emilk/egui/pull/543)) * [mankinskin](https://github.com/mankinskin) ([#543](https://github.com/emilk/egui/pull/543))

View file

@ -1,6 +1,6 @@
use std::hash::Hash; use std::hash::Hash;
use crate::{widgets::Label, *}; use crate::*;
use epaint::{Shape, TextStyle}; use epaint::{Shape, TextStyle};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -141,7 +141,7 @@ pub(crate) fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) {
/// ``` /// ```
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct CollapsingHeader { pub struct CollapsingHeader {
label: Label, text: WidgetText,
default_open: bool, default_open: bool,
id_source: Id, id_source: Id,
enabled: bool, enabled: bool,
@ -157,11 +157,11 @@ impl CollapsingHeader {
/// If the label is unique and static this is fine, /// If the label is unique and static this is fine,
/// but if it changes or there are several `CollapsingHeader` with the same title /// but if it changes or there are several `CollapsingHeader` with the same title
/// you need to provide a unique id source with [`Self::id_source`]. /// you need to provide a unique id source with [`Self::id_source`].
pub fn new(label: impl ToString) -> Self { pub fn new(text: impl Into<WidgetText>) -> Self {
let label = Label::new(label).wrap(false); let text = text.into();
let id_source = Id::new(label.text()); let id_source = Id::new(text.text());
Self { Self {
label, text,
default_open: false, default_open: false,
id_source, id_source,
enabled: true, enabled: true,
@ -185,10 +185,9 @@ impl CollapsingHeader {
self self
} }
/// By default, the `CollapsingHeader` text style is `TextStyle::Button`. #[deprecated = "Replaced by: CollapsingHeader::new(RichText::new(text).text_style(…))"]
/// Call `.text_style(style)` to change this.
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.label = self.label.text_style(text_style); self.text = self.text.text_style(text_style);
self self
} }
@ -252,7 +251,7 @@ impl CollapsingHeader {
"Horizontal collapsing is unimplemented" "Horizontal collapsing is unimplemented"
); );
let Self { let Self {
mut label, text,
default_open, default_open,
id_source, id_source,
enabled: _, enabled: _,
@ -261,11 +260,6 @@ impl CollapsingHeader {
show_background: _, show_background: _,
} = self; } = self;
label.text_style = label
.text_style
.or(ui.style().override_text_style)
.or(Some(TextStyle::Button));
// TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame. // TODO: horizontal layout, with icon and text as labels. Insert background behind using Frame.
let id = ui.make_persistent_id(id_source); let id = ui.make_persistent_id(id_source);
@ -273,23 +267,24 @@ impl CollapsingHeader {
let available = ui.available_rect_before_wrap(); let available = ui.available_rect_before_wrap();
let text_pos = available.min + vec2(ui.spacing().indent, 0.0); let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
let galley = let wrap_width = available.right() - text_pos.x;
label.layout_width(ui, available.right() - text_pos.x, Color32::TEMPORARY_COLOR); let wrap = Some(false);
let text_max_x = text_pos.x + galley.size().x; let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button);
let text_max_x = text_pos.x + text.size().x;
let mut desired_width = text_max_x + button_padding.x - available.left(); let mut desired_width = text_max_x + button_padding.x - available.left();
if ui.visuals().collapsing_header_frame { if ui.visuals().collapsing_header_frame {
desired_width = desired_width.max(available.width()); // fill full width desired_width = desired_width.max(available.width()); // fill full width
} }
let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y); let mut desired_size = vec2(desired_width, text.size().y + 2.0 * button_padding.y);
desired_size = desired_size.at_least(ui.spacing().interact_size); desired_size = desired_size.at_least(ui.spacing().interact_size);
let (_, rect) = ui.allocate_space(desired_size); let (_, rect) = ui.allocate_space(desired_size);
let mut header_response = ui.interact(rect, id, Sense::click()); let mut header_response = ui.interact(rect, id, Sense::click());
let text_pos = pos2( let text_pos = pos2(
text_pos.x, text_pos.x,
header_response.rect.center().y - galley.size().y / 2.0, header_response.rect.center().y - text.size().y / 2.0,
); );
let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open);
@ -298,16 +293,11 @@ impl CollapsingHeader {
header_response.mark_changed(); header_response.mark_changed();
} }
header_response header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text())); .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text()));
let visuals = ui let visuals = ui
.style() .style()
.interact_selectable(&header_response, self.selected); .interact_selectable(&header_response, self.selected);
let text_color = ui
.style()
.visuals
.override_text_color
.unwrap_or_else(|| visuals.text_color());
if ui.visuals().collapsing_header_frame || self.show_background { if ui.visuals().collapsing_header_frame || self.show_background {
ui.painter().add(epaint::RectShape { ui.painter().add(epaint::RectShape {
@ -343,7 +333,7 @@ impl CollapsingHeader {
paint_icon(ui, openness, &icon_response); paint_icon(ui, openness, &icon_response);
} }
ui.painter().galley_with_color(text_pos, galley, text_color); text.paint_with_visuals(ui.painter(), text_pos, &visuals);
Prepared { Prepared {
id, id,

View file

@ -1,8 +1,6 @@
use crate::{style::WidgetVisuals, *}; use crate::{style::WidgetVisuals, *};
use epaint::Shape; use epaint::Shape;
// TODO: this should be builder struct so we can set options like width.
/// A drop-down selection menu with a descriptive label. /// A drop-down selection menu with a descriptive label.
/// ///
/// ``` /// ```
@ -10,7 +8,7 @@ use epaint::Shape;
/// # enum Enum { First, Second, Third } /// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First; /// # let mut selected = Enum::First;
/// # let mut ui = &mut egui::Ui::__test(); /// # let mut ui = &mut egui::Ui::__test();
/// egui::ComboBox::from_label( "Select one!") /// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected)) /// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| { /// .show_ui(ui, |ui| {
/// ui.selectable_value(&mut selected, Enum::First, "First"); /// ui.selectable_value(&mut selected, Enum::First, "First");
@ -22,14 +20,14 @@ use epaint::Shape;
#[must_use = "You should call .show*"] #[must_use = "You should call .show*"]
pub struct ComboBox { pub struct ComboBox {
id_source: Id, id_source: Id,
label: Option<Label>, label: Option<WidgetText>,
selected_text: String, selected_text: WidgetText,
width: Option<f32>, width: Option<f32>,
} }
impl ComboBox { impl ComboBox {
/// Label shown next to the combo box /// Label shown next to the combo box
pub fn from_label(label: impl Into<Label>) -> Self { pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into(); let label = label.into();
Self { Self {
id_source: Id::new(label.text()), id_source: Id::new(label.text()),
@ -56,9 +54,8 @@ impl ComboBox {
} }
/// What we show as the currently selected value /// What we show as the currently selected value
#[allow(clippy::needless_pass_by_value)] pub fn selected_text(mut self, selected_text: impl Into<WidgetText>) -> Self {
pub fn selected_text(mut self, selected_text: impl ToString) -> Self { self.selected_text = selected_text.into();
self.selected_text = selected_text.to_string();
self self
} }
@ -95,7 +92,7 @@ impl ComboBox {
if let Some(label) = label { if let Some(label) = label {
ir.response ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text())); .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
ir.response |= ui.add(label); ir.response |= ui.label(label);
} else { } else {
ir.response ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, "")); .widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ""));
@ -115,7 +112,7 @@ impl ComboBox {
/// # let mut ui = &mut egui::Ui::__test(); /// # let mut ui = &mut egui::Ui::__test();
/// let alternatives = ["a", "b", "c", "d"]; /// let alternatives = ["a", "b", "c", "d"];
/// let mut selected = 2; /// let mut selected = 2;
/// egui::ComboBox::from_label( "Select one!").show_index( /// egui::ComboBox::from_label("Select one!").show_index(
/// ui, /// ui,
/// &mut selected, /// &mut selected,
/// alternatives.len(), /// alternatives.len(),
@ -151,11 +148,10 @@ impl ComboBox {
} }
} }
#[allow(clippy::needless_pass_by_value)]
fn combo_box_dyn<'c, R>( fn combo_box_dyn<'c, R>(
ui: &mut Ui, ui: &mut Ui,
button_id: Id, button_id: Id,
selected: impl ToString, selected_text: WidgetText,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
let popup_id = button_id.with("popup"); let popup_id = button_id.with("popup");
@ -166,9 +162,7 @@ fn combo_box_dyn<'c, R>(
let full_minimum_width = ui.spacing().slider_width; let full_minimum_width = ui.spacing().slider_width;
let icon_size = Vec2::splat(ui.spacing().icon_width); let icon_size = Vec2::splat(ui.spacing().icon_width);
let galley = let galley = selected_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
ui.fonts()
.layout_delayed_color(selected.to_string(), TextStyle::Button, f32::INFINITY);
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x; let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
let width = width.at_least(full_minimum_width); let width = width.at_least(full_minimum_width);
@ -188,8 +182,7 @@ fn combo_box_dyn<'c, R>(
paint_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals); paint_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
ui.painter() galley.paint_with_visuals(ui.painter(), text_rect.min, visuals);
.galley_with_color(text_rect.min, galley, visuals.text_color());
}); });
if button_response.clicked() { if button_response.clicked() {

View file

@ -226,9 +226,9 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
/// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text"); /// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text");
/// } /// }
/// ``` /// ```
pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl ToString) -> Option<()> { pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into<WidgetText>) -> Option<()> {
show_tooltip(ctx, id, |ui| { show_tooltip(ctx, id, |ui| {
ui.add(crate::widgets::Label::new(text)); crate::widgets::Label::new(text).ui(ui);
}) })
} }

View file

@ -1,6 +1,6 @@
// WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts. // WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts.
use crate::{widgets::*, *}; use crate::{widget_text::WidgetTextGalley, *};
use epaint::*; use epaint::*;
use super::*; use super::*;
@ -23,7 +23,7 @@ use super::*;
/// }); /// });
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct Window<'open> { pub struct Window<'open> {
title_label: Label, title: WidgetText,
open: Option<&'open mut bool>, open: Option<&'open mut bool>,
area: Area, area: Area,
frame: Option<Frame>, frame: Option<Frame>,
@ -37,13 +37,11 @@ impl<'open> Window<'open> {
/// The window title is used as a unique [`Id`] and must be unique, and should not change. /// The window title is used as a unique [`Id`] and must be unique, and should not change.
/// This is true even if you disable the title bar with `.title_bar(false)`. /// This is true even if you disable the title bar with `.title_bar(false)`.
/// If you need a changing title, you must call `window.id(…)` with a fixed id. /// If you need a changing title, you must call `window.id(…)` with a fixed id.
#[allow(clippy::needless_pass_by_value)] pub fn new(title: impl Into<WidgetText>) -> Self {
pub fn new(title: impl ToString) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading);
let title = title.to_string(); let area = Area::new(title.text());
let area = Area::new(&title);
let title_label = Label::new(title).text_style(TextStyle::Heading).wrap(false);
Self { Self {
title_label, title,
open: None, open: None,
area, area,
frame: None, frame: None,
@ -250,7 +248,7 @@ impl<'open> Window<'open> {
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<InnerResponse<Option<R>>> { ) -> Option<InnerResponse<Option<R>>> {
let Window { let Window {
title_label, title,
open, open,
area, area,
frame, frame,
@ -299,7 +297,7 @@ impl<'open> Window<'open> {
.and_then(|window_interaction| { .and_then(|window_interaction| {
// Calculate roughly how much larger the window size is compared to the inner rect // Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar { let title_bar_height = if with_title_bar {
title_label.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing title.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing
} else { } else {
0.0 0.0
}; };
@ -336,7 +334,7 @@ impl<'open> Window<'open> {
let title_bar = if with_title_bar { let title_bar = if with_title_bar {
let title_bar = show_title_bar( let title_bar = show_title_bar(
&mut frame.content_ui, &mut frame.content_ui,
title_label, title,
show_close_button, show_close_button,
collapsing_id, collapsing_id,
&mut collapsing, &mut collapsing,
@ -745,22 +743,21 @@ fn paint_frame_interaction(
struct TitleBar { struct TitleBar {
id: Id, id: Id,
title_label: Label, title_galley: WidgetTextGalley,
title_galley: std::sync::Arc<Galley>,
min_rect: Rect, min_rect: Rect,
rect: Rect, rect: Rect,
} }
fn show_title_bar( fn show_title_bar(
ui: &mut Ui, ui: &mut Ui,
title_label: Label, title: WidgetText,
show_close_button: bool, show_close_button: bool,
collapsing_id: Id, collapsing_id: Id,
collapsing: &mut collapsing_header::State, collapsing: &mut collapsing_header::State,
collapsible: bool, collapsible: bool,
) -> TitleBar { ) -> TitleBar {
let inner_response = ui.horizontal(|ui| { let inner_response = ui.horizontal(|ui| {
let height = title_label let height = title
.font_height(ui.fonts(), ui.style()) .font_height(ui.fonts(), ui.style())
.max(ui.spacing().interact_size.y); .max(ui.spacing().interact_size.y);
ui.set_min_height(height); ui.set_min_height(height);
@ -782,7 +779,7 @@ fn show_title_bar(
collapsing_header::paint_icon(ui, openness, &collapse_button_response); collapsing_header::paint_icon(ui, openness, &collapse_button_response);
} }
let title_galley = title_label.layout(ui); let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading);
let minimum_width = if collapsible || show_close_button { let minimum_width = if collapsible || show_close_button {
// If at least one button is shown we make room for both buttons (since title is centered): // If at least one button is shown we make room for both buttons (since title is centered):
@ -795,7 +792,6 @@ fn show_title_bar(
TitleBar { TitleBar {
id, id,
title_label,
title_galley, title_galley,
min_rect, min_rect,
rect: Rect::NAN, // Will be filled in later rect: Rect::NAN, // Will be filled in later
@ -830,20 +826,16 @@ impl TitleBar {
} }
} }
// Always have inactive style for the window.
// It is VERY annoying to e.g. change it when moving the window.
let style = ui.visuals().widgets.inactive;
self.title_label = self.title_label.text_color(style.fg_stroke.color);
let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range()); let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range());
let text_pos = let text_pos =
emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top();
let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); let text_pos = text_pos - self.title_galley.galley().rect.min.to_vec2();
let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better)
let text_color = ui.visuals().text_color(); self.title_galley.paint_with_fallback_color(
self.title_label ui.painter(),
.paint_galley(ui, text_pos, self.title_galley, false, text_color); text_pos,
ui.visuals().text_color(),
);
if let Some(content_response) = &content_response { if let Some(content_response) = &content_response {
// paint separator between title and content: // paint separator between title and content:

View file

@ -915,7 +915,7 @@ impl Context {
let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),); let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),);
// TODO: `Sense::hover_highlight()` // TODO: `Sense::hover_highlight()`
if ui if ui
.add(Label::new(text).monospace().sense(Sense::click())) .add(Label::new(RichText::new(text).monospace()).sense(Sense::click()))
.hovered .hovered
&& is_visible && is_visible
{ {

View file

@ -367,6 +367,7 @@ mod sense;
pub mod style; pub mod style;
mod ui; mod ui;
pub mod util; pub mod util;
mod widget_text;
pub mod widgets; pub mod widgets;
pub use epaint; pub use epaint;
@ -408,6 +409,7 @@ pub use {
style::{Style, Visuals}, style::{Style, Visuals},
text::{Galley, TextFormat}, text::{Galley, TextFormat},
ui::Ui, ui::Ui,
widget_text::{RichText, WidgetText},
widgets::*, widgets::*,
}; };
@ -416,10 +418,10 @@ pub use {
/// Helper function that adds a label when compiling with debug assertions enabled. /// Helper function that adds a label when compiling with debug assertions enabled.
pub fn warn_if_debug_build(ui: &mut crate::Ui) { pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
ui.add( ui.label(
crate::Label::new("‼ Debug build ‼") RichText::new("‼ Debug build ‼")
.small() .small()
.text_color(crate::Color32::RED), .color(crate::Color32::RED),
) )
.on_hover_text("egui was compiled with debug assertions enabled."); .on_hover_text("egui was compiled with debug assertions enabled.");
} }
@ -437,7 +439,7 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
macro_rules! github_link_file_line { macro_rules! github_link_file_line {
($github_url: expr, $label: expr) => {{ ($github_url: expr, $label: expr) => {{
let url = format!("{}{}#L{}", $github_url, file!(), line!()); let url = format!("{}{}#L{}", $github_url, file!(), line!());
$crate::Hyperlink::new(url).text($label) $crate::Hyperlink::from_label_and_url($label, url)
}}; }};
} }
@ -451,7 +453,7 @@ macro_rules! github_link_file_line {
macro_rules! github_link_file { macro_rules! github_link_file {
($github_url: expr, $label: expr) => {{ ($github_url: expr, $label: expr) => {{
let url = format!("{}{}", $github_url, file!()); let url = format!("{}{}", $github_url, file!());
$crate::Hyperlink::new(url).text($label) $crate::Hyperlink::from_label_and_url($label, url)
}}; }};
} }

View file

@ -92,7 +92,7 @@ pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResp
/// Returns `None` if the menu is not open. /// Returns `None` if the menu is not open.
pub fn menu_button<R>( pub fn menu_button<R>(
ui: &mut Ui, ui: &mut Ui,
title: impl ToString, title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents)) stationary_menu_impl(ui, title, Box::new(add_contents))
@ -103,18 +103,17 @@ pub fn menu_button<R>(
pub(crate) fn submenu_button<R>( pub(crate) fn submenu_button<R>(
ui: &mut Ui, ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>, parent_state: Arc<RwLock<MenuState>>,
title: impl ToString, title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents) SubMenu::new(parent_state, title).show(ui, add_contents)
} }
/// wrapper for the contents of every menu. /// wrapper for the contents of every menu.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn menu_ui<'c, R>( pub(crate) fn menu_ui<'c, R>(
ctx: &CtxRef, ctx: &CtxRef,
menu_id: impl std::hash::Hash, menu_id: impl std::hash::Hash,
menu_state_arc: Arc<RwLock<MenuState>>, menu_state_arc: &Arc<RwLock<MenuState>>,
mut style: Style, mut style: Style,
add_contents: impl FnOnce(&mut Ui) -> R + 'c, add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
@ -152,15 +151,14 @@ pub(crate) fn menu_ui<'c, R>(
} }
/// build a top level menu with a button /// build a top level menu with a button
#[allow(clippy::needless_pass_by_value)]
fn stationary_menu_impl<'c, R>( fn stationary_menu_impl<'c, R>(
ui: &mut Ui, ui: &mut Ui,
title: impl ToString, title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
let title = title.to_string(); let title = title.into();
let bar_id = ui.id(); let bar_id = ui.id();
let menu_id = bar_id.with(&title); let menu_id = bar_id.with(title.text());
let mut bar_state = BarState::load(ui.ctx(), bar_id); let mut bar_state = BarState::load(ui.ctx(), bar_id);
@ -372,20 +370,20 @@ impl MenuResponse {
} }
} }
pub struct SubMenuButton { pub struct SubMenuButton {
text: String, text: WidgetText,
icon: String, icon: WidgetText,
index: usize, index: usize,
} }
impl SubMenuButton { impl SubMenuButton {
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label /// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
#[allow(clippy::needless_pass_by_value)] fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
fn new(text: impl ToString, icon: impl ToString, index: usize) -> Self {
Self { Self {
text: text.to_string(), text: text.into(),
icon: icon.to_string(), icon: icon.into(),
index, index,
} }
} }
fn visuals<'a>( fn visuals<'a>(
ui: &'a Ui, ui: &'a Ui,
response: &'_ Response, response: &'_ Response,
@ -398,11 +396,12 @@ impl SubMenuButton {
ui.style().interact(response) ui.style().interact(response)
} }
} }
#[allow(clippy::needless_pass_by_value)]
pub fn icon(mut self, icon: impl ToString) -> Self { pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.to_string(); self.icon = icon.into();
self self
} }
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response { pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let SubMenuButton { text, icon, .. } = self; let SubMenuButton { text, icon, .. } = self;
@ -412,14 +411,10 @@ impl SubMenuButton {
let button_padding = ui.spacing().button_padding; let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding; let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x; let text_available_width = ui.available_width() - total_extra.x;
let text_galley = ui let text_galley = text.into_galley(ui, Some(true), text_available_width, text_style);
.fonts()
.layout_delayed_color(text, text_style, text_available_width);
let icon_available_width = text_available_width - text_galley.size().x; let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = ui let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style);
.fonts()
.layout_delayed_color(icon, text_style, icon_available_width);
let text_and_icon_size = Vec2::new( let text_and_icon_size = Vec2::new(
text_galley.size().x + icon_galley.size().x, text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y), text_galley.size().y.max(icon_galley.size().y),
@ -447,10 +442,8 @@ impl SubMenuButton {
); );
let text_color = visuals.text_color(); let text_color = visuals.text_color();
ui.painter() text_galley.paint_with_fallback_color(ui.painter(), text_pos, text_color);
.galley_with_color(text_pos, text_galley, text_color); icon_galley.paint_with_fallback_color(ui.painter(), icon_pos, text_color);
ui.painter()
.galley_with_color(icon_pos, icon_galley, text_color);
} }
response response
} }
@ -460,14 +453,14 @@ pub struct SubMenu {
parent_state: Arc<RwLock<MenuState>>, parent_state: Arc<RwLock<MenuState>>,
} }
impl SubMenu { impl SubMenu {
#[allow(clippy::needless_pass_by_value)] fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl ToString) -> Self {
let index = parent_state.write().next_entry_index(); let index = parent_state.write().next_entry_index();
Self { Self {
button: SubMenuButton::new(text, "", index), button: SubMenuButton::new(text, "", index),
parent_state, parent_state,
} }
} }
pub fn show<R>( pub fn show<R>(
self, self,
ui: &mut Ui, ui: &mut Ui,
@ -522,8 +515,7 @@ impl MenuState {
}, },
..Default::default() ..Default::default()
}; };
let menu_state_arc = menu_state.clone(); crate::menu::menu_ui(ctx, id, menu_state, style, add_contents)
crate::menu::menu_ui(ctx, id, menu_state_arc, style, add_contents)
} }
fn show_submenu<R>( fn show_submenu<R>(
&mut self, &mut self,

View file

@ -1,8 +1,7 @@
use crate::{ use crate::{
emath::{lerp, Align, Pos2, Rect, Vec2}, emath::{lerp, Align, Pos2, Rect, Vec2},
CursorIcon, PointerButton, NUM_POINTER_BUTTONS, CtxRef, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, NUM_POINTER_BUTTONS,
}; };
use crate::{CtxRef, Id, LayerId, Sense, Ui};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -389,14 +388,14 @@ impl Response {
/// ///
/// If you call this multiple times the tooltips will stack underneath the previous ones. /// If you call this multiple times the tooltips will stack underneath the previous ones.
#[doc(alias = "tooltip")] #[doc(alias = "tooltip")]
pub fn on_hover_text(self, text: impl ToString) -> Self { pub fn on_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_hover_ui(|ui| { self.on_hover_ui(|ui| {
ui.add(crate::widgets::Label::new(text)); ui.add(crate::widgets::Label::new(text));
}) })
} }
/// Show this text when hovering if the widget is disabled. /// Show this text when hovering if the widget is disabled.
pub fn on_disabled_hover_text(self, text: impl ToString) -> Self { pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_disabled_hover_ui(|ui| { self.on_disabled_hover_ui(|ui| {
ui.add(crate::widgets::Label::new(text)); ui.add(crate::widgets::Label::new(text));
}) })

View file

@ -2,7 +2,7 @@
#![allow(clippy::if_same_then_else)] #![allow(clippy::if_same_then_else)]
use crate::{color::*, emath::*, Response}; use crate::{color::*, emath::*, Response, RichText, WidgetText};
use epaint::{Shadow, Stroke, TextStyle}; use epaint::{Shadow, Stroke, TextStyle};
/// Specifies the look and feel of egui. /// Specifies the look and feel of egui.
@ -581,15 +581,8 @@ impl Style {
ui.label("Default body text style:"); ui.label("Default body text style:");
ui.horizontal(|ui| { ui.horizontal(|ui| {
for &style in &[TextStyle::Body, TextStyle::Monospace] { for &style in &[TextStyle::Body, TextStyle::Monospace] {
if ui let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
.add( ui.radio_value(body_text_style, style, text);
RadioButton::new(*body_text_style == style, format!("{:?}", style))
.text_style(style),
)
.clicked()
{
*body_text_style = style;
};
} }
}); });
ui.end_row(); ui.end_row();
@ -603,17 +596,8 @@ impl Style {
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(override_text_style, None, "None"); ui.selectable_value(override_text_style, None, "None");
for style in TextStyle::all() { for style in TextStyle::all() {
// ui.selectable_value(override_text_style, Some(style), format!("{:?}", style)); let text = crate::RichText::new(format!("{:?}", style)).text_style(style);
let selected = *override_text_style == Some(style); ui.selectable_value(override_text_style, Some(style), text);
if ui
.add(
SelectableLabel::new(selected, format!("{:?}", style))
.text_style(style),
)
.clicked()
{
*override_text_style = Some(style);
}
} }
}); });
ui.end_row(); ui.end_row();
@ -879,7 +863,7 @@ impl Visuals {
&mut widgets.noninteractive.fg_stroke.color, &mut widgets.noninteractive.fg_stroke.color,
"Text color", "Text color",
); );
ui_color(ui, code_bg_color, Label::new("Code background").code()).on_hover_ui(|ui| { ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.label("For monospaced inlined text "); ui.label("For monospaced inlined text ");
@ -949,10 +933,10 @@ fn slider_vec2<'a>(
} }
} }
fn ui_color(ui: &mut Ui, srgba: &mut Color32, label: impl Into<Label>) -> Response { fn ui_color(ui: &mut Ui, srgba: &mut Color32, label: impl Into<WidgetText>) -> Response {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.color_edit_button_srgba(srgba); ui.color_edit_button_srgba(srgba);
ui.add(label.into()); ui.label(label);
}) })
.response .response
} }

View file

@ -283,6 +283,7 @@ impl Ui {
} }
/// Should text wrap in this `Ui`? /// Should text wrap in this `Ui`?
///
/// This is determined first by [`Style::wrap`], and then by the layout of this `Ui`. /// This is determined first by [`Style::wrap`], and then by the layout of this `Ui`.
pub fn wrap_text(&self) -> bool { pub fn wrap_text(&self) -> bool {
if let Some(wrap) = self.style.wrap { if let Some(wrap) = self.style.wrap {
@ -290,8 +291,8 @@ impl Ui {
} else if let Some(grid) = self.placer.grid() { } else if let Some(grid) = self.placer.grid() {
grid.wrap_text() grid.wrap_text()
} else { } else {
// In vertical layouts we wrap text, but in horizontal we keep going. let layout = self.layout();
self.layout().is_vertical() layout.is_vertical() || layout.is_horizontal() && layout.main_wrap()
} }
} }
@ -990,50 +991,54 @@ impl Ui {
/// ///
/// See also [`Label`]. /// See also [`Label`].
#[inline(always)] #[inline(always)]
pub fn label(&mut self, text: impl ToString) -> Response { pub fn label(&mut self, text: impl Into<WidgetText>) -> Response {
Label::new(text).ui(self) Label::new(text).ui(self)
} }
/// Show colored text. /// Show colored text.
/// ///
/// Shortcut for `add(Label::new(text).text_color(color))` /// Shortcut for `ui.label(RichText::new(text).color(color))`
pub fn colored_label(&mut self, color: impl Into<Color32>, text: impl ToString) -> Response { pub fn colored_label(
Label::new(text).text_color(color).ui(self) &mut self,
color: impl Into<Color32>,
text: impl Into<RichText>,
) -> Response {
Label::new(text.into().color(color)).ui(self)
} }
/// Show large text. /// Show large text.
/// ///
/// Shortcut for `add(Label::new(text).heading())` /// Shortcut for `ui.label(RichText::new(text).heading())`
pub fn heading(&mut self, text: impl ToString) -> Response { pub fn heading(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text).heading().ui(self) Label::new(text.into().heading()).ui(self)
} }
/// Show monospace (fixed width) text. /// Show monospace (fixed width) text.
/// ///
/// Shortcut for `add(Label::new(text).monospace())` /// Shortcut for `ui.label(RichText::new(text).monospace())`
pub fn monospace(&mut self, text: impl ToString) -> Response { pub fn monospace(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text).monospace().ui(self) Label::new(text.into().monospace()).ui(self)
} }
/// Show text as monospace with a gray background. /// Show text as monospace with a gray background.
/// ///
/// Shortcut for `add(Label::new(text).code())` /// Shortcut for `ui.label(RichText::new(text).code())`
pub fn code(&mut self, text: impl ToString) -> Response { pub fn code(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text).code().ui(self) Label::new(text.into().code()).ui(self)
} }
/// Show small text. /// Show small text.
/// ///
/// Shortcut for `add(Label::new(text).small())` /// Shortcut for `ui.label(RichText::new(text).small())`
pub fn small(&mut self, text: impl ToString) -> Response { pub fn small(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text).small().ui(self) Label::new(text.into().small()).ui(self)
} }
/// Show text that stand out a bit (e.g. slightly brighter). /// Show text that stand out a bit (e.g. slightly brighter).
/// ///
/// Shortcut for `add(Label::new(text).strong())` /// Shortcut for `ui.label(RichText::new(text).strong())`
pub fn strong(&mut self, text: impl ToString) -> Response { pub fn strong(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text).strong().ui(self) Label::new(text.into().strong()).ui(self)
} }
/// Shortcut for `add(Hyperlink::new(url))` /// Shortcut for `add(Hyperlink::new(url))`
@ -1051,8 +1056,8 @@ impl Ui {
/// ``` /// ```
/// ///
/// See also [`Hyperlink`]. /// See also [`Hyperlink`].
pub fn hyperlink_to(&mut self, label: impl ToString, url: impl ToString) -> Response { pub fn hyperlink_to(&mut self, label: impl Into<WidgetText>, url: impl ToString) -> Response {
Hyperlink::new(url).text(label).ui(self) Hyperlink::from_label_and_url(label, url).ui(self)
} }
/// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`). /// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`).
@ -1089,9 +1094,21 @@ impl Ui {
/// Shortcut for `add(Button::new(text))` /// Shortcut for `add(Button::new(text))`
/// ///
/// See also [`Button`]. /// See also [`Button`].
///
/// ```
/// # let ui = &mut egui::Ui::__test();
/// if ui.button("Click me!").clicked() {
/// // …
/// }
///
/// # use egui::{RichText, Color32};
/// if ui.button(RichText::new("delete").color(Color32::RED)).clicked() {
/// // …
/// }
/// ```
#[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "] #[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "]
#[inline(always)] #[inline]
pub fn button(&mut self, text: impl ToString) -> Response { pub fn button(&mut self, text: impl Into<WidgetText>) -> Response {
Button::new(text).ui(self) Button::new(text).ui(self)
} }
@ -1101,19 +1118,21 @@ impl Ui {
/// ///
/// Shortcut for `add(Button::new(text).small())` /// Shortcut for `add(Button::new(text).small())`
#[must_use = "You should check if the user clicked this with `if ui.small_button(…).clicked() { … } "] #[must_use = "You should check if the user clicked this with `if ui.small_button(…).clicked() { … } "]
pub fn small_button(&mut self, text: impl ToString) -> Response { pub fn small_button(&mut self, text: impl Into<WidgetText>) -> Response {
Button::new(text).small().ui(self) Button::new(text).small().ui(self)
} }
/// Show a checkbox. /// Show a checkbox.
pub fn checkbox(&mut self, checked: &mut bool, text: impl ToString) -> Response { #[inline]
pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<WidgetText>) -> Response {
Checkbox::new(checked, text).ui(self) Checkbox::new(checked, text).ui(self)
} }
/// Show a [`RadioButton`]. /// Show a [`RadioButton`].
/// Often you want to use [`Self::radio_value`] instead. /// Often you want to use [`Self::radio_value`] instead.
#[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "] #[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "]
pub fn radio(&mut self, selected: bool, text: impl ToString) -> Response { #[inline]
pub fn radio(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
RadioButton::new(selected, text).ui(self) RadioButton::new(selected, text).ui(self)
} }
@ -1134,11 +1153,12 @@ impl Ui {
/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() { /// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
/// my_enum = Enum::First /// my_enum = Enum::First
/// } /// }
/// ```
pub fn radio_value<Value: PartialEq>( pub fn radio_value<Value: PartialEq>(
&mut self, &mut self,
current_value: &mut Value, current_value: &mut Value,
selected_value: Value, selected_value: Value,
text: impl ToString, text: impl Into<WidgetText>,
) -> Response { ) -> Response {
let mut response = self.radio(*current_value == selected_value, text); let mut response = self.radio(*current_value == selected_value, text);
if response.clicked() { if response.clicked() {
@ -1152,7 +1172,7 @@ impl Ui {
/// ///
/// See also [`SelectableLabel`]. /// See also [`SelectableLabel`].
#[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "] #[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "]
pub fn selectable_label(&mut self, checked: bool, text: impl ToString) -> Response { pub fn selectable_label(&mut self, checked: bool, text: impl Into<WidgetText>) -> Response {
SelectableLabel::new(checked, text).ui(self) SelectableLabel::new(checked, text).ui(self)
} }
@ -1166,7 +1186,7 @@ impl Ui {
&mut self, &mut self,
current_value: &mut Value, current_value: &mut Value,
selected_value: Value, selected_value: Value,
text: impl ToString, text: impl Into<WidgetText>,
) -> Response { ) -> Response {
let mut response = self.selectable_label(*current_value == selected_value, text); let mut response = self.selectable_label(*current_value == selected_value, text);
if response.clicked() { if response.clicked() {
@ -1373,7 +1393,7 @@ impl Ui {
/// A [`CollapsingHeader`] that starts out collapsed. /// A [`CollapsingHeader`] that starts out collapsed.
pub fn collapsing<R>( pub fn collapsing<R>(
&mut self, &mut self,
heading: impl ToString, heading: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
CollapsingHeader::new(heading).show(self, add_contents) CollapsingHeader::new(heading).show(self, add_contents)
@ -1642,10 +1662,6 @@ impl Ui {
self.placer.is_grid() self.placer.is_grid()
} }
pub(crate) fn grid(&self) -> Option<&grid::GridLayout> {
self.placer.grid()
}
/// Move to the next row in a grid layout or wrapping layout. /// Move to the next row in a grid layout or wrapping layout.
/// Otherwise does nothing. /// Otherwise does nothing.
pub fn end_row(&mut self) { pub fn end_row(&mut self) {
@ -1749,7 +1765,7 @@ impl Ui {
/// ``` /// ```
pub fn menu_button<R>( pub fn menu_button<R>(
&mut self, &mut self,
title: impl ToString, title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> { ) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() { if let Some(menu_state) = self.menu_state.clone() {

635
egui/src/widget_text.rs Normal file
View file

@ -0,0 +1,635 @@
use std::sync::Arc;
use crate::{
style::WidgetVisuals, text::LayoutJob, Align, Color32, 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.
#[derive(Default)]
pub struct RichText {
text: String,
text_style: Option<TextStyle>,
background_color: Color32,
text_color: Option<Color32>,
code: bool,
strong: bool,
weak: bool,
strikethrough: bool,
underline: bool,
italics: bool,
raised: bool,
}
impl From<&str> for RichText {
#[inline]
fn from(text: &str) -> Self {
RichText::new(text)
}
}
impl From<&String> for RichText {
#[inline]
fn from(text: &String) -> Self {
RichText::new(text)
}
}
impl From<String> for RichText {
#[inline]
fn from(text: String) -> Self {
RichText::new(text)
}
}
impl RichText {
#[inline]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[inline]
pub fn text(&self) -> &str {
&self.text
}
/// Override the [`TextStyle`].
#[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style);
self
}
/// Set the [`TextStyle`] unless it has already been set
#[inline]
pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
self.text_style.get_or_insert(text_style);
self
}
/// Use [`TextStyle::Heading`].
#[inline]
pub fn heading(self) -> Self {
self.text_style(TextStyle::Heading)
}
/// Use [`TextStyle::Monospace`].
#[inline]
pub fn monospace(self) -> Self {
self.text_style(TextStyle::Monospace)
}
/// Monospace label with different background color.
#[inline]
pub fn code(mut self) -> Self {
self.code = true;
self.text_style(TextStyle::Monospace)
}
/// Extra strong text (stronger color).
#[inline]
pub fn strong(mut self) -> Self {
self.strong = true;
self
}
/// Extra weak text (fainter color).
#[inline]
pub fn weak(mut self) -> Self {
self.weak = true;
self
}
/// Draw a line under the text.
///
/// If you want to control the line color, use [`LayoutJob`] instead.
#[inline]
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
/// Draw a line through the text, crossing it out.
///
/// If you want to control the strikethrough line color, use [`LayoutJob`] instead.
#[inline]
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
/// Tilt the characters to the right.
#[inline]
pub fn italics(mut self) -> Self {
self.italics = true;
self
}
/// Smaller text.
#[inline]
pub fn small(self) -> Self {
self.text_style(TextStyle::Small)
}
/// For e.g. exponents.
#[inline]
pub fn small_raised(self) -> Self {
self.text_style(TextStyle::Small).raised()
}
/// Align text to top. Only applicable together with [`Self::small()`].
#[inline]
pub fn raised(mut self) -> Self {
self.raised = true;
self
}
/// Fill-color behind the text.
#[inline]
pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
self.background_color = background_color.into();
self
}
/// Override text color.
#[inline]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.text_color = Some(color.into());
self
}
/// Read the font height of the selected text style.
pub fn font_height(&self, fonts: &epaint::text::Fonts, style: &crate::Style) -> f32 {
let text_style = self
.text_style
.or(style.override_text_style)
.unwrap_or(style.body_text_style);
fonts.row_height(text_style)
}
fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
default_valign: Align,
) -> WidgetTextJob {
let text_color = self.get_text_color(&style.visuals);
let Self {
text,
text_style,
background_color,
text_color: _, // already used by `get_text_color`
code,
strong: _, // already used by `get_text_color`
weak: _, // already used by `get_text_color`
strikethrough,
underline,
italics,
raised,
} = self;
let job_has_color = text_color.is_some();
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 mut background_color = background_color;
if code {
background_color = style.visuals.code_bg_color;
}
let underline = if underline {
crate::Stroke::new(1.0, line_color)
} else {
crate::Stroke::none()
};
let strikethrough = if strikethrough {
crate::Stroke::new(1.0, line_color)
} else {
crate::Stroke::none()
};
let valign = if raised {
crate::Align::TOP
} else {
default_valign
};
let text_format = crate::text::TextFormat {
style: text_style,
color: text_color,
background: background_color,
italics,
underline,
strikethrough,
valign,
};
let job = LayoutJob::single_section(text, text_format);
WidgetTextJob { job, job_has_color }
}
fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
if let Some(text_color) = self.text_color {
Some(text_color)
} else if self.strong {
Some(visuals.strong_text_color())
} else if self.weak {
Some(visuals.weak_text_color())
} else {
visuals.override_text_color
}
}
}
// ----------------------------------------------------------------------------
/// This is how you specify text for a widget.
///
/// A lot of widgets use `impl Into<WidgetText>` as an argument,
/// allowing you to pass in [`String`], [`RichText`], [`LayoutJob`], and more.
///
/// Often a [`WidgetText`] is just a simple [`String`],
/// but it can be a [`RichText`] (text with color, style, etc),
/// a [`LayoutJob`] (for when you want full control of how the text looks)
/// or text that has already been layed out in a [`Galley`].
pub enum WidgetText {
RichText(RichText),
/// Use this [`LayoutJob`] when laying out the text.
///
/// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected.
///
/// [`LayoutJob::wrap_width`], [`LayoutJob::halign`], [`LayoutJob::justify`]
/// and [`LayoutJob::first_row_min_height`] will likely be determined by the [`crate::Layout`]
/// of the [`Ui`] the widget is placed in.
/// 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>),
}
impl Default for WidgetText {
fn default() -> Self {
Self::RichText(RichText::default())
}
}
impl WidgetText {
#[inline]
pub fn is_empty(&self) -> bool {
match self {
Self::RichText(text) => text.is_empty(),
Self::LayoutJob(job) => job.is_empty(),
Self::Galley(galley) => galley.is_empty(),
}
}
#[inline]
pub fn text(&self) -> &str {
match self {
Self::RichText(text) => text.text(),
Self::LayoutJob(job) => &job.text,
Self::Galley(galley) => galley.text(),
}
}
/// Override the [`TextStyle`] if, and only if, this is a [`RichText`].
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Set the [`TextStyle`] unless it has already been set
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Override text color if, and only if, this is a [`RichText`].
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn color(self, color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.color(color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn heading(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.heading()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn monospace(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.monospace()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn code(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.code()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn strong(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strong()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn weak(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.weak()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn underline(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.underline()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn strikethrough(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strikethrough()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn italics(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.italics()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn small(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn small_raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small_raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.background_color(background_color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
pub(crate) fn font_height(&self, fonts: &epaint::text::Fonts, style: &crate::Style) -> f32 {
match self {
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()
} else {
galley.size().y
}
}
}
}
pub fn into_text_job(
self,
style: &Style,
default_text_style: TextStyle,
default_valign: Align,
) -> WidgetTextJob {
match self {
Self::RichText(text) => text.into_text_job(style, default_text_style, default_valign),
Self::LayoutJob(job) => WidgetTextJob {
job,
job_has_color: true,
},
Self::Galley(galley) => {
let job: LayoutJob = (*galley.job).clone();
WidgetTextJob {
job,
job_has_color: true,
}
}
}
}
/// Layout with wrap mode based on the containing `Ui`.
///
/// wrap: override for [`Ui::wrap_text`].
pub fn into_galley(
self,
ui: &Ui,
wrap: Option<bool>,
available_width: f32,
default_text_style: TextStyle,
) -> WidgetTextGalley {
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
let wrap_width = if wrap { available_width } else { f32::INFINITY };
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);
text_job.job.wrap_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(text_job.job),
galley_has_color: text_job.job_has_color,
}
}
Self::LayoutJob(mut job) => {
job.wrap_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts().layout_job(job),
galley_has_color: true,
}
}
Self::Galley(galley) => WidgetTextGalley {
galley,
galley_has_color: true,
},
}
}
}
impl From<&str> for WidgetText {
#[inline]
fn from(text: &str) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<&String> for WidgetText {
#[inline]
fn from(text: &String) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<String> for WidgetText {
#[inline]
fn from(text: String) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<RichText> for WidgetText {
#[inline]
fn from(rich_text: RichText) -> Self {
Self::RichText(rich_text)
}
}
impl From<LayoutJob> for WidgetText {
#[inline]
fn from(layout_job: LayoutJob) -> Self {
Self::LayoutJob(layout_job)
}
}
impl From<Arc<Galley>> for WidgetText {
#[inline]
fn from(galley: Arc<Galley>) -> Self {
Self::Galley(galley)
}
}
// ----------------------------------------------------------------------------
pub struct WidgetTextJob {
pub job: LayoutJob,
pub job_has_color: bool,
}
impl WidgetTextJob {
pub fn into_galley(self, fonts: &crate::text::Fonts) -> WidgetTextGalley {
let Self { job, job_has_color } = self;
let galley = fonts.layout_job(job);
WidgetTextGalley {
galley,
galley_has_color: job_has_color,
}
}
}
// ----------------------------------------------------------------------------
/// Text that has been layed out and ready to be painted.
pub struct WidgetTextGalley {
pub galley: Arc<Galley>,
pub galley_has_color: bool,
}
impl WidgetTextGalley {
/// Size of the layed out text.
#[inline]
pub fn size(&self) -> crate::Vec2 {
self.galley.size()
}
/// Size of the layed out text.
#[inline]
pub fn text(&self) -> &str {
self.galley.text()
}
#[inline]
pub fn galley(&self) -> &Arc<Galley> {
&self.galley
}
/// Use the colors in the original [`WidgetText`] if any,
/// else fall back to the one specified by the [`WidgetVisuals`].
pub fn paint_with_visuals(
self,
painter: &crate::Painter,
text_pos: Pos2,
visuals: &WidgetVisuals,
) {
self.paint_with_fallback_color(painter, text_pos, visuals.text_color());
}
/// Use the colors in the original [`WidgetText`] if any,
/// else fall back to the given color.
pub fn paint_with_fallback_color(
self,
painter: &crate::Painter,
text_pos: Pos2,
text_color: Color32,
) {
if self.galley_has_color {
painter.galley(text_pos, self.galley);
} else {
painter.galley_with_color(text_pos, self.galley, text_color);
}
}
/// Paint with this specific color.
pub fn paint_with_color_override(
self,
painter: &crate::Painter,
text_pos: Pos2,
text_color: Color32,
) {
painter.galley_with_color(text_pos, self.galley, text_color);
}
}

View file

@ -1,14 +1,5 @@
use crate::*; use crate::*;
/// For those of us who miss `a ? yes : no`.
fn select<T>(b: bool, if_true: T, if_false: T) -> T {
if b {
if_true
} else {
if_false
}
}
/// Clickable button with text. /// Clickable button with text.
/// ///
/// See also [`Ui::button`]. /// See also [`Ui::button`].
@ -17,7 +8,7 @@ fn select<T>(b: bool, if_true: T, if_false: T) -> T {
/// # let ui = &mut egui::Ui::__test(); /// # let ui = &mut egui::Ui::__test();
/// # fn do_stuff() {} /// # fn do_stuff() {}
/// ///
/// if ui.add(egui::Button::new("Click mew")).clicked() { /// if ui.add(egui::Button::new("Click me")).clicked() {
/// do_stuff(); /// do_stuff();
/// } /// }
/// ///
@ -28,48 +19,53 @@ fn select<T>(b: bool, if_true: T, if_false: T) -> T {
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button { pub struct Button {
text: String, text: WidgetText,
text_color: Option<Color32>, wrap: Option<bool>,
text_style: Option<TextStyle>,
/// None means default for interact /// None means default for interact
fill: Option<Color32>, fill: Option<Color32>,
stroke: Option<Stroke>, stroke: Option<Stroke>,
sense: Sense, sense: Sense,
small: bool, small: bool,
frame: Option<bool>, frame: Option<bool>,
wrap: Option<bool>,
min_size: Vec2, min_size: Vec2,
} }
impl Button { impl Button {
#[allow(clippy::needless_pass_by_value)] pub fn new(text: impl Into<WidgetText>) -> Self {
pub fn new(text: impl ToString) -> Self {
Self { Self {
text: text.to_string(), text: text.into(),
text_color: None, wrap: None,
text_style: None,
fill: None, fill: None,
stroke: None, stroke: None,
sense: Sense::click(), sense: Sense::click(),
small: false, small: false,
frame: None, frame: None,
wrap: None,
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
} }
} }
/// If `true`, the text will wrap to stay within the max width of the `Ui`.
///
/// By default [`Self::wrap`] will be true in vertical layouts
/// and horizontal layouts with wrapping,
/// and false on non-wrapping horizontal layouts.
///
/// Note that any `\n` in the text will always produce a new line.
#[inline]
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap);
self
}
#[deprecated = "Replaced by: Button::new(RichText::new(text).color(…))"]
pub fn text_color(mut self, text_color: Color32) -> Self { pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color); self.text = self.text.color(text_color);
self
}
pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
self.text_color = text_color;
self self
} }
#[deprecated = "Replaced by: Button::new(RichText::new(text).text_style(…))"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style); self.text = self.text.text_style(text_style);
self self
} }
@ -91,7 +87,7 @@ impl Button {
/// Make this a small button, suitable for embedding into text. /// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self { pub fn small(mut self) -> Self {
self.text_style = Some(TextStyle::Body); self.text = self.text.text_style(TextStyle::Body);
self.small = true; self.small = true;
self self
} }
@ -109,17 +105,6 @@ impl Button {
self self
} }
/// If `true`, the text will wrap at the `max_width`.
/// By default [`Self::wrap`] will be true in vertical layouts
/// and horizontal layouts with wrapping,
/// and false on non-wrapping horizontal layouts.
///
/// Note that any `\n` in the button text will always produce a new line.
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap);
self
}
pub(crate) fn min_size(mut self, min_size: Vec2) -> Self { pub(crate) fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size; self.min_size = min_size;
self self
@ -130,49 +115,40 @@ impl Widget for Button {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Button { let Button {
text, text,
text_color, wrap,
text_style,
fill, fill,
stroke, stroke,
sense, sense,
small, small,
frame, frame,
wrap,
min_size, min_size,
} = self; } = self;
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let mut button_padding = ui.spacing().button_padding; let mut button_padding = ui.spacing().button_padding;
if small { if small {
button_padding.y = 0.0; button_padding.y = 0.0;
} }
let total_extra = button_padding + button_padding; let total_extra = button_padding + button_padding;
let wrap = wrap.unwrap_or_else(|| ui.wrap_text()); let wrap_width = ui.available_width() - total_extra.x;
let wrap_width = select(wrap, ui.available_width() - total_extra.x, f32::INFINITY); let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let mut desired_size = galley.size() + 2.0 * button_padding; let mut desired_size = text.size() + 2.0 * button_padding;
if !small { if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
} }
desired_size = desired_size.at_least(min_size); desired_size = desired_size.at_least(min_size);
let (rect, response) = ui.allocate_at_least(desired_size, sense); let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, galley.text())); response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
if ui.clip_rect().intersects(rect) { if ui.clip_rect().intersects(rect) {
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
let text_pos = ui let text_pos = ui
.layout() .layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding)) .align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min; .min;
if frame { if frame {
@ -186,10 +162,7 @@ impl Widget for Button {
); );
} }
let text_color = text_color text.paint_with_visuals(ui.painter(), text_pos, visuals);
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
} }
response response
@ -211,48 +184,35 @@ impl Widget for Button {
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked")); /// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug)]
pub struct Checkbox<'a> { pub struct Checkbox<'a> {
checked: &'a mut bool, checked: &'a mut bool,
text: String, text: WidgetText,
text_color: Option<Color32>,
text_style: Option<TextStyle>,
} }
impl<'a> Checkbox<'a> { impl<'a> Checkbox<'a> {
#[allow(clippy::needless_pass_by_value)] pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
pub fn new(checked: &'a mut bool, text: impl ToString) -> Self {
Checkbox { Checkbox {
checked, checked,
text: text.to_string(), text: text.into(),
text_color: None,
text_style: None,
} }
} }
#[deprecated = "Replaced by: Checkbox::new(RichText::new(text).color(…))"]
pub fn text_color(mut self, text_color: Color32) -> Self { pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color); self.text = self.text.color(text_color);
self self
} }
#[deprecated = "Replaced by: Checkbox::new(RichText::new(text).text_style(…))"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style); self.text = self.text.text_style(text_style);
self self
} }
} }
impl<'a> Widget for Checkbox<'a> { impl<'a> Widget for Checkbox<'a> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Checkbox { let Checkbox { checked, text } = self;
checked,
text,
text_color,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let spacing = &ui.spacing(); let spacing = &ui.spacing();
let icon_width = spacing.icon_width; let icon_width = spacing.icon_width;
@ -260,16 +220,10 @@ 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 wrap_width = select( let wrap_width = ui.available_width() - total_extra.x;
ui.wrap_text(), let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
ui.available_width() - total_extra.x,
f32::INFINITY,
);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let mut desired_size = total_extra + galley.size(); let mut desired_size = total_extra + text.size();
desired_size = desired_size.at_least(spacing.interact_size); desired_size = desired_size.at_least(spacing.interact_size);
desired_size.y = desired_size.y.max(icon_width); desired_size.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
@ -278,14 +232,13 @@ impl<'a> Widget for Checkbox<'a> {
*checked = !*checked; *checked = !*checked;
response.mark_changed(); response.mark_changed();
} }
response response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, text.text()));
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
let text_pos = pos2( let text_pos = pos2(
rect.min.x + button_padding.x + icon_width + icon_spacing, rect.min.x + button_padding.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size().y, rect.center().y - 0.5 * text.size().y,
); );
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape { ui.painter().add(epaint::RectShape {
@ -307,10 +260,7 @@ impl<'a> Widget for Checkbox<'a> {
)); ));
} }
let text_color = text_color text.paint_with_visuals(ui.painter(), text_pos, visuals);
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
response response
} }
} }
@ -336,73 +286,54 @@ impl<'a> Widget for Checkbox<'a> {
/// } /// }
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug)]
pub struct RadioButton { pub struct RadioButton {
checked: bool, checked: bool,
text: String, text: WidgetText,
text_color: Option<Color32>,
text_style: Option<TextStyle>,
} }
impl RadioButton { impl RadioButton {
#[allow(clippy::needless_pass_by_value)] pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
pub fn new(checked: bool, text: impl ToString) -> Self {
Self { Self {
checked, checked,
text: text.to_string(), text: text.into(),
text_color: None,
text_style: None,
} }
} }
#[deprecated = "Replaced by: RadioButton::new(RichText::new(text).color(…))"]
pub fn text_color(mut self, text_color: Color32) -> Self { pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color); self.text = self.text.color(text_color);
self self
} }
#[deprecated = "Replaced by: RadioButton::new(RichText::new(text).text_style(…))"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style); self.text = self.text.text_style(text_style);
self self
} }
} }
impl Widget for RadioButton { impl Widget for RadioButton {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let RadioButton { let RadioButton { checked, text } = self;
checked,
text,
text_color,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let icon_width = ui.spacing().icon_width; let icon_width = ui.spacing().icon_width;
let icon_spacing = ui.spacing().icon_spacing; let icon_spacing = ui.spacing().icon_spacing;
let button_padding = ui.spacing().button_padding; let button_padding = ui.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 wrap_width = select( let wrap_width = ui.available_width() - total_extra.x;
ui.wrap_text(), let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
ui.available_width() - total_extra.x,
f32::INFINITY,
);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let mut desired_size = total_extra + galley.size(); let mut desired_size = total_extra + text.size();
desired_size = desired_size.at_least(ui.spacing().interact_size); desired_size = desired_size.at_least(ui.spacing().interact_size);
desired_size.y = desired_size.y.max(icon_width); desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
response response
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, galley.text())); .widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, text.text()));
let text_pos = pos2( let text_pos = pos2(
rect.min.x + button_padding.x + icon_width + icon_spacing, rect.min.x + button_padding.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size().y, rect.center().y - 0.5 * text.size().y,
); );
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful // let visuals = ui.style().interact_selectable(&response, checked); // too colorful
@ -429,10 +360,7 @@ impl Widget for RadioButton {
}); });
} }
let text_color = text_color text.paint_with_visuals(ui.painter(), text_pos, visuals);
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
painter.galley_with_color(text_pos, galley, text_color);
response response
} }
} }

View file

@ -208,11 +208,12 @@ impl<'a> Widget for DragValue<'a> {
} }
response response
} else { } else {
let button = Button::new(format!("{}{}{}", prefix, value_text, suffix)) let button = Button::new(
.sense(Sense::click_and_drag()) RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
.text_style(TextStyle::Monospace) )
.wrap(false) .wrap(false)
.min_size(ui.spacing().interact_size); // TODO: find some more generic solution to this .sense(Sense::click_and_drag())
.min_size(ui.spacing().interact_size); // TODO: find some more generic solution to `min_size`
let response = ui.add(button); let response = ui.add(button);
let mut response = response.on_hover_cursor(CursorIcon::ResizeHorizontal); let mut response = response.on_hover_cursor(CursorIcon::ResizeHorizontal);

View file

@ -12,7 +12,7 @@ use crate::*;
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Hyperlink { pub struct Hyperlink {
url: String, url: String,
label: Label, text: WidgetText,
} }
impl Hyperlink { impl Hyperlink {
@ -21,41 +21,45 @@ impl Hyperlink {
let url = url.to_string(); let url = url.to_string();
Self { Self {
url: url.clone(), url: url.clone(),
label: Label::new(url).sense(Sense::click()), text: url.into(),
} }
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn from_label_and_url(label: impl Into<Label>, url: impl ToString) -> Self { pub fn from_label_and_url(text: impl Into<WidgetText>, url: impl ToString) -> Self {
Self { Self {
url: url.to_string(), url: url.to_string(),
label: label.into(), text: text.into(),
} }
} }
/// Show some other text than the url #[deprecated = "Use Hyperlink::from_label_and_url instead"]
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn text(mut self, text: impl ToString) -> Self { pub fn text(mut self, text: impl ToString) -> Self {
self.label.text = text.to_string(); self.text = text.to_string().into();
self self
} }
/// The default is [`Style::body_text_style`] (generally [`TextStyle::Body`]). #[deprecated = "Use Hyperlink::from_label_and_url instead"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.label = self.label.text_style(text_style); self.text = self.text.text_style(text_style);
self self
} }
pub fn small(self) -> Self { #[deprecated = "Use Hyperlink::from_label_and_url instead"]
self.text_style(TextStyle::Small) pub fn small(mut self) -> Self {
self.text = self.text.text_style(TextStyle::Small);
self
} }
} }
impl Widget for Hyperlink { impl Widget for Hyperlink {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Hyperlink { url, label } = self; let Hyperlink { url, text } = self;
let (pos, galley, response) = label.layout_in_ui(ui); let label = Label::new(text).sense(Sense::click());
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, galley.text()));
let (pos, text_galley, response) = label.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, text_galley.text()));
if response.hovered() { if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand; ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
@ -85,7 +89,7 @@ impl Widget for Hyperlink {
ui.painter().add(epaint::TextShape { ui.painter().add(epaint::TextShape {
pos, pos,
galley, galley: text_galley.galley,
override_text_color: Some(color), override_text_color: Some(color),
underline, underline,
angle: 0.0, angle: 0.0,

View file

@ -1,9 +1,4 @@
use crate::*; use crate::{widget_text::WidgetTextGalley, *};
use epaint::{
text::{LayoutJob, LayoutSection, TextFormat},
Galley,
};
use std::sync::Arc;
/// Static text. /// Static text.
/// ///
@ -11,135 +6,123 @@ use std::sync::Arc;
/// # let ui = &mut egui::Ui::__test(); /// # let ui = &mut egui::Ui::__test();
/// ui.label("Equivalent"); /// ui.label("Equivalent");
/// ui.add(egui::Label::new("Equivalent")); /// ui.add(egui::Label::new("Equivalent"));
/// ui.add(egui::Label::new("With Options").text_color(egui::Color32::RED)); /// ui.add(egui::Label::new("With Options").wrap(false));
/// ui.label(egui::RichText::new("With formatting").underline());
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Label { pub struct Label {
// TODO: not pub text: WidgetText,
pub(crate) text: String, wrap: Option<bool>,
pub(crate) wrap: Option<bool>,
pub(crate) text_style: Option<TextStyle>,
pub(crate) background_color: Color32,
pub(crate) text_color: Option<Color32>,
code: bool,
strong: bool,
weak: bool,
strikethrough: bool,
underline: bool,
italics: bool,
raised: bool,
sense: Sense, sense: Sense,
} }
impl Label { impl Label {
#[allow(clippy::needless_pass_by_value)] pub fn new(text: impl Into<WidgetText>) -> Self {
pub fn new(text: impl ToString) -> Self {
Self { Self {
text: text.to_string(), text: text.into(),
wrap: None, wrap: None,
text_style: None,
background_color: Color32::TRANSPARENT,
text_color: None,
code: false,
strong: false,
weak: false,
strikethrough: false,
underline: false,
italics: false,
raised: false,
sense: Sense::focusable_noninteractive(), sense: Sense::focusable_noninteractive(),
} }
} }
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
&self.text self.text.text()
} }
/// If `true`, the text will wrap at the `max_width`. /// If `true`, the text will wrap to stay within the max width of the `Ui`.
///
/// By default [`Self::wrap`] will be true in vertical layouts /// By default [`Self::wrap`] will be true in vertical layouts
/// and horizontal layouts with wrapping, /// and horizontal layouts with wrapping,
/// and false on non-wrapping horizontal layouts. /// and false on non-wrapping horizontal layouts.
/// ///
/// Note that any `\n` in the text label will always produce a new line. /// Note that any `\n` in the text will always produce a new line.
#[inline]
pub fn wrap(mut self, wrap: bool) -> Self { pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap); self.wrap = Some(wrap);
self self
} }
/// The default is [`Style::body_text_style`] (generally [`TextStyle::Body`]). #[deprecated = "Replaced by Label::new(RichText::new(…).text_style(…))"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style); self.text = self.text.text_style(text_style);
self self
} }
pub fn heading(self) -> Self { #[deprecated = "Replaced by Label::new(RichText::new(…).heading())"]
self.text_style(TextStyle::Heading) pub fn heading(mut self) -> Self {
self.text = self.text.heading();
self
} }
pub fn monospace(self) -> Self { #[deprecated = "Replaced by Label::new(RichText::new(…).monospace())"]
self.text_style(TextStyle::Monospace) pub fn monospace(mut self) -> Self {
self.text = self.text.monospace();
self
} }
/// Monospace label with gray background #[deprecated = "Replaced by Label::new(RichText::new(…).code())"]
pub fn code(mut self) -> Self { pub fn code(mut self) -> Self {
self.code = true; self.text = self.text.code();
self.text_style(TextStyle::Monospace) self
} }
/// Extra strong text (stronger color). #[deprecated = "Replaced by Label::new(RichText::new(…).strong())"]
pub fn strong(mut self) -> Self { pub fn strong(mut self) -> Self {
self.strong = true; self.text = self.text.strong();
self self
} }
/// Extra weak text (fainter color). #[deprecated = "Replaced by Label::new(RichText::new(…).weak())"]
pub fn weak(mut self) -> Self { pub fn weak(mut self) -> Self {
self.weak = true; self.text = self.text.weak();
self self
} }
/// draw a line under the text #[deprecated = "Replaced by Label::new(RichText::new(…).underline())"]
pub fn underline(mut self) -> Self { pub fn underline(mut self) -> Self {
self.underline = true; self.text = self.text.underline();
self self
} }
/// draw a line through the text, crossing it out #[deprecated = "Replaced by Label::new(RichText::new(…).strikethrough())"]
pub fn strikethrough(mut self) -> Self { pub fn strikethrough(mut self) -> Self {
self.strikethrough = true; self.text = self.text.strikethrough();
self self
} }
/// tilt the characters to the right. #[deprecated = "Replaced by Label::new(RichText::new(…).italics())"]
pub fn italics(mut self) -> Self { pub fn italics(mut self) -> Self {
self.italics = true; self.text = self.text.italics();
self self
} }
/// Smaller text #[deprecated = "Replaced by Label::new(RichText::new(…).small())"]
pub fn small(self) -> Self { pub fn small(mut self) -> Self {
self.text_style(TextStyle::Small) self.text = self.text.small();
self
} }
/// For e.g. exponents #[deprecated = "Replaced by Label::new(RichText::new(…).small_raised())"]
pub fn small_raised(self) -> Self { pub fn small_raised(mut self) -> Self {
self.text_style(TextStyle::Small).raised() self.text = self.text.small_raised();
self
} }
/// Align text to top. Only applicable together with [`Self::small()`]. #[deprecated = "Replaced by Label::new(RichText::new(…).raised())"]
pub fn raised(mut self) -> Self { pub fn raised(mut self) -> Self {
self.raised = true; self.text = self.text.raised();
self self
} }
/// Fill-color behind the text #[deprecated = "Replaced by Label::new(RichText::new(…).background_color(…))"]
pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self { pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
self.background_color = background_color.into(); self.text = self.text.background_color(background_color);
self self
} }
#[deprecated = "Replaced by Label::new(RichText::new(…).text_color())"]
pub fn text_color(mut self, text_color: impl Into<Color32>) -> Self { pub fn text_color(mut self, text_color: impl Into<Color32>) -> Self {
self.text_color = Some(text_color.into()); self.text = self.text.color(text_color);
self self
} }
@ -163,238 +146,119 @@ impl Label {
} }
impl Label { impl Label {
pub fn layout(&self, ui: &Ui) -> Arc<Galley> { /// Do layout and position the galley in the ui, without painting it or adding widget info.
let max_width = ui.available_width(); pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let line_color = self.get_text_color(ui, ui.visuals().text_color()); if let WidgetText::Galley(galley) = self.text {
self.layout_width(ui, max_width, line_color) // If the user said "use this specific galley", then just use it:
} let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
/// `line_color`: used for underline and strikethrough, if any.
pub fn layout_width(&self, ui: &Ui, max_width: f32, line_color: Color32) -> Arc<Galley> {
let (halign, justify) = if ui.is_grid() {
(Align::LEFT, false) // TODO: remove special Grid hacks like these
} else {
(
ui.layout().horizontal_placement(),
ui.layout().horizontal_justify(),
)
};
self.layout_impl(ui, 0.0, max_width, 0.0, line_color, halign, justify)
}
#[allow(clippy::too_many_arguments)]
fn layout_impl(
&self,
ui: &Ui,
leading_space: f32,
max_width: f32,
first_row_min_height: f32,
line_color: Color32,
halign: Align,
justify: bool,
) -> Arc<Galley> {
let text_style = self.text_style_or_default(ui.style());
let wrap_width = if self.should_wrap(ui) {
max_width
} else {
f32::INFINITY
};
let mut background_color = self.background_color;
if self.code {
background_color = ui.visuals().code_bg_color;
}
let underline = if self.underline {
Stroke::new(1.0, line_color)
} else {
Stroke::none()
};
let strikethrough = if self.strikethrough {
Stroke::new(1.0, line_color)
} else {
Stroke::none()
};
let valign = if self.raised {
Align::TOP
} else {
ui.layout().vertical_align()
};
let job = LayoutJob {
text: self.text.clone(), // TODO: avoid clone
sections: vec![LayoutSection {
leading_space,
byte_range: 0..self.text.len(),
format: TextFormat {
style: text_style,
color: Color32::TEMPORARY_COLOR,
background: background_color,
italics: self.italics,
underline,
strikethrough,
valign,
},
}],
wrap_width,
first_row_min_height,
halign,
justify,
..Default::default()
};
ui.fonts().layout_job(job)
}
/// `has_focus`: the item is selected with the keyboard, so highlight with underline.
/// `response_color`: Unless we have a special color set, use this.
pub(crate) fn paint_galley(
&self,
ui: &mut Ui,
pos: Pos2,
galley: Arc<Galley>,
has_focus: bool,
response_color: Color32,
) {
let text_color = self.get_text_color(ui, response_color);
let underline = if has_focus {
Stroke::new(1.0, text_color)
} else {
Stroke::none()
};
ui.painter().add(epaint::TextShape {
pos,
galley,
override_text_color: Some(text_color),
underline,
angle: 0.0,
});
}
/// `response_color`: Unless we have a special color set, use this.
fn get_text_color(&self, ui: &Ui, response_color: Color32) -> Color32 {
if let Some(text_color) = self.text_color {
text_color
} else if self.strong {
ui.visuals().strong_text_color()
} else if self.weak {
ui.visuals().weak_text_color()
} else {
response_color
}
}
pub fn font_height(&self, fonts: &epaint::text::Fonts, style: &Style) -> f32 {
let text_style = self.text_style_or_default(style);
fonts.row_height(text_style)
}
// TODO: this should return a LabelLayout which has a paint method.
// We can then split Widget::Ui in two: layout + allocating space, and painting.
// this allows us to assemble labels, THEN detect interaction, THEN chose color style based on that.
// pub fn layout(self, ui: &mut ui) -> LabelLayout { }
// TODO: a paint method for painting anywhere in a ui.
// This should be the easiest method of putting text anywhere.
/// Read the text style, or get the default for the current style
pub fn text_style_or_default(&self, style: &Style) -> TextStyle {
self.text_style
.or(style.override_text_style)
.unwrap_or(style.body_text_style)
}
fn should_wrap(&self, ui: &Ui) -> bool {
self.wrap.or(ui.style().wrap).unwrap_or_else(|| {
if let Some(grid) = ui.grid() {
grid.wrap_text()
} else {
let layout = ui.layout();
layout.is_vertical() || layout.is_horizontal() && layout.main_wrap()
}
})
}
/// Do layout and place the galley in the ui, without painting it or adding widget info.
pub(crate) fn layout_in_ui(&self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
let sense = self.sense;
let max_width = ui.available_width();
if self.should_wrap(ui)
&& ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap()
&& max_width.is_finite()
{
// On a wrapping horizontal layout we want text to start after the previous widget,
// then continue on the line below! This will take some extra work:
let cursor = ui.cursor();
let first_row_indentation = max_width - ui.available_size_before_wrap().x;
egui_assert!(first_row_indentation.is_finite());
let first_row_min_height = cursor.height();
let default_color = self.get_text_color(ui, ui.visuals().text_color());
let halign = Align::Min;
let justify = false;
let galley = self.layout_impl(
ui,
first_row_indentation,
max_width,
first_row_min_height,
default_color,
halign,
justify,
);
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(!galley.rows.is_empty(), "Galleys are never empty");
// collect a response from many rows:
let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y));
let mut response = ui.allocate_rect(rect, sense);
for row in galley.rows.iter().skip(1) {
let rect = row.rect.translate(vec2(pos.x, pos.y));
response |= ui.allocate_rect(rect, sense);
}
(pos, galley, response)
} else {
let galley = self.layout(ui);
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
let pos = match galley.job.halign { let pos = match galley.job.halign {
Align::LEFT => rect.left_top(), Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(), Align::Center => rect.center_top(),
Align::RIGHT => rect.right_top(), Align::RIGHT => rect.right_top(),
}; };
(pos, galley, response) let text_galley = WidgetTextGalley {
galley,
galley_has_color: true,
};
return (pos, text_galley, response);
}
let valign = ui.layout().vertical_align();
let mut text_job = self.text.into_text_job(ui.style(), TextStyle::Body, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width();
if should_wrap
&& ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap()
&& available_width.is_finite()
{
// On a wrapping horizontal layout we want text to start after the previous widget,
// then continue on the line below! This will take some extra work:
let cursor = ui.cursor();
let first_row_indentation = available_width - ui.available_size_before_wrap().x;
egui_assert!(first_row_indentation.is_finite());
text_job.job.wrap_width = available_width;
text_job.job.first_row_min_height = cursor.height();
text_job.job.halign = Align::Min;
text_job.job.justify = false;
if let Some(first_section) = text_job.job.sections.first_mut() {
first_section.leading_space = first_row_indentation;
}
let text_galley = text_job.into_galley(ui.fonts());
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(
!text_galley.galley.rows.is_empty(),
"Galleys are never empty"
);
// collect a response from many rows:
let rect = text_galley.galley.rows[0]
.rect
.translate(vec2(pos.x, pos.y));
let mut response = ui.allocate_rect(rect, self.sense);
for row in text_galley.galley.rows.iter().skip(1) {
let rect = row.rect.translate(vec2(pos.x, pos.y));
response |= ui.allocate_rect(rect, self.sense);
}
(pos, text_galley, response)
} else {
if should_wrap {
text_job.job.wrap_width = available_width;
} else {
text_job.job.wrap_width = f32::INFINITY;
};
if ui.is_grid() {
// TODO: remove special Grid hacks like these
text_job.job.halign = Align::LEFT;
text_job.job.justify = false;
} else {
text_job.job.halign = ui.layout().horizontal_placement();
text_job.job.justify = ui.layout().horizontal_justify();
};
let text_galley = text_job.into_galley(ui.fonts());
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.sense);
let pos = match text_galley.galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),
Align::RIGHT => rect.right_top(),
};
(pos, text_galley, response)
} }
} }
} }
impl Widget for Label { impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let (pos, galley, response) = self.layout_in_ui(ui); let (pos, text_galley, response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text())); response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));
let response_color = ui.style().interact(&response).text_color(); let response_color = ui.style().interact(&response).text_color();
self.paint_galley(ui, pos, galley, response.has_focus(), response_color);
let underline = if response.has_focus() {
Stroke::new(1.0, response_color)
} else {
Stroke::none()
};
let override_text_color = if text_galley.galley_has_color {
None
} else {
Some(response_color)
};
ui.painter().add(epaint::TextShape {
pos,
galley: text_galley.galley,
override_text_color,
underline,
angle: 0.0,
});
response response
} }
} }
impl From<&str> for Label {
fn from(s: &str) -> Label {
Label::new(s)
}
}
impl From<&String> for Label {
fn from(s: &String) -> Label {
Label::new(s)
}
}
impl From<String> for Label {
fn from(s: String) -> Label {
Label::new(s)
}
}

View file

@ -1,7 +1,7 @@
use crate::*; use crate::*;
enum ProgressBarText { enum ProgressBarText {
Custom(String), Custom(WidgetText),
Percentage, Percentage,
} }
@ -31,9 +31,8 @@ impl ProgressBar {
} }
/// A custom text to display on the progress bar. /// A custom text to display on the progress bar.
#[allow(clippy::needless_pass_by_value)] pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
pub fn text(mut self, text: impl ToString) -> Self { self.text = Some(ProgressBarText::Custom(text.into()));
self.text = Some(ProgressBarText::Custom(text.to_string()));
self self
} }
@ -124,17 +123,19 @@ impl Widget for ProgressBar {
if let Some(text_kind) = text { if let Some(text_kind) = text {
let text = match text_kind { let text = match text_kind {
ProgressBarText::Custom(string) => string, ProgressBarText::Custom(text) => text,
ProgressBarText::Percentage => format!("{}%", (progress * 100.0) as usize), ProgressBarText::Percentage => format!("{}%", (progress * 100.0) as usize).into(),
}; };
ui.painter().sub_region(outer_rect).text( let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
outer_rect.left_center() + vec2(ui.spacing().item_spacing.x, 0.0), let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0)
Align2::LEFT_CENTER, + vec2(ui.spacing().item_spacing.x, 0.0);
text, let text_color = visuals
TextStyle::Button, .override_text_color
visuals .unwrap_or(visuals.selection.stroke.color);
.override_text_color galley.paint_with_fallback_color(
.unwrap_or(visuals.selection.stroke.color), &ui.painter().sub_region(outer_rect),
text_pos,
text_color,
); );
} }

View file

@ -21,64 +21,46 @@ use crate::*;
/// } /// }
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug)]
pub struct SelectableLabel { pub struct SelectableLabel {
selected: bool, selected: bool,
text: String, text: WidgetText,
text_style: Option<TextStyle>,
} }
impl SelectableLabel { impl SelectableLabel {
#[allow(clippy::needless_pass_by_value)] pub fn new(selected: bool, text: impl Into<WidgetText>) -> Self {
pub fn new(selected: bool, text: impl ToString) -> Self {
Self { Self {
selected, selected,
text: text.to_string(), text: text.into(),
text_style: None,
} }
} }
#[deprecated = "Replaced by: Button::new(RichText::new(text).text_style(…))"]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style); self.text = self.text.text_style(text_style);
self self
} }
} }
impl Widget for SelectableLabel { impl Widget for SelectableLabel {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Self { let Self { selected, text } = self;
selected,
text,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let button_padding = ui.spacing().button_padding; let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding; let total_extra = button_padding + button_padding;
let wrap_width = if ui.wrap_text() { let wrap_width = ui.available_width() - total_extra.x;
ui.available_width() - total_extra.x let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
} else {
f32::INFINITY
};
let galley = ui let mut desired_size = total_extra + text.size();
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let mut desired_size = total_extra + galley.size();
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click()); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
response.widget_info(|| { response.widget_info(|| {
WidgetInfo::selected(WidgetType::SelectableLabel, selected, galley.text()) WidgetInfo::selected(WidgetType::SelectableLabel, selected, text.text())
}); });
let text_pos = ui let text_pos = ui
.layout() .layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding)) .align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min; .min;
let visuals = ui.style().interact_selectable(&response, selected); let visuals = ui.style().interact_selectable(&response, selected);
@ -91,12 +73,7 @@ impl Widget for SelectableLabel {
.rect(rect, corner_radius, visuals.bg_fill, visuals.bg_stroke); .rect(rect, corner_radius, visuals.bg_fill, visuals.bg_stroke);
} }
let text_color = ui text.paint_with_visuals(ui.painter(), text_pos, &visuals);
.style()
.visuals
.override_text_color
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
response response
} }
} }

View file

@ -1,8 +1,9 @@
#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString` #![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString`
use crate::{widgets::Label, *};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use crate::*;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Combined into one function (rather than two) to make it easier /// Combined into one function (rather than two) to make it easier
@ -356,7 +357,8 @@ impl<'a> Slider<'a> {
fn label_ui(&mut self, ui: &mut Ui) { fn label_ui(&mut self, ui: &mut Ui) {
if !self.text.is_empty() { if !self.text.is_empty() {
let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color());
ui.add(Label::new(&self.text).wrap(false).text_color(text_color)); let text = RichText::new(&self.text).color(text_color);
ui.add(Label::new(text).wrap(false));
} }
} }

View file

@ -45,7 +45,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct TextEdit<'t> { pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer, text: &'t mut dyn TextBuffer,
hint_text: String, hint_text: WidgetText,
id: Option<Id>, id: Option<Id>,
id_source: Option<Id>, id_source: Option<Id>,
text_style: Option<TextStyle>, text_style: Option<TextStyle>,
@ -127,9 +127,8 @@ impl<'t> TextEdit<'t> {
} }
/// Show a faint hint text when the text field is empty. /// Show a faint hint text when the text field is empty.
#[allow(clippy::needless_pass_by_value)] pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
pub fn hint_text(mut self, hint_text: impl ToString) -> Self { self.hint_text = hint_text.into();
self.hint_text = hint_text.to_string();
self self
} }
@ -512,12 +511,12 @@ impl<'t> TextEdit<'t> {
if text.as_ref().is_empty() && !hint_text.is_empty() { if text.as_ref().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color(); let hint_text_color = ui.visuals().weak_text_color();
let galley = ui.fonts().layout_job(if multiline { let galley = if multiline {
LayoutJob::simple(hint_text, text_style, hint_text_color, desired_size.x) hint_text.into_galley(ui, Some(true), desired_size.x, text_style)
} else { } else {
LayoutJob::simple_singleline(hint_text, text_style, hint_text_color) hint_text.into_galley(ui, Some(false), f32::INFINITY, text_style)
}); };
painter.galley(response.rect.min, galley); galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
} }
if ui.memory().has_focus(id) { if ui.memory().has_focus(id) {

View file

@ -42,13 +42,10 @@ impl super::View for FontBook {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.label("You can add more characters by installing additional fonts with "); ui.label("You can add more characters by installing additional fonts with ");
ui.add( ui.add(egui::Hyperlink::from_label_and_url(
egui::Hyperlink::from_label_and_url( egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
"Context::set_fonts", "https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts", ));
)
.text_style(egui::TextStyle::Monospace),
);
ui.label("."); ui.label(".");
}); });
@ -90,10 +87,13 @@ impl super::View for FontBook {
for (&chr, name) in named_chars { for (&chr, name) in named_chars {
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() { if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
let button = egui::Button::new(chr).text_style(text_style).frame(false); let button = egui::Button::new(
egui::RichText::new(chr.to_string()).text_style(text_style),
)
.frame(false);
let tooltip_ui = |ui: &mut egui::Ui| { let tooltip_ui = |ui: &mut egui::Ui| {
ui.add(egui::Label::new(chr).text_style(text_style)); ui.label(egui::RichText::new(chr.to_string()).text_style(text_style));
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32)); ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
}; };

View file

@ -142,7 +142,7 @@ impl Widgets {
// Trick so we don't have to add spaces in the text below: // Trick so we don't have to add spaces in the text below:
ui.spacing_mut().item_spacing.x = ui.fonts()[TextStyle::Body].glyph_width(' '); ui.spacing_mut().item_spacing.x = ui.fonts()[TextStyle::Body].glyph_width(' ');
ui.add(Label::new("Text can have").text_color(Color32::from_rgb(110, 255, 110))); ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text( ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.", "This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
@ -365,7 +365,7 @@ impl SubTree {
) -> Action { ) -> Action {
if depth > 0 if depth > 0
&& ui && ui
.add(Button::new("delete").text_color(Color32::RED)) .button(RichText::new("delete").color(Color32::RED))
.clicked() .clicked()
{ {
return Action::Delete; return Action::Delete;
@ -565,12 +565,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
}, },
); );
job.wrap_width = ui.available_width(); ui.label(job);
let galley = ui.fonts().layout_job(job);
let (response, painter) = ui.allocate_painter(galley.size(), Sense::hover());
painter.add(Shape::galley(response.rect.min, galley));
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add(crate::__egui_github_link_file_line!()); ui.add(crate::__egui_github_link_file_line!());

View file

@ -465,7 +465,7 @@ fn lorem_ipsum(ui: &mut egui::Ui, text: &str) {
ui.with_layout( ui.with_layout(
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true), egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|ui| { |ui| {
ui.add(egui::Label::new(text).weak()); ui.label(egui::RichText::new(text).weak());
}, },
); );
} }

View file

@ -84,7 +84,7 @@ fn lorem_ipsum(ui: &mut egui::Ui) {
ui.with_layout( ui.with_layout(
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true), egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|ui| { |ui| {
ui.add(egui::Label::new(crate::LOREM_IPSUM_LONG).small().weak()); ui.label(egui::RichText::new(crate::LOREM_IPSUM_LONG).small().weak());
}, },
); );
} }

View file

@ -116,9 +116,9 @@ impl epi::App for HttpApp {
} }
Err(error) => { Err(error) => {
// This should only happen if the fetch API isn't available or something similar. // This should only happen if the fetch API isn't available or something similar.
ui.add( ui.colored_label(
egui::Label::new(if error.is_empty() { "Error" } else { error }) egui::Color32::RED,
.text_color(egui::Color32::RED), if error.is_empty() { "Error" } else { error },
); );
} }
} }

View file

@ -39,10 +39,10 @@ pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
} }
easy_mark::Item::Text(style, text) => { easy_mark::Item::Text(style, text) => {
ui.add(label_from_style(text, &style)); ui.label(rich_text_from_style(text, &style));
} }
easy_mark::Item::Hyperlink(style, text, url) => { easy_mark::Item::Hyperlink(style, text, url) => {
let label = label_from_style(text, &style); let label = rich_text_from_style(text, &style);
ui.add(Hyperlink::from_label_and_url(label, url)); ui.add(Hyperlink::from_label_and_url(label, url));
} }
@ -87,7 +87,7 @@ pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
}; };
} }
fn label_from_style(text: &str, style: &easy_mark::Style) -> Label { fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
let easy_mark::Style { let easy_mark::Style {
heading, heading,
quoted, quoted,
@ -102,34 +102,34 @@ fn label_from_style(text: &str, style: &easy_mark::Style) -> Label {
let small = small || raised; // Raised text is also smaller let small = small || raised; // Raised text is also smaller
let mut label = Label::new(text); let mut rich_text = RichText::new(text);
if heading && !small { if heading && !small {
label = label.heading().strong(); rich_text = rich_text.heading().strong();
} }
if small && !heading { if small && !heading {
label = label.small(); rich_text = rich_text.small();
} }
if code { if code {
label = label.code(); rich_text = rich_text.code();
} }
if strong { if strong {
label = label.strong(); rich_text = rich_text.strong();
} else if quoted { } else if quoted {
label = label.weak(); rich_text = rich_text.weak();
} }
if underline { if underline {
label = label.underline(); rich_text = rich_text.underline();
} }
if strikethrough { if strikethrough {
label = label.strikethrough(); rich_text = rich_text.strikethrough();
} }
if italics { if italics {
label = label.italics(); rich_text = rich_text.italics();
} }
if raised { if raised {
label = label.raised(); rich_text = rich_text.raised();
} }
label rich_text
} }
fn bullet_point(ui: &mut Ui, width: f32) -> Response { fn bullet_point(ui: &mut Ui, width: f32) -> Response {

View file

@ -105,7 +105,10 @@ macro_rules! __egui_github_link_file {
crate::__egui_github_link_file!("(source code)") crate::__egui_github_link_file!("(source code)")
}; };
($label: expr) => { ($label: expr) => {
egui::github_link_file!("https://github.com/emilk/egui/blob/master/", $label).small() egui::github_link_file!(
"https://github.com/emilk/egui/blob/master/",
egui::RichText::new($label).small()
)
}; };
} }
@ -117,7 +120,10 @@ macro_rules! __egui_github_link_file_line {
crate::__egui_github_link_file_line!("(source code)") crate::__egui_github_link_file_line!("(source code)")
}; };
($label: expr) => { ($label: expr) => {
egui::github_link_file_line!("https://github.com/emilk/egui/blob/master/", $label).small() egui::github_link_file_line!(
"https://github.com/emilk/egui/blob/master/",
egui::RichText::new($label).small()
)
}; };
} }

View file

@ -237,5 +237,5 @@ fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Respons
(time % 1.0 * 100.0).floor() (time % 1.0 * 100.0).floor()
); );
ui.add(egui::Button::new(time).text_style(egui::TextStyle::Monospace)) ui.button(egui::RichText::new(time).monospace())
} }

View file

@ -121,7 +121,7 @@ impl EguiGlium {
&self.egui_ctx &self.egui_ctx
} }
/// useful for calling e.g. [`crate::Painter::register_glium_texture`]. /// useful for calling e.g. [`crate::Painter::alloc_user_texture`].
pub fn painter_mut(&mut self) -> &mut crate::Painter { pub fn painter_mut(&mut self) -> &mut crate::Painter {
&mut self.painter &mut self.painter
} }

View file

@ -37,6 +37,9 @@ use emath::*;
/// }, /// },
/// ); /// );
/// ``` /// ```
///
/// As you can see, constructing a `LayoutJob` is currently a lot of work.
/// It would be nice to have a helper macro for it!
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct LayoutJob { pub struct LayoutJob {
@ -118,6 +121,21 @@ impl LayoutJob {
} }
} }
#[inline]
pub fn single_section(text: String, format: TextFormat) -> Self {
Self {
sections: vec![LayoutSection {
leading_space: 0.0,
byte_range: 0..text.len(),
format,
}],
text,
wrap_width: f32::INFINITY,
break_on_newline: true,
..Default::default()
}
}
#[inline(always)] #[inline(always)]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.sections.is_empty() self.sections.is_empty()
@ -134,6 +152,15 @@ impl LayoutJob {
format, format,
}); });
} }
/// The height of the tallest used font in the job.
pub fn font_height(&self, fonts: &crate::Fonts) -> f32 {
let mut max_height = 0.0_f32;
for section in &self.sections {
max_height = max_height.max(fonts.row_height(section.format.style));
}
max_height
}
} }
impl std::hash::Hash for LayoutJob { impl std::hash::Hash for LayoutJob {