"""Slide element classes: base class, all element types, and Slide container.""" import pygame from ._drawing import _parse_color from ._enums import Anchor, EntryAnim, _parse_anchor, _parse_entry, _parse_transition class SlideElement: """Base class for all slide elements.""" def __init__(self, pos=(0, 0), anchor="top_left", z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, visible=True, opacity=1.0, name=None, group=None, **kwargs): # Friendly aliases for host integrations. if "anim" in kwargs: entry_anim = kwargs.pop("anim") if "delay" in kwargs: anim_delay = kwargs.pop("delay") if "dur" in kwargs: anim_duration = kwargs.pop("dur") self.pos = pos # normalized (x, y) self.anchor = _parse_anchor(anchor) self.z_order = z_order self.entry_anim = _parse_entry(entry_anim) self.anim_delay = anim_delay self.anim_duration = max(0.05, anim_duration) self.visible = visible self.opacity = opacity self.name = name self.group = group # runtime state self._anim_progress = 0.0 if self.entry_anim != EntryAnim.NONE else 1.0 self._started = False def _resolve_rect(self, elem_w, elem_h, slide_w, slide_h): px = self.pos[0] * slide_w py = self.pos[1] * slide_h a = self.anchor if a in (Anchor.TOP_CENTER, Anchor.CENTER, Anchor.BOTTOM_CENTER): px -= elem_w / 2 elif a in (Anchor.TOP_RIGHT, Anchor.CENTER_RIGHT, Anchor.BOTTOM_RIGHT): px -= elem_w if a in (Anchor.CENTER_LEFT, Anchor.CENTER, Anchor.CENTER_RIGHT): py -= elem_h / 2 elif a in (Anchor.BOTTOM_LEFT, Anchor.BOTTOM_CENTER, Anchor.BOTTOM_RIGHT): py -= elem_h return px, py def _ease(self, t): # ease-out cubic return 1 - (1 - t) ** 3 def _apply_anim(self, surface, dest_x, dest_y, slide_w, slide_h, progress): p = self._ease(progress) anim = self.entry_anim alpha = int(255 * self.opacity * (p if anim in ( EntryAnim.FADE_IN, EntryAnim.ZOOM_IN, EntryAnim.SLIDE_IN_LEFT, EntryAnim.SLIDE_IN_RIGHT, EntryAnim.SLIDE_IN_UP, EntryAnim.SLIDE_IN_DOWN ) else self.opacity)) alpha = max(0, min(255, alpha)) ox, oy = 0, 0 if anim == EntryAnim.SLIDE_IN_LEFT: ox = -(1 - p) * slide_w * 0.15 elif anim == EntryAnim.SLIDE_IN_RIGHT: ox = (1 - p) * slide_w * 0.15 elif anim == EntryAnim.SLIDE_IN_UP: oy = -(1 - p) * slide_h * 0.1 elif anim == EntryAnim.SLIDE_IN_DOWN: oy = (1 - p) * slide_h * 0.1 if anim == EntryAnim.ZOOM_IN and p < 1.0: scale = 0.7 + 0.3 * p new_w = int(surface.get_width() * scale) new_h = int(surface.get_height() * scale) if new_w > 0 and new_h > 0: surface = pygame.transform.smoothscale(surface, (new_w, new_h)) ox += (surface.get_width() - new_w) / 2 * (1 - p) oy += (surface.get_height() - new_h) / 2 * (1 - p) if alpha < 255: surface.set_alpha(alpha) return surface, dest_x + ox, dest_y + oy class TextBox(SlideElement): """A text element with full styling control.""" def __init__(self, text="", pos=(0, 0), anchor="top_left", style="body", color=None, font_size=None, bold=None, italic=False, underline=False, width=None, line_spacing=1.4, align="left", bg_color=None, bg_radius=8, bg_padding=(10, 8), bg_pad=None, shadow=False, border_color=None, border_width=2, bullet=None, bullet_indent=0.02, max_lines=None, z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, opacity=1.0, name=None, group=None, visible=True): if bg_pad is not None: bg_padding = bg_pad if isinstance(bg_padding, (int, float)): bg_padding = (bg_padding, bg_padding) super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.text = text self.style = style self.color = _parse_color(color) if color else None self.font_size = font_size # override; absolute pixels or ratio if < 1 self.bold = bold self.italic = italic self.underline = underline self.width = width # normalized width for wrapping self.line_spacing = line_spacing self.align = align self.bg_color = _parse_color(bg_color) if bg_color else None self.bg_radius = bg_radius self.bg_padding = bg_padding if isinstance(bg_padding, (list, tuple)) else (bg_padding, bg_padding) self.shadow = shadow self.border_color = _parse_color(border_color) if border_color else None self.border_width = border_width self.bullet = bullet self.bullet_indent = bullet_indent self.max_lines = max_lines class ImageBox(SlideElement): """An image element loaded from file path or pygame Surface.""" def __init__(self, source=None, pos=(0, 0), anchor="top_left", size=None, maintain_aspect=True, border_radius=0, border_color=None, border_width=2, shadow=True, opacity=1.0, tint=None, z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, name=None, group=None, visible=True): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.source = source # file path or pygame.Surface self.size = size # (norm_w, norm_h) or None self.maintain_aspect = maintain_aspect self.border_radius = border_radius self.border_color = _parse_color(border_color) if border_color else None self.border_width = border_width self.shadow = shadow self.tint = _parse_color(tint) if tint else None self._cached_surface = None self._cached_key = None class Shape(SlideElement): """A geometric shape: rect, rounded_rect, circle, ellipse, line, polygon.""" def __init__(self, shape_type="rect", pos=(0, 0), size=(0.1, 0.1), anchor="top_left", color="#ffffff", fill=True, border_color=None, border_width=2, radius=12, shadow=False, opacity=1.0, points=None, # for polygon/line: list of (norm_x, norm_y) gradient=None, # (color_top, color_bottom) z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, name=None, group=None, visible=True): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.shape_type = shape_type self.size = size self.color = _parse_color(color) self.fill = fill self.border_color = _parse_color(border_color) if border_color else None self.border_width = border_width self.radius = radius self.shadow = shadow self.gradient = gradient self.points = points class DynamicGraphic(SlideElement): """A dynamic/animated element rendered by a user callback. The callback signature is: def render(surface, rect, dt, elapsed, theme, **kwargs) where: surface = pygame surface to draw on (full slide size) rect = (x, y, w, h) pixel rect allocated for this element dt = delta time since last frame elapsed = total time on this slide theme = current theme dict """ def __init__(self, render_fn=None, pos=(0, 0), size=(0.3, 0.3), anchor="top_left", z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, opacity=1.0, name=None, group=None, visible=True, user_data=None): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.render_fn = render_fn self.size = size self.user_data = user_data or {} class BulletList(SlideElement): """Convenience element: a styled bullet-point list.""" def __init__(self, items=None, pos=(0, 0), anchor="top_left", style="body", color=None, bullet_char="●", bullet_color=None, indent=0.03, item_spacing=1.6, width=None, font_size=None, bold=None, z_order=0, entry_anim="none", stagger_delay=0.15, anim_delay=0.0, anim_duration=0.4, opacity=1.0, name=None, group=None, visible=True, sub_bullet_char="○", nested_indent=0.025): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.items = items or [] self.style = style self.color = _parse_color(color) if color else None self.bullet_char = bullet_char self.bullet_color = _parse_color(bullet_color) if bullet_color else None self.indent = indent self.item_spacing = item_spacing self.width = width self.font_size = font_size self.bold = bold self.stagger_delay = stagger_delay self.sub_bullet_char = sub_bullet_char self.nested_indent = nested_indent class CodeBlock(SlideElement): """A syntax-highlighted (styled) code block.""" def __init__(self, code="", pos=(0, 0), anchor="top_left", width=None, height=None, font_size=None, bg_color=None, text_color=None, border_radius=10, shadow=True, line_numbers=False, z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.5, opacity=1.0, name=None, group=None, visible=True, title=None, title_color=None): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.code = code self.width = width self.height = height self.font_size = font_size self.bg_color = _parse_color(bg_color) if bg_color else None self.text_color = _parse_color(text_color) if text_color else None self.border_radius = border_radius self.shadow = shadow self.line_numbers = line_numbers self.title = title self.title_color = _parse_color(title_color) if title_color else None class ProgressBar(SlideElement): """A progress/percentage bar.""" def __init__(self, value=0.5, pos=(0, 0), anchor="top_left", size=(0.3, 0.02), bg_color=None, fill_color=None, border_radius=6, label=None, label_color=None, animated=True, z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.8, opacity=1.0, name=None, group=None, visible=True): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.value = value self.size = size self.bg_color = _parse_color(bg_color) if bg_color else None self.fill_color = _parse_color(fill_color) if fill_color else None self.border_radius = border_radius self.label = label self.label_color = _parse_color(label_color) if label_color else None self.animated = animated class Divider(SlideElement): """A horizontal or vertical divider line.""" def __init__(self, pos=(0, 0), length=0.9, thickness=2, color=None, orientation="horizontal", anchor="top_left", z_order=0, entry_anim="none", anim_delay=0.0, anim_duration=0.3, opacity=0.5, name=None, group=None, visible=True, gradient_fade=True): super().__init__(pos, anchor, z_order, entry_anim, anim_delay, anim_duration, visible, opacity, name, group) self.length = length self.thickness = thickness self.color = _parse_color(color) if color else None self.orientation = orientation self.gradient_fade = gradient_fade class Slide: """A single slide containing elements.""" def __init__(self, transition="fade", transition_duration=0.4, bg_color=None, bg_gradient=None, bg_image=None, accent_bar=True, accent_bar_pos="bottom", accent_bar_thickness=4, slide_number=True, footer_text=None, on_enter=None, on_exit=None, duration=None, auto_advance=False, name=None): self.elements: list[SlideElement] = [] self.transition = _parse_transition(transition) self.transition_duration = transition_duration self.bg_color = _parse_color(bg_color) if bg_color else None self.bg_gradient = bg_gradient self.bg_image = bg_image self.accent_bar = accent_bar self.accent_bar_pos = accent_bar_pos self.accent_bar_thickness = accent_bar_thickness self.slide_number = slide_number self.footer_text = footer_text self.on_enter = on_enter self.on_exit = on_exit self.duration = duration self.auto_advance = auto_advance self.name = name self._elapsed = 0.0 def add(self, element: SlideElement): self.elements.append(element) return self # chainable def add_all(self, *elements): for e in elements: self.elements.append(e) return self