""" Behaviors/Elevation =================== .. seealso:: `Material Design spec, Elevation `_ .. 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 = ''' adaptive_size: True orientation: "vertical" spacing: "36dp" 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 = ''' : 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 = ''' : 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 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 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 canvas.before: PushMatrix [...] canvas: PopMatrix Correct: .. code-block:: kv 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 or . """ 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