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 ⭐
* 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)).
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
### 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)).
* `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 🙏
* [mankinskin](https://github.com/mankinskin) ([#543](https://github.com/emilk/egui/pull/543))

View file

@ -1,6 +1,6 @@
use std::hash::Hash;
use crate::{widgets::Label, *};
use crate::*;
use epaint::{Shape, TextStyle};
#[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()"]
pub struct CollapsingHeader {
label: Label,
text: WidgetText,
default_open: bool,
id_source: Id,
enabled: bool,
@ -157,11 +157,11 @@ impl CollapsingHeader {
/// If the label is unique and static this is fine,
/// 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`].
pub fn new(label: impl ToString) -> Self {
let label = Label::new(label).wrap(false);
let id_source = Id::new(label.text());
pub fn new(text: impl Into<WidgetText>) -> Self {
let text = text.into();
let id_source = Id::new(text.text());
Self {
label,
text,
default_open: false,
id_source,
enabled: true,
@ -185,10 +185,9 @@ impl CollapsingHeader {
self
}
/// By default, the `CollapsingHeader` text style is `TextStyle::Button`.
/// Call `.text_style(style)` to change this.
#[deprecated = "Replaced by: CollapsingHeader::new(RichText::new(text).text_style(…))"]
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
}
@ -252,7 +251,7 @@ impl CollapsingHeader {
"Horizontal collapsing is unimplemented"
);
let Self {
mut label,
text,
default_open,
id_source,
enabled: _,
@ -261,11 +260,6 @@ impl CollapsingHeader {
show_background: _,
} = 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.
let id = ui.make_persistent_id(id_source);
@ -273,23 +267,24 @@ impl CollapsingHeader {
let available = ui.available_rect_before_wrap();
let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
let galley =
label.layout_width(ui, available.right() - text_pos.x, Color32::TEMPORARY_COLOR);
let text_max_x = text_pos.x + galley.size().x;
let wrap_width = available.right() - text_pos.x;
let wrap = Some(false);
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();
if ui.visuals().collapsing_header_frame {
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);
let (_, rect) = ui.allocate_space(desired_size);
let mut header_response = ui.interact(rect, id, Sense::click());
let text_pos = pos2(
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);
@ -298,16 +293,11 @@ impl CollapsingHeader {
header_response.mark_changed();
}
header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text()));
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text()));
let visuals = ui
.style()
.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 {
ui.painter().add(epaint::RectShape {
@ -343,7 +333,7 @@ impl CollapsingHeader {
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 {
id,

View file

@ -1,8 +1,6 @@
use crate::{style::WidgetVisuals, *};
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.
///
/// ```
@ -10,7 +8,7 @@ use epaint::Shape;
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// # let mut ui = &mut egui::Ui::__test();
/// egui::ComboBox::from_label( "Select one!")
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
/// ui.selectable_value(&mut selected, Enum::First, "First");
@ -22,14 +20,14 @@ use epaint::Shape;
#[must_use = "You should call .show*"]
pub struct ComboBox {
id_source: Id,
label: Option<Label>,
selected_text: String,
label: Option<WidgetText>,
selected_text: WidgetText,
width: Option<f32>,
}
impl ComboBox {
/// 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();
Self {
id_source: Id::new(label.text()),
@ -56,9 +54,8 @@ impl ComboBox {
}
/// What we show as the currently selected value
#[allow(clippy::needless_pass_by_value)]
pub fn selected_text(mut self, selected_text: impl ToString) -> Self {
self.selected_text = selected_text.to_string();
pub fn selected_text(mut self, selected_text: impl Into<WidgetText>) -> Self {
self.selected_text = selected_text.into();
self
}
@ -95,7 +92,7 @@ impl ComboBox {
if let Some(label) = label {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
ir.response |= ui.add(label);
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ""));
@ -115,7 +112,7 @@ impl ComboBox {
/// # let mut ui = &mut egui::Ui::__test();
/// let alternatives = ["a", "b", "c", "d"];
/// let mut selected = 2;
/// egui::ComboBox::from_label( "Select one!").show_index(
/// egui::ComboBox::from_label("Select one!").show_index(
/// ui,
/// &mut selected,
/// alternatives.len(),
@ -151,11 +148,10 @@ impl ComboBox {
}
}
#[allow(clippy::needless_pass_by_value)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,
selected: impl ToString,
selected_text: WidgetText,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
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 icon_size = Vec2::splat(ui.spacing().icon_width);
let galley =
ui.fonts()
.layout_delayed_color(selected.to_string(), TextStyle::Button, f32::INFINITY);
let galley = selected_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
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);
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
ui.painter()
.galley_with_color(text_rect.min, galley, visuals.text_color());
galley.paint_with_visuals(ui.painter(), text_rect.min, visuals);
});
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");
/// }
/// ```
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| {
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.
use crate::{widgets::*, *};
use crate::{widget_text::WidgetTextGalley, *};
use epaint::*;
use super::*;
@ -23,7 +23,7 @@ use super::*;
/// });
#[must_use = "You should call .show()"]
pub struct Window<'open> {
title_label: Label,
title: WidgetText,
open: Option<&'open mut bool>,
area: Area,
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.
/// 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.
#[allow(clippy::needless_pass_by_value)]
pub fn new(title: impl ToString) -> Self {
let title = title.to_string();
let area = Area::new(&title);
let title_label = Label::new(title).text_style(TextStyle::Heading).wrap(false);
pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(title.text());
Self {
title_label,
title,
open: None,
area,
frame: None,
@ -250,7 +248,7 @@ impl<'open> Window<'open> {
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<InnerResponse<Option<R>>> {
let Window {
title_label,
title,
open,
area,
frame,
@ -299,7 +297,7 @@ impl<'open> Window<'open> {
.and_then(|window_interaction| {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
title_label.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing
title.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing
} else {
0.0
};
@ -336,7 +334,7 @@ impl<'open> Window<'open> {
let title_bar = if with_title_bar {
let title_bar = show_title_bar(
&mut frame.content_ui,
title_label,
title,
show_close_button,
collapsing_id,
&mut collapsing,
@ -745,22 +743,21 @@ fn paint_frame_interaction(
struct TitleBar {
id: Id,
title_label: Label,
title_galley: std::sync::Arc<Galley>,
title_galley: WidgetTextGalley,
min_rect: Rect,
rect: Rect,
}
fn show_title_bar(
ui: &mut Ui,
title_label: Label,
title: WidgetText,
show_close_button: bool,
collapsing_id: Id,
collapsing: &mut collapsing_header::State,
collapsible: bool,
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = title_label
let height = title
.font_height(ui.fonts(), ui.style())
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);
@ -782,7 +779,7 @@ fn show_title_bar(
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 {
// 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 {
id,
title_label,
title_galley,
min_rect,
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 text_pos =
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_color = ui.visuals().text_color();
self.title_label
.paint_galley(ui, text_pos, self.title_galley, false, text_color);
self.title_galley.paint_with_fallback_color(
ui.painter(),
text_pos,
ui.visuals().text_color(),
);
if let Some(content_response) = &content_response {
// paint separator between title and content:

View file

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

View file

@ -367,6 +367,7 @@ mod sense;
pub mod style;
mod ui;
pub mod util;
mod widget_text;
pub mod widgets;
pub use epaint;
@ -408,6 +409,7 @@ pub use {
style::{Style, Visuals},
text::{Galley, TextFormat},
ui::Ui,
widget_text::{RichText, WidgetText},
widgets::*,
};
@ -416,10 +418,10 @@ pub use {
/// Helper function that adds a label when compiling with debug assertions enabled.
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if cfg!(debug_assertions) {
ui.add(
crate::Label::new("‼ Debug build ‼")
ui.label(
RichText::new("‼ Debug build ‼")
.small()
.text_color(crate::Color32::RED),
.color(crate::Color32::RED),
)
.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 {
($github_url: expr, $label: expr) => {{
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 {
($github_url: expr, $label: expr) => {{
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.
pub fn menu_button<R>(
ui: &mut Ui,
title: impl ToString,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents))
@ -103,18 +103,17 @@ pub fn menu_button<R>(
pub(crate) fn submenu_button<R>(
ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>,
title: impl ToString,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents)
}
/// wrapper for the contents of every menu.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn menu_ui<'c, R>(
ctx: &CtxRef,
menu_id: impl std::hash::Hash,
menu_state_arc: Arc<RwLock<MenuState>>,
menu_state_arc: &Arc<RwLock<MenuState>>,
mut style: Style,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
@ -152,15 +151,14 @@ pub(crate) fn menu_ui<'c, R>(
}
/// build a top level menu with a button
#[allow(clippy::needless_pass_by_value)]
fn stationary_menu_impl<'c, R>(
ui: &mut Ui,
title: impl ToString,
title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let title = title.to_string();
let title = title.into();
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);
@ -372,20 +370,20 @@ impl MenuResponse {
}
}
pub struct SubMenuButton {
text: String,
icon: String,
text: WidgetText,
icon: WidgetText,
index: usize,
}
impl SubMenuButton {
/// 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 ToString, icon: impl ToString, index: usize) -> Self {
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
Self {
text: text.to_string(),
icon: icon.to_string(),
text: text.into(),
icon: icon.into(),
index,
}
}
fn visuals<'a>(
ui: &'a Ui,
response: &'_ Response,
@ -398,11 +396,12 @@ impl SubMenuButton {
ui.style().interact(response)
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn icon(mut self, icon: impl ToString) -> Self {
self.icon = icon.to_string();
pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.into();
self
}
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let SubMenuButton { text, icon, .. } = self;
@ -412,14 +411,10 @@ impl SubMenuButton {
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = ui
.fonts()
.layout_delayed_color(text, text_style, text_available_width);
let text_galley = text.into_galley(ui, Some(true), text_available_width, text_style);
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = ui
.fonts()
.layout_delayed_color(icon, text_style, icon_available_width);
let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style);
let text_and_icon_size = Vec2::new(
text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
@ -447,10 +442,8 @@ impl SubMenuButton {
);
let text_color = visuals.text_color();
ui.painter()
.galley_with_color(text_pos, text_galley, text_color);
ui.painter()
.galley_with_color(icon_pos, icon_galley, text_color);
text_galley.paint_with_fallback_color(ui.painter(), text_pos, text_color);
icon_galley.paint_with_fallback_color(ui.painter(), icon_pos, text_color);
}
response
}
@ -460,14 +453,14 @@ pub struct SubMenu {
parent_state: Arc<RwLock<MenuState>>,
}
impl SubMenu {
#[allow(clippy::needless_pass_by_value)]
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl ToString) -> Self {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
let index = parent_state.write().next_entry_index();
Self {
button: SubMenuButton::new(text, "", index),
parent_state,
}
}
pub fn show<R>(
self,
ui: &mut Ui,
@ -522,8 +515,7 @@ impl MenuState {
},
..Default::default()
};
let menu_state_arc = menu_state.clone();
crate::menu::menu_ui(ctx, id, menu_state_arc, style, add_contents)
crate::menu::menu_ui(ctx, id, menu_state, style, add_contents)
}
fn show_submenu<R>(
&mut self,

View file

@ -1,8 +1,7 @@
use crate::{
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.
#[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| {
ui.add(crate::widgets::Label::new(text));
})
}
/// 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| {
ui.add(crate::widgets::Label::new(text));
})

View file

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

View file

@ -283,6 +283,7 @@ impl Ui {
}
/// Should text wrap in this `Ui`?
///
/// This is determined first by [`Style::wrap`], and then by the layout of this `Ui`.
pub fn wrap_text(&self) -> bool {
if let Some(wrap) = self.style.wrap {
@ -290,8 +291,8 @@ impl Ui {
} else if let Some(grid) = self.placer.grid() {
grid.wrap_text()
} else {
// In vertical layouts we wrap text, but in horizontal we keep going.
self.layout().is_vertical()
let layout = self.layout();
layout.is_vertical() || layout.is_horizontal() && layout.main_wrap()
}
}
@ -990,50 +991,54 @@ impl Ui {
///
/// See also [`Label`].
#[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)
}
/// Show colored text.
///
/// Shortcut for `add(Label::new(text).text_color(color))`
pub fn colored_label(&mut self, color: impl Into<Color32>, text: impl ToString) -> Response {
Label::new(text).text_color(color).ui(self)
/// Shortcut for `ui.label(RichText::new(text).color(color))`
pub fn colored_label(
&mut self,
color: impl Into<Color32>,
text: impl Into<RichText>,
) -> Response {
Label::new(text.into().color(color)).ui(self)
}
/// Show large text.
///
/// Shortcut for `add(Label::new(text).heading())`
pub fn heading(&mut self, text: impl ToString) -> Response {
Label::new(text).heading().ui(self)
/// Shortcut for `ui.label(RichText::new(text).heading())`
pub fn heading(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text.into().heading()).ui(self)
}
/// Show monospace (fixed width) text.
///
/// Shortcut for `add(Label::new(text).monospace())`
pub fn monospace(&mut self, text: impl ToString) -> Response {
Label::new(text).monospace().ui(self)
/// Shortcut for `ui.label(RichText::new(text).monospace())`
pub fn monospace(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text.into().monospace()).ui(self)
}
/// Show text as monospace with a gray background.
///
/// Shortcut for `add(Label::new(text).code())`
pub fn code(&mut self, text: impl ToString) -> Response {
Label::new(text).code().ui(self)
/// Shortcut for `ui.label(RichText::new(text).code())`
pub fn code(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text.into().code()).ui(self)
}
/// Show small text.
///
/// Shortcut for `add(Label::new(text).small())`
pub fn small(&mut self, text: impl ToString) -> Response {
Label::new(text).small().ui(self)
/// Shortcut for `ui.label(RichText::new(text).small())`
pub fn small(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text.into().small()).ui(self)
}
/// Show text that stand out a bit (e.g. slightly brighter).
///
/// Shortcut for `add(Label::new(text).strong())`
pub fn strong(&mut self, text: impl ToString) -> Response {
Label::new(text).strong().ui(self)
/// Shortcut for `ui.label(RichText::new(text).strong())`
pub fn strong(&mut self, text: impl Into<RichText>) -> Response {
Label::new(text.into().strong()).ui(self)
}
/// Shortcut for `add(Hyperlink::new(url))`
@ -1051,8 +1056,8 @@ impl Ui {
/// ```
///
/// See also [`Hyperlink`].
pub fn hyperlink_to(&mut self, label: impl ToString, url: impl ToString) -> Response {
Hyperlink::new(url).text(label).ui(self)
pub fn hyperlink_to(&mut self, label: impl Into<WidgetText>, url: impl ToString) -> Response {
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`).
@ -1089,9 +1094,21 @@ impl Ui {
/// Shortcut for `add(Button::new(text))`
///
/// 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() { … } "]
#[inline(always)]
pub fn button(&mut self, text: impl ToString) -> Response {
#[inline]
pub fn button(&mut self, text: impl Into<WidgetText>) -> Response {
Button::new(text).ui(self)
}
@ -1101,19 +1118,21 @@ impl Ui {
///
/// Shortcut for `add(Button::new(text).small())`
#[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)
}
/// 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)
}
/// Show a [`RadioButton`].
/// Often you want to use [`Self::radio_value`] instead.
#[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)
}
@ -1134,11 +1153,12 @@ impl Ui {
/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
/// my_enum = Enum::First
/// }
/// ```
pub fn radio_value<Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
text: impl ToString,
text: impl Into<WidgetText>,
) -> Response {
let mut response = self.radio(*current_value == selected_value, text);
if response.clicked() {
@ -1152,7 +1172,7 @@ impl Ui {
///
/// See also [`SelectableLabel`].
#[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)
}
@ -1166,7 +1186,7 @@ impl Ui {
&mut self,
current_value: &mut Value,
selected_value: Value,
text: impl ToString,
text: impl Into<WidgetText>,
) -> Response {
let mut response = self.selectable_label(*current_value == selected_value, text);
if response.clicked() {
@ -1373,7 +1393,7 @@ impl Ui {
/// A [`CollapsingHeader`] that starts out collapsed.
pub fn collapsing<R>(
&mut self,
heading: impl ToString,
heading: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
CollapsingHeader::new(heading).show(self, add_contents)
@ -1642,10 +1662,6 @@ impl Ui {
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.
/// Otherwise does nothing.
pub fn end_row(&mut self) {
@ -1749,7 +1765,7 @@ impl Ui {
/// ```
pub fn menu_button<R>(
&mut self,
title: impl ToString,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
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::*;
/// 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.
///
/// 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();
/// # fn do_stuff() {}
///
/// if ui.add(egui::Button::new("Click mew")).clicked() {
/// if ui.add(egui::Button::new("Click me")).clicked() {
/// 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);`"]
pub struct Button {
text: String,
text_color: Option<Color32>,
text_style: Option<TextStyle>,
text: WidgetText,
wrap: Option<bool>,
/// None means default for interact
fill: Option<Color32>,
stroke: Option<Stroke>,
sense: Sense,
small: bool,
frame: Option<bool>,
wrap: Option<bool>,
min_size: Vec2,
}
impl Button {
#[allow(clippy::needless_pass_by_value)]
pub fn new(text: impl ToString) -> Self {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self {
text: text.to_string(),
text_color: None,
text_style: None,
text: text.into(),
wrap: None,
fill: None,
stroke: None,
sense: Sense::click(),
small: false,
frame: None,
wrap: None,
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 {
self.text_color = Some(text_color);
self
}
pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
self.text_color = text_color;
self.text = self.text.color(text_color);
self
}
#[deprecated = "Replaced by: Button::new(RichText::new(text).text_style(…))"]
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
}
@ -91,7 +87,7 @@ impl Button {
/// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self {
self.text_style = Some(TextStyle::Body);
self.text = self.text.text_style(TextStyle::Body);
self.small = true;
self
}
@ -109,17 +105,6 @@ impl Button {
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 {
self.min_size = min_size;
self
@ -130,49 +115,40 @@ impl Widget for Button {
fn ui(self, ui: &mut Ui) -> Response {
let Button {
text,
text_color,
text_style,
wrap,
fill,
stroke,
sense,
small,
frame,
wrap,
min_size,
} = self;
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;
if small {
button_padding.y = 0.0;
}
let total_extra = button_padding + button_padding;
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
let wrap_width = select(wrap, ui.available_width() - total_extra.x, f32::INFINITY);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button);
let mut desired_size = galley.size() + 2.0 * button_padding;
let mut desired_size = text.size() + 2.0 * button_padding;
if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
}
desired_size = desired_size.at_least(min_size);
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) {
let visuals = ui.style().interact(&response);
let text_pos = ui
.layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min;
if frame {
@ -186,10 +162,7 @@ impl Widget for Button {
);
}
let text_color = text_color
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
text.paint_with_visuals(ui.painter(), text_pos, visuals);
}
response
@ -211,48 +184,35 @@ impl Widget for Button {
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug)]
pub struct Checkbox<'a> {
checked: &'a mut bool,
text: String,
text_color: Option<Color32>,
text_style: Option<TextStyle>,
text: WidgetText,
}
impl<'a> Checkbox<'a> {
#[allow(clippy::needless_pass_by_value)]
pub fn new(checked: &'a mut bool, text: impl ToString) -> Self {
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
Checkbox {
checked,
text: text.to_string(),
text_color: None,
text_style: None,
text: text.into(),
}
}
#[deprecated = "Replaced by: Checkbox::new(RichText::new(text).color(…))"]
pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color);
self.text = self.text.color(text_color);
self
}
#[deprecated = "Replaced by: Checkbox::new(RichText::new(text).text_style(…))"]
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
}
}
impl<'a> Widget for Checkbox<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let Checkbox {
checked,
text,
text_color,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let Checkbox { checked, text } = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
@ -260,16 +220,10 @@ impl<'a> Widget for Checkbox<'a> {
let button_padding = spacing.button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let wrap_width = select(
ui.wrap_text(),
ui.available_width() - total_extra.x,
f32::INFINITY,
);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
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.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
@ -278,14 +232,13 @@ impl<'a> Widget for Checkbox<'a> {
*checked = !*checked;
response.mark_changed();
}
response
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, text.text()));
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response);
let text_pos = pos2(
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);
ui.painter().add(epaint::RectShape {
@ -307,10 +260,7 @@ impl<'a> Widget for Checkbox<'a> {
));
}
let text_color = text_color
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
text.paint_with_visuals(ui.painter(), text_pos, visuals);
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);`"]
#[derive(Debug)]
pub struct RadioButton {
checked: bool,
text: String,
text_color: Option<Color32>,
text_style: Option<TextStyle>,
text: WidgetText,
}
impl RadioButton {
#[allow(clippy::needless_pass_by_value)]
pub fn new(checked: bool, text: impl ToString) -> Self {
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
Self {
checked,
text: text.to_string(),
text_color: None,
text_style: None,
text: text.into(),
}
}
#[deprecated = "Replaced by: RadioButton::new(RichText::new(text).color(…))"]
pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color);
self.text = self.text.color(text_color);
self
}
#[deprecated = "Replaced by: RadioButton::new(RichText::new(text).text_style(…))"]
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
}
}
impl Widget for RadioButton {
fn ui(self, ui: &mut Ui) -> Response {
let RadioButton {
checked,
text,
text_color,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let RadioButton { checked, text } = self;
let icon_width = ui.spacing().icon_width;
let icon_spacing = ui.spacing().icon_spacing;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding;
let wrap_width = select(
ui.wrap_text(),
ui.available_width() - total_extra.x,
f32::INFINITY,
);
let galley = ui
.fonts()
.layout_delayed_color(text, text_style, wrap_width);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
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.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
response
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, galley.text()));
.widget_info(|| WidgetInfo::selected(WidgetType::RadioButton, checked, text.text()));
let text_pos = pos2(
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
@ -429,10 +360,7 @@ impl Widget for RadioButton {
});
}
let text_color = text_color
.or(ui.visuals().override_text_color)
.unwrap_or_else(|| visuals.text_color());
painter.galley_with_color(text_pos, galley, text_color);
text.paint_with_visuals(ui.painter(), text_pos, visuals);
response
}
}

View file

@ -208,11 +208,12 @@ impl<'a> Widget for DragValue<'a> {
}
response
} else {
let button = Button::new(format!("{}{}{}", prefix, value_text, suffix))
.sense(Sense::click_and_drag())
.text_style(TextStyle::Monospace)
.wrap(false)
.min_size(ui.spacing().interact_size); // TODO: find some more generic solution to this
let button = Button::new(
RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
)
.wrap(false)
.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 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);`"]
pub struct Hyperlink {
url: String,
label: Label,
text: WidgetText,
}
impl Hyperlink {
@ -21,41 +21,45 @@ impl Hyperlink {
let url = url.to_string();
Self {
url: url.clone(),
label: Label::new(url).sense(Sense::click()),
text: url.into(),
}
}
#[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 {
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)]
pub fn text(mut self, text: impl ToString) -> Self {
self.label.text = text.to_string();
self.text = text.to_string().into();
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 {
self.label = self.label.text_style(text_style);
self.text = self.text.text_style(text_style);
self
}
pub fn small(self) -> Self {
self.text_style(TextStyle::Small)
#[deprecated = "Use Hyperlink::from_label_and_url instead"]
pub fn small(mut self) -> Self {
self.text = self.text.text_style(TextStyle::Small);
self
}
}
impl Widget for Hyperlink {
fn ui(self, ui: &mut Ui) -> Response {
let Hyperlink { url, label } = self;
let (pos, galley, response) = label.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, galley.text()));
let Hyperlink { url, text } = self;
let label = Label::new(text).sense(Sense::click());
let (pos, text_galley, response) = label.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Hyperlink, text_galley.text()));
if response.hovered() {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
@ -85,7 +89,7 @@ impl Widget for Hyperlink {
ui.painter().add(epaint::TextShape {
pos,
galley,
galley: text_galley.galley,
override_text_color: Some(color),
underline,
angle: 0.0,

View file

@ -1,9 +1,4 @@
use crate::*;
use epaint::{
text::{LayoutJob, LayoutSection, TextFormat},
Galley,
};
use std::sync::Arc;
use crate::{widget_text::WidgetTextGalley, *};
/// Static text.
///
@ -11,135 +6,123 @@ use std::sync::Arc;
/// # let ui = &mut egui::Ui::__test();
/// ui.label("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);`"]
pub struct Label {
// TODO: not pub
pub(crate) text: String,
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,
text: WidgetText,
wrap: Option<bool>,
sense: Sense,
}
impl Label {
#[allow(clippy::needless_pass_by_value)]
pub fn new(text: impl ToString) -> Self {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self {
text: text.to_string(),
text: text.into(),
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(),
}
}
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
/// and horizontal layouts with wrapping,
/// 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 {
self.wrap = Some(wrap);
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 {
self.text_style = Some(text_style);
self.text = self.text.text_style(text_style);
self
}
pub fn heading(self) -> Self {
self.text_style(TextStyle::Heading)
#[deprecated = "Replaced by Label::new(RichText::new(…).heading())"]
pub fn heading(mut self) -> Self {
self.text = self.text.heading();
self
}
pub fn monospace(self) -> Self {
self.text_style(TextStyle::Monospace)
#[deprecated = "Replaced by Label::new(RichText::new(…).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 {
self.code = true;
self.text_style(TextStyle::Monospace)
self.text = self.text.code();
self
}
/// Extra strong text (stronger color).
#[deprecated = "Replaced by Label::new(RichText::new(…).strong())"]
pub fn strong(mut self) -> Self {
self.strong = true;
self.text = self.text.strong();
self
}
/// Extra weak text (fainter color).
#[deprecated = "Replaced by Label::new(RichText::new(…).weak())"]
pub fn weak(mut self) -> Self {
self.weak = true;
self.text = self.text.weak();
self
}
/// draw a line under the text
#[deprecated = "Replaced by Label::new(RichText::new(…).underline())"]
pub fn underline(mut self) -> Self {
self.underline = true;
self.text = self.text.underline();
self
}
/// draw a line through the text, crossing it out
#[deprecated = "Replaced by Label::new(RichText::new(…).strikethrough())"]
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self.text = self.text.strikethrough();
self
}
/// tilt the characters to the right.
#[deprecated = "Replaced by Label::new(RichText::new(…).italics())"]
pub fn italics(mut self) -> Self {
self.italics = true;
self.text = self.text.italics();
self
}
/// Smaller text
pub fn small(self) -> Self {
self.text_style(TextStyle::Small)
#[deprecated = "Replaced by Label::new(RichText::new(…).small())"]
pub fn small(mut self) -> Self {
self.text = self.text.small();
self
}
/// For e.g. exponents
pub fn small_raised(self) -> Self {
self.text_style(TextStyle::Small).raised()
#[deprecated = "Replaced by Label::new(RichText::new(…).small_raised())"]
pub fn small_raised(mut self) -> Self {
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 {
self.raised = true;
self.text = self.text.raised();
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 {
self.background_color = background_color.into();
self.text = self.text.background_color(background_color);
self
}
#[deprecated = "Replaced by Label::new(RichText::new(…).text_color())"]
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
}
@ -163,238 +146,119 @@ impl Label {
}
impl Label {
pub fn layout(&self, ui: &Ui) -> Arc<Galley> {
let max_width = ui.available_width();
let line_color = self.get_text_color(ui, ui.visuals().text_color());
self.layout_width(ui, max_width, line_color)
}
/// `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);
/// Do layout and position the galley in the ui, without painting it or adding widget info.
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
if let WidgetText::Galley(galley) = self.text {
// If the user said "use this specific galley", then just use it:
let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
let pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_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 {
fn ui(self, ui: &mut Ui) -> Response {
let (pos, galley, response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text()));
let (pos, text_galley, response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));
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
}
}
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::*;
enum ProgressBarText {
Custom(String),
Custom(WidgetText),
Percentage,
}
@ -31,9 +31,8 @@ impl ProgressBar {
}
/// A custom text to display on the progress bar.
#[allow(clippy::needless_pass_by_value)]
pub fn text(mut self, text: impl ToString) -> Self {
self.text = Some(ProgressBarText::Custom(text.to_string()));
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
self.text = Some(ProgressBarText::Custom(text.into()));
self
}
@ -124,17 +123,19 @@ impl Widget for ProgressBar {
if let Some(text_kind) = text {
let text = match text_kind {
ProgressBarText::Custom(string) => string,
ProgressBarText::Percentage => format!("{}%", (progress * 100.0) as usize),
ProgressBarText::Custom(text) => text,
ProgressBarText::Percentage => format!("{}%", (progress * 100.0) as usize).into(),
};
ui.painter().sub_region(outer_rect).text(
outer_rect.left_center() + vec2(ui.spacing().item_spacing.x, 0.0),
Align2::LEFT_CENTER,
text,
TextStyle::Button,
visuals
.override_text_color
.unwrap_or(visuals.selection.stroke.color),
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0)
+ vec2(ui.spacing().item_spacing.x, 0.0);
let text_color = visuals
.override_text_color
.unwrap_or(visuals.selection.stroke.color);
galley.paint_with_fallback_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);`"]
#[derive(Debug)]
pub struct SelectableLabel {
selected: bool,
text: String,
text_style: Option<TextStyle>,
text: WidgetText,
}
impl SelectableLabel {
#[allow(clippy::needless_pass_by_value)]
pub fn new(selected: bool, text: impl ToString) -> Self {
pub fn new(selected: bool, text: impl Into<WidgetText>) -> Self {
Self {
selected,
text: text.to_string(),
text_style: None,
text: text.into(),
}
}
#[deprecated = "Replaced by: Button::new(RichText::new(text).text_style(…))"]
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
}
}
impl Widget for SelectableLabel {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
selected,
text,
text_style,
} = self;
let text_style = text_style
.or(ui.style().override_text_style)
.unwrap_or(TextStyle::Button);
let Self { selected, text } = self;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let wrap_width = if ui.wrap_text() {
ui.available_width() - total_extra.x
} else {
f32::INFINITY
};
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
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.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click());
response.widget_info(|| {
WidgetInfo::selected(WidgetType::SelectableLabel, selected, galley.text())
WidgetInfo::selected(WidgetType::SelectableLabel, selected, text.text())
});
let text_pos = ui
.layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min;
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);
}
let text_color = ui
.style()
.visuals
.override_text_color
.unwrap_or_else(|| visuals.text_color());
ui.painter().galley_with_color(text_pos, galley, text_color);
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
response
}
}

View file

@ -1,8 +1,9 @@
#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString`
use crate::{widgets::Label, *};
use std::ops::RangeInclusive;
use crate::*;
// ----------------------------------------------------------------------------
/// 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) {
if !self.text.is_empty() {
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);`"]
pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer,
hint_text: String,
hint_text: WidgetText,
id: Option<Id>,
id_source: Option<Id>,
text_style: Option<TextStyle>,
@ -127,9 +127,8 @@ impl<'t> TextEdit<'t> {
}
/// 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 ToString) -> Self {
self.hint_text = hint_text.to_string();
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
self.hint_text = hint_text.into();
self
}
@ -512,12 +511,12 @@ impl<'t> TextEdit<'t> {
if text.as_ref().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let galley = ui.fonts().layout_job(if multiline {
LayoutJob::simple(hint_text, text_style, hint_text_color, desired_size.x)
let galley = if multiline {
hint_text.into_galley(ui, Some(true), desired_size.x, text_style)
} else {
LayoutJob::simple_singleline(hint_text, text_style, hint_text_color)
});
painter.galley(response.rect.min, galley);
hint_text.into_galley(ui, Some(false), f32::INFINITY, text_style)
};
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
}
if ui.memory().has_focus(id) {

View file

@ -42,13 +42,10 @@ impl super::View for FontBook {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("You can add more characters by installing additional fonts with ");
ui.add(
egui::Hyperlink::from_label_and_url(
"Context::set_fonts",
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
)
.text_style(egui::TextStyle::Monospace),
);
ui.add(egui::Hyperlink::from_label_and_url(
egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
));
ui.label(".");
});
@ -90,10 +87,13 @@ impl super::View for FontBook {
for (&chr, name) in named_chars {
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| {
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));
};

View file

@ -142,7 +142,7 @@ impl Widgets {
// 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.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.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.",
@ -365,7 +365,7 @@ impl SubTree {
) -> Action {
if depth > 0
&& ui
.add(Button::new("delete").text_color(Color32::RED))
.button(RichText::new("delete").color(Color32::RED))
.clicked()
{
return Action::Delete;
@ -565,12 +565,7 @@ fn text_layout_ui(ui: &mut egui::Ui) {
},
);
job.wrap_width = ui.available_width();
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.label(job);
ui.vertical_centered(|ui| {
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(
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|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(
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|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) => {
// This should only happen if the fetch API isn't available or something similar.
ui.add(
egui::Label::new(if error.is_empty() { "Error" } else { error })
.text_color(egui::Color32::RED),
ui.colored_label(
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) => {
ui.add(label_from_style(text, &style));
ui.label(rich_text_from_style(text, &style));
}
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));
}
@ -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 {
heading,
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 mut label = Label::new(text);
let mut rich_text = RichText::new(text);
if heading && !small {
label = label.heading().strong();
rich_text = rich_text.heading().strong();
}
if small && !heading {
label = label.small();
rich_text = rich_text.small();
}
if code {
label = label.code();
rich_text = rich_text.code();
}
if strong {
label = label.strong();
rich_text = rich_text.strong();
} else if quoted {
label = label.weak();
rich_text = rich_text.weak();
}
if underline {
label = label.underline();
rich_text = rich_text.underline();
}
if strikethrough {
label = label.strikethrough();
rich_text = rich_text.strikethrough();
}
if italics {
label = label.italics();
rich_text = rich_text.italics();
}
if raised {
label = label.raised();
rich_text = rich_text.raised();
}
label
rich_text
}
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)")
};
($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)")
};
($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()
);
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
}
/// 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 {
&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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
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)]
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
@ -134,6 +152,15 @@ impl LayoutJob {
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 {