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