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