Skip to main content

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