desklab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. desklab/__init__.py +9 -0
  2. desklab/_assets/__init__.py +0 -0
  3. desklab/_assets/desklab.png +0 -0
  4. desklab/_check/__init__.py +10 -0
  5. desklab/_check/_check_operations/__init__.py +9 -0
  6. desklab/_check/_check_operations/_check.py +14 -0
  7. desklab/_check/_check_operations/_length.py +33 -0
  8. desklab/_check/_check_operations/_range.py +41 -0
  9. desklab/_check/_value.py +31 -0
  10. desklab/_utils/__init__.py +10 -0
  11. desklab/_utils/_geometry.py +36 -0
  12. desklab/_utils/_imageio_ffmpeg_exe.py +163 -0
  13. desklab/_utils/_regex.py +5 -0
  14. desklab/areas/__init__.py +9 -0
  15. desklab/areas/_area_interface.py +17 -0
  16. desklab/areas/_clickable_area.py +27 -0
  17. desklab/areas/_rectangular_area.py +102 -0
  18. desklab/components/__init__.py +15 -0
  19. desklab/components/_button.py +61 -0
  20. desklab/components/_drag_drop.py +58 -0
  21. desklab/components/_drawing_area.py +205 -0
  22. desklab/components/_text.py +117 -0
  23. desklab/components/_text_input.py +344 -0
  24. desklab/components/_window.py +124 -0
  25. desklab/containers/__init__.py +9 -0
  26. desklab/containers/_constants.py +18 -0
  27. desklab/containers/_flexbox.py +61 -0
  28. desklab/containers/_flexbox_interface.py +205 -0
  29. desklab/containers/_protected_flexbox.py +133 -0
  30. desklab/entity_types/__init__.py +20 -0
  31. desklab/entity_types/_colorable.py +26 -0
  32. desklab/entity_types/_containable.py +12 -0
  33. desklab/entity_types/_copiable.py +19 -0
  34. desklab/entity_types/_dimensionable.py +25 -0
  35. desklab/entity_types/_displayable.py +14 -0
  36. desklab/entity_types/_entity.py +5 -0
  37. desklab/entity_types/_event_sensitive.py +27 -0
  38. desklab/entity_types/_positionable.py +22 -0
  39. desklab/exceptions/__init__.py +12 -0
  40. desklab/exceptions/_exceptions.py +37 -0
  41. desklab/listeners/__init__.py +38 -0
  42. desklab/listeners/_clipboard.py +18 -0
  43. desklab/listeners/_default.py +21 -0
  44. desklab/listeners/_hover.py +36 -0
  45. desklab/listeners/_keyboard.py +71 -0
  46. desklab/listeners/_mouse.py +26 -0
  47. desklab/listeners/_protected_listener.py +89 -0
  48. desklab/listeners/_system_listener.py +46 -0
  49. desklab/media/__init__.py +7 -0
  50. desklab/media/_audio.py +55 -0
  51. desklab/media/_image.py +64 -0
  52. desklab/primitives/__init__.py +7 -0
  53. desklab/primitives/_color.py +142 -0
  54. desklab/primitives/_font.py +86 -0
  55. desklab/system/__init__.py +12 -0
  56. desklab/system/_clipboard.py +248 -0
  57. desklab/system/_keyboard.py +148 -0
  58. desklab/system/_mouse.py +63 -0
  59. desklab/system/_system_input.py +23 -0
  60. desklab-0.1.0.dist-info/METADATA +121 -0
  61. desklab-0.1.0.dist-info/RECORD +63 -0
  62. desklab-0.1.0.dist-info/WHEEL +5 -0
  63. desklab-0.1.0.dist-info/top_level.txt +1 -0
