Skip to main content

ferroui_controls/primitives/
access_text.rs

1use crate::TopLevel;
2use crate::{ControlImpl, TextBlock, TextBlockImpl, TextBlockImplExt};
3use ferroui_base::input::{AccessKeyHandler, IAccessKeyHandler, InputElementImpl};
4use ferroui_base::interactivity::InteractiveImpl;
5use ferroui_base::layout::LayoutableImpl;
6use ferroui_base::media::text_formatting::TextLayout;
7use ferroui_base::media::{DrawingContext, IPen, Pen};
8use ferroui_base::reactive::ObservableExt;
9use ferroui_base::{
10    ferro_class, ferro_property, instantiate, AttachedProperty, FerroObjectExtensions, FerroObjectImpl,
11    FerroObject, FerroObjectImplExt, Point, Ref, StyledElementImpl, StyledProperty, Visual, VisualImpl, VisualImplExt,
12    VisualTreeAttachmentEventArgs,
13};
14use std::cell::RefCell;
15use std::rc::Rc;
16
17/// A text block that displays a character prefixed with an underscore as an
18/// access key.
19#[repr(C)]
20pub struct AccessText {
21    base: TextBlock,
22    access_key: RefCell<Option<String>>,
23    access_keys: RefCell<Option<Rc<dyn IAccessKeyHandler>>>,
24}
25
26ferro_class!(AccessText: TextBlock);
27ferroui_base::ferro_class_info!(AccessText { new: AccessText::new });
28
29ferroui_base::ferro_impl_classes!(AccessText: StyledElementImpl);
30ferroui_base::ferro_impl_classes!(AccessText: LayoutableImpl);
31ferroui_base::ferro_impl_classes!(AccessText: InteractiveImpl);
32ferroui_base::ferro_impl_classes!(AccessText: InputElementImpl);
33impl ControlImpl for AccessText {
34    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
35        crate::automation::peers::NoneAutomationPeer::new(this).upcast()
36    }
37}
38
39impl FerroObjectImpl for AccessText {
40    fn constructed(this: &Self) {
41        Self::parent_constructed(this);
42
43        let weak = this.to_ref().downgrade();
44        FerroObjectExtensions::get_observable::<StyledProperty<Option<String>>>(as_object(this), TextBlock::text_property()).subscribe_fn(move |text| {
45            if let Some(this) = weak.upgrade() {
46                this.text_changed(text.as_deref());
47            }
48        });
49    }
50}
51
52impl VisualImpl for AccessText {
53    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
54        Self::parent_on_attached_to_visual_tree(this, e);
55        let access_keys = TopLevel::get_top_level(Some(this)).and_then(|top_level| top_level.access_key_handler());
56        *this.access_keys.borrow_mut() = access_keys.clone();
57
58        if let (Some(access_keys), Some(access_key)) = (access_keys, this.access_key()) {
59            if !access_key.is_empty() {
60                access_keys.register(&access_key, &this.to_ref().upcast());
61            }
62        }
63    }
64
65    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
66        Self::parent_on_detached_from_visual_tree(this, e);
67
68        let access_keys = this.access_keys.borrow().clone();
69        if let Some(access_keys) = access_keys {
70            if this.access_key().is_some_and(|access_key| !access_key.is_empty()) {
71                access_keys.unregister(this);
72                *this.access_keys.borrow_mut() = None;
73            }
74        }
75    }
76}
77
78impl TextBlockImpl for AccessText {
79    /// Renders the `AccessText` to a drawing context.
80    fn render_core(this: &Self, context: &mut DrawingContext) {
81        Self::parent_render_core(this, context);
82
83        let underscore = this.text().and_then(|text| utf16_index_of_underscore(&text));
84
85        if let Some(underscore) = underscore {
86            if this.show_access_key() {
87                let rect = this.text_layout().hit_test_text_position(underscore);
88
89                let x1 = rect.left().round();
90                let x2 = rect.right().round();
91                // A FerroUI addition: the underline moves with the text when the height layout
92                // rounding adds is above it (`TextBlock.LayoutRoundingPlacement`).
93                let y = (rect.bottom() + this.layout_rounding_offset()).round() - 1.5;
94
95                let pen: Rc<dyn IPen> = Pen::with_brush(this.foreground(), 1.0).into();
96
97                context.draw_line(&pen, Point::new(x1, y), Point::new(x2, y));
98            }
99        }
100    }
101
102    fn create_text_layout(this: &Self, text: Option<&str>) -> Rc<TextLayout> {
103        let text = Self::remove_access_key_marker(text);
104        Self::parent_create_text_layout(this, text.as_deref())
105    }
106}
107
108fn as_object(object: &FerroObject) -> &FerroObject {
109    object
110}
111
112/// The index, in UTF-16 code units, of the first underscore of `text`.
113fn utf16_index_of_underscore(text: &str) -> Option<i32> {
114    text.find('_').map(|index| text[..index].encode_utf16().count() as i32)
115}
116
117ferroui_base::ferro_properties! { impl AccessText {
118    ferro_property!(
119        /// Defines the `ShowAccessKey` attached property.
120        pub fn show_access_key_property() -> AttachedProperty<bool> {
121            AccessKeyHandler::show_access_key_property().add_owner::<AccessText>()
122        }
123    );
124} }
125
126impl AccessText {
127    fn static_constructor() {
128        Visual::affects_render::<AccessText>(&[Self::show_access_key_property().as_property()]);
129    }
130
131    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
132    pub fn construct() -> Self {
133        Self { base: TextBlock::construct(), access_key: RefCell::new(None), access_keys: RefCell::new(None) }
134    }
135
136    /// Initializes a new instance of the `AccessText` class.
137    pub fn new() -> Ref<Self> {
138        instantiate(Self::construct())
139    }
140
141    /// The access key.
142    pub fn access_key(&self) -> Option<String> {
143        self.access_key.borrow().clone()
144    }
145
146    /// A value indicating whether the access key should be underlined.
147    pub fn show_access_key(&self) -> bool {
148        self.get_value(Self::show_access_key_property())
149    }
150
151    pub fn set_show_access_key(&self, value: bool) {
152        self.set_value(Self::show_access_key_property(), value)
153    }
154
155    /// Removes the first access key marker of a text and unescapes doubled
156    /// markers.
157    pub fn remove_access_key_marker(text: Option<&str>) -> Option<String> {
158        let text = text?;
159        let mut text = text.to_owned();
160
161        if !text.is_empty() {
162            if let Some(index) = Self::find_access_key_marker(&text) {
163                if index < text.len() - 1 {
164                    text.remove(index);
165                }
166            }
167            text = text.replace("__", "_");
168        }
169
170        Some(text)
171    }
172
173    /// The byte index of the first underscore that is not followed by
174    /// another one.
175    fn find_access_key_marker(text: &str) -> Option<usize> {
176        let bytes = text.as_bytes();
177        let length = bytes.len();
178        let mut start_index = 0;
179
180        while start_index < length {
181            let index = start_index + bytes[start_index..].iter().position(|b| *b == b'_')?;
182            if index + 1 < length && bytes[index + 1] != b'_' {
183                return Some(index);
184            }
185            start_index = index + 2;
186        }
187
188        None
189    }
190
191    /// Called when the `Text` property changes.
192    fn text_changed(&self, text: Option<&str>) {
193        let mut key: Option<String> = None;
194
195        if let Some(text) = text {
196            if let Some(underscore) = text.find('_') {
197                // The key is the code point after the marker.
198                if let Some(rune) = text[underscore + 1..].chars().next() {
199                    key = Some(rune.to_string());
200                }
201            }
202        }
203
204        *self.access_key.borrow_mut() = key;
205
206        let access_keys = self.access_keys.borrow().clone();
207        if let (Some(access_keys), Some(access_key)) = (access_keys, self.access_key()) {
208            if !access_key.is_empty() {
209                access_keys.register(&access_key, &self.to_ref().upcast());
210            }
211        }
212    }
213}