use crate::icons;
use eframe::egui::{
self,
text::{ByteIndex, LayoutJob, LayoutSection},
Align, FontSelection, Response, Sense, TextWrapMode, Widget, WidgetText,
};
pub fn image(icon: &'static icons::Icon, height: f32) -> egui::Image<'static> {
let size = egui::vec2(height * icon.aspect, height);
egui::Image::new(egui::ImageSource::Bytes {
uri: icon.uri.into(),
bytes: egui::load::Bytes::Static(icon.svg.as_bytes()),
})
.fit_to_exact_size(size)
}
pub fn icon_button<'a>(ui: &egui::Ui, label: &'a str) -> egui::Button<'a> {
icon_button_colored(ui, label, None)
}
pub fn icon_button_colored<'a>(
ui: &egui::Ui,
label: &'a str,
color: Option<egui::Color32>,
) -> egui::Button<'a> {
let Some((icon, rest)) = split_leading(label) else {
return match color {
Some(c) => egui::Button::new(egui::RichText::new(label).color(c)),
None => egui::Button::new(label),
};
};
egui_extras::install_image_loaders(ui.ctx());
let mut art = image(icon, ui.text_style_height(&egui::TextStyle::Body));
if let (Some(c), true) = (color, icon.mono) {
art = art.tint(c);
}
let button = match (rest.is_empty(), color) {
(true, _) => egui::Button::new(art),
(false, Some(c)) => egui::Button::new((art, egui::RichText::new(rest).color(c))),
(false, None) => egui::Button::new((art, rest)),
};
button.image_tint_follows_text_color(icon.mono && color.is_none())
}
fn split_leading(label: &str) -> Option<(&'static icons::Icon, &str)> {
let mut chars = label.chars();
let icon = icons::lookup(chars.next()?)?;
Some((icon, chars.as_str().trim_start()))
}
pub fn selectable_icon_label(ui: &mut egui::Ui, selected: bool, label: &str) -> Response {
let Some((icon, rest)) = split_leading(label) else {
return ui.selectable_label(selected, label);
};
egui_extras::install_image_loaders(ui.ctx());
let art = image(icon, ui.text_style_height(&egui::TextStyle::Body));
ui.add(
egui::Button::selectable(selected, (art, rest))
.image_tint_follows_text_color(icon.mono),
)
}
pub fn glyph(ui: &egui::Ui, glyph: &str, color: egui::Color32) -> Option<egui::Image<'static>> {
let icon = icons::artwork(glyph)?;
egui_extras::install_image_loaders(ui.ctx());
let art = image(icon, ui.text_style_height(&egui::TextStyle::Body));
Some(if icon.mono { art.tint(color) } else { art })
}
pub fn split_caption(label: &str) -> (Option<&'static icons::Icon>, &str) {
match split_leading(label) {
Some((icon, rest)) => (Some(icon), rest),
None => (None, label),
}
}
enum Segment {
Text(LayoutJob),
Icon(&'static icons::Icon, egui::TextFormat),
}
#[derive(Clone, Copy, Default)]
struct Opts {
wrap_mode: Option<TextWrapMode>,
sense: Option<Sense>,
selectable: Option<bool>,
}
#[must_use = "widgets do nothing unless you add them to a Ui"]
pub struct IconText {
text: WidgetText,
opts: Opts,
}
impl IconText {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self { text: text.into(), opts: Opts::default() }
}
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.opts.wrap_mode = Some(wrap_mode);
self
}
#[inline]
pub fn wrap(self) -> Self {
self.wrap_mode(TextWrapMode::Wrap)
}
#[inline]
pub fn truncate(self) -> Self {
self.wrap_mode(TextWrapMode::Truncate)
}
#[inline]
pub fn extend(self) -> Self {
self.wrap_mode(TextWrapMode::Extend)
}
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.opts.sense = Some(sense);
self
}
#[inline]
pub fn selectable(mut self, selectable: bool) -> Self {
self.opts.selectable = Some(selectable);
self
}
pub fn show(self, ui: &mut egui::Ui) -> Response {
let Self { text, opts } = self;
let job = std::sync::Arc::unwrap_or_clone(text.into_layout_job(
ui.style(),
FontSelection::Default,
ui.text_valign(),
));
let segments = split(&job);
if segments.len() == 1 {
if let Some(Segment::Text(_)) = segments.first() {
return opts.label(job).ui(ui);
}
}
egui_extras::install_image_loaders(ui.ctx());
if ui.layout().is_horizontal() && ui.layout().main_wrap() {
opts.emit(ui, segments)
} else {
ui.horizontal_wrapped(|ui| opts.emit(ui, segments)).inner
}
}
}
impl Opts {
fn emit(&self, ui: &mut egui::Ui, segments: Vec<Segment>) -> Response {
let spacing = ui.spacing().item_spacing;
ui.spacing_mut().item_spacing.x = 0.0;
let mut response: Option<Response> = None;
let mut union = |acc: &mut Option<Response>, r: Response| {
*acc = Some(match acc.take() {
Some(prev) => prev | r,
None => r,
});
};
for segment in segments {
match segment {
Segment::Text(job) => union(&mut response, self.label(job).ui(ui)),
Segment::Icon(icon, format) => {
union(&mut response, self.icon(ui, icon, &format));
}
}
}
ui.spacing_mut().item_spacing = spacing;
response.unwrap_or_else(|| ui.allocate_response(egui::Vec2::ZERO, Sense::hover()))
}
fn icon(&self, ui: &mut egui::Ui, icon: &'static icons::Icon, format: &egui::TextFormat) -> Response {
let height = ui.fonts_mut(|f| f.row_height(&format.font_id));
let size = egui::vec2(height * icon.aspect, height);
let mut image = image(icon, height);
if icon.mono {
image = image.tint(resolve(ui, format.color));
}
let response = ui.add_sized(size, image);
ui.put(
response.rect,
egui::Label::new(
egui::RichText::new(icon.ch)
.font(format.font_id.clone())
.color(egui::Color32::TRANSPARENT),
)
.selectable(self.selectable.unwrap_or(false)),
) | response
}
fn label(&self, job: LayoutJob) -> egui::Label {
let mut label = egui::Label::new(job);
if let Some(wrap_mode) = self.wrap_mode {
label = label.wrap_mode(wrap_mode);
}
if let Some(sense) = self.sense {
label = label.sense(sense);
}
if let Some(selectable) = self.selectable {
label = label.selectable(selectable);
}
label
}
}
impl Widget for IconText {
fn ui(self, ui: &mut egui::Ui) -> Response {
self.show(ui)
}
}
fn resolve(ui: &egui::Ui, color: egui::Color32) -> egui::Color32 {
if color == egui::Color32::PLACEHOLDER {
ui.visuals().text_color()
} else {
color
}
}
fn split(job: &LayoutJob) -> Vec<Segment> {
let mut segments = Vec::new();
let mut run_start = 0;
for (at, ch) in job.text.char_indices() {
let Some(icon) = icons::lookup(ch) else { continue };
if run_start < at {
segments.push(Segment::Text(slice(job, run_start, at)));
}
segments.push(Segment::Icon(icon, format_at(job, at)));
run_start = at + ch.len_utf8();
}
if segments.is_empty() {
return vec![Segment::Text(job.clone())];
}
if run_start < job.text.len() {
segments.push(Segment::Text(slice(job, run_start, job.text.len())));
}
segments
}
fn format_at(job: &LayoutJob, at: usize) -> egui::TextFormat {
if job.sections.is_empty() {
return egui::TextFormat::default();
}
job.format_at_byte(ByteIndex(at)).clone()
}
fn slice(job: &LayoutJob, start: usize, end: usize) -> LayoutJob {
let mut out = job.clone();
out.text = job.text[start..end].to_owned();
out.sections = job
.sections
.iter()
.filter_map(|s| {
let lo = s.byte_range.start.0.max(start);
let hi = s.byte_range.end.0.min(end);
(lo < hi).then(|| LayoutSection {
leading_space: if s.byte_range.start.0 >= start { s.leading_space } else { 0.0 },
byte_range: ByteIndex(lo - start)..ByteIndex(hi - start),
format: s.format.clone(),
})
})
.collect();
out.first_row_min_height = 0.0;
out.halign = Align::LEFT;
out
}
pub trait IconTextUi {
fn icon_label(&mut self, text: impl Into<WidgetText>) -> Response;
}
impl IconTextUi for egui::Ui {
fn icon_label(&mut self, text: impl Into<WidgetText>) -> Response {
IconText::new(text).show(self)
}
}