Fix the shell icon picker: real icon size, grid layout, no horizontal scroll
Three real bugs from the first pass, all from actually looking at the running app instead of just the code: - Icons rendered tiny regardless of source resolution: Gtk.Image caps displayed size to GTK's icon-size classes (built for symbolic 16/32px icons) no matter what you load into it. Switched to Gtk.Picture, which sizes by the image's real dimensions. - The popover's plain vertical list of rows squeezed down to a near- unreadable width: ScrolledWindow sizes to its content's minimum, not natural, size unless told otherwise, and a wrapping description label's minimum width can shrink to almost nothing. Replaced with a FlowBox grid (icon + blast radius under it, full description as a tooltip) with an explicit natural width. - That grid's column count was pinned to a fixed minimum, so a popover that didn't have the room for that many columns overflowed sideways and grew a horizontal scrollbar. min_children_per_line dropped to 1 (lets it reflow to fewer columns instead) plus hscrollbar_policy=NEVER as a hard backstop, not everyone has a horizontal scroll wheel. Also: the firing card's shell button now shows the icon alone (it already has the shell's short code baked in, a text label next to it was redundant on an already-tight row), sized to 64px wide/32px tall to actually be legible, and a new small CSS rule trims the excess button chrome around an icon-only face so the button isn't visibly much larger than the icon it holds.
This commit is contained in:
parent
2b73a96c7a
commit
a8f77b98cb
@ -381,8 +381,16 @@ class FiringPanel(Gtk.Box):
|
|||||||
charges: int, elev_value_label: Gtk.Label) -> Gtk.Widget:
|
charges: int, elev_value_label: Gtk.Label) -> Gtk.Widget:
|
||||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||||
|
|
||||||
shell_btn = icons.build_shell_button(target.effective_shell, lambda s: self._pick_shell(target, s))
|
# Icon only, not name-plus-icon: the icon already has the shell's
|
||||||
shell_btn.set_tooltip_text("Change shell (blast radius)")
|
# short code baked in (see assets/icons/README.md), a redundant
|
||||||
|
# text label would just eat width on an already-tight card row.
|
||||||
|
# 64px wide (the source art is a fixed 2:1 rectangle, so 32 tall)
|
||||||
|
# is what it actually takes to read the baked-in code at this
|
||||||
|
# size, anything smaller and it blurs into an unreadable smear.
|
||||||
|
shell_btn = icons.build_shell_button(
|
||||||
|
target.effective_shell, lambda s: self._pick_shell(target, s), show_label=False, icon_width=64
|
||||||
|
)
|
||||||
|
shell_btn.set_tooltip_text(f"{target.effective_shell.name}: change shell (blast radius)")
|
||||||
row.append(shell_btn)
|
row.append(shell_btn)
|
||||||
|
|
||||||
segments: list[Gtk.Button] = []
|
segments: list[Gtk.Button] = []
|
||||||
|
|||||||
@ -12,81 +12,148 @@ from pathlib import Path
|
|||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("GdkPixbuf", "2.0")
|
gi.require_version("Gdk", "4.0")
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
from gi.repository import GdkPixbuf, Gtk # noqa: E402
|
from gi.repository import Gdk, Gtk # noqa: E402
|
||||||
|
|
||||||
from .shells import Shell
|
from .shells import Shell
|
||||||
|
|
||||||
_ICONS_DIR = Path(__file__).resolve().parent.parent.parent / "assets" / "icons"
|
_ICONS_DIR = Path(__file__).resolve().parent.parent.parent / "assets" / "icons"
|
||||||
|
|
||||||
|
_ICON_BUTTON_CSS_CLASS = "fenigma-icon-button"
|
||||||
|
_css_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_icon_button_css() -> None:
|
||||||
|
"""A plain 'flat' Gtk.Button still carries libadwaita's normal button
|
||||||
|
padding/min-size, fine for a text label, way too much empty chrome
|
||||||
|
around a single icon (the button ends up visibly larger than the
|
||||||
|
icon it holds). Loaded lazily (not at import time) and only once, a
|
||||||
|
headless import (e.g. from a test) shouldn't need a live display."""
|
||||||
|
global _css_loaded
|
||||||
|
if _css_loaded:
|
||||||
|
return
|
||||||
|
display = Gdk.Display.get_default()
|
||||||
|
if display is None:
|
||||||
|
return
|
||||||
|
provider = Gtk.CssProvider()
|
||||||
|
provider.load_from_string(f".{_ICON_BUTTON_CSS_CLASS} {{ padding: 2px; min-width: 0; min-height: 0; }}")
|
||||||
|
Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||||
|
_css_loaded = True
|
||||||
|
|
||||||
|
|
||||||
def shell_icon_path(shell_name: str) -> Path:
|
def shell_icon_path(shell_name: str) -> Path:
|
||||||
return _ICONS_DIR / "shells" / f"{shell_name}.png"
|
return _ICONS_DIR / "shells" / f"{shell_name}.png"
|
||||||
|
|
||||||
|
|
||||||
def shell_icon_image(shell_name: str, width: int = 64) -> Gtk.Image:
|
def shell_icon_image(shell_name: str, width: int = 64) -> Gtk.Widget:
|
||||||
"""A Gtk.Image for a Shell enum member's icon, scaled to `width` px
|
"""A widget showing a Shell enum member's icon at `width` px wide
|
||||||
wide (the source art is a fixed 2:1 rectangle, height follows).
|
(the source art is a fixed 2:1 rectangle, height follows). Gtk.Image
|
||||||
|
looked right in code but rendered tiny regardless of the source
|
||||||
|
file's real resolution, it caps displayed size to GTK's icon-size
|
||||||
|
classes (meant for symbolic 16/32px icons) no matter what you load
|
||||||
|
into it. Gtk.Picture doesn't do that, it sizes by the image's actual
|
||||||
|
dimensions, which is what a real multi-hundred-pixel icon needs.
|
||||||
Falls back to a generic missing-image icon rather than raising, a
|
Falls back to a generic missing-image icon rather than raising, a
|
||||||
gap in the icon set shouldn't crash the shell picker."""
|
gap in the icon set shouldn't crash the shell picker."""
|
||||||
path = shell_icon_path(shell_name)
|
path = shell_icon_path(shell_name)
|
||||||
|
height = width // 2
|
||||||
if path.exists():
|
if path.exists():
|
||||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(path), width, width // 2, False)
|
picture = Gtk.Picture.new_for_filename(str(path))
|
||||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
picture.set_content_fit(Gtk.ContentFit.CONTAIN)
|
||||||
|
picture.set_can_shrink(True)
|
||||||
|
picture.set_size_request(width, height)
|
||||||
|
return picture
|
||||||
image = Gtk.Image.new_from_icon_name("image-missing-symbolic")
|
image = Gtk.Image.new_from_icon_name("image-missing-symbolic")
|
||||||
image.set_pixel_size(width // 2)
|
image.set_pixel_size(min(width, height))
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def _shell_row(s: Shell) -> Gtk.Widget:
|
_GRID_ICON_WIDTH = 88 # per-cell icon in the picker grid, large enough to actually read
|
||||||
radius = f"{s.blast_radius_km}km blast radius" if s.blast_radius_km is not None else "blast radius unknown"
|
_GRID_COLUMNS = 4
|
||||||
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
||||||
content.append(shell_icon_image(s.name, width=56))
|
|
||||||
label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0, hexpand=True)
|
def _shell_radius_text(s: Shell) -> str:
|
||||||
name_label = Gtk.Label(label=s.name, xalign=0)
|
return f"{s.blast_radius_km}km" if s.blast_radius_km is not None else "unknown radius"
|
||||||
name_label.add_css_class("heading")
|
|
||||||
desc_label = Gtk.Label(label=f"{s.description} · {radius}", xalign=0, wrap=True)
|
|
||||||
desc_label.add_css_class("caption")
|
def _shell_cell(s: Shell) -> Gtk.Widget:
|
||||||
desc_label.add_css_class("dim-label")
|
cell = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2,
|
||||||
label_box.append(name_label)
|
margin_top=4, margin_bottom=4, margin_start=4, margin_end=4)
|
||||||
label_box.append(desc_label)
|
cell.append(shell_icon_image(s.name, width=_GRID_ICON_WIDTH))
|
||||||
content.append(label_box)
|
radius_label = Gtk.Label(label=_shell_radius_text(s))
|
||||||
return content
|
radius_label.add_css_class("caption")
|
||||||
|
radius_label.add_css_class("dim-label")
|
||||||
|
cell.append(radius_label)
|
||||||
|
return cell
|
||||||
|
|
||||||
|
|
||||||
def build_shell_popover(on_pick) -> Gtk.Popover:
|
def build_shell_popover(on_pick) -> Gtk.Popover:
|
||||||
"""Popover listing every Shell with its icon, name, description, and
|
"""Popover with a grid of every Shell (icon + blast radius under
|
||||||
blast radius, replacing a plain text dropdown/list with something
|
it, full description as a tooltip), replacing a plain text dropdown/
|
||||||
that actually shows what each shell looks like. Calls `on_pick(shell)`
|
list with something that actually shows what each shell looks like.
|
||||||
and closes itself when a row is clicked."""
|
A plain vertical box of rows here previously ended up squeezed down
|
||||||
|
to a near-unreadable width, ScrolledWindow sizes to its content's
|
||||||
|
*minimum*, not natural, size unless told otherwise, and a wrapping
|
||||||
|
description label's minimum width can shrink to almost nothing. The
|
||||||
|
grid avoids that by giving the FlowBox a sensible natural width up
|
||||||
|
front (cell width * column count) instead of hoping the content asks
|
||||||
|
for enough space on its own. min_children_per_line is deliberately
|
||||||
|
NOT pinned to the same column count: a fixed minimum forces that
|
||||||
|
many columns even when the popover doesn't actually have the room
|
||||||
|
(near a screen edge, a narrow window), which used to overflow
|
||||||
|
sideways and need a horizontal scrollbar to reach. Letting it reflow
|
||||||
|
down to fewer columns instead, plus hscrollbar_policy=NEVER as a
|
||||||
|
hard backstop, means scrolling here is always vertical only, no
|
||||||
|
horizontal scroll wheel required (plenty of mice don't have one).
|
||||||
|
Calls `on_pick(shell)` and closes itself when a cell is clicked."""
|
||||||
popover = Gtk.Popover()
|
popover = Gtk.Popover()
|
||||||
scroller = Gtk.ScrolledWindow(max_content_height=420, propagate_natural_height=True)
|
flow = Gtk.FlowBox(
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2,
|
min_children_per_line=1, max_children_per_line=_GRID_COLUMNS,
|
||||||
margin_top=6, margin_bottom=6, margin_start=6, margin_end=6)
|
row_spacing=4, column_spacing=4, homogeneous=True,
|
||||||
|
selection_mode=Gtk.SelectionMode.NONE,
|
||||||
|
margin_top=8, margin_bottom=8, margin_start=8, margin_end=8,
|
||||||
|
)
|
||||||
|
_ensure_icon_button_css()
|
||||||
for s in Shell:
|
for s in Shell:
|
||||||
row = Gtk.Button(child=_shell_row(s))
|
btn = Gtk.Button(child=_shell_cell(s))
|
||||||
row.add_css_class("flat")
|
btn.add_css_class("flat")
|
||||||
row.connect("clicked", lambda _b, s=s: (popover.popdown(), on_pick(s)))
|
btn.add_css_class(_ICON_BUTTON_CSS_CLASS)
|
||||||
box.append(row)
|
btn.set_tooltip_text(f"{s.name}: {s.description} ({_shell_radius_text(s)} blast radius)")
|
||||||
scroller.set_child(box)
|
btn.connect("clicked", lambda _b, s=s: (popover.popdown(), on_pick(s)))
|
||||||
|
flow.append(btn)
|
||||||
|
scroller = Gtk.ScrolledWindow(
|
||||||
|
max_content_height=440, propagate_natural_height=True, propagate_natural_width=True,
|
||||||
|
hscrollbar_policy=Gtk.PolicyType.NEVER,
|
||||||
|
)
|
||||||
|
scroller.set_min_content_width(_GRID_COLUMNS * (_GRID_ICON_WIDTH + 16))
|
||||||
|
scroller.set_child(flow)
|
||||||
popover.set_child(scroller)
|
popover.set_child(scroller)
|
||||||
return popover
|
return popover
|
||||||
|
|
||||||
|
|
||||||
def build_shell_button(selected: Shell, on_pick) -> Gtk.MenuButton:
|
def build_shell_button(selected: Shell, on_pick, *, show_label: bool = True, icon_width: int = 28) -> Gtk.MenuButton:
|
||||||
"""A flat MenuButton showing the currently selected shell's icon and
|
"""A flat MenuButton showing the currently selected shell's icon
|
||||||
name, opening build_shell_popover() to change it. `on_pick(shell)`
|
(plus its name, unless `show_label` is False, the icon already has
|
||||||
fires on selection, after this button's own face has already been
|
the shell's short code baked in, redundant next to a firing card
|
||||||
updated to match, the caller only needs to react to the new value
|
that's tight on space), opening build_shell_popover() to change it.
|
||||||
(persist it, refresh dependents), not maintain the button's display."""
|
`on_pick(shell)` fires on selection, after this button's own face
|
||||||
|
has already been updated to match, the caller only needs to react
|
||||||
|
to the new value (persist it, refresh dependents), not maintain the
|
||||||
|
button's display."""
|
||||||
btn = Gtk.MenuButton()
|
btn = Gtk.MenuButton()
|
||||||
btn.add_css_class("flat")
|
btn.add_css_class("flat")
|
||||||
|
if not show_label:
|
||||||
|
_ensure_icon_button_css()
|
||||||
|
btn.add_css_class(_ICON_BUTTON_CSS_CLASS)
|
||||||
|
|
||||||
def render(s: Shell) -> None:
|
def render(s: Shell) -> None:
|
||||||
|
if show_label:
|
||||||
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
content.append(shell_icon_image(s.name, width=28))
|
content.append(shell_icon_image(s.name, width=icon_width))
|
||||||
content.append(Gtk.Label(label=s.name))
|
content.append(Gtk.Label(label=s.name))
|
||||||
|
else:
|
||||||
|
content = shell_icon_image(s.name, width=icon_width)
|
||||||
btn.set_child(content)
|
btn.set_child(content)
|
||||||
|
|
||||||
def handle_pick(s: Shell) -> None:
|
def handle_pick(s: Shell) -> None:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user