11from enum import Enum
2+ from typing import Optional , Tuple , List
23import copy
34# //utility for dumping layout
45
@@ -12,17 +13,17 @@ class SizePolicies(Enum):
1213 GROW = "grow"
1314
1415class SizePolicy :
15- def __init__ (self , mode , value = None ):
16- self .mode = mode
17- self .value = value
18- def __repr__ (self ):
19- return f"{ self .mode } ({ self .value } )" if self .value is not None else self .mode
16+ def __init__ (self , size_policy : SizePolicies , value : Optional [ float ] = None ):
17+ self .size_policy = size_policy
18+ self .value : Optional [ int ] = value
19+ def __repr__ (self ) -> str :
20+ return f"{ self .size_policy . value } ({ self .value } )" if self .value is not None else self .size_policy . value
2021
21- def FIXED (value ):
22+ def FIXED (value ) -> SizePolicy :
2223 return SizePolicy (SizePolicies .FIXED , value )
23- def FIT ():
24+ def FIT () -> SizePolicy :
2425 return SizePolicy (SizePolicies .FIT , 0 )
25- def GROW ():
26+ def GROW () -> SizePolicy :
2627 return SizePolicy (SizePolicies .GROW , 0 )
2728
2829class Padding :
@@ -34,27 +35,33 @@ def __init__(self, top=0, bottom=0, left=0, right=0):
3435
3536
3637class Layout :
37- def __init__ (self , backgroundColor = "white" , sizing = (FIT (), FIT ()), padding = Padding (0 , 0 , 0 , 0 ), direction = Direction .ROW , child_gap = 0 ):
38- self .sizing = sizing
39- self .backgroundColor = backgroundColor
40- self .direction = direction
41- self .child_gap = child_gap
42- self .padding = padding
43- self .children = []
38+ def __init__ (
39+ self ,
40+ sizing : Tuple [SizePolicy , SizePolicy ] = (FIT (), FIT ()),
41+ padding : Padding = Padding (0 , 0 , 0 , 0 ),
42+ direction : Direction = Direction .ROW ,
43+ child_gap : float = 0 ,
44+ color : Optional [str ] = None ):
45+ self .sizing : Tuple [SizePolicy , SizePolicy ] = sizing
46+ self .direction : Direction = direction
47+ self .child_gap : float = child_gap
48+ self .padding : Padding = padding
49+ self .color : Optional [str ] = color
50+ self .children : List ['Layout' ] = []
4451 self .x = 0
4552 self .y = 0
46- self .width = sizing [0 ].value if sizing [0 ].mode == SizePolicies .FIXED else 0
47- self .height = sizing [1 ].value if sizing [1 ].mode == SizePolicies .FIXED else 0
53+ self .width = sizing [0 ].value if sizing [0 ].size_policy == SizePolicies .FIXED else 0
54+ self .height = sizing [1 ].value if sizing [1 ].size_policy == SizePolicies .FIXED else 0
4855
49- def add_child (self , child ) :
50- self .children .append (child )
56+ def add_child (self , child : 'Layout' ) -> None :
57+ self .children .append (copy . deepcopy ( child ) )
5158
52- def _inner_dims (self ):
59+ def _inner_dims (self ) -> Tuple [ float , float ] :
5360 iw = max (0 , self .width - self .padding .left - self .padding .right )
5461 ih = max (0 , self .height - self .padding .top - self .padding .bottom )
5562 return iw , ih
5663
57- def child_size (self ,logger = None ):
64+ def child_size (self ,logger : Optional [ 'LayoutLogger' ] = None ) -> None :
5865 # Always size children — even if self has fixed size
5966 inner_available_width = self .width - self .padding .left - self .padding .right
6067 inner_available_height = self .height - self .padding .top - self .padding .bottom
@@ -63,17 +70,17 @@ def child_size(self,logger=None):
6370 child_width_policy , child_height_policy = child .sizing
6471 # Only pass constraints if needed
6572 child_width = (
66- inner_available_width if child_width_policy .mode != SizePolicies .FIXED else child_width_policy .value
73+ inner_available_width if child_width_policy .size_policy != SizePolicies .FIXED else child_width_policy .value
6774 )
6875 child_height = (
69- inner_available_height if child_height_policy .mode != SizePolicies .FIXED else child_height_policy .value
76+ inner_available_height if child_height_policy .size_policy != SizePolicies .FIXED else child_height_policy .value
7077 )
7178 child .compute_size (child_width , child_height ,logger )
7279
7380
7481 # Sizing
7582
76- def compute_size (self , available_width = None , available_height = None , logger = None ):
83+ def compute_size (self , available_width = None , available_height = None , logger : Optional [ 'LayoutLogger' ] = None ) -> None :
7784 if logger : logger .snapshot (self , "before_compute" )
7885 if self .direction == Direction .ROW :
7986 self ._compute_width ()
@@ -91,25 +98,21 @@ def compute_size(self, available_width=None, available_height=None, logger=None)
9198 self ._compute_grow_width ()
9299 if logger : logger .snapshot (self , "after_compute" )
93100
94- def _compute_width (self ):
101+ def _compute_width (self ) -> None :
95102 width_policy , _ = self .sizing
96- if width_policy .mode == SizePolicies .FIXED :
103+ if width_policy .size_policy == SizePolicies .FIXED :
97104 self .width = width_policy .value
98- elif width_policy .mode == SizePolicies .FIT :
99- self .width = self ._compute_fit_width ()
100- # elif width_policy.mode == SizePolicies.GROW:
101- # self.width = 0
105+ elif width_policy .size_policy == SizePolicies .FIT :
106+ self .width = self ._compute_fit_width ()
102107
103- def _compute_height (self ):
108+ def _compute_height (self ) -> None :
104109 _ , height_policy = self .sizing
105- if height_policy .mode == SizePolicies .FIXED :
110+ if height_policy .size_policy == SizePolicies .FIXED :
106111 self .height = height_policy .value
107- elif height_policy .mode == SizePolicies .FIT :
112+ elif height_policy .size_policy == SizePolicies .FIT :
108113 self .height = self ._compute_fit_height ()
109- # elif height_policy.mode == SizePolicies.GROW:
110- # self.height = 0
111114
112- def _compute_fit_width (self ):
115+ def _compute_fit_width (self ) -> float :
113116 self .child_size ()
114117 if self .direction == Direction .ROW :
115118 content_width = sum (c .width for c in self .children )
@@ -120,7 +123,7 @@ def _compute_fit_width(self):
120123
121124 return content_width + self .padding .left + self .padding .right
122125
123- def _compute_fit_height (self ):
126+ def _compute_fit_height (self ) -> float :
124127 self .child_size ()
125128 if self .direction == Direction .COLUMN :
126129 content_height = sum (c .height for c in self .children )
@@ -131,10 +134,10 @@ def _compute_fit_height(self):
131134
132135 return content_height + self .padding .top + self .padding .bottom
133136
134- def _compute_grow_width (self ):
137+ def _compute_grow_width (self ) -> None :
135138 if self .direction == Direction .ROW :
136139 # Horizontal GROW
137- total_fixed_width = sum (child .width for child in self .children if child .sizing [0 ].mode != SizePolicies .GROW )
140+ total_fixed_width = sum (child .width for child in self .children if child .sizing [0 ].size_policy != SizePolicies .GROW )
138141 total_gap = (len (self .children ) - 1 ) * self .child_gap
139142 remaining_width = self .width - self .padding .left - self .padding .right - total_fixed_width - total_gap
140143 self ._grow_children_evenly (self .children , remaining_width , axis = 0 )
@@ -143,26 +146,26 @@ def _compute_grow_width(self):
143146 # cross-axis GROW
144147 remaining_width = self .width - self .padding .left - self .padding .right
145148 for child in self .children :
146- if child .sizing [0 ].mode == SizePolicies .GROW :
149+ if child .sizing [0 ].size_policy == SizePolicies .GROW :
147150 child .width = remaining_width
148151
149- def _compute_grow_height (self ):
152+ def _compute_grow_height (self ) -> None :
150153 if self .direction == Direction .ROW :
151154 # cross-axis GROW
152155 remaining_height = self .height - self .padding .top - self .padding .bottom
153156 for child in self .children :
154- if child .sizing [1 ].mode == SizePolicies .GROW :
157+ if child .sizing [1 ].size_policy == SizePolicies .GROW :
155158 child .height = remaining_height
156159
157160 if self .direction == Direction .COLUMN :
158161 # Vertical GROW
159- total_fixed_height = sum (child .height for child in self .children if child .sizing [1 ].mode != SizePolicies .GROW )
162+ total_fixed_height = sum (child .height for child in self .children if child .sizing [1 ].size_policy != SizePolicies .GROW )
160163 total_gap = self .child_gap * (len (self .children ) - 1 )
161164 remaining_height = self .height - self .padding .top - self .padding .bottom - total_fixed_height - total_gap
162165 self ._grow_children_evenly (self .children , remaining_height , axis = 1 )
163166
164- def _grow_children_evenly (self , children , remaining , axis ):
165- growable = [c for c in children if c .sizing [axis ].mode == SizePolicies .GROW ]
167+ def _grow_children_evenly (self , children : List [ 'Layout' ] , remaining : float , axis : int ):
168+ growable = [c for c in children if c .sizing [axis ].size_policy == SizePolicies .GROW ]
166169 while growable and remaining > 0 :
167170 growable .sort (key = lambda c : c .width if axis == 0 else c .height )
168171 smallest = growable [0 ]
@@ -192,7 +195,7 @@ def _grow_children_evenly(self, children, remaining, axis):
192195 c .height += grow_amount
193196 remaining -= grow_amount
194197 growable = [c for c in growable if (c .width if axis == 0 else c .height ) < second_smallest ]
195- shrinkable = [c for c in children if c .sizing [axis ].mode != SizePolicies .FIXED ]
198+ shrinkable = [c for c in children if c .sizing [axis ].size_policy != SizePolicies .FIXED ]
196199 while shrinkable and remaining < 0 :
197200 shrinkable .sort (key = lambda c : c .width if axis == 0 else c .height , reverse = True )
198201 largest = shrinkable [0 ]
@@ -227,7 +230,7 @@ def _grow_children_evenly(self, children, remaining, axis):
227230 shrinkable = [c for c in shrinkable if (c .width if axis == 0 else c .height ) > second_largest_val ]
228231
229232 # Position
230- def layout (self , x = 0 , y = 0 , logger = None ):
233+ def layout (self , x : int = 0 , y : int = 0 , logger : Optional [ 'LayoutLogger' ] = None ) -> None :
231234 self .x = x
232235 self .y = y
233236 if logger : logger .snapshot (self , "before_layout" )
@@ -237,22 +240,38 @@ def layout(self, x=0, y=0, logger=None):
237240 self ._layout_column_children ()
238241 if logger : logger .snapshot (self , "after_layout" )
239242
240- def _layout_row_children (self ):
243+ def _layout_row_children (self ) -> None :
241244 left_offset = self .padding .left
242245 for child in self .children :
243246 child_pos_x = self .x + left_offset
244247 child_pos_y = self .y + self .padding .top
245248 child .layout (child_pos_x , child_pos_y )
246249 left_offset += child .width + self .child_gap
247250
248- def _layout_column_children (self ):
251+ def _layout_column_children (self ) -> None :
249252 top_offset = self .padding .top
250253 for child in self .children :
251254 child_pos_x = self .x + self .padding .left
252255 child_pos_y = self .y + top_offset
253256 child .layout (child_pos_x , child_pos_y )
254257 top_offset += child .height + self .child_gap
255258
259+ def to_dot (self , dot : 'Diagraph' , logger : Optional ['LayoutLogger' ] = None ):
260+ """Generate Graphviz DOT representation for this node and its children."""
261+ from graphviz import Digraph
262+ nid = str (logger ._attach_id (self ) if logger else id (self ))
263+ label = f'{ self .__class__ .__name__ } #{ nid } \n '
264+ label += f'{ self .sizing [0 ].size_policy .value } ×{ self .sizing [1 ].size_policy .value } \n '
265+ label += f'dir={ self .direction .value if self .direction else "-" } \n '
266+ label += f'size=({ self .width } ×{ self .height } )\n '
267+ label += f'pos=({ self .x } ,{ self .y } )'
268+ color = self .color if self .color else "#dddddd"
269+ dot .node (nid , label , style = "filled" , fillcolor = color )
270+ for child in self .children :
271+ child_nid = child .to_dot (dot , logger )
272+ dot .edge (nid , child_nid )
273+ return nid
274+
256275
257276
258277
0 commit comments