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 &menu_bar_state,
359 &pending_action,
360 renderer,
361 viewport,
362 cursor,
363 self.window_id,
364 );
365
366 self.inner
367 .set_context_menu_position(&mut tree.children[0], None);
368 }
369
370 {
371 let wrapper_state = tree.state.downcast_ref::<TextWrapperState>();
372 let pending_action = wrapper_state.pending_action.clone();
373 if let Some(action) =
374 crate::widget::text_context_menu::take_pending_action(&pending_action)
375 {
376 match action {
377 crate::widget::text_context_menu::TextCtxAction::Copy => {}
378 crate::widget::text_context_menu::TextCtxAction::SelectAll => {
379 self.inner.select_all(&mut tree.children[0]);
380 shell.request_redraw();
381 }
382 _ => {}
383 }
384 }
385 }
386
387 let wrapper_state = tree.state.downcast_ref::<TextWrapperState>();
388 let menu_bar_state = wrapper_state.menu_bar_state.clone();
389 crate::widget::text_context_menu::dismiss_popup_on_event(
390 &menu_bar_state,
391 event,
392 self.window_id,
393 );
394 }
395 } else {
396 w_mut::<Message>(&mut self.inner).update(
397 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
398 );
399 }
400 }
401
402 fn mouse_interaction(
403 &self,
404 tree: &Tree,
405 layout: Layout<'_>,
406 cursor: mouse::Cursor,
407 viewport: &Rectangle,
408 renderer: &Renderer,
409 ) -> mouse::Interaction {
410 let inner_tree = if self.shows_context_menu() {
411 &tree.children[0]
412 } else {
413 tree
414 };
415 w::<Message>(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer)
416 }
417
418 fn overlay<'b>(
419 &'b mut self,
420 tree: &'b mut Tree,
421 _layout: Layout<'b>,
422 _renderer: &Renderer,
423 _viewport: &Rectangle,
424 translation: Vector,
425 ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
426 if self.shows_context_menu() && !self.uses_popup_context_menu() {
427 let menu_bar_state = tree
428 .state
429 .downcast_ref::<TextWrapperState>()
430 .menu_bar_state
431 .clone();
432 let inner_tree = &mut tree.children[0];
433 crate::widget::text_context_menu::context_menu_overlay(
434 &self.inner,
435 inner_tree,
436 None,
437 translation,
438 menu_bar_state,
439 )
440 } else {
441 None
442 }
443 }
444
445 fn operate(
446 &mut self,
447 tree: &mut Tree,
448 layout: Layout<'_>,
449 renderer: &Renderer,
450 operation: &mut dyn iced_core::widget::Operation,
451 ) {
452 let inner_tree = if self.shows_context_menu() {
453 &mut tree.children[0]
454 } else {
455 tree
456 };
457 w_mut::<Message>(&mut self.inner).operate(inner_tree, layout, renderer, operation);
458 }
459
460 #[cfg(feature = "a11y")]
461 fn a11y_nodes(
462 &self,
463 layout: Layout<'_>,
464 state: &Tree,
465 cursor: mouse::Cursor,
466 ) -> iced_accessibility::A11yTree {
467 let inner_state = if self.shows_context_menu() {
468 &state.children[0]
469 } else {
470 state
471 };
472 w::<Message>(&self.inner).a11y_nodes(layout, inner_state, cursor)
473 }
474
475 fn id(&self) -> Option<iced_core::widget::Id> {
476 w::<Message>(&self.inner).id()
477 }
478
479 fn set_id(&mut self, id: iced_core::widget::Id) {
480 w_mut::<Message>(&mut self.inner).set_id(id);
481 }
482}
483
484impl<'a, Message: Clone + 'static> From<SelectableText<'a>> for crate::Element<'a, Message> {
485 fn from(text: SelectableText<'a>) -> Self {
486 let SelectableText {
487 inner,
488 selectable,
489 context_menu,
490 window_id,
491 } = text;
492 let inner = if selectable {
497 inner.selectable()
498 } else {
499 inner
500 };
501 Self::new(SelectableText {
502 inner,
503 selectable,
504 context_menu,
505 window_id,
506 })
507 }
508}
509
510pub fn selectable_text<'a>(content: impl Into<Cow<'a, str>> + 'a) -> SelectableText<'a> {
516 SelectableText::new(content.into()).font(crate::font::default())
517}
518
519macro_rules! selectable_preset {
521 ($(#[$meta:meta])* $name:ident) => {
522 $(#[$meta])*
523 pub fn $name<'a>(content: impl Into<Cow<'a, str>> + 'a) -> SelectableText<'a> {
524 SelectableText::from(crate::widget::text::$name(content))
525 }
526 };
527}
528
529selectable_preset!(title1);
530selectable_preset!(title2);
531selectable_preset!(title3);
532selectable_preset!(title4);
533selectable_preset!(heading);
534selectable_preset!(caption_heading);
535selectable_preset!(body);
536selectable_preset!(caption);
537selectable_preset!(monotext);