Initial push

This commit is contained in:
2025-09-12 12:07:47 -05:00
parent 77a8ac4941
commit edbd080ad6
48 changed files with 1603 additions and 2 deletions

0
src/widgets/__init__.py Normal file
View File

View File

@@ -0,0 +1,7 @@
from .brush_base import BrushBase
from .grid import Grid
from .arrow import Arrow
from .line import Line
from .circle import Circle
from .square import Square
from .erase import Erase

View File

@@ -0,0 +1,45 @@
# Python imports
# Lib imports
import cairo
import traceback
# Application imports
from data.point import Point
from data.points import Points
from . import BrushBase
class Arrow(BrushBase):
def __init__(self):
super(Arrow, self).__init__()
self.points: Points = Points()
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve = None):
if len(self.points) < 2:
self.points.append( Point(eve.x, eve.y) )
return
self.points[1] = Point(eve.x, eve.y)
self.is_valid = True
def process(self, brush: cairo.Context):
brush.set_line_width(self.size)
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
brush.set_source_rgba(*self.color)
brush.set_operator(cairo.OPERATOR_OVER);
brush.move_to(self.points[0].x, self.points[0].y)
brush.line_to(self.points[1].x, self.points[1].y)
brush.stroke()

View File

@@ -0,0 +1,27 @@
# Python imports
# Lib imports
import cairo
# Application imports
from data.event import Event
class UnboundException(Exception):
...
class BrushBase(Event):
def __init__(self):
super(BrushBase, self).__init__()
self.is_valid: bool = False
self.size: int = 12
self.color: [] = [0.0, 0.0, 0.0, 1.0]
def update(self, eve):
raise UnboundException("Method hasn't been overriden...")
def process(self, brush: cairo.Context):
raise UnboundException("Method hasn't been overriden...")

View File

@@ -0,0 +1,37 @@
# Python imports
# Lib imports
import cairo
# Application imports
from data.point import Point
from data.points import Points
from . import BrushBase
class Circle(BrushBase):
def __init__(self):
super(Circle, self).__init__()
self.points: Points = Points()
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve):
self.points.append( Point(eve.x, eve.y) )
self.is_valid = True
def process(self, brush: cairo.Context):
brush.set_source_rgba(*self.color)
brush.set_operator(cairo.OPERATOR_OVER);
for point in self.points:
brush.move_to(point.x, point.y)
brush.arc(point.x, point.y, self.size, 0, 2 * 3.14)
brush.fill()

View File

@@ -0,0 +1,42 @@
# Python imports
# Lib imports
import cairo
# Application imports
from data.point import Point
from data.points import Points
from . import BrushBase
class Erase(BrushBase):
def __init__(self):
super(Erase, self).__init__()
self.points: Points = Points()
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve):
self.points.append( Point(eve.x, eve.y) )
self.is_valid = True
def process(self, brush: cairo.Context):
if len(self.points) < 2: return
brush.set_line_width(self.size)
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
brush.set_source_rgba(*self.color)
brush.set_operator(cairo.OPERATOR_CLEAR);
brush.move_to(self.points[0].x, self.points[0].y)
for point in self.points[1 : -1]:
brush.line_to(point.x, point.y)
brush.stroke()
brush.move_to(point.x, point.y)

View File

@@ -0,0 +1,46 @@
# Python imports
# Lib imports
import cairo
# Application imports
from . import BrushBase
class Grid(BrushBase):
def __init__(self):
super(Grid, self).__init__()
self.width = 800
self.height = 800
self.grid_spacing = 12 # px
self.color = [0.2, 0.2, 0.2, 0.64] # light gray
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve):
self.is_valid = True
def process(self, brush: cairo.Context):
brush.set_source_rgba(*self.color)
# Draw vertical lines
x = 0
while x < self.width:
brush.move_to(x, 0)
brush.line_to(x, self.height)
x += self.grid_spacing
# Draw horizontal lines
y = 0
while y < self.height:
brush.move_to(0, y)
brush.line_to(self.width, y)
y += self.grid_spacing
brush.stroke()

