EG/ui/Builtin/LUISlider.py
2026-02-25 11:49:31 +08:00

215 lines
8.3 KiB
Python

from LUIObject import LUIObject
from LUISprite import LUISprite
from LUIInitialState import LUIInitialState
from LUILayouts import LUIHorizontalStretchedLayout
class LUISlider(LUIObject):
""" Slider which can be used to control values """
KNOB_Y_OFFSET = -5.0
def __init__(self, parent=None, filled=True, min_value=0.0, max_value=1.0, width=100.0, value=None, **kwargs):
""" Constructs a new slider. If filled is True, the part behind the knob
will be solid """
LUIObject.__init__(self, x=0, y=0, solid=True)
self.set_width(width)
self._knob = LUISprite(self, "SliderKnob", "skin")
self._knob.z_offset = 2
self._knob.solid = True
# Construct the background
self._slider_bg = LUIHorizontalStretchedLayout(parent=self, prefix="SliderBg", center_vertical=True, width="100%", margin=(-1, 0, 0, 0))
self._filled = filled
self._min_value = min_value
self._max_value = max_value
self._side_margin = self._knob.width / 4
self._effective_width = self.width - 2 * self._side_margin
if self._filled:
self._slider_fill = LUIObject(self)
self._fill_left = LUISprite(self._slider_fill, "SliderBgFill_Left", "skin")
self._fill_mid = LUISprite(self._slider_fill, "SliderBgFill", "skin")
self._fill_mid.left = self._fill_left.width
self._slider_fill.z_offset = 1
self._slider_fill.center_vertical = True
self._slider_fill.width = "100%"
self._slider_fill.height = "100%"
if parent is not None:
self.parent = parent
# Handle various events
self._knob.bind("mousedown", self._start_drag)
self._knob.bind("mousemove", self._update_drag)
self._knob.bind("mouseup", self._stop_drag)
self._knob.bind("keydown", self._on_keydown)
self._knob.bind("blur", self._stop_drag)
self._knob.bind("keyrepeat", self._on_keydown)
self._drag_start_pos = None
self._dragging = False
self._drag_start_val = 0
self.current_val = 10
# Set initial value
if value is None:
self.set_value( (self._min_value + self._max_value) / 2.0 )
else:
self.set_value(value)
self._update_knob()
LUIInitialState.init(self, kwargs)
self._refresh_layout()
def on_click(self, event):
""" Internal on click handler """
# I don't like this behaviour
# relative_pos = self.get_relative_pos(event.coordinates)
# if not self._dragging:
# self._set_current_val(relative_pos.x)
def _update_knob(self):
""" Internal method to update the slider knob """
self._knob.left = self.current_val - (self._knob.width / 2) + self._side_margin
if self._filled:
knob_center = self._knob.left + (self._knob.width / 2)
fill_len = max(0, knob_center - self._fill_left.width)
self._fill_mid.width = fill_len
if hasattr(self._slider_fill, 'left'):
self._slider_fill.left = 0
def _refresh_layout(self):
"""Recalculate slider layout after size changes."""
# Guard for early init before internal parts are created
if not hasattr(self, "_knob"):
return
try:
# Use half knob width so knob center aligns with bar ends
self._side_margin = self._knob.width / 2
except Exception:
self._side_margin = 0
try:
self._effective_width = max(1, self.width - (self._knob.width if hasattr(self, "_knob") else 0))
except Exception:
self._effective_width = 1
if hasattr(self, "_slider_bg"):
if hasattr(self._slider_bg, 'width'):
self._slider_bg.width = "100%"
if hasattr(self._slider_bg, 'height'):
self._slider_bg.height = "100%"
if hasattr(self._slider_bg, 'center_vertical'):
self._slider_bg.center_vertical = True
if hasattr(self._knob, 'center_vertical'):
self._knob.center_vertical = True
# Explicit vertical centering for knob to avoid mismatch after resize
try:
base_h = self.height
base_top = 0
if hasattr(self, "_slider_bg") and self._slider_bg is not None:
bg_h = getattr(self._slider_bg, "height", None)
if bg_h is not None and bg_h != "100%":
base_h = bg_h
base_top = getattr(self._slider_bg, "top", 0) or 0
if hasattr(self._knob, "height"):
# Visual offset compensates for the skin's knob pivot
self._knob.top = (base_h - self._knob.height) / 2.0 + self.KNOB_Y_OFFSET
except Exception:
pass
if self._filled and hasattr(self, '_slider_fill'):
self._slider_fill.center_vertical = True
try:
base_h = self.height
base_top = 0
if hasattr(self, "_slider_bg") and self._slider_bg is not None:
bg_h = getattr(self._slider_bg, "height", None)
if bg_h is not None and bg_h != "100%":
base_h = bg_h
base_top = getattr(self._slider_bg, "top", 0) or 0
if hasattr(self._slider_fill, 'height'):
self._slider_fill.top = base_top + (base_h - self._slider_fill.height) / 2.0 + self.KNOB_Y_OFFSET
except Exception:
pass
if hasattr(self._slider_fill, 'width'):
self._slider_fill.width = "100%"
if hasattr(self._slider_fill, 'height'):
self._slider_fill.height = "100%"
try:
self.current_val = max(0, min(self.current_val, self._effective_width))
except Exception:
pass
self._update_knob()
def set_width(self, width):
"""Set slider width and refresh layout."""
self.width = width
if hasattr(self, "_slider_bg"):
self._refresh_layout()
def set_height(self, height):
"""Set slider height and refresh layout."""
self.height = height
if hasattr(self, "_slider_bg"):
self._refresh_layout()
def _set_current_val(self, pixels):
""" Internal method to set the current value in pixels """
pixels = max(0, min(self._effective_width, pixels))
self.current_val = pixels
self.trigger_event("changed")
self._update_knob()
def _start_drag(self, event):
""" Internal drag start handler """
self._knob.request_focus()
if not self._dragging:
self._drag_start_pos = event.coordinates
self._dragging = True
self._drag_start_val = self.current_val
self._knob.color = (0.8,0.8,0.8,1.0)
def set_value(self, value):
""" Sets the value of the slider, should be between minimum and maximum. """
scaled = (float(value) - float(self._min_value)) \
/ (float(self._max_value) - float(self._min_value)) \
* self._effective_width
self._set_current_val(scaled)
def get_value(self):
""" Returns the current value of the slider """
return (self.current_val / float(self._effective_width)) \
* (float(self._max_value) - float(self._min_value)) \
+ self._min_value
value = property(get_value, set_value)
def _on_keydown(self, event):
""" Internal keydown handler """
if event.message == "arrow_right":
self._set_current_val(self.current_val + 2)
elif event.message == "arrow_left":
self._set_current_val(self.current_val - 2)
elif event.message == "escape":
self.current_val = self._drag_start_val
self._stop_drag(event)
self._update_knob()
def _update_drag(self, event):
""" Internal drag handler """
if self._dragging:
dragOffset = event.coordinates.x - self._drag_start_pos.x
finalValue = self._drag_start_val + dragOffset
self._set_current_val(finalValue)
def _stop_drag(self, event):
""" Internal drag stop handelr """
self._drag_start_pos = None
self._dragging = False
self._drag_start_val = self.current_val
self._knob.color = (1,1,1,1)