diff --git a/src/Drawables/Drawable.vala b/src/Drawables/Drawable.vala index 42d4b787..2f888db7 100644 --- a/src/Drawables/Drawable.vala +++ b/src/Drawables/Drawable.vala @@ -43,8 +43,8 @@ public class Akira.Drawables.Drawable { // Style public double line_width { get; set; default = 0; } - public Gdk.RGBA fill_rgba { get; set; default = Gdk.RGBA (); } - public Gdk.RGBA stroke_rgba { get; set; default = Gdk.RGBA (); } + public Cairo.Pattern fill_pattern { get; set; default = new Cairo.Pattern.rgba (1, 1, 1, 1); } + public Cairo.Pattern border_pattern { get; set; default = new Cairo.Pattern.rgba (1, 1, 1, 1); } public BorderType border_type { get; set; default = BorderType.CENTER; } public double radius_tr { get; set; default = 0; } public double radius_tl { get; set; default = 0; } @@ -383,7 +383,7 @@ public class Akira.Drawables.Drawable { if (draw_type == DrawType.XRAY) { return false; } - context.set_source_rgba (fill_rgba.red, fill_rgba.green, fill_rgba.blue, fill_rgba.alpha); + context.set_source (fill_pattern); context.set_antialias (Cairo.Antialias.GRAY); return true; } @@ -396,7 +396,7 @@ public class Akira.Drawables.Drawable { return true; } - context.set_source_rgba (stroke_rgba.red, stroke_rgba.green, stroke_rgba.blue, stroke_rgba.alpha); + context.set_source (border_pattern); context.set_line_width (line_width); context.set_antialias (Cairo.Antialias.GRAY); return line_width > 0; diff --git a/src/Layouts/BordersList/BorderItemModel.vala b/src/Layouts/BordersList/BorderItemModel.vala index 5a45d3cd..c2a0eb6a 100644 --- a/src/Layouts/BordersList/BorderItemModel.vala +++ b/src/Layouts/BordersList/BorderItemModel.vala @@ -26,7 +26,7 @@ public class Akira.Layouts.BordersList.BorderItemModel : Models.ColorModel { private unowned Lib.ViewCanvas _view_canvas; - private Lib.Items.ModelInstance _cached_instance; + // private Lib.Items.ModelInstance _cached_instance; public int border_id; @@ -39,9 +39,10 @@ public class Akira.Layouts.BordersList.BorderItemModel : Models.ColorModel { var node = im.item_model.node_from_id (_cached_instance.id); assert (node != null); - var new_color = Lib.Components.Color.from_rgba (color, hidden); + var new_pattern = pattern.copy (); var new_borders = node.instance.components.borders.copy (); - new_borders.replace (Lib.Components.Borders.Border (border_id, new_color)); + var new_border = new_borders.border_from_id (border_id).with_replaced_pattern (new_pattern); + new_borders.replace (new_border); node.instance.components.borders = new_borders; im.item_model.alert_node_changed (node, Lib.Components.Component.Type.COMPILED_BORDER); @@ -74,7 +75,12 @@ public class Akira.Layouts.BordersList.BorderItemModel : Models.ColorModel { (blocker); var border = _cached_instance.components.borders.border_from_id (border_id); - color = border.color; + active_pattern_type = border.active_pattern; + + solid_pattern = border.solid_pattern; + linear_pattern = border.linear_pattern; + radial_pattern = border.radial_pattern; + hidden = border.hidden; } } diff --git a/src/Layouts/BordersList/BorderListItem.vala b/src/Layouts/BordersList/BorderListItem.vala index da5b59d6..be8857fe 100644 --- a/src/Layouts/BordersList/BorderListItem.vala +++ b/src/Layouts/BordersList/BorderListItem.vala @@ -50,7 +50,7 @@ public class Akira.Layouts.BordersList.BorderListItem : VirtualizingListBoxRow { context.add_class ("selected-color-container"); context.add_class ("bg-pattern"); - color_button = new Widgets.ColorButton (); + color_button = new Widgets.ColorButton (view_canvas.window); container.add (color_button); eyedropper_button = new Widgets.EyeDropperButton () {}; diff --git a/src/Layouts/FillsList/FillItemModel.vala b/src/Layouts/FillsList/FillItemModel.vala index c924206f..79a6d0f8 100644 --- a/src/Layouts/FillsList/FillItemModel.vala +++ b/src/Layouts/FillsList/FillItemModel.vala @@ -26,7 +26,7 @@ public class Akira.Layouts.FillsList.FillItemModel : Models.ColorModel { private unowned Akira.Lib.ViewCanvas _view_canvas; - private Lib.Items.ModelInstance _cached_instance; + // private Lib.Items.ModelInstance _cached_instance; public int fill_id; @@ -39,9 +39,10 @@ public class Akira.Layouts.FillsList.FillItemModel : Models.ColorModel { var node = im.item_model.node_from_id (_cached_instance.id); assert (node != null); - var new_color = Lib.Components.Color.from_rgba (color, hidden); + var new_pattern = pattern.copy (); var new_fills = node.instance.components.fills.copy (); - new_fills.replace (Lib.Components.Fills.Fill (fill_id, new_color)); + var new_fill = new_fills.fill_from_id (fill_id).with_replaced_pattern (new_pattern); + new_fills.replace (new_fill); node.instance.components.fills = new_fills; im.item_model.alert_node_changed (node, Lib.Components.Component.Type.COMPILED_FILL); @@ -74,7 +75,12 @@ public class Akira.Layouts.FillsList.FillItemModel : Models.ColorModel { (blocker); var fill = _cached_instance.components.fills.fill_from_id (fill_id); - color = fill.color; + active_pattern_type = fill.active_pattern; + + solid_pattern = fill.solid_pattern; + linear_pattern = fill.linear_pattern; + radial_pattern = fill.radial_pattern; + hidden = fill.hidden; } } diff --git a/src/Layouts/FillsList/FillListItem.vala b/src/Layouts/FillsList/FillListItem.vala index 9d2bf915..2ba6a8bc 100644 --- a/src/Layouts/FillsList/FillListItem.vala +++ b/src/Layouts/FillsList/FillListItem.vala @@ -50,7 +50,7 @@ public class Akira.Layouts.FillsList.FillListItem : VirtualizingListBoxRow { context.add_class ("selected-color-container"); context.add_class ("bg-pattern"); - color_button = new Widgets.ColorButton (); + color_button = new Widgets.ColorButton (view_canvas.window); container.add (color_button); eyedropper_button = new Widgets.EyeDropperButton () {}; diff --git a/src/Lib/Components/Borders.vala b/src/Lib/Components/Borders.vala index dc30f281..a968549e 100644 --- a/src/Lib/Components/Borders.vala +++ b/src/Lib/Components/Borders.vala @@ -22,25 +22,79 @@ public class Akira.Lib.Components.Borders : Component, Copyable { public struct Border { public int _id; - public Color _color; + public Pattern _pattern { + get { + switch (active_pattern) { + case Pattern.PatternType.SOLID: + return solid_pattern; + case Pattern.PatternType.LINEAR: + return linear_pattern; + case Pattern.PatternType.RADIAL: + return radial_pattern; + default: + return solid_pattern; + } + } + + set { + switch (active_pattern) { + case Pattern.PatternType.SOLID: + solid_pattern = value; + break; + case Pattern.PatternType.LINEAR: + linear_pattern = value; + break; + case Pattern.PatternType.RADIAL: + radial_pattern = value; + break; + default: + solid_pattern = value; + break; + } + } + } + + // Each border item will have patterns for all three types, + // so that user can easily switch between them. + // However, the non active patterns will not be serialized. + public Pattern solid_pattern; + public Pattern linear_pattern; + public Pattern radial_pattern; + + public Pattern.PatternType active_pattern; + public double _size; - public Border (int id = -1, Color color = Color (), double? size = null) { + public Border (int id = -1, Pattern pattern = new Pattern (), double? size = null) { _id = id; - _color = color; + active_pattern = Pattern.PatternType.SOLID; + + var border_rgba = Gdk.RGBA (); + border_rgba.parse (settings.border_color); + solid_pattern = new Pattern.solid (border_rgba, false); + + linear_pattern = new Pattern.linear (Geometry.Point (5, 5), Geometry.Point (95, 95), false); + radial_pattern = new Pattern.radial (Geometry.Point (5, 5), Geometry.Point (95, 95), false); + _size = size != null ? size : settings.border_size; } public Border.deserialized (int id, Json.Object obj) { _id = id; - _color = Color.deserialized (obj.get_object_member ("color")); _size = (double)obj.get_int_member ("size"); } + public Border.with_all_patterns (int id, Pattern solid_pattern, Pattern linear_pattern, Pattern radial_pattern, Pattern.PatternType type) { + this.solid_pattern = solid_pattern; + this.linear_pattern = linear_pattern; + this.radial_pattern = radial_pattern; + this.active_pattern = type; + } + public Json.Node serialize () { var obj = new Json.Object (); obj.set_int_member ("id", _id); - obj.set_member ("color", _color.serialize ()); + obj.set_member ("pattern", _pattern.serialize ()); obj.set_double_member ("size", _size); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (obj); @@ -53,14 +107,14 @@ public class Akira.Lib.Components.Borders : Component, Copyable { return _id; } } - public Gdk.RGBA color { + public Pattern pattern { get { - return _color.rgba; + return _pattern; } } public bool hidden { get { - return _color.hidden; + return _pattern.hidden; } } public double size { @@ -71,11 +125,20 @@ public class Akira.Lib.Components.Borders : Component, Copyable { // Mutators. public Border with_color (Color new_color) { - return Border (_id, new_color, _size); + Pattern pattern = new Pattern.solid (new_color.rgba, new_color.hidden); + var border = Border (id, pattern, _size); + return border; + } + + public Border with_replaced_pattern (Pattern new_pattern) { + var new_border = Border.with_all_patterns (_id, solid_pattern, linear_pattern, radial_pattern, new_pattern.type); + new_border._pattern = new_pattern; + + return new_border; } public Border with_size (double new_size) { - return Border (_id, _color, new_size); + return Border (_id, _pattern, new_size); } } @@ -87,7 +150,7 @@ public class Akira.Lib.Components.Borders : Component, Copyable { public Borders.single_color (Color color, int size) { data = new Border[1]; - data[0] = Border (0, color, size); + data[0] = Border (0, new Pattern.solid (color.rgba, color.hidden), size); } public Borders.deserialized (Json.Object obj) { @@ -124,7 +187,7 @@ public class Akira.Lib.Components.Borders : Component, Copyable { public Border? border_from_id (int id) { foreach (unowned var border in data) { if (border.id == id) { - return border.with_color (border._color); + return Border.with_all_patterns (id, border.solid_pattern, border.linear_pattern, border.radial_pattern, border.active_pattern); } } return null; @@ -150,7 +213,10 @@ public class Akira.Lib.Components.Borders : Component, Copyable { latest_id = int.max (latest_id, border.id); } latest_id++; - var border = Border ((int) latest_id, color, size); + + var pattern = new Pattern.solid (color.rgba, color.hidden); + var border = Border ((int) latest_id, pattern); + data.resize (data.length + 1); data[data.length - 1] = border; } diff --git a/src/Lib/Components/CompiledBorder.vala b/src/Lib/Components/CompiledBorder.vala index 0e788faa..7a5200a7 100644 --- a/src/Lib/Components/CompiledBorder.vala +++ b/src/Lib/Components/CompiledBorder.vala @@ -20,12 +20,12 @@ */ public class Akira.Lib.Components.CompiledBorder : Copyable { - private Gdk.RGBA _color; + private Pattern _pattern; private double _size; private bool _visible; - public Gdk.RGBA color { - get { return _color; } + public Pattern pattern { + get { return _pattern; } } public double size { @@ -36,42 +36,41 @@ public class Akira.Lib.Components.CompiledBorder : Copyable { get { return _visible; } } - public CompiledBorder (Gdk.RGBA color, double size, bool visible) { - _color = color; + public CompiledBorder (Pattern pattern, double size, bool visible) { + _pattern = pattern; _size = size; _visible = visible; } public CompiledBorder.as_empty () { - _color = Gdk.RGBA () { red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0}; + _pattern = new Pattern.solid (Gdk.RGBA () {red = 0, green = 0, blue = 0, alpha = 0}, false); _size = 0; _visible = false; } public CompiledBorder copy () { - return new CompiledBorder (_color, _size, _visible); + return new CompiledBorder (_pattern, size, _visible); } public static CompiledBorder compile (Components? components, Lib.Items.ModelNode? node) { - var rgba_border = Gdk.RGBA (); + var pattern_border = new Pattern (); bool has_colors = false; - double size = 0; + double border_size = 0; if (components == null) { - return new CompiledBorder (rgba_border, size, has_colors); + return new CompiledBorder (pattern_border, border_size, has_colors); } unowned var borders = components.borders; unowned var opacity = components.opacity; - - // Set an initial arbitrary color with full transparency. - rgba_border.alpha = 0; + unowned var size = components.size; + unowned var center = components.center; if (borders == null) { - return new CompiledBorder (rgba_border, size, false); + return new CompiledBorder (pattern_border, border_size, has_colors); } - // Loop through all the configured borders and reload the color. + // Loop through all the configured borders. for (var i = 0; i < borders.data.length; ++i) { // Skip if the border is hidden as we don't need to blend colors. if (borders.data[i].hidden) { @@ -79,17 +78,22 @@ public class Akira.Lib.Components.CompiledBorder : Copyable { } // Set the new blended color. - rgba_border = Utils.Color.blend_colors (rgba_border, borders.data[i].color); - size = double.max (size, borders.data[i].size); + // rgba_border = Utils.Color.blend_colors (rgba_border, borders.data[i].pattern.get_first_color ()); + border_size = borders.data[i].size; + pattern_border = Utils.Pattern.create_pattern_with_converted_positions (borders.data[i].pattern, size, center); has_colors = true; + + // TODO: Temporarily disable blending patterns. Not implemented. + break; } // Apply the mixed RGBA value only if we had one. if (has_colors && opacity != null) { - // Keep in consideration the global opacity to properly update the border color. - rgba_border.alpha = rgba_border.alpha * opacity.opacity / 100; + // Keep in consideration the global opacity to properly update the fill color. + // TODO: Disable this too. + // rgba_border.alpha = rgba_border.alpha * opacity.opacity / 100; } - return new CompiledBorder (rgba_border, size, has_colors && size != 0); + return new CompiledBorder (pattern_border, border_size, has_colors); } } diff --git a/src/Lib/Components/CompiledFill.vala b/src/Lib/Components/CompiledFill.vala index 106b2387..c4540698 100644 --- a/src/Lib/Components/CompiledFill.vala +++ b/src/Lib/Components/CompiledFill.vala @@ -20,46 +20,46 @@ */ public class Akira.Lib.Components.CompiledFill : Copyable { - private Gdk.RGBA _color; + private Pattern _pattern; private bool _visible; - public Gdk.RGBA color { - get { return _color; } + public Pattern pattern { + get { return _pattern; } } public bool is_visible { get { return _visible; } } - public CompiledFill (Gdk.RGBA color, bool visible) { - _color = color; + public CompiledFill (Pattern pattern, bool visible) { + _pattern = pattern; _visible = visible; } public CompiledFill.as_empty () { - _color = Gdk.RGBA () { red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0}; + _pattern = new Pattern.solid (Gdk.RGBA () {red = 0, green = 0, blue = 0, alpha = 0}, false); _visible = false; } public CompiledFill copy () { - return new CompiledFill (_color, _visible); + return new CompiledFill (_pattern, _visible); } public static CompiledFill compile (Components? components, Lib.Items.ModelNode? node) { - var rgba_fill = Gdk.RGBA (); + var pattern_fill = new Pattern (); bool has_colors = false; - // Set an initial arbitrary color with full transparency. - rgba_fill.alpha = 0; if (components == null) { - return new CompiledFill (rgba_fill, has_colors); + return new CompiledFill (pattern_fill, has_colors); } unowned var fills = components.fills; unowned var opacity = components.opacity; + unowned var size = components.size; + unowned var center = components.center; if (fills == null) { - return new CompiledFill (rgba_fill, has_colors); + return new CompiledFill (pattern_fill, has_colors); } // Loop through all the configured fills. @@ -70,16 +70,21 @@ public class Akira.Lib.Components.CompiledFill : Copyable { } // Set the new blended color. - rgba_fill = Utils.Color.blend_colors (rgba_fill, fills.data[i].color); + // rgba_fill = Utils.Color.blend_colors (rgba_fill, fills.data[i].pattern.get_first_color ()); + pattern_fill = Utils.Pattern.create_pattern_with_converted_positions (fills.data[i].pattern, size, center); has_colors = true; + + // TODO: Temporarily disable blending patterns. Not implemented. + break; } // Apply the mixed RGBA value only if we had one. if (has_colors && opacity != null) { // Keep in consideration the global opacity to properly update the fill color. - rgba_fill.alpha = rgba_fill.alpha * opacity.opacity / 100; + // TODO: Disable this too. + // rgba_fill.alpha = rgba_fill.alpha * opacity.opacity / 100; } - return new CompiledFill (rgba_fill, has_colors); + return new CompiledFill (pattern_fill, has_colors); } } diff --git a/src/Lib/Components/Fills.vala b/src/Lib/Components/Fills.vala index c9ea2f90..62cab222 100644 --- a/src/Lib/Components/Fills.vala +++ b/src/Lib/Components/Fills.vala @@ -22,16 +22,78 @@ public class Akira.Lib.Components.Fills : Component, Copyable { public struct Fill { public int _id; - public Color _color; + public Pattern _pattern { + get { + switch (active_pattern) { + case Pattern.PatternType.SOLID: + return solid_pattern; + case Pattern.PatternType.LINEAR: + return linear_pattern; + case Pattern.PatternType.RADIAL: + return radial_pattern; + default: + return solid_pattern; + } + } + + set { + switch (active_pattern) { + case Pattern.PatternType.SOLID: + solid_pattern = value; + break; + case Pattern.PatternType.LINEAR: + linear_pattern = value; + break; + case Pattern.PatternType.RADIAL: + radial_pattern = value; + break; + default: + solid_pattern = value; + break; + } + } + } - public Fill (int id = -1, Color color = Color ()) { + // Each fill item will have patterns for all three types, + // so that user can easily switch between them. + // However, the non active patterns will not be serialized. + public Pattern solid_pattern; + public Pattern linear_pattern; + public Pattern radial_pattern; + + public Pattern.PatternType active_pattern; + + public Fill (int id = -1, Pattern pattern = new Pattern ()) { _id = id; - _color = color; + active_pattern = Pattern.PatternType.SOLID; + + var fill_rgba = Gdk.RGBA (); + fill_rgba.parse (settings.fill_color); + solid_pattern = new Pattern.solid (fill_rgba, false); + + linear_pattern = new Pattern.linear (Geometry.Point (5, 5), Geometry.Point (95, 95), false); + radial_pattern = new Pattern.radial (Geometry.Point (5, 5), Geometry.Point (95, 95), false); + } + + public Fill.with_all_patterns (int id, Pattern solid_pattern, Pattern linear_pattern, Pattern radial_pattern, Pattern.PatternType type) { + this._id = id; + this.solid_pattern = solid_pattern; + this.linear_pattern = linear_pattern; + this.radial_pattern = radial_pattern; + this.active_pattern = type; } public Fill.deserialized (int id, Json.Object obj) { _id = id; - _color = Color.deserialized (obj.get_object_member ("color")); + // active_pattern = Pattern.PatternType.SOLID; + + // solid_pattern = new Pattern.solid (0, 0, 0, 1); + + // linear_pattern = new Pattern.linear (Geometry.Point (0, 0), Geometry.Point (100, 100), false); + // linear_pattern.add_stop_color (Gdk.RGBA () {red = 0, green = 0, blue = 0, alpha = 0}, 0); + // linear_pattern.add_stop_color (Gdk.RGBA () {red = 1, green = 1, blue = 1, alpha = 0}, 1); + + // radial_pattern = new Pattern.radial (); } // Recommended accessors. @@ -40,26 +102,36 @@ public class Akira.Lib.Components.Fills : Component, Copyable { return _id; } } - public Gdk.RGBA color { + public Pattern pattern { get { - return _color.rgba; + return _pattern; } } public bool hidden { get { - return _color.hidden; + return _pattern.hidden; } } // Mutators. - public Fill with_color (Color new_color) { - return Fill (_id, new_color); + public Fill with_color (Color new_color, int id = 0) { + Pattern pattern = new Pattern.solid (new_color.rgba, new_color.hidden); + var fill = Fill (id, pattern); + fill._id = id; + return fill; + } + + public Fill with_replaced_pattern (Pattern new_pattern) { + var new_fill = Fill.with_all_patterns (_id, solid_pattern, linear_pattern, radial_pattern, new_pattern.type); + new_fill._pattern = new_pattern; + + return new_fill; } public Json.Node serialize () { var obj = new Json.Object (); obj.set_int_member ("id", _id); - obj.set_member ("color", _color.serialize ()); + obj.set_member ("pattern", _pattern.serialize ()); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (obj); return node; @@ -74,7 +146,7 @@ public class Akira.Lib.Components.Fills : Component, Copyable { public Fills.with_color (Color color) { data = new Fill[1]; - data[0] = Fill (0, color); + data[0] = Fill (0, new Pattern.solid (color.rgba, color.hidden)); } public Fills.deserialized (Json.Object obj) { @@ -110,7 +182,7 @@ public class Akira.Lib.Components.Fills : Component, Copyable { public Fill? fill_from_id (int id) { foreach (unowned var fill in data) { if (fill.id == id) { - return fill.with_color (fill._color); + return Fill.with_all_patterns (id, fill.solid_pattern, fill.linear_pattern, fill.radial_pattern, fill.active_pattern); } } return null; @@ -136,7 +208,8 @@ public class Akira.Lib.Components.Fills : Component, Copyable { latest_id = int.max (latest_id, fill.id); } latest_id++; - var fill = Fill ((int) latest_id, color); + var pattern = new Pattern.solid (color.rgba, color.hidden); + var fill = Fill ((int) latest_id, pattern); data.resize (data.length + 1); data[data.length - 1] = fill; } diff --git a/src/Lib/Components/Pattern.vala b/src/Lib/Components/Pattern.vala new file mode 100644 index 00000000..784b55f7 --- /dev/null +++ b/src/Lib/Components/Pattern.vala @@ -0,0 +1,169 @@ + +/** + * Copyright (c) 2022 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Ashish Shevale + */ + +public class Akira.Lib.Components.Pattern { + public enum PatternType { + SOLID = 0, + LINEAR = 1, + RADIAL = 2, + } + + public struct StopColor { + // Value between 0 and 1. Represents what distance the stop color is located at. + public double offset; + public Gdk.RGBA color; + + public StopColor.deserialized (Json.Object obj) { + offset = obj.get_double_member ("offset"); + + color = Gdk.RGBA (); + color.red = obj.get_double_member ("red"); + color.green = obj.get_double_member ("green"); + color.blue = obj.get_double_member ("blue"); + color.alpha = obj.get_double_member ("alpha"); + } + + public Json.Node serialize () { + var obj = new Json.Object (); + + obj.set_double_member ("offset", offset); + obj.set_double_member ("red", color.red); + obj.set_double_member ("green", color.green); + obj.set_double_member ("blue", color.blue); + obj.set_double_member ("alpha", color.alpha); + + var node = new Json.Node (Json.NodeType.OBJECT); + node.set_object (obj); + return node; + } + } + + public PatternType type; + public Gee.TreeSet colors; + public bool hidden; + + // These values denote the position of guide points of gradients. + // The values are relative to the canvas item's position. + public Geometry.Point start; + public Geometry.Point end; + + public Pattern.solid (Gdk.RGBA color, bool hidden) { + this.type = PatternType.SOLID; + this.hidden = hidden; + + colors = new Gee.TreeSet (are_equal); + colors.add (StopColor () {offset = 0, color = color}); + } + + public Pattern.linear (Geometry.Point start, Geometry.Point end, bool hidden) { + this.start = start; + this.end = end; + + // By default, all linear gradients will be created with black and white colors at start and end. + colors = new Gee.TreeSet (are_equal); + colors.add (StopColor () {offset = 0, color = Gdk.RGBA () {red = 0, green = 0, blue = 0, alpha = 1}}); + colors.add (StopColor () {offset = 1, color = Gdk.RGBA () {red = 1, green = 1, blue = 1, alpha = 1}}); + + this.type = PatternType.LINEAR; + this.hidden = hidden; + } + + public Pattern.radial (Geometry.Point start, Geometry.Point end, bool hidden) { + this.start = start; + this.end = end; + + // By default, all linear gradients will be created with black and white colors at start and end. + colors = new Gee.TreeSet (are_equal); + colors.add (StopColor () {offset = 0, color = Gdk.RGBA () {red = 0, green = 0, blue = 0, alpha = 1}}); + colors.add (StopColor () {offset = 1, color = Gdk.RGBA () {red = 1, green = 1, blue = 1, alpha = 1}}); + + this.type = PatternType.RADIAL; + this.hidden = hidden; + } + + public Pattern copy () { + Pattern new_pattern = new Pattern (); + + new_pattern.type = this.type; + new_pattern.colors = this.colors; + new_pattern.hidden = this.hidden; + new_pattern.start = this.start; + new_pattern.end = this.end; + + return new_pattern; + } + + public void add_stop_color (Gdk.RGBA color, double offset) { + if (type == PatternType.SOLID) { + // Adding stop colors for solid pattern is not allowed. + assert (false); + } + + colors.add (StopColor () {offset = offset, color = color}); + } + + public Gdk.RGBA get_first_color () { + return colors.first ().color; + } + + private int are_equal (StopColor? first, StopColor? second) { + if (first.offset < second.offset) { + return -1; + } else if (first.offset > second.offset) { + return 1; + } + + return 0; + } + + public Pattern.deserialized (Json.Object obj) { + this.start = Geometry.Point.deserialized (obj.get_object_member ("start")); + this.end = Geometry.Point.deserialized (obj.get_object_member ("end")); + this.type = (PatternType) obj.get_int_member ("type"); + this.hidden = obj.get_boolean_member ("hidden"); + + var color_array = obj.get_array_member ("colors"); + this.colors = new Gee.TreeSet (are_equal); + foreach (var color_item in color_array.get_elements ()) { + colors.add (StopColor.deserialized (color_item.get_object ())); + } + } + + public Json.Node serialize () { + var obj = new Json.Object (); + obj.set_member ("start", start.serialize ()); + obj.set_member ("end", end.serialize ()); + obj.set_int_member ("type", (int) type); + obj.set_boolean_member ("hidden", hidden); + + var color_array = new Json.Array (); + foreach (var color in colors) { + color_array.add_element (color.serialize ()); + } + + obj.set_array_member ("colors", color_array); + + var node = new Json.Node (Json.NodeType.OBJECT); + node.set_object (obj); + return node; + } +} diff --git a/src/Lib/Items/ModelTypeArtboard.vala b/src/Lib/Items/ModelTypeArtboard.vala index 7f789db4..43ce2d19 100644 --- a/src/Lib/Items/ModelTypeArtboard.vala +++ b/src/Lib/Items/ModelTypeArtboard.vala @@ -61,11 +61,11 @@ public class Akira.Lib.Items.ModelTypeArtboard : ModelType { switch (type) { case Lib.Components.Component.Type.COMPILED_FILL: if (!instance.compiled_fill.is_visible) { - instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.fill_pattern = Utils.Pattern.default_pattern (); break; } - instance.drawable.fill_rgba = instance.compiled_fill.color; + instance.drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_fill.pattern); break; case Lib.Components.Component.Type.COMPILED_GEOMETRY: instance.drawable.width = instance.components.size.width; diff --git a/src/Lib/Items/ModelTypeEllipse.vala b/src/Lib/Items/ModelTypeEllipse.vala index 9e47d71e..b76247d4 100644 --- a/src/Lib/Items/ModelTypeEllipse.vala +++ b/src/Lib/Items/ModelTypeEllipse.vala @@ -61,22 +61,22 @@ public class Akira.Lib.Items.ModelTypeEllipse : ModelType { case Lib.Components.Component.Type.COMPILED_BORDER: if (!instance.compiled_border.is_visible) { instance.drawable.line_width = 0; - instance.drawable.stroke_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.border_pattern = Utils.Pattern.default_pattern (); break; } // The "line-width" property expects a DOUBLE type, but we don't support subpixels // so we always handle the border size as INT, therefore we need to type cast it here. instance.drawable.line_width = (double) instance.compiled_border.size; - instance.drawable.stroke_rgba = instance.compiled_border.color; + instance.drawable.border_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_border.pattern); break; case Lib.Components.Component.Type.COMPILED_FILL: if (!instance.compiled_fill.is_visible) { - instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.fill_pattern = Utils.Pattern.default_pattern (); break; } - instance.drawable.fill_rgba = instance.compiled_fill.color; + instance.drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_fill.pattern); break; case Lib.Components.Component.Type.COMPILED_GEOMETRY: var ellipse = instance.drawable as Drawables.DrawableEllipse; diff --git a/src/Lib/Items/ModelTypePath.vala b/src/Lib/Items/ModelTypePath.vala index 532e711c..9e7feb67 100644 --- a/src/Lib/Items/ModelTypePath.vala +++ b/src/Lib/Items/ModelTypePath.vala @@ -70,22 +70,22 @@ public class Akira.Lib.Items.ModelTypePath : ModelType { case Lib.Components.Component.Type.COMPILED_BORDER: if (!instance.compiled_border.is_visible) { instance.drawable.line_width = 0; - instance.drawable.stroke_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.border_pattern = Utils.Pattern.default_pattern (); break; } // The "line-width" property expects a DOUBLE type, but we don't support subpixels // so we always handle the border size as INT, therefore we need to type cast it here. instance.drawable.line_width = (double) instance.compiled_border.size; - instance.drawable.stroke_rgba = instance.compiled_border.color; + instance.drawable.border_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_border.pattern); break; case Lib.Components.Component.Type.COMPILED_FILL: if (!instance.compiled_fill.is_visible) { - instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.fill_pattern = Utils.Pattern.default_pattern (); break; } - instance.drawable.fill_rgba = instance.compiled_fill.color; + instance.drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_fill.pattern); break; case Lib.Components.Component.Type.COMPILED_GEOMETRY: // The points property is only available to DrawablePath, so first typecast it diff --git a/src/Lib/Items/ModelTypeRect.vala b/src/Lib/Items/ModelTypeRect.vala index c0938452..720af761 100644 --- a/src/Lib/Items/ModelTypeRect.vala +++ b/src/Lib/Items/ModelTypeRect.vala @@ -66,22 +66,22 @@ public class Akira.Lib.Items.ModelTypeRect : ModelType { case Lib.Components.Component.Type.COMPILED_BORDER: if (!instance.compiled_border.is_visible) { instance.drawable.line_width = 0; - instance.drawable.stroke_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.border_pattern = Utils.Pattern.default_pattern (); break; } // The "line-width" property expects a DOUBLE type, but we don't support subpixels // so we always handle the border size as INT, therefore we need to type cast it here. instance.drawable.line_width = (double) instance.compiled_border.size; - instance.drawable.stroke_rgba = instance.compiled_border.color; + instance.drawable.border_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_border.pattern); break; case Lib.Components.Component.Type.COMPILED_FILL: if (!instance.compiled_fill.is_visible) { - instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.fill_pattern = Utils.Pattern.default_pattern (); break; } - instance.drawable.fill_rgba = instance.compiled_fill.color; + instance.drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_fill.pattern); break; case Lib.Components.Component.Type.COMPILED_GEOMETRY: instance.drawable.width = instance.components.size.width; diff --git a/src/Lib/Items/ModelTypeText.vala b/src/Lib/Items/ModelTypeText.vala index 445039ac..e2ed429a 100644 --- a/src/Lib/Items/ModelTypeText.vala +++ b/src/Lib/Items/ModelTypeText.vala @@ -65,22 +65,22 @@ public class Akira.Lib.Items.ModelTypeText : ModelType { case Lib.Components.Component.Type.COMPILED_BORDER: if (!instance.compiled_border.is_visible) { instance.drawable.line_width = 0; - instance.drawable.stroke_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.border_pattern = Utils.Pattern.default_pattern (); break; } // The "line-width" property expects a DOUBLE type, but we don't support subpixels // so we always handle the border size as INT, therefore we need to type cast it here. instance.drawable.line_width = (double) instance.compiled_border.size; - instance.drawable.stroke_rgba = instance.compiled_border.color; + instance.drawable.border_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_border.pattern); break; case Lib.Components.Component.Type.COMPILED_FILL: if (!instance.compiled_fill.is_visible) { - instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 }; + instance.drawable.fill_pattern = Utils.Pattern.default_pattern (); break; } - instance.drawable.fill_rgba = instance.compiled_fill.color; + instance.drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (instance.compiled_fill.pattern); break; case Lib.Components.Component.Type.COMPILED_GEOMETRY: instance.drawable.width = instance.components.size.width; diff --git a/src/Lib/Managers/NobManager.vala b/src/Lib/Managers/NobManager.vala index 5747ea28..917e2c83 100644 --- a/src/Lib/Managers/NobManager.vala +++ b/src/Lib/Managers/NobManager.vala @@ -38,6 +38,16 @@ public class Akira.Lib.Managers.NobManager : Object { public NobManager (Lib.ViewCanvas canvas) { Object (view_canvas: canvas); + + view_canvas.window.event_bus.change_gradient_nobs_visibility.connect ((visible) => { + if (visible) { + nob_layer.render_gradient_nobs = true; + } else { + nob_layer.render_gradient_nobs = false; + } + + update_nob_layer (); + }); } construct { @@ -100,6 +110,11 @@ public class Akira.Lib.Managers.NobManager : Object { var active_nob_id = view_canvas.mode_manager.active_mode_nob; foreach (var nob in nobs.data) { + if (Utils.Nobs.is_gradient_nob (nob.handle_id)) { + nob.active = true; + continue; + } + bool set_visible = true; if (!show_h_centers && Utils.Nobs.is_horizontal_center (nob.handle_id)) { @@ -155,6 +170,25 @@ public class Akira.Lib.Managers.NobManager : Object { maybe_create_anchor_point_effect (); } + public void set_gradient_nob_position (Utils.Nobs.Nob nob, Geometry.Point position) { + nobs.data[nob].center_x = position.x; + nobs.data[nob].center_y = position.y; + } + + // This method will set the render flags for ViewLayerNobs and redraw the layer. + // It must be called only after positions of all nobs have been set. + public void set_layer_flags_from_pattern_type (Lib.Components.Pattern.PatternType ptype) { + if (ptype == Lib.Components.Pattern.PatternType.SOLID) { + nob_layer.render_gradient_nobs = false; + } else if (ptype == Lib.Components.Pattern.PatternType.LINEAR) { + nob_layer.render_gradient_nobs = true; + } else if (ptype == Lib.Components.Pattern.PatternType.RADIAL) { + nob_layer.render_gradient_nobs = true; + } + + nob_layer.update_nob_data (nobs); + } + private void remove_anchor_point_effect () { nob_layer.add_anchor_point (null); } diff --git a/src/Lib/Modes/TransformMode.vala b/src/Lib/Modes/TransformMode.vala index 9fb1405f..c801a3aa 100644 --- a/src/Lib/Modes/TransformMode.vala +++ b/src/Lib/Modes/TransformMode.vala @@ -180,6 +180,17 @@ public class Akira.Lib.Modes.TransformMode : AbstractInteractionMode { event.y ); break; + case Utils.Nobs.Nob.GRADIENT_START: + case Utils.Nobs.Nob.GRADIENT_END: + move_gradient_nob ( + view_canvas, + nob, + selection, + initial_drag_state, + event.x, + event.y + ); + break; default: effective_nob = scale_from_event ( view_canvas, @@ -541,4 +552,22 @@ public class Akira.Lib.Modes.TransformMode : AbstractInteractionMode { } } } + + private static void move_gradient_nob ( + Lib.ViewCanvas view_canvas, + Utils.Nobs.Nob nob, + Lib.Items.NodeSelection selection, + InitialDragState initial_drag_state, + double event_x, + double event_y + ) { + var position = Geometry.Point ( + initial_drag_state.press_x - event_x, + initial_drag_state.press_y - event_y + ); + initial_drag_state.press_x = event_x; + initial_drag_state.press_y = event_y; + + view_canvas.window.event_bus.translate_gradient_nob_by_delta (nob, position); + } } diff --git a/src/Models/ColorModel.vala b/src/Models/ColorModel.vala index 4364d3df..cfb61589 100644 --- a/src/Models/ColorModel.vala +++ b/src/Models/ColorModel.vala @@ -40,18 +40,57 @@ public class Akira.Models.ColorModel : GLib.Object { } protected int block_signal = 0; + protected Lib.Items.ModelInstance _cached_instance; - private Gdk.RGBA _color; - public Gdk.RGBA color { + // All three types of patterns will be stored here. + // Based on which type is active, update it. + public Lib.Components.Pattern solid_pattern; + public Lib.Components.Pattern linear_pattern; + public Lib.Components.Pattern radial_pattern; + + public Lib.Components.Pattern.PatternType _active_pattern_type; + public Lib.Components.Pattern.PatternType active_pattern_type { get { - return _color; + return _active_pattern_type; } + set { - if (value == _color) { - return; + _active_pattern_type = value; + on_value_changed (); + value_changed (); + } + } + + public Lib.Components.Pattern pattern { + get { + switch (_active_pattern_type) { + case Lib.Components.Pattern.PatternType.SOLID: + return solid_pattern; + case Lib.Components.Pattern.PatternType.LINEAR: + return linear_pattern; + case Lib.Components.Pattern.PatternType.RADIAL: + return radial_pattern; + default: + return solid_pattern; + } + } + + set { + switch (_active_pattern_type) { + case Lib.Components.Pattern.PatternType.SOLID: + solid_pattern = value; + break; + case Lib.Components.Pattern.PatternType.LINEAR: + linear_pattern = value; + break; + case Lib.Components.Pattern.PatternType.RADIAL: + radial_pattern = value; + break; + default: + solid_pattern = value; + break; } - _color = value; on_value_changed (); value_changed (); } @@ -89,6 +128,33 @@ public class Akira.Models.ColorModel : GLib.Object { } } + public void move_pattern_position_by_delta (Utils.Nobs.Nob nob, Geometry.Point delta) { + Geometry.Point percent_delta = Geometry.Point ( + delta.x * 100.0 / _cached_instance.components.size.width, + delta.y * 100.0 / _cached_instance.components.size.height + ); + + switch (nob) { + case Utils.Nobs.Nob.GRADIENT_START: + pattern.start = Geometry.Point ( + pattern.start.x - percent_delta.x, + pattern.start.y - percent_delta.y + ); + break; + case Utils.Nobs.Nob.GRADIENT_END: + pattern.end = Geometry.Point ( + pattern.end.x - percent_delta.x, + pattern.end.y - percent_delta.y + ); + break; + default: + break; + } + + on_value_changed (); + value_changed (); + } + public virtual void on_value_changed () {} public virtual void delete () {} } diff --git a/src/Services/EventBus.vala b/src/Services/EventBus.vala index acd95ceb..d72dad4c 100644 --- a/src/Services/EventBus.vala +++ b/src/Services/EventBus.vala @@ -48,6 +48,8 @@ public class Akira.Services.EventBus : Object { public signal void update_snaps_color (); public signal void update_snap_decorators (); public signal void zoom_changed (double new_zoom); + public signal void change_gradient_nobs_visibility (bool visible); + public signal void translate_gradient_nob_by_delta (Utils.Nobs.Nob nob, Geometry.Point delta); // Canvas triggers. public signal void adjust_zoom (double zoom, bool absolute, Geometry.Point? reference); diff --git a/src/Utils/Nobs.vala b/src/Utils/Nobs.vala index 9a2b4597..004cc8fd 100644 --- a/src/Utils/Nobs.vala +++ b/src/Utils/Nobs.vala @@ -43,6 +43,8 @@ public class Akira.Utils.Nobs : Object { BOTTOM_LEFT, LEFT_CENTER, ROTATE, + GRADIENT_START, + GRADIENT_END, ALL } @@ -71,15 +73,15 @@ public class Akira.Utils.Nobs : Object { public NobData[] data; public NobSet () { - data = new NobData[9]; - for (var i = 0; i < 9; i++) { + data = new NobData[11]; + for (var i = 0; i < 11; i++) { data[i] = new NobData ((Nob)i, 0, 0, false); } } public NobSet.clone (NobSet other) { - data = new NobData[9]; - for (var i = 0; i < 9; i++) { + data = new NobData[11]; + for (var i = 0; i < 11; i++) { data[i] = other.data[i].copy (); } } @@ -169,6 +171,13 @@ public class Akira.Utils.Nobs : Object { return (nob == Utils.Nobs.Nob.TOP_CENTER || nob == Utils.Nobs.Nob.BOTTOM_CENTER); } + public static bool is_gradient_nob (Nob nob) { + return ( + nob == Nob.GRADIENT_START || + nob == Nob.GRADIENT_END + ); + } + /* * Return a cursor type based of the type of nob. */ @@ -205,6 +214,12 @@ public class Akira.Utils.Nobs : Object { case Nob.ROTATE: result = Gdk.CursorType.EXCHANGE; break; + case Nob.GRADIENT_START: + result = Gdk.CursorType.TCROSS; + break; + case Nob.GRADIENT_END: + result = Gdk.CursorType.TCROSS; + break; default: break; } diff --git a/src/Utils/Pattern.vala b/src/Utils/Pattern.vala new file mode 100644 index 00000000..f50fcc9e --- /dev/null +++ b/src/Utils/Pattern.vala @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2022 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Ashish Shevale + */ + +public class Akira.Utils.Pattern { + public static Cairo.Pattern convert_to_cairo_pattern (Lib.Components.Pattern pattern) { + Cairo.Pattern converted; + + switch (pattern.type) { + case Lib.Components.Pattern.PatternType.SOLID: + var color = pattern.colors.first ().color; + converted = new Cairo.Pattern.rgba (color.red, color.green, color.blue, color.alpha); + return converted; + case Lib.Components.Pattern.PatternType.LINEAR: + converted = new Cairo.Pattern.linear (pattern.start.x, pattern.start.y, pattern.end.x, pattern.end.y); + break; + case Lib.Components.Pattern.PatternType.RADIAL: + double distance = Utils.GeometryMath.distance (pattern.start.x, pattern.start.y, pattern.end.x, pattern.end.y); + converted = new Cairo.Pattern.radial ( + pattern.start.x, + pattern.start.y, + 0, + pattern.start.x, + pattern.start.y, + distance + ); + break; + default: + assert (false); + converted = new Cairo.Pattern.rgba (0, 0, 0, 0); + break; + } + + // If the pattern was linear or radial, add all the stop colors. + foreach (var stop_color in pattern.colors) { + var color = stop_color.color; + converted.add_color_stop_rgba ( + stop_color.offset, + color.red, + color.green, + color.blue, + color.alpha + ); + } + + return converted; + } + + public static Cairo.Pattern default_pattern () { + return new Cairo.Pattern.rgba (1, 1, 1, 1); + } + + // In Components.Pattern, the start and end positions of gradients are stored as + // relative positions as percentages of width and height of canvas item. + // This method will convert those values to actual values, to be stored in CompiledFill. + public static Lib.Components.Pattern create_pattern_with_converted_positions (Lib.Components.Pattern pattern, Lib.Components.Size size, Lib.Components.Coordinates center) { + var new_pattern = pattern.copy (); + + new_pattern.start = Geometry.Point ( + pattern.start.x * size.width / 100.0 - size.width / 2.0, + pattern.start.y * size.height / 100.0 - size.height / 2.0 + ); + new_pattern.end = Geometry.Point ( + pattern.end.x * size.width / 100.0 - size.width / 2.0, + pattern.end.y * size.height / 100.0 - size.height / 2.0 + ); + + return new_pattern; + } + + public static string convert_to_css_linear_gradient (Lib.Components.Pattern pattern) { + if (pattern.type == Lib.Components.Pattern.PatternType.SOLID) { + var color = pattern.get_first_color ().to_string (); + return """linear-gradient(to right, %s, %s)""".printf (color, color); + } + + string css_result = "linear-gradient(to right"; + + foreach (var stop_color in pattern.colors) { + css_result += """ ,%s %f""".printf (stop_color.color.to_string (), stop_color.offset * 100.0); + css_result += "%"; + } + + css_result += ")"; + return css_result; + } +} diff --git a/src/ViewLayers/ViewLayerMultiSelect.vala b/src/ViewLayers/ViewLayerMultiSelect.vala index 9620131d..a0090f6b 100644 --- a/src/ViewLayers/ViewLayerMultiSelect.vala +++ b/src/ViewLayers/ViewLayerMultiSelect.vala @@ -23,6 +23,9 @@ public class Akira.ViewLayers.ViewLayerMultiSelect : ViewLayer { private const double UI_LINE_WIDTH = 1.0; private Gdk.RGBA fill { get; default = Gdk.RGBA () { red = 0.25, green = 0.79, blue = 0.98, alpha = 0.2 }; } + private Lib.Components.Pattern fill_pattern { + get; + default = new Lib.Components.Pattern.solid (Gdk.RGBA () { red = 0.25, green = 0.79, blue = 0.98, alpha = 0.2 }, false); } private Gdk.RGBA stroke { get { var color = fill; @@ -91,9 +94,9 @@ public class Akira.ViewLayers.ViewLayerMultiSelect : ViewLayer { return; } - drawable.fill_rgba = fill; + drawable.fill_pattern = Utils.Pattern.convert_to_cairo_pattern (fill_pattern); drawable.line_width = UI_LINE_WIDTH / scale; - drawable.stroke_rgba = stroke; + drawable.border_pattern = Utils.Pattern.convert_to_cairo_pattern (new Lib.Components.Pattern.solid (stroke, false)); drawable.paint (context, target_bounds, scale, Drawables.Drawable.DrawType.NORMAL); last_drawn_bb = drawable.bounds; diff --git a/src/ViewLayers/ViewLayerNobs.vala b/src/ViewLayers/ViewLayerNobs.vala index 58c1d5a6..efa0ba35 100644 --- a/src/ViewLayers/ViewLayerNobs.vala +++ b/src/ViewLayers/ViewLayerNobs.vala @@ -33,6 +33,9 @@ public class Akira.ViewLayers.ViewLayerNobs : ViewLayer { private Drawables.Drawable? old_anchor_point_drawable = null; private Geometry.Rectangle anchor_point_last_bb_drawn = Geometry.Rectangle.empty (); + // If this is true, draw the start and end nobs for linear and radial gradients. + public bool render_gradient_nobs = false; + public void update_nob_data (Utils.Nobs.NobSet? new_nobs) { if (nobs != null) { old_nobs = new Utils.Nobs.NobSet.clone (nobs); @@ -102,6 +105,17 @@ public class Akira.ViewLayers.ViewLayerNobs : ViewLayer { if (!nob.active) { continue; } + + // Render gradient nobs only if the proper flags are set. + if ( + nob.handle_id == Utils.Nobs.Nob.GRADIENT_START || + nob.handle_id == Utils.Nobs.Nob.GRADIENT_END + ) { + if (!render_gradient_nobs) { + continue; + } + } + context.save (); context.new_path (); @@ -109,10 +123,13 @@ public class Akira.ViewLayers.ViewLayerNobs : ViewLayer { context.set_line_width (line_width); context.translate (nob.center_x, nob.center_y); - if (nob.handle_id == Utils.Nobs.Nob.ROTATE) { + if ( + nob.handle_id == Utils.Nobs.Nob.ROTATE || + nob.handle_id == Utils.Nobs.Nob.GRADIENT_START || + nob.handle_id == Utils.Nobs.Nob.GRADIENT_END + ) { context.arc (0, 0, radius, 0, 2.0 * GLib.Math.PI); - } - else { + } else { double x = -radius; double w = radius * 2; double y = -radius; diff --git a/src/Widgets/ColorButton.vala b/src/Widgets/ColorButton.vala index d989af93..fca79e6f 100644 --- a/src/Widgets/ColorButton.vala +++ b/src/Widgets/ColorButton.vala @@ -25,11 +25,12 @@ */ public class Akira.Widgets.ColorButton : Gtk.Button { private unowned Models.ColorModel model; + private unowned Window window; private Gtk.Popover color_popover; private Widgets.ColorChooser? color_chooser = null; - private string? current_color = null; + private string? current_pattern = null; public class SignalBlocker { private unowned ColorButton item; @@ -46,7 +47,9 @@ public class Akira.Widgets.ColorButton : Gtk.Button { protected int block_signal = 0; - public ColorButton () { + public ColorButton (Window window) { + this.window = window; + get_style_context ().add_class ("selected-color"); vexpand = true; width_request = 40; @@ -56,8 +59,20 @@ public class Akira.Widgets.ColorButton : Gtk.Button { color_popover = new Gtk.Popover (this) { position = Gtk.PositionType.BOTTOM }; + color_popover.modal = false; clicked.connect (on_clicked); + + color_popover.closed.connect (() => { + window.event_bus.change_gradient_nobs_visibility (false); + }); + + // This is for preventing a weird bug introduced by making the popover non-modal. + // When a item is selected, sometimes the popover would open and close real fast. + // Making it invisible after unrealizing prevents this bug. + color_popover.unrealize.connect (() => { + color_popover.visible = false; + }); } ~ColorButton () { @@ -76,8 +91,9 @@ public class Akira.Widgets.ColorButton : Gtk.Button { private void on_model_changed () { sensitive = !model.hidden; - var new_color = model.color.to_string (); - if (new_color == current_color) { + // var new_color = model.color.to_string (); + var new_pattern = Utils.Pattern.convert_to_css_linear_gradient (model.pattern); + if (new_pattern == current_pattern) { return; } @@ -86,13 +102,13 @@ public class Akira.Widgets.ColorButton : Gtk.Button { var context = get_style_context (); var css = """.selected-color { - background-color: %s; - border-color: shade (%s, 0.75); - }""".printf (new_color, new_color); + background-image: %s; + border: none; + }""".printf (new_pattern); provider.load_from_data (css, css.length); context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - current_color = new_color; + current_pattern = new_pattern; } catch (Error e) { warning ("Style error: %s", e.message); } @@ -103,12 +119,12 @@ public class Akira.Widgets.ColorButton : Gtk.Button { return; } - color_chooser = new ColorChooser (); - color_chooser.color_changed.connect (color => { + color_chooser = new ColorChooser (model, window); + color_chooser.pattern_changed.connect (pattern => { if (block_signal > 0) { return; } - model.color = color; + model.pattern = pattern; }); color_popover.add (color_chooser); } @@ -120,7 +136,31 @@ public class Akira.Widgets.ColorButton : Gtk.Button { var blocker = new SignalBlocker (this); (blocker); - color_chooser.set_color (model.color); + + color_chooser.set_pattern (model.pattern); color_popover.popup (); + + var canvas = window.main_window.main_view_canvas.canvas; + + var coords = canvas.selection_manager.selection.first_node ().instance.components.center; + var size = canvas.selection_manager.selection.first_node ().instance.components.size; + + Geometry.Point origin = Geometry.Point (coords.x - size.width / 2.0, coords.y - size.height / 2.0); + + // Update position of nobs in ViewLayerNobs. + var start_nob_pos = Geometry.Point ( + model.pattern.start.x * size.width / 100.0 + origin.x, + model.pattern.start.y * size.height / 100.0 + origin.y + ); + var end_nob_pos = Geometry.Point ( + model.pattern.end.x * size.width / 100.0 + origin.x, + model.pattern.end.y * size.height / 100.0 + origin.y + ); + + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_START, start_nob_pos); + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_END, end_nob_pos); + canvas.nob_manager.set_layer_flags_from_pattern_type (model.pattern.type); + + window.event_bus.change_gradient_nobs_visibility (true); } } diff --git a/src/Widgets/ColorChooser.vala b/src/Widgets/ColorChooser.vala index dea66091..4b3c39bc 100644 --- a/src/Widgets/ColorChooser.vala +++ b/src/Widgets/ColorChooser.vala @@ -23,10 +23,14 @@ * Helper class to create a container for the GtkColorChooser. */ public class Akira.Widgets.ColorChooser : Gtk.Grid { - public signal void color_changed (Gdk.RGBA color); + public signal void pattern_changed (Lib.Components.Pattern pattern); private Gtk.ColorChooserWidget chooser; private Gtk.FlowBox global_flowbox; + private GradientEditor gradient_editor; + private PatternTypeChooser pattern_chooser; + + private Models.ColorModel model; /* * Type of color containers to add new colors to. We can potentially create @@ -37,11 +41,26 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { DOCUMENT } - public ColorChooser () { + public ColorChooser (Models.ColorModel model, Window window) { margin_top = margin_bottom = 12; margin_start = margin_end = 3; row_spacing = 12; get_style_context ().add_class ("color-picker"); + this.model = model; + + pattern_chooser = new PatternTypeChooser (model, window); + attach (pattern_chooser, 0, 0, 1, 1); + + gradient_editor = new GradientEditor (model.pattern); + attach (gradient_editor, 0, 1, 1, 1); + + pattern_chooser.pattern_changed.connect ((pattern) => { + gradient_editor.set_pattern (pattern); + }); + + gradient_editor.pattern_edited.connect ((pattern) => { + model.pattern = pattern; + }); chooser = new Gtk.ColorChooserWidget () { hexpand = true, @@ -49,13 +68,17 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { }; chooser.notify["rgba"].connect (on_color_changed); - attach (chooser, 0, 0, 1, 1); + gradient_editor.color_changed.connect ((color) => { + chooser.rgba = color; + }); + + attach (chooser, 0, 2, 1, 1); var global_label = new Gtk.Label (_("Global colors")) { halign = Gtk.Align.START, margin_start = margin_end = 6 }; - attach (global_label, 0, 1, 1, 1); + attach (global_label, 0, 3, 1, 1); global_flowbox = new Gtk.FlowBox () { selection_mode = Gtk.SelectionMode.NONE, @@ -75,11 +98,21 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { global_flowbox.add (add_global_color_btn); foreach (string color in settings.global_colors) { - var btn = create_color_button (color); - global_flowbox.add (btn); + var parser = new Json.Parser (); + try { + parser.load_from_data (color); + var node = parser.get_root (); + + var pattern = new Lib.Components.Pattern.deserialized (node.get_object ()); + + var btn = create_color_button (pattern); + global_flowbox.add (btn); + } catch (Error e) { + warning ("Unable to parse pattern. %s\n", e.message); + } } - attach (global_flowbox, 0, 2, 1, 1); + attach (global_flowbox, 0, 4, 1, 1); show_all (); } @@ -87,18 +120,18 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { * Add the current color to the parent flowbox. */ private void on_save_color (Container parent) { - // Get the currently active color. - var color = chooser.rgba.to_string (); + // Get the currently active pattern. + var pattern = model.pattern; // Create the new color button and connect to its signal. - var btn = create_color_button (color); + var btn = create_color_button (pattern); // Update the colors list and the schema based on the colors container. switch (parent) { case Container.GLOBAL: global_flowbox.add (btn); var array = settings.global_colors; - array += color; + array += Json.to_string (pattern.serialize (), false); settings.global_colors = array; break; @@ -108,16 +141,16 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { } } - private Gtk.FlowBoxChild create_color_button (string color) { + private Gtk.FlowBoxChild create_color_button (Lib.Components.Pattern pattern) { var child = new Gtk.FlowBoxChild () { valign = halign = Gtk.Align.CENTER }; - var btn = new RoundedColorButton (color); - btn.set_color.connect ((color) => { - var rgba_color = Gdk.RGBA (); - rgba_color.parse (color); - chooser.set_rgba (rgba_color); + var btn = new RoundedColorButton (pattern); + btn.set_pattern.connect ((pattern) => { + model.active_pattern_type = pattern.type; + model.pattern = pattern; + pattern_chooser.set_pattern_type (pattern.type); }); child.add (btn); @@ -129,11 +162,12 @@ public class Akira.Widgets.ColorChooser : Gtk.Grid { return (a is AddColorButton) ? -1 : 1; } - public void set_color (Gdk.RGBA color) { - chooser.set_rgba (color); + public void set_pattern (Lib.Components.Pattern pattern) { + pattern_chooser.pattern_changed (pattern); + gradient_editor.pattern_edited (pattern); } private void on_color_changed () { - color_changed (chooser.get_rgba ()); + gradient_editor.color_changed (chooser.get_rgba ()); } } diff --git a/src/Widgets/ColorField.vala b/src/Widgets/ColorField.vala index aaa0aef3..28b52898 100644 --- a/src/Widgets/ColorField.vala +++ b/src/Widgets/ColorField.vala @@ -72,7 +72,7 @@ public class Akira.Widgets.ColorField : Gtk.Entry { var blocker = new SignalBlocker (this); (blocker); - text = Utils.Color.rgba_to_hex_string (model.color); + text = Utils.Color.rgba_to_hex_string (model.pattern.get_first_color ()); sensitive = !model.hidden; } @@ -87,7 +87,7 @@ public class Akira.Widgets.ColorField : Gtk.Entry { } var new_rgba = Utils.Color.hex_to_rgba (text); - model.color = new_rgba; + model.pattern = new Lib.Components.Pattern.solid (new_rgba, false); } private void on_insert_text (string text, int length, ref int position) { diff --git a/src/Widgets/EyeDropperButton.vala b/src/Widgets/EyeDropperButton.vala index 11ac5e5e..d495dedc 100644 --- a/src/Widgets/EyeDropperButton.vala +++ b/src/Widgets/EyeDropperButton.vala @@ -56,7 +56,7 @@ public class Akira.Widgets.EyeDropperButton : Gtk.Button { eyedropper.show_all (); eyedropper.picked.connect ((picked_color) => { - model.color = picked_color; + model.pattern = new Lib.Components.Pattern.solid (picked_color, false); eyedropper.close (); }); diff --git a/src/Widgets/GradientEditor.vala b/src/Widgets/GradientEditor.vala new file mode 100644 index 00000000..091987f6 --- /dev/null +++ b/src/Widgets/GradientEditor.vala @@ -0,0 +1,229 @@ + +/** + * Copyright (c) 2022 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Ashish Shevale + */ + +/* + * This class creates a widget that allows user to create new stop points, + * select existing stop points and assign them colors. + * + * When a different PatternType is selected from PatternChooser, this widget + * will redraw using the stop colors of that pattern. + * + * In order to visualize the pattern, the background of this widget will be colored + * with a linear gradient. + * + * It is disabled for SOLID pattern type. + */ +public class Akira.Widgets.GradientEditor : Gtk.DrawingArea { + public const double NOB_RADII = 5; + public const double STROKE_WIDTH = 0.5; + + // This signal will be triggered when pattern is editor using this editor. + // The PatternChooser will handle this signal. + public signal void pattern_edited (Lib.Components.Pattern pattern); + public signal void color_changed (Gdk.RGBA color); + + // Dimensions of the widget. + private double width; + private double height; + + // Represents all the stop colors of current pattern. + private Lib.Components.Pattern pattern; + // Represents currently active stop color. + private unowned Lib.Components.Pattern.StopColor selected_stop_color; + + public GradientEditor (Lib.Components.Pattern pattern) { + hexpand = true; + height_request = 40; + margin = 5; + + set_events ( + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.BUTTON_MOTION_MASK + ); + + this.pattern = pattern; + + size_allocate.connect (() => { + width = get_allocated_width (); + height = get_allocated_height (); + + draw.connect (draw_editor); + }); + + color_changed.connect (handle_color_changed); + + this.button_press_event.connect (handle_button_press); + this.motion_notify_event.connect (handle_motion_notify); + this.button_release_event.connect (handle_button_release); + } + + public void set_pattern (Lib.Components.Pattern pattern) { + this.pattern = pattern; + queue_draw (); + } + + private bool draw_editor (Cairo.Context context) { + draw_pattern (context); + draw_stop_colors (context); + + return true; + } + + private void draw_pattern (Cairo.Context context) { + context.set_source_rgba (1, 1, 0, 1); + context.move_to (0, 0); + context.rectangle (0, 0, width, height); + context.stroke (); + + var linear_pattern = new Lib.Components.Pattern.linear ( + Geometry.Point (0, height / 2.0), + Geometry.Point (width, height / 2.0), + false + ); + + linear_pattern.colors = pattern.colors; + + var converted_pattern = Utils.Pattern.convert_to_cairo_pattern (linear_pattern); + context.set_source (converted_pattern); + context.rectangle (0, 0, width, height); + context.fill (); + } + + private void draw_stop_colors (Cairo.Context context) { + if (is_solid_pattern ()) { + return; + } + + context.set_source_rgba (0, 0, 0, 1); + context.set_line_width (STROKE_WIDTH); + + // First, draw the horizontal line over which all stop color nobs will be placed. + // Painting two strokes of contrasting colors makes it easier to spot the line. + context.move_to (0, height / 2.0 - STROKE_WIDTH); + context.line_to (width, height / 2.0 - STROKE_WIDTH); + + context.stroke (); + context.set_source_rgba (1, 1, 1, 1); + + context.move_to (0, height / 2.0 + STROKE_WIDTH); + context.line_to (width, height / 2.0 + STROKE_WIDTH); + + context.stroke (); + + // Paint the stop colors. + foreach (var stop_color in pattern.colors) { + var position = stop_color.offset * width; + + if (selected_stop_color.offset == stop_color.offset) { + context.set_source_rgba (0.1568, 0.4745, 0.9823, 1); + } else { + context.set_source_rgba (0, 0, 0, 1); + } + + context.arc (position, height / 2.0, NOB_RADII, 0, 2 * Math.PI); + context.fill (); + + context.set_source_rgba (1, 1, 1, 1); + context.set_line_width (2 * STROKE_WIDTH); + context.arc (position, height / 2.0, NOB_RADII + 1, 0, 2 * Math.PI); + context.stroke (); + } + } + + private void handle_color_changed (Gdk.RGBA color) { + pattern.colors.remove (selected_stop_color); + selected_stop_color.color = color; + pattern.colors.add (selected_stop_color); + + pattern_edited (pattern); + queue_draw (); + } + + private bool handle_button_press (Gdk.EventButton event) { + if (is_solid_pattern ()) { + return true; + } + + // Offset of current event expressed as fraction of width. + double event_offset = event.x / width; + + // Get the stop color at this location. + // If none exists, get ones that are just greater or smaller than it. + var left_stop_color = pattern.colors.floor (Lib.Components.Pattern.StopColor () {offset = event_offset}); + var right_stop_color = pattern.colors.ceil (Lib.Components.Pattern.StopColor () {offset = event_offset}); + + double left_offset_actual_value = (left_stop_color.offset - event_offset) * width; + double right_offset_actual_value = (right_stop_color.offset - event_offset) * width; + + // Check which one is within the threshold. + if (left_offset_actual_value.abs () < NOB_RADII) { + selected_stop_color = left_stop_color; + } else if (right_offset_actual_value.abs () < NOB_RADII) { + selected_stop_color = right_stop_color; + } else { + // If neither of the stop colors is anywhere near close, create a new one here. + var new_stop_color = Lib.Components.Pattern.StopColor () {offset = event_offset, color = left_stop_color.color}; + pattern.colors.add (new_stop_color); + selected_stop_color = new_stop_color; + } + + color_changed (selected_stop_color.color); + pattern_edited (pattern); + queue_draw (); + + return true; + } + + // As we used the BUTTON_MOTION_MASK, this method will be called only when mouse is clicked and dragged. + private bool handle_motion_notify (Gdk.EventMotion event) { + if (selected_stop_color.offset == 0 || selected_stop_color.offset == 1) { + // First and last stop colors are not allowed to be moved. + return true; + } + + double event_offset = event.x / width; + + pattern.colors.remove (selected_stop_color); + selected_stop_color.offset = event_offset; + pattern.colors.add (selected_stop_color); + + pattern_edited (pattern); + queue_draw (); + + return true; + } + + private bool handle_button_release (Gdk.EventButton event) { + return true; + } + + private bool is_solid_pattern () { + if (pattern.colors.size == 1) { + // Single stop color means solid pattern. + return true; + } + + return false; + } + +} diff --git a/src/Widgets/OpacityField.vala b/src/Widgets/OpacityField.vala index 3855977c..11e64146 100644 --- a/src/Widgets/OpacityField.vala +++ b/src/Widgets/OpacityField.vala @@ -76,9 +76,9 @@ public class Akira.Widgets.OpacityField : Gtk.Grid { return; } - var new_color = model.color; + var new_color = model.pattern.get_first_color (); new_color.alpha = field.entry.value / 100; - model.color = new_color; + model.pattern = new Lib.Components.Pattern.solid (new_color, false); } /* @@ -86,12 +86,13 @@ public class Akira.Widgets.OpacityField : Gtk.Grid { */ private void on_model_changed () { sensitive = !model.hidden; - if (field.entry.value / 100 == model.color.alpha) { + var color = model.pattern.get_first_color (); + if (field.entry.value / 100 == color.alpha) { return; } var blocker = new SignalBlocker (this); (blocker); - field.entry.value = Math.round (model.color.alpha * 100); + field.entry.value = Math.round (color.alpha * 100); } } diff --git a/src/Widgets/PatternTypeChooser.vala b/src/Widgets/PatternTypeChooser.vala new file mode 100644 index 00000000..98821c19 --- /dev/null +++ b/src/Widgets/PatternTypeChooser.vala @@ -0,0 +1,108 @@ + +/** + * Copyright (c) 2022 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Ashish Shevale + */ + +/* + * This widget provides a set of options that allow user to select what kind of pattern + * they want to draw. + * Selecting ony one of the buttons will change the pattern type (SOLID, LINEAR, RADIAL). + */ +public class Akira.Widgets.PatternTypeChooser : Granite.Widgets.ModeButton { + private unowned Lib.ViewCanvas canvas; + + // Trigger this signal when the active PatternType changes. + public signal void pattern_changed (Lib.Components.Pattern pattern); + + private Models.ColorModel model; + + public PatternTypeChooser (Models.ColorModel model, Window window) { + construct_chooser_widget (); + + this.set_active ((int) model.pattern.type); + this.model = model; + + // Connect signals. + this.mode_changed.connect ((window) => { + var active_mode = (Lib.Components.Pattern.PatternType) this.selected; + + model.active_pattern_type = active_mode; + pattern_changed (model.pattern); + + handle_pattern_changed (); + }); + + this.canvas = window.main_window.main_view_canvas.canvas; + + window.event_bus.translate_gradient_nob_by_delta.connect ((nob, delta) => { + // This condition checks if this widget is currently open as there can be multiple instances + // if PatternTypeChooser present. And we don't want all of them to handle this signal. + if (!is_drawable ()) { + return; + } + + model.move_pattern_position_by_delta (nob, delta); + handle_pattern_changed (); + }); + } + + public void set_pattern_type (Lib.Components.Pattern.PatternType type) { + this.set_active ((int) type); + } + + private void construct_chooser_widget () { + this.append_text ("Solid"); + this.append_text ("Linear"); + this.append_text ("Radial"); + } + + private void handle_pattern_changed () { + var active_mode = (Lib.Components.Pattern.PatternType) this.selected; + + var coords = canvas.selection_manager.selection.first_node ().instance.components.center; + var size = canvas.selection_manager.selection.first_node ().instance.components.size; + + Geometry.Point origin = Geometry.Point (coords.x - size.width / 2.0, coords.y - size.height / 2.0); + + // Update position of nobs in ViewLayerNobs. + Geometry.Point hidden_pos = Geometry.Point (0, 0); + var start_nob_pos = Geometry.Point ( + model.pattern.start.x * size.width / 100.0 + origin.x, + model.pattern.start.y * size.height / 100.0 + origin.y + ); + var end_nob_pos = Geometry.Point ( + model.pattern.end.x * size.width / 100.0 + origin.x, + model.pattern.end.y * size.height / 100.0 + origin.y + ); + + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_START, hidden_pos); + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_END, hidden_pos); + + if (active_mode == Lib.Components.Pattern.PatternType.LINEAR) { + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_START, start_nob_pos); + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_END, end_nob_pos); + } else if (active_mode == Lib.Components.Pattern.PatternType.RADIAL) { + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_START, start_nob_pos); + canvas.nob_manager.set_gradient_nob_position (Utils.Nobs.Nob.GRADIENT_END, end_nob_pos); + } + + canvas.nob_manager.set_layer_flags_from_pattern_type (active_mode); + } +} diff --git a/src/Widgets/RoundedColorButton.vala b/src/Widgets/RoundedColorButton.vala index 63a87968..14443a55 100644 --- a/src/Widgets/RoundedColorButton.vala +++ b/src/Widgets/RoundedColorButton.vala @@ -21,9 +21,9 @@ */ public class Akira.Widgets.RoundedColorButton : Gtk.Grid { - public signal void set_color (string color); + public signal void set_pattern (Lib.Components.Pattern pattern); - public RoundedColorButton (string color) { + public RoundedColorButton (Lib.Components.Pattern pattern) { var context = get_style_context (); context.add_class ("saved-color-button"); context.add_class ("bg-pattern"); @@ -35,14 +35,15 @@ public class Akira.Widgets.RoundedColorButton : Gtk.Grid { btn.width_request = btn.height_request = 24; btn.valign = btn.halign = Gtk.Align.CENTER; btn.can_focus = false; - btn.tooltip_text = _("Set color to " + color); + // btn.tooltip_text = _("Set color to " + color); try { var provider = new Gtk.CssProvider (); + var css_pattern = Utils.Pattern.convert_to_css_linear_gradient (pattern); var css = """.color-item { - background-color: %s; - border-color: shade (%s, 0.75); - }""".printf (color, color); + background: %s; + border: none; + }""".printf (css_pattern); provider.load_from_data (css, css.length); btn_context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); @@ -52,7 +53,7 @@ public class Akira.Widgets.RoundedColorButton : Gtk.Grid { // Emit the set_color signal when the button is clicked. btn.clicked.connect (() => { - set_color (color); + set_pattern (pattern); }); add (btn); diff --git a/src/meson.build b/src/meson.build index d6ae0bb0..c73501a0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -43,6 +43,7 @@ sources = files( 'Utils/GeometryMath.vala', 'Utils/Bezier.vala', 'Utils/Delegates.vala', + 'Utils/Pattern.vala', 'Layouts/HeaderBar.vala', 'Layouts/MainViewCanvas.vala', @@ -88,6 +89,8 @@ sources = files( 'Widgets/OpacityField.vala', 'Widgets/RoundedColorButton.vala', 'Widgets/ZoomButton.vala', + 'Widgets/GradientEditor.vala', + 'Widgets/PatternTypeChooser.vala', 'Widgets/VirtualizingListBox/VirtualizingListBox.vala', 'Widgets/VirtualizingListBox/VirtualizingListBoxModel.vala', @@ -140,6 +143,7 @@ sources = files( 'Lib/Components/Size.vala', 'Lib/Components/Text.vala', 'Lib/Components/Transform.vala', + 'Lib/Components/Pattern.vala', 'Lib/Items/Model.vala', 'Lib/Items/ModelInstance.vala',