View File

@@ -0,0 +1,43 @@
# Python imports
# Lib imports
import cairo
import traceback
# Application imports
from data.point import Point
from data.points import Points
from . import BrushBase
class Line(BrushBase):
def __init__(self):
super(Line, self).__init__()
self.points: Points = Points()
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve = None):
self.points.append( Point(eve.x, eve.y) )
if len(self.points) < 2: return
self.is_valid = True
def process(self, brush: cairo.Context):
brush.set_line_width(self.size)
brush.set_line_cap(1) # 0 = BUTT, 1 = ROUND, 2 = SQUARE
brush.set_source_rgba(*self.color)
brush.set_operator(cairo.OPERATOR_OVER);
brush.move_to(self.points[0].x, self.points[0].y)
for point in self.points[1 : -1]:
brush.line_to(point.x, point.y)
brush.stroke()
brush.move_to(point.x, point.y)

View File

@@ -0,0 +1,43 @@
# Python imports
# Lib imports
import cairo
# Application imports
from data.point import Point
from data.points import Points
from . import BrushBase
class Square(BrushBase):
def __init__(self):
super(Square, self).__init__()
self.points: Points = Points()
def __copy__(self):
copy = type(self)()
copy.color = self.color
return copy
def update(self, eve):
if len(self.points) < 2:
self.points.append( Point(eve.x, eve.y) )
return
self.points[1] = Point(eve.x, eve.y)
self.is_valid = True
def process(self, brush: cairo.Context):
x1, y1 = self.points[0].x, self.points[0].y
x2, y2 = self.points[1].x, self.points[1].y
w = x2 - x1
h = y2 - y1
brush.set_source_rgba(*self.color)
brush.set_operator(cairo.OPERATOR_OVER);
brush.rectangle(x1, y1, w, h)
brush.fill()

103
src/widgets/button_box.py Normal file
View File

@@ -0,0 +1,103 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
from .controls.save_as_button import SaveAsButton
class ButtonBox(Gtk.Box):
def __init__(self):
super(ButtonBox, self).__init__()
self.active_btn = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
def _setup_styling(self):
...
def _setup_signals(self):
...
def _load_widgets(self):
arrow_btn = Gtk.ToggleButton(label = "Arrow")
line_btn = Gtk.ToggleButton(label = "Line")
circle_btn = Gtk.ToggleButton(label = "Circle")
square_btn = Gtk.ToggleButton(label = "Square")
erase_btn = Gtk.ToggleButton(label = "Erase")
# save_btn = Gtk.Button(label = "Save")
save_btn = SaveAsButton()
color_btn = Gtk.ColorButton()
size_btn = Gtk.SpinButton()
self.active_btn = line_btn
line_btn.set_active(True)
color_btn.set_title("Pick a Color")
color_btn.set_use_alpha(True)
color_btn.set_rgba( Gdk.RGBA(0.0, 0.0, 0.0, 1.0) )
size_btn.set_numeric(True)
size_btn.set_digits(0)
size_btn.set_snap_to_ticks(True)
size_btn.set_increments(1.0, 10.0)
size_btn.set_range(1.0, 100.0)
size_btn.set_value(12.0)
color_btn.connect("color-set", self.set_brush_color)
size_btn.connect("value-changed", self.set_brush_size)
self.add(arrow_btn)
self.add(line_btn)
self.add(circle_btn)
self.add(square_btn)
self.add(color_btn)
self.add(size_btn)
self.add(erase_btn)
self.add(save_btn)
save_btn.connect("clicked", self.save_image)
# Note: For buttons and mapping assignment. Spin and Color bttn will
# conveniently throw errors OR not be set regarding get_label and NOT map.
for child in self.get_children():
child.set_hexpand(True)
try:
if child.get_label():
child.set_can_focus(False)
child.connect("released", self.set_brush_mode, child.get_label())
except Exception as e:
...
def set_brush_color(self, widget: any):
event_system.emit("set-brush-color", (widget.get_rgba(),))
def set_brush_size(self, widget: any):
event_system.emit("set-brush-size", (widget.get_value(),))
def set_brush_mode(self, widget: any, mode: str):
if mode in ["Save As"]: return
self.active_btn.set_active(False)
widget.set_active(True)
self.active_btn = widget
event_system.emit("set-brush-mode", (mode,))
def save_image(self, widget: any):
fpath = event_system.emit_and_await("save-as")
if not fpath: return
event_system.emit("save-image", (fpath,))

