"""
inner/outer elliptic/square reliefs for any kivy widget
=======================================================
the :class:`ReliefCanvas` mixin class of this ae namespace portion can be added to any square or elliptic Kivy widget
to draw an inner and/or outer relief, in order to convert your widget to have an outstanding or sunken 3D-appearance.
"""
from typing import Any, Callable, Tuple, Union
from kivy.graphics import Color, Line # type: ignore
from kivy.factory import Factory # type: ignore
# pylint: disable=no-name-in-module
from kivy.graphics.instructions import InstructionGroup # type: ignore # noqa: E0611
from kivy.properties import ListProperty, NumericProperty, ObjectProperty # type: ignore # noqa: E0611
from ae.gui_app import ColorRGB, ColorOrInk # type: ignore
__version__ = '0.3.10'
ANGLE_BEG = 87
ANGLE_END = 267
ReliefColors = Union[Tuple[ColorRGB, ColorRGB], Tuple] #: tuple of (top, bottom) relief colors or empty tuple
ReliefBrightness = Tuple[float, float] #: top and bottom brightness/darken factor
[docs]def relief_colors(color_or_ink: ColorOrInk = (0, 0, 0), darken_factors: ReliefBrightness = (0.6, 0.3)) -> ReliefColors:
""" calculate the (top and bottom) colors used for the relief lines/drawings.
:param color_or_ink: color used to calculate the relief colors from, which will first be lightened
until one of the color parts (R, G or B) reach the value 1.0; then
:paramref:`.darken factors` will be applied to the color parts. If not passed then grey
colors will be returned.
.. note::
if the alpha value of paramref:`~relief_colors.color_or_ink` is zero then no relief
colors will be calculated and an empty tuple will be returned (disabling relief).
:param darken_factors: two factors to darken (1) the top and (2) the bottom relief color parts.
:return: tuple with darkened colors calculated from :paramref:`.color_or_ink` and
:paramref:`.darken_factors` or an empty tuple if the alpha value of
:paramref:`.color_or_ink` has a zero value.
"""
if len(color_or_ink) > 3 and not color_or_ink[3]:
return ()
max_col_part = max(color_or_ink[:3])
if max_col_part == 0: # prevent zero division if color_or_ink is black/default
lightened_color = [1.0, 1.0, 1.0]
else:
brighten_factor = 1 / max_col_part
lightened_color = [(col * brighten_factor) for col in color_or_ink[:3]]
return tuple([col_part * darken for col_part in lightened_color] for darken in darken_factors)
[docs]class ReliefCanvas: # pragma: no cover
""" relief canvas mixin class.
to activate the drawing of a relief you have to specify two colors, one for the top part and another one
for the bottom part of the relief, which are both stored in a single kivy property. the function
:func:`relief_colors` can be used to calculate lightened and darkened values of the surface color of the widget::
MySquareRaisedWidgetWithColoredSurface:
surface_color: 0.9, 0.6, 0.3, 1.0
relief_square_outer_colors: relief_colors(color_or_ink=self.surface_color)
this will result in a raised widget with a square outer relief where the top/left relief color get a lightened
value and the bottom/right relief a darkened value of the color specified by `surface_color`.
using the default values will result in raised widgets with the inner part sunken, simulating the light source in
the top left window border/corner. to make a sunken widget for the same light source position you simply have
to flip the items of the :paramref:`~relief_colors.darken_factors` argument of the :func:`relief_colors` function.
the following example shows this for a round/elliptic button widget::
MyRoundSunkenButton:
relief_ellipse_outer_colors: relief_colors(darken_factors=(0.3, 0.6))
the other color attributes of this mixin class control the relief colors for the inner part of a square shaped
widget (:attr:`.relief_square_inner_colors`) and for the inner part of an elliptic shape widget
(:attr:`.relief_ellipse_inner_colors`).
the depth of the outer raise/sunk effect can be controlled with the :attr:`.relief_square_outer_lines`
property/attribute. :attr:`.relief_square_inner_lines` controls the raise/sunk depth of the inner surface of a
square widget. :attr:`.relief_ellipse_inner_lines` and :attr:`.relief_ellipse_outer_lines` are doing the same for
widgets with a round/elliptic shape.
the properties :attr:`.relief_square_inner_offset` and :attr:`.relief_ellipse_inner_offset` are specifying the width
of the widget border (the part between the outer and the inner relief) in pixels.
.. note::
at least one of the classes that is mixing in this class has to inherit from Widget (or EventDispatcher) to get
the widgets `pos`, `size`, `canvas` properties and the `bind` method.
"""
relief_pos_size = ListProperty([])
""" list/tuple of optional relief position and size (x, y, width, height) in pixels.
if not specified or empty list/tuple, than the pos/size values of the mixing-in widget will be used instead.
:attr:`relief_pos_size` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list.
"""
relief_ellipse_inner_colors: ReliefColors = ObjectProperty(())
""" list/tuple of ellipse inner (top, bottom) rgb colors.
:attr:`relief_ellipse_inner_colors` is a :class:`~kivy.properties.ObjectProperty` and defaults to an empty tuple.
"""
relief_ellipse_inner_lines = NumericProperty('3sp')
""" number of ellipse inner lines/pixels to be drawn.
:attr:`relief_ellipse_inner_lines` is a :class:`~kivy.properties.NumericProperty` and defaults to '3sp'.
"""
relief_ellipse_inner_offset = NumericProperty('1sp')
""" number of pixels left unchanged at the border of the inner elliptic surface before the inner relief starts.
:attr:`relief_ellipse_inner_offset` is a :class:`~kivy.properties.NumericProperty` and defaults to '1sp'.
"""
relief_ellipse_outer_colors: ReliefColors = ObjectProperty(())
""" list/tuple of ellipse outer (top, bottom) rgb colors.
:attr:`relief_ellipse_outer_colors` is a :class:`~kivy.properties.ObjectProperty` and defaults to an empty tuple.
"""
relief_ellipse_outer_lines = NumericProperty('3sp')
""" number of ellipse outer lines/pixels to be drawn.
:attr:`relief_ellipse_outer_lines` is a :class:`~kivy.properties.NumericProperty` and defaults to '3sp'.
"""
relief_square_inner_colors = ObjectProperty(())
""" list/tuple of square inner (top, bottom) rgb colors.
:attr:`relief_square_inner_colors` is a :class:`~kivy.properties.ObjectProperty` and defaults to an empty tuple.
"""
relief_square_inner_lines = NumericProperty('3sp')
""" number of square inner lines/pixels to be drawn.
:attr:`relief_square_inner_lines` is a :class:`~kivy.properties.NumericProperty` and defaults to '3sp'.
"""
relief_square_inner_offset = NumericProperty('1sp')
""" number of pixels left unchanged at the border of the square inner surface before the inner relief starts.
:attr:`relief_square_inner_offset` is a :class:`~kivy.properties.NumericProperty` and defaults to '1sp'.
"""
relief_square_outer_colors: ReliefColors = ObjectProperty(())
""" list/tuple of square outer (top, bottom) rgb colors.
:attr:`relief_square_outer_colors` is a :class:`~kivy.properties.ObjectProperty` and defaults to an empty tuple.
"""
relief_square_outer_lines: NumericProperty = NumericProperty('3sp')
""" number of square outer lines/pixels to be drawn.
:attr:`relief_square_outer_lines` is a :class:`~kivy.properties.NumericProperty` and defaults to '3sp'.
"""
# attributes provided by the class to be mixed into
bind: Any
canvas: Any
pos: list
size: list
[docs] def __init__(self, **kwargs):
super().__init__(**kwargs)
bind_func, bound_func = self.bind, self._relief_refresh
bind_func(pos=bound_func)
bind_func(size=bound_func)
bind_func(relief_pos_size=bound_func)
bind_func(relief_ellipse_inner_colors=bound_func)
bind_func(relief_ellipse_inner_lines=bound_func)
bind_func(relief_ellipse_inner_offset=bound_func)
bind_func(relief_ellipse_outer_colors=bound_func)
bind_func(relief_ellipse_outer_lines=bound_func)
bind_func(relief_square_inner_colors=bound_func)
bind_func(relief_square_inner_lines=bound_func)
bind_func(relief_square_inner_offset=bound_func)
bind_func(relief_square_outer_colors=bound_func)
bind_func(relief_square_outer_lines=bound_func)
self._relief_graphic_instructions = InstructionGroup()
[docs] def _relief_refresh(self, *_args):
""" pos/size or color changed event handler. """
if self._relief_graphic_instructions.length():
self.canvas.after.remove(self._relief_graphic_instructions)
self._relief_graphic_instructions.clear()
add = self._relief_graphic_instructions.add
pos_size = self.relief_pos_size or (*self.pos, *self.size)
if self.relief_ellipse_inner_colors and self.relief_ellipse_inner_lines:
self._relief_ellipse_inner_refresh(add, *self.relief_ellipse_inner_colors, *pos_size)
if self.relief_ellipse_outer_colors and self.relief_ellipse_outer_lines:
self._relief_ellipse_outer_refresh(add, *self.relief_ellipse_outer_colors, *pos_size)
if self.relief_square_inner_colors and self.relief_square_inner_lines:
self._relief_square_inner_refresh(add, *self.relief_square_inner_colors, *pos_size)
if self.relief_square_outer_colors and self.relief_square_outer_lines:
self._relief_square_outer_refresh(add, *self.relief_square_outer_colors, *pos_size)
if self._relief_graphic_instructions.length():
self.canvas.after.add(self._relief_graphic_instructions)
[docs] def _relief_ellipse_inner_refresh(self, add_instruction: Callable,
top_color: ColorRGB, bottom_color: ColorRGB,
wid_x: float, wid_y: float, wid_width: float, wid_height: float):
""" ellipse pos/size or color changed event handler. """
lines = int(self.relief_ellipse_inner_lines)
offset = int(self.relief_ellipse_inner_offset)
for line in range(1, lines + 1):
alpha = 0.9 - (line / lines) * 0.8997
line += offset
line2 = 2 * line
in_x1 = wid_x + line
in_y1 = wid_y + line
in_width = wid_width - line2
in_height = wid_height - line2
add_instruction(Color(*top_color, alpha)) # inside top left
add_instruction(Line(ellipse=[in_x1, in_y1, in_width, in_height, ANGLE_END, 360 + ANGLE_BEG]))
add_instruction(Color(*bottom_color, alpha)) # inside bottom right
add_instruction(Line(ellipse=[in_x1, in_y1, in_width, in_height, ANGLE_BEG, ANGLE_END]))
[docs] def _relief_ellipse_outer_refresh(self, add_instruction: Callable,
top_color: ColorRGB, bottom_color: ColorRGB,
wid_x: float, wid_y: float, wid_width: float, wid_height: float):
""" ellipse pos/size or color changed event handler. """
lines = int(self.relief_ellipse_outer_lines)
for line in range(1, lines + 1):
alpha = 0.9 - (line / lines) * 0.8997
line2 = 2 * line
out_x1 = wid_x - line
out_y1 = wid_y - line
out_width = wid_width + line2
out_height = wid_height + line2
add_instruction(Color(*top_color, alpha)) # outside top left
add_instruction(Line(ellipse=[out_x1, out_y1, out_width, out_height, ANGLE_END, 360 + ANGLE_BEG]))
add_instruction(Color(*bottom_color, alpha)) # outside bottom right
add_instruction(Line(ellipse=[out_x1, out_y1, out_width, out_height, ANGLE_BEG, ANGLE_END]))
[docs] def _relief_square_inner_refresh(self, add_instruction: Callable,
top_color: ColorRGB, bottom_color: ColorRGB,
wid_x: float, wid_y: float, wid_width: float, wid_height: float):
""" square pos/size or color changed event handler. """
lines = int(self.relief_square_inner_lines)
offset = int(self.relief_square_inner_offset)
for line in range(1, lines + 1):
alpha = 0.9 - (line / lines) * 0.8997
line += offset
line2 = 2 * line
in_x1 = wid_x + line
in_x2 = in_x1 + wid_width - line2
in_y1 = wid_y + line
in_y2 = in_y1 + wid_height - line2
add_instruction(Color(*top_color, alpha)) # inside top left
add_instruction(Line(points=[in_x1, in_y1, in_x1, in_y2, in_x2, in_y2]))
add_instruction(Color(*bottom_color, alpha)) # inside bottom right
add_instruction(Line(points=[in_x1, in_y1, in_x2, in_y1, in_x2, in_y2]))
[docs] def _relief_square_outer_refresh(self, add_instruction: Callable,
top_color: ColorRGB, bottom_color: ColorRGB,
wid_x: float, wid_y: float, wid_width: float, wid_height: float):
""" square pos/size or color changed event handler. """
lines = int(self.relief_square_outer_lines)
for line in range(1, lines + 1):
alpha = 0.9 - (line / lines) * 0.8997
line2 = 2 * line
out_x1 = wid_x - line
out_x2 = out_x1 + wid_width + line2
out_y1 = wid_y - line
out_y2 = out_y1 + wid_height + line2
add_instruction(Color(*top_color, alpha)) # outside upper left
add_instruction(Line(points=[out_x1, out_y1, out_x1, out_y2, out_x2, out_y2]))
add_instruction(Color(*bottom_color, alpha)) # outside bottom right
add_instruction(Line(points=[out_x1, out_y1, out_x2, out_y1, out_x2, out_y2]))
Factory.register('ReliefCanvas', cls=ReliefCanvas)