desklab/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .primitives import *
2
+ from .areas import *
3
+ from .components import *
4
+ from .containers import *
5
+ from .entity_types import *
6
+ from .listeners import *
7
+ from .media import *
8
+ from .system import *
9
+ from .exceptions import *
File without changes
Binary file
@@ -0,0 +1,10 @@
1
+ from ._value import value_check
2
+ from ._check_operations import (Check, CheckRange,
3
+ CheckLength)
4
+
5
+ __all__ = [
6
+ 'value_check',
7
+ 'Check',
8
+ 'CheckRange',
9
+ 'CheckLength'
10
+ ]
@@ -0,0 +1,9 @@
1
+ from ._check import Check
2
+ from ._range import CheckRange
3
+ from ._length import CheckLength
4
+
5
+ __all__ = [
6
+ 'Check',
7
+ 'CheckRange',
8
+ 'CheckLength'
9
+ ]
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+ from typing import Any, Callable
3
+
4
+
5
+ class Check:
6
+ def __init__(self, rule: Callable[[Any], bool], description: str | None = None) -> None:
7
+ self.__rule = rule
8
+ self.__description = description
9
+
10
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
11
+ return self.__rule(*args, **kwargs)
12
+
13
+ def __str__(self) -> str:
14
+ return self.__description or self.__rule.__name__
@@ -0,0 +1,33 @@
1
+ from typing import Any
2
+
3
+ from desklab._check._check_operations import Check
4
+ from desklab.exceptions import InvalidParameterValue
5
+
6
+
7
+ class CheckLength(Check):
8
+
9
+ def __init__(self, reference_length: int, comparison: str = "=", variable_name: str | None = None) -> None:
10
+
11
+ operators = {"=", ">", "<", ">=", "<="}
12
+ if comparison not in operators:
13
+ raise InvalidParameterValue(f"comparison", comparison,
14
+ f"'comparison' must be one of: {operators}")
15
+
16
+ def compare(value: Any) -> bool:
17
+ if comparison == "=":
18
+ return len(value) == reference_length
19
+ elif comparison == ">":
20
+ return len(value) > reference_length
21
+ elif comparison == "<":
22
+ return len(value) < reference_length
23
+ elif comparison == ">=":
24
+ return len(value) >= reference_length
25
+ elif comparison == "<=":
26
+ return len(value) <= reference_length
27
+ raise InvalidParameterValue(f"comparison", comparison,
28
+ f"'comparison' must be one of: {operators}")
29
+
30
+ name = variable_name or "Value"
31
+ message = f"length({name}) {comparison} {reference_length}"
32
+
33
+ super().__init__(compare, message)
@@ -0,0 +1,41 @@
1
+ from typing import Any
2
+ from typing import Any, cast
3
+ from desklab.exceptions import MissingParameters
4
+ from desklab._check._check_operations import Check
5
+ from collections.abc import Iterable
6
+
7
+
8
+ class CheckRange(Check):
9
+
10
+ def __init__(self, min_value: Any = None, max_value: Any = None, variable_name: str | None = None) -> None:
11
+
12
+ if min_value is None and max_value is None:
13
+ raise MissingParameters(["min_value", "max_value"],
14
+ "At least one of 'min_value' or 'max_value' must be provided")
15
+
16
+ def is_valid(value: Any) -> bool:
17
+
18
+ if isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
19
+ iterable = cast(Iterable[Any], value)
20
+ return all(is_valid(v) for v in iterable)
21
+
22
+ if min_value is not None and value < min_value:
23
+ return False
24
+
25
+ if max_value is not None and value > max_value:
26
+ return False
27
+
28
+ return True
29
+
30
+ name = variable_name or "Value"
31
+
32
+ if min_value is not None and max_value is not None:
33
+ message = f"{name} must be between {min_value} and {max_value}"
34
+
35
+ elif min_value is not None:
36
+ message = f"{name} must be greater than or equal to {min_value}"
37
+
38
+ else:
39
+ message = f"{name} must be less than or equal to {max_value}"
40
+
41
+ super().__init__(is_valid, message)
@@ -0,0 +1,31 @@
1
+ import inspect
2
+ from typing import Callable, TypeVar, ParamSpec
3
+ from functools import wraps
4
+ from desklab.exceptions import InvalidParameterValue
5
+ from desklab._check._check_operations import Check
6
+
7
+
8
+ P = ParamSpec("P")
9
+ R = TypeVar("R")
10
+
11
+
12
+ def value_check(**validations: Check) -> Callable[[Callable[P, R]], Callable[P, R]]:
13
+ def decorator(function: Callable[P, R]) -> Callable[P, R]:
14
+ signature = inspect.signature(function)
15
+
16
+ @wraps(function)
17
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
18
+ bound_arguments = signature.bind(*args, **kwargs)
19
+ bound_arguments.apply_defaults()
20
+
21
+ for param_name, param_value in bound_arguments.arguments.items():
22
+ if param_name in ("self", "cls"):
23
+ continue
24
+ validation_rule = validations.get(param_name)
25
+ if validation_rule is not None and not validation_rule(param_value):
26
+ raise InvalidParameterValue(param_name, param_value,
27
+ str(validation_rule))
28
+
29
+ return function(*args, **kwargs)
30
+ return wrapper
31
+ return decorator
@@ -0,0 +1,10 @@
1
+ from ._geometry import point_to_segment_distance, is_inside_circle
2
+ from ._regex import camel_case_to_snake_case
3
+ from ._imageio_ffmpeg_exe import get_ffmpeg_exe
4
+
5
+ __all__ = [
6
+ "point_to_segment_distance",
7
+ "is_inside_circle",
8
+ "camel_case_to_snake_case",
9
+ "get_ffmpeg_exe"
10
+ ]
@@ -0,0 +1,36 @@
1
+ import math
2
+
3
+
4
+ def point_to_segment_distance(point: tuple[int, int], segment_start: tuple[int, int], segment_end: tuple[int, int]) -> float:
5
+ point_x, point_y = point
6
+ start_x, start_y = segment_start
7
+ end_x, end_y = segment_end
8
+
9
+ vector_segment_x = end_x - start_x
10
+ vector_segment_y = end_y - start_y
11
+
12
+ vector_point_x = point_x - start_x
13
+ vector_point_y = point_y - start_y
14
+
15
+ segment_length_squared = vector_segment_x ** 2 + vector_segment_y ** 2
16
+
17
+ if segment_length_squared == 0:
18
+ return math.sqrt((point_x - start_x) ** 2 + (point_y - start_y) ** 2)
19
+
20
+ dot_product = vector_point_x * vector_segment_x + \
21
+ vector_point_y * vector_segment_y
22
+ projection_ratio = max(0.0, min(1.0, dot_product / segment_length_squared))
23
+
24
+ closest_x = start_x + vector_segment_x * projection_ratio
25
+ closest_y = start_y + vector_segment_y * projection_ratio
26
+
27
+ distance_x = point_x - closest_x
28
+ distance_y = point_y - closest_y
29
+
30
+ return math.sqrt(distance_x ** 2 + distance_y ** 2)
31
+
32
+
33
+ def is_inside_circle(coordinates: tuple[int, int], circle_center: tuple[int, int], circle_radius: int):
34
+ x, y = coordinates
35
+ cx, cy = circle_center
36
+ return (x - cx)**2 + (y - cy)**2 <= circle_radius**2
@@ -0,0 +1,163 @@
1
+ # ==============================================================================
2
+ # The following code block or function incorporates code from 'imageio'.
3
+ #
4
+ # Copyright (c) 2019-2025, imageio
5
+ # All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright notice, this
11
+ # list of conditions and the following disclaimer.
12
+ #
13
+ # * Redistributions in binary form must reproduce the above copyright notice,
14
+ # this list of conditions and the following disclaimer in the documentation
15
+ # and/or other materials provided with the distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLD
21
+
22
+ import os
23
+ import platform
24
+ import subprocess
25
+ import sys
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from typing import Any, Optional, no_type_check
29
+
30
+ FNAME_PER_PLATFORM = {
31
+ "macos-aarch64": "ffmpeg-macos-aarch64-v7.1",
32
+ "macos-x86_64": "ffmpeg-macos-x86_64-v7.1",
33
+ "windows-x86_64": "ffmpeg-win-x86_64-v7.1.exe",
34
+ "windows-i686": "ffmpeg-win32-v4.2.2.exe",
35
+ "linux-aarch64": "ffmpeg-linux-aarch64-v7.0.2",
36
+ "linux-x86_64": "ffmpeg-linux-x86_64-v7.0.2",
37
+ }
38
+
39
+
40
+ def _get_os_string() -> str:
41
+ if sys.platform.startswith("win"):
42
+ return "windows"
43
+ elif sys.platform.startswith("darwin"):
44
+ return "macos"
45
+ elif sys.platform.startswith("linux"):
46
+ return "linux"
47
+ return sys.platform
48
+
49
+
50
+ def _get_arch() -> str:
51
+ is_64_bit = sys.maxsize > 2**32
52
+ machine = platform.machine()
53
+
54
+ if machine == "armv7l":
55
+ return "armv7"
56
+ elif is_64_bit and machine.startswith(("arm", "aarch64")):
57
+ return "aarch64"
58
+ elif is_64_bit:
59
+ return "x86_64"
60
+ return "i686"
61
+
62
+
63
+ def _get_platform() -> str:
64
+ return _get_os_string() + "-" + _get_arch()
65
+
66
+
67
+ @no_type_check
68
+ def _popen_kwargs() -> dict[str, Any]:
69
+ startupinfo = None
70
+ creationflags = 0
71
+ if sys.platform.startswith("win"):
72
+ startupinfo = subprocess.STARTUPINFO()
73
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
74
+ return {
75
+ "startupinfo": startupinfo,
76
+ "creationflags": creationflags,
77
+ }
78
+
79
+
80
+ def _is_valid_exe(exe: str) -> bool:
81
+ cmd = [exe, "-version"]
82
+ try:
83
+ with open(os.devnull, "w") as null:
84
+ subprocess.check_call(
85
+ cmd, stdout=null, stderr=subprocess.STDOUT, **_popen_kwargs()
86
+ )
87
+ return True
88
+ except (OSError, ValueError, subprocess.CalledProcessError):
89
+ return False
90
+
91
+
92
+ @lru_cache(maxsize=1)
93
+ def _find_ffmpeg_path() -> Optional[Path]:
94
+ plat = _get_platform()
95
+ os_str = _get_os_string()
96
+ expected_filename = FNAME_PER_PLATFORM.get(plat, "ffmpeg")
97
+
98
+ possible_paths: list[Path] = []
99
+ home = Path.home()
100
+
101
+ imageio_cache_dir = home / ".imageio" / "ffmpeg"
102
+ desklab_cache_dir = home / ".desklab" / "ffmpeg"
103
+
104
+ possible_paths.extend([
105
+ imageio_cache_dir / expected_filename,
106
+ desklab_cache_dir / expected_filename,
107
+ ])
108
+
109
+ prefix_path = Path(sys.prefix)
110
+ if os_str == "windows":
111
+ possible_paths.append(prefix_path / "Library" / "bin" / "ffmpeg.exe")
112
+ else:
113
+ possible_paths.append(prefix_path / "bin" / "ffmpeg")
114
+
115
+ if os_str == "windows":
116
+ appdata_local = Path(os.environ.get(
117
+ "LOCALAPPDATA", home / "AppData/Local"))
118
+ possible_paths.extend([
119
+ appdata_local / "Microsoft/WindowsApps/ffmpeg.exe",
120
+ home / "ffmpeg" / "bin" / "ffmpeg.exe",
121
+ Path("C:/Program Files/ffmpeg/bin/ffmpeg.exe"),
122
+ Path("C:/ffmpeg/bin/ffmpeg.exe"),
123
+ ])
124
+ elif os_str == "macos":
125
+ possible_paths.extend([
126
+ Path("/opt/homebrew/bin/ffmpeg"),
127
+ Path("/usr/local/bin/ffmpeg"),
128
+ Path("/usr/bin/ffmpeg"),
129
+ home / "bin" / "ffmpeg",
130
+ ])
131
+ elif os_str == "linux":
132
+ possible_paths.extend([
133
+ Path("/usr/bin/ffmpeg"),
134
+ Path("/usr/local/bin/ffmpeg"),
135
+ Path("/snap/bin/ffmpeg"),
136
+ home / "bin" / "ffmpeg",
137
+ home / ".local" / "bin" / "ffmpeg",
138
+ ])
139
+
140
+ for path in possible_paths:
141
+ if path.is_file() and _is_valid_exe(str(path)):
142
+ return path
143
+
144
+ if _is_valid_exe("ffmpeg"):
145
+ return Path("ffmpeg")
146
+
147
+ return None
148
+
149
+
150
+ def get_ffmpeg_exe() -> Path:
151
+ exe_env = os.getenv("DESKLAB_FFMPEG_EXE") or os.getenv(
152
+ "IMAGEIO_FFMPEG_EXE")
153
+ if exe_env:
154
+ return Path(exe_env)
155
+
156
+ detected_path = _find_ffmpeg_path()
157
+ if detected_path:
158
+ return detected_path
159
+
160
+ raise RuntimeError(
161
+ "No ffmpeg exe could be found. Install ffmpeg on your system, "
162
+ "or set the DESKLAB_FFMPEG_EXE environment variable."
163
+ )
@@ -0,0 +1,5 @@
1
+ import re
2
+
3
+
4
+ def camel_case_to_snake_case(text: str) -> str:
5
+ return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower()
@@ -0,0 +1,9 @@
1
+ from ._area_interface import AreaInterface
2
+ from ._rectangular_area import RectangularArea
3
+ from ._clickable_area import ClickableArea
4
+
5
+ __all__ = [
6
+ "AreaInterface",
7
+ "RectangularArea",
8
+ "ClickableArea"
9
+ ]
@@ -0,0 +1,17 @@
1
+ from desklab.entity_types import ContainableEntity, DisplayableEntity, CopiableEntity, ColorableEntity
2
+ from desklab.primitives import Color
3
+ from abc import abstractmethod
4
+
5
+
6
+ class AreaInterface(ContainableEntity, DisplayableEntity, ColorableEntity, CopiableEntity):
7
+
8
+ def __init__(self, width: int, height: int, color: Color | tuple[int, ...] | str = "BLACK") -> None:
9
+ super().__init__(x=0, y=0, width=width, height=height, color=color)
10
+
11
+ @abstractmethod
12
+ def contains(self, coordinates: tuple[int, int]) -> bool:
13
+ pass
14
+
15
+ def get_rect(self) -> tuple[int, int, int, int]:
16
+ return (self.get_x(), self.get_y(),
17
+ self.get_width(), self.get_height())
@@ -0,0 +1,27 @@
1
+ from typing import Any
2
+ from ._rectangular_area import RectangularArea
3
+ from desklab.entity_types import EventSensitiveEntity
4
+ from desklab.primitives import Color
5
+ from desklab.system import Mouse
6
+
7
+
8
+ class ClickableArea(RectangularArea, EventSensitiveEntity):
9
+
10
+ def __init__(self, width: int, height: int, color: Color | tuple[int, ...] | str = "BLACK", corners_radius: tuple[int, int, int, int] | int = 0) -> None:
11
+ super().__init__(width, height, color, corners_radius)
12
+ self.__is_clicked = False
13
+ self.__is_held = False
14
+
15
+ def is_clicked(self) -> bool:
16
+ return self.__is_clicked
17
+
18
+ def is_held(self) -> bool:
19
+ return self.__is_held
20
+
21
+ def handle_event(self, *args: Any, **kwargs: Any) -> None:
22
+ super().handle_event(*args, **kwargs)
23
+ mouse = self._get_from_kwargs(Mouse, kwargs)
24
+ inside = self.contains(mouse.get_position())
25
+
26
+ self.__is_clicked = mouse.is_clicked() and inside
27
+ self.__is_held = mouse.is_held() and inside
@@ -0,0 +1,102 @@
1
+ # fmt: off
2
+ from desklab._check import value_check, CheckRange, CheckLength
3
+ from desklab._utils import is_inside_circle
4
+ from ._area_interface import AreaInterface
5
+ from desklab.primitives import Color
6
+ from typing import Any
7
+ import os
8
+ os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
9
+ import pygame
10
+ from pygame import Surface
11
+ # fmt: on
12
+
13
+
14
+ class RectangularArea(AreaInterface):
15
+
16
+ def __init__(self,
17
+ width: int,
18
+ height: int,
19
+ color: Color | tuple[int, ...] | str = "BLACK",
20
+ corners_radius: tuple[int, int, int, int] | int = 0) -> None:
21
+ super().__init__(width, height, color)
22
+ self.set_corners_radius(corners_radius)
23
+
24
+ @value_check(corners_radius=CheckRange(min_value=0, variable_name="corners_radius"))
25
+ def __validate_corners_range(self, corners_radius: tuple[int, ...]) -> None:
26
+ pass
27
+
28
+ @value_check(corners_radius=CheckLength(reference_length=4, variable_name="corners_radius"))
29
+ def __validate_corners_length(self, corners_radius: tuple[int, ...]) -> None:
30
+ pass
31
+
32
+ def set_corners_radius(self, corners_radius: tuple[int, int, int, int] | int) -> None:
33
+ if isinstance(corners_radius, int):
34
+ corners = (corners_radius, ) * 4
35
+ else:
36
+ corners = corners_radius
37
+
38
+ self.__validate_corners_length(corners)
39
+ self.__validate_corners_range(corners)
40
+ self.__set_corners_radius(corners)
41
+
42
+ def __set_corners_radius(self, corners: tuple[int, ...]) -> None:
43
+ self.__corners_radius = corners
44
+
45
+ def get_corners_radius(self) -> tuple[int, int, int, int]:
46
+ assert len(self.__corners_radius) == 4
47
+ return self.__corners_radius
48
+
49
+ def contains(self, coordinates: tuple[int, int]) -> bool:
50
+ x, y = coordinates
51
+ self_x, self_y, self_w, self_h = self.get_rect()
52
+ if not (self_x <= x <= self_x + self_w and
53
+ self_y <= y <= self_y + self_h):
54
+ return False
55
+ return not self.__exceeds_corners(coordinates)
56
+
57
+ def __exceeds_corners(self, coordinates: tuple[int, int]) -> bool:
58
+ rect_x, rect_y, rect_width, rect_height = self.get_rect()
59
+ radius_top_left, radius_top_right, radius_bottom_left, radius_bottom_right = self.get_corners_radius()
60
+
61
+ corners_geometry_map = [
62
+ (radius_top_left, rect_x + radius_top_left,
63
+ rect_y + radius_top_left, True, True), # Top-Left
64
+ (radius_top_right, rect_x + rect_width - radius_top_right,
65
+ rect_y + radius_top_right, False, True), # Top-Right
66
+ (radius_bottom_left, rect_x + radius_bottom_left, rect_y +
67
+ rect_height - radius_bottom_left, True, False), # Bottom-Left
68
+ (radius_bottom_right, rect_x + rect_width - radius_bottom_right,
69
+ rect_y + rect_height - radius_bottom_right, False, False) # Bottom-Right
70
+ ]
71
+
72
+ for corner_radius, arc_center_x, arc_center_y, check_left, check_top in corners_geometry_map:
73
+
74
+ if self.__is_point_inside_corner_bounding_box(coordinates, arc_center_x, arc_center_y, check_left, check_top):
75
+ if not is_inside_circle(coordinates, (arc_center_x, arc_center_y), corner_radius):
76
+ return True
77
+
78
+ return False
79
+
80
+ def __is_point_inside_corner_bounding_box(self, coordinates: tuple[int, int], arc_center_x: int, arc_center_y: int, check_left: bool, check_top: bool) -> bool:
81
+ point_x, point_y = coordinates
82
+ is_within_horizontal_boundary = point_x < arc_center_x if check_left else point_x > arc_center_x
83
+ is_within_vertical_boundary = point_y < arc_center_y if check_top else point_y > arc_center_y
84
+ return is_within_horizontal_boundary and is_within_vertical_boundary
85
+
86
+ def display(self, screen: Surface) -> None:
87
+ corners = self.get_corners_radius()
88
+ pygame.draw.rect(screen,
89
+ self.get_color_tuple(),
90
+ self.get_rect(),
91
+ border_top_left_radius=corners[0],
92
+ border_top_right_radius=corners[1],
93
+ border_bottom_left_radius=corners[2],
94
+ border_bottom_right_radius=corners[3])
95
+
96
+ def _get_copy_replacement_map(self) -> dict[str, Any]:
97
+ return {
98
+ "width": self.get_width(),
99
+ "height": self.get_height(),
100
+ "color": self.get_color(),
101
+ "corners_radius": self.get_corners_radius()
102
+ }
@@ -0,0 +1,15 @@
1
+ from ._text import Text
2
+ from ._button import Button
3
+ from ._drag_drop import DragDrop
4
+ from ._drawing_area import DrawingArea
5
+ from ._text_input import TextInput
6
+ from ._window import Window
7
+
8
+ __all__ = [
9
+ "Text",
10
+ "Button",
11
+ "DragDrop",
12
+ "DrawingArea",
13
+ "TextInput",
14
+ "Window"
15
+ ]
@@ -0,0 +1,61 @@
1
+ from typing import Any, Callable
2
+ from desklab.containers import (FlexDirection, HorizontalAlignment,
3
+ VerticalAlignment, FlexBox)
4
+ from desklab.areas import ClickableArea
5
+ from desklab.primitives import Color
6
+
7
+
8
+ class Button(FlexBox, ClickableArea):
9
+
10
+ def __init__(self,
11
+ width: int,
12
+ height: int,
13
+ actions: Callable[..., Any] | list[Callable[..., Any]] = [],
14
+ padding: int = 0,
15
+ space_between: int = 0,
16
+ flex_direction: str | FlexDirection = FlexDirection.COLUMN,
17
+ horizontal_alignment: str | HorizontalAlignment = HorizontalAlignment.CENTER,
18
+ vertical_alignment: str | VerticalAlignment = VerticalAlignment.CENTER,
19
+ corners_radius: tuple[int, int, int, int] | int = 0,
20
+ color: Color | tuple[int, ...] | str = "BLACK",
21
+ bounded: bool = True) -> None:
22
+
23
+ super().__init__(width, height, padding,
24
+ space_between, flex_direction,
25
+ horizontal_alignment, vertical_alignment,
26
+ corners_radius, color, bounded)
27
+ self.__actions: list[Callable[..., Any]] = []
28
+ self.add_actions(actions)
29
+
30
+ def __add_click_listener(self):
31
+ if self.is_clicked():
32
+ for action in self.get_actions():
33
+ action()
34
+
35
+ def set_actions(self, actions: Callable[..., Any] | list[Callable[..., Any]]):
36
+ if not isinstance(actions, list):
37
+ actions = [actions]
38
+ self.__actions = actions
39
+
40
+ def add_actions(self, actions: Callable[..., Any] | list[Callable[..., Any]]):
41
+ if isinstance(actions, Callable):
42
+ actions = [actions]
43
+ self.__actions.extend(actions)
44
+
45
+ def get_actions(self) -> list[Callable[..., Any]]:
46
+ return self.__actions.copy()
47
+
48
+ def remove_actions(self, actions: Callable[..., Any] | list[Callable[..., Any]]) -> None:
49
+ if isinstance(actions, Callable):
50
+ actions = [actions]
51
+ for a in actions:
52
+ self.__actions.remove(a)
53
+
54
+ def handle_event(self, *args: Any, **kwargs: Any) -> None:
55
+ super().handle_event(*args, **kwargs)
56
+ self.__add_click_listener()
57
+
58
+ def _get_copy_replacement_map(self) -> dict[str, Any]:
59
+ replace = super()._get_copy_replacement_map()
60
+ replace["actions"] = self.get_actions()
61
+ return replace
@@ -0,0 +1,58 @@
1
+ # fmt: off
2
+ from desklab.primitives import Color
3
+ from typing import Any, Optional
4
+ from desklab.system import Mouse
5
+ from desklab.containers import FlexBox, FlexDirection, HorizontalAlignment, VerticalAlignment
6
+ import os
7
+ os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
8
+ from pygame.event import Event
9
+ # fmt: on
10
+
11
+
12
+ class DragDrop(FlexBox):
13
+
14
+ def __init__(self,
15
+ width: int,
16
+ height: int,
17
+ padding: int = 0,
18
+ space_between: int = 0,
19
+ flex_direction: str | FlexDirection = FlexDirection.COLUMN,
20
+ horizontal_alignment: str | HorizontalAlignment = HorizontalAlignment.CENTER,
21
+ vertical_alignment: str | VerticalAlignment = VerticalAlignment.CENTER,
22
+ corners_radius: tuple[int, int, int, int] | int = 0,
23
+ color: Color | tuple[int, ...] | str = "BLACK",
24
+ bounded: bool = True) -> None:
25
+
26
+ self.__files: list[str] = []
27
+ super().__init__(width, height, padding, space_between, flex_direction,
28
+ horizontal_alignment, vertical_alignment, corners_radius,
29
+ color, bounded)
30
+
31
+ def handle_event(self, *args: Any, **kwargs: Any) -> None:
32
+ super().handle_event(*args, **kwargs)
33
+ mouse = self._get_from_kwargs(Mouse, kwargs)
34
+ event = self._get_from_kwargs(Event, kwargs, _raise=False)
35
+ if event:
36
+ self.__add_file_drop_listener(event, mouse)
37
+
38
+ def __add_file_drop_listener(self, event: Event, mouse: Mouse) -> None:
39
+ if mouse.is_dropping_file() and self.contains(mouse.get_position()):
40
+ file_path = event.file
41
+ self.__files.append(file_path)
42
+
43
+ def get_files(self) -> list[str]:
44
+ return self.__files.copy()
45
+
46
+ def pop_file(self) -> Optional[str]:
47
+ if not self.__files:
48
+ return None
49
+ return self.__files.pop()
50
+
51
+ def clear_files(self) -> None:
52
+ self.__files.clear()
53
+
54
+ def has_files(self) -> bool:
55
+ return len(self.__files) > 0
56
+
57
+ def count_files(self) -> int:
58
+ return len(self.__files)