Skip to content

Commit

Permalink
Rich text for all widgets (#855)
Browse files Browse the repository at this point in the history
Introduce `RichText` and `WidgetText`
  • Loading branch information
emilk authored Nov 1, 2021
1 parent 9378cd5 commit 09b8269
Show file tree
Hide file tree
Showing 30 changed files with 1,099 additions and 690 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
44 changes: 17 additions & 27 deletions egui/src/containers/collapsing_header.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::hash::Hash;

use crate::{widgets::Label, *};
use crate::*;
use epaint::{Shape, TextStyle};

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
}

Expand Down Expand Up @@ -252,7 +251,7 @@ impl CollapsingHeader {
"Horizontal collapsing is unimplemented"
);
let Self {
mut label,
text,
default_open,
id_source,
enabled: _,
Expand All @@ -261,35 +260,31 @@ 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);
let button_padding = ui.spacing().button_padding;

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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 11 additions & 18 deletions egui/src/containers/combo_box.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
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.
///
/// ```
/// # #[derive(Debug, PartialEq)]
/// # 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");
Expand All @@ -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()),
Expand All @@ -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
}

Expand Down Expand Up @@ -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, ""));
Expand All @@ -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(),
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand All @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
}

Expand Down
46 changes: 19 additions & 27 deletions egui/src/containers/window.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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>,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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):
Expand All @@ -795,7 +792,6 @@ fn show_title_bar(

TitleBar {
id,
title_label,
title_galley,
min_rect,
rect: Rect::NAN, // Will be filled in later
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading

0 comments on commit 09b8269

Please sign in to comment.