Skip to main content

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::message::MessageData;
27use crate::{
28    core::{
29        algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
30        variable::InheritableVariable, visitor::prelude::*,
31    },
32    draw::{CommandTexture, Draw, DrawingContext},
33    message::UiMessage,
34    style::{resource::StyleResourceExt, Style, StyledProperty},
35    widget::{Widget, WidgetBuilder},
36    BuildContext, Control, Thickness, UiNode, UserInterface,
37};
38use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
39
40/// The Border widget provides a stylized, static border around its child widget. Below is an example of creating a 1 pixel
41/// thick border around a button widget:
42///
43/// ```rust
44/// use fyrox_ui::{
45///     UserInterface,
46///     widget::WidgetBuilder,
47///     border::BorderBuilder,
48///     Thickness,
49///     text::TextBuilder,
50/// };
51///
52/// fn create_border_with_button(ui: &mut UserInterface) {
53///     BorderBuilder::new(
54///         WidgetBuilder::new()
55///             .with_child(
56///                 TextBuilder::new(WidgetBuilder::new())
57///                     .with_text("I'm boxed in!")
58///                     .build(&mut ui.build_ctx())
59///             )
60///     )
61///     //You can also use Thickness::uniform(1.0)
62///     .with_stroke_thickness(Thickness {left: 1.0, right: 1.0, top: 1.0, bottom: 1.0}.into())
63///     .build(&mut ui.build_ctx());
64/// }
65/// ```
66///
67/// As with other UI elements, we create the border using the BorderBuilder helper struct. The widget that should have a
68/// border around it is added as a child of the base WidgetBuilder, and the border thickness can be set by providing a
69/// Thickness struct to the BorderBuilder's *with_stroke_thickness* function. This means you can set different thicknesses
70/// for each edge of the border.
71///
72/// You can style the border by creating a Brush and setting the border's base WidgetBuilder's foreground or background.
73/// The foreground will set the style of the boarder itself, while setting the background will color the whole area within
74/// the border. Below is an example of a blue border and a red background with white text inside.
75///
76/// ```rust
77/// # use fyrox_ui::{
78/// #     brush::Brush,
79/// #     core::color::Color,
80/// #     widget::WidgetBuilder,
81/// #     text::TextBuilder,
82/// #     border::BorderBuilder,
83/// #     UserInterface,
84/// #     Thickness,
85/// # };
86///
87/// # let mut ui = UserInterface::new(Default::default());
88///
89/// BorderBuilder::new(
90///     WidgetBuilder::new()
91///         .with_foreground(Brush::Solid(Color::opaque(0, 0, 200)).into())
92///         .with_background(Brush::Solid(Color::opaque(200, 0, 0)).into())
93///         .with_child(
94///             TextBuilder::new(WidgetBuilder::new())
95///                 .with_text("I'm boxed in Blue and backed in Red!")
96///                 .build(&mut ui.build_ctx())
97///         )
98/// )
99/// .with_stroke_thickness(Thickness {left: 2.0, right: 2.0, top: 2.0, bottom: 2.0}.into())
100/// .build(&mut ui.build_ctx());
101/// ```
102#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
103#[type_uuid(id = "6aba3dc5-831d-481a-bc83-ec10b2b2bf12")]
104#[reflect(derived_type = "UiNode")]
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                    .to_base()
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.
138    StrokeThickness(StyledProperty<Thickness>),
139    /// Allows you to set corner radius at runtime.
140    CornerRadius(StyledProperty<f32>),
141    /// Allows you to enable or disable padding the children nodes by corner radius.
142    PadByCornerRadius(bool),
143}
144impl MessageData for BorderMessage {}
145
146fn corner_offset(radius: f32) -> f32 {
147    radius * 0.5 * (std::f32::consts::SQRT_2 - 1.0)
148}
149
150impl Control for Border {
151    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
152        let corner_offset = if *self.pad_by_corner_radius {
153            corner_offset(**self.corner_radius)
154        } else {
155            0.0
156        };
157        let double_corner_offset = 2.0 * corner_offset;
158
159        let margin_x =
160            self.stroke_thickness.left + self.stroke_thickness.right + double_corner_offset;
161        let margin_y =
162            self.stroke_thickness.top + self.stroke_thickness.bottom + double_corner_offset;
163
164        let size_for_child = Vector2::new(available_size.x - margin_x, available_size.y - margin_y);
165        let mut desired_size = Vector2::default();
166
167        for child_handle in self.widget.children() {
168            ui.measure_node(*child_handle, size_for_child);
169            let child = ui.nodes.borrow(*child_handle);
170            let child_desired_size = child.desired_size();
171            if child_desired_size.x > desired_size.x {
172                desired_size.x = child_desired_size.x;
173            }
174            if child_desired_size.y > desired_size.y {
175                desired_size.y = child_desired_size.y;
176            }
177        }
178
179        desired_size.x += margin_x;
180        desired_size.y += margin_y;
181
182        desired_size
183    }
184
185    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
186        let corner_offset = if *self.pad_by_corner_radius {
187            corner_offset(**self.corner_radius)
188        } else {
189            0.0
190        };
191        let double_corner_offset = 2.0 * corner_offset;
192
193        let rect_for_child = Rect::new(
194            self.stroke_thickness.left + corner_offset,
195            self.stroke_thickness.top + corner_offset,
196            final_size.x
197                - (self.stroke_thickness.right + self.stroke_thickness.left + double_corner_offset),
198            final_size.y
199                - (self.stroke_thickness.bottom + self.stroke_thickness.top + double_corner_offset),
200        );
201
202        for child_handle in self.widget.children() {
203            ui.arrange_node(*child_handle, &rect_for_child);
204        }
205
206        final_size
207    }
208
209    fn draw(&self, drawing_context: &mut DrawingContext) {
210        let bounds = self.widget.bounding_rect();
211
212        if (**self.corner_radius).eq(&0.0) {
213            DrawingContext::push_rect_filled(drawing_context, &bounds, None);
214            drawing_context.commit(
215                self.clip_bounds(),
216                self.widget.background(),
217                CommandTexture::None,
218                &self.material,
219                None,
220            );
221
222            drawing_context.push_rect_vary(&bounds, **self.stroke_thickness);
223            drawing_context.commit(
224                self.clip_bounds(),
225                self.widget.foreground(),
226                CommandTexture::None,
227                &self.material,
228                None,
229            );
230        } else {
231            let corner_arc_length = std::f32::consts::TAU * **self.corner_radius * (90.0 / 360.0);
232            let corner_subdivisions = ((corner_arc_length as usize) / 2).min(4);
233
234            let thickness = self.stroke_thickness.left;
235            let half_thickness = thickness / 2.0;
236
237            DrawingContext::push_rounded_rect_filled(
238                drawing_context,
239                &bounds.deflate(half_thickness, half_thickness),
240                **self.corner_radius,
241                corner_subdivisions,
242            );
243            drawing_context.commit(
244                self.clip_bounds(),
245                self.widget.background(),
246                CommandTexture::None,
247                &self.material,
248                None,
249            );
250
251            drawing_context.push_rounded_rect(
252                &bounds,
253                thickness,
254                **self.corner_radius,
255                corner_subdivisions,
256            );
257            drawing_context.commit(
258                self.clip_bounds(),
259                self.widget.foreground(),
260                CommandTexture::None,
261                &self.material,
262                None,
263            );
264        }
265    }
266
267    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
268        self.widget.handle_routed_message(ui, message);
269
270        if let Some(msg) = message.data_for::<BorderMessage>(self.handle()) {
271            match msg {
272                BorderMessage::StrokeThickness(thickness) => {
273                    if *thickness != *self.stroke_thickness {
274                        self.stroke_thickness
275                            .set_value_and_mark_modified(thickness.clone());
276                        ui.send_message(message.reverse());
277                        self.invalidate_layout();
278                    }
279                }
280                BorderMessage::CornerRadius(radius) => {
281                    if *radius != *self.corner_radius {
282                        self.corner_radius
283                            .set_value_and_mark_modified(radius.clone());
284                        ui.send_message(message.reverse());
285                        self.invalidate_layout();
286                    }
287                }
288                BorderMessage::PadByCornerRadius(pad) => {
289                    if *pad != *self.pad_by_corner_radius {
290                        self.pad_by_corner_radius.set_value_and_mark_modified(*pad);
291                        ui.send_message(message.reverse());
292                        self.invalidate_layout();
293                    }
294                }
295            }
296        }
297    }
298}
299
300/// Border builder.
301pub struct BorderBuilder {
302    /// Widget builder that will be used to build the base of the widget.
303    pub widget_builder: WidgetBuilder,
304    /// Stroke thickness for each side of the border. Default is 1px wide border for each side.
305    pub stroke_thickness: StyledProperty<Thickness>,
306    /// Radius at each of four corners of the border. Default is zero.
307    pub corner_radius: StyledProperty<f32>,
308    /// Enables or disables padding the children nodes by corner radius. If disabled, then the
309    /// children nodes layout won't be affected by the corner radius. Default is `true`.
310    pub pad_by_corner_radius: bool,
311}
312
313impl BorderBuilder {
314    /// Creates a new border builder with a widget builder specified.
315    pub fn new(widget_builder: WidgetBuilder) -> Self {
316        Self {
317            widget_builder,
318            stroke_thickness: Thickness::uniform(1.0).into(),
319            corner_radius: 0.0.into(),
320            pad_by_corner_radius: true,
321        }
322    }
323
324    /// Sets the desired stroke thickness for each side of the border.
325    pub fn with_stroke_thickness(mut self, stroke_thickness: StyledProperty<Thickness>) -> Self {
326        self.stroke_thickness = stroke_thickness;
327        self
328    }
329
330    /// Sets the desired corner radius.
331    pub fn with_corner_radius(mut self, corner_radius: StyledProperty<f32>) -> Self {
332        self.corner_radius = corner_radius;
333        self
334    }
335
336    /// Enables or disables padding the children nodes by corner radius.
337    pub fn with_pad_by_corner_radius(mut self, pad: bool) -> Self {
338        self.pad_by_corner_radius = pad;
339        self
340    }
341
342    /// Creates a [`Border`] widget, but does not add it to the user interface. Also see [`Self::build`] docs.
343    pub fn build_border(mut self, ctx: &BuildContext) -> Border {
344        if self.widget_builder.foreground.is_none() {
345            self.widget_builder.foreground = Some(ctx.style.property(Style::BRUSH_PRIMARY));
346        }
347        Border {
348            widget: self.widget_builder.build(ctx),
349            stroke_thickness: self.stroke_thickness.into(),
350            corner_radius: self.corner_radius.into(),
351            pad_by_corner_radius: self.pad_by_corner_radius.into(),
352        }
353    }
354
355    /// Finishes border building and adds it to the user interface. See examples in [`Border`] docs.
356    pub fn build(self, ctx: &mut BuildContext<'_>) -> Handle<Border> {
357        ctx.add(self.build_border(ctx))
358    }
359}
360
361#[cfg(test)]
362mod test {
363    use crate::border::BorderBuilder;
364    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
365
366    #[test]
367    fn test_deletion() {
368        test_widget_deletion(|ctx| BorderBuilder::new(WidgetBuilder::new()).build(ctx));
369    }
370}