test-kivy-app/kivy_venv/lib/python3.11/site-packages/kivy/uix/layout.py
2024-09-15 15:12:16 +03:00

323 lines
13 KiB
Python

'''
Layout
======
Layouts are used to calculate and assign widget positions.
The :class:`Layout` class itself cannot be used directly.
You should use one of the following layout classes:
- Anchor layout: :class:`kivy.uix.anchorlayout.AnchorLayout`
- Box layout: :class:`kivy.uix.boxlayout.BoxLayout`
- Float layout: :class:`kivy.uix.floatlayout.FloatLayout`
- Grid layout: :class:`kivy.uix.gridlayout.GridLayout`
- Page Layout: :class:`kivy.uix.pagelayout.PageLayout`
- Relative layout: :class:`kivy.uix.relativelayout.RelativeLayout`
- Scatter layout: :class:`kivy.uix.scatterlayout.ScatterLayout`
- Stack layout: :class:`kivy.uix.stacklayout.StackLayout`
Understanding the `size_hint` Property in `Widget`
--------------------------------------------------
The :attr:`~kivy.uix.Widget.size_hint` is a tuple of values used by
layouts to manage the sizes of their children. It indicates the size
relative to the layout's size instead of an absolute size (in
pixels/points/cm/etc). The format is::
widget.size_hint = (width_proportion, height_proportion)
The proportions are specified as floating point numbers in the range 0-1. For
example, 0.5 represents 50%, 1 represents 100%.
If you want a widget's width to be half of the parent's width and the
height to be identical to the parent's height, you would do::
widget.size_hint = (0.5, 1.0)
If you don't want to use a size_hint for either the width or height, set the
value to None. For example, to make a widget that is 250px wide and 30%
of the parent's height, do::
widget.size_hint = (None, 0.3)
widget.width = 250
Being :class:`Kivy properties <kivy.properties>`, these can also be set via
constructor arguments::
widget = Widget(size_hint=(None, 0.3), width=250)
.. versionchanged:: 1.4.1
The `reposition_child` internal method (made public by mistake) has
been removed.
'''
__all__ = ('Layout', )
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.compat import isclose
class Layout(Widget):
'''Layout interface class, used to implement every layout. See module
documentation for more information.
'''
_trigger_layout = None
def __init__(self, **kwargs):
if self.__class__ == Layout:
raise Exception('The Layout class is abstract and \
cannot be used directly.')
if self._trigger_layout is None:
self._trigger_layout = Clock.create_trigger(self.do_layout, -1)
super(Layout, self).__init__(**kwargs)
def do_layout(self, *largs):
'''This function is called when a layout is called by a trigger.
If you are writing a new Layout subclass, don't call this function
directly but use :meth:`_trigger_layout` instead.
The function is by default called *before* the next frame, therefore
the layout isn't updated immediately. Anything depending on the
positions of e.g. children should be scheduled for the next frame.
.. versionadded:: 1.0.8
'''
raise NotImplementedError('Must be implemented in subclasses.')
def add_widget(self, widget, *args, **kwargs):
fbind = widget.fbind
fbind('size', self._trigger_layout)
fbind('size_hint', self._trigger_layout)
fbind('size_hint_max', self._trigger_layout)
fbind('size_hint_min', self._trigger_layout)
super(Layout, self).add_widget(widget, *args, **kwargs)
def remove_widget(self, widget, *args, **kwargs):
funbind = widget.funbind
funbind('size', self._trigger_layout)
funbind('size_hint', self._trigger_layout)
funbind('size_hint_max', self._trigger_layout)
funbind('size_hint_min', self._trigger_layout)
super(Layout, self).remove_widget(widget, *args, **kwargs)
def layout_hint_with_bounds(
self, sh_sum, available_space, min_bounded_size, sh_min_vals,
sh_max_vals, hint):
'''(internal) Computes the appropriate (size) hint for all the
widgets given (potential) min or max bounds on the widgets' size.
The ``hint`` list is updated with appropriate sizes.
It walks through the hints and for any widgets whose hint will result
in violating min or max constraints, it fixes the hint. Any remaining
or missing space after all the widgets are fixed get distributed
to the widgets making them smaller or larger according to their
size hint.
This algorithms knows nothing about the widgets other than what is
passed through the input params, so it's fairly generic for laying
things out according to constraints using size hints.
:Parameters:
`sh_sum`: float
The sum of the size hints (basically ``sum(size_hint)``).
`available_space`: float
The amount of pixels available for all the widgets
whose size hint is not None. Cannot be zero.
`min_bounded_size`: float
The minimum amount of space required according to the
`size_hint_min` of the widgets (basically
``sum(size_hint_min)``).
`sh_min_vals`: list or iterable
Items in the iterable are the size_hint_min for each widget.
Can be None. The length should be the same as ``hint``
`sh_max_vals`: list or iterable
Items in the iterable are the size_hint_max for each widget.
Can be None. The length should be the same as ``hint``
`hint`: list
A list whose size is the same as the length of ``sh_min_vals``
and ``sh_min_vals`` whose each element is the corresponding
size hint value of that element. This list is updated in place
with correct size hints that ensure the constraints are not
violated.
:returns:
Nothing. ``hint`` is updated in place.
'''
if not sh_sum:
return
# TODO: test when children have size_hint, max/min of zero
# all divs are float denominator ;)
stretch_ratio = sh_sum / float(available_space)
if available_space <= min_bounded_size or \
isclose(available_space, min_bounded_size):
# too small, just set to min
for i, (sh, sh_min) in enumerate(zip(hint, sh_min_vals)):
if sh is None:
continue
if sh_min is not None:
hint[i] = sh_min * stretch_ratio # set to min size
else:
hint[i] = 0. # everything else is zero
return
# these dicts take i (widget child) as key
not_mined_contrib = {} # all who's sh > min_sh or had no min_sh
not_maxed_contrib = {} # all who's sh < max_sh or had no max_sh
sh_mins_avail = {} # the sh amt removable until we hit sh_min
sh_maxs_avail = {} # the sh amt addable until we hit sh_max
oversize_amt = undersize_amt = 0
hint_orig = hint[:]
# first, for all the items, set them to be within their max/min
# size_hint bound, also find how much their size_hint can be reduced
# or increased
for i, (sh, sh_min, sh_max) in enumerate(
zip(hint, sh_min_vals, sh_max_vals)):
if sh is None:
continue
diff = 0
if sh_min is not None:
sh_min *= stretch_ratio
diff = sh_min - sh # how much we are under the min
if diff > 0:
hint[i] = sh_min
undersize_amt += diff
else:
not_mined_contrib[i] = None
sh_mins_avail[i] = hint[i] - sh_min
else:
not_mined_contrib[i] = None
sh_mins_avail[i] = hint[i]
if sh_max is not None:
sh_max *= stretch_ratio
diff = sh - sh_max
if diff > 0:
hint[i] = sh_max # how much we are over the max
oversize_amt += diff
else:
not_maxed_contrib[i] = None
sh_maxs_avail[i] = sh_max - hint[i]
else:
not_maxed_contrib[i] = None
sh_maxs_avail[i] = sh_sum - hint[i]
if i in not_mined_contrib:
not_mined_contrib[i] = max(0., diff) # how much got removed
if i in not_maxed_contrib:
not_maxed_contrib[i] = max(0., diff) # how much got added
# if margin is zero, the amount of the widgets that were made smaller
# magically equals the amount of the widgets that were made larger
# so we're all good
margin = oversize_amt - undersize_amt
if isclose(oversize_amt, undersize_amt, abs_tol=1e-15):
return
# we need to redistribute the margin among all widgets
# if margin is positive, then we have extra space because the widgets
# that were larger and were reduced contributed more, so increase
# the size hint for those that are allowed to be larger by the
# most allowed, proportionately to their size (or inverse size hint).
# similarly for the opposite case
if margin > 1e-15:
contrib_amt = not_maxed_contrib
sh_available = sh_maxs_avail
mult = 1.
contrib_proportion = hint_orig
elif margin < -1e-15:
margin *= -1.
contrib_amt = not_mined_contrib
sh_available = sh_mins_avail
mult = -1.
# when reducing the size of widgets proportionately, those with
# larger sh get reduced less, and those with smaller, more.
mn = min((h for h in hint_orig if h))
mx = max((h for h in hint_orig if h is not None))
hint_top = (2. * mn if mn else 1.) if mn == mx else mn + mx
contrib_proportion = [None if h is None else hint_top - h for
h in hint_orig]
# contrib_amt is all the widgets that are not their max/min and
# can afford to be made bigger/smaller
# We only use the contrib_amt indices from now on
contrib_prop_sum = float(
sum((contrib_proportion[i] for i in contrib_amt)))
if contrib_prop_sum < 1e-9:
assert mult == 1. # should only happen when all sh are zero
return
contrib_height = {
i: val / (contrib_proportion[i] / contrib_prop_sum) for
i, val in contrib_amt.items()}
items = sorted(
(i for i in contrib_amt),
key=lambda x: contrib_height[x])
j = items[0]
sum_i_contributed = contrib_amt[j]
last_height = contrib_height[j]
sh_available_i = {j: sh_available[j]}
contrib_prop_sum_i = contrib_proportion[j]
n = len(items) # check when n <= 1
i = 1
if 1 < n:
j = items[1]
curr_height = contrib_height[j]
done = False
while not done and i < n:
while i < n and last_height == curr_height:
j = items[i]
sum_i_contributed += contrib_amt[j]
contrib_prop_sum_i += contrib_proportion[j]
sh_available_i[j] = sh_available[j]
curr_height = contrib_height[j]
i += 1
last_height = curr_height
while not done:
margin_height = ((margin + sum_i_contributed) /
(contrib_prop_sum_i / contrib_prop_sum))
if margin_height - curr_height > 1e-9 and i < n:
break
done = True
for k, available_sh in list(sh_available_i.items()):
if margin_height - available_sh / (
contrib_proportion[k] / contrib_prop_sum) > 1e-9:
del sh_available_i[k]
sum_i_contributed -= contrib_amt[k]
contrib_prop_sum_i -= contrib_proportion[k]
margin -= available_sh
hint[k] += mult * available_sh
done = False
if not sh_available_i: # all were under the margin
break
if sh_available_i:
assert contrib_prop_sum_i and margin
margin_height = ((margin + sum_i_contributed) /
(contrib_prop_sum_i / contrib_prop_sum))
for i in sh_available_i:
hint[i] += mult * (
margin_height * contrib_proportion[i] / contrib_prop_sum -
contrib_amt[i])