1use crate::Renderer;
2use crate::widget::menu::MenuBarState;
3
4use iced::window;
5use iced_core::event::Event;
6use iced_core::text::LineHeight;
7use iced_core::widget::Widget;
8use iced_core::widget::text::{Alignment, Catalog, Ellipsize, Shaping, Style, Wrapping};
9use iced_core::widget::tree::{self, Tree};
10use iced_core::{
11 Clipboard, Color, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, alignment, mouse,
12 overlay, renderer,
13};
14use std::borrow::Cow;
15
16type InnerText<'a> = iced_core::widget::text::Text<'a, crate::Theme, Renderer>;
17
18pub struct SelectableText<'a> {
31 inner: InnerText<'a>,
32 selectable: bool,
33 context_menu: bool,
34 window_id: window::Id,
35}
36
37impl<'a> SelectableText<'a> {
38 pub fn new(content: impl iced_core::text::IntoFragment<'a>) -> Self {
42 Self {
43 inner: InnerText::new(content),
44 selectable: true,
45 context_menu: true,
46 window_id: crate::widget::text_context_menu::current_window_id(),
47 }
48 }
49
50 pub fn selectable(mut self, enabled: bool) -> Self {
55 self.selectable = enabled;
56 self
57 }
58
59 pub fn context_menu(mut self, enabled: bool) -> Self {
63 self.context_menu = enabled;
64 self
65 }
66
67 fn shows_context_menu(&self) -> bool {
71 self.selectable && self.context_menu
72 }
73
74 fn uses_popup_context_menu(&self) -> bool {
75 #[cfg(all(wayland_platform, feature = "winit"))]
76 if matches!(
77 crate::app::cosmic::WINDOWING_SYSTEM.get(),
78 Some(crate::app::cosmic::WindowingSystem::Wayland)
79 ) {
80 return true;
81 }
82 false
83 }
84
85 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
86 self.inner = self.inner.size(size);
87 self
88 }
89
90 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
91 self.inner = self.inner.line_height(line_height);
92 self
93 }
94
95 pub fn font(mut self, font: impl Into<<Renderer as iced_core::text::Renderer>::Font>) -> Self {
96 self.inner = self.inner.font(font);
97 self
98 }
99
100 pub fn font_maybe(
101 mut self,
102 font: Option<impl Into<<Renderer as iced_core::text::Renderer>::Font>>,
103 ) -> Self {
104 self.inner = self.inner.font_maybe(font);
105 self
106 }
107
108 pub fn width(mut self, width: impl Into<Length>) -> Self {
109 self.inner = self.inner.width(width);
110 self
111 }
112
113 pub fn height(mut self, height: impl Into<Length>) -> Self {
114 self.inner = self.inner.height(height);
115 self
116 }
117
118 pub fn center(mut self) -> Self {
119 self.inner = self.inner.center();
120 self
121 }
122
123 pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
124 self.inner = self.inner.align_x(alignment);
125 self
126 }
127
128 pub fn align_y(mut self, alignment: impl Into<alignment::Vertical>) -> Self {
129 self.inner = self.inner.align_y(alignment);
130 self
131 }
132
133 pub fn shaping(mut self, shaping: Shaping) -> Self {
134 self.inner = self.inner.shaping(shaping);
135 self
136 }
137
138 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
139 self.inner = self.inner.wrapping(wrapping);
140 self
141 }
142
143 pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self {
144 self.inner = self.inner.ellipsize(ellipsize);
145 self
146 }
147
148 #[must_use]
149 pub fn style(mut self, style: impl Fn(&crate::Theme) -> Style + 'a) -> Self
150 where
151 <crate::Theme as Catalog>::Class<'a>:
152 From<iced_core::widget::text::StyleFn<'a, crate::Theme>>,
153 {
154 self.inner = self.inner.style(style);
155 self
156 }
157
158 pub fn color(mut self, color: impl Into<Color>) -> Self
159 where
160 <crate::Theme as Catalog>::Class<'a>:
161 From<iced_core::widget::text::StyleFn<'a, crate::Theme>>,
162 {
163 self.inner = self.inner.color(color);
164 self
165 }
166
167 pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self
168 where
169 <crate::Theme as Catalog>::Class<'a>:
170 From<iced_core::widget::text::StyleFn<'a, crate::Theme>>,
171 {
172 self.inner = self.inner.color_maybe(color);
173 self
174 }
175
176 #[must_use]
177 pub fn class(mut self, class: impl Into<<crate::Theme as Catalog>::Class<'a>>) -> Self {
178 self.inner = self.inner.class(class);
179 self
180 }
181}
182
183impl<'a> From<InnerText<'a>> for SelectableText<'a> {
184 fn from(inner: InnerText<'a>) -> Self {
185 Self {
186 inner,
187 selectable: true,
188 context_menu: true,
189 window_id: crate::widget::text_context_menu::current_window_id(),
190 }
191 }
192}
193
194impl<'a> From<&'a str> for SelectableText<'a> {
195 fn from(content: &'a str) -> Self {
196 Self::new(content)
197 }
198}
199
200struct TextWrapperState {
201 menu_bar_state: MenuBarState,
202 pending_action: crate::widget::text_context_menu::PendingAction,
203}
204
205fn w<'x, Message>(inner: &'x InnerText<'_>) -> &'x dyn Widget<Message, crate::Theme, Renderer> {
206 inner
207}
208
209fn w_mut<'x, Message>(
210 inner: &'x mut InnerText<'_>,
211) -> &'x mut dyn Widget<Message, crate::Theme, Renderer> {
212 inner
213}
214
215impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, Renderer> for SelectableText<'a> {
216 fn tag(&self) -> tree::Tag {
217 if self.shows_context_menu() {
218 tree::Tag::of::<TextWrapperState>()
219 } else {
220 w::<Message>(&self.inner).tag()
221 }
222 }
223
224 fn state(&self) -> tree::State {
225 if self.shows_context_menu() {
226 tree::State::new(TextWrapperState {
227 menu_bar_state: MenuBarState::default(),
228 pending_action: crate::widget::text_context_menu::pending_action(),
229 })
230 } else {
231 w::<Message>(&self.inner).state()
232 }
233 }
234
235 fn children(&self) -> Vec<Tree> {
236 if self.shows_context_menu() {
237 vec![Tree::new(w::<Message>(&self.inner))]
238 } else {
239 Vec::new()
240 }
241 }
242
243 fn diff(&mut self, tree: &mut Tree) {
244 if self.shows_context_menu() {
245 if let Some(child) = tree.children.first_mut() {
246 child.diff(&mut self.inner as &mut dyn Widget<Message, crate::Theme, Renderer>);
247 }
248 }
249 }
250
251 fn size(&self) -> Size<Length> {
252 w::<Message>(&self.inner).size()
253 }
254
255 fn layout(
256 &mut self,
257 tree: &mut Tree,
258 renderer: &Renderer,
259 limits: &iced_core::layout::Limits,
260 ) -> iced_core::layout::Node {
261 let inner_tree = if self.shows_context_menu() {
262 &mut tree.children[0]
263 } else {
264 tree
265 };
266 w_mut::<Message>(&mut self.inner).layout(inner_tree, renderer, limits)
267 }
268
269 fn draw(
270 &self,
271 tree: &Tree,
272 renderer: &mut Renderer,
273 theme: &crate::Theme,
274 defaults: &renderer::Style,
275 layout: Layout<'_>,
276 cursor: mouse::Cursor,
277 viewport: &Rectangle,
278 ) {
279 let inner_tree = if self.shows_context_menu() {
280 &tree.children[0]
281 } else {
282 tree
283 };
284 w::<Message>(&self.inner).draw(
285 inner_tree, renderer, theme, defaults, layout, cursor, viewport,
286 );
287
288 use iced_core::renderer::Renderer as RendererExt;
290 use iced_core::widget::text::State as TextState;
291
292 let state = inner_tree
293 .state
294 .downcast_ref::<TextState<<Renderer as iced_core::text::Renderer>::Paragraph>>();
295 if state.is_keyboard_focused() {
296 let cosmic = theme.cosmic();
297 let accent: Color = cosmic.accent.base.into();
298 renderer.fill_quad(
299 renderer::Quad {
300 bounds: layout.bounds(),
301 border: iced_core::Border {
302 color: accent,
303 width: 1.0,
304 radius: cosmic.corner_radii.radius_s.into(),
305 },
306 ..renderer::Quad::default()
307 },
308 Color::TRANSPARENT,
309 );
310 }
311 }
312
313 fn update(
314 &mut self,
315 tree: &mut Tree,
316 event: &Event,
317 layout: Layout<'_>,
318 cursor: mouse::Cursor,
319 renderer: &Renderer,
320 clipboard: &mut dyn Clipboard,
321 shell: &mut Shell<'_, Message>,
322 viewport: &Rectangle,
323 ) {
324 if self.shows_context_menu() {
325 w_mut::<Message>(&mut self.inner).update(
326 &mut tree.children[0],
327 event,
328 layout,
329 cursor,
330 renderer,
331 clipboard,
332 shell,
333 viewport,
334 );
335
336 #[cfg(wayland_platform)]
337 if self.uses_popup_context_menu() {
338 use iced_core::widget::text::HasSelectableText;
339
340 if self
341 .inner
342 .context_menu_position(&tree.children[0])
343 .is_some()
344 {
345 let selected_text = self.inner.selected_text(&tree.children[0]);
346 let has_selection = selected_text.is_some();
347 let click_position =
348 self.inner.context_menu_position(&tree.children[0]).unwrap();
349 let wrapper_state = tree.state.downcast_ref::<TextWrapperState>();
350 let menu_bar_state = wrapper_state.menu_bar_state.clone();
351 let pending_action = wrapper_state.pending_action.clone();
352
353 crate::widget::text_context_menu::create_text_context_popup(
354 click_position,
355 selected_text,
356 false,
357 has_selection,
358 self.inner.has_text(&tree.children[0]),
359 self.inner.clipboard_has_text(&tree.children[0]),
360 &menu_bar_state,
361 &pending_action,
362 renderer,
363 viewport,
364 cursor,
365 self.window_id,
366 );
367
368 self.inner
369 .set_context_menu_position(&mut tree.children[0], None);
370 }
371
372 {
373 let wrapper_state = tree.state.downcast_ref::<TextWrapperState>();
374 let pending_action = wrapper_state.pending_action.clone();
375 if let Some(action) =
376 crate::widget::text_context_menu::take_pending_action(&pending_action)
377 {
378 match action {
379 crate::widget::text_context_menu::TextCtxAction::Copy => {}
380 crate::widget::text_context_menu::TextCtxAction::SelectAll => {
381 self.inner.select_all(&mut tree.children[0]);
382 shell.request_redraw();
383 }
384 _ => {}
385 }
386 }
387 }
388
389 let wrapper_state = tree.state.downcast_ref::<TextWrapperState>();
390 let menu_bar_state = wrapper_state.menu_bar_state.clone();
391 crate::widget::text_context_menu::dismiss_popup_on_event(
392 &menu_bar_state,
393 event,
394 self.window_id,
395 );
396 }
397 } else {
398 w_mut::<Message>(&mut self.inner).update(
399 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
400 );
401 }
402 }
403
404 fn mouse_interaction(
405 &self,
406 tree: &Tree,
407 layout: Layout<'_>,
408 cursor: mouse::Cursor,
409 viewport: &Rectangle,
410 renderer: &Renderer,
411 ) -> mouse::Interaction {
412 let inner_tree = if self.shows_context_menu() {
413 &tree.children[0]
414 } else {
415 tree
416 };
417 w::<Message>(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer)
418 }
419
420 fn overlay<'b>(
421 &'b mut self,
422 tree: &'b mut Tree,
423 _layout: Layout<'b>,
424 _renderer: &Renderer,
425 _viewport: &Rectangle,
426 translation: Vector,
427 ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
428 if self.shows_context_menu() && !self.uses_popup_context_menu() {
429 let menu_bar_state = tree
430 .state
431 .downcast_ref::<TextWrapperState>()
432 .menu_bar_state
433 .clone();
434 let inner_tree = &mut tree.children[0];
435 crate::widget::text_context_menu::context_menu_overlay(
436 &self.inner,
437 inner_tree,
438 None,
439 translation,
440 menu_bar_state,
441 )
442 } else {
443 None
444 }
445 }
446
447 fn operate(
448 &mut self,
449 tree: &mut Tree,
450 layout: Layout<'_>,
451 renderer: &Renderer,
452 operation: &mut dyn iced_core::widget::Operation,
453 ) {
454 let inner_tree = if self.shows_context_menu() {
455 &mut tree.children[0]
456 } else {
457 tree
458 };
459 w_mut::<Message>(&mut self.inner).operate(inner_tree, layout, renderer, operation);
460 }
461
462 #[cfg(feature = "a11y")]
463 fn a11y_nodes(
464 &self,
465 layout: Layout<'_>,
466 state: &Tree,
467 cursor: mouse::Cursor,
468 ) -> iced_accessibility::A11yTree {
469 let inner_state = if self.shows_context_menu() {
470 &state.children[0]
471 } else {
472 state
473 };
474 w::<Message>(&self.inner).a11y_nodes(layout, inner_state, cursor)
475 }
476
477 fn id(&self) -> Option<iced_core::widget::Id> {
478 w::<Message>(&self.inner).id()
479 }
480
481 fn set_id(&mut self, id: iced_core::widget::Id) {
482 w_mut::<Message>(&mut self.inner).set_id(id);
483 }
484}
485
486impl<'a, Message: Clone + 'static> From<SelectableText<'a>> for crate::Element<'a, Message> {
487 fn from(text: SelectableText<'a>) -> Self {
488 let SelectableText {
489 inner,
490 selectable,
491 context_menu,
492 window_id,
493 } = text;
494 let inner = if selectable {
499 inner.selectable()
500 } else {
501 inner
502 };
503 Self::new(SelectableText {
504 inner,
505 selectable,
506 context_menu,
507 window_id,
508 })
509 }
510}
511
512pub fn selectable_text<'a>(content: impl Into<Cow<'a, str>> + 'a) -> SelectableText<'a> {
518 SelectableText::new(content.into()).font(crate::font::default())
519}
520
521macro_rules! selectable_preset {
523 ($(#[$meta:meta])* $name:ident) => {
524 $(#[$meta])*
525 pub fn $name<'a>(content: impl Into<Cow<'a, str>> + 'a) -> SelectableText<'a> {
526 SelectableText::from(crate::widget::text::$name(content))
527 }
528 };
529}
530
531selectable_preset!(title1);
532selectable_preset!(title2);
533selectable_preset!(title3);
534selectable_preset!(title4);
535selectable_preset!(heading);
536selectable_preset!(caption_heading);
537selectable_preset!(body);
538selectable_preset!(caption);
539selectable_preset!(monotext);