diff --git a/gradia/backend/tool_config.py b/gradia/backend/tool_config.py index 5d8ab08..ec206a7 100644 --- a/gradia/backend/tool_config.py +++ b/gradia/backend/tool_config.py @@ -21,6 +21,7 @@ import re import json from gradia.backend.settings import Settings +from gradia.utils.colors import make_rgba, tuple_to_rgba class ToolOption: def __init__( @@ -36,9 +37,14 @@ def __init__( ) -> None: self.mode = mode self._size = size - self._primary_color_str = self._rgba_to_str(primary_color or Gdk.RGBA(0,0,0,1)) - self._fill_color_str = self._rgba_to_str(fill_color or Gdk.RGBA(1,1,1,1)) - self._border_color_str = self._rgba_to_str(border_color or Gdk.RGBA(0,0,0,0)) + + default_primary = make_rgba(0.0, 0.0, 0.0, 1.0) + default_fill = make_rgba(1.0, 1.0, 1.0, 1.0) + default_border = make_rgba(0.0, 0.0, 0.0, 0.0) + + self._primary_color_str = self._rgba_to_str(primary_color or default_primary) + self._fill_color_str = self._rgba_to_str(fill_color or default_fill) + self._border_color_str = self._rgba_to_str(border_color or default_border) self._font = font or "Adwaita Sans" self._on_change_callback = on_change_callback self._is_temporary = is_temporary @@ -51,7 +57,7 @@ def _str_to_rgba(self, s: str) -> Gdk.RGBA: if not m: return Gdk.RGBA(0,0,0,1) r, g, b, a = map(float, m.groups()) - return Gdk.RGBA(r, g, b, a) + return make_rgba(r, g, b, a) def _notify_change(self): if self._on_change_callback and not self._is_temporary: @@ -123,11 +129,6 @@ def serialize(self) -> str: @classmethod def deserialize(cls, json_str: str, on_change_callback: Optional[Callable[['ToolOption'], None]] = None) -> "ToolOption": - def tuple_to_rgba(t): - if not t or len(t) != 4: - return Gdk.RGBA(0, 0, 0, 1) - return Gdk.RGBA(t[0], t[1], t[2], t[3]) - data = json.loads(json_str) mode = DrawingMode[data.get("mode", "PEN")] return cls( @@ -211,22 +212,22 @@ def export_config(self) -> dict: class ToolConfig: TEXT_COLORS = [ - (Gdk.RGBA(0.65,0.11,0.18, 1), _("Red")), - (Gdk.RGBA(0.15,0.64,0.41, 1), _("Green")), - (Gdk.RGBA(0.1,0.37,0.71, 1), _("Blue")), - (Gdk.RGBA(0.9,0.65,0.04, 1), _("Yellow")), - (Gdk.RGBA(0,0,0, 1), _("Black")), - (Gdk.RGBA(1.0, 1.0, 1.0, 1), _("White")), + (make_rgba(0.65, 0.11, 0.18, 1.0), _("Red")), + (make_rgba(0.15, 0.64, 0.41, 1.0), _("Green")), + (make_rgba(0.10, 0.37, 0.71, 1.0), _("Blue")), + (make_rgba(0.90, 0.65, 0.04, 1.0), _("Yellow")), + (make_rgba(0.00, 0.00, 0.00, 1.0), _("Black")), + (make_rgba(1.00, 1.00, 1.00, 1.0), _("White")), ] TEXT_BACKGROUND_COLORS = [ - (Gdk.RGBA(0.96,0.38,0.32, 1), _("Red")), - (Gdk.RGBA(0.56,0.94,0.64, 1), _("Green")), - (Gdk.RGBA(0.6,0.76,0.95, 1), _("Blue")), - (Gdk.RGBA(0.98,0.94,0.42, 1), _("Yellow")), - (Gdk.RGBA(1.0, 1.0, 1.0, 1), _("White")), - (Gdk.RGBA(0,0,0, 1), _("Black")), - (Gdk.RGBA(0, 0, 0, 0), _("Transparent")) + (make_rgba(0.96, 0.38, 0.32, 1.0), _("Red")), + (make_rgba(0.56, 0.94, 0.64, 1.0), _("Green")), + (make_rgba(0.60, 0.76, 0.95, 1.0), _("Blue")), + (make_rgba(0.98, 0.94, 0.42, 1.0), _("Yellow")), + (make_rgba(1.00, 1.00, 1.00, 1.0), _("White")), + (make_rgba(0.00, 0.00, 0.00, 1.0), _("Black")), + (make_rgba(0.00, 0.00, 0.00, 0.0), _("Transparent")) ] def __init__( @@ -256,10 +257,6 @@ def __init__( @staticmethod def get_all_tools_positions(): - black = Gdk.RGBA(red=0, green=0, blue=0, alpha=1) - white = Gdk.RGBA(red=1, green=1, blue=1, alpha=1) - transparent = Gdk.RGBA(red=0, green=0, blue=0, alpha=0) - return [ ToolConfig( mode=DrawingMode.SELECT, @@ -331,12 +328,12 @@ def get_all_tools_positions(): has_scale=True, has_primary_color=True, primary_color_list=[ - (Gdk.RGBA(0.88, 0.11, 0.14, 0.4), _("Red")), - (Gdk.RGBA(0.18, 0.76, 0.49, 0.4), _("Green")), - (Gdk.RGBA(0.21, 0.52, 0.89, 0.4), _("Blue")), - (Gdk.RGBA(0.96, 0.83, 0.18, 0.4), _("Yellow")), - (Gdk.RGBA(0.51, 0.24, 0.61, 0.4), _("Purple")), - (Gdk.RGBA(1.0, 1.0, 1.0, 0.4), _("White")), + (make_rgba(0.88, 0.11, 0.14, 0.4), _("Red")), + (make_rgba(0.18, 0.76, 0.49, 0.4), _("Green")), + (make_rgba(0.21, 0.52, 0.89, 0.4), _("Blue")), + (make_rgba(0.96, 0.83, 0.18, 0.4), _("Yellow")), + (make_rgba(0.51, 0.24, 0.61, 0.4), _("Purple")), + (make_rgba(1.0, 1.0, 1.0, 0.4), _("White")), ] ), ToolConfig( diff --git a/gradia/overlay/drawing_actions.py b/gradia/overlay/drawing_actions.py index 6a8b872..59cc169 100644 --- a/gradia/overlay/drawing_actions.py +++ b/gradia/overlay/drawing_actions.py @@ -22,7 +22,7 @@ from enum import Enum import math from gradia.backend.logger import Logger -from gradia.utils.colors import has_visible_color +from gradia.utils.colors import rgba_to_tuple import time import unicodedata @@ -162,7 +162,7 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t coords = [image_to_widget_coords(x, y) for x, y in self.stroke] line_width = self.options.size * scale self._build_path(cr, coords) - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.set_line_width(line_width) cr.stroke() @@ -240,7 +240,7 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t cr.set_line_width(width * scale) cr.set_line_cap(cairo.LINE_CAP_ROUND) - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.move_to(start_x, start_y) cr.line_to(end_x, end_y) @@ -412,23 +412,33 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t text_x_widget = x_widget - text_width_widget / 2 text_y_widget = y_widget - text_height_widget - if self.options.fill_color and any(c > 0 for c in self.options.fill_color): - cr.set_source_rgba(*self.options.fill_color) + if self.options.fill_color and ( + self.options.fill_color.red > 0 or + self.options.fill_color.green > 0 or + self.options.fill_color.blue > 0 or + self.options.fill_color.alpha > 0 + ): + cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color)) self.draw_per_line_background(cr, layout, text_x_widget, text_y_widget, scale) cr.move_to(text_x_widget, text_y_widget) if self.contains_emoji(): - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) PangoCairo.show_layout(cr, layout) else: PangoCairo.layout_path(cr, layout) - if self.options.border_color and any(c > 0 for c in self.options.border_color): - cr.set_source_rgba(*self.options.border_color) + if self.options.border_color and ( + self.options.border_color.red > 0 or + self.options.border_color.green > 0 or + self.options.border_color.blue > 0 or + self.options.border_color.alpha > 0 + ): + cr.set_source_rgba(*rgba_to_tuple(self.options.border_color)) base_line_width = 2.0 adjusted_line_width = base_line_width * scale * (self.font_size / 14.0) cr.set_line_width(adjusted_line_width) cr.stroke_preserve() - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.fill() def get_bounds(self) -> QuadBounds: @@ -453,7 +463,12 @@ def get_bounds(self) -> QuadBounds: x_img, y_img = self.position outline_padding = 0 - if self.options.border_color and any(c > 0 for c in self.options.border_color): + if self.options.border_color and ( + self.options.border_color.red > 0 or + self.options.border_color.green > 0 or + self.options.border_color.blue > 0 or + self.options.border_color.alpha > 0 + ): outline_padding = int(2.0 * (self.font_size / 14.0)) + 1 left_img = x_img - text_width_img // 2 - self.PADDING_X_IMG - outline_padding @@ -485,7 +500,7 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t cr.set_line_width(line_width) cr.move_to(start_x, start_y) cr.line_to(end_x, end_y) - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.stroke() def get_bounds(self) -> QuadBounds: @@ -540,10 +555,10 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t if w > 0 and h > 0: if self.options.fill_color: - cr.set_source_rgba(*self.options.fill_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color)) cr.rectangle(x, y, w, h) cr.fill() - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.set_line_width(self.options.size * scale) cr.rectangle(x, y, w, h) cr.stroke() @@ -599,9 +614,9 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t cr.restore() if self.options.fill_color: - cr.set_source_rgba(*self.options.fill_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color)) cr.fill_preserve() - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.set_line_width(self.options.size * scale) cr.stroke() @@ -621,7 +636,7 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t return coords = [image_to_widget_coords(x, y) for x, y in self.stroke] cr.set_operator(cairo.Operator.MULTIPLY) - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.set_line_width(self.options.size * scale * 2) cr.set_line_cap(cairo.LineCap.BUTT) cr.move_to(*coords[0]) @@ -724,12 +739,12 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t x_widget, y_widget = image_to_widget_coords(*self.position) r_widget = self.options.size * 2 * scale - cr.set_source_rgba(*self.options.fill_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.fill_color)) cr.arc(x_widget, y_widget, r_widget, 0, 2 * math.pi) cr.fill_preserve() if self.options.border_color.alpha != 0 and self.options.fill_color.alpha != 0: - cr.set_source_rgba(*self.options.border_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.border_color)) cr.set_line_width(2.0 * scale) cr.stroke() else: @@ -746,12 +761,17 @@ def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], t cr.move_to(tx, ty) cr.text_path(text) - if self.options.border_color and any(c > 0 for c in self.options.border_color): - cr.set_source_rgba(*self.options.border_color) + if self.options.border_color and ( + self.options.border_color.red > 0 or + self.options.border_color.green > 0 or + self.options.border_color.blue > 0 or + self.options.border_color.alpha > 0 + ): + cr.set_source_rgba(*rgba_to_tuple(self.options.border_color)) cr.set_line_width(4 * scale) cr.stroke_preserve() - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba(*rgba_to_tuple(self.options.primary_color)) cr.fill() def contains_point(self, px_img: int, py_img: int) -> bool: @@ -762,7 +782,14 @@ def contains_point(self, px_img: int, py_img: int) -> bool: def get_bounds(self) -> QuadBounds: x_img, y_img = self.position - outline_padding = 2 if self.options.border_color and any(c > 0 for c in self.options.border_color) else 0 + outline_padding = 0 + if self.options.border_color and ( + self.options.border_color.red > 0 or + self.options.border_color.green > 0 or + self.options.border_color.blue > 0 or + self.options.border_color.alpha > 0 + ): + outline_padding = 2 total_radius = self.options.size * 2 + outline_padding + 1 return QuadBounds.from_rect(x_img - total_radius, y_img - total_radius, x_img + total_radius, y_img + total_radius) diff --git a/gradia/overlay/drawing_overlay.py b/gradia/overlay/drawing_overlay.py index f25b8e1..cc0c257 100644 --- a/gradia/overlay/drawing_overlay.py +++ b/gradia/overlay/drawing_overlay.py @@ -371,7 +371,7 @@ def _draw_selection_box(self, cr: cairo.Context, scale: float): points = bounds.get_points() widget_points = [self._image_to_widget_coords(int(p[0]), int(p[1])) for p in points] accent = Adw.StyleManager.get_default().get_accent_color_rgba() - cr.set_source_rgba(*accent) + cr.set_source_rgba(accent.red, accent.green, accent.blue, accent.alpha) cr.set_line_width(2) cr.move_to(*widget_points[0]) for point in widget_points[1:]: @@ -384,7 +384,7 @@ def _draw_selection_box(self, cr: cairo.Context, scale: float): cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.rectangle(handle_x, handle_y, HANDLE_SIZE, HANDLE_SIZE) cr.fill() - cr.set_source_rgba(*accent) + cr.set_source_rgba(accent.red, accent.green, accent.blue, accent.alpha) cr.rectangle(handle_x, handle_y, HANDLE_SIZE, HANDLE_SIZE) cr.stroke() @@ -769,7 +769,12 @@ def _on_draw(self, area, cr: cairo.Context, width: int, height: int): action.draw(cr, self._image_to_widget_coords, scale) if self.is_drawing and self.options.mode != DrawingMode.TEXT and self.options.mode != DrawingMode.NUMBER: - cr.set_source_rgba(*self.options.primary_color) + cr.set_source_rgba( + self.options.primary_color.red, + self.options.primary_color.green, + self.options.primary_color.blue, + self.options.primary_color.alpha, + ) if self.options.mode == DrawingMode.PEN and len(self.current_stroke) > 1: StrokeAction(self.current_stroke, self.options.copy()).draw(cr, self._image_to_widget_coords, scale) elif self.options.mode == DrawingMode.HIGHLIGHTER and len(self.current_stroke) > 1: diff --git a/gradia/ui/widget/quick_color_picker.py b/gradia/ui/widget/quick_color_picker.py index 7e1d82a..fb93299 100644 --- a/gradia/ui/widget/quick_color_picker.py +++ b/gradia/ui/widget/quick_color_picker.py @@ -16,22 +16,22 @@ # SPDX-License-Identifier: GPL-3.0-or-later from gi.repository import Gtk, Adw, Gdk, GObject, Gio -from gradia.utils.colors import is_light_color_rgba, rgba_to_hex +from gradia.utils.colors import is_light_color_rgba, rgba_to_hex, make_rgba + class ColorPickerMixin: def _get_base_colors(self, alpha=1.0, secondary=False): base_colors = [ - (Gdk.RGBA(0.88, 0.11, 0.14, alpha), _("Red")), - (Gdk.RGBA(0.18, 0.76, 0.49, alpha), _("Green")), - (Gdk.RGBA(0.21, 0.52, 0.89, alpha), _("Blue")), - (Gdk.RGBA(0.96, 0.83, 0.18, alpha), _("Yellow")), - (Gdk.RGBA(0.0, 0.0, 0.0, alpha), _("Black")), - (Gdk.RGBA(1.0, 1.0, 1.0, alpha), _("White")), - ] + (make_rgba(0.88, 0.11, 0.14, alpha), _("Red")), + (make_rgba(0.18, 0.76, 0.49, alpha), _("Green")), + (make_rgba(0.21, 0.52, 0.89, alpha), _("Blue")), + (make_rgba(0.96, 0.83, 0.18, alpha), _("Yellow")), + (make_rgba(0.0, 0.0, 0.0, alpha), _("Black")), + (make_rgba(1.0, 1.0, 1.0, alpha), _("White")), + ] if secondary: - base_colors.append((Gdk.RGBA(0, 0, 0, 0), _("Transparent"))) - + base_colors.append((make_rgba(0.0, 0.0, 0.0, 0.0), _("Transparent"))) return base_colors diff --git a/gradia/utils/colors.py b/gradia/utils/colors.py index 260a4dc..f0cb51e 100644 --- a/gradia/utils/colors.py +++ b/gradia/utils/colors.py @@ -20,6 +20,19 @@ HexColor = str RGBTuple = tuple[int, int, int] +def make_rgba(r: float, g: float, b: float, a: float) -> Gdk.RGBA: + """Utility to construct a Gdk.RGBA with the given components. + + Gdk.RGBA does not take RGBA values as constructor arguments; you must + instantiate it and then assign the channels explicitly. + """ + rgba = Gdk.RGBA() + rgba.red = r + rgba.green = g + rgba.blue = b + rgba.alpha = a + return rgba + def hex_to_rgba(hex_color: HexColor, alpha: float | None = None) -> Gdk.RGBA: """ Converts hexadecimal color code to `Gdk.RGBA` object. @@ -58,6 +71,23 @@ def hex_to_rgb(hex_color: HexColor) -> RGBTuple: r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) return (r, g, b) +def rgba_to_tuple(color: Gdk.RGBA) -> tuple[float, float, float, float]: + return (color.red, color.green, color.blue, color.alpha) + +def tuple_to_rgba(color: tuple[float, float, float, float] | None) -> Gdk.RGBA: + rgba = Gdk.RGBA() + if not color or len(color) != 4: + rgba.red = 0.0 + rgba.green = 0.0 + rgba.blue = 0.0 + rgba.alpha = 1.0 + return rgba + rgba.red = float(color[0]) + rgba.green = float(color[1]) + rgba.blue = float(color[2]) + rgba.alpha = float(color[3]) + return rgba + def has_visible_color(color): return any(c > 0 for c in color[:3]) or (len(color) > 3 and color[3] > 0)