mirror of
https://github.com/markqvist/Sideband.git
synced 2024-12-12 17:34:32 -05:00
1476 lines
45 KiB
Python
1476 lines
45 KiB
Python
|
"""
|
|||
|
Behaviors/Elevation
|
|||
|
===================
|
|||
|
|
|||
|
.. seealso::
|
|||
|
|
|||
|
`Material Design spec, Elevation <https://material.io/design/environment/elevation.html>`_
|
|||
|
|
|||
|
.. rubric:: Elevation is the relative distance between two surfaces along the z-axis.
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-previous.png
|
|||
|
:align: center
|
|||
|
|
|||
|
There are 5 classes in KivyMD that can simulate shadow:
|
|||
|
|
|||
|
#. :class:`~FakeRectangularElevationBehavior`
|
|||
|
#. :class:`~FakeCircularElevationBehavior`
|
|||
|
|
|||
|
#. :class:`~RectangularElevationBehavior`
|
|||
|
#. :class:`~CircularElevationBehavior`
|
|||
|
#. :class:`~RoundedRectangularElevationBehavior`
|
|||
|
|
|||
|
By default, KivyMD widgets use the elevation behavior implemented in classes
|
|||
|
:class:`~FakeRectangularElevationBehavior` and :class:`~FakeCircularElevationBehavior`
|
|||
|
for cast shadows. These classes use the old method of rendering shadows and it
|
|||
|
doesn't look very aesthetically pleasing. Shadows are harsh, no softness:
|
|||
|
|
|||
|
The :class:`~RectangularElevationBehavior`, :class:`~CircularElevationBehavior`,
|
|||
|
:class:`~RoundedRectangularElevationBehavior` classes use the new shadow
|
|||
|
rendering algorithm, based on textures creation using the `Pillow` library.
|
|||
|
It looks very aesthetically pleasing and beautiful.
|
|||
|
|
|||
|
.. warning:: Remember that :class:`~RectangularElevationBehavior`,
|
|||
|
:class:`~CircularElevationBehavior`, :class:`~RoundedRectangularElevationBehavior`
|
|||
|
classes require a lot of resources from the device on which your application will run,
|
|||
|
so you should not use these classes on mobile devices.
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.uix.widget import Widget
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.uix.card import MDCard
|
|||
|
from kivymd.uix.behaviors import RectangularElevationBehavior
|
|||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|||
|
|
|||
|
KV = '''
|
|||
|
<Box@MDBoxLayout>
|
|||
|
adaptive_size: True
|
|||
|
orientation: "vertical"
|
|||
|
spacing: "36dp"
|
|||
|
|
|||
|
|
|||
|
<BaseShadowWidget>
|
|||
|
size_hint: None, None
|
|||
|
size: 100, 100
|
|||
|
md_bg_color: 0, 0, 1, 1
|
|||
|
elevation: 36
|
|||
|
pos_hint: {'center_x': .5}
|
|||
|
|
|||
|
|
|||
|
MDFloatLayout:
|
|||
|
|
|||
|
MDBoxLayout:
|
|||
|
adaptive_size: True
|
|||
|
pos_hint: {'center_x': .5, 'center_y': .5}
|
|||
|
spacing: "56dp"
|
|||
|
|
|||
|
Box:
|
|||
|
|
|||
|
MDLabel:
|
|||
|
text: "Deprecated shadow rendering"
|
|||
|
adaptive_size: True
|
|||
|
|
|||
|
DeprecatedShadowWidget:
|
|||
|
|
|||
|
MDLabel:
|
|||
|
text: "Doesn't require a lot of resources"
|
|||
|
adaptive_size: True
|
|||
|
|
|||
|
Box:
|
|||
|
|
|||
|
MDLabel:
|
|||
|
text: "New shadow rendering"
|
|||
|
adaptive_size: True
|
|||
|
|
|||
|
NewShadowWidget:
|
|||
|
|
|||
|
MDLabel:
|
|||
|
text: "It takes a lot of resources"
|
|||
|
adaptive_size: True
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class BaseShadowWidget(Widget):
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class DeprecatedShadowWidget(MDCard, BaseShadowWidget):
|
|||
|
'''Deprecated shadow rendering. Doesn't require a lot of resources.'''
|
|||
|
|
|||
|
|
|||
|
class NewShadowWidget(RectangularElevationBehavior, BaseShadowWidget, MDBoxLayout):
|
|||
|
'''New shadow rendering. It takes a lot of resources.'''
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-differential.png
|
|||
|
:align: center
|
|||
|
|
|||
|
|
|||
|
For example, let's create an button with a rectangular elevation effect:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.uix.behaviors import ButtonBehavior
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.uix.behaviors import (
|
|||
|
RectangularRippleBehavior,
|
|||
|
BackgroundColorBehavior,
|
|||
|
FakeRectangularElevationBehavior,
|
|||
|
)
|
|||
|
|
|||
|
KV = '''
|
|||
|
<RectangularElevationButton>:
|
|||
|
size_hint: None, None
|
|||
|
size: "250dp", "50dp"
|
|||
|
|
|||
|
|
|||
|
MDScreen:
|
|||
|
|
|||
|
# With elevation effect
|
|||
|
RectangularElevationButton:
|
|||
|
pos_hint: {"center_x": .5, "center_y": .6}
|
|||
|
elevation: 18
|
|||
|
|
|||
|
# Without elevation effect
|
|||
|
RectangularElevationButton:
|
|||
|
pos_hint: {"center_x": .5, "center_y": .4}
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class RectangularElevationButton(
|
|||
|
RectangularRippleBehavior,
|
|||
|
FakeRectangularElevationBehavior,
|
|||
|
ButtonBehavior,
|
|||
|
BackgroundColorBehavior,
|
|||
|
):
|
|||
|
md_bg_color = [0, 0, 1, 1]
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-effect.gif
|
|||
|
:align: center
|
|||
|
|
|||
|
Similarly, create a circular button:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.uix.behaviors import ButtonBehavior
|
|||
|
|
|||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.uix.behaviors import (
|
|||
|
CircularRippleBehavior,
|
|||
|
FakeCircularElevationBehavior,
|
|||
|
)
|
|||
|
|
|||
|
KV = '''
|
|||
|
<CircularElevationButton>:
|
|||
|
size_hint: None, None
|
|||
|
size: "100dp", "100dp"
|
|||
|
radius: self.size[0] / 2
|
|||
|
md_bg_color: 0, 0, 1, 1
|
|||
|
|
|||
|
MDIcon:
|
|||
|
icon: "hand-heart"
|
|||
|
halign: "center"
|
|||
|
valign: "center"
|
|||
|
size: root.size
|
|||
|
pos: root.pos
|
|||
|
font_size: root.size[0] * .6
|
|||
|
theme_text_color: "Custom"
|
|||
|
text_color: [1] * 4
|
|||
|
|
|||
|
|
|||
|
MDScreen:
|
|||
|
|
|||
|
CircularElevationButton:
|
|||
|
pos_hint: {"center_x": .5, "center_y": .6}
|
|||
|
elevation: 24
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class CircularElevationButton(
|
|||
|
FakeCircularElevationBehavior,
|
|||
|
CircularRippleBehavior,
|
|||
|
ButtonBehavior,
|
|||
|
MDBoxLayout,
|
|||
|
):
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-fake-elevation.png
|
|||
|
:align: center
|
|||
|
|
|||
|
Animating the elevation
|
|||
|
-----------------------
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.animation import Animation
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.properties import ObjectProperty
|
|||
|
from kivy.uix.behaviors import ButtonBehavior
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.theming import ThemableBehavior
|
|||
|
from kivymd.uix.behaviors import FakeRectangularElevationBehavior, RectangularRippleBehavior
|
|||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|||
|
|
|||
|
KV = '''
|
|||
|
MDFloatLayout:
|
|||
|
|
|||
|
ElevatedWidget:
|
|||
|
pos_hint: {'center_x': .5, 'center_y': .5}
|
|||
|
size_hint: None, None
|
|||
|
size: 100, 100
|
|||
|
md_bg_color: 0, 0, 1, 1
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class ElevatedWidget(
|
|||
|
ThemableBehavior,
|
|||
|
FakeRectangularElevationBehavior,
|
|||
|
RectangularRippleBehavior,
|
|||
|
ButtonBehavior,
|
|||
|
MDBoxLayout,
|
|||
|
):
|
|||
|
shadow_animation = ObjectProperty()
|
|||
|
|
|||
|
def on_press(self, *args):
|
|||
|
if self.shadow_animation:
|
|||
|
Animation.cancel_all(self, "_elevation")
|
|||
|
self.shadow_animation = Animation(_elevation=self.elevation + 10, d=0.4)
|
|||
|
self.shadow_animation.start(self)
|
|||
|
|
|||
|
def on_release(self, *args):
|
|||
|
if self.shadow_animation:
|
|||
|
Animation.cancel_all(self, "_elevation")
|
|||
|
self.shadow_animation = Animation(_elevation=self.elevation, d=0.1)
|
|||
|
self.shadow_animation.start(self)
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-animation-effect.gif
|
|||
|
:align: center
|
|||
|
|
|||
|
Lighting position
|
|||
|
-----------------
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.lang import Builder
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.uix.card import MDCard
|
|||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|||
|
from kivymd.uix.behaviors import RectangularElevationBehavior
|
|||
|
|
|||
|
KV = '''
|
|||
|
MDScreen:
|
|||
|
|
|||
|
ShadowCard:
|
|||
|
pos_hint: {'center_x': .5, 'center_y': .5}
|
|||
|
size_hint: None, None
|
|||
|
size: 100, 100
|
|||
|
shadow_pos: -10 + slider.value, -10 + slider.value
|
|||
|
elevation: 24
|
|||
|
md_bg_color: 1, 1, 1, 1
|
|||
|
|
|||
|
MDSlider:
|
|||
|
id: slider
|
|||
|
max: 20
|
|||
|
size_hint_x: .6
|
|||
|
pos_hint: {'center_x': .5, 'center_y': .3}
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class ShadowCard(RectangularElevationBehavior, MDBoxLayout):
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-pos.gif
|
|||
|
:align: center
|
|||
|
"""
|
|||
|
|
|||
|
__all__ = (
|
|||
|
"CommonElevationBehavior",
|
|||
|
"RectangularElevationBehavior",
|
|||
|
"CircularElevationBehavior",
|
|||
|
"RoundedRectangularElevationBehavior",
|
|||
|
"ObservableShadow",
|
|||
|
"FakeRectangularElevationBehavior",
|
|||
|
"FakeCircularElevationBehavior",
|
|||
|
)
|
|||
|
|
|||
|
from io import BytesIO
|
|||
|
from weakref import WeakMethod, ref
|
|||
|
|
|||
|
from kivy import Logger
|
|||
|
from kivy.clock import Clock
|
|||
|
from kivy.core.image import Image as CoreImage
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.metrics import dp
|
|||
|
from kivy.properties import (
|
|||
|
AliasProperty,
|
|||
|
BooleanProperty,
|
|||
|
BoundedNumericProperty,
|
|||
|
ListProperty,
|
|||
|
NumericProperty,
|
|||
|
ObjectProperty,
|
|||
|
ReferenceListProperty,
|
|||
|
StringProperty,
|
|||
|
VariableListProperty,
|
|||
|
)
|
|||
|
from kivy.uix.widget import Widget
|
|||
|
from PIL import Image, ImageDraw, ImageFilter
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
|
|||
|
Builder.load_string(
|
|||
|
"""
|
|||
|
#:import InstructionGroup kivy.graphics.instructions.InstructionGroup
|
|||
|
|
|||
|
|
|||
|
<CommonElevationBehavior>
|
|||
|
canvas.before:
|
|||
|
# SOFT SHADOW
|
|||
|
PushMatrix
|
|||
|
Rotate:
|
|||
|
angle: self.angle
|
|||
|
origin: self._shadow_origin
|
|||
|
Color:
|
|||
|
group: "soft_shadow"
|
|||
|
rgba: root.soft_shadow_cl
|
|||
|
Rectangle:
|
|||
|
group: "soft_shadow"
|
|||
|
texture: self._soft_shadow_texture
|
|||
|
size: self.soft_shadow_size
|
|||
|
pos: self.soft_shadow_pos
|
|||
|
PopMatrix
|
|||
|
|
|||
|
# HARD SHADOW
|
|||
|
PushMatrix
|
|||
|
Rotate:
|
|||
|
angle: self.angle
|
|||
|
origin: self.center
|
|||
|
Color:
|
|||
|
group: "hard_shadow"
|
|||
|
rgba: root.hard_shadow_cl
|
|||
|
Rectangle:
|
|||
|
group: "hard_shadow"
|
|||
|
texture: self.hard_shadow_texture
|
|||
|
size: self.hard_shadow_size
|
|||
|
pos: self.hard_shadow_pos
|
|||
|
PopMatrix
|
|||
|
Color:
|
|||
|
group: "shadow"
|
|||
|
a: 1
|
|||
|
""",
|
|||
|
filename="CommonElevationBehavior.kv",
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class CommonElevationBehavior(Widget):
|
|||
|
"""Common base class for rectangular and circular elevation behavior."""
|
|||
|
|
|||
|
elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
|
|||
|
"""
|
|||
|
Elevation of the widget.
|
|||
|
|
|||
|
.. note::
|
|||
|
Although, this value does not represent the current elevation of the
|
|||
|
widget. :attr:`~CommonElevationBehavior._elevation` can be used to
|
|||
|
animate the current elevation and come back using the
|
|||
|
:attr:`~CommonElevationBehavior.elevation` property directly.
|
|||
|
|
|||
|
For example:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
from kivy.lang import Builder
|
|||
|
from kivy.uix.behaviors import ButtonBehavior
|
|||
|
|
|||
|
from kivymd.app import MDApp
|
|||
|
from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior
|
|||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|||
|
|
|||
|
KV = '''
|
|||
|
#:import Animation kivy.animation.Animation
|
|||
|
|
|||
|
|
|||
|
<WidgetWithShadow>
|
|||
|
size_hint: [None, None]
|
|||
|
elevation: 6
|
|||
|
animation_: None
|
|||
|
md_bg_color: [1] * 4
|
|||
|
on_size:
|
|||
|
self.radius = [self.height / 2] * 4
|
|||
|
on_press:
|
|||
|
if self.animation_: \
|
|||
|
self.animation_.cancel(self); \
|
|||
|
self.animation_ = Animation(_elevation=self.elevation + 6, d=0.08); \
|
|||
|
self.animation_.start(self)
|
|||
|
on_release:
|
|||
|
if self.animation_: \
|
|||
|
self.animation_.cancel(self); \
|
|||
|
self.animation_ = Animation(_elevation = self.elevation, d=0.08); \
|
|||
|
self.animation_.start(self)
|
|||
|
|
|||
|
MDFloatLayout:
|
|||
|
|
|||
|
WidgetWithShadow:
|
|||
|
size: [root.size[1] / 2] * 2
|
|||
|
pos_hint: {"center": [0.5, 0.5]}
|
|||
|
'''
|
|||
|
|
|||
|
|
|||
|
class WidgetWithShadow(
|
|||
|
CircularElevationBehavior,
|
|||
|
CircularRippleBehavior,
|
|||
|
ButtonBehavior,
|
|||
|
MDBoxLayout,
|
|||
|
):
|
|||
|
def __init__(self, **kwargs):
|
|||
|
# always set the elevation before the super().__init__ call
|
|||
|
# self.elevation = 6
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def on_size(self, *args):
|
|||
|
self.radius = [self.size[0] / 2]
|
|||
|
|
|||
|
|
|||
|
class Example(MDApp):
|
|||
|
def build(self):
|
|||
|
return Builder.load_string(KV)
|
|||
|
|
|||
|
|
|||
|
Example().run()
|
|||
|
|
|||
|
:attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty`
|
|||
|
and defaults to `0`.
|
|||
|
"""
|
|||
|
|
|||
|
# Shadow rendering properties.
|
|||
|
# Shadow rotation memory - SHARED ACROSS OTHER CLASSES.
|
|||
|
angle = NumericProperty(0)
|
|||
|
"""
|
|||
|
Angle of rotation in degrees of the current shadow.
|
|||
|
This value is shared across different widgets.
|
|||
|
|
|||
|
.. note::
|
|||
|
This value will affect both, hard and soft shadows.
|
|||
|
Each shadow has his own origin point that's computed every time the
|
|||
|
elevation changes.
|
|||
|
|
|||
|
.. warning::
|
|||
|
Do not add `PushMatrix` inside the canvas before and add `PopMatrix`
|
|||
|
in the next layer, this will cause visual errors, because the stack
|
|||
|
used will clip the push and pop matrix already inside the canvas.before
|
|||
|
canvas layer.
|
|||
|
|
|||
|
Incorrect:
|
|||
|
|
|||
|
.. code-block:: kv
|
|||
|
|
|||
|
<TiltedWidget>
|
|||
|
canvas.before:
|
|||
|
PushMatrix
|
|||
|
[...]
|
|||
|
canvas:
|
|||
|
PopMatrix
|
|||
|
|
|||
|
Correct:
|
|||
|
|
|||
|
.. code-block:: kv
|
|||
|
|
|||
|
<TiltedWidget>
|
|||
|
canvas.before:
|
|||
|
PushMatrix
|
|||
|
[...]
|
|||
|
PopMatrix
|
|||
|
|
|||
|
|
|||
|
|
|||
|
:attr:`angle` is an :class:`~kivy.properties.NumericProperty`
|
|||
|
and defaults to `0`.
|
|||
|
"""
|
|||
|
|
|||
|
radius = VariableListProperty([0])
|
|||
|
"""
|
|||
|
Radius of the corners of the shadow.
|
|||
|
This values represents each corner of the shadow, starting from `top-left`
|
|||
|
corner and going clockwise.
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
radius = [
|
|||
|
"top-left",
|
|||
|
"top-right",
|
|||
|
"bottom-right",
|
|||
|
"bottom-left",
|
|||
|
]
|
|||
|
|
|||
|
This value can be expanded thus allowing this settings to be valid:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
widget.radius=[0] # Translates to [0, 0, 0, 0]
|
|||
|
widget.radius=[10, 3] # Translates to [10, 3, 10, 3]
|
|||
|
widget.radius=[7.0, 8.7, 1.5, 3.0] # Translates to [7, 8, 1, 3]
|
|||
|
|
|||
|
.. note::
|
|||
|
This value will affect both, hard and soft shadows.
|
|||
|
This value only affects :class:`~RoundedRectangularElevationBehavior`
|
|||
|
for now, but can be stored and used by custom shadow draw functions.
|
|||
|
|
|||
|
:attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
|
|||
|
and defaults to `[0, 0, 0, 0]`.
|
|||
|
"""
|
|||
|
|
|||
|
# Position of the shadow.
|
|||
|
_shadow_origin_x = NumericProperty(0)
|
|||
|
"""
|
|||
|
Shadow origin `x` position for the rotation origin.
|
|||
|
|
|||
|
Managed by `_shadow_origin`.
|
|||
|
|
|||
|
:attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty`
|
|||
|
and defaults to `0`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed. by _shadow_origin.
|
|||
|
"""
|
|||
|
|
|||
|
_shadow_origin_y = NumericProperty(0)
|
|||
|
"""
|
|||
|
Shadow origin y position for the rotation origin.
|
|||
|
|
|||
|
Managed by :attr:`_shadow_origin`.
|
|||
|
|
|||
|
:attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty`
|
|||
|
and defaults to `0`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed.
|
|||
|
"""
|
|||
|
|
|||
|
_shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y)
|
|||
|
"""
|
|||
|
Soft shadow rotation origin point.
|
|||
|
|
|||
|
:attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed and relative to the canvas center.
|
|||
|
"""
|
|||
|
|
|||
|
_shadow_pos = ListProperty([0, 0]) # custom offset
|
|||
|
"""
|
|||
|
Soft shadow origin point.
|
|||
|
|
|||
|
:attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed and relative to the widget's
|
|||
|
canvas center.
|
|||
|
"""
|
|||
|
|
|||
|
shadow_pos = ListProperty([0, 0]) # bottom left corner
|
|||
|
"""
|
|||
|
Custom shadow origin point. If this property is set, :attr:`_shadow_pos`
|
|||
|
will be ommited.
|
|||
|
|
|||
|
This property allows users to fake light source.
|
|||
|
|
|||
|
:attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
this value overwrite the :attr:`_shadow_pos` processing.
|
|||
|
"""
|
|||
|
|
|||
|
# Shadow Group shared memory
|
|||
|
__shadow_groups = {"global": []}
|
|||
|
|
|||
|
shadow_group = StringProperty("global")
|
|||
|
"""
|
|||
|
Widget's shadow group.
|
|||
|
By default every widget with a shadow is saved inside the memory
|
|||
|
:attr:`__shadow_groups` as a weakref. This means that you can have multiple
|
|||
|
light sources, one for every shadow group.
|
|||
|
|
|||
|
To fake a light source use :attr:`force_shadow_pos`.
|
|||
|
|
|||
|
:attr:`shadow_group` is an :class:`~kivy.properties.StringProperty`
|
|||
|
and defaults to `"global"`.
|
|||
|
"""
|
|||
|
|
|||
|
_elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
|
|||
|
"""
|
|||
|
Current elevation of the widget.
|
|||
|
|
|||
|
.. warning::
|
|||
|
This property is the current elevation of the widget, do not
|
|||
|
use this property directly, instead, use :class:`~CommonElevationBehavior`
|
|||
|
elevation.
|
|||
|
|
|||
|
:attr:`_elevation` is an :class:`~kivy.properties.NumericProperty`
|
|||
|
and defaults to `0`.
|
|||
|
"""
|
|||
|
|
|||
|
# soft shadow
|
|||
|
_soft_shadow_texture = ObjectProperty()
|
|||
|
"""
|
|||
|
Texture of the soft shadow texture for the canvas.
|
|||
|
|
|||
|
:attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image`
|
|||
|
and defaults to `None`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed.
|
|||
|
"""
|
|||
|
|
|||
|
soft_shadow_size = ListProperty([0, 0])
|
|||
|
"""
|
|||
|
Size of the soft shadow texture over the canvas.
|
|||
|
|
|||
|
:attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed.
|
|||
|
"""
|
|||
|
|
|||
|
soft_shadow_pos = ListProperty([0, 0])
|
|||
|
"""
|
|||
|
Position of the hard shadow texture over the canvas.
|
|||
|
|
|||
|
:attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed.
|
|||
|
"""
|
|||
|
|
|||
|
soft_shadow_cl = ListProperty([0, 0, 0, 0.50])
|
|||
|
"""
|
|||
|
Color of the soft shadow.
|
|||
|
|
|||
|
:attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0, 0, 0.15]`.
|
|||
|
"""
|
|||
|
|
|||
|
# hard shadow
|
|||
|
hard_shadow_texture = ObjectProperty()
|
|||
|
"""
|
|||
|
Texture of the hard shadow texture for the canvas.
|
|||
|
|
|||
|
:attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image`
|
|||
|
and defaults to `None`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed when elevation is changed.
|
|||
|
"""
|
|||
|
|
|||
|
hard_shadow_size = ListProperty([0, 0])
|
|||
|
"""
|
|||
|
Size of the hard shadow texture over the canvas.
|
|||
|
|
|||
|
:attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed when elevation is changed.
|
|||
|
"""
|
|||
|
|
|||
|
hard_shadow_pos = ListProperty([0, 0])
|
|||
|
"""
|
|||
|
Position of the hard shadow texture over the canvas.
|
|||
|
|
|||
|
:attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0]`.
|
|||
|
|
|||
|
.. note::
|
|||
|
This property is automatically processed when elevation is changed.
|
|||
|
"""
|
|||
|
|
|||
|
hard_shadow_cl = ListProperty([0, 0, 0, 0.15])
|
|||
|
"""
|
|||
|
Color of the hard shadow.
|
|||
|
|
|||
|
.. note::
|
|||
|
:attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty`
|
|||
|
and defaults to `[0, 0, 0, 0.15]`.
|
|||
|
"""
|
|||
|
|
|||
|
# Shared property for some calculations.
|
|||
|
# This values are used to improve the gaussain blur and avoid that
|
|||
|
# the blur goes outside the texture.
|
|||
|
hard_shadow_offset = BoundedNumericProperty(
|
|||
|
2, min=0, errorhandler=lambda x: 0 if x < 0 else x
|
|||
|
)
|
|||
|
"""
|
|||
|
This value sets a special offset to the shadow canvas, this offset allows a
|
|||
|
correct draw of the canvas size. allowing the effect to correctly blur the
|
|||
|
image in the given space.
|
|||
|
|
|||
|
:attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
|
|||
|
and defaults to `2`.
|
|||
|
"""
|
|||
|
|
|||
|
soft_shadow_offset = BoundedNumericProperty(
|
|||
|
4, min=0, errorhandler=lambda x: 0 if x < 0 else x
|
|||
|
)
|
|||
|
"""
|
|||
|
This value sets a special offset to the shadow canvas, this offset allows a
|
|||
|
correct draw of the canvas size. allowing the effect to correctly blur the
|
|||
|
image in the given space.
|
|||
|
|
|||
|
:attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
|
|||
|
and defaults to `4`.
|
|||
|
"""
|
|||
|
|
|||
|
draw_shadow = ObjectProperty(None)
|
|||
|
"""
|
|||
|
This property controls the draw call of the context.
|
|||
|
|
|||
|
This property is automatically set to :attr:`__draw_shadow__` inside the
|
|||
|
`super().__init__ call.` unless the property is different of None.
|
|||
|
|
|||
|
To set a different drawing instruction function, set this property before the
|
|||
|
`super(),__init__` call inside the `__init__` definition of the new class.
|
|||
|
|
|||
|
You can use the source for this classes as example of how to draw over
|
|||
|
with the context:
|
|||
|
|
|||
|
Real time shadows:
|
|||
|
#. :class:`~RectangularElevationBehavior`
|
|||
|
#. :class:`~CircularElevationBehavior`
|
|||
|
#. :class:`~RoundedRectangularElevationBehavior`
|
|||
|
#. :class:`~ObservableShadow`
|
|||
|
|
|||
|
|
|||
|
Fake shadows (d`ont use this property):
|
|||
|
#. :class:`~FakeRectangularElevationBehavior`
|
|||
|
#. :class:`~FakeCircularElevationBehavior`
|
|||
|
|
|||
|
:attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty`
|
|||
|
and defaults to `None`.
|
|||
|
|
|||
|
.. note:: If this property is left to `None` the
|
|||
|
:class:`~CommonElevationBehavior` will set to a function that will
|
|||
|
raise a `NotImplementedError` inside `super().__init__`.
|
|||
|
|
|||
|
Follow the next example to set a new draw instruction for the class
|
|||
|
inside `__init__`:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
class RoundedRectangularElevationBehavior(CommonElevationBehavior):
|
|||
|
'''
|
|||
|
Shadow class for the RoundedRectangular shadow behavior.
|
|||
|
Controls the size and position of the shadow.
|
|||
|
'''
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
self._draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
context.draw(...)
|
|||
|
|
|||
|
Context is a `Pillow` `ImageDraw` class. For more information check the
|
|||
|
[Pillow official documentation](https://github.com/python-pillow/Pillow/).
|
|||
|
"""
|
|||
|
|
|||
|
# All classes that uses a fake shadow shall set this value as `True`
|
|||
|
# for performance.
|
|||
|
_fake_elevation = BooleanProperty(False)
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
if self.draw_shadow is None:
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
self.prev_shadow_group = None
|
|||
|
im = BytesIO()
|
|||
|
Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
|
|||
|
im.seek(0)
|
|||
|
# Setting a empty image as texture, improves performance.
|
|||
|
self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
|
|||
|
im, ext="png"
|
|||
|
).texture
|
|||
|
Clock.schedule_once(self.shadow_preset, -1)
|
|||
|
self.on_shadow_group(self, self.shadow_group)
|
|||
|
|
|||
|
self.bind(
|
|||
|
pos=self._update_shadow,
|
|||
|
size=self._update_shadow,
|
|||
|
radius=self._update_shadow,
|
|||
|
)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def on_shadow_group(self, instance, value):
|
|||
|
"""
|
|||
|
This function controls the shadow group of the widget.
|
|||
|
Do not use Directly to change the group. instead, use the shadow_group
|
|||
|
:attr:`property`.
|
|||
|
"""
|
|||
|
|
|||
|
groups = CommonElevationBehavior.__shadow_groups
|
|||
|
if self.prev_shadow_group:
|
|||
|
group = groups[self.prev_shadow_group]
|
|||
|
for widget in group[:]:
|
|||
|
if widget() is self:
|
|||
|
group.remove(widget)
|
|||
|
group = self.prev_shadow_group = self.shadow_group
|
|||
|
if group not in groups:
|
|||
|
groups[group] = []
|
|||
|
r = ref(self, CommonElevationBehavior._clear_shadow_groups)
|
|||
|
groups[group].append(r)
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def _clear_shadow_groups(wk):
|
|||
|
# auto flush the element when the weak reference have been deleted
|
|||
|
groups = CommonElevationBehavior.__shadow_groups
|
|||
|
for group in list(groups.values()):
|
|||
|
if not group:
|
|||
|
break
|
|||
|
if wk in group:
|
|||
|
group.remove(wk)
|
|||
|
break
|
|||
|
|
|||
|
def force_shadow_pos(self, shadow_pos):
|
|||
|
"""
|
|||
|
This property forces the shadow position in every widget inside the
|
|||
|
widget. The argument :attr:`shadow_pos` is expected as a <class 'list'>
|
|||
|
or <class 'tuple'>.
|
|||
|
"""
|
|||
|
|
|||
|
if self.shadow_group is None:
|
|||
|
return
|
|||
|
group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
|
|||
|
for wk in group[:]:
|
|||
|
widget = wk()
|
|||
|
if widget is None:
|
|||
|
group.remove(wk)
|
|||
|
widget.shadow_pos = shadow_pos
|
|||
|
del group
|
|||
|
|
|||
|
def update_group_property(self, property_name, value):
|
|||
|
"""
|
|||
|
This functions allows to change properties of every widget inside the
|
|||
|
shadow group.
|
|||
|
"""
|
|||
|
|
|||
|
if self.shadow_group is None:
|
|||
|
return
|
|||
|
group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
|
|||
|
for wk in group[:]:
|
|||
|
widget = wk()
|
|||
|
if widget is None:
|
|||
|
group.remove(wk)
|
|||
|
setattr(widget, property_name, value)
|
|||
|
del group
|
|||
|
|
|||
|
def shadow_preset(self, *args):
|
|||
|
"""
|
|||
|
This function is meant to set the default configuration of the
|
|||
|
elevation.
|
|||
|
|
|||
|
After a new instance is created, the elevation property will be launched
|
|||
|
and thus this function will update the elevation if the KV lang have not
|
|||
|
done it already.
|
|||
|
|
|||
|
Works similar to an `__after_init__` call inside a widget.
|
|||
|
"""
|
|||
|
|
|||
|
from kivymd.uix.card import MDCard
|
|||
|
|
|||
|
if self.elevation is None and not issubclass(self.__class__, MDCard):
|
|||
|
self.elevation = 10
|
|||
|
if self._fake_elevation is False:
|
|||
|
self._update_shadow(self, self.elevation)
|
|||
|
self.bind(
|
|||
|
pos=self._update_shadow,
|
|||
|
size=self._update_shadow,
|
|||
|
_elevation=self._update_shadow,
|
|||
|
)
|
|||
|
|
|||
|
def on_elevation(self, instance, value):
|
|||
|
"""
|
|||
|
Elevation event that sets the current elevation value to `_elevation`.
|
|||
|
"""
|
|||
|
|
|||
|
if value is not None:
|
|||
|
self._elevation = value
|
|||
|
|
|||
|
def _set_soft_shadow_a(self, value):
|
|||
|
value = 0 if value < 0 else (1 if value > 1 else value)
|
|||
|
self.soft_shadow_cl[-1] = value
|
|||
|
return True
|
|||
|
|
|||
|
def _set_hard_shadow_a(self, value):
|
|||
|
value = 0 if value < 0 else (1 if value > 1 else value)
|
|||
|
self.hard_shadow_cl[-1] = value
|
|||
|
return True
|
|||
|
|
|||
|
def _get_soft_shadow_a(self):
|
|||
|
return self.soft_shadow_cl[-1]
|
|||
|
|
|||
|
def _get_hard_shadow_a(self):
|
|||
|
return self.hard_shadow_cl[-1]
|
|||
|
|
|||
|
_soft_shadow_a = AliasProperty(
|
|||
|
_get_soft_shadow_a, _set_soft_shadow_a, bind=["soft_shadow_cl"]
|
|||
|
)
|
|||
|
_hard_shadow_a = AliasProperty(
|
|||
|
_get_hard_shadow_a, _set_hard_shadow_a, bind=["hard_shadow_cl"]
|
|||
|
)
|
|||
|
|
|||
|
def on_disabled(self, instance, value):
|
|||
|
"""
|
|||
|
This function hides the shadow when the widget is disabled.
|
|||
|
It sets the shadow to `0`.
|
|||
|
"""
|
|||
|
|
|||
|
if self.disabled is True:
|
|||
|
self._elevation = 0
|
|||
|
else:
|
|||
|
self._elevation = 0 if self.elevation is None else self.elevation
|
|||
|
self._update_shadow(self, self._elevation)
|
|||
|
try:
|
|||
|
super().on_disabled(instance, value)
|
|||
|
except Exception:
|
|||
|
pass
|
|||
|
|
|||
|
def _update_elevation(self, instance, value):
|
|||
|
self._elevation = value
|
|||
|
self._update_shadow(instance, value)
|
|||
|
|
|||
|
def _update_shadow_pos(self, instance, value):
|
|||
|
if self._elevation > 0:
|
|||
|
self.hard_shadow_pos = [
|
|||
|
self.x - dp(self.hard_shadow_offset), # + self.shadow_pos[0],
|
|||
|
self.y - dp(self.hard_shadow_offset), # + self.shadow_pos[1],
|
|||
|
]
|
|||
|
if self.shadow_pos == [0, 0]:
|
|||
|
self.soft_shadow_pos = [
|
|||
|
self.x
|
|||
|
+ self._shadow_pos[0]
|
|||
|
- self._elevation
|
|||
|
- dp(self.soft_shadow_offset),
|
|||
|
self.y
|
|||
|
+ self._shadow_pos[1]
|
|||
|
- self._elevation
|
|||
|
- dp(self.soft_shadow_offset),
|
|||
|
]
|
|||
|
else:
|
|||
|
self.soft_shadow_pos = [
|
|||
|
self.x
|
|||
|
+ self.shadow_pos[0]
|
|||
|
- self._elevation
|
|||
|
- dp(self.soft_shadow_offset),
|
|||
|
self.y
|
|||
|
+ self.shadow_pos[1]
|
|||
|
- self._elevation
|
|||
|
- dp(self.soft_shadow_offset),
|
|||
|
]
|
|||
|
self._shadow_origin = [
|
|||
|
self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2,
|
|||
|
self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2,
|
|||
|
]
|
|||
|
|
|||
|
def on__shadow_pos(self, ins, val):
|
|||
|
"""
|
|||
|
Updates the shadow with the computed value.
|
|||
|
|
|||
|
Call this function every time you need to force a shadow update.
|
|||
|
"""
|
|||
|
|
|||
|
self._update_shadow_pos(ins, val)
|
|||
|
|
|||
|
def on_shadow_pos(self, ins, val):
|
|||
|
"""
|
|||
|
Updates the shadow with the fixed value.
|
|||
|
|
|||
|
Call this function every time you need to force a shadow update.
|
|||
|
"""
|
|||
|
|
|||
|
self._update_shadow_pos(ins, val)
|
|||
|
|
|||
|
def _update_shadow(self, instance, value):
|
|||
|
self._update_shadow_pos(instance, value)
|
|||
|
if self._elevation > 0 and self._fake_elevation is False:
|
|||
|
# dynamic elevation position for the shadow
|
|||
|
if self.shadow_pos == [0, 0]:
|
|||
|
self._shadow_pos = [0, -self._elevation * 0.4]
|
|||
|
|
|||
|
# HARD Shadow
|
|||
|
offset = int(dp(self.hard_shadow_offset))
|
|||
|
size = [
|
|||
|
int(self.size[0] + (offset * 2)),
|
|||
|
int(self.size[1] + (offset * 2)),
|
|||
|
]
|
|||
|
im = BytesIO()
|
|||
|
# context
|
|||
|
img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0))
|
|||
|
# draw context
|
|||
|
shadow = ImageDraw.Draw(img)
|
|||
|
self.draw_shadow()(
|
|||
|
[offset, offset],
|
|||
|
[
|
|||
|
int(size[0] - 1 - offset),
|
|||
|
int(size[1] - 1 - offset),
|
|||
|
],
|
|||
|
context=shadow
|
|||
|
# context=ref(shadow)
|
|||
|
)
|
|||
|
img = img.filter(
|
|||
|
ImageFilter.GaussianBlur(
|
|||
|
radius=int(dp(1 + self.hard_shadow_offset / 3))
|
|||
|
)
|
|||
|
)
|
|||
|
img.save(im, format="png")
|
|||
|
im.seek(0)
|
|||
|
self.hard_shadow_size = size
|
|||
|
self.hard_shadow_texture = CoreImage(im, ext="png").texture
|
|||
|
|
|||
|
# soft shadow
|
|||
|
if self.soft_shadow_cl[-1] > 0:
|
|||
|
offset = dp(self.soft_shadow_offset)
|
|||
|
size = [
|
|||
|
int(self.size[0] + dp(self._elevation * 2) + (offset * 2)),
|
|||
|
int(self.size[1] + dp(self._elevation * 2) + (offset * 2)),
|
|||
|
# ((self._elevation)*2) + x + (offset*2)) for x in self.size
|
|||
|
]
|
|||
|
im = BytesIO()
|
|||
|
img = Image.new("RGBA", tuple(size), color=((0,) * 4))
|
|||
|
shadow = ImageDraw.Draw(img)
|
|||
|
_offset = int(dp(self._elevation + offset))
|
|||
|
self.draw_shadow()(
|
|||
|
[
|
|||
|
_offset,
|
|||
|
_offset,
|
|||
|
],
|
|||
|
[int(size[0] - _offset - 1), int(size[1] - _offset - 1)],
|
|||
|
context=shadow
|
|||
|
# context=ref(shadow)
|
|||
|
)
|
|||
|
img = img.filter(
|
|||
|
ImageFilter.GaussianBlur(radius=self._elevation // 2)
|
|||
|
)
|
|||
|
shadow = ImageDraw.Draw(img)
|
|||
|
img.save(im, format="png")
|
|||
|
im.seek(0)
|
|||
|
self.soft_shadow_size = size
|
|||
|
self._soft_shadow_texture = CoreImage(im, ext="png").texture
|
|||
|
else:
|
|||
|
im = BytesIO()
|
|||
|
Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
|
|||
|
im.seek(0)
|
|||
|
self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
|
|||
|
im, ext="png"
|
|||
|
).texture
|
|||
|
return
|
|||
|
|
|||
|
def _get_center(self):
|
|||
|
center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2]
|
|||
|
return center
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
Logger.warning(
|
|||
|
f"KivyMD: "
|
|||
|
f"If you see this error, this means that either youre using "
|
|||
|
f"`CommonElevationBehavio`r directly or your 'shader' dont have a "
|
|||
|
f"`_draw_shadow` instruction, remember to overwrite this function"
|
|||
|
f"to draw over the image context. Тhe figure you would like. "
|
|||
|
f"Or your class {self.__class__.__name__} is not inherited from "
|
|||
|
f"any of the classes {__all__}"
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class RectangularElevationBehavior(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
Base class for a rectangular elevation behavior.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
context.rectangle(origin + end, fill=tuple([255] * 4))
|
|||
|
|
|||
|
|
|||
|
class CircularElevationBehavior(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
Base class for a circular elevation behavior.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
context.ellipse(origin + end, fill=tuple([255] * 4))
|
|||
|
|
|||
|
|
|||
|
class RoundedRectangularElevationBehavior(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
Base class for rounded rectangular elevation behavior.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
self.bind(
|
|||
|
radius=self._update_shadow,
|
|||
|
)
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
if self.radius == [0, 0, 0, 0]:
|
|||
|
context.rectangle(origin + end, fill=tuple([255] * 4))
|
|||
|
else:
|
|||
|
radius = [x * 2 for x in self.radius]
|
|||
|
context.pieslice(
|
|||
|
[
|
|||
|
origin[0],
|
|||
|
origin[1],
|
|||
|
origin[0] + radius[0],
|
|||
|
origin[1] + radius[0],
|
|||
|
],
|
|||
|
180,
|
|||
|
270,
|
|||
|
fill=(255, 255, 255, 255),
|
|||
|
)
|
|||
|
context.pieslice(
|
|||
|
[
|
|||
|
end[0] - radius[1],
|
|||
|
origin[1],
|
|||
|
end[0],
|
|||
|
origin[1] + radius[1],
|
|||
|
],
|
|||
|
270,
|
|||
|
360,
|
|||
|
fill=(255, 255, 255, 255),
|
|||
|
)
|
|||
|
context.pieslice(
|
|||
|
[
|
|||
|
end[0] - radius[2],
|
|||
|
end[1] - radius[2],
|
|||
|
end[0],
|
|||
|
end[1],
|
|||
|
],
|
|||
|
0,
|
|||
|
90,
|
|||
|
fill=(255, 255, 255, 255),
|
|||
|
)
|
|||
|
context.pieslice(
|
|||
|
[
|
|||
|
origin[0],
|
|||
|
end[1] - radius[3],
|
|||
|
origin[0] + radius[3],
|
|||
|
end[1],
|
|||
|
],
|
|||
|
90,
|
|||
|
180,
|
|||
|
fill=(255, 255, 255, 255),
|
|||
|
)
|
|||
|
if all((x == self.radius[0] for x in self.radius)):
|
|||
|
radius = int(self.radius[0])
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0] + radius,
|
|||
|
origin[1],
|
|||
|
end[0] - radius,
|
|||
|
end[1],
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0],
|
|||
|
origin[1] + radius,
|
|||
|
end[0],
|
|||
|
end[1] - radius,
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
else:
|
|||
|
radius = [
|
|||
|
max((self.radius[0], self.radius[1])),
|
|||
|
max((self.radius[1], self.radius[2])),
|
|||
|
max((self.radius[2], self.radius[3])),
|
|||
|
max((self.radius[3], self.radius[0])),
|
|||
|
]
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0] + self.radius[0],
|
|||
|
origin[1],
|
|||
|
end[0] - self.radius[1],
|
|||
|
end[1] - radius[2],
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0] + radius[3],
|
|||
|
origin[1] + self.radius[1],
|
|||
|
end[0],
|
|||
|
end[1] - self.radius[2],
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0] + self.radius[3],
|
|||
|
origin[1] + radius[0],
|
|||
|
end[0] - self.radius[2],
|
|||
|
end[1],
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
context.rectangle(
|
|||
|
[
|
|||
|
origin[0],
|
|||
|
origin[1] + self.radius[0],
|
|||
|
end[0] - radius[2],
|
|||
|
end[1] - self.radius[3],
|
|||
|
],
|
|||
|
fill=(255,) * 4,
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class ObservableShadow(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
ObservableShadow is real time shadow render that it's intended to only
|
|||
|
render a partial shadow of widgets based upon on the window observable
|
|||
|
area, this is meant to improve the performance of bigger widgets.
|
|||
|
|
|||
|
.. warning::
|
|||
|
This is an empty class, the name has been reserved for future use.
|
|||
|
if you include this clas in your object, you wil get a
|
|||
|
`NotImplementedError`.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
# self._shadow = MDApp.get_running_app().theme_cls.round_shadow
|
|||
|
# self._fake_elevation=True
|
|||
|
raise NotImplementedError(
|
|||
|
"ObservableShadow:\n\t" "This class is in current development"
|
|||
|
)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
|
|||
|
class FakeRectangularElevationBehavior(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
`FakeRectangularElevationBehavio`r is a shadow mockup for widgets. Improves
|
|||
|
performance using cached images inside `kivymd.images` dir
|
|||
|
|
|||
|
This class cast a fake Rectangular shadow behaind the widget.
|
|||
|
|
|||
|
You can either use this behavior to overwrite the elevation of a prefab
|
|||
|
widget, or use it directly inside a new widget class definition.
|
|||
|
|
|||
|
Use this class as follows for new widgets:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
class NewWidget(
|
|||
|
ThemableBehavior,
|
|||
|
FakeCircularElevationBehavior,
|
|||
|
SpecificBackgroundColorBehavior,
|
|||
|
# here you add the other front end classes for the widget front_end,
|
|||
|
):
|
|||
|
[...]
|
|||
|
|
|||
|
With this method each class can draw it's content in the canvas in the
|
|||
|
correct order, avoiding some visual errors.
|
|||
|
|
|||
|
`FakeCircularElevationBehavior` will load prefabricated textures to
|
|||
|
optimize loading times.
|
|||
|
|
|||
|
.. note:: About rounded corners:
|
|||
|
be careful, since this behavior is a mockup and will not draw any
|
|||
|
rounded corners.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
# self._shadow = MDApp.get_running_app().theme_cls.round_shadow
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
self._fake_elevation = True
|
|||
|
self._update_shadow(self, self.elevation)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def _update_shadow(self, *args):
|
|||
|
if self._elevation > 0:
|
|||
|
# Set shadow size.
|
|||
|
ratio = self.width / (self.height if self.height != 0 else 1)
|
|||
|
if -2 < ratio < 2:
|
|||
|
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
|
|||
|
width = soft_width = self.width * 1.9
|
|||
|
height = soft_height = self.height * 1.9
|
|||
|
elif ratio <= -2:
|
|||
|
self._shadow = MDApp.get_running_app().theme_cls.rec_st_shadow
|
|||
|
ratio = abs(ratio)
|
|||
|
if ratio > 5:
|
|||
|
ratio = ratio * 22
|
|||
|
else:
|
|||
|
ratio = ratio * 11.5
|
|||
|
width = soft_width = self.width * 1.9
|
|||
|
height = self.height + dp(ratio)
|
|||
|
soft_height = (
|
|||
|
self.height + dp(ratio) + dp(self._elevation) * 0.5
|
|||
|
)
|
|||
|
else:
|
|||
|
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
|
|||
|
width = soft_width = self.width * 1.8
|
|||
|
height = soft_height = self.height * 1.8
|
|||
|
|
|||
|
self.soft_shadow_size = (soft_width, soft_height)
|
|||
|
self.hard_shadow_size = (width, height)
|
|||
|
# Set ``soft_shadow`` parameters.
|
|||
|
center_x, center_y = self._get_center()
|
|||
|
self.hard_shadow_pos = self.soft_shadow_pos = (
|
|||
|
center_x - soft_width / 2,
|
|||
|
center_y - soft_height / 2 - dp(self._elevation * 0.5),
|
|||
|
)
|
|||
|
# Set transparency
|
|||
|
self._soft_shadow_a = 0.1 * 1.05**self._elevation
|
|||
|
self._hard_shadow_a = 0.4 * 0.8**self._elevation
|
|||
|
t = int(round(self._elevation))
|
|||
|
if 0 < t <= 23:
|
|||
|
self._soft_shadow_texture = (
|
|||
|
self._hard_shadow_texture
|
|||
|
) = self._shadow.textures[str(t)]
|
|||
|
else:
|
|||
|
self._soft_shadow_texture = (
|
|||
|
self._hard_shadow_texture
|
|||
|
) = self._shadow.textures["23"]
|
|||
|
else:
|
|||
|
self._soft_shadow_a = 0
|
|||
|
self._hard_shadow_a = 0
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class FakeCircularElevationBehavior(CommonElevationBehavior):
|
|||
|
"""
|
|||
|
`FakeCircularElevationBehavior` is a shadow mockup for widgets. Improves
|
|||
|
performance using cached images inside `kivymd.images` dir
|
|||
|
|
|||
|
This class cast a fake elliptic shadow behaind the widget.
|
|||
|
|
|||
|
You can either use this behavior to overwrite the elevation of a prefab
|
|||
|
widget, or use it directly inside a new widget class definition.
|
|||
|
|
|||
|
Use this class as follows for new widgets:
|
|||
|
|
|||
|
.. code-block:: python
|
|||
|
|
|||
|
class NewWidget(
|
|||
|
ThemableBehavior,
|
|||
|
FakeCircularElevationBehavior,
|
|||
|
SpecificBackgroundColorBehavior,
|
|||
|
# here you add the other front end classes for the widget front_end,
|
|||
|
):
|
|||
|
[...]
|
|||
|
|
|||
|
With this method each class can draw it's content in the canvas in the
|
|||
|
correct order, avoiding some visual errors.
|
|||
|
|
|||
|
`FakeCircularElevationBehavior` will load prefabricated textures to optimize
|
|||
|
loading times.
|
|||
|
|
|||
|
.. note:: About rounded corners:
|
|||
|
be careful, since this behavior is a mockup and will not draw any rounded
|
|||
|
corners. only perfect ellipses.
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
self._shadow = MDApp.get_running_app().theme_cls.round_shadow
|
|||
|
self.draw_shadow = WeakMethod(self.__draw_shadow__)
|
|||
|
self._fake_elevation = True
|
|||
|
self._update_shadow(self, self.elevation)
|
|||
|
super().__init__(**kwargs)
|
|||
|
|
|||
|
def _update_shadow(self, *args):
|
|||
|
if self._elevation > 0:
|
|||
|
# set shadow size
|
|||
|
width = self.width * 2
|
|||
|
height = self.height * 2
|
|||
|
center_x, center_y = self._get_center()
|
|||
|
|
|||
|
x = center_x - width / 2
|
|||
|
self.soft_shadow_size = (width, height)
|
|||
|
self.hard_shadow_size = (width, height)
|
|||
|
# set ``soft_shadow`` parameters
|
|||
|
y = center_y - height / 2 - dp(0.5 * self._elevation)
|
|||
|
self.soft_shadow_pos = (x, y)
|
|||
|
|
|||
|
# set ``hard_shadow`` parameters
|
|||
|
y = center_y - height / 2 - dp(0.5 * self._elevation)
|
|||
|
self.hard_shadow_pos = (x, y)
|
|||
|
|
|||
|
# shadow transparency
|
|||
|
self._soft_shadow_a = 0.1 * 1.05**self._elevation
|
|||
|
self._hard_shadow_a = 0.4 * 0.8**self._elevation
|
|||
|
t = int(round(self._elevation))
|
|||
|
if 0 < t <= 23:
|
|||
|
if hasattr(self, "_shadow"):
|
|||
|
self._soft_shadow_texture = (
|
|||
|
self._hard_shadow_texture
|
|||
|
) = self._shadow.textures[str(t)]
|
|||
|
else:
|
|||
|
self._soft_shadow_texture = (
|
|||
|
self._hard_shadow_texture
|
|||
|
) = self._shadow.textures["23"]
|
|||
|
else:
|
|||
|
self._soft_shadow_a = 0
|
|||
|
self._hard_shadow_a = 0
|
|||
|
|
|||
|
def __draw_shadow__(self, origin, end, context=None):
|
|||
|
pass
|