-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdemo.py
467 lines (378 loc) · 22.8 KB
/
demo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
from icedpygui import IPG, IpgTextParam, IpgRadioDirection
from icedpygui import IpgButtonParam, IpgProgressBarParam
from icedpygui import IpgAlignment
from icedpygui import IpgTableWidget, IpgTableRowHighLight
import random
"""
IcedPyGui is based on Rust Iced gui at https://github.com/iced-rs/iced.
Some code is used from Iced_aw at https://github.com/iced-rs/iced_aw.
Pyo3 is used as the python wrapper at https://github.com/pyo3/pyo3.
Maturin is used to build and publish the module at https://github.com/PyO3/maturin.
IPG is easy to use. The syntax used follows closely with
that found using dearpygui, my inspiration for this.
IPG has a backend of Rust versus c++ which does result in some
differences. The key difference will be in the way
certain data is structured. Rust doesn't allow mixed types in lists or
dictionaries, but this difference has mostly been shielded from the user.
The table is where you will mostly see this. For example, instead of
using a dictionary for the data, List of dictionaries where used and
have distinct data types like {string, List[int]} or
{String, List[float]}, etc. The only mixed list is a tuple but even
that has to be strictly defined in rust like (int, string) or (string, int).
The user data is special because it is only passed through to rust and
back out as a PyObject or PyAny. Therefore any python data can be used
since it is never extracted into a rust type.
A few simple rules need to be followed.
import IPG as indicated above in this demo.
The last line of code to execute must be ipg.start_session().
Any code after that will not be executed because rust Iced is now
running. You can place it anywhere, just make sure its last executed.
If you start your program and nothing happens, it might mean that
you aren't executing start_session() or you forgot to add it in.
Every widget needs to have a parent container previously defined and
every container needs to have a window and optionally a parent container
defined. If the container is placed into a window then no parent_id is required.
Therefore at least one window needs to be added first and at least
one container needs to be added to the window before any widgets are
added. As long as you have defined a parent, you can add to it.
The organization of your program is your choice. You can use a class
or just functions for simple programs. A @dataclass is not supported
at this time but should be soon. You can add all of your containers
at once if you want or add the container and all or some of its widgets.
The key is that a container needs to be added first.
The major containers are Container, Column, and Row. A Container can
have only one widget. Therefore, if you have more than one, you need
to add them to a Column or a Row first and then place the Column or
Row into the Container, if needed.
There are some non-obvious containers like Scrollable. It's not only a
container but a widget too, because it has callbacks.
Besides width and height, the Container defaults to centering which
aligns the item in the center of the Container. This is very handy
for the centering of your Column or Row. Other options are available.
A Column aligns your widget vertically. So as you add widgets, they are
placed top to bottom. The Column has a spacing parameter but you can add
the spacing widget, if you have other spacing requirements.
A Row is like the Column except it aligns the widgets horizontally.
As you place your widgets into a row, they are placed left to right.
The alignment depends a lot on the width and height of the container.
The 3 basic options for setting the width and height are:
1. Shrink (default): container shrinks to the size of the largest widget.
2. Specific value using a float.
3. Setting the width_fill or height_fill parameters to True which
overrides the float, fills available space container its in.
The interaction of the above setting can be a bit difficult to figure out
sometimes. However, by using the debug=True option in the window parameters,
you will be able to see how the layout looks. If you don't see your widget
on the screen, its because certain combinations cause the fill to
exceed the windows width or height and your widgets are off screen.
I find placing everything into a Container and centering usually
brings it back on the screen. You can also set a specific width
and height to help you figure how things are placed.
The nice thing about using fill as much as possible is that when you
resize your your window, everything resizes and you don't have to
go back and recalculate your sizes based on the window size.
In some cases you will need to do this so use the window
on_resize to get the width and height then recalculate your sizes
as needed in your callback using the update_item.
If you hide a widget, currently a small placeholder remains
future additions will add hidden with and without a placeholder.
A big part of constructing your gui layout is using the id's of
the widgets, containers, and windows. The ids are a central part of
how IPG operates. During the execution of the python program,
the functions are called in rust and a structure having all of the
necessary information for each widget is stored in a global list
based on the window id and the id of all the widgets.
Once the session is started, Iced is started up, the empty windows are
created, unique container ids are determined, and a recursive function
processes all of the nested children.
When the window needs to be updated, the update routine in Iced
determines which widget type needs to be updated by processing a generic
enum structure and then the widget type module is called.
The module for the widget type determines which widget needs changing
based on the id, makes changes and returns to the iced update function
which sends any new messages and/or updates the windows.
Since the window_id, container_id, and parent_id are strings, typo's
can occur throughout your program and changing them can be tedious
for large programs. Therefore for larger programs, I prefer to assign
my ids in the class such as self.wnd_1: str="window_id_1", for example.
Then your IDE will supply a dropdown of your variables and hopefully
reduce typos. If you group them together properly, you might find
that your naming could be improved and easily changed.
Callbacks are the only way to update your windows, as discussed above,
Iced uses a messaging system and these are processed and sent back to
python by calling the specified function set by the user. For example,
a button has an on_press=a user supplied function, on_press=button_pressed.
The returning callback data varies depending on the widget.
For example, a button has no data so the callback only sends back
an id of the button. A color_picker sends back a list of the rgba
values and so on. These are documented but if you are unsure or the
docs are behind in updating, just print the data and see what it looks like.
The callbacks, as you'll see in the below program, have up to 3 returning
pieces of data, widget id, some data, and user_data. Keep in mind that the id is the
id of the calling widget, which may or may not be the id you want to use
for updating an item. Try not to use the term id in the parameter list because
that is a python element. Also, name the id after the calling widget so that
you remember what the widget is and if that's the id you want to use.
For example, if you have a callback for the button widget and want to change
a text widget to read "Button Pressed" then to update the text widget,
you'll need the text widget id. You can get this by equating the text
widget to a variable which you would use as the id in update_item.
def create_button_and_text():
ipg.add_button(parent_id="col", "Press Me", on_press=button_pressed)
text_id = ipg.add_text(parent_id="col", content="Some text")
Your callback function
def button_pressed(btn_id):
ipg.update(text_id, IpgTextParam.Content, "Button was Pressed")
In this callback function you only have one returning parameter, btn_id.
Most other widgets have a second data parameter. The user_data parameters
depends on if you use the user_data option on the widget. If you don't use
the user_data option,make sure and not to put that parameter in the callback function
or you'll get an error. You'll also get an error if you use the user_data and forget
to add that parameter to your callback function. The names of the parameters
can be whatever you like, the order is the most important:
calling widget id, data(if present), user_data.
It's important to look through all the demos to get a feel for how things operate.
I tried to vary up things to include different ideas. However, a demo doesn't
really do much just use a lot of text widgets to show the results. Give it a try with
with a real program and let me know through the git repository or discord if you have problems,
questions, or suggestions.
Have fun with IPG!!
"""
class Demo:
def __init__(self) -> None:
self.ipg = IPG() # initialize IPG, must use
# window ids
self.wnd_1: str = "main_1"
self.wnd_2: str = "main_2"
# containers for window 1
self.row_1: str = "row_1"
self.l_col_1: str = "left_col_1"
self.r_col_1: str = "right_col_1"
# widgets in window 1
# 0 is not a valid id so if not initialized,
# you'll get an error of not finding
# the id.
self.btn_id: int = 0
self.button_presses: int = 0
self.btn_text_id: int = 0
self.text_id_chkbox: int = 0
self.bar_id: int = 0
self.slider_text_id: int = 0
self.picklist_text_id: int = 0
self.radio_1_text_id: int = 0
self.radio_2_text_id: int = 0
self.selectable_text_id: int = 0
self.text_input_id: int= 0
# containers for window 2
self.l_col_2: str = "left_col_2"
self.r_col_2: str = "right_col_2"
# Widgets in window 2
self.date_text_id: int = 0
def start_gui(self):
self.construct_window_1()
self.construct_button()
self.construct_checkbox()
self.construct_slider_and_progress_bar()
self.construct_pick_list()
self.construct_radio_buttons_v()
self.construct_radio_buttons_h()
self.construct_selectable_text()
self.construct_text_input()
self.construct_window_2()
self.construct_date_picker()
self.construct_table()
# required to be last executed
self.ipg.start_session()
def construct_window_1(self):
self.ipg.add_window(self.wnd_1, "Demo Window 1 - Iced Wrapped in Python",
width=500, height=500, pos_x=100, pos_y=25)
self.ipg.add_row(self.wnd_1, container_id=self.row_1, width_fill=True, height_fill=True)
self.ipg.add_column(self.wnd_1, container_id=self.l_col_1, parent_id=self.row_1)
self.ipg.add_column(self.wnd_1, container_id=self.r_col_1, parent_id=self.row_1)
# A button is defined, a text is defined with info.
# The callback function follows where the content of the text
# is replaced by the user_data.
def construct_button(self):
self.btn_id = self.ipg.add_button(parent_id=self.l_col_1, label="Press Me!",
on_press=self.button_pressed)
self.btn_text_id = self.ipg.add_text(self.l_col_1,
f"A text can count too {self.button_presses}")
def button_pressed(self, btn_id):
self.button_presses += 1
self.ipg.update_item(btn_id, IpgButtonParam.Label, f"You Pressed {self.button_presses} times!")
self.ipg.update_item(self.btn_text_id, IpgTextParam.Content,
f"A text can count too: {self.button_presses} times!")
# A checkbox is defined and a text is defined with the show value of False.
# Unlike the button above, in this case we hid the text and then will show it
# when the box is checked.
def construct_checkbox(self):
self.ipg.add_checkbox(parent_id=self.l_col_1, label="Check Me",
on_toggle=self.box_checked_id)
self.text_id_chkbox = self.ipg.add_text(parent_id=self.l_col_1,
content="You Checked the box above",
show=False) # note: show is False
def box_checked_id(self, _chk_id, data):
self.ipg.update_item(self.text_id_chkbox, IpgTextParam.Show, data) # show set to True
# a progress bar and a slider are defined and connected together via the callbacks
def construct_slider_and_progress_bar(self):
self.bar_id = self.ipg.add_progress_bar(parent_id=self.l_col_1, min=0.0, max=100.0,
value=50.0, width_fill=True)
self.ipg.add_slider(parent_id=self.l_col_1, min=0.0, max=100.0,
step=1.0, value=50.0, width_fill=True,
on_change=self.slider_on_change,
on_release=self.slider_on_release)
self.slider_text_id = self.ipg.add_text(self.l_col_1, "Slider content here.")
# Both callbacks were used in this case for demonstration but it is
# expected that you probably only will use the release mostly.
# if you have a costly calculation you are using, you may want to
# not use the on_change or filter it by using a counter to select
# only a few changes.
def slider_on_change(self, _slider_id, data):
self.ipg.update_item(self.slider_text_id, IpgTextParam.Content, f"Slide = {data}")
self.ipg.update_item(self.bar_id, IpgProgressBarParam.Value, data)
def slider_on_release(self, _slider_id, data):
self.ipg.update_item(self.bar_id, IpgProgressBarParam.Value, data)
# A picklist is defined here width a place holder. The option list holder the selections.
def construct_pick_list(self):
self.ipg.add_pick_list(parent_id=self.l_col_1, options=["one", "two", "three"],
on_select=self.picked_item,
placeholder="Choose a string number")
self.picklist_text_id = self.ipg.add_text(self.l_col_1, "You picked:")
def picked_item(self, id, data):
self.ipg.update_item(self.picklist_text_id, IpgTextParam.Content, f"You Picked: {data}")
# *****************Right Column in Window 1*************************
# Two groups of radio buttons are defined, one vertical and one horizontal
# Currently there is a limit of 26 buttons per group.
# This set of radio buttons will be vertical
def construct_radio_buttons_v(self):
labels = ["Radio A", "Radio B", "Radio C"]
self.ipg.add_radio(parent_id=self.r_col_1, labels=labels, on_select=self.radio_selected_v)
self.radio_1_text_id = self.ipg.add_text(self.r_col_1, "You selected:")
# The radio on_select returns a tuple (index, label)
def radio_selected_v(self, _radio_id, data):
self.ipg.update_item(self.radio_1_text_id, IpgTextParam.Content, f"You selected: {data}")
# This set of radio buttons will be horizontal
def construct_radio_buttons_h(self):
self.ipg.add_radio(parent_id=self.r_col_1, labels=["A", "B", "C"],
direction=IpgRadioDirection.Horizontal,
on_select=self.radio_selected_h)
self.radio_2_text_id = self.ipg.add_text(self.r_col_1, "You selected:")
# The radio on_select returns a tuple (index, label)
def radio_selected_h(self, _radio_id, data):
self.ipg.update_item(self.radio_2_text_id, IpgTextParam.Content, f"You selected: {data}")
# A button style can act as a selectable text but has only one callback.
# A selectable text has a number of different callbacks for all the mouse buttons and
# mouse enter and exit.
def construct_selectable_text(self):
self.ipg.add_selectable_text(self.r_col_1, "My Selectable Text",
on_press=self.selecting_text,
on_release=self.selecting_text,
on_middle_press=self.selecting_text,
on_middle_release=self.selecting_text,
on_right_press=self.selecting_text,
on_right_release=self.selecting_text,
on_enter=self.selecting_text,
on_move=self.selecting_text_with_point,
on_exit=self.selecting_text
)
self.selectable_text_id = self.ipg.add_text(self.r_col_1, "Selectable actions:")
def selecting_text(self, sel_txt_id):
self.ipg.update_item(self.selectable_text_id, IpgTextParam.Content, f"Selectable id: {sel_txt_id}")
def selecting_text_with_point(self, _sel_txt_id, data):
self.ipg.update_item(self.selectable_text_id, IpgTextParam.Content, f"point: {data}")
def construct_text_input(self):
self.ipg.add_text_input(parent_id=self.r_col_1,
placeholder="My Placeholder",
width=200.0,
on_submit=self.text_input_submitted,
on_input=self.text_on_input)
self.text_input_id = self.ipg.add_text(self.r_col_1, "Will fill while typing")
# Only one callback used in this case (two could be used). Determining which callback is based on name.
# Maybe helpful in some cases where callbacks are similar or there are many.
def text_input_submitted(self, _input_id, data):
self.ipg.update_item(self.text_input_id, IpgTextParam.Content,
f"You submitted: {data}")
def text_on_input(self, _input_id, data):
self.ipg.update_item(self.text_input_id, IpgTextParam.Content,
f"Adding while typing: {data}")
# **********************window_2*****************************************************
def construct_window_2(self):
self.ipg.add_window(self.wnd_2, "Demo Window 2 - Iced Wrapped in Python",
width=600, height=500,
pos_x=650, pos_y=25)
self.ipg.add_column(window_id=self.wnd_2, container_id=self.l_col_2,
width_fill=True, align_items=IpgAlignment.Center)
# A date picker is defined and the results are put in a text widget.
def construct_date_picker(self):
self.ipg.add_date_picker(self.l_col_2, on_submit=self.date_selected)
self.date_text_id = self.ipg.add_text(self.l_col_2,
"You selected:")
def date_selected(self, _date_id, date):
self.ipg.update_item(self.date_text_id, IpgTextParam.Content, f"You selected: {date}")
# A table is defined with 6 columns of widgets and random items.
# Rust does not allow types to be mixed in a list.
# Therefore, if a mixed list is needed, convert it to a list[str].
# The gui converts the list to strings anyway.
# Width and height are required for the table.
def construct_table(self):
# Initialize the lists.
col0 = []
col1 = []
col2 = []
col3 = []
col4 = []
col5 = []
col6 = []
# Add some random data of different types
for i in range(0, 20):
# labels for the button widget
col0.append("Button")
# labels for the checkboxes
col1.append("")
# make a float random number
col2.append(random.randrange(10, 99) + random.randrange(10, 99) / 100)
col3.append(random.choice(["one", "two", "three", "four", "five", "six", "seven"]))
col4.append(random.randrange(10, 99))
col5.append(random.choice([True, False]))
# Create the table, the requirement is a list of dictionaries.
# Rust does not have dictionaries but a similar type is called a HashMap.
# The reason for the list of dictionaries is that you cannot extract a
# mixed dictionary into a Rust HashMap. The HashMap has to have predefined
# types. In this case they are types dict[str, list[Widgets]], dict[str, list[float or int]],
# list[str, list[str]], and list[str], list[bool].
# Each column is extracted based on a single type. If a mixed column occurs, then an error
# will be generated. If no existing type is found, then an error occurs or just skipped.
# Currently, not every variation is covered but that can be improved in future versions.
# This probably covers the vast majority of needs. If you need that mixed column, convert
# the list to a string. When the final version is displayed, it's converted to a string anyway.
data = [{"Button": col0},
{"CheckBox": col1},
{"Col2": col2},
{"Col3": col3},
{"Col4": col4},
{"Col5": col5}]
column_widths = [75.0, 100.0, 100.0, 100.0, 100.0, 100.0]
tbl_width = sum(column_widths)
# The table is added.
self.ipg.add_table(window_id=self.wnd_2,
table_id="table",
title="My Table",
data=data,
column_widths=column_widths,
width=tbl_width,
height=300.0,
row_highlight=IpgTableRowHighLight.Lighter,
data_length=len(col0),
button_fill_columns=[0],
checkbox_fill_columns=[1],
on_button=self.widget_button,
on_checkbox=self.widget_checkbox,
)
def widget_button(self, tbl_id: int, wid_index: tuple[int, int]):
print(tbl_id, wid_index)
def widget_checkbox(self, tbl_id: int, wid_index: tuple[int, int], is_checked: bool):
print(tbl_id, wid_index, is_checked)
def on_text_enter(self, tbl_id, text_index: tuple[int, int]):
print(tbl_id, text_index)
demo = Demo()
demo.start_gui()