py10x-universe 0.1.3__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.
- core_10x/__init__.py +42 -0
- core_10x/backbone/__init__.py +0 -0
- core_10x/backbone/backbone_store.py +59 -0
- core_10x/backbone/backbone_traitable.py +30 -0
- core_10x/backbone/backbone_user.py +66 -0
- core_10x/backbone/bound_data_domain.py +49 -0
- core_10x/backbone/namespace.py +101 -0
- core_10x/backbone/vault.py +38 -0
- core_10x/code_samples/__init__.py +0 -0
- core_10x/code_samples/_package_manifest.py +3 -0
- core_10x/code_samples/directories.py +181 -0
- core_10x/code_samples/person.py +76 -0
- core_10x/concrete_traits.py +356 -0
- core_10x/conftest.py +12 -0
- core_10x/curve.py +321 -0
- core_10x/data_domain.py +48 -0
- core_10x/data_domain_binder.py +45 -0
- core_10x/directory.py +250 -0
- core_10x/entity.py +8 -0
- core_10x/entity_filter.py +5 -0
- core_10x/environment_variables.py +147 -0
- core_10x/exec_control.py +84 -0
- core_10x/experimental/__init__.py +0 -0
- core_10x/experimental/data_protocol_ex.py +34 -0
- core_10x/global_cache.py +121 -0
- core_10x/manual_tests/__init__.py +0 -0
- core_10x/manual_tests/calendar_test.py +35 -0
- core_10x/manual_tests/ctor_update_bug.py +58 -0
- core_10x/manual_tests/debug_graph_on.py +17 -0
- core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
- core_10x/manual_tests/enum_bits_test.py +17 -0
- core_10x/manual_tests/env_vars_trivial_test.py +12 -0
- core_10x/manual_tests/existing_traitable.py +33 -0
- core_10x/manual_tests/k10x_test1.py +13 -0
- core_10x/manual_tests/named_constant_test.py +121 -0
- core_10x/manual_tests/nucleus_trivial_test.py +42 -0
- core_10x/manual_tests/polars_test.py +14 -0
- core_10x/manual_tests/py_class_test.py +4 -0
- core_10x/manual_tests/rc_test.py +42 -0
- core_10x/manual_tests/rdate_test.py +12 -0
- core_10x/manual_tests/reference_serialization_bug.py +19 -0
- core_10x/manual_tests/resource_trivial_test.py +10 -0
- core_10x/manual_tests/store_uri_test.py +6 -0
- core_10x/manual_tests/trait_definition_test.py +19 -0
- core_10x/manual_tests/trait_filter_test.py +15 -0
- core_10x/manual_tests/trait_flag_modification_test.py +42 -0
- core_10x/manual_tests/trait_modification_bug.py +26 -0
- core_10x/manual_tests/traitable_as_of_test.py +82 -0
- core_10x/manual_tests/traitable_heir_test.py +39 -0
- core_10x/manual_tests/traitable_history_test.py +41 -0
- core_10x/manual_tests/traitable_serialization_test.py +54 -0
- core_10x/manual_tests/traitable_trivial_test.py +71 -0
- core_10x/manual_tests/trivial_graph_test.py +16 -0
- core_10x/manual_tests/ts_class_association_test.py +64 -0
- core_10x/manual_tests/ts_trivial_test.py +35 -0
- core_10x/named_constant.py +425 -0
- core_10x/nucleus.py +81 -0
- core_10x/package_manifest.py +85 -0
- core_10x/package_refactoring.py +153 -0
- core_10x/py_class.py +431 -0
- core_10x/rc.py +155 -0
- core_10x/rdate.py +339 -0
- core_10x/resource.py +189 -0
- core_10x/roman_number.py +67 -0
- core_10x/testlib/__init__.py +0 -0
- core_10x/testlib/test_store.py +240 -0
- core_10x/testlib/traitable_history_tests.py +787 -0
- core_10x/testlib/ts_tests.py +280 -0
- core_10x/trait.py +377 -0
- core_10x/trait_definition.py +176 -0
- core_10x/trait_filter.py +205 -0
- core_10x/trait_method_error.py +36 -0
- core_10x/traitable.py +1082 -0
- core_10x/traitable_cli.py +153 -0
- core_10x/traitable_heir.py +33 -0
- core_10x/traitable_id.py +31 -0
- core_10x/ts_store.py +172 -0
- core_10x/ts_store_type.py +26 -0
- core_10x/ts_union.py +147 -0
- core_10x/ui_hint.py +153 -0
- core_10x/unit_tests/test_concrete_traits.py +156 -0
- core_10x/unit_tests/test_converters.py +51 -0
- core_10x/unit_tests/test_curve.py +157 -0
- core_10x/unit_tests/test_directory.py +54 -0
- core_10x/unit_tests/test_documentation.py +172 -0
- core_10x/unit_tests/test_environment_variables.py +15 -0
- core_10x/unit_tests/test_filters.py +239 -0
- core_10x/unit_tests/test_graph.py +348 -0
- core_10x/unit_tests/test_named_constant.py +98 -0
- core_10x/unit_tests/test_rc.py +11 -0
- core_10x/unit_tests/test_rdate.py +484 -0
- core_10x/unit_tests/test_trait_method_error.py +80 -0
- core_10x/unit_tests/test_trait_modification.py +19 -0
- core_10x/unit_tests/test_traitable.py +959 -0
- core_10x/unit_tests/test_traitable_history.py +1 -0
- core_10x/unit_tests/test_ts_store.py +1 -0
- core_10x/unit_tests/test_ts_union.py +369 -0
- core_10x/unit_tests/test_ui_nodes.py +81 -0
- core_10x/unit_tests/test_xxcalendar.py +471 -0
- core_10x/vault/__init__.py +0 -0
- core_10x/vault/sec_keys.py +133 -0
- core_10x/vault/security_keys_old.py +168 -0
- core_10x/vault/vault.py +56 -0
- core_10x/vault/vault_traitable.py +56 -0
- core_10x/vault/vault_user.py +70 -0
- core_10x/xdate_time.py +136 -0
- core_10x/xnone.py +71 -0
- core_10x/xxcalendar.py +228 -0
- infra_10x/__init__.py +0 -0
- infra_10x/manual_tests/__init__.py +0 -0
- infra_10x/manual_tests/test_misc.py +16 -0
- infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
- infra_10x/mongodb_admin.py +111 -0
- infra_10x/mongodb_store.py +346 -0
- infra_10x/mongodb_utils.py +129 -0
- infra_10x/unit_tests/conftest.py +13 -0
- infra_10x/unit_tests/test_mongo_db.py +36 -0
- infra_10x/unit_tests/test_mongo_history.py +1 -0
- py10x_universe-0.1.3.dist-info/METADATA +406 -0
- py10x_universe-0.1.3.dist-info/RECORD +214 -0
- py10x_universe-0.1.3.dist-info/WHEEL +4 -0
- py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
- ui_10x/__init__.py +0 -0
- ui_10x/apps/__init__.py +0 -0
- ui_10x/apps/collection_editor_app.py +100 -0
- ui_10x/choice.py +212 -0
- ui_10x/collection_editor.py +135 -0
- ui_10x/concrete_trait_widgets.py +220 -0
- ui_10x/conftest.py +8 -0
- ui_10x/entity_stocker.py +173 -0
- ui_10x/examples/__init__.py +0 -0
- ui_10x/examples/_guess_word_data.py +14076 -0
- ui_10x/examples/collection_editor.py +17 -0
- ui_10x/examples/date_selector.py +14 -0
- ui_10x/examples/entity_stocker.py +18 -0
- ui_10x/examples/guess_word.py +392 -0
- ui_10x/examples/message_box.py +20 -0
- ui_10x/examples/multi_choice.py +17 -0
- ui_10x/examples/py_data_browser.py +66 -0
- ui_10x/examples/radiobox.py +29 -0
- ui_10x/examples/single_choice.py +31 -0
- ui_10x/examples/style_sheet.py +47 -0
- ui_10x/examples/trivial_entity_editor.py +18 -0
- ui_10x/platform.py +20 -0
- ui_10x/platform_interface.py +517 -0
- ui_10x/py_data_browser.py +249 -0
- ui_10x/qt6/__init__.py +0 -0
- ui_10x/qt6/conftest.py +8 -0
- ui_10x/qt6/manual_tests/__init__.py +0 -0
- ui_10x/qt6/manual_tests/basic_test.py +35 -0
- ui_10x/qt6/platform_implementation.py +275 -0
- ui_10x/qt6/utils.py +665 -0
- ui_10x/rio/__init__.py +0 -0
- ui_10x/rio/apps/examples/examples/__init__.py +22 -0
- ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
- ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
- ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
- ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
- ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
- ui_10x/rio/apps/examples/rio.toml +14 -0
- ui_10x/rio/component_builder.py +497 -0
- ui_10x/rio/components/__init__.py +9 -0
- ui_10x/rio/components/group_box.py +31 -0
- ui_10x/rio/components/labeled_checkbox.py +18 -0
- ui_10x/rio/components/line_edit.py +37 -0
- ui_10x/rio/components/radio_button.py +32 -0
- ui_10x/rio/components/separator.py +24 -0
- ui_10x/rio/components/splitter.py +121 -0
- ui_10x/rio/components/tree_view.py +75 -0
- ui_10x/rio/conftest.py +35 -0
- ui_10x/rio/internals/__init__.py +0 -0
- ui_10x/rio/internals/app.py +192 -0
- ui_10x/rio/manual_tests/__init__.py +0 -0
- ui_10x/rio/manual_tests/basic_test.py +24 -0
- ui_10x/rio/manual_tests/splitter.py +27 -0
- ui_10x/rio/platform_implementation.py +91 -0
- ui_10x/rio/style_sheet.py +53 -0
- ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
- ui_10x/rio/unit_tests/test_internals.py +630 -0
- ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
- ui_10x/rio/widgets/__init__.py +46 -0
- ui_10x/rio/widgets/application.py +109 -0
- ui_10x/rio/widgets/button.py +48 -0
- ui_10x/rio/widgets/button_group.py +60 -0
- ui_10x/rio/widgets/calendar.py +23 -0
- ui_10x/rio/widgets/checkbox.py +24 -0
- ui_10x/rio/widgets/dialog.py +137 -0
- ui_10x/rio/widgets/group_box.py +27 -0
- ui_10x/rio/widgets/layout.py +34 -0
- ui_10x/rio/widgets/line_edit.py +37 -0
- ui_10x/rio/widgets/list.py +105 -0
- ui_10x/rio/widgets/message_box.py +70 -0
- ui_10x/rio/widgets/scroll_area.py +31 -0
- ui_10x/rio/widgets/spacer.py +6 -0
- ui_10x/rio/widgets/splitter.py +45 -0
- ui_10x/rio/widgets/text_edit.py +28 -0
- ui_10x/rio/widgets/tree.py +89 -0
- ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
- ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
- ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
- ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
- ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
- ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
- ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
- ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
- ui_10x/table_header_view.py +305 -0
- ui_10x/table_view.py +174 -0
- ui_10x/trait_editor.py +189 -0
- ui_10x/trait_widget.py +131 -0
- ui_10x/traitable_editor.py +200 -0
- ui_10x/traitable_view.py +131 -0
- ui_10x/unit_tests/conftest.py +8 -0
- ui_10x/unit_tests/test_platform.py +9 -0
- ui_10x/utils.py +661 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import rio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LineEditComponent(rio.Component):
|
|
7
|
+
text: str | None = None
|
|
8
|
+
tooltip: str | None = None
|
|
9
|
+
is_sensitive: bool = True
|
|
10
|
+
on_change: rio.EventHandler[[str]] = None
|
|
11
|
+
on_lose_focus: rio.EventHandler[[str]] = None
|
|
12
|
+
text_style: rio.TextStyle | None = None
|
|
13
|
+
is_secret: bool = False
|
|
14
|
+
on_pointer_up: rio.EventHandler[[rio.PointerEvent]] = None
|
|
15
|
+
|
|
16
|
+
def build(self):
|
|
17
|
+
component = rio.TextInput(
|
|
18
|
+
self.bind().text,
|
|
19
|
+
is_sensitive=self.is_sensitive,
|
|
20
|
+
on_change=self.on_change,
|
|
21
|
+
on_lose_focus=self.on_lose_focus,
|
|
22
|
+
text_style=self.text_style,
|
|
23
|
+
is_secret=self.is_secret,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if self.on_pointer_up is not None:
|
|
27
|
+
component = rio.PointerEventListener(
|
|
28
|
+
component,
|
|
29
|
+
on_pointer_up=self.on_pointer_up,
|
|
30
|
+
consume_events=False,
|
|
31
|
+
event_order='before-child',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if self.tooltip is not None:
|
|
35
|
+
component = rio.Tooltip(anchor=component, tip=self.tooltip)
|
|
36
|
+
|
|
37
|
+
return component
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
import rio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RadioButton(rio.Component):
|
|
9
|
+
label: str = ''
|
|
10
|
+
value: str = ''
|
|
11
|
+
checked: bool = False
|
|
12
|
+
on_select: rio.EventHandler[[]] = None
|
|
13
|
+
|
|
14
|
+
async def on_press(self, button) -> None:
|
|
15
|
+
if self.on_select:
|
|
16
|
+
self.on_select()
|
|
17
|
+
else:
|
|
18
|
+
self.checked = not self.checked
|
|
19
|
+
button.icon = self.icon_name()
|
|
20
|
+
|
|
21
|
+
def icon_name(self) -> str:
|
|
22
|
+
return f'radio_button_{"checked" if self.checked else "unchecked"}'
|
|
23
|
+
|
|
24
|
+
def build(self) -> rio.Component:
|
|
25
|
+
# Use an icon to visually represent the radio button state
|
|
26
|
+
icon_button = rio.IconButton(self.icon_name())
|
|
27
|
+
icon_button.on_press = partial(self.on_press, icon_button)
|
|
28
|
+
return rio.Row(
|
|
29
|
+
icon_button,
|
|
30
|
+
rio.Text(self.label),
|
|
31
|
+
spacing=0.5, # Space between icon and label
|
|
32
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import rio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Separator(rio.Component):
|
|
7
|
+
"""
|
|
8
|
+
A component that draws a horizontal or vertical line to separate UI sections.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
orientation: "horizontal" or "vertical" to define the separator's direction.
|
|
12
|
+
thickness: The width of the line in Rio units (default: 0.1).
|
|
13
|
+
color: The color of the line (default: gray).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
orientation: typing.Literal['horizontal', 'vertical'] = 'horizontal'
|
|
17
|
+
thickness: float = 0.1
|
|
18
|
+
color: rio.Color = rio.Color.GRAY
|
|
19
|
+
|
|
20
|
+
def build(self) -> rio.Component:
|
|
21
|
+
if self.orientation == 'horizontal':
|
|
22
|
+
return rio.Rectangle(min_height=self.thickness, fill=self.color, margin_y=0.5, grow_y=False, align_y=0.5)
|
|
23
|
+
else:
|
|
24
|
+
return rio.Rectangle(min_width=self.thickness, fill=self.color, margin_x=0.5, grow_x=False, align_x=0.5)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import rio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Splitter(rio.Component):
|
|
9
|
+
"""
|
|
10
|
+
A custom Rio component that arranges children horizontally like a Row,
|
|
11
|
+
or vertically as a column with draggable splitters between them for resizing.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Props
|
|
15
|
+
children: list[rio.Component] = []
|
|
16
|
+
direction: t.Literal['horizontal', 'vertical'] = 'vertical'
|
|
17
|
+
handle_size: float = 0.25 # Width of the splitter handle
|
|
18
|
+
min_size_percent: float = 10.0 # Minimum width for each child (%)
|
|
19
|
+
child_proportions: t.Literal['homogeneous'] | t.Sequence[float] = 'homogeneous'
|
|
20
|
+
_component_width: float = 0.0
|
|
21
|
+
_component_height: float = 0.0
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
if not isinstance(self.child_proportions, (list, tuple)):
|
|
25
|
+
num_children = len(self.children)
|
|
26
|
+
self.child_proportions = [1.0] * num_children if num_children else []
|
|
27
|
+
else:
|
|
28
|
+
assert len(self.child_proportions) == len(self.children)
|
|
29
|
+
assert all(p >= 0 for p in self.child_proportions), 'Proportions must be non-negative'
|
|
30
|
+
self.child_proportions = list(self.child_proportions)
|
|
31
|
+
|
|
32
|
+
def on_drag(self, index: int, event: rio.PointerMoveEvent) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Handle drag events on the splitter at the given index.
|
|
35
|
+
Adjusts the proportions of the two adjacent children.
|
|
36
|
+
"""
|
|
37
|
+
horizontal = self.direction == 'horizontal'
|
|
38
|
+
total_size = self._component_width if horizontal else self._component_height
|
|
39
|
+
|
|
40
|
+
# Convert drag movement to a proportion change
|
|
41
|
+
total_proportion = sum(self.child_proportions)
|
|
42
|
+
relative_size = event.relative_x if horizontal else event.relative_y
|
|
43
|
+
delta_proportion = (relative_size / total_size) * total_proportion
|
|
44
|
+
|
|
45
|
+
# Adjust the proportions of the left and right children
|
|
46
|
+
prev_index = index
|
|
47
|
+
next_index = index + 1
|
|
48
|
+
|
|
49
|
+
# Calculate current sizes as percentages to check minimum constraints
|
|
50
|
+
current_sizes = [(p / total_proportion * 100.0) for p in self.child_proportions]
|
|
51
|
+
new_left_size = current_sizes[prev_index] + (delta_proportion / total_proportion * 100.0)
|
|
52
|
+
new_right_size = current_sizes[next_index] - (delta_proportion / total_proportion * 100.0)
|
|
53
|
+
|
|
54
|
+
# Check minimum size constraints
|
|
55
|
+
if new_left_size >= self.min_size_percent and new_right_size >= self.min_size_percent:
|
|
56
|
+
self.child_proportions[prev_index] += delta_proportion
|
|
57
|
+
self.child_proportions[next_index] -= delta_proportion
|
|
58
|
+
# Ensure proportions don't go negative
|
|
59
|
+
self.child_proportions[prev_index] = max(0.0, self.child_proportions[prev_index])
|
|
60
|
+
self.child_proportions[next_index] = max(0.0, self.child_proportions[next_index])
|
|
61
|
+
|
|
62
|
+
self.child_proportions = self.child_proportions # force refresh
|
|
63
|
+
|
|
64
|
+
def build(self) -> rio.Component:
|
|
65
|
+
# If no children, return an empty component
|
|
66
|
+
if not self.children:
|
|
67
|
+
return rio.Rectangle()
|
|
68
|
+
|
|
69
|
+
# Build the layout with children and splitters
|
|
70
|
+
components = []
|
|
71
|
+
horizontal = self.direction == 'horizontal'
|
|
72
|
+
for i, child in enumerate(self.children):
|
|
73
|
+
# Wrap child in ScrollArea
|
|
74
|
+
scrollable_content = rio.ScrollContainer(
|
|
75
|
+
content=child,
|
|
76
|
+
# scroll_x='never' if horizontal else 'auto',
|
|
77
|
+
# scroll_y='auto' if horizontal else 'never',
|
|
78
|
+
)
|
|
79
|
+
# Create the pane
|
|
80
|
+
pane = rio.Rectangle(
|
|
81
|
+
content=scrollable_content,
|
|
82
|
+
**{'grow_x' if horizontal else 'grow_y': True}, # Stretch to fill proportional space
|
|
83
|
+
margin=1, # Spacing around the child content
|
|
84
|
+
)
|
|
85
|
+
# Add a splitter handle to the right of all but the last pane
|
|
86
|
+
if i < len(self.children) - 1:
|
|
87
|
+
splitter = rio.PointerEventListener(
|
|
88
|
+
content=rio.Rectangle(
|
|
89
|
+
**{'grow_x' if horizontal else 'grow_y': False, 'min_width' if horizontal else 'min_height': self.handle_size},
|
|
90
|
+
fill=rio.Color.from_hex('#808080'),
|
|
91
|
+
cursor='move', # Valid CursorStyle for dragging
|
|
92
|
+
),
|
|
93
|
+
on_drag_move=lambda event, idx=i: self.on_drag(idx, event),
|
|
94
|
+
**{
|
|
95
|
+
'align_x' if horizontal else 'align_y': 1.0, # Position at the right edge
|
|
96
|
+
'margin_right' if horizontal else 'margin_bottom': -self.handle_size / 2, # Extend slightly into the next pane
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
# Combine pane and splitter in a Stack
|
|
100
|
+
components.append(
|
|
101
|
+
rio.Stack(
|
|
102
|
+
pane,
|
|
103
|
+
splitter,
|
|
104
|
+
**{'grow_x' if horizontal else 'grow_y': True}, # Ensure the Stack follows the proportion
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
# Last pane has no splitter
|
|
109
|
+
components.append(pane)
|
|
110
|
+
|
|
111
|
+
container = rio.Row if self.direction == 'horizontal' else rio.Column
|
|
112
|
+
return container(
|
|
113
|
+
*components,
|
|
114
|
+
spacing=0,
|
|
115
|
+
proportions=self.bind().child_proportions, # Dynamically control pane sizes
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@rio.event.on_resize
|
|
119
|
+
def _on_resize(self, event: rio.event.ComponentResizeEvent) -> None:
|
|
120
|
+
self._component_width = event.width
|
|
121
|
+
self._component_height = event.height
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import rio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RioTreeItem(rio.Component):
|
|
9
|
+
"""same as SimpleTreeItem, but includes tooltip and supports double-click"""
|
|
10
|
+
|
|
11
|
+
text: str = ''
|
|
12
|
+
on_double_press: rio.EventHandler[[]] = None
|
|
13
|
+
on_press: rio.EventHandler[[]] = None
|
|
14
|
+
on_change: rio.EventHandler[[]] = None
|
|
15
|
+
tooltip: str | None = None
|
|
16
|
+
editable: bool = False
|
|
17
|
+
editing: bool = False
|
|
18
|
+
children: list[RioTreeItem] = []
|
|
19
|
+
is_expanded: bool = False
|
|
20
|
+
|
|
21
|
+
def build_primary_text(self):
|
|
22
|
+
if not self.editing:
|
|
23
|
+
return rio.Text(self.text, justify='left', selectable=False)
|
|
24
|
+
return rio.TextInput(
|
|
25
|
+
self.text,
|
|
26
|
+
align_x=0, # justify left?
|
|
27
|
+
on_confirm=self.handle_edit_confirm,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def build_content(self):
|
|
31
|
+
content = self.build_primary_text()
|
|
32
|
+
if self.tooltip:
|
|
33
|
+
content = rio.Row(content, rio.Tooltip(anchor=content, tip=self.tooltip))
|
|
34
|
+
if self.on_double_press:
|
|
35
|
+
content = rio.PointerEventListener(content, on_double_press=self.handle_double_press)
|
|
36
|
+
return content
|
|
37
|
+
|
|
38
|
+
def handle_double_press(self, ev: rio.PointerEvent):
|
|
39
|
+
if self.editable:
|
|
40
|
+
self.editing = True
|
|
41
|
+
if self.on_double_press:
|
|
42
|
+
self.on_double_press()
|
|
43
|
+
|
|
44
|
+
def handle_edit_confirm(self, text):
|
|
45
|
+
assert self.editing
|
|
46
|
+
self.text = text
|
|
47
|
+
if self.on_change:
|
|
48
|
+
self.on_change()
|
|
49
|
+
|
|
50
|
+
def build(self):
|
|
51
|
+
return rio.SimpleTreeItem(
|
|
52
|
+
content=self.build_content(),
|
|
53
|
+
children=[child.build() for child in self.children],
|
|
54
|
+
is_expanded=self.is_expanded,
|
|
55
|
+
on_press=self.on_press,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RioTreeView(rio.Component):
|
|
60
|
+
"""makes item-level callbacks available on the tree level"""
|
|
61
|
+
|
|
62
|
+
children: list[rio.Component] = None
|
|
63
|
+
selection_mode: Literal['none', 'single', 'multiple'] = ('none',)
|
|
64
|
+
col_count: int = 1
|
|
65
|
+
header_labels: list[str] | None = None
|
|
66
|
+
|
|
67
|
+
def __post__init__(self):
|
|
68
|
+
if self.header_labels is None:
|
|
69
|
+
self.header_labels = ['header']
|
|
70
|
+
|
|
71
|
+
def build(self):
|
|
72
|
+
return rio.TreeView(
|
|
73
|
+
*[item.build() for item in self.children], # TODO: should not need to build each item
|
|
74
|
+
selection_mode=self.selection_mode,
|
|
75
|
+
)
|
ui_10x/rio/conftest.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import rio.testing.browser_client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def running_with_coverage(config):
|
|
8
|
+
if not config.pluginmanager.getplugin('pytest_cov'):
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
if not config.getoption('--cov', default='COV_CORE_SOURCE' in os.environ):
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
return not config.getoption('--no-cov', default=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(scope='session', autouse=True)
|
|
18
|
+
async def manage_server(request):
|
|
19
|
+
if running_with_coverage(request.config):
|
|
20
|
+
# run headless client even if running with coverage
|
|
21
|
+
rio.testing.browser_client.DEBUGGER_ACTIVE = False
|
|
22
|
+
pytest.mark.timeout(180)
|
|
23
|
+
else:
|
|
24
|
+
if rio.testing.browser_client.DEBUGGER_ACTIVE:
|
|
25
|
+
pytest.mark.timeout(0 if rio.testing.browser_client.DEBUGGER_ACTIVE else 90)
|
|
26
|
+
|
|
27
|
+
async with rio.testing.browser_client.prepare_browser_client():
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(autouse=True)
|
|
32
|
+
def setup_ui_platform(monkeypatch):
|
|
33
|
+
monkeypatch.setenv('UI_PLATFORM', 'Rio')
|
|
34
|
+
yield
|
|
35
|
+
monkeypatch.undo()
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import pathlib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import ordered_set
|
|
10
|
+
from webview_proc import WebViewProcess
|
|
11
|
+
|
|
12
|
+
import rio
|
|
13
|
+
from rio import app_server, errors, utils
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable, Iterable
|
|
17
|
+
|
|
18
|
+
import uvicorn
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class App10x:
|
|
23
|
+
"""
|
|
24
|
+
# App10x - a custom App to use in dialog.
|
|
25
|
+
# rio.App is t.final, so App10x wraps rio.App and implements run_in_window
|
|
26
|
+
# --> run webview out-of-process
|
|
27
|
+
# --> run fastapi server in-process
|
|
28
|
+
# --> uses a subclass fo FastAPIServer to create a custom session class
|
|
29
|
+
# --> custom session class communicates with out-of-process webview (rather than using webview_shim)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
app: rio.App
|
|
33
|
+
webview: WebViewProcess | None = None
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _update_window_size(width: float | None, height: float | None) -> None:
|
|
37
|
+
import webview # imported here as called in separate process
|
|
38
|
+
|
|
39
|
+
if width is None and height is None:
|
|
40
|
+
return
|
|
41
|
+
window = webview.windows[0]
|
|
42
|
+
print(f'Current window size: {window.width}x{window.height} pixels')
|
|
43
|
+
pixels_per_rem = window.evaluate_js("""
|
|
44
|
+
let measure = document.createElement('div');
|
|
45
|
+
document.body.appendChild(measure);
|
|
46
|
+
measure.style.height = '1rem';
|
|
47
|
+
let pixels_per_rem = measure.getBoundingClientRect().height * window.devicePixelRatio;
|
|
48
|
+
measure.remove();
|
|
49
|
+
pixels_per_rem;
|
|
50
|
+
""")
|
|
51
|
+
width_in_pixels = window.width if width is None else round(width * pixels_per_rem)
|
|
52
|
+
height_in_pixels = window.height if height is None else round(height * pixels_per_rem)
|
|
53
|
+
print(f'Resizing window to {width_in_pixels}x{height_in_pixels} pixels ({width}x{height} rem at {pixels_per_rem} pixels/rem)')
|
|
54
|
+
window.resize(width_in_pixels, height_in_pixels)
|
|
55
|
+
|
|
56
|
+
def _run_in_window(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
quiet: bool = True,
|
|
60
|
+
maximized: bool = False,
|
|
61
|
+
fullscreen: bool = False,
|
|
62
|
+
width: float | None = None,
|
|
63
|
+
height: float | None = None,
|
|
64
|
+
debug_mode: bool = False,
|
|
65
|
+
on_server_created: Callable[[uvicorn.Server], None] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
host = 'localhost'
|
|
68
|
+
port = utils.ensure_valid_port(host, None)
|
|
69
|
+
url = f'http://{host}:{port}'
|
|
70
|
+
|
|
71
|
+
server: uvicorn.Server | None = None
|
|
72
|
+
|
|
73
|
+
icon_path = asyncio.run(self.app._fetch_icon_as_png_path())
|
|
74
|
+
|
|
75
|
+
self.webview = webview = WebViewProcess(
|
|
76
|
+
url=url,
|
|
77
|
+
title=self.app.name,
|
|
78
|
+
maximized=maximized,
|
|
79
|
+
fullscreen=fullscreen,
|
|
80
|
+
icon_path=icon_path,
|
|
81
|
+
func=partial(self._update_window_size, width, height),
|
|
82
|
+
on_close=lambda: server and setattr(server, 'should_exit', True),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _on_server_created(_server: uvicorn.Server) -> None:
|
|
86
|
+
nonlocal server
|
|
87
|
+
server = _server
|
|
88
|
+
|
|
89
|
+
fastapi_server = server.config.app
|
|
90
|
+
fastapi_server.__class__ = FastapiServer
|
|
91
|
+
fastapi_server.app10x = self
|
|
92
|
+
if on_server_created:
|
|
93
|
+
on_server_created(server)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
self.app._run_as_web_server(
|
|
97
|
+
host=host,
|
|
98
|
+
port=port,
|
|
99
|
+
quiet=quiet,
|
|
100
|
+
running_in_window=True,
|
|
101
|
+
internal_on_app_start=webview.start,
|
|
102
|
+
internal_on_server_created=_on_server_created,
|
|
103
|
+
debug_mode=debug_mode,
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f'Error running app: {e}')
|
|
107
|
+
finally:
|
|
108
|
+
if webview.is_alive():
|
|
109
|
+
webview.close()
|
|
110
|
+
webview.join()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class FastapiServer(app_server.FastapiServer):
|
|
114
|
+
app10x: App10x
|
|
115
|
+
|
|
116
|
+
async def create_session(self, *args, **kwargs) -> rio.Session:
|
|
117
|
+
session = await super().create_session(*args, **kwargs)
|
|
118
|
+
session.__class__ = Session
|
|
119
|
+
session.app10x = self.app10x
|
|
120
|
+
return session
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Session(rio.Session):
|
|
124
|
+
app10x: App10x
|
|
125
|
+
|
|
126
|
+
async def _close(self, close_remote_session: bool) -> None:
|
|
127
|
+
if not self.running_in_window:
|
|
128
|
+
await super()._close(close_remote_session=close_remote_session)
|
|
129
|
+
|
|
130
|
+
await super()._close(close_remote_session=False)
|
|
131
|
+
if close_remote_session:
|
|
132
|
+
self.app10x.webview.close()
|
|
133
|
+
|
|
134
|
+
async def _get_webview_window(self):
|
|
135
|
+
raise RuntimeError('Should not be called required in out-of-process webview')
|
|
136
|
+
|
|
137
|
+
async def set_title(self, title: str) -> None:
|
|
138
|
+
if not self.running_in_window:
|
|
139
|
+
await super().set_title(title)
|
|
140
|
+
|
|
141
|
+
self.app10x.webview.set_title(title)
|
|
142
|
+
|
|
143
|
+
async def pick_folder(self) -> pathlib.Path:
|
|
144
|
+
if not self.running_in_window:
|
|
145
|
+
return await super().pick_folder()
|
|
146
|
+
|
|
147
|
+
return pathlib.Path(self.app10x.webview.pick_folder())
|
|
148
|
+
|
|
149
|
+
async def pick_file(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
file_types: Iterable[str] | None = None,
|
|
153
|
+
multiple: bool = False,
|
|
154
|
+
) -> utils.FileInfo | list[utils.FileInfo]:
|
|
155
|
+
if not self.running_in_window:
|
|
156
|
+
return await super().pick_file(file_types=file_types, multiple=multiple)
|
|
157
|
+
|
|
158
|
+
# Normalize the file types
|
|
159
|
+
if file_types is not None:
|
|
160
|
+
# Normalize and deduplicate, but maintain the order
|
|
161
|
+
file_types = list(ordered_set.OrderedSet(utils.normalize_file_extension(file_type) for file_type in file_types))
|
|
162
|
+
|
|
163
|
+
selected = self.app10x.webview.pick_file(
|
|
164
|
+
file_types=[f'{extension} (*.{extension})' for extension in file_types],
|
|
165
|
+
multiple=multiple,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not selected:
|
|
169
|
+
raise errors.NoFileSelectedError()
|
|
170
|
+
|
|
171
|
+
return [utils.FileInfo._from_path(path) for path in selected] if multiple else utils.FileInfo._from_path(selected)
|
|
172
|
+
|
|
173
|
+
async def save_file(
|
|
174
|
+
self,
|
|
175
|
+
file_contents: pathlib.Path | str | bytes,
|
|
176
|
+
file_name: str = 'Unnamed File',
|
|
177
|
+
*,
|
|
178
|
+
media_type: str | None = None,
|
|
179
|
+
directory: pathlib.Path | None = None,
|
|
180
|
+
) -> None:
|
|
181
|
+
if not self.running_in_window:
|
|
182
|
+
return await super().save_file(file_contents, file_name, media_type=media_type, directory=directory)
|
|
183
|
+
|
|
184
|
+
self.app10x.webview.save_file(
|
|
185
|
+
file_contents=file_contents,
|
|
186
|
+
directory='' if directory is None else str(directory),
|
|
187
|
+
file_name=file_name,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
Session._local_methods_ = rio.Session._local_methods_.copy()
|
|
192
|
+
Session._remote_methods_ = rio.Session._remote_methods_.copy()
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
if __name__ == '__main__':
|
|
4
|
+
assert os.environ.setdefault('UI_PLATFORM', 'Rio') == os.getenv('UI_PLATFORM')
|
|
5
|
+
from ui_10x.platform import ux
|
|
6
|
+
|
|
7
|
+
print(ux.Dialog(children=[ux.Label('Message'), ux.Label('Message2')], title='Title').exec())
|
|
8
|
+
|
|
9
|
+
# from functools import partial
|
|
10
|
+
#
|
|
11
|
+
# from rio import project_config, App, Text, TextStyle, ComponentPage, page
|
|
12
|
+
|
|
13
|
+
# app = App(
|
|
14
|
+
# pages=[
|
|
15
|
+
# ComponentPage( '', '',
|
|
16
|
+
# build=lambda: ux.VBoxLayout(
|
|
17
|
+
# ux.Label('a'),
|
|
18
|
+
# ux.PushButton('b',on_press=partial(print, 'b pressed')),
|
|
19
|
+
# )(),
|
|
20
|
+
# )
|
|
21
|
+
# ]
|
|
22
|
+
# )
|
|
23
|
+
# app.run_in_window()
|
|
24
|
+
#
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from rio.debug.monkeypatches import apply_monkeypatches
|
|
2
|
+
|
|
3
|
+
import rio
|
|
4
|
+
from ui_10x.rio.components.splitter import Splitter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Example app to demonstrate the Splitter component
|
|
8
|
+
class SplitterApp(rio.Component):
|
|
9
|
+
def build(self) -> rio.Component:
|
|
10
|
+
splitter = Splitter(
|
|
11
|
+
children=[
|
|
12
|
+
rio.Text('Pane 1', style='heading3'),
|
|
13
|
+
rio.Text('Pane 2', style='heading3'),
|
|
14
|
+
rio.Rectangle(content=rio.Container(rio.Container(rio.FlowContainer()))),
|
|
15
|
+
],
|
|
16
|
+
handle_size=0.3, # Splitter handle width
|
|
17
|
+
direction='horizontal',
|
|
18
|
+
)
|
|
19
|
+
return rio.Container(rio.Column(rio.Column(rio.Row(rio.Button('A'), rio.Button('B')), rio.Container(splitter))))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Run the app
|
|
23
|
+
if __name__ == '__main__':
|
|
24
|
+
app = rio.App(build=SplitterApp)
|
|
25
|
+
apply_monkeypatches()
|
|
26
|
+
# app._run_in_window(debug_mode=True)
|
|
27
|
+
app._run_as_web_server(debug_mode=True, port=8080, host='')
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from core_10x.global_cache import cache
|
|
4
|
+
|
|
5
|
+
from ui_10x.rio import component_builder, widgets
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@cache
|
|
9
|
+
def init(): ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Object: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Application = widgets.Application
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DirectConnection = component_builder.ConnectionType.DIRECT
|
|
19
|
+
QueuedConnection = component_builder.ConnectionType.QUEUED
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SignalDecl = component_builder.SignalDecl
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def signal_decl(arg=object):
|
|
26
|
+
assert arg is object, 'arg must be object'
|
|
27
|
+
return SignalDecl()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
MouseEvent = component_builder.MouseEvent
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SCROLL = widgets.SCROLL
|
|
34
|
+
|
|
35
|
+
Point = component_builder.Point
|
|
36
|
+
FontMetrics = component_builder.FontMetrics
|
|
37
|
+
SizePolicy = component_builder.FontMetrics
|
|
38
|
+
TEXT_ALIGN = component_builder.TEXT_ALIGN
|
|
39
|
+
|
|
40
|
+
Widget = component_builder.Widget
|
|
41
|
+
Layout = component_builder.Layout
|
|
42
|
+
FlowLayout = component_builder.FlowLayout
|
|
43
|
+
|
|
44
|
+
LineEdit = widgets.LineEdit
|
|
45
|
+
Label = widgets.Label
|
|
46
|
+
PushButton = widgets.PushButton
|
|
47
|
+
|
|
48
|
+
Spacer = widgets.Spacer
|
|
49
|
+
|
|
50
|
+
HBoxLayout = widgets.HBoxLayout
|
|
51
|
+
VBoxLayout = widgets.VBoxLayout
|
|
52
|
+
FormLayout = widgets.FormLayout
|
|
53
|
+
|
|
54
|
+
Dialog = widgets.Dialog
|
|
55
|
+
|
|
56
|
+
MessageBox = widgets.MessageBox
|
|
57
|
+
|
|
58
|
+
RadioButton = widgets.RadioButton
|
|
59
|
+
ButtonGroup = widgets.ButtonGroup
|
|
60
|
+
|
|
61
|
+
GroupBox = widgets.GroupBox
|
|
62
|
+
|
|
63
|
+
TextEdit = widgets.TextEdit
|
|
64
|
+
CheckBox = widgets.CheckBox
|
|
65
|
+
ScrollArea = widgets.ScrollArea
|
|
66
|
+
Separator = widgets.Separator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def separator(horizontal=True) -> Separator:
|
|
70
|
+
return Separator() if horizontal else Separator(orientation='vertical')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Direction = widgets.Direction
|
|
74
|
+
|
|
75
|
+
Vertical = Direction.VERTICAL
|
|
76
|
+
Horizontal = Direction.HORIZONTAL
|
|
77
|
+
|
|
78
|
+
Splitter = widgets.Splitter
|
|
79
|
+
|
|
80
|
+
Style = widgets.Style
|
|
81
|
+
|
|
82
|
+
FindFlags = widgets.FindFlags
|
|
83
|
+
MatchExactly = FindFlags.MATCH_EXACTLY
|
|
84
|
+
|
|
85
|
+
ListWidget = widgets.ListWidget
|
|
86
|
+
ListItem = widgets.ListItem
|
|
87
|
+
|
|
88
|
+
TreeWidget = widgets.TreeWidget
|
|
89
|
+
TreeItem = widgets.TreeItem
|
|
90
|
+
|
|
91
|
+
CalendarWidget = widgets.CalendarWidget
|