fyrox_ui/color/
gradient.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
21use crate::menu::ContextMenuBuilder;
22use crate::{
23    brush::Brush,
24    color::{ColorFieldBuilder, ColorFieldMessage},
25    core::{
26        algebra::Vector2,
27        color::Color,
28        color_gradient::{ColorGradient, GradientPoint},
29        math::Rect,
30        pool::Handle,
31        reflect::prelude::*,
32        type_traits::prelude::*,
33        visitor::prelude::*,
34    },
35    define_constructor, define_widget_deref,
36    draw::{CommandTexture, Draw, DrawingContext},
37    grid::{Column, GridBuilder, Row},
38    menu::{MenuItemBuilder, MenuItemContent, MenuItemMessage},
39    message::{CursorIcon, MessageDirection, MouseButton, UiMessage},
40    popup::{Placement, PopupBuilder, PopupMessage},
41    stack_panel::StackPanelBuilder,
42    widget::{Widget, WidgetBuilder, WidgetMessage},
43    BuildContext, Control, RcUiNodeHandle, UiNode, UserInterface,
44};
45
46use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
47use fyrox_graph::BaseSceneGraph;
48use std::{
49    cell::Cell,
50    ops::{Deref, DerefMut},
51};
52
53#[derive(Debug, Clone, PartialEq)]
54pub enum ColorGradientEditorMessage {
55    /// Sets new color gradient.
56    Value(ColorGradient),
57}
58
59impl ColorGradientEditorMessage {
60    define_constructor!(ColorGradientEditorMessage:Value => fn value(ColorGradient), layout: false);
61}
62
63#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
64#[type_uuid(id = "50d00eb7-f30b-4973-8a36-03d6b8f007ec")]
65#[reflect(derived_type = "UiNode")]
66pub struct ColorGradientField {
67    widget: Widget,
68    color_gradient: ColorGradient,
69}
70
71impl ConstructorProvider<UiNode, UserInterface> for ColorGradientField {
72    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
73        GraphNodeConstructor::new::<Self>()
74            .with_variant("Color Gradient Field", |ui| {
75                ColorGradientFieldBuilder::new(
76                    WidgetBuilder::new().with_name("Color Gradient Field"),
77                )
78                .build(&mut ui.build_ctx())
79                .into()
80            })
81            .with_group("Color")
82    }
83}
84
85define_widget_deref!(ColorGradientField);
86
87const SYNC_FLAG: u64 = 1;
88
89impl Control for ColorGradientField {
90    fn draw(&self, drawing_context: &mut DrawingContext) {
91        // Draw checkerboard background.
92        super::draw_checker_board(
93            self.bounding_rect(),
94            self.clip_bounds(),
95            6.0,
96            &self.material,
97            drawing_context,
98        );
99
100        let size = self.bounding_rect().size;
101
102        if self.color_gradient.points().is_empty() {
103            drawing_context.push_rect_filled(&self.bounding_rect(), None);
104
105            drawing_context.commit(
106                self.clip_bounds(),
107                Brush::Solid(ColorGradient::STUB_COLOR),
108                CommandTexture::None,
109                &self.material,
110                None,
111            );
112        } else {
113            let first = self.color_gradient.points().first().unwrap();
114            drawing_context.push_rect_multicolor(
115                &Rect::new(0.0, 0.0, first.location() * size.x, size.y),
116                [first.color(), first.color(), first.color(), first.color()],
117            );
118
119            for pair in self.color_gradient.points().windows(2) {
120                let left = &pair[0];
121                let right = &pair[1];
122
123                let left_pos = left.location() * size.x;
124                let right_pos = right.location() * size.x;
125                let bounds = Rect::new(left_pos, 0.0, right_pos - left_pos, size.y);
126
127                drawing_context.push_rect_multicolor(
128                    &bounds,
129                    [left.color(), right.color(), right.color(), left.color()],
130                );
131            }
132
133            let last = self.color_gradient.points().last().unwrap();
134            let last_pos = last.location() * size.x;
135            drawing_context.push_rect_multicolor(
136                &Rect::new(last_pos, 0.0, size.x - last_pos, size.y),
137                [last.color(), last.color(), last.color(), last.color()],
138            );
139
140            drawing_context.commit(
141                self.clip_bounds(),
142                Brush::Solid(Color::WHITE),
143                CommandTexture::None,
144                &self.material,
145                None,
146            );
147        }
148    }
149
150    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
151        self.widget.handle_routed_message(ui, message);
152
153        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
154        {
155            if let Some(ColorGradientEditorMessage::Value(value)) = message.data() {
156                self.color_gradient = value.clone();
157            }
158        }
159    }
160}
161
162pub struct ColorGradientFieldBuilder {
163    widget_builder: WidgetBuilder,
164    color_gradient: ColorGradient,
165}
166
167impl ColorGradientFieldBuilder {
168    pub fn new(widget_builder: WidgetBuilder) -> Self {
169        Self {
170            widget_builder,
171            color_gradient: Default::default(),
172        }
173    }
174
175    pub fn with_color_gradient(mut self, gradient: ColorGradient) -> Self {
176        self.color_gradient = gradient;
177        self
178    }
179
180    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
181        let field = ColorGradientField {
182            widget: self.widget_builder.build(ctx),
183            color_gradient: self.color_gradient,
184        };
185
186        ctx.add_node(UiNode::new(field))
187    }
188}
189
190#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
191#[type_uuid(id = "82843d8b-1972-46e6-897c-9619b74059cc")]
192#[reflect(derived_type = "UiNode")]
193pub struct ColorGradientEditor {
194    widget: Widget,
195    gradient_field: Handle<UiNode>,
196    selector_field: Handle<UiNode>,
197    points_canvas: Handle<UiNode>,
198    context_menu: RcUiNodeHandle,
199    point_context_menu: RcUiNodeHandle,
200    add_point: Handle<UiNode>,
201    remove_point: Handle<UiNode>,
202    context_menu_target: Cell<Handle<UiNode>>,
203    context_menu_open_position: Cell<Vector2<f32>>,
204}
205
206impl ConstructorProvider<UiNode, UserInterface> for ColorGradientEditor {
207    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
208        GraphNodeConstructor::new::<Self>()
209            .with_variant("Color Gradient Editor", |ui| {
210                ColorGradientEditorBuilder::new(
211                    WidgetBuilder::new().with_name("Color Gradient Editor"),
212                )
213                .build(&mut ui.build_ctx())
214                .into()
215            })
216            .with_group("Color")
217    }
218}
219
220define_widget_deref!(ColorGradientEditor);
221
222impl Control for ColorGradientEditor {
223    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
224        self.widget.handle_routed_message(ui, message);
225
226        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
227        {
228            if let Some(ColorGradientEditorMessage::Value(value)) = message.data() {
229                // Re-cast to inner field.
230                ui.send_message(ColorGradientEditorMessage::value(
231                    self.gradient_field,
232                    MessageDirection::ToWidget,
233                    value.clone(),
234                ));
235
236                for &point in ui.node(self.points_canvas).children() {
237                    ui.send_message(WidgetMessage::remove(point, MessageDirection::ToWidget));
238                }
239
240                let points = create_color_points(
241                    value,
242                    self.point_context_menu.clone(),
243                    &mut ui.build_ctx(),
244                );
245
246                for point in points {
247                    ui.send_message(WidgetMessage::link(
248                        point,
249                        MessageDirection::ToWidget,
250                        self.points_canvas,
251                    ));
252                }
253            }
254        }
255
256        if message.direction() == MessageDirection::FromWidget {
257            if let Some(ColorPointMessage::Location(_)) = message.data() {
258                let gradient = self.fetch_gradient(Handle::NONE, ui);
259
260                ui.send_message(ColorGradientEditorMessage::value(
261                    self.handle,
262                    MessageDirection::FromWidget,
263                    gradient,
264                ));
265            }
266        }
267    }
268
269    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
270        if let Some(MenuItemMessage::Click) = message.data() {
271            if message.destination() == self.add_point {
272                let mut gradient = self.fetch_gradient(Handle::NONE, ui);
273
274                let location = (self
275                    .screen_to_local(self.context_menu_open_position.get())
276                    .x
277                    / self.actual_local_size().x)
278                    .clamp(0.0, 1.0);
279
280                gradient.add_point(GradientPoint::new(location, Color::WHITE));
281
282                ui.send_message(ColorGradientEditorMessage::value(
283                    self.handle,
284                    MessageDirection::FromWidget,
285                    gradient,
286                ));
287            } else if message.destination() == self.remove_point
288                && ui.try_get_node(self.context_menu_target.get()).is_some()
289            {
290                let gradient = self.fetch_gradient(self.context_menu_target.get(), ui);
291
292                ui.send_message(ColorGradientEditorMessage::value(
293                    self.handle,
294                    MessageDirection::FromWidget,
295                    gradient,
296                ));
297            }
298        } else if let Some(ColorFieldMessage::Color(color)) = message.data() {
299            if message.destination() == self.selector_field
300                && message.direction() == MessageDirection::FromWidget
301                && message.flags != SYNC_FLAG
302            {
303                let mut gradient = ColorGradient::new();
304
305                for (handle, pt) in ui
306                    .node(self.points_canvas)
307                    .children()
308                    .iter()
309                    .map(|c| (*c, ui.node(*c).query_component::<ColorPoint>().unwrap()))
310                {
311                    gradient.add_point(GradientPoint::new(
312                        pt.location,
313                        if handle == self.context_menu_target.get() {
314                            *color
315                        } else {
316                            pt.color()
317                        },
318                    ));
319                }
320
321                ui.send_message(ColorGradientEditorMessage::value(
322                    self.handle,
323                    MessageDirection::FromWidget,
324                    gradient,
325                ));
326            }
327        } else if let Some(PopupMessage::Placement(Placement::Cursor(target))) = message.data() {
328            if message.destination() == self.context_menu.handle()
329                || message.destination() == self.point_context_menu.handle()
330            {
331                self.context_menu_open_position.set(ui.cursor_position());
332                self.context_menu_target.set(*target);
333
334                if message.destination() == self.point_context_menu.handle() {
335                    if let Some(point) = ui
336                        .try_get_node(self.context_menu_target.get())
337                        .and_then(|n| n.query_component::<ColorPoint>())
338                    {
339                        let mut msg = ColorFieldMessage::color(
340                            self.selector_field,
341                            MessageDirection::ToWidget,
342                            point.color(),
343                        );
344
345                        msg.flags = SYNC_FLAG;
346
347                        ui.send_message(msg)
348                    }
349                }
350            }
351        }
352    }
353}
354
355impl ColorGradientEditor {
356    fn fetch_gradient(&self, exclude: Handle<UiNode>, ui: &UserInterface) -> ColorGradient {
357        let mut gradient = ColorGradient::new();
358
359        for pt in ui
360            .node(self.points_canvas)
361            .children()
362            .iter()
363            .filter(|c| **c != exclude)
364            .map(|c| ui.node(*c).query_component::<ColorPoint>().unwrap())
365        {
366            gradient.add_point(GradientPoint::new(pt.location, pt.color()));
367        }
368
369        gradient
370    }
371}
372
373pub struct ColorGradientEditorBuilder {
374    widget_builder: WidgetBuilder,
375    color_gradient: ColorGradient,
376}
377
378fn create_color_points(
379    color_gradient: &ColorGradient,
380    point_context_menu: RcUiNodeHandle,
381    ctx: &mut BuildContext,
382) -> Vec<Handle<UiNode>> {
383    color_gradient
384        .points()
385        .iter()
386        .map(|pt| {
387            ColorPointBuilder::new(
388                WidgetBuilder::new()
389                    .with_context_menu(point_context_menu.clone())
390                    .with_cursor(Some(CursorIcon::EwResize))
391                    .with_width(6.0)
392                    .with_foreground(Brush::Solid(pt.color()).into()),
393            )
394            .with_location(pt.location())
395            .build(ctx)
396        })
397        .collect::<Vec<_>>()
398}
399
400impl ColorGradientEditorBuilder {
401    pub fn new(widget_builder: WidgetBuilder) -> Self {
402        Self {
403            widget_builder,
404            color_gradient: Default::default(),
405        }
406    }
407
408    pub fn with_color_gradient(mut self, gradient: ColorGradient) -> Self {
409        self.color_gradient = gradient;
410        self
411    }
412
413    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
414        let add_point;
415        let context_menu = ContextMenuBuilder::new(
416            PopupBuilder::new(WidgetBuilder::new())
417                .with_content(
418                    StackPanelBuilder::new(WidgetBuilder::new().with_child({
419                        add_point = MenuItemBuilder::new(WidgetBuilder::new())
420                            .with_content(MenuItemContent::text("Add Point"))
421                            .build(ctx);
422                        add_point
423                    }))
424                    .build(ctx),
425                )
426                .with_restrict_picking(false),
427        )
428        .build(ctx);
429        let context_menu = RcUiNodeHandle::new(context_menu, ctx.sender());
430
431        let selector_field;
432        let remove_point;
433        let point_context_menu = ContextMenuBuilder::new(
434            PopupBuilder::new(WidgetBuilder::new().with_width(200.0))
435                .with_content(
436                    StackPanelBuilder::new(
437                        WidgetBuilder::new()
438                            .with_child({
439                                remove_point = MenuItemBuilder::new(WidgetBuilder::new())
440                                    .with_content(MenuItemContent::text("Remove Point"))
441                                    .build(ctx);
442                                remove_point
443                            })
444                            .with_child({
445                                selector_field =
446                                    ColorFieldBuilder::new(WidgetBuilder::new().with_height(18.0))
447                                        .build(ctx);
448                                selector_field
449                            }),
450                    )
451                    .build(ctx),
452                )
453                .with_restrict_picking(false),
454        )
455        .build(ctx);
456        let point_context_menu = RcUiNodeHandle::new(point_context_menu, ctx.sender());
457
458        let points_canvas = ColorPointsCanvasBuilder::new(
459            WidgetBuilder::new()
460                .with_height(10.0)
461                .on_row(0)
462                .on_column(0)
463                .with_children(create_color_points(
464                    &self.color_gradient,
465                    point_context_menu.clone(),
466                    ctx,
467                )),
468        )
469        .build(ctx);
470
471        let gradient_field = ColorGradientFieldBuilder::new(
472            WidgetBuilder::new()
473                .with_height(20.0)
474                .on_row(1)
475                .on_column(0),
476        )
477        .with_color_gradient(self.color_gradient)
478        .build(ctx);
479
480        let grid = GridBuilder::new(
481            WidgetBuilder::new()
482                .with_child(points_canvas)
483                .with_child(gradient_field),
484        )
485        .add_row(Row::auto())
486        .add_row(Row::stretch())
487        .add_column(Column::stretch())
488        .build(ctx);
489
490        let editor = ColorGradientEditor {
491            widget: self
492                .widget_builder
493                .with_preview_messages(true)
494                .with_context_menu(context_menu.clone())
495                .with_child(grid)
496                .build(ctx),
497            points_canvas,
498            gradient_field,
499            selector_field,
500            context_menu,
501            point_context_menu,
502            add_point,
503            remove_point,
504            context_menu_target: Cell::new(Default::default()),
505            context_menu_open_position: Cell::new(Default::default()),
506        };
507
508        ctx.add_node(UiNode::new(editor))
509    }
510}
511
512#[derive(Debug, Clone, PartialEq)]
513pub enum ColorPointMessage {
514    Location(f32),
515}
516
517impl ColorPointMessage {
518    define_constructor!(ColorPointMessage:Location => fn location(f32), layout: false);
519}
520
521#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
522#[type_uuid(id = "a493a603-3451-4005-8c80-559707729e70")]
523#[reflect(derived_type = "UiNode")]
524pub struct ColorPoint {
525    pub widget: Widget,
526    pub location: f32,
527    pub dragging: bool,
528}
529
530impl ConstructorProvider<UiNode, UserInterface> for ColorPoint {
531    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
532        GraphNodeConstructor::new::<Self>()
533            .with_variant("Color Point", |ui| {
534                ColorPointBuilder::new(WidgetBuilder::new().with_name("Color Point"))
535                    .build(&mut ui.build_ctx())
536                    .into()
537            })
538            .with_group("Color")
539    }
540}
541
542define_widget_deref!(ColorPoint);
543
544impl Control for ColorPoint {
545    fn draw(&self, ctx: &mut DrawingContext) {
546        let size = self.bounding_rect().size;
547
548        ctx.push_triangle_filled([
549            Vector2::new(0.0, 0.0),
550            Vector2::new(size.x, 0.0),
551            Vector2::new(size.x * 0.5, size.y),
552        ]);
553
554        ctx.commit(
555            self.clip_bounds(),
556            self.foreground(),
557            CommandTexture::None,
558            &self.material,
559            None,
560        );
561    }
562
563    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
564        self.widget.handle_routed_message(ui, message);
565
566        if message.destination() == self.handle {
567            if message.direction() == MessageDirection::ToWidget {
568                if let Some(msg) = message.data::<ColorPointMessage>() {
569                    match msg {
570                        ColorPointMessage::Location(location) => {
571                            if *location != self.location {
572                                self.location = *location;
573                                self.invalidate_layout();
574                                ui.send_message(message.reverse());
575                            }
576                        }
577                    }
578                }
579            }
580
581            if message.direction() == MessageDirection::FromWidget {
582                if let Some(msg) = message.data::<WidgetMessage>() {
583                    match msg {
584                        WidgetMessage::MouseDown { button, .. } => {
585                            if *button == MouseButton::Left {
586                                ui.capture_mouse(self.handle);
587
588                                self.dragging = true;
589                            }
590                        }
591                        WidgetMessage::MouseUp { button, .. } => {
592                            if *button == MouseButton::Left {
593                                ui.release_mouse_capture();
594
595                                self.dragging = false;
596
597                                ui.send_message(ColorPointMessage::location(
598                                    self.handle,
599                                    MessageDirection::FromWidget,
600                                    self.location,
601                                ));
602                            }
603                        }
604                        WidgetMessage::MouseMove { pos, .. } => {
605                            if self.dragging {
606                                let parent_canvas = ui.node(self.parent);
607
608                                let cursor_x_local_to_parent =
609                                    parent_canvas.screen_to_local(*pos).x;
610
611                                self.location = (cursor_x_local_to_parent
612                                    / parent_canvas.actual_local_size().x)
613                                    .clamp(0.0, 1.0);
614
615                                self.invalidate_layout();
616                            }
617                        }
618                        _ => (),
619                    }
620                }
621            }
622        }
623    }
624}
625
626impl ColorPoint {
627    fn color(&self) -> Color {
628        if let Brush::Solid(color) = self.foreground.property {
629            color
630        } else {
631            unreachable!()
632        }
633    }
634}
635
636struct ColorPointBuilder {
637    widget_builder: WidgetBuilder,
638    location: f32,
639}
640
641impl ColorPointBuilder {
642    pub fn new(widget_builder: WidgetBuilder) -> Self {
643        Self {
644            widget_builder,
645            location: 0.0,
646        }
647    }
648
649    pub fn with_location(mut self, location: f32) -> Self {
650        self.location = location;
651        self
652    }
653
654    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
655        ctx.add_node(UiNode::new(ColorPoint {
656            widget: self.widget_builder.build(ctx),
657            location: self.location,
658            dragging: false,
659        }))
660    }
661}
662
663#[derive(Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
664#[type_uuid(id = "2608955a-4095-4fd1-af71-99bcdf2600f0")]
665#[reflect(derived_type = "UiNode")]
666struct ColorPointsCanvas {
667    widget: Widget,
668}
669
670define_widget_deref!(ColorPointsCanvas);
671
672impl Control for ColorPointsCanvas {
673    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
674        for &child in self.children() {
675            let child_ref = ui.node(child);
676            if let Some(color_point) = child_ref.query_component::<ColorPoint>() {
677                let x_pos = final_size.x * color_point.location - child_ref.desired_size().x * 0.5;
678
679                ui.arrange_node(
680                    child,
681                    &Rect::new(x_pos, 0.0, child_ref.desired_size().x, final_size.y),
682                );
683            }
684        }
685
686        final_size
687    }
688
689    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
690        self.widget.handle_routed_message(ui, message)
691    }
692}
693
694pub struct ColorPointsCanvasBuilder {
695    widget_builder: WidgetBuilder,
696}
697
698impl ColorPointsCanvasBuilder {
699    pub fn new(widget_builder: WidgetBuilder) -> Self {
700        Self { widget_builder }
701    }
702
703    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
704        ctx.add_node(UiNode::new(ColorPointsCanvas {
705            widget: self.widget_builder.build(ctx),
706        }))
707    }
708}
709
710#[cfg(test)]
711mod test {
712    use crate::color::gradient::ColorGradientEditorBuilder;
713    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
714
715    #[test]
716    fn test_deletion() {
717        test_widget_deletion(|ctx| {
718            ColorGradientEditorBuilder::new(WidgetBuilder::new()).build(ctx)
719        });
720    }
721}