-
Notifications
You must be signed in to change notification settings - Fork 77
/
ui_element.py
851 lines (685 loc) · 33.6 KB
/
ui_element.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
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
from sys import version_info
import warnings
from collections import namedtuple
from typing import List, Union, Tuple, Dict, Any, Callable, Set
import pygame
from pygame_gui.core.interfaces import IUIElementInterface
from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
from pygame_gui.core.utility import render_white_text_alpha_black_bg, USE_PREMULTIPLIED_ALPHA
from pygame_gui.core.utility import basic_blit
from pygame_gui.core.layered_gui_group import GUISprite
if version_info.minor >= 7:
ObjectID = namedtuple('ObjectID',
field_names=('object_id', 'class_id'),
defaults=(None, None))
else:
ObjectID = namedtuple('ObjectID', field_names=('object_id', 'class_id'))
class UIElement(GUISprite, IUIElementInterface):
"""
A base class for UI elements. You shouldn't create UI Element objects, instead all UI Element
classes should derive from this class. Inherits from pygame.sprite.Sprite.
:param relative_rect: A rectangle shape of the UI element, the position is relative to the
element's container.
:param manager: The UIManager that manages this UIElement.
:param container: A container that this element is contained in.
:param starting_height: Used to record how many layers above it's container this element
should be. Normally 1.
:param layer_thickness: Used to record how 'thick' this element is in layers. Normally 1.
:param anchors: A dictionary describing what this element's relative_rect is relative to.
:param visible: Whether the element is visible by default. Warning - container visibility may
override this.
"""
def __init__(self, relative_rect: pygame.Rect,
manager: IUIManagerInterface,
container: Union[IContainerLikeInterface, None],
*,
starting_height: int,
layer_thickness: int,
anchors: Dict[str, str] = None,
visible: int = 1):
self._layer = 0
self.ui_manager = manager
super().__init__(self.ui_manager.get_sprite_group())
self.relative_rect = relative_rect.copy()
self.rect = self.relative_rect.copy()
self.ui_group = self.ui_manager.get_sprite_group()
self.ui_theme = self.ui_manager.get_theme()
self.object_ids = None
self.class_ids = None
self.element_ids = None
self.combined_element_ids = None
self.most_specific_combined_id = 'no_id'
self.anchors = anchors
if self.anchors is None:
self.anchors = {'left': 'left',
'top': 'top',
'right': 'left',
'bottom': 'top'}
self.drawable_shape = None # type: Union['DrawableShape', None]
self.image = None
if visible:
self.visible = 1
else:
self.visible = 0
self.blendmode = pygame.BLEND_PREMULTIPLIED if USE_PREMULTIPLIED_ALPHA else 0
# self.source_rect = None
self.relative_bottom_margin = None
self.relative_right_margin = None
self.layer_thickness = layer_thickness
self.starting_height = starting_height
self.is_enabled = True
self.hovered = False
self.is_focused = False
self.hover_time = 0.0
self.pre_debug_image = None
self._pre_clipped_image = None
self._image_clip = None
self._visual_debug_mode = False
# Themed parameters
self.shadow_width = None # type: Union[None, int]
self.border_width = None # type: Union[None, int]
self.shape_corner_radius = None # type: Union[None, int]
self._setup_container(container)
self.dirty = 1
self.visible = 0
self._setup_visibility(visible)
self._update_absolute_rect_position_from_anchors()
self._update_container_clip()
self._focus_set = {self}
def _setup_visibility(self, visible):
if visible:
self.visible = 1
if self.ui_container is not None and not self.ui_container.visible:
self.visible = 0
def _setup_container(self, container):
if container is None:
if self.ui_manager.get_root_container() is not None:
container = self.ui_manager.get_root_container()
else:
container = self
if isinstance(container, IContainerLikeInterface):
self.ui_container = container.get_container()
if self.ui_container is not None and self.ui_container is not self:
self.ui_container.add_element(self)
def get_focus_set(self) -> Set['UIElement']:
return self._focus_set
def set_focus_set(self, focus_set: Set['UIElement']):
if self.ui_manager.get_focus_set() is self._focus_set:
self.ui_manager.set_focus_set(focus_set)
self._focus_set = focus_set
def join_focus_sets(self, element: 'UIElement'):
union_of_sets = set(self._focus_set | element.get_focus_set())
for item in union_of_sets:
item.set_focus_set(union_of_sets)
def remove_element_from_focus_set(self, element):
self._focus_set.discard(element)
def get_relative_rect(self) -> pygame.Rect:
"""
The relative positioning rect.
:return: A pygame rect.
"""
return self.relative_rect
def get_abs_rect(self) -> pygame.Rect:
"""
The absolute positioning rect.
:return: A pygame rect.
"""
return self.rect
def get_element_ids(self) -> List[str]:
"""
A list of all the element IDs in this element's theming/event hierarchy.
:return: a list of strings, one ofr each element in the hierarchy.
"""
return self.element_ids
def _create_valid_ids(self,
container: Union[IContainerLikeInterface, None],
parent_element: Union[None, 'UIElement'],
object_id: Union[ObjectID, str, None],
element_id: str):
"""
Creates valid id lists for an element. It will assert if users supply object IDs that
won't work such as those containing full stops. These ID lists are used by the theming
system to identify what theming parameters to apply to which element.
:param container: The container for this element. If parent is None the container will be
used as the parent.
:param parent_element: Element that this element 'belongs to' in theming. Elements inherit
colours from parents.
:param object_id: An optional set of IDs to help distinguish this element
from other elements.
:param element_id: A string ID representing this element's class.
"""
if parent_element is None and container is not None:
id_parent = container
else:
id_parent = parent_element
if isinstance(object_id, str):
if object_id is not None and ('.' in object_id or ' ' in object_id):
raise ValueError('Object ID cannot contain fullstops or spaces: ' + str(object_id))
obj_id = object_id
class_id = None
elif isinstance(object_id, ObjectID):
obj_id = object_id.object_id
class_id = object_id.class_id
else:
obj_id = None
class_id = None
if id_parent is not None:
self.element_ids = id_parent.element_ids.copy()
self.element_ids.append(element_id)
self.class_ids = id_parent.class_ids.copy()
self.class_ids.append(class_id)
self.object_ids = id_parent.object_ids.copy()
self.object_ids.append(obj_id)
else:
self.element_ids = [element_id]
self.class_ids = [class_id]
self.object_ids = [obj_id]
self.combined_element_ids = self.ui_manager.get_theme().build_all_combined_ids(
self.element_ids,
self.class_ids,
self.object_ids)
if self.combined_element_ids is not None and len(self.combined_element_ids) > 0:
self.most_specific_combined_id = self.combined_element_ids[0]
else:
self.most_specific_combined_id = 'no_id'
def _update_absolute_rect_position_from_anchors(self, recalculate_margins=False):
"""
Called when our element's relative position has changed.
"""
new_top = 0
if self.anchors['top'] == 'top':
new_top = self.relative_rect.top + self.ui_container.get_rect().top
elif self.anchors['top'] == 'bottom':
new_top = self.relative_rect.top + self.ui_container.get_rect().bottom
else:
warnings.warn('Unsupported anchor top target: ' + self.anchors['top'])
new_bottom = 0
if self.anchors['bottom'] == 'top':
new_bottom = self.relative_rect.bottom + self.ui_container.get_rect().top
elif self.anchors['bottom'] == 'bottom':
if self.relative_bottom_margin is None or recalculate_margins:
self.relative_bottom_margin = (self.ui_container.get_rect().bottom -
(new_top + self.relative_rect.height))
new_bottom = self.ui_container.get_rect().bottom - self.relative_bottom_margin
else:
warnings.warn('Unsupported anchor bottom target: ' + self.anchors['bottom'])
new_left = 0
if self.anchors['left'] == 'left':
new_left = self.relative_rect.left + self.ui_container.get_rect().left
elif self.anchors['left'] == 'right':
new_left = self.relative_rect.left + self.ui_container.get_rect().right
else:
warnings.warn('Unsupported anchor top target: ' + self.anchors['left'])
new_right = 0
if self.anchors['right'] == 'left':
new_right = self.relative_rect.right + self.ui_container.get_rect().left
elif self.anchors['right'] == 'right':
if self.relative_right_margin is None or recalculate_margins:
self.relative_right_margin = (self.ui_container.get_rect().right -
(new_left + self.relative_rect.width))
new_right = self.ui_container.get_rect().right - self.relative_right_margin
else:
warnings.warn('Unsupported anchor bottom target: ' + self.anchors['right'])
self.rect.left = new_left
self.rect.top = new_top
new_height = new_bottom - new_top
new_width = new_right - new_left
if (new_height != self.relative_rect.height) or (new_width != self.relative_rect.width):
self.set_dimensions((new_width, new_height))
def _update_relative_rect_position_from_anchors(self, recalculate_margins=False):
"""
Called when our element's absolute position has been forcibly changed.
"""
# This is a bit easier to calculate than getting the absolute position from the
# relative one, because the absolute position rectangle is always relative to the top
# left of the screen.
# Setting these to None means we are always recalculating the margins in here.
self.relative_bottom_margin = None
self.relative_right_margin = None
new_top = 0
if self.anchors['top'] == 'top':
new_top = self.rect.top - self.ui_container.get_rect().top
elif self.anchors['top'] == 'bottom':
new_top = self.rect.top - self.ui_container.get_rect().bottom
else:
warnings.warn('Unsupported anchor top target: ' + self.anchors['top'])
new_bottom = 0
if self.anchors['bottom'] == 'top':
new_bottom = self.rect.bottom - self.ui_container.get_rect().top
elif self.anchors['bottom'] == 'bottom':
if self.relative_bottom_margin is None or recalculate_margins:
self.relative_bottom_margin = self.ui_container.get_rect().bottom - self.rect.bottom
new_bottom = self.rect.bottom - self.ui_container.get_rect().bottom
else:
warnings.warn('Unsupported anchor bottom target: ' + self.anchors['bottom'])
new_left = 0
if self.anchors['left'] == 'left':
new_left = self.rect.left - self.ui_container.get_rect().left
elif self.anchors['left'] == 'right':
new_left = self.rect.left - self.ui_container.get_rect().right
else:
warnings.warn('Unsupported anchor top target: ' + self.anchors['left'])
new_right = 0
if self.anchors['right'] == 'left':
new_right = self.rect.right - self.ui_container.get_rect().left
elif self.anchors['right'] == 'right':
if self.relative_right_margin is None or recalculate_margins:
self.relative_right_margin = self.ui_container.get_rect().right - self.rect.right
new_right = self.rect.right - self.ui_container.get_rect().right
else:
warnings.warn('Unsupported anchor bottom target: ' + self.anchors['right'])
# set bottom and right first in case these are only anchors available
self.relative_rect.bottom = new_bottom
self.relative_rect.right = new_right
# set top and left last to give these priority, in most cases where all anchors are set
# we want relative_rect parameters to be correct for whatever the top & left sides are
# anchored to. The data for the bottom and right in cases where left is anchored
# differently to right and/or top is anchored differently to bottom should be captured by
# the bottom and right margins.
self.relative_rect.left = new_left
self.relative_rect.top = new_top
def _update_container_clip(self):
"""
Creates a clipping rectangle for the element's image surface based on whether this
element is inside its container, part-way in it, or all the way out of it.
"""
if self.ui_container.get_image_clipping_rect() is not None:
container_clip_rect = self.ui_container.get_image_clipping_rect().copy()
container_clip_rect.left += self.ui_container.get_rect().left
container_clip_rect.top += self.ui_container.get_rect().top
if not container_clip_rect.contains(self.rect):
left = max(0, container_clip_rect.left - self.rect.left)
right = max(0, self.rect.width - max(0,
self.rect.right -
container_clip_rect.right))
top = max(0, container_clip_rect.top - self.rect.top)
bottom = max(0, self.rect.height - max(0,
self.rect.bottom -
container_clip_rect.bottom))
clip_rect = pygame.Rect(left, top,
max(0, right - left),
max(0, bottom - top))
self._clip_images_for_container(clip_rect)
else:
self._restore_container_clipped_images()
elif not self.ui_container.get_rect().contains(self.rect):
left = max(0, self.ui_container.get_rect().left - self.rect.left)
right = max(0, self.rect.width - max(0,
self.rect.right -
self.ui_container.get_rect().right))
top = max(0, self.ui_container.get_rect().top - self.rect.top)
bottom = max(0, self.rect.height - max(0,
self.rect.bottom -
self.ui_container.get_rect().bottom))
clip_rect = pygame.Rect(left, top,
max(0, right - left),
max(0, bottom - top))
self._clip_images_for_container(clip_rect)
else:
self._restore_container_clipped_images()
def update_containing_rect_position(self):
"""
Updates the position of this element based on the position of it's container. Usually
called when the container has moved.
"""
self._update_absolute_rect_position_from_anchors()
if self.drawable_shape is not None:
self.drawable_shape.set_position(self.rect.topleft)
self._update_container_clip()
def set_relative_position(self, position: Union[pygame.math.Vector2,
Tuple[int, int],
Tuple[float, float]]):
"""
Method to directly set the relative rect position of an element.
:param position: The new position to set.
"""
self.relative_rect.x = int(position[0])
self.relative_rect.y = int(position[1])
self._update_absolute_rect_position_from_anchors(recalculate_margins=True)
if self.drawable_shape is not None:
self.drawable_shape.set_position(self.rect.topleft)
self._update_container_clip()
def set_position(self, position: Union[pygame.math.Vector2,
Tuple[int, int],
Tuple[float, float]]):
"""
Method to directly set the absolute screen rect position of an element.
:param position: The new position to set.
"""
self.rect.x = int(position[0])
self.rect.y = int(position[1])
self._update_relative_rect_position_from_anchors(recalculate_margins=True)
if self.drawable_shape is not None:
self.drawable_shape.set_position(self.rect.topleft)
self._update_container_clip()
def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
Tuple[int, int],
Tuple[float, float]]):
"""
Method to directly set the dimensions of an element.
NOTE: Using this on elements inside containers with non-default anchoring arrangements
may make a mess of them.
:param dimensions: The new dimensions to set.
"""
self.relative_rect.width = int(dimensions[0])
self.relative_rect.height = int(dimensions[1])
self.rect.size = self.relative_rect.size
if dimensions[0] >= 0 and dimensions[1] >= 0:
self._update_absolute_rect_position_from_anchors(recalculate_margins=True)
if self.drawable_shape is not None:
self.drawable_shape.set_dimensions(self.relative_rect.size)
# needed to stop resizing 'lag'
self.set_image(self.drawable_shape.get_fresh_surface())
self._update_container_clip()
def update(self, time_delta: float):
"""
Updates this element's drawable shape, if it has one.
:param time_delta: The time passed between frames, measured in seconds.
"""
if self.alive() and self.drawable_shape is not None:
self.drawable_shape.update(time_delta)
if self.drawable_shape.has_fresh_surface():
self.on_fresh_drawable_shape_ready()
def change_layer(self, new_layer: int):
"""
Changes the layer this element is on.
:param new_layer: The layer to change this element to.
"""
if new_layer != self._layer:
self.ui_group.change_layer(self, new_layer)
self._layer = new_layer
def kill(self):
"""
Overriding regular sprite kill() method to remove the element from it's container.
"""
self.ui_container.remove_element(self)
self.remove_element_from_focus_set(self)
super().kill()
def check_hover(self, time_delta: float, hovered_higher_element: bool) -> bool:
"""
A method that helps us to determine which, if any, UI Element is currently being hovered
by the mouse.
:param time_delta: A float, the time in seconds between the last call to this function
and now (roughly).
:param hovered_higher_element: A boolean, representing whether we have already hovered a
'higher' element.
:return bool: A boolean that is true if we have hovered a UI element, either just now or
before this method.
"""
if self.alive() and self.can_hover():
mouse_x, mouse_y = self.ui_manager.get_mouse_position()
mouse_pos = pygame.math.Vector2(mouse_x, mouse_y)
if (self.is_enabled and
self.hover_point(mouse_x, mouse_y) and
not hovered_higher_element):
if not self.hovered:
self.hovered = True
self.on_hovered()
hovered_higher_element = True
self.while_hovering(time_delta, mouse_pos)
else:
if self.hovered:
self.hovered = False
self.on_unhovered()
elif self.hovered:
self.hovered = False
return hovered_higher_element
def on_fresh_drawable_shape_ready(self):
"""
Called when our drawable shape has finished rebuilding the active surface. This is needed
because sometimes we defer rebuilding until a more advantageous (read quieter) moment.
"""
self.set_image(self.drawable_shape.get_fresh_surface())
def on_hovered(self):
"""
A stub to override. Called when this UI element first enters the 'hovered' state.
"""
def on_unhovered(self):
"""
A stub to override. Called when this UI element leaves the 'hovered' state.
"""
def while_hovering(self, time_delta: float, mouse_pos: pygame.math.Vector2):
"""
A stub method to override. Called when this UI element is currently hovered.
:param time_delta: A float, the time in seconds between the last call to this function
and now (roughly).
:param mouse_pos: The current position of the mouse as 2D Vector.
"""
def can_hover(self) -> bool:
"""
A stub method to override. Called to test if this method can be hovered.
"""
return self.alive()
def hover_point(self, hover_x: float, hover_y: float) -> bool:
"""
Test if a given point counts as 'hovering' this UI element. Normally that is a
straightforward matter of seeing if a point is inside the rectangle. Occasionally it
will also check if we are in a wider zone around a UI element once it is already active,
this makes it easier to move scroll bars and the like.
:param hover_x: The x (horizontal) position of the point.
:param hover_y: The y (vertical) position of the point.
:return: Returns True if we are hovering this element.
"""
container_clip_rect = self.ui_container.get_rect().copy()
if self.ui_container.get_image_clipping_rect() is not None:
container_clip_rect.size = self.ui_container.get_image_clipping_rect().size
container_clip_rect.left += self.ui_container.get_image_clipping_rect().left
container_clip_rect.top += self.ui_container.get_image_clipping_rect().top
if self.drawable_shape is not None:
return (self.drawable_shape.collide_point((hover_x, hover_y)) and
bool(container_clip_rect.collidepoint(hover_x, hover_y)))
return (bool(self.rect.collidepoint(hover_x, hover_y)) and
bool(container_clip_rect.collidepoint(hover_x, hover_y)))
# pylint: disable=unused-argument,no-self-use
def process_event(self, event: pygame.event.Event) -> bool:
"""
A stub to override. Gives UI Elements access to pygame events.
:param event: The event to process.
:return: Should return True if this element makes use of this event.
"""
return False
def focus(self):
"""
A stub to override. Called when we focus this UI element.
"""
self.is_focused = True
def unfocus(self):
"""
A stub to override. Called when we stop focusing this UI element.
"""
self.is_focused = False
def rebuild_from_changed_theme_data(self):
"""
A stub to override when we want to rebuild from theme data.
"""
# self.combined_element_ids = self.ui_theme.build_all_combined_ids(self.element_ids,
# self.object_ids)
def rebuild(self):
"""
Takes care of rebuilding this element. Most derived elements are going to override this,
and hopefully call the super() class method.
"""
if self._visual_debug_mode:
self.set_image(self.pre_debug_image)
self.pre_debug_image = None
def set_visual_debug_mode(self, activate_mode: bool):
"""
Enables a debug mode for the element which displays layer information on top of it in
a tiny font.
:param activate_mode: True or False to enable or disable the mode.
"""
if activate_mode:
default_font = self.ui_manager.get_theme().get_font_dictionary().get_default_font()
layer_text_render = render_white_text_alpha_black_bg(default_font,
"UI Layer: " + str(self._layer))
if self.image is not None:
self.pre_debug_image = self.image.copy()
# check if our surface is big enough to hold the debug info,
# if not make a new, bigger copy
make_new_larger_surface = False
surf_width = self.image.get_width()
surf_height = self.image.get_height()
if self.image.get_width() < layer_text_render.get_width():
make_new_larger_surface = True
surf_width = layer_text_render.get_width()
if self.image.get_height() < layer_text_render.get_height():
make_new_larger_surface = True
surf_height = layer_text_render.get_height()
if make_new_larger_surface:
new_surface = pygame.surface.Surface((surf_width, surf_height),
flags=pygame.SRCALPHA,
depth=32)
basic_blit(new_surface, self.image, (0, 0))
self.set_image(new_surface)
basic_blit(self.image, layer_text_render, (0, 0))
else:
self.set_image(layer_text_render)
self._visual_debug_mode = True
else:
self.rebuild()
self._visual_debug_mode = False
def _clip_images_for_container(self, clip_rect: Union[pygame.Rect, None]):
"""
Set the current image clip based on the container.
:param clip_rect: The clipping rectangle.
"""
self.set_image_clip(clip_rect)
def _restore_container_clipped_images(self):
"""
Clear the image clip.
"""
self.set_image_clip(None)
def set_image_clip(self, rect: Union[pygame.Rect, None]):
"""
Sets a clipping rectangle on this element's image determining what portion of it will
actually be displayed when this element is blitted to the screen.
:param rect: A clipping rectangle, or None to clear the clip.
"""
if rect is not None:
if rect.width < 0:
rect.width = 0
if rect.height < 0:
rect.height = 0
if self._pre_clipped_image is None and self.image is not None:
self._pre_clipped_image = self.image.copy()
self._image_clip = rect
if self.image is not None:
self.image.fill(pygame.Color('#00000000'))
basic_blit(self.image, self._pre_clipped_image, self._image_clip, self._image_clip)
elif self._image_clip is not None:
self._image_clip = None
self.set_image(self._pre_clipped_image)
else:
self._image_clip = None
def get_image_clipping_rect(self) -> Union[pygame.Rect, None]:
"""
Obtain the current image clipping rect.
:return: The current clipping rect. May be None.
"""
return self._image_clip
def set_image(self, new_image: Union[pygame.surface.Surface, None]):
"""
Wraps setting the image variable of this element so that we also set the current image
clip on the image at the same time.
:param new_image: The new image to set.
"""
if self.get_image_clipping_rect() is not None and new_image is not None:
self._pre_clipped_image = new_image
if (self.get_image_clipping_rect().width == 0 and
self.get_image_clipping_rect().height == 0):
self.image = self.ui_manager.get_universal_empty_surface()
else:
self.image = pygame.surface.Surface(self._pre_clipped_image.get_size(),
flags=pygame.SRCALPHA,
depth=32)
self.image.fill(pygame.Color('#00000000'))
basic_blit(self.image,
self._pre_clipped_image,
self.get_image_clipping_rect(),
self.get_image_clipping_rect())
else:
self.image = new_image.copy() if new_image is not None else None
self._pre_clipped_image = None
def get_top_layer(self) -> int:
"""
Assuming we have correctly calculated the 'thickness' of it, this method will
return the top of this element.
:return int: An integer representing the current highest layer being used by this element.
"""
return self._layer + self.layer_thickness
def get_starting_height(self) -> int:
"""
Get the starting layer height of this element. (i.e. the layer we start placing it on
*above* it's container, it may use more layers above this layer)
:return: an integer representing the starting layer height.
"""
return self.starting_height
def _check_shape_theming_changed(self, defaults: Dict[str, Any]) -> bool:
"""
Checks all the standard miscellaneous shape theming parameters.
:param defaults: A dictionary of default values
:return: True if any have changed.
"""
has_any_changed = False
if self._check_misc_theme_data_changed('border_width', defaults['border_width'], int):
has_any_changed = True
if self._check_misc_theme_data_changed('shadow_width', defaults['shadow_width'], int):
has_any_changed = True
if self._check_misc_theme_data_changed('shape_corner_radius',
defaults['shape_corner_radius'], int):
has_any_changed = True
return has_any_changed
def _check_misc_theme_data_changed(self,
attribute_name: str,
default_value: Any,
casting_func: Callable[[Any], Any],
allowed_values: Union[List, None] = None) -> bool:
"""
Checks if the value of a pieces of miscellaneous theming data has changed, and if it has,
updates the corresponding attribute on the element and returns True.
:param attribute_name: The name of the attribute.
:param default_value: The default value for the attribute.
:param casting_func: The function to cast to the type of the data.
:return: True if the attribute has changed.
"""
has_changed = False
attribute_value = default_value
try:
attribute_value = casting_func(
self.ui_theme.get_misc_data(attribute_name, self.combined_element_ids))
except (LookupError, ValueError):
attribute_value = default_value
finally:
if allowed_values and attribute_value not in allowed_values:
attribute_value = default_value
if attribute_value != getattr(self, attribute_name, default_value):
setattr(self, attribute_name, attribute_value)
has_changed = True
return has_changed
def disable(self):
"""
Disables elements so they are no longer interactive.
This is just a default fallback implementation for elements that don't define their own.
Elements should handle their own enabling and disabling.
"""
self.is_enabled = False
def enable(self):
"""
Enables elements so they are interactive again.
This is just a default fallback implementation for elements that don't define their own.
Elements should handle their own enabling and disabling.
"""
self.is_enabled = True
def show(self):
"""
Shows the widget, which means the widget will get drawn and will process events.
"""
self.visible = 1
def hide(self):
"""
Hides the widget, which means the widget will not get drawn and will not process events.
Clear hovered state.
"""
self.visible = 0
self.hovered = False
self.hover_time = 0.0