1use egui::{
2 Align, Color32, FontSelection, Image, Pos2, Rect, Response, Sense, Stroke, StrokeKind,
3 TextWrapMode, Ui, Vec2, Widget, WidgetText,
4 epaint::{RectShape, TextShape},
5 style::WidgetVisuals,
6 text::LayoutJob,
7};
8use std::sync::Arc;
9use zeus_theme::LabelVisuals;
10
11#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
12#[derive(Clone)]
13pub struct Label {
14 text: WidgetText,
15 visuals: Option<LabelVisuals>,
16 pub(crate) image: Option<Image<'static>>,
17 spacing: f32,
18 expansion: Option<f32>,
19 pub(crate) sense: Option<Sense>,
20 wrap_mode: Option<TextWrapMode>,
21 selectable: Option<bool>,
22 text_first: bool,
23 selected: bool,
24 interactive: bool,
25 fill_width: bool,
26}
27impl Label {
28 pub fn new(text: impl Into<WidgetText>, image: Option<Image<'static>>) -> Self {
31 Self {
32 text: text.into(),
33 visuals: None,
34 image,
35 spacing: 6.0,
36 expansion: None,
37 sense: None,
38 wrap_mode: None,
39 selectable: None,
40 text_first: true,
41 selected: false,
42 interactive: true,
43 fill_width: false,
44 }
45 }
46
47 pub fn visuals(mut self, visuals: LabelVisuals) -> Self {
49 self.visuals = Some(visuals);
50 self
51 }
52
53 pub fn spacing(mut self, spacing: f32) -> Self {
55 self.spacing = spacing;
56 self
57 }
58
59 pub fn expand(mut self, expansion: Option<f32>) -> Self {
61 self.expansion = expansion;
62 self
63 }
64
65 pub fn sense(mut self, sense: Sense) -> Self {
68 self.sense = Some(sense);
69 self.selectable = Some(false);
70 self
71 }
72
73 pub fn interactive(mut self, interactive: bool) -> Self {
77 self.interactive = interactive;
78 self
79 }
80
81 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
82 self.wrap_mode = Some(wrap_mode);
83 self
84 }
85
86 pub fn wrap(mut self) -> Self {
87 self.wrap_mode = Some(TextWrapMode::Wrap);
88 self
89 }
90
91 pub fn selectable(mut self, selectable: bool) -> Self {
93 self.selectable = Some(selectable);
94 self
95 }
96
97 pub fn image_on_left(mut self) -> Self {
99 self.text_first = false;
100 self
101 }
102
103 pub fn selected(mut self, selected: bool) -> Self {
104 self.selected = selected;
105 self
106 }
107
108 pub fn fill_width(mut self, fill: bool) -> Self {
109 self.fill_width = fill;
110 self
111 }
112
113 pub fn galley_and_size(
117 &self,
118 ui: &Ui,
119 available_width_for_text: f32,
120 ) -> (Arc<egui::Galley>, Vec2) {
121 let layout_job = self.prepare_layout_job(ui, available_width_for_text);
122 let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
123 let text_size = galley.size();
124
125 let image_size = if let Some(image) = &self.image {
126 image.calc_size(ui.available_size(), image.size())
127 } else {
128 Vec2::ZERO
129 };
130
131 let total_width = text_size.x
132 + if self.image.is_some() {
133 self.spacing + image_size.x
134 } else {
135 0.0
136 };
137
138 let total_height = text_size.y.max(image_size.y);
139 (galley, Vec2::new(total_width, total_height))
140 }
141
142 fn prepare_layout_job(&self, ui: &Ui, wrap_width: f32) -> LayoutJob {
143 let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
144 let layout_job = self.text.clone().into_layout_job(
145 ui.style(),
146 FontSelection::Default,
147 ui.text_valign(),
148 );
149
150 let mut layout_job: LayoutJob = (*layout_job).clone();
152 match wrap_mode {
153 TextWrapMode::Extend => {
154 layout_job.wrap.max_width = f32::INFINITY;
155 }
156 TextWrapMode::Wrap => {
157 layout_job.wrap.max_width = wrap_width;
158 }
159 TextWrapMode::Truncate => {
160 layout_job.wrap.max_width = wrap_width;
161 layout_job.wrap.max_rows = 1;
162 layout_job.wrap.break_anywhere = true;
163 }
164 }
165
166 layout_job.halign = Align::LEFT;
167 layout_job
168 }
169 pub(crate) fn paint_content_within_rect(
170 &self,
171 ui: &mut Ui,
172 rect: Rect,
173 button_visuals: &WidgetVisuals,
174 ) {
175 let available_width_for_text = if self.image.is_some() {
177 (rect.width()
178 - self.image.as_ref().map_or(0.0, |img| img.size().map_or(0.0, |s| s.x))
179 - self.spacing)
180 .max(0.0)
181 } else {
182 rect.width()
183 };
184
185 let (galley, _) = self.galley_and_size(ui, available_width_for_text);
187 if ui.is_rect_visible(rect) {
188 let (text_pos, image_rect_opt) = layout_content_within_rect(
189 ui,
190 rect,
191 &galley,
192 &self.image,
193 self.spacing,
194 self.text_first,
195 );
196
197 let text_color =
198 self.visuals.as_ref().map(|v| v.text).unwrap_or(button_visuals.text_color());
199 ui.painter().add(TextShape::new(
200 text_pos,
201 galley.clone(),
202 text_color,
203 ));
204
205 if let Some(image_rect) = image_rect_opt {
206 if let Some(image) = &self.image {
207 image.paint_at(ui, image_rect);
208 }
209 }
210 }
211 }
212}
213
214fn layout_content_within_rect(
215 ui: &Ui,
216 rect: Rect,
217 galley: &egui::Galley,
218 image: &Option<Image<'static>>,
219 spacing: f32,
220 text_first: bool,
221) -> (Pos2, Option<Rect>) {
222 let text_size = galley.size();
223 let image_size = if let Some(image) = image {
224 image.calc_size(ui.available_size(), image.size())
225 } else {
226 Vec2::ZERO
227 };
228
229 let total_content_height = text_size.y.max(image_size.y);
230 let top_y = ui
231 .layout()
232 .align_size_within_rect(Vec2::new(0.0, total_content_height), rect)
233 .min
234 .y;
235
236 let (text_start_x, image_final_rect) = if text_first {
237 let text_start_x = rect.left();
239 let image_start_x = text_start_x + text_size.x + spacing;
240 let image_final_rect = image.as_ref().map(|_| {
241 let image_pos = Pos2::new(
242 image_start_x,
243 top_y + (total_content_height - image_size.y) * 0.5,
244 );
245 Rect::from_min_size(image_pos, image_size)
246 });
247 (text_start_x, image_final_rect)
248 } else {
249 let image_start_x = rect.left();
251 let text_start_x = image_start_x + image_size.x + spacing;
252 let image_final_rect = image.as_ref().map(|_| {
253 let image_pos = Pos2::new(
254 image_start_x,
255 top_y + (total_content_height - image_size.y) * 0.5,
256 );
257 Rect::from_min_size(image_pos, image_size)
258 });
259 (text_start_x, image_final_rect)
260 };
261
262 let text_pos = Pos2::new(
263 text_start_x,
264 top_y + (total_content_height - text_size.y) * 0.5,
265 );
266 (text_pos, image_final_rect)
267}
268
269impl Widget for Label {
270 fn ui(self, ui: &mut Ui) -> Response {
271 let image_size = if let Some(image) = &self.image {
273 image.calc_size(ui.available_size(), image.size())
274 } else {
275 Vec2::ZERO
276 };
277
278 let effective_wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode());
279
280 let available_width_for_text = match effective_wrap_mode {
281 TextWrapMode::Extend => f32::INFINITY,
282 _ => {
283 if self.image.is_some() {
285 (ui.available_width() - self.spacing - image_size.x).max(10.0)
286 } else {
287 ui.available_width()
288 }
289 }
290 };
291
292 let (galley, content_size) = self.galley_and_size(ui, available_width_for_text);
293 let desired_size = if self.fill_width {
294 Vec2::new(ui.available_width(), content_size.y)
295 } else {
296 content_size
297 };
298
299 let sense = self.sense.unwrap_or(Sense::hover());
301 let (rect, response) = ui.allocate_exact_size(desired_size, sense);
302
303 if ui.is_rect_visible(rect) {
305 let visuals = ui.style().interact_selectable(&response, self.selected);
306
307 let fill = if self.selected {
308 self.visuals.as_ref().map(|v| v.bg_selected).unwrap_or(visuals.bg_fill)
309 } else if response.hovered() || response.has_focus() {
310 match self.interactive {
311 true => self.visuals.as_ref().map(|v| v.bg_hover).unwrap_or(visuals.weak_bg_fill),
312 false => Color32::TRANSPARENT,
313 }
314 } else {
315 self.visuals.as_ref().map(|v| v.bg).unwrap_or(Color32::TRANSPARENT)
316 };
317
318 let stroke = if self.selected {
319 self.visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke)
320 } else if response.hovered() || response.has_focus() {
321 match self.interactive {
322 true => self.visuals.as_ref().map(|v| v.border_hover).unwrap_or(visuals.bg_stroke),
323 false => Stroke::new(0.0, Color32::TRANSPARENT),
324 }
325 } else {
326 self.visuals.as_ref().map(|v| v.border).unwrap_or(Stroke::NONE)
327 };
328
329 let expansion = self.expansion.unwrap_or(visuals.expansion);
330 let corner =
331 self.visuals.as_ref().map(|v| v.corner_radius).unwrap_or(visuals.corner_radius);
332 let background_rect = rect.expand(expansion);
333
334 ui.painter().add(RectShape::new(
335 background_rect,
336 corner,
337 fill,
338 stroke,
339 StrokeKind::Inside,
340 ));
341
342 let (text_pos, image_rect_opt) = layout_content_within_rect(
344 ui,
345 rect,
346 &galley,
347 &self.image,
348 self.spacing,
349 self.text_first,
350 );
351
352 let text_color = self.visuals.as_ref().map(|v| v.text).unwrap_or(visuals.text_color());
353
354 ui.painter().add(TextShape::new(
355 text_pos,
356 galley.clone(),
357 text_color,
358 ));
359
360 if let Some(image_rect) = image_rect_opt {
361 if let Some(image) = self.image {
362 image.paint_at(ui, image_rect);
363 }
364 }
365 }
366 response
367 }
368}