@@ -707,26 +707,36 @@ async def _paint_reveal(self, progress):
707707
708708
709709class DropFromSky :
710- """New content slides in from the top: text starts at y=0 and falls to its
711- natural y position over FALL_FRAMES frames.
710+ """New content slides into place from an edge — by default from the TOP (text
711+ starts above its row and falls to its natural y over FALL_FRAMES frames). Set
712+ ``direction`` to "top" (default), "bottom", "left", or "right" to slide it in
713+ from that edge instead.
712714
713715 Works by hooking into _display_process *before* content.render() via
714- pre_render_hook(), which temporarily sets content.y to the animated
715- position. render() then restores y so state never drifts.
716+ pre_render_hook(), which temporarily sets content.x / content. y to the animated
717+ position. render() then restores it so state never drifts.
716718
717- No OverlayMask, no double-rendering, no layer-order issues. The Labels
718- are positioned correctly from the first draw call each frame.
719+ No OverlayMask, no double-rendering, no layer-order issues. The Labels are
720+ positioned correctly from the first draw call each frame.
719721 """
720722
721723 FALL_FRAMES = 10
724+ _DIRECTIONS = ("top" , "bottom" , "left" , "right" )
722725
723- def __init__ (self ):
726+ def __init__ (self , direction = "top" ):
727+ self .direction = direction if direction in self ._DIRECTIONS else "top"
724728 self ._frame = 0
729+ self ._target_x = None
725730 self ._target_y = None
731+ self ._w = 0
732+ self ._h = 0
726733 self ._is_complete = False
727734
728735 async def start (self , display , swap_callback ):
736+ self ._w = display .width
737+ self ._h = display .height
729738 self ._frame = 0
739+ self ._target_x = None
730740 self ._target_y = None
731741 self ._is_complete = False
732742
@@ -735,24 +745,36 @@ def is_complete(self):
735745 return self ._is_complete
736746
737747 def pre_render_hook (self , content ):
738- """Set content.y to the animated position BEFORE content.render()."""
748+ """Set content.x/ y to the animated entry position BEFORE content.render()."""
739749 if self ._is_complete :
740750 return
741751 if self ._target_y is None :
742752 self ._target_y = getattr (content , 'y' , 0 )
743- # y travels from 0 (frame 0) to _target_y (frame FALL_FRAMES)
753+ self ._target_x = getattr (content , 'x' , 0 )
754+ # 0 -> 1 over the slide; only the axis for `direction` moves, from an
755+ # off-screen edge to the content's natural position.
744756 progress = min (1.0 , self ._frame / self .FALL_FRAMES )
745- if hasattr (content , 'y' ):
746- content .y = int (self ._target_y * progress )
757+ d = self .direction
758+ if d == "top" or d == "bottom" :
759+ start_y = 0 if d == "top" else self ._h
760+ if hasattr (content , 'y' ):
761+ content .y = int (start_y + (self ._target_y - start_y ) * progress )
762+ else : # left / right
763+ start_x = - self ._w if d == "left" else self ._w
764+ if hasattr (content , 'x' ):
765+ content .x = int (start_x + (self ._target_x - start_x ) * progress )
747766
748767 async def render (self , display , content = None ):
749- """Restore content.y after render and advance frame counter."""
768+ """Restore content.x/ y after render and advance the frame counter."""
750769 if self ._is_complete :
751770 return
752- # Restore y so the content object always carries its natural position .
771+ # Restore whichever axis we animated to its natural value .
753772 if content is not None and self ._target_y is not None :
754- if hasattr (content , 'y' ):
755- content .y = self ._target_y
773+ if self .direction == "top" or self .direction == "bottom" :
774+ if hasattr (content , 'y' ):
775+ content .y = self ._target_y
776+ elif hasattr (content , 'x' ) and self ._target_x is not None :
777+ content .x = self ._target_x
756778 self ._frame += 1
757779 if self ._frame > self .FALL_FRAMES :
758780 self ._is_complete = True
0 commit comments