fyrox_ui/
border.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21#![warn(missing_docs)]
22
23//! The Border widget provides a stylized, static border around its child widget. See [`Border`] docs for more info and
24//! usage examples.
25
26use crate::{
27    core::{
28        algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
29        variable::InheritableVariable, visitor::prelude::*,
30    },
31    define_constructor,
32    draw::{CommandTexture, Draw, DrawingContext},
33    message::UiMessage,
34    style::{resource::StyleResourceExt, Style, StyledProperty},
35    widget::{Widget, WidgetBuilder},
36    BuildContext, Control, MessageDirection, Thickness, UiNode, UserInterface,
37};
38use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
39use std::ops::{Deref, DerefMut};
40
41/// The Border widget provides a stylized, static border around its child widget. Below is an example of creating a 1 pixel
42/// thick border around a button widget:
43///
44/// ```rust
45/// use fyrox_ui::{
46///     UserInterface,
47///     widget::WidgetBuilder,
48///     border::BorderBuilder,
49///     Thickness,
50///     text::TextBuilder,
51/// };
52///
53/// fn create_border_with_button(ui: &mut UserInterface) {
54///     BorderBuilder::new(
55///         WidgetBuilder::new()
56///             .with_child(
57///                 TextBuilder::new(WidgetBuilder::new())
58///                     .with_text("I'm boxed in!")
59///                     .build(&mut ui.build_ctx())
60///             )
61///     )
62///     //You can also use Thickness::uniform(1.0)
63///     .with_stroke_thickness(Thickness {left: 1.0, right: 1.0, top: 1.0, bottom: 1.0}.into())
64///     .build(&mut ui.build_ctx());
65/// }
66/// ```
67///
68/// As with other UI elements, we create the border using the BorderBuilder helper struct. The widget that should have a
69/// border around it is added as a child of the base WidgetBuilder, and the border thickness can be set by providing a
70/// Thickness struct to the BorderBuilder's *with_stroke_thickness* function. This means you can set different thicknesses
71/// for each edge of the border.
72///
73/// You can style the border by creating a Brush and setting the border's base WidgetBuilder's foreground or background.
74/// The foreground will set the style of the boarder itself, while setting the background will color the whole area within
75/// the border. Below is an example of a blue border and a red background with white text inside.
76///
77/// ```rust
78/// # use fyrox_ui::{
79/// #     brush::Brush,
80/// #     core::color::Color,
81/// #     widget::WidgetBuilder,
82/// #     text::TextBuilder,
83/// #     border::BorderBuilder,
84/// #     UserInterface,
85/// #     Thickness,
86/// # };
87///
88/// # let mut ui = UserInterface::new(Default::default());
89///
90/// BorderBuilder::new(
91///     WidgetBuilder::new()
92///         .with_foreground(Brush::Solid(Color::opaque(0, 0, 200)).into())
93///         .with_background(Brush::Solid(Color::opaque(200, 0, 0)).into())
94///         .with_child(
95///             TextBuilder::new(WidgetBuilder::new())
96///                 .with_text("I'm boxed in Blue and backed in Red!")
97///                 .build(&mut ui.build_ctx())
98///         )
99/// )
100/// .with_stroke_thickness(Thickness {left: 2.0, right: 2.0, top: 2.0, bottom: 2.0}.into())
101/// .build(&mut ui.build_ctx());
102/// ```
103#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
104#[type_uuid(id = "6aba3dc5-831d-481a-bc83-ec10b2b2bf12")]
105pub struct Border {
106    /// Base widget of the border. See [`Widget`] docs for more info.
107    pub widget: Widget,
108    /// Stroke thickness for each side of the border.
109    pub stroke_thickness: InheritableVariable<StyledProperty<Thickness>>,
110    /// Corner radius.
111    #[visit(optional)]
112    pub corner_radius: InheritableVariable<StyledProperty<f32>>,
113    /// Enables or disables padding the children nodes by corner radius. If disabled, then the
114    /// children nodes layout won't be affected by the corner radius.
115    #[visit(optional)]
116    pub pad_by_corner_radius: InheritableVariable<bool>,
117}
118
119impl ConstructorProvider<UiNode, UserInterface> for Border {
120    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
121        GraphNodeConstructor::new::<Self>()
122            .with_variant("Border", |ui| {
123                BorderBuilder::new(WidgetBuilder::new().with_name("Border"))
124                    .build(&mut ui.build_ctx())
125                    .into()
126            })
127            .with_group("Visual")
128    }
129}
130
131crate::define_widget_deref!(Border);
132
133/// Supported border-specific messages.
134#[derive(Debug, Clone, PartialEq)]
135pub enum BorderMessage {
136    /// Allows you to set stroke thickness at runtime. See [`Self::stroke_thickness`] docs for more.
137    StrokeThickness(StyledProperty<Thickness>),
138    /// Allows you to set corner radius at runtime. See [`Self::corner_radius`] docs for more.
139    CornerRadius(StyledProperty<f32>),
140    /// Allows you to enable or disable padding the children nodes by corner radius. See
141    /// [`Self::pad_by_corner_radius`] docs for more.
142    PadByCornerRadius(bool),
143}
144
145impl BorderMessage {
146    define_constructor!(
147        /// Creates a new [Self::StrokeThickness] message.
148        BorderMessage:StrokeThickness => fn stroke_thickness(StyledProperty<Thickness>), layout: false
149    );
150    define_constructor!(
151        /// Creates a new [Self::CornerRadius] message.
152        BorderMessage:CornerRadius => fn corner_radius(StyledProperty<f32>), layout: false
153    );
154    define_constructor!(
155        /// Creates a new [Self::PadByCornerRadius] message.
156        BorderMessage:PadByCornerRadius => fn pad_by_corner_radius(bool), layout: false
157    );
158}
159
160fn corner_offset(radius: f32) -> f32 {
161    radius * 0.5 * (std::f32::consts::SQRT_2 - 1.0)
162}
163
164impl Control for Border {
165    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
166        let corner_offset = if *self.pad_by_corner_radius {
167            corner_offset(**self.corner_radius)
168        } else {
169            0.0
170        };
171        let double_corner_offset = 2.0 * corner_offset;
172
173        let margin_x =
174            self.stroke_thickness.left + self.stroke_thickness.right + double_corner_offset;
175        let margin_y =
176            self.stroke_thickness.top + self.stroke_thickness.bottom + double_corner_offset;
177
178        let size_for_child = Vector2::new(available_size.x - margin_x, available_size.y - margin_y);
179        let mut desired_size = Vector2::default();
180
181        for child_handle in self.widget.children() {
182            ui.measure_node(*child_handle, size_for_child);
183            let child = ui.nodes.borrow(*child_handle);
184            let child_desired_size = child.desired_size();
185            if child_desired_size.x > desired_size.x {
186                desired_size.x = child_desired_size.x;
187            }
188            if child_desired_size.y > desired_size.y {
189                desired_size.y = child_desired_size.y;
190            }
191        }
192
193        desired_size.x += margin_x;
194        desired_size.y += margin_y;
195
196        desired_size
197    }
198
199    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
200        let corner_offset = if *self.pad_by_corner_radius {
201            corner_offset(**self.corner_radius)
202        } else {
203            0.0
204        };
205        let double_corner_offset = 2.0 * corner_offset;
206
207        let rect_for_child = Rect::new(
208            self.stroke_thickness.left + corner_offset,
209            self.stroke_thickness.top + corner_offset,
210            final_size.x
211                - (self.stroke_thickness.right + self.stroke_thickness.left + double_corner_offset),
212            final_size.y
213                - (self.stroke_thickness.bottom + self.stroke_thickness.top + double_corner_offset),
214        );
215
216        for child_handle in self.widget.children() {
217            ui.arrange_node(*child_handle, &rect_for_child);
218        }
219
220        final_size
221    }
222
223    fn draw(&self, drawing_context: &mut DrawingContext) {
224        let bounds = self.widget.bounding_rect();
225
226        if (**self.corner_radius).eq(&0.0) {
227            DrawingContext::push_rect_filled(drawing_context, &bounds, None);
228            drawing_context.commit(
229                self.clip_bounds(),
230                self.widget.background(),
231                CommandTexture::None,
232                None,
233            );
234
235            drawing_context.push_rect_vary(&bounds, **self.stroke_thickness);
236            drawing_context.commit(
237                self.clip_bounds(),
238                self.widget.foreground(),
239                CommandTexture::None,
240                None,
241            );
242        } else {
243            let thickness = self.stroke_thickness.left;
244            let half_thickness = thickness / 2.0;
245
246            DrawingContext::push_rounded_rect_filled(
247                drawing_context,
248                &bounds.deflate(half_thickness, half_thickness),
249                **self.corner_radius,
250                16,
251            );
252            drawing_context.commit(
253                self.clip_bounds(),
254                self.widget.background(),
255                CommandTexture::None,
256                None,
257            );
258
259            drawing_context.push_rounded_rect(&bounds, thickness, **self.corner_radius, 16);
260            drawing_context.commit(
261                self.clip_bounds(),
262                self.widget.foreground(),
263                CommandTexture::None,
264                None,
265            );
266        }
267    }
268
269    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
270        self.widget.handle_routed_message(ui, message);
271
272        if message.destination() == self.handle()
273            && message.direction() == MessageDirection::ToWidget
274        {
275            if let Some(msg) = message.data::<BorderMessage>() {
276                match msg {
277                    BorderMessage::StrokeThickness(thickness) => {
278                        if *thickness != *self.stroke_thickness {
279                            self.stroke_thickness
280                                .set_value_and_mark_modified(thickness.clone());
281                            ui.send_message(message.reverse());
282                            self.invalidate_layout();
283                        }
284                    }
285                    BorderMessage::CornerRadius(radius) => {
286                        if *radius != *self.corner_radius {
287                            self.corner_radius
288                                .set_value_and_mark_modified(radius.clone());
289                            ui.send_message(message.reverse());
290                            self.invalidate_layout();
291                        }
292                    }
293                    BorderMessage::PadByCornerRadius(pad) => {
294                        if *pad != *self.pad_by_corner_radius {
295                            self.pad_by_corner_radius.set_value_and_mark_modified(*pad);
296                            ui.send_message(message.reverse());
297                            self.invalidate_layout();
298                        }
299                    }
300                }
301            }
302        }
303    }
304}
305
306/// Border builder.
307pub struct BorderBuilder {
308    /// Widget builder that will be used to build the base of the widget.
309    pub widget_builder: WidgetBuilder,
310    /// Stroke thickness for each side of the border. Default is 1px wide border for each side.
311    pub stroke_thickness: StyledProperty<Thickness>,
312    /// Radius at each of four corners of the border. Default is zero.
313    pub corner_radius: StyledProperty<f32>,
314    /// Enables or disables padding the children nodes by corner radius. If disabled, then the
315    /// children nodes layout won't be affected by the corner radius. Default is `true`.
316    pub pad_by_corner_radius: bool,
317}
318
319impl BorderBuilder {
320    /// Creates a new border builder with a widget builder specified.
321    pub fn new(widget_builder: WidgetBuilder) -> Self {
322        Self {
323            widget_builder,
324            stroke_thickness: Thickness::uniform(1.0).into(),
325            corner_radius: 0.0.into(),
326            pad_by_corner_radius: true,
327        }
328    }
329
330    /// Sets the desired stroke thickness for each side of the border.
331    pub fn with_stroke_thickness(mut self, stroke_thickness: StyledProperty<Thickness>) -> Self {
332        self.stroke_thickness = stroke_thickness;
333        self
334    }
335
336    /// Sets the desired corner radius.
337    pub fn with_corner_radius(mut self, corner_radius: StyledProperty<f32>) -> Self {
338        self.corner_radius = corner_radius;
339        self
340    }
341
342    /// Enables or disables padding the children nodes by corner radius.
343    pub fn with_pad_by_corner_radius(mut self, pad: bool) -> Self {
344        self.pad_by_corner_radius = pad;
345        self
346    }
347
348    /// Creates a [`Border`] widget, but does not add it to the user interface. Also see [`Self::build`] docs.
349    pub fn build_border(mut self, ctx: &BuildContext) -> Border {
350        if self.widget_builder.foreground.is_none() {
351            self.widget_builder.foreground = Some(ctx.style.property(Style::BRUSH_PRIMARY));
352        }
353        Border {
354            widget: self.widget_builder.build(ctx),
355            stroke_thickness: self.stroke_thickness.into(),
356            corner_radius: self.corner_radius.into(),
357            pad_by_corner_radius: self.pad_by_corner_radius.into(),
358        }
359    }
360
361    /// Finishes border building and adds it to the user interface. See examples in [`Border`] docs.
362    pub fn build(self, ctx: &mut BuildContext<'_>) -> Handle<UiNode> {
363        ctx.add_node(UiNode::new(self.build_border(ctx)))
364    }
365}
366
367#[cfg(test)]
368mod test {
369    use crate::border::BorderBuilder;
370    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
371
372    #[test]
373    fn test_deletion() {
374        test_widget_deletion(|ctx| BorderBuilder::new(WidgetBuilder::new()).build(ctx));
375    }
376}