fixed window size and added area of drag
This commit is contained in:
@@ -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
|
||||
|
||||
132
shikaku.py
132
shikaku.py
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user