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")]
105#[reflect(derived_type = "UiNode")]
106pub struct Border {
107    /// Base widget of the border. See [`Widget`] docs for more info.
108    pub widget: Widget,
109    /// Stroke thickness for each side of the border.
110    pub stroke_thickness: InheritableVariable<StyledProperty<Thickness>>,
111    /// Corner radius.
112    #[visit(optional)]
113    pub corner_radius: InheritableVariable<StyledProperty<f32>>,
114    /// Enables or disables padding the children nodes by corner radius. If disabled, then the
115    /// children nodes layout won't be affected by the corner radius.
116    #[visit(optional)]
117    pub pad_by_corner_radius: InheritableVariable<bool>,
118}
119
120impl ConstructorProvider<UiNode, UserInterface> for Border {
121    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
122        GraphNodeConstructor::new::<Self>()
123            .with_variant("Border", |ui| {
124                BorderBuilder::new(WidgetBuilder::new().with_name("Border"))
125                    .build(&mut ui.build_ctx())
126                    .into()
127            })
128            .with_group("Visual")
129    }
130}
131
132crate::define_widget_deref!(Border);
133
134/// Supported border-specific messages.
135#[derive(Debug, Clone, PartialEq)]
136pub enum BorderMessage {
137    /// Allows you to set stroke thickness at runtime. See [`Self::stroke_thickness`] docs for more.
138    StrokeThickness(StyledProperty<Thickness>),
139    /// Allows you to set corner radius at runtime. See [`Self::corner_radius`] docs for more.
140    CornerRadius(StyledProperty<f32>),
141    /// Allows you to enable or disable padding the children nodes by corner radius. See
142    /// [`Self::pad_by_corner_radius`] docs for more.
143    PadByCornerRadius(bool),
144}
145
146impl BorderMessage {
147    define_constructor!(
148        /// Creates a new [Self::StrokeThickness] message.
149        BorderMessage:StrokeThickness => fn stroke_thickness(StyledProperty<Thickness>), layout: false
150    );
151    define_constructor!(
152        /// Creates a new [Self::CornerRadius] message.
153        BorderMessage:CornerRadius => fn corner_radius(StyledProperty<f32>), layout: false
154    );
155    define_constructor!(
156        /// Creates a new [Self::PadByCornerRadius] message.
157        BorderMessage:PadByCornerRadius => fn pad_by_corner_radius(bool), layout: false
158    );
159}
160
161fn corner_offset(radius: f32) -> f32 {
162    radius * 0.5 * (std::f32::consts::SQRT_2 - 1.0)
163}
164
165impl Control for Border {
166    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
167        let corner_offset = if *self.pad_by_corner_radius {
168            corner_offset(**self.corner_radius)
169        } else {
170            0.0
171        };
172        let double_corner_offset = 2.0 * corner_offset;
173
174        let margin_x =
175            self.stroke_thickness.left + self.stroke_thickness.right + double_corner_offset;
176        let margin_y =
177            self.stroke_thickness.top + self.stroke_thickness.bottom + double_corner_offset;
178
179        let size_for_child = Vector2::new(available_size.x - margin_x, available_size.y - margin_y);
180        let mut desired_size = Vector2::default();
181
182        for child_handle in self.widget.children() {
183            ui.measure_node(*child_handle, size_for_child);
184            let child = ui.nodes.borrow(*child_handle);
185            let child_desired_size = child.desired_size();
186            if child_desired_size.x > desired_size.x {
187                desired_size.x = child_desired_size.x;
188            }
189            if child_desired_size.y > desired_size.y {
190                desired_size.y = child_desired_size.y;
191            }
192        }
193
194        desired_size.x += margin_x;
195        desired_size.y += margin_y;
196
197        desired_size
198    }
199
200    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
201        let corner_offset = if *self.pad_by_corner_radius {
202            corner_offset(**self.corner_radius)
203        } else {
204            0.0
205        };
206        let double_corner_offset = 2.0 * corner_offset;
207
208        let rect_for_child = Rect::new(
209            self.stroke_thickness.left + corner_offset,
210            self.stroke_thickness.top + corner_offset,
211            final_size.x
212                - (self.stroke_thickness.right + self.stroke_thickness.left + double_corner_offset),
213            final_size.y
214                - (self.stroke_thickness.bottom + self.stroke_thickness.top + double_corner_offset),
215        );
216
217        for child_handle in self.widget.children() {
218            ui.arrange_node(*child_handle, &rect_for_child);
219        }
220
221        final_size
222    }
223
224    fn draw(&self, drawing_context: &mut DrawingContext) {
225        let bounds = self.widget.bounding_rect();
226
227        if (**self.corner_radius).eq(&0.0) {
228            DrawingContext::push_rect_filled(drawing_context, &bounds, None);
229            drawing_context.commit(
230                self.clip_bounds(),
231                self.widget.background(),
232                CommandTexture::None,
233                &self.material,
234                None,
235            );
236
237            drawing_context.push_rect_vary(&bounds, **self.stroke_thickness);
238            drawing_context.commit(
239                self.clip_bounds(),
240                self.widget.foreground(),
241                CommandTexture::None,
242                &self.material,
243                None,
244            );
245        } else {
246            let corner_arc_length = std::f32::consts::TAU * **self.corner_radius * (90.0 / 360.0);
247            let corner_subdivisions = ((corner_arc_length as usize) / 2).min(4);
248
249            let thickness = self.stroke_thickness.left;
250            let half_thickness = thickness / 2.0;
251
252            DrawingContext::push_rounded_rect_filled(
253                drawing_context,
254                &bounds.deflate(half_thickness, half_thickness),
255                **self.corner_radius,
256                corner_subdivisions,
257            );
258            drawing_context.commit(
259                self.clip_bounds(),
260                self.widget.background(),
261                CommandTexture::None,
262                &self.material,
263                None,
264            );
265
266            drawing_context.push_rounded_rect(
267                &bounds,
268                thickness,
269                **self.corner_radius,
270                corner_subdivisions,
271            );
272            drawing_context.commit(
273                self.clip_bounds(),
274                self.widget.foreground(),
275                CommandTexture::None,
276                &self.material,
277                None,
278            );
279        }
280    }
281
282    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
283        self.widget.handle_routed_message(ui, message);
284
285        if message.destination() == self.handle()
286            && message.direction() == MessageDirection::ToWidget
287        {
288            if let Some(msg) = message.data::<BorderMessage>() {
289                match msg {
290                    BorderMessage::StrokeThickness(thickness) => {
291                        if *thickness != *self.stroke_thickness {
292                            self.stroke_thickness
293                                .set_value_and_mark_modified(thickness.clone());
294                            ui.send_message(message.reverse());
295                            self.invalidate_layout();
296                        }
297                    }
298                    BorderMessage::CornerRadius(radius) => {
299                        if *radius != *self.corner_radius {
300                            self.corner_radius
301                                .set_value_and_mark_modified(radius.clone());
302                            ui.send_message(message.reverse());
303                            self.invalidate_layout();
304                        }
305                    }
306                    BorderMessage::PadByCornerRadius(pad) => {
307                        if *pad != *self.pad_by_corner_radius {
308                            self.pad_by_corner_radius.set_value_and_mark_modified(*pad);
309                            ui.send_message(message.reverse());
310                            self.invalidate_layout();
311                        }
312                    }
313                }
314            }
315        }
316    }
317}
318
319/// Border builder.
320pub struct BorderBuilder {
321    /// Widget builder that will be used to build the base of the widget.
322    pub widget_builder: WidgetBuilder,
323    /// Stroke thickness for each side of the border. Default is 1px wide border for each side.
324    pub stroke_thickness: StyledProperty<Thickness>,
325    /// Radius at each of four corners of the border. Default is zero.
326    pub corner_radius: StyledProperty<f32>,
327    /// Enables or disables padding the children nodes by corner radius. If disabled, then the
328    /// children nodes layout won't be affected by the corner radius. Default is `true`.
329    pub pad_by_corner_radius: bool,
330}
331
332impl BorderBuilder {
333    /// Creates a new border builder with a widget builder specified.
334    pub fn new(widget_builder: WidgetBuilder) -> Self {
335        Self {
336            widget_builder,
337            stroke_thickness: Thickness::uniform(1.0).into(),
338            corner_radius: 0.0.into(),
339            pad_by_corner_radius: true,
340        }
341    }
342
343    /// Sets the desired stroke thickness for each side of the border.
344    pub fn with_stroke_thickness(mut self, stroke_thickness: StyledProperty<Thickness>) -> Self {
345        self.stroke_thickness = stroke_thickness;
346        self
347    }
348
349    /// Sets the desired corner radius.
350    pub fn with_corner_radius(mut self, corner_radius: StyledProperty<f32>) -> Self {
351        self.corner_radius = corner_radius;
352        self
353    }
354
355    /// Enables or disables padding the children nodes by corner radius.
356    pub fn with_pad_by_corner_radius(mut self, pad: bool) -> Self {
357        self.pad_by_corner_radius = pad;
358        self
359    }
360
361    /// Creates a [`Border`] widget, but does not add it to the user interface. Also see [`Self::build`] docs.
362    pub fn build_border(mut self, ctx: &BuildContext) -> Border {
363        if self.widget_builder.foreground.is_none() {
364            self.widget_builder.foreground = Some(ctx.style.property(Style::BRUSH_PRIMARY));
365        }
366        Border {
367            widget: self.widget_builder.build(ctx),
368            stroke_thickness: self.stroke_thickness.into(),
369            corner_radius: self.corner_radius.into(),
370            pad_by_corner_radius: self.pad_by_corner_radius.into(),
371        }
372    }
373
374    /// Finishes border building and adds it to the user interface. See examples in [`Border`] docs.
375    pub fn build(self, ctx: &mut BuildContext<'_>) -> Handle<UiNode> {
376        ctx.add_node(UiNode::new(self.build_border(ctx)))
377    }
378}
379
380#[cfg(test)]
381mod test {
382    use crate::border::BorderBuilder;
383    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
384
385    #[test]
386    fn test_deletion() {
387        test_widget_deletion(|ctx| BorderBuilder::new(WidgetBuilder::new()).build(ctx));
388    }
389}