1#![warn(missing_docs)]
22
23use 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#[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 pub widget: Widget,
108 pub stroke_thickness: InheritableVariable<StyledProperty<Thickness>>,
110 #[visit(optional)]
112 pub corner_radius: InheritableVariable<StyledProperty<f32>>,
113 #[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#[derive(Debug, Clone, PartialEq)]
136pub enum BorderMessage {
137 StrokeThickness(StyledProperty<Thickness>),
139 CornerRadius(StyledProperty<f32>),
141 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
300pub struct BorderBuilder {
302 pub widget_builder: WidgetBuilder,
304 pub stroke_thickness: StyledProperty<Thickness>,
306 pub corner_radius: StyledProperty<f32>,
308 pub pad_by_corner_radius: bool,
311}
312
313impl BorderBuilder {
314 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 pub fn with_stroke_thickness(mut self, stroke_thickness: StyledProperty<Thickness>) -> Self {
326 self.stroke_thickness = stroke_thickness;
327 self
328 }
329
330 pub fn with_corner_radius(mut self, corner_radius: StyledProperty<f32>) -> Self {
332 self.corner_radius = corner_radius;
333 self
334 }
335
336 pub fn with_pad_by_corner_radius(mut self, pad: bool) -> Self {
338 self.pad_by_corner_radius = pad;
339 self
340 }
341
342 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 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}