use egui::{
Align, Color32, FontSelection, Image, Pos2, Rect, Response, Sense, Stroke, StrokeKind,
TextWrapMode, Ui, Vec2, Widget, WidgetText,
epaint::{RectShape, TextShape},
style::WidgetVisuals,
text::LayoutJob,
};
use std::sync::Arc;
use zeus_theme::LabelVisuals;
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
#[derive(Clone)]
pub struct Label {
text: WidgetText,
visuals: Option<LabelVisuals>,
pub(crate) image: Option<Image<'static>>,
spacing: f32,
expansion: Option<f32>,
pub(crate) sense: Option<Sense>,
wrap_mode: Option<TextWrapMode>,
selectable: Option<bool>,
text_first: bool,
selected: bool,
interactive: bool,
fill_width: bool,
}
impl Label {
pub fn new(text: impl Into<WidgetText>, image: Option<Image<'static>>) -> Self {
Self {
text: text.into(),
visuals: None,
image,
spacing: 6.0,
expansion: None,
sense: None,
wrap_mode: None,
selectable: None,
text_first: true,
selected: false,
interactive: true,
fill_width: false,
}
}
pub fn visuals(mut self, visuals: LabelVisuals) -> Self {
self.visuals = Some(visuals);
self
}
pub fn spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
pub fn expand(mut self, expansion: Option<f32>) -> Self {
self.expansion = expansion;
self
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self.selectable = Some(false);
self
}
pub fn interactive(mut self, interactive: bool) -> Self {
self.interactive = interactive;
self
}
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(TextWrapMode::Wrap);
self
}
pub fn selectable(mut self, selectable: bool) -> Self {
self.selectable = Some(selectable);
self
}
pub fn image_on_left(mut self) -> Self {
self.text_first = false;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn fill_width(mut self, fill: bool) -> Self {
self.fill_width = fill;
self
}
pub fn galley_and_size(
&self,
ui: &Ui,
available_width_for_text: f32,
) -> (Arc<egui::Galley>, Vec2) {
let layout_job = self.prepare_layout_job(ui, available_width_for_text);
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let text_size = galley.size();
let image_size = if let Some(image) = &self.image {
image.calc_size(ui.available_size(), image.size())
} else {
Vec2::ZERO
};
let total_width = text_size.x
+ if self.image.is_some() {
self.spacing + image_size.x
} else {
0.0
};
let total_height = text_size.y.max(image_size.y);
(galley, Vec2::new(total_width, total_height))
}
fn prepare_layout_job(&self, ui: &Ui, wrap_width: f32) -> LayoutJob {
let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let layout_job = self.text.clone().into_layout_job(
ui.style(),
FontSelection::Default,
ui.text_valign(),
);
let mut layout_job: LayoutJob = (*layout_job).clone();
match wrap_mode {
TextWrapMode::Extend => {
layout_job.wrap.max_width = f32::INFINITY;
}
TextWrapMode::Wrap => {
layout_job.wrap.max_width = wrap_width;
}
TextWrapMode::Truncate => {
layout_job.wrap.max_width = wrap_width;
layout_job.wrap.max_rows = 1;
layout_job.wrap.break_anywhere = true;
}
}
layout_job.halign = Align::LEFT;
layout_job
}
pub(crate) fn paint_content_within_rect(
&self,
ui: &mut Ui,
rect: Rect,
button_visuals: &WidgetVisuals,
) {
let available_width_for_text = if self.image.is_some() {
(rect.width()
- self.image.as_ref().map_or(0.0, |img| img.size().map_or(0.0, |s| s.x))
- self.spacing)
.max(0.0)
} else {
rect.width()
};
let (galley, _) = self.galley_and_size(ui, available_width_for_text);
if ui.is_rect_visible(rect) {
let (text_pos, image_rect_opt) = layout_content_within_rect(
ui,
rect,
&galley,
&self.image,
self.spacing,
self.text_first,
);
let text_color =
self.visuals.as_ref().map(|v| v.text).unwrap_or(button_visuals.text_color());
ui.painter().add(TextShape::new(
text_pos,
galley.clone(),
text_color,
));
if let Some(image_rect) = image_rect_opt {
if let Some(image) = &self.image {
image.paint_at(ui, image_rect);
}
}
}
}
}
fn layout_content_within_rect(
ui: &Ui,
rect: Rect,
galley: &egui::Galley,
image: &Option<Image<'static>>,
spacing: f32,
text_first: bool,
) -> (Pos2, Option<Rect>) {
let text_size = galley.size();
let image_size = if let Some(image) = image {
image.calc_size(ui.available_size(), image.size())
} else {
Vec2::ZERO
};
let total_content_height = text_size.y.max(image_size.y);
let top_y = ui
.layout()
.align_size_within_rect(Vec2::new(0.0, total_content_height), rect)
.min
.y;
let (text_start_x, image_final_rect) = if text_first {
let text_start_x = rect.left();
let image_start_x = text_start_x + text_size.x + spacing;
let image_final_rect = image.as_ref().map(|_| {
let image_pos = Pos2::new(
image_start_x,
top_y + (total_content_height - image_size.y) * 0.5,
);
Rect::from_min_size(image_pos, image_size)
});
(text_start_x, image_final_rect)
} else {
let image_start_x = rect.left();
let text_start_x = image_start_x + image_size.x + spacing;
let image_final_rect = image.as_ref().map(|_| {
let image_pos = Pos2::new(
image_start_x,
top_y + (total_content_height - image_size.y) * 0.5,
);
Rect::from_min_size(image_pos, image_size)
});
(text_start_x, image_final_rect)
};
let text_pos = Pos2::new(
text_start_x,
top_y + (total_content_height - text_size.y) * 0.5,
);
(text_pos, image_final_rect)
}
impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response {
let image_size = if let Some(image) = &self.image {
image.calc_size(ui.available_size(), image.size())
} else {
Vec2::ZERO
};
let effective_wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let available_width_for_text = match effective_wrap_mode {
TextWrapMode::Extend => f32::INFINITY,
_ => {
if self.image.is_some() {
(ui.available_width() - self.spacing - image_size.x).max(10.0)
} else {
ui.available_width()
}
}
};
let (galley, content_size) = self.galley_and_size(ui, available_width_for_text);
let desired_size = if self.fill_width {
Vec2::new(ui.available_width(), content_size.y)
} else {
content_size
};
let sense = self.sense.unwrap_or(Sense::hover());
let (rect, response) = ui.allocate_exact_size(desired_size, sense);
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact_selectable(&response, self.selected);
let fill = if self.selected {
self.visuals.as_ref().map(|v| v.bg_selected).unwrap_or(visuals.bg_fill)
} else if response.hovered() || response.has_focus() {
match self.interactive {
true => self.visuals.as_ref().map(|v| v.bg_hover).unwrap_or(visuals.weak_bg_fill),
false => Color32::TRANSPARENT,
}
} else {
self.visuals.as_ref().map(|v| v.bg).unwrap_or(Color32::TRANSPARENT)
};
let stroke = if self.selected {
self.visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke)
} else if response.hovered() || response.has_focus() {
match self.interactive {
true => self.visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke),
false => Stroke::new(0.0, Color32::TRANSPARENT),
}
} else {
self.visuals.as_ref().map(|v| v.border).unwrap_or(Stroke::NONE)
};
let expansion = self.expansion.unwrap_or(visuals.expansion);
let corner =
self.visuals.as_ref().map(|v| v.corner_radius).unwrap_or(visuals.corner_radius);
let background_rect = rect.expand(expansion);
ui.painter().add(RectShape::new(
background_rect,
corner,
fill,
stroke,
StrokeKind::Inside,
));
let (text_pos, image_rect_opt) = layout_content_within_rect(
ui,
rect,
&galley,
&self.image,
self.spacing,
self.text_first,
);
let text_color = self.visuals.as_ref().map(|v| v.text).unwrap_or(visuals.text_color());
ui.painter().add(TextShape::new(
text_pos,
galley.clone(),
text_color,
));
if let Some(image_rect) = image_rect_opt {
if let Some(image) = self.image {
image.paint_at(ui, image_rect);
}
}
}
response
}
}