fixed window size and added area of drag

This commit is contained in:
2026-04-30 10:12:28 -04:00
parent 304886fcd5
commit fa717c5871
2 changed files with 121 additions and 22 deletions

View File

@@ -36,12 +36,13 @@ placing clue numbers, so every puzzle has a solution by construction.
---
## TODO
- [ ] Fix window size to auto resize when changing game mode
- [ ] Show number of cells selected during a mouse drag, before placing
- [ ] Remove ability to select just 1 cell
- [ ] Add some kind of zoom feature
- [x] Fix window size to auto resize when changing game mode
- [x] Show number of cells selected during a mouse drag, before placing
- [x] Remove ability to select just 1 cell
- [x] Add some kind of zoom feature
- Scroll wheel
- and/or a ui button for zoom in/out
- [x] Remove red color from rectangle option
### Possible Features
- Keep local "high score" for each puzzle type

View File

@@ -221,8 +221,8 @@ def verify_solution(size: int, player_rects: list[PlayerRect], clues: dict) -> t
PADDING = 32
# Cell size and font scale down for larger grids so they fit comfortably on screen
CELL_SIZES = {
# Base cell sizes per grid — zoom multiplier is applied on top of these
BASE_CELL_SIZES = {
5: 56,
7: 52,
10: 46,
@@ -230,7 +230,7 @@ CELL_SIZES = {
20: 28,
25: 24,
}
FONT_SIZES = {
BASE_FONT_SIZES = {
5: 14,
7: 13,
10: 12,
@@ -239,11 +239,18 @@ FONT_SIZES = {
25: 7,
}
def cell_size(grid_size: int) -> int:
return CELL_SIZES.get(grid_size, 40)
ZOOM_MIN = 0.5
ZOOM_MAX = 3.0
ZOOM_STEP = 0.15
def font_size(grid_size: int) -> int:
return FONT_SIZES.get(grid_size, 10)
def cell_size(grid_size: int, zoom: float = 1.0) -> int:
return max(8, int(BASE_CELL_SIZES.get(grid_size, 40) * zoom))
def font_size(grid_size: int, zoom: float = 1.0) -> int:
return max(5, int(BASE_FONT_SIZES.get(grid_size, 10) * zoom))
# Approximate height of window chrome (toolbar + size bar + bottom bar + status)
CHROME_HEIGHT = 160
# Palette — deep ink & paper feel
BG_COLOR = (0.11, 0.10, 0.13, 1.0) # near-black background
@@ -258,7 +265,7 @@ RECT_COLORS = [
(0.76, 0.45, 0.82, 0.45), # purple
(0.88, 0.76, 0.30, 0.45), # yellow
(0.45, 0.82, 0.85, 0.45), # cyan
(0.85, 0.42, 0.52, 0.45), # rose
(0.90, 0.65, 0.80, 0.45), # rose
(0.55, 0.72, 0.35, 0.45), # lime
]
RECT_BORDER = [
@@ -268,7 +275,7 @@ RECT_BORDER = [
(0.56, 0.25, 0.65, 1.0),
(0.70, 0.58, 0.08, 1.0),
(0.22, 0.62, 0.68, 1.0),
(0.68, 0.20, 0.32, 1.0),
(0.80, 0.45, 0.65, 1.0),
(0.35, 0.54, 0.18, 1.0),
]
DRAG_COLOR = (0.50, 0.70, 0.95, 0.30)
@@ -281,6 +288,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
def __init__(self, game):
super().__init__()
self.game = game
self.zoom = 1.0
self.set_draw_func(self._draw)
self.set_can_focus(True)
@@ -292,6 +300,8 @@ class ShikakuCanvas(Gtk.DrawingArea):
# Callback fired after every board change — set by the window
self.on_board_changed = None
# Callback fired when zoom changes — set by the window
self.on_zoom_changed = None
# Gestures
drag = Gtk.GestureDrag()
@@ -305,8 +315,14 @@ class ShikakuCanvas(Gtk.DrawingArea):
click.connect("pressed", self._on_right_click)
self.add_controller(click)
# Scroll wheel for zoom
scroll = Gtk.EventControllerScroll()
scroll.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)
scroll.connect("scroll", self._on_scroll)
self.add_controller(scroll)
def _cell_at(self, x, y):
cs = cell_size(self.game.size)
cs = cell_size(self.game.size, self.zoom)
col = int((x - PADDING) // cs)
row = int((y - PADDING) // cs)
size = self.game.size
@@ -339,13 +355,14 @@ class ShikakuCanvas(Gtk.DrawingArea):
sr, sc = self._drag_start
er, ec = self._drag_end
pr = PlayerRect(sr, sc, er, ec)
if pr.area() > 1: # enforce minimum area of 2
self.game.place_rect(pr)
if self.on_board_changed:
self.on_board_changed()
self._drag_start = None
self._drag_end = None
self._drag_preview = None
self.queue_draw()
if self.on_board_changed:
self.on_board_changed()
def _on_right_click(self, gesture, n, x, y):
cell = self._cell_at(x, y)
@@ -355,6 +372,20 @@ class ShikakuCanvas(Gtk.DrawingArea):
if self.on_board_changed:
self.on_board_changed()
def _on_scroll(self, controller, dx, dy):
# Ctrl+scroll or plain scroll both zoom
old_zoom = self.zoom
self.zoom = max(ZOOM_MIN, min(ZOOM_MAX, self.zoom - dy * ZOOM_STEP))
if self.zoom != old_zoom:
if self.on_zoom_changed:
self.on_zoom_changed()
return True # consume event
def set_zoom(self, zoom: float):
self.zoom = max(ZOOM_MIN, min(ZOOM_MAX, zoom))
if self.on_zoom_changed:
self.on_zoom_changed()
def mark_errors(self, error_indices: set[int]):
self._error_rects = error_indices
self.queue_draw()
@@ -367,7 +398,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
size = self.game.size
clues = self.game.clues
player_rects = self.game.player_rects
cs = cell_size(size)
cs = cell_size(size, self.zoom)
grid_w = size * cs
# Background
@@ -410,14 +441,36 @@ class ShikakuCanvas(Gtk.DrawingArea):
ry = PADDING + n.start_row * cs
rw = (n.end_col - n.start_col + 1) * cs
rh = (n.end_row - n.start_row + 1) * cs
cr.set_source_rgba(*DRAG_COLOR)
preview_area = self._drag_preview.area()
is_invalid = preview_area == 1
cr.set_source_rgba(*(ERROR_COLOR if is_invalid else DRAG_COLOR))
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.fill()
cr.set_source_rgba(*DRAG_BORDER)
cr.set_source_rgba(*(ERROR_BORDER if is_invalid else DRAG_BORDER))
cr.set_line_width(2.0)
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
cr.stroke()
# Draw cell count label inside the drag preview
count_layout = self.create_pango_layout("")
count_fs = max(8, int(font_size(size, self.zoom) * 0.95))
count_layout.set_font_description(
Pango.FontDescription(f"Iosevka, Monospace Bold {count_fs}")
)
count_layout.set_text(str(preview_area), -1)
_, ext = count_layout.get_pixel_extents()
tx = rx + rw // 2 - ext.width // 2
ty = ry + rh // 2 - ext.height // 2
# White text with dark shadow for readability on any background
cr.set_source_rgba(0.1, 0.05, 0.15, 0.6)
cr.move_to(tx + 1, ty + 1)
from gi.repository import PangoCairo
PangoCairo.show_layout(cr, count_layout)
cr.set_source_rgba(1.0, 1.0, 1.0, 0.95)
cr.move_to(tx, ty)
PangoCairo.show_layout(cr, count_layout)
# Grid lines
cr.set_line_width(0.8)
cr.set_source_rgba(*GRID_LINE)
@@ -438,7 +491,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
# Clue numbers
layout = self.create_pango_layout("")
fs = font_size(size)
fs = font_size(size, self.zoom)
layout.set_font_description(Pango.FontDescription(f"Iosevka, Monospace Bold {fs}"))
for (r, c), num in clues.items():
layout.set_text(str(num), -1)
@@ -574,6 +627,7 @@ class ShikakuWindow(Gtk.ApplicationWindow):
self.canvas.set_halign(Gtk.Align.CENTER)
self.canvas.set_valign(Gtk.Align.CENTER)
self.canvas.on_board_changed = self._trigger_auto_check
self.canvas.on_zoom_changed = self._on_zoom_changed
scroll.set_child(self.canvas)
outer.append(scroll)
@@ -584,6 +638,31 @@ class ShikakuWindow(Gtk.ApplicationWindow):
botbar.set_margin_bottom(18)
outer.append(botbar)
zoombar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
zoombar.set_halign(Gtk.Align.CENTER)
zoombar.set_margin_bottom(10)
outer.append(zoombar)
zoom_out_btn = Gtk.Button(label="")
zoom_out_btn.add_css_class("zoom-btn")
zoom_out_btn.connect("clicked", self._on_zoom_out)
zoombar.append(zoom_out_btn)
self._zoom_label = Gtk.Label(label="100%")
self._zoom_label.add_css_class("zoom-label")
self._zoom_label.set_width_chars(5)
zoombar.append(self._zoom_label)
zoom_in_btn = Gtk.Button(label="+")
zoom_in_btn.add_css_class("zoom-btn")
zoom_in_btn.connect("clicked", self._on_zoom_in)
zoombar.append(zoom_in_btn)
zoom_reset_btn = Gtk.Button(label="1:1")
zoom_reset_btn.add_css_class("action-btn-ghost")
zoom_reset_btn.connect("clicked", self._on_zoom_reset)
zoombar.append(zoom_reset_btn)
new_btn = Gtk.Button(label="New Puzzle")
new_btn.add_css_class("action-btn")
new_btn.connect("clicked", self._on_new)
@@ -599,6 +678,7 @@ class ShikakuWindow(Gtk.ApplicationWindow):
clear_btn.connect("clicked", self._on_clear)
botbar.append(clear_btn)
self._status_label = Gtk.Label(label="Draw rectangles around each number — right-click to erase")
self._status_label.add_css_class("status-label")
self._status_label.set_margin_top(4)
@@ -706,10 +786,11 @@ class ShikakuWindow(Gtk.ApplicationWindow):
def _update_canvas_size(self):
sz = self.game.size
cs = cell_size(sz)
cs = cell_size(sz, self.canvas.zoom)
total = sz * cs + 2 * PADDING
self.canvas.set_content_width(total)
self.canvas.set_content_height(total)
self.set_default_size(total + 80, total + CHROME_HEIGHT + 60)
def _on_size_clicked(self, btn, size):
self._current_size = size
@@ -717,6 +798,8 @@ class ShikakuWindow(Gtk.ApplicationWindow):
self.canvas.game = self.game
self.canvas.on_board_changed = self._trigger_auto_check
self.canvas._error_rects = set()
self.canvas.zoom = 1.0 # reset zoom on size change
self._zoom_label.set_text("100%") # reset zoom label
self._update_canvas_size()
self._update_size_buttons()
self._status_label.remove_css_class("status-ok")
@@ -781,6 +864,21 @@ class ShikakuWindow(Gtk.ApplicationWindow):
self._timer_label.set_text(f"{m}:{s:02d}")
return True # keep ticking
def _on_zoom_changed(self):
pct = int(self.canvas.zoom * 100)
self._zoom_label.set_text(f"{pct}%")
self._update_canvas_size()
self.canvas.queue_draw()
def _on_zoom_in(self, btn):
self.canvas.set_zoom(self.canvas.zoom + ZOOM_STEP)
def _on_zoom_out(self, btn):
self.canvas.set_zoom(self.canvas.zoom - ZOOM_STEP)
def _on_zoom_reset(self, btn):
self.canvas.set_zoom(1.0)
# ─── Application entry ────────────────────────────────────────────────────────