This commit is contained in:
2026-06-03 21:26:54 +02:00
parent 05e6b8d061
commit d21e5175d5
125 changed files with 41986 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
import math
from typing import Literal
import cairo
import gi
from fabric.core.service import Property
from fabric.widgets.widget import Widget
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GdkPixbuf, Gtk # noqa: E402
class CircleImage(Gtk.DrawingArea, Widget):
"""A widget that displays an image in a circular shape with a 1:1 aspect ratio."""
@Property(int, "read-write")
def angle(self) -> int:
return self._angle
@angle.setter
def angle(self, value: int):
self._angle = value % 360
self.queue_draw()
def __init__(
self,
image_file: str | None = None,
pixbuf: GdkPixbuf.Pixbuf | None = None,
name: str | None = None,
visible: bool = True,
all_visible: bool = False,
style: str | None = None,
tooltip_text: str | None = None,
tooltip_markup: str | None = None,
h_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None,
v_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None,
h_expand: bool = False,
v_expand: bool = False,
size: int | None = None,
**kwargs,
):
Gtk.DrawingArea.__init__(self)
Widget.__init__(
self,
name=name,
visible=visible,
all_visible=all_visible,
style=style,
tooltip_text=tooltip_text,
tooltip_markup=tooltip_markup,
h_align=h_align,
v_align=v_align,
h_expand=h_expand,
v_expand=v_expand,
size=size,
**kwargs,
)
self.size = size if size is not None else 100 # Default size if not provided
self._angle = 0
self._orig_image: GdkPixbuf.Pixbuf | None = None # Original image for reprocessing
self._image: GdkPixbuf.Pixbuf | None = None
if image_file:
pix = GdkPixbuf.Pixbuf.new_from_file(image_file)
self._orig_image = pix
self._image = self._process_image(pix)
elif pixbuf:
self._orig_image = pixbuf
self._image = self._process_image(pixbuf)
self.connect("draw", self.on_draw)
def _process_image(self, pixbuf: GdkPixbuf.Pixbuf) -> GdkPixbuf.Pixbuf:
"""Crop the image to a centered square and scale it to the widgets size."""
width, height = pixbuf.get_width(), pixbuf.get_height()
if width != height:
square_size = min(width, height)
x_offset = (width - square_size) // 2
y_offset = (height - square_size) // 2
pixbuf = pixbuf.new_subpixbuf(x_offset, y_offset, square_size, square_size)
else:
square_size = width
if square_size != self.size:
pixbuf = pixbuf.scale_simple(self.size, self.size, GdkPixbuf.InterpType.BILINEAR)
return pixbuf
def on_draw(self, widget: "CircleImage", ctx: cairo.Context):
if self._image:
ctx.save()
# Create a circular clipping path
ctx.arc(self.size / 2, self.size / 2, self.size / 2, 0, 2 * math.pi)
ctx.clip()
# Rotate around the center of the square image
ctx.translate(self.size / 2, self.size / 2)
ctx.rotate(self._angle * math.pi / 180.0)
ctx.translate(-self.size / 2, -self.size / 2)
Gdk.cairo_set_source_pixbuf(ctx, self._image, 0, 0)
ctx.paint()
ctx.restore()
def set_image_from_file(self, new_image_file: str):
if not new_image_file:
return
pixbuf = GdkPixbuf.Pixbuf.new_from_file(new_image_file)
self._orig_image = pixbuf
self._image = self._process_image(pixbuf)
self.queue_draw()
def set_image_from_pixbuf(self, pixbuf: GdkPixbuf.Pixbuf):
if not pixbuf:
return
self._orig_image = pixbuf
self._image = self._process_image(pixbuf)
self.queue_draw()
def set_image_size(self, size: int):
self.size = size
if self._orig_image:
self._image = self._process_image(self._orig_image)
self.queue_draw()
+38
View File
@@ -0,0 +1,38 @@
import math
from typing import cast
import cairo
from fabric.widgets.image import Image
from gi.repository import Gtk
class CustomImage(Image):
def do_render_rectangle(
self, cr: cairo.Context, width: int, height: int, radius: int = 0
):
cr.move_to(radius, 0)
cr.line_to(width - radius, 0)
cr.arc(width - radius, radius, radius, -(math.pi / 2), 0)
cr.line_to(width, height - radius)
cr.arc(width - radius, height - radius, radius, 0, (math.pi / 2))
cr.line_to(radius, height)
cr.arc(radius, height - radius, radius, (math.pi / 2), math.pi)
cr.line_to(0, radius)
cr.arc(radius, radius, radius, math.pi, (3 * (math.pi / 2)))
cr.close_path()
def do_draw(self, cr: cairo.Context):
context = self.get_style_context()
width, height = self.get_allocated_width(), self.get_allocated_height()
cr.save()
self.do_render_rectangle(
cr,
width,
height,
cast(int, context.get_property("border-radius", Gtk.StateFlags.NORMAL)),
)
cr.clip()
Image.do_draw(self, cr)
cr.restore()
+367
View File
@@ -0,0 +1,367 @@
from collections.abc import Iterable
from enum import Enum
from typing import Literal, cast, overload
import gi
import OpenGL.GL as GL
from fabric import Application, Property, Signal
from fabric.widgets.widget import Widget
from OpenGL.GL.shaders import compileProgram, compileShader
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk
class ShadertoyUniformType(Enum):
# TODO: add more types
FLOAT = 1
INTEGER = 2
VECTOR = 3
TEXTURE = 4
class ShadertoyCompileError(Exception): ...
class Shadertoy(Gtk.GLArea, Widget):
@Signal # pygobject signal
def ready(self) -> None: ...
@Property(str, "read-write")
def shader_buffer(self) -> str:
return self._shader_buffer
@shader_buffer.setter
def shader_buffer(self, shader_buffer: str) -> None:
self._shader_buffer = shader_buffer
if not self._ready:
return
self._shader_uniforms.clear()
self.do_realize()
self.queue_draw()
return
# signatures for building a replica of shadertoy
DEFAULT_VERTEX_SHADER = """
#version 330
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
"""
DEFAULT_FRAGMENT_UNIFORMS = """
#version 330
uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
uniform float iTimeDelta; // render time (in seconds)
uniform float iFrameRate; // shader frame rate
uniform int iFrame; // shader playback frame
uniform float iChannelTime[4]; // channel playback time (in seconds)
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
uniform sampler2D iChannel0; // input channel. XX = 2D/Cube
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
uniform vec4 iDate; // (year, month, day, time in seconds)
uniform float iSampleRate; // sound sample rate (i.e., 44100)
"""
FRAGMENT_MAIN_FUNCTION = """
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
"""
def __init__(
self,
shader_buffer: str,
shader_uniforms: list[
tuple[
str,
ShadertoyUniformType,
bool | float | int | tuple[float, ...] | GdkPixbuf.Pixbuf,
]
]
| None = None,
name: str | None = None,
visible: bool = True,
all_visible: bool = False,
style: str | None = None,
style_classes: Iterable[str] | str | None = None,
tooltip_text: str | None = None,
tooltip_markup: str | None = None,
h_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
v_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
h_expand: bool = False,
v_expand: bool = False,
size: Iterable[int] | int | None = None,
**kwargs,
):
Gtk.GLArea.__init__(
self # type: ignore
)
Widget.__init__(
self,
name,
visible,
all_visible,
style,
style_classes,
tooltip_text,
tooltip_markup,
h_align,
v_align,
h_expand,
v_expand,
size,
**kwargs,
)
self._shader_buffer = shader_buffer
self._shader_uniforms = shader_uniforms or []
# widget settings
self.set_required_version(3, 3)
self.set_has_depth_buffer(False)
self.set_has_stencil_buffer(False)
self._ready = False
self._program = None
self._vao = None
self._quad_vbo = None
self._texture_units = {}
# timer
self._start_time = GLib.get_monotonic_time() / 1e6
self._frame_time = self._start_time
self._frame_count = 0
# to avoid a constant framerate we tell
# gtk to render a frame whenever possible
self._tick_id = self.add_tick_callback(lambda *_: (self.queue_draw(), True)[1])
def do_bake_program(self):
try:
vertex_shader = compileShader(
self.DEFAULT_VERTEX_SHADER, GL.GL_VERTEX_SHADER
)
fragment_shader = compileShader(
self.DEFAULT_FRAGMENT_UNIFORMS
+ self._shader_buffer
+ self.FRAGMENT_MAIN_FUNCTION,
GL.GL_FRAGMENT_SHADER,
)
except Exception as e:
raise ShadertoyCompileError(
f"couldn't compile the provided shader, OpenGL error:\n {e}"
)
return compileProgram(vertex_shader, fragment_shader)
def do_realize(self, *_):
Gtk.GLArea.do_realize(self)
if not self._ready:
ctx = self.get_context()
if (err := self.get_error()) or not ctx:
raise RuntimeError(
f"couldn't initialize the drawing context, error: {err or 'context is None'}"
)
ctx.make_current()
if self._program:
GL.glDeleteProgram(self._program)
self._program = None
self._program = self.do_bake_program()
# NOTE: for this to work (alpha pixels) `self.set_has_alpha(True)` must be done
# this breaks some fragment shaders, for some reason, so i'm leaving it for anyone willing to use
GL.glEnable(GL.GL_BLEND)
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
self._quad_vbo = GL.glGenBuffers(1)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._quad_vbo)
# this is not so good, unless the introduction of numpy, we must do
# a hack to generate an array GL would accept, i've tried using
# the "array" python library but it doesn't seem to be working
# cast python type into GL type (list[float] -> arraybuf[GLfloat])
quad_verts = (-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0)
array_type = GL.GLfloat * len(quad_verts)
GL.glBufferData(
GL.GL_ARRAY_BUFFER,
len(quad_verts) * 4,
array_type(*quad_verts),
GL.GL_STATIC_DRAW,
)
self._vao = GL.glGenVertexArrays(1)
GL.glBindVertexArray(self._vao)
position = GL.glGetAttribLocation(self._program, "position")
GL.glEnableVertexAttribArray(position)
GL.glVertexAttribPointer(position, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None)
for uname, utype, uvalue in self._shader_uniforms:
self.set_uniform(uname, utype, uvalue) # type: ignore
self._ready = True
self.ready()
return
def do_get_timing(self) -> tuple[float, float, float]:
current_time = GLib.get_monotonic_time() / 1e6
delta_time = current_time - self._frame_time
return current_time, delta_time, (1.0 / delta_time) if delta_time > 0 else 0.0
def do_post_render(self, time: float):
self._frame_time = time
self._frame_count += 1
return
def do_render(self, ctx: Gdk.GLContext):
if not self._program:
if self._tick_id:
self.remove_tick_callback(self._tick_id)
self._tick_id = 0
return False
GL.glUseProgram(self._program)
# clear up for next frame
GL.glClear(GL.GL_COLOR_BUFFER_BIT)
alloc = self.get_allocation()
width: int = alloc.width # type: ignore
height: int = alloc.height # type: ignore
mouse_pos = cast(tuple[int, int], self.get_pointer())
current_time, delta_time, frame_rate = self.do_get_timing()
self.set_uniform(
"iTime", ShadertoyUniformType.FLOAT, current_time - self._start_time
)
self.set_uniform("iFrame", ShadertoyUniformType.INTEGER, self._frame_count)
self.set_uniform("iTimeDelta", ShadertoyUniformType.FLOAT, delta_time)
self.set_uniform("iFrameRate", ShadertoyUniformType.FLOAT, frame_rate)
self.set_uniform(
"iResolution", ShadertoyUniformType.VECTOR, (width, height, 1.0)
)
self.set_uniform(
"iMouse",
ShadertoyUniformType.VECTOR,
(mouse_pos[0], height - mouse_pos[1], 0, 0),
)
# paint the quad
GL.glBindVertexArray(self._vao)
GL.glDrawArrays(GL.GL_TRIANGLE_STRIP, 0, 4)
self.do_post_render(current_time)
return True
def do_resize(self, width: int, height: int):
Gtk.GLArea.do_resize(self, width, height)
GL.glViewport(0, 0, width, height)
return
@overload
def set_uniform(
self, name: str, type: Literal[ShadertoyUniformType.FLOAT], value: float
): ...
@overload
def set_uniform(
self, name: str, type: Literal[ShadertoyUniformType.INTEGER], value: int
): ...
@overload
def set_uniform(
self,
name: str,
type: Literal[ShadertoyUniformType.VECTOR],
value: tuple[float, ...],
): ...
@overload
def set_uniform(
self,
name: str,
type: Literal[ShadertoyUniformType.TEXTURE],
value: GdkPixbuf.Pixbuf,
): ...
def set_uniform(
self,
name: str,
type: ShadertoyUniformType,
value: bool | float | int | tuple[float, ...] | GdkPixbuf.Pixbuf,
):
if not self._program:
raise RuntimeError("the shader program is not initialized")
GL.glUseProgram(self._program)
location = GL.glGetUniformLocation(self._program, name)
match type:
case ShadertoyUniformType.VECTOR:
value = cast(tuple[float, ...], value)
(
GL.glUniform2f
if (vlen := len(value)) == 2
else GL.glUniform3f
if vlen == 3
else GL.glUniform4f
)(location, *value)
case ShadertoyUniformType.FLOAT:
GL.glUniform1f(location, value)
case ShadertoyUniformType.INTEGER:
GL.glUniform1i(location, value)
case ShadertoyUniformType.TEXTURE:
# who dislikes boilerplate?
value = cast(GdkPixbuf.Pixbuf, value).flip(False)
format = GL.GL_RGBA if value.get_has_alpha() else GL.GL_RGB
if name not in self._texture_units:
texture = GL.glGenTextures(1)
self._texture_units[name] = (len(self._texture_units), texture)
else:
texture_unit, texture = self._texture_units[name]
texture_unit = self._texture_units[name][0]
GL.glActiveTexture(GL.GL_TEXTURE0 + texture_unit)
GL.glBindTexture(GL.GL_TEXTURE_2D, texture)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
GL.glTexParameteri(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR
)
GL.glTexParameteri(
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR
)
# "upload" the texture
GL.glTexImage2D(
GL.GL_TEXTURE_2D,
0, # detail level (woah?)
format, # result format
value.get_width(),
value.get_height(),
0, # "border"
format, # input format
GL.GL_UNSIGNED_BYTE,
value.get_pixels(),
)
GL.glGenerateMipmap(GL.GL_TEXTURE_2D)
# all aboard...
GL.glUniform1i(location, texture_unit)
+359
View File
@@ -0,0 +1,359 @@
from gi.repository import Gdk, GObject, Gtk
import re
from collections.abc import Iterable
from enum import Enum
from typing import Literal, cast
import cairo
import gi
from fabric.core.service import Property
from fabric.utils.helpers import extract_css_values, get_enum_member
from fabric.widgets.window import Window
from loguru import logger
gi.require_version("Gtk", "3.0")
try:
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import GtkLayerShell
except:
raise ImportError(
"looks like we don't have gtk-layer-shell installed, make sure to install it first (as well as using wayland)"
)
class WaylandWindowExclusivity(Enum):
NONE = 1
NORMAL = 2
AUTO = 3
class Layer(GObject.GEnum):
BACKGROUND = 0
BOTTOM = 1
TOP = 2
OVERLAY = 3
ENTRY_NUMBER = 4
class KeyboardMode(GObject.GEnum):
NONE = 0
EXCLUSIVE = 1
ON_DEMAND = 2
ENTRY_NUMBER = 3
class Edge(GObject.GEnum):
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
ENTRY_NUMBER = 4
class WaylandWindow(Window):
@Property(
Layer,
flags="read-write",
default_value=Layer.TOP,
)
def layer(self) -> Layer: # type: ignore
return self._layer # type: ignore
@layer.setter
def layer(
self,
value: Literal["background", "bottom", "top", "overlay"] | Layer,
) -> None:
self._layer = get_enum_member(Layer, value, default=Layer.TOP)
return GtkLayerShell.set_layer(self, self._layer)
@Property(int, "read-write")
def monitor(self) -> int:
if not (monitor := cast(Gdk.Monitor, GtkLayerShell.get_monitor(self))):
return -1
display = monitor.get_display() or Gdk.Display.get_default()
for i in range(0, display.get_n_monitors()):
if display.get_monitor(i) is monitor:
return i
return -1
@monitor.setter
def monitor(self, monitor: int | Gdk.Monitor) -> bool:
if isinstance(monitor, int):
display = Gdk.Display().get_default()
monitor = display.get_monitor(monitor)
return (
(GtkLayerShell.set_monitor(self, monitor), True)[1]
if monitor is not None
else False
)
@Property(WaylandWindowExclusivity, "read-write")
def exclusivity(self) -> WaylandWindowExclusivity:
return self._exclusivity
@exclusivity.setter
def exclusivity(
self, value: Literal["none", "normal", "auto"] | WaylandWindowExclusivity
) -> None:
value = get_enum_member(
WaylandWindowExclusivity, value, default=WaylandWindowExclusivity.NONE
)
self._exclusivity = value
match value:
case WaylandWindowExclusivity.NORMAL:
return GtkLayerShell.set_exclusive_zone(self, True)
case WaylandWindowExclusivity.AUTO:
return GtkLayerShell.auto_exclusive_zone_enable(self)
case _:
return GtkLayerShell.set_exclusive_zone(self, False)
@Property(bool, "read-write", default_value=False)
def pass_through(self) -> bool:
return self._pass_through
@pass_through.setter
def pass_through(self, pass_through: bool = False):
self._pass_through = pass_through
region = cairo.Region() if pass_through is True else None
self.input_shape_combine_region(region)
del region
return
@Property(
KeyboardMode,
"read-write",
default_value=KeyboardMode.NONE,
)
def keyboard_mode(self) -> KeyboardMode:
return self._keyboard_mode
@keyboard_mode.setter
def keyboard_mode(
self,
value: Literal[
"none",
"exclusive",
"on-demand",
"entry-number",
]
| KeyboardMode,
):
self._keyboard_mode = get_enum_member(
KeyboardMode, value, default=KeyboardMode.NONE
)
return GtkLayerShell.set_keyboard_mode(self, self._keyboard_mode)
@Property(tuple[Edge, ...], "read-write")
def anchor(self):
return tuple(
x
for x in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]
if GtkLayerShell.get_anchor(self, x)
)
@anchor.setter
def anchor(self, value: str | Iterable[Edge]) -> None:
self._anchor = value
if isinstance(value, (list, tuple)) and all(
isinstance(edge, Edge) for edge in value
):
for edge in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]:
if edge not in value:
GtkLayerShell.set_anchor(self, edge, False)
GtkLayerShell.set_anchor(self, edge, True)
return
elif isinstance(value, str):
for edge, anchored in WaylandWindow.extract_edges_from_string(
value
).items():
GtkLayerShell.set_anchor(self, edge, anchored)
return
@Property(tuple[int, ...], flags="read-write")
def margin(self) -> tuple[int, ...]:
return tuple(
GtkLayerShell.get_margin(self, x)
for x in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]
)
@margin.setter
def margin(self, value: str | Iterable[int]) -> None:
for edge, mrgv in WaylandWindow.extract_margin(value).items():
GtkLayerShell.set_margin(self, edge, mrgv)
return
@Property(object, "read-write")
def keyboard_mode(self):
kb_mode = GtkLayerShell.get_keyboard_mode(self)
if GtkLayerShell.get_keyboard_interactivity(self):
kb_mode = KeyboardMode.EXCLUSIVE
return kb_mode
@keyboard_mode.setter
def keyboard_mode(
self,
value: Literal["none", "exclusive", "on-demand"] | KeyboardMode,
):
return GtkLayerShell.set_keyboard_mode(
self,
get_enum_member(
KeyboardMode,
value,
default=KeyboardMode.NONE,
),
)
def __init__(
self,
layer: Literal["background", "bottom", "top", "overlay"] | Layer = Layer.TOP,
anchor: str = "",
margin: str | Iterable[int] = "0px 0px 0px 0px",
exclusivity: Literal["auto", "normal", "none"]
| WaylandWindowExclusivity = WaylandWindowExclusivity.NONE,
keyboard_mode: Literal["none", "exclusive", "on-demand"]
| KeyboardMode = KeyboardMode.NONE,
pass_through: bool = False,
monitor: int | Gdk.Monitor | None = None,
title: str = "fabric",
type: Literal["top-level", "popup"] | Gtk.WindowType = Gtk.WindowType.TOPLEVEL,
child: Gtk.Widget | None = None,
name: str | None = None,
visible: bool = True,
all_visible: bool = False,
style: str | None = None,
style_classes: Iterable[str] | str | None = None,
tooltip_text: str | None = None,
tooltip_markup: str | None = None,
h_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
v_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
h_expand: bool = False,
v_expand: bool = False,
size: Iterable[int] | int | None = None,
**kwargs,
):
Window.__init__(
self,
title=title,
type=type,
child=child,
name=name,
visible=False,
all_visible=False,
style=style,
style_classes=style_classes,
tooltip_text=tooltip_text,
tooltip_markup=tooltip_markup,
h_align=h_align,
v_align=v_align,
h_expand=h_expand,
v_expand=v_expand,
size=size,
**kwargs,
)
self._layer = Layer.ENTRY_NUMBER
self._keyboard_mode = KeyboardMode.NONE
self._anchor = anchor
self._exclusivity = WaylandWindowExclusivity.NONE
self._pass_through = pass_through
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_namespace(self, title)
self.connect(
"notify::title",
lambda *_: GtkLayerShell.set_namespace(self, self.get_title()),
)
if monitor is not None:
self.monitor = monitor
self.layer = layer
self.anchor = anchor
self.margin = margin
self.keyboard_mode = keyboard_mode
self.exclusivity = exclusivity
self.pass_through = pass_through
self.show_all() if all_visible is True else self.show() if visible is True else None
def steal_input(self) -> None:
return GtkLayerShell.set_keyboard_interactivity(self, True)
def return_input(self) -> None:
return GtkLayerShell.set_keyboard_interactivity(self, False)
# custom overrides
def show(self) -> None:
super().show()
return self.do_handle_post_show_request()
def show_all(self) -> None:
super().show_all()
return self.do_handle_post_show_request()
def do_handle_post_show_request(self) -> None:
if not self.get_children():
logger.warning(
"[WaylandWindow] showing an empty window is not recommended, some compositors might freak out."
)
self.pass_through = self._pass_through
return
@staticmethod
def extract_anchor_values(string: str) -> tuple[str, ...]:
"""
extracts the geometry values from a given geometry string.
:param string: the string containing the geometry values.
:type string: str
:return: a list of unique directions extracted from the geometry string.
:rtype: list
"""
direction_map = {"l": "left", "t": "top", "r": "right", "b": "bottom"}
pattern = re.compile(r"\b(left|right|top|bottom)\b", re.IGNORECASE)
matches = pattern.findall(string)
return tuple(set(tuple(direction_map[match.lower()[0]] for match in matches)))
@staticmethod
def extract_edges_from_string(string: str) -> dict["Edge", bool]:
anchor_values = WaylandWindow.extract_anchor_values(string.lower())
return {
Edge.TOP: "top" in anchor_values,
Edge.RIGHT: "right" in anchor_values,
Edge.BOTTOM: "bottom" in anchor_values,
Edge.LEFT: "left" in anchor_values,
}
@staticmethod
def extract_margin(input: str | Iterable[int]) -> dict["Edge", int]:
margins = (
extract_css_values(input.lower())
if isinstance(input, str)
else input
if isinstance(input, (tuple, list)) and len(input) == 4
else (0, 0, 0, 0)
)
return {
Edge.TOP: margins[0],
Edge.RIGHT: margins[1],
Edge.BOTTOM: margins[2],
Edge.LEFT: margins[3],
}