View File

View File

@@ -0,0 +1,57 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from widgets.button_box import ButtonBox
from widgets.draw_area import DrawArea
class ScrollWindow(Gtk.ScrolledWindow):
def __init__(self):
super(ScrollWindow, self).__init__()
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_hexpand(True)
self.set_vexpand(True)
def _setup_signals(self):
...
def _load_widgets(self):
view_port = Gtk.Viewport()
view_port.add( DrawArea() )
self.add(view_port)
class Container(Gtk.Box):
def __init__(self):
super(Container, self).__init__()
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show()
def _setup_styling(self):
self.set_orientation(Gtk.Orientation.VERTICAL)
def _setup_signals(self):
...
def _load_widgets(self):
self.add( ButtonBox() )
self.add( ScrollWindow() )

View File

View File

@@ -0,0 +1,78 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class OpenFileButton(Gtk.Button):
"""docstring for OpenFileButton."""
def __init__(self):
super(OpenFileButton, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_label("Open Image...")
self.set_image( Gtk.Image.new_from_icon_name("gtk-open", 4) )
self.set_always_show_image(True)
self.set_image_position(1) # Left - 0, Right = 1
self.set_hexpand(False)
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("open-file", self._open_file)
def _load_widgets(self):
...
def _open_file(self, widget = None, eve = None, gfile = None):
start_dir = None
if gfile and gfile.query_exists():
start_dir = gfile.get_parent()
chooser = Gtk.FileChooserDialog("Open Image...", None,
Gtk.FileChooserAction.OPEN,
(
Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN,
Gtk.ResponseType.OK
)
)
chooser.set_select_multiple(False)
try:
folder = widget.get_current_file().get_parent() if not start_dir else start_dir
chooser.set_current_folder( folder.get_path() )
except Exception as e:
...
response = chooser.run()
if not response == Gtk.ResponseType.OK:
print("response")
chooser.destroy()
return _gfiles
filename = chooser.get_filename()
if not filename:
chooser.destroy()
return _gfile
chooser.destroy()
return filename

View File

@@ -0,0 +1,68 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class SaveAsButton(Gtk.Button):
def __init__(self):
super(SaveAsButton, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_label("Save As")
self.set_image( Gtk.Image.new_from_icon_name("gtk-save-as", 4) )
self.set_always_show_image(True)
self.set_image_position(1) # Left - 0, Right = 1
self.set_hexpand(False)
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("save-as", self._save_as)
def _load_widgets(self):
...
def _save_as(self, widget = None, eve = None, gfile = None):
start_dir = None
_gfile = None
chooser = Gtk.FileChooserDialog("Save File As...", None,
Gtk.FileChooserAction.SAVE,
(
Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE_AS,
Gtk.ResponseType.OK
)
)
response = chooser.run()
if not response == Gtk.ResponseType.OK:
chooser.destroy()
return _gfile
file = chooser.get_filename()
if not file:
chooser.destroy()
return _gfile
path = file if os.path.isabs(file) else os.path.abspath(file)
print(f"File To Save As: {path}")
chooser.destroy()
return path

225
src/widgets/draw_area.py Normal file
View File

@@ -0,0 +1,225 @@
# Python imports
from copy import copy, deepcopy
# Lib imports
import cairo
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
from libs.surface_manager import SurfaceManager
from libs.event_collection import EventCollection
from data.mouse_buttons import MouseButton
from .surface import Surface
from . import brushes
class DrawArea(Gtk.DrawingArea):
def __init__(self):
super(DrawArea, self).__init__()
self.background_surface: Surface = None
self.primary_surface: Surface = None
self.intermediate_surface: Surface = None
self.surface_manager: SurfaceManager = SurfaceManager()
self.event_collection: EventCollection = EventCollection()
self.is_drawing: bool = False
self.brush_mode: str = "Line"
self.grid_brush: BrushBase = getattr(brushes, "Grid")()
self.brush: BrushBase = getattr(brushes, self.brush_mode)()
self.brush_color = self.brush.color
self.brush_size = self.brush.size
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._set_new_surface()
def _setup_styling(self):
margin_px = 24
self.set_margin_top(margin_px)
self.set_margin_left(margin_px)
self.set_margin_right(margin_px)
self.set_margin_end(margin_px)
def _setup_signals(self):
self.set_can_focus(True)
self.set_size_request(800, 600)
self.add_events(Gdk.EventMask.KEY_PRESS_MASK)
self.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
self.add_events(Gdk.EventMask.BUTTON1_MOTION_MASK)
self.connect("key-release-event", self._key_release_event)
self.connect("button-press-event", self._button_press_event)
self.connect("button-release-event", self._button_release_event)
self.connect("motion-notify-event", self._motion_notify_event)
self.connect("draw", self._draw)
def _subscribe_to_events(self):
event_system.subscribe("save-image", self._save_image)
event_system.subscribe("set-brush-color", self._set_brush_color)
event_system.subscribe("set-brush-size", self._set_brush_size)
event_system.subscribe("set-brush-mode", self._set_brush_mode)
def _load_widgets(self):
...
def _set_new_surface(self):
size_w, \
size_h, \
image_surface = event_system.emit_and_await("get-image-size")
self.grid_brush.width = size_w
self.grid_brush.height = size_h
self.background_surface = Surface(size_w, size_h)
self.primary_surface = Surface(size_w, size_h)
self.intermediate_surface = Surface(size_w, size_h)
if image_surface:
self.primary_surface.create_new_area(image_surface)
self.primary_surface.set_image_data(image_surface.get_data())
self.event_collection.clear()
self.surface_manager.clear()
self.grid_brush.update(None)
self.set_size_request(size_w, size_h)
self.background_surface.update(self.grid_brush)
self.surface_manager.append( self.primary_surface )
def _save_image(self, fpath: str):
self.primary_surface.area.write_to_png(fpath)
def _do_save_image(self):
fpath = event_system.emit_and_await("save-as")
if not fpath: return
self._save_image(fpath)
def _set_brush_color(self, color: Gdk.RGBA):
self.brush = getattr(brushes, self.brush_mode)()
self.brush_color = [color.red, color.green, color.blue, color.alpha]
self.brush.color = self.brush_color
def _set_brush_size(self, size: int):
self.brush_size = size
self.brush = getattr(brushes, self.brush_mode)()
self.brush.size = self.brush_size
def _set_brush_mode(self, mode: str):
self.brush_mode = mode
self.brush = getattr(brushes, self.brush_mode)()
self.brush.color = self.brush_color
self.brush.size = self.brush_size
def _button_press_event(self, widget, eve):
if not self.has_focus():
self.grab_focus()
if eve.type == Gdk.EventType.BUTTON_PRESS and eve.button == MouseButton.LEFT_BUTTON:
self.event_collection.clear()
self.is_drawing = True
self._set_brush_mode(self.brush_mode)
self.intermediate_surface.update(self.brush)
self.brush.update(eve)
if eve.type == Gdk.EventType.BUTTON_PRESS and eve.button == MouseButton.RIGHT_BUTTON:
...
def _button_release_event(self, widget, eve):
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.LEFT_BUTTON:
self.is_drawing = False
self.intermediate_surface.history_manager.clear()
if not self.brush.is_valid: return
self.primary_surface.update(self.brush)
self.queue_draw()
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == MouseButton.RIGHT_BUTTON:
...
def _key_release_event(self, widget, eve):
keyname = Gdk.keyval_name(eve.keyval)
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
is_control = True if modifiers & Gdk.ModifierType.CONTROL_MASK else False
is_shift = True if modifiers & Gdk.ModifierType.SHIFT_MASK else False
if is_control:
if keyname == "z":
if len(self.primary_surface.history_manager) == 0: return
self.event_collection.append( self.primary_surface.history_manager.pop() )
elif keyname == "y":
if len(self.event_collection) == 0: return
self.primary_surface.history_manager.append( self.event_collection.pop() )
elif keyname == "n":
self._set_new_surface()
elif keyname == "s":
self._do_save_image()
self.queue_draw()
def _motion_notify_event(self, area, eve):
if not self.is_drawing: return
self.brush.update(eve)
self.queue_draw()
def _draw(self, area, brush: cairo.Context):
self._draw_background(area, brush)
self._draw_surfaces(area, brush)
self._draw_overlay(area, brush)
return False
def _draw_background(self, area, brush: cairo.Context):
# Note: While drawing, only overlay needs to re-calculate its stack.
# This can just copy what exists than re-calculating the surface.
if self.is_drawing:
brush.set_source_surface(self.background_surface.area, 0.0, 0.0)
brush.paint()
return
brush.set_source_surface(self.background_surface.area, 0.0, 0.0)
self.background_surface.draw()
brush.paint()
def _draw_surfaces(self, area, brush: cairo.Context):
# Note: While drawing, only overlay needs to re-calculate its stack.
# This can just copy what exists than re-calculating each surface.
if self.is_drawing:
for surface in self.surface_manager:
brush.set_source_surface(surface.area, 0.0, 0.0)
brush.paint()
return
for surface in self.surface_manager:
brush.set_source_surface(surface.area, 0.0, 0.0)
surface.draw()
brush.paint()
def _draw_overlay(self, area, brush: cairo.Context):
# Note: When NOT drawing, no overlay data should exist nor be processed...
if not self.is_drawing:
self.intermediate_surface.clear_surface()
return
brush.set_source_surface(self.intermediate_surface.area, 0.0, 0.0)
self.intermediate_surface.draw()
brush.paint()

View File

@@ -0,0 +1,135 @@
# Python imports
import ctypes
# Lib imports
import cairo
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import GdkPixbuf
# Application imports
from .controls.open_file_button import OpenFileButton
from data.cbindings import pixbuf2cairo
class ImageTypeDialog(Gtk.Dialog):
def __init__(self):
super(ImageTypeDialog, self).__init__()
self.img_path = None
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_size_request(420, 320)
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("get-image-size", self._get_image_size)
def _load_widgets(self):
vbox = self.get_content_area()
hbox1 = Gtk.Box()
hbox2 = Gtk.Box()
head_lbl = Gtk.Label("Image Size")
size_w_lbl = Gtk.Label("Width:")
size_h_lbl = Gtk.Label("Height:")
open_file_btn = OpenFileButton()
self.size_w_btn = Gtk.SpinButton()
self.size_h_btn = Gtk.SpinButton()
self.size_w_btn.set_numeric(True)
self.size_w_btn.set_digits(0)
self.size_w_btn.set_snap_to_ticks(True)
self.size_w_btn.set_increments(1.0, 100.0)
self.size_w_btn.set_range(32.0, 10000.0)
self.size_w_btn.set_value(800.0)
self.size_h_btn.set_numeric(True)
self.size_h_btn.set_digits(0)
self.size_h_btn.set_snap_to_ticks(True)
self.size_h_btn.set_increments(1.0, 100.0)
self.size_h_btn.set_range(32.0, 10000.0)
self.size_h_btn.set_value(600.0)
hbox1.set_margin_top(24)
head_lbl.set_hexpand(True)
size_w_lbl.set_hexpand(True)
size_h_lbl.set_hexpand(True)
open_file_btn.set_hexpand(True)
open_file_btn.set_margin_top(24)
open_file_btn.set_margin_bottom(24)
open_file_btn.set_margin_left(24)
open_file_btn.set_margin_right(24)
self.size_w_btn.set_margin_right(24)
self.size_h_btn.set_margin_right(24)
self.size_w_btn.set_hexpand(True)
self.size_h_btn.set_hexpand(True)
open_file_btn.connect("button-release-event", self._open_files)
vbox.add(head_lbl)
hbox1.add(size_w_lbl)
hbox1.add(self.size_w_btn)
hbox2.add(size_h_lbl)
hbox2.add(self.size_h_btn)
vbox.add(hbox1)
vbox.add(hbox2)
vbox.add(open_file_btn)
self.add_button("Continue", Gtk.ResponseType.OK)
vbox.show_all()
def _get_image_size(self):
self.img_path = None
response = self.run()
if response == -4 or response == Gtk.ResponseType.OK:
self.hide()
surface = None
width, height = self.size_w_btn.get_value_as_int(), self.size_h_btn.get_value_as_int()
if self.img_path:
pixbuf = Gtk.Image.new_from_file(self.img_path).get_pixbuf()
width = pixbuf.get_width()
height = pixbuf.get_height()
surface = self._pixels_to_cairo_surface(pixbuf, width, height)
return width, height, surface
def _open_files(self, widget, eve):
self.img_path = event_system.emit_and_await("open-file")
def _pixels_to_cairo_surface(
self,
pixbuf: GdkPixbuf.Pixbuf,
width: int,
height: int
) -> cairo.ImageSurface:
pixbuf_ptr = int(hash(pixbuf)) # WARNING: not stable across runs
cairo_data = pixbuf2cairo.pixbuf_to_cairo_data(pixbuf_ptr)
cairo_stride = width * 4
return cairo.ImageSurface.create_for_data(
bytearray(cairo_data),
cairo.FORMAT_ARGB32,
width,
height,
cairo_stride
)
return surface

53
src/widgets/surface.py Normal file
View File

@@ -0,0 +1,53 @@
# Python imports
# Lib imports
import cairo
# Application imports
from libs.history_manager import HistoryManager
from data.event import Event
class Surface:
def __init__(self, w = 1, h = 1):
super(Surface, self).__init__()
self.image_data = None
self.history_manager: HistoryManager = HistoryManager()
self.create_new_area(None, w, h)
# Note: https://martimm.github.io/gnome-gtk3/content-docs/tutorial/Cairo/drawing-model.html
def create_new_area(self, surface: cairo.ImageSurface = None, w: int = None, h: int = None):
if surface:
self.area = surface
self.brush = cairo.Context(surface)
return
self.area: cairo.ImageSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
self.brush: cairo.Context = cairo.Context(self.area)
def set_image_data(self, data: any):
self.image_data = bytearray(data)
def update(self, event: Event):
self.history_manager.append(event)
def draw(self):
if not self.image_data:
self.clear_surface()
elif self.image_data:
dest_data = self.area.get_data()
dest_data[:len(self.image_data)] = self.image_data[:]
for event in self.history_manager:
if not event.is_valid: continue
event.process(self.brush)
def clear_surface(self):
self.brush.set_operator(cairo.OPERATOR_CLEAR)
self.brush.paint()
self.brush.set_operator(cairo.OPERATOR_OVER)