Files
Flora/ui/scaling.py
T

150 lines
5.1 KiB
Python

import sys
import ctypes
from ctypes import wintypes
import pygame
from assets import plot_icon_path, rune_icon_path, type_icon_path
# Set when the user closes the window (X button). Screens keep running loops,
# but the top-level driver (main.py) checks this after every screen and bails
# out of the whole app so closing never lands you on another menu.
QUIT_REQUESTED = False
_desktop = None
def request_quit():
global QUIT_REQUESTED
QUIT_REQUESTED = True
def _work_area():
"""Usable desktop area (excludes the Windows taskbar), or None if unavailable."""
if sys.platform != "win32":
return None
try:
rect = wintypes.RECT()
ctypes.windll.user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(rect), 0) # SPI_GETWORKAREA
return rect.right - rect.left, rect.bottom - rect.top
except Exception:
return None
def _desktop_size():
"""The actual monitor resolution, cached after the first call. On macOS,
pygame.display.Info() reports the *current window* size, so using it here
makes each make_window() treat the last window as the available space and
the window shrinks on every screen transition. get_desktop_sizes() is
window-independent."""
global _desktop
if _desktop is None:
sizes = pygame.display.get_desktop_sizes()
if sizes:
_desktop = sizes[0]
else:
info = pygame.display.Info()
_desktop = (info.current_w, info.current_h)
return _desktop
def make_window(logical_w: int, logical_h: int):
"""Create a window sized to fit the usable desktop (downscales only, keeps
aspect ratio). Returns (window_surface, scale_factor). Render at the logical
size and smoothscale into the window each frame; divide mouse coordinates by
`scale` to get logical coordinates."""
pygame.display.init()
area = _work_area() or _desktop_size()
avail_w = max(1, area[0] - 24) # window border safety
avail_h = max(1, area[1] - 48) # title bar safety
scale = min(1.0, avail_w / logical_w, avail_h / logical_h)
scale = max(0.2, scale)
size = (int(logical_w * scale), int(logical_h * scale))
window = pygame.display.set_mode(size)
return window, scale
def to_logical(pos, scale: float):
return (int(pos[0] / scale), int(pos[1] / scale))
def fit_surface(img, size):
"""Scale `img` to fit within a `size`x`size` box, preserving aspect ratio.
Square sources become `size`x`size`; non-square sources shrink the smaller
axis so nothing is stretched or squished (e.g. a portrait water drop)."""
w, h = img.get_size()
scale = size / max(w, h)
nw = max(1, int(round(w * scale)))
nh = max(1, int(round(h * scale)))
return pygame.transform.smoothscale(img, (nw, nh))
_TYPE_ICON_CACHE = {}
_PLOT_ICON_CACHE = {}
_RUNE_ICON_CACHE = {}
def load_type_icon(ptype: str, size: int):
"""Plant type icon scaled to fit a `size` box (aspect preserved), cached."""
key = (ptype, size)
if key not in _TYPE_ICON_CACHE:
try:
img = pygame.image.load(type_icon_path(ptype)).convert_alpha()
_TYPE_ICON_CACHE[key] = fit_surface(img, size)
except (pygame.error, OSError):
_TYPE_ICON_CACHE[key] = None
return _TYPE_ICON_CACHE[key]
def load_plot_icon(name: str, size: int):
"""Soil plot sprite scaled to fit a `size` box (aspect preserved), cached."""
key = (name, size)
if key not in _PLOT_ICON_CACHE:
try:
img = pygame.image.load(plot_icon_path(name)).convert_alpha()
_PLOT_ICON_CACHE[key] = fit_surface(img, size)
except (pygame.error, OSError):
_PLOT_ICON_CACHE[key] = None
return _PLOT_ICON_CACHE[key]
def load_rune_icon(color: str, size: int):
"""Rune sprite scaled to fit a `size` box (aspect preserved), cached. Falls
back to the brown rune sprite if a color's icon is missing."""
key = (color, size)
if key not in _RUNE_ICON_CACHE:
try:
img = pygame.image.load(rune_icon_path(color)).convert_alpha()
_RUNE_ICON_CACHE[key] = fit_surface(img, size)
except (pygame.error, OSError):
_RUNE_ICON_CACHE[key] = None
return _RUNE_ICON_CACHE[key]
def draw_type_icon(screen, ptype, pos, font, color, size, center=False):
"""Draw a plant type icon (or the type name as fallback text) at `pos`,
returning its rect. Uses midleft alignment unless `center` is True."""
ic = load_type_icon(ptype, size)
if ic is None:
surf = font.render(ptype, True, color)
rect = surf.get_rect(center=pos) if center else surf.get_rect(midleft=pos)
screen.blit(surf, rect)
return rect
rect = ic.get_rect(center=pos) if center else ic.get_rect(midleft=pos)
screen.blit(ic, rect)
return rect
def wrap_text(text, font, width):
"""Split `text` into lines that each fit within `width` pixels."""
words = text.split()
lines = []
cur = ""
for w in words:
if font.size(cur + " " + w)[0] > width and cur:
lines.append(cur)
cur = w
else:
cur = (cur + " " + w).strip()
if cur:
lines.append(cur)
return lines