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
|
## TODO
|
||||||
- [ ] Fix window size to auto resize when changing game mode
|
- [x] Fix window size to auto resize when changing game mode
|
||||||
- [ ] Show number of cells selected during a mouse drag, before placing
|
- [x] Show number of cells selected during a mouse drag, before placing
|
||||||
- [ ] Remove ability to select just 1 cell
|
- [x] Remove ability to select just 1 cell
|
||||||
- [ ] Add some kind of zoom feature
|
- [x] Add some kind of zoom feature
|
||||||
- Scroll wheel
|
- Scroll wheel
|
||||||
- and/or a ui button for zoom in/out
|
- and/or a ui button for zoom in/out
|
||||||
|
- [x] Remove red color from rectangle option
|
||||||
|
|
||||||
### Possible Features
|
### Possible Features
|
||||||
- Keep local "high score" for each puzzle type
|
- Keep local "high score" for each puzzle type
|
||||||
|
|||||||
134
shikaku.py
134
shikaku.py
@@ -221,8 +221,8 @@ def verify_solution(size: int, player_rects: list[PlayerRect], clues: dict) -> t
|
|||||||
|
|
||||||
PADDING = 32
|
PADDING = 32
|
||||||
|
|
||||||
# Cell size and font scale down for larger grids so they fit comfortably on screen
|
# Base cell sizes per grid — zoom multiplier is applied on top of these
|
||||||
CELL_SIZES = {
|
BASE_CELL_SIZES = {
|
||||||
5: 56,
|
5: 56,
|
||||||
7: 52,
|
7: 52,
|
||||||
10: 46,
|
10: 46,
|
||||||
@@ -230,7 +230,7 @@ CELL_SIZES = {
|
|||||||
20: 28,
|
20: 28,
|
||||||
25: 24,
|
25: 24,
|
||||||
}
|
}
|
||||||
FONT_SIZES = {
|
BASE_FONT_SIZES = {
|
||||||
5: 14,
|
5: 14,
|
||||||
7: 13,
|
7: 13,
|
||||||
10: 12,
|
10: 12,
|
||||||
@@ -239,11 +239,18 @@ FONT_SIZES = {
|
|||||||
25: 7,
|
25: 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
def cell_size(grid_size: int) -> int:
|
ZOOM_MIN = 0.5
|
||||||
return CELL_SIZES.get(grid_size, 40)
|
ZOOM_MAX = 3.0
|
||||||
|
ZOOM_STEP = 0.15
|
||||||
|
|
||||||
def font_size(grid_size: int) -> int:
|
def cell_size(grid_size: int, zoom: float = 1.0) -> int:
|
||||||
return FONT_SIZES.get(grid_size, 10)
|
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
|
# Palette — deep ink & paper feel
|
||||||
BG_COLOR = (0.11, 0.10, 0.13, 1.0) # near-black background
|
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.76, 0.45, 0.82, 0.45), # purple
|
||||||
(0.88, 0.76, 0.30, 0.45), # yellow
|
(0.88, 0.76, 0.30, 0.45), # yellow
|
||||||
(0.45, 0.82, 0.85, 0.45), # cyan
|
(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
|
(0.55, 0.72, 0.35, 0.45), # lime
|
||||||
]
|
]
|
||||||
RECT_BORDER = [
|
RECT_BORDER = [
|
||||||
@@ -268,7 +275,7 @@ RECT_BORDER = [
|
|||||||
(0.56, 0.25, 0.65, 1.0),
|
(0.56, 0.25, 0.65, 1.0),
|
||||||
(0.70, 0.58, 0.08, 1.0),
|
(0.70, 0.58, 0.08, 1.0),
|
||||||
(0.22, 0.62, 0.68, 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),
|
(0.35, 0.54, 0.18, 1.0),
|
||||||
]
|
]
|
||||||
DRAG_COLOR = (0.50, 0.70, 0.95, 0.30)
|
DRAG_COLOR = (0.50, 0.70, 0.95, 0.30)
|
||||||
@@ -281,6 +288,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
def __init__(self, game):
|
def __init__(self, game):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
|
self.zoom = 1.0
|
||||||
self.set_draw_func(self._draw)
|
self.set_draw_func(self._draw)
|
||||||
self.set_can_focus(True)
|
self.set_can_focus(True)
|
||||||
|
|
||||||
@@ -292,6 +300,8 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
|
|
||||||
# Callback fired after every board change — set by the window
|
# Callback fired after every board change — set by the window
|
||||||
self.on_board_changed = None
|
self.on_board_changed = None
|
||||||
|
# Callback fired when zoom changes — set by the window
|
||||||
|
self.on_zoom_changed = None
|
||||||
|
|
||||||
# Gestures
|
# Gestures
|
||||||
drag = Gtk.GestureDrag()
|
drag = Gtk.GestureDrag()
|
||||||
@@ -305,8 +315,14 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
click.connect("pressed", self._on_right_click)
|
click.connect("pressed", self._on_right_click)
|
||||||
self.add_controller(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):
|
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)
|
col = int((x - PADDING) // cs)
|
||||||
row = int((y - PADDING) // cs)
|
row = int((y - PADDING) // cs)
|
||||||
size = self.game.size
|
size = self.game.size
|
||||||
@@ -339,13 +355,14 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
sr, sc = self._drag_start
|
sr, sc = self._drag_start
|
||||||
er, ec = self._drag_end
|
er, ec = self._drag_end
|
||||||
pr = PlayerRect(sr, sc, er, ec)
|
pr = PlayerRect(sr, sc, er, ec)
|
||||||
self.game.place_rect(pr)
|
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_start = None
|
||||||
self._drag_end = None
|
self._drag_end = None
|
||||||
self._drag_preview = None
|
self._drag_preview = None
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
if self.on_board_changed:
|
|
||||||
self.on_board_changed()
|
|
||||||
|
|
||||||
def _on_right_click(self, gesture, n, x, y):
|
def _on_right_click(self, gesture, n, x, y):
|
||||||
cell = self._cell_at(x, y)
|
cell = self._cell_at(x, y)
|
||||||
@@ -355,6 +372,20 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
if self.on_board_changed:
|
if self.on_board_changed:
|
||||||
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]):
|
def mark_errors(self, error_indices: set[int]):
|
||||||
self._error_rects = error_indices
|
self._error_rects = error_indices
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
@@ -367,7 +398,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
size = self.game.size
|
size = self.game.size
|
||||||
clues = self.game.clues
|
clues = self.game.clues
|
||||||
player_rects = self.game.player_rects
|
player_rects = self.game.player_rects
|
||||||
cs = cell_size(size)
|
cs = cell_size(size, self.zoom)
|
||||||
grid_w = size * cs
|
grid_w = size * cs
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
@@ -410,14 +441,36 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
ry = PADDING + n.start_row * cs
|
ry = PADDING + n.start_row * cs
|
||||||
rw = (n.end_col - n.start_col + 1) * cs
|
rw = (n.end_col - n.start_col + 1) * cs
|
||||||
rh = (n.end_row - n.start_row + 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)
|
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
|
||||||
cr.fill()
|
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)
|
cr.set_line_width(2.0)
|
||||||
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
|
_rounded_rect(cr, rx + 2, ry + 2, rw - 4, rh - 4, 6)
|
||||||
cr.stroke()
|
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
|
# Grid lines
|
||||||
cr.set_line_width(0.8)
|
cr.set_line_width(0.8)
|
||||||
cr.set_source_rgba(*GRID_LINE)
|
cr.set_source_rgba(*GRID_LINE)
|
||||||
@@ -438,7 +491,7 @@ class ShikakuCanvas(Gtk.DrawingArea):
|
|||||||
|
|
||||||
# Clue numbers
|
# Clue numbers
|
||||||
layout = self.create_pango_layout("")
|
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}"))
|
layout.set_font_description(Pango.FontDescription(f"Iosevka, Monospace Bold {fs}"))
|
||||||
for (r, c), num in clues.items():
|
for (r, c), num in clues.items():
|
||||||
layout.set_text(str(num), -1)
|
layout.set_text(str(num), -1)
|
||||||
@@ -574,6 +627,7 @@ class ShikakuWindow(Gtk.ApplicationWindow):
|
|||||||
self.canvas.set_halign(Gtk.Align.CENTER)
|
self.canvas.set_halign(Gtk.Align.CENTER)
|
||||||
self.canvas.set_valign(Gtk.Align.CENTER)
|
self.canvas.set_valign(Gtk.Align.CENTER)
|
||||||
self.canvas.on_board_changed = self._trigger_auto_check
|
self.canvas.on_board_changed = self._trigger_auto_check
|
||||||
|
self.canvas.on_zoom_changed = self._on_zoom_changed
|
||||||
scroll.set_child(self.canvas)
|
scroll.set_child(self.canvas)
|
||||||
outer.append(scroll)
|
outer.append(scroll)
|
||||||
|
|
||||||
@@ -584,6 +638,31 @@ class ShikakuWindow(Gtk.ApplicationWindow):
|
|||||||
botbar.set_margin_bottom(18)
|
botbar.set_margin_bottom(18)
|
||||||
outer.append(botbar)
|
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 = Gtk.Button(label="New Puzzle")
|
||||||
new_btn.add_css_class("action-btn")
|
new_btn.add_css_class("action-btn")
|
||||||
new_btn.connect("clicked", self._on_new)
|
new_btn.connect("clicked", self._on_new)
|
||||||
@@ -599,6 +678,7 @@ class ShikakuWindow(Gtk.ApplicationWindow):
|
|||||||
clear_btn.connect("clicked", self._on_clear)
|
clear_btn.connect("clicked", self._on_clear)
|
||||||
botbar.append(clear_btn)
|
botbar.append(clear_btn)
|
||||||
|
|
||||||
|
|
||||||
self._status_label = Gtk.Label(label="Draw rectangles around each number — right-click to erase")
|
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.add_css_class("status-label")
|
||||||
self._status_label.set_margin_top(4)
|
self._status_label.set_margin_top(4)
|
||||||
@@ -706,10 +786,11 @@ class ShikakuWindow(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
def _update_canvas_size(self):
|
def _update_canvas_size(self):
|
||||||
sz = self.game.size
|
sz = self.game.size
|
||||||
cs = cell_size(sz)
|
cs = cell_size(sz, self.canvas.zoom)
|
||||||
total = sz * cs + 2 * PADDING
|
total = sz * cs + 2 * PADDING
|
||||||
self.canvas.set_content_width(total)
|
self.canvas.set_content_width(total)
|
||||||
self.canvas.set_content_height(total)
|
self.canvas.set_content_height(total)
|
||||||
|
self.set_default_size(total + 80, total + CHROME_HEIGHT + 60)
|
||||||
|
|
||||||
def _on_size_clicked(self, btn, size):
|
def _on_size_clicked(self, btn, size):
|
||||||
self._current_size = size
|
self._current_size = size
|
||||||
@@ -717,6 +798,8 @@ class ShikakuWindow(Gtk.ApplicationWindow):
|
|||||||
self.canvas.game = self.game
|
self.canvas.game = self.game
|
||||||
self.canvas.on_board_changed = self._trigger_auto_check
|
self.canvas.on_board_changed = self._trigger_auto_check
|
||||||
self.canvas._error_rects = set()
|
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_canvas_size()
|
||||||
self._update_size_buttons()
|
self._update_size_buttons()
|
||||||
self._status_label.remove_css_class("status-ok")
|
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}")
|
self._timer_label.set_text(f"{m}:{s:02d}")
|
||||||
return True # keep ticking
|
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 ────────────────────────────────────────────────────────
|
# ─── Application entry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user