From 52e057c484035c03eede43ca5706845abe9e70e1 Mon Sep 17 00:00:00 2001 From: bplu4t2f Date: Tue, 29 Sep 2026 18:43:42 +0200 Subject: [PATCH 1/3] Add win32/fentool example. --- .github/workflows/check.yml | 1 + win32/fentool/README.md | 129 +++++ win32/fentool/about.bmp | Bin 0 -> 27702 bytes win32/fentool/about.odin | 57 +++ win32/fentool/app.manifest | 20 + win32/fentool/billchess.png | Bin 0 -> 7505 bytes win32/fentool/board.odin | 134 +++++ win32/fentool/board_control.odin | 820 ++++++++++++++++++++++++++++++ win32/fentool/clack.wav | Bin 0 -> 7362 bytes win32/fentool/doc/screenshot1.png | Bin 0 -> 35691 bytes win32/fentool/doc/screenshot2.png | Bin 0 -> 29507 bytes win32/fentool/fen.odin | 471 +++++++++++++++++ win32/fentool/file_dialog.odin | 127 +++++ win32/fentool/icon.ico | Bin 0 -> 82214 bytes win32/fentool/main.odin | 725 ++++++++++++++++++++++++++ win32/fentool/piecesnormal.png | Bin 0 -> 31133 bytes win32/fentool/resource.h | 86 ++++ win32/fentool/resource.odin | 89 ++++ win32/fentool/resource.rc | 416 +++++++++++++++ win32/fentool/util.odin | 227 +++++++++ 20 files changed, 3302 insertions(+) create mode 100644 win32/fentool/README.md create mode 100644 win32/fentool/about.bmp create mode 100644 win32/fentool/about.odin create mode 100644 win32/fentool/app.manifest create mode 100644 win32/fentool/billchess.png create mode 100644 win32/fentool/board.odin create mode 100644 win32/fentool/board_control.odin create mode 100644 win32/fentool/clack.wav create mode 100644 win32/fentool/doc/screenshot1.png create mode 100644 win32/fentool/doc/screenshot2.png create mode 100644 win32/fentool/fen.odin create mode 100644 win32/fentool/file_dialog.odin create mode 100644 win32/fentool/icon.ico create mode 100644 win32/fentool/main.odin create mode 100644 win32/fentool/piecesnormal.png create mode 100644 win32/fentool/resource.h create mode 100644 win32/fentool/resource.odin create mode 100644 win32/fentool/resource.rc create mode 100644 win32/fentool/util.odin diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e15fe0d..7c48488 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -157,6 +157,7 @@ jobs: odin check win32/game_of_life -target:windows_amd64 $FLAGS odin check win32/open_window -target:windows_amd64 $FLAGS odin check win32/embedded_manifest -target:windows_amd64 $FLAGS + odin check win32/fentool -target:windows_amd64 $FLAGS odin check nanovg/example.odin -file $FLAGS odin check nanovg/fbo.odin -file $FLAGS diff --git a/win32/fentool/README.md b/win32/fentool/README.md new file mode 100644 index 0000000..22726d5 --- /dev/null +++ b/win32/fentool/README.md @@ -0,0 +1,129 @@ +FEN tool demo project +===================== + +Aims to demonstrate using Win32 embedded resources, in particular dialog templates, +in a somewhat realistic application setting. + +The tool lets the user visualize and configure a chess FEN (Forsyth-Edwards Notation) string. + +This application must be compiled with `-resource:resource.rc`, for example: + +``` +odin build . -vet -strict-style -resource:resource.rc +``` + +This requires the environment being set properly for Windows SDK builds. The easiest way +to get this going is to build from the *Developer Command Prompt for VS*. + +![Screenshot 1](doc/screenshot1.png) + +Naturally, it also implements ginger mode. + +![Screenshot 2](doc/screenshot2.png) + +Here is a rough overview of the topics covered: + + - Spinning up a message loop with a main window loaded from a dialog template. + - Defining resource IDs (`resource.h` + `resource.odin`). + - Instructing the resource compiler (`rc.exe`) to read the file as UTF-8. + - Using a dialog template resource (`DIALOGEX`) containing standard system control, as + well as user-defined controls (`CONTROL`) statement. + - Showing simple modal dialogs from a dialog template resource (`IDD_ABOUT`). + - Storing contextual data in `cbWndExtra` for non-dialogs (`board_control`). + - Storing contextual data in `DWLP_USER` for dialogs. + - Handling default dialog command IDs `IDOK` and `IDCANCEL` for `Esc` and `Enter` support. + - Defining main menu and context menus via the resource script. + - Loading string resources (`STRINGTABLE`) for use in user-facing messages. + - Using an accelerator table (`ACCELERATORS`) to implement keyboard shortcuts. + - Supporting standard keyboard navigation. + - Using various dialog item helper functions for manipulating dialog controls, such + as setting the text or checking/unchecking check boxes and radio buttons. + - Declaring and implementing DPI awareness. + - Buffered painting. + - Playing a .wav sound from a resource. + - Loading 24bpp Bitmaps from a resource and painting it with a transparency key. + - Loading 32bpp PNGs from a resource and painting it with alpha blending (using `png`). + - Setting the main window's icon and the executable's icon. + - Showing menu item help hints in the status bar. + - Localizing resources for different languages. + +Please note that Win32 application programming as demonstrated in this example is +inherently object oriented to some degree. For example, the `board_control` aims to +be a somewhat self-contained control class. + +Notes +----- + +**DPI awareness** + +DPI awareness is a large topic. This example relies on Windows already doing most +of the work simply by virtue of using the dialog manager functions rather than +implementing the window contents manually in code. The painting code adjusts for +DPI by where necessary by calling `adjust_for_dpi`. + +Also note in general that the position and size numbers specified in dialog template +resources are "dialog units", not pixels. They can be converted with `MapDialogRect`. + +**Main message loop** + +If you intend to use a dialog template as the application's main window, you have +three options: + + 1. Just fire it up with `DialogBox[Param]W` + 2. Create the entire dialog window with `CreateDialog[Param]W`, but implement the + message loop manually (demonstrated in this example) + 3. Create a separate main window class, and use `CreateDialog[Param]W` only to + create a child control that fills out the main window. + +Option 1 is the easiest. It will require `EndDialog` to be called, and the system +handles the rest. However, this option does not give you control over the message +loop, in case you need to inject something there. That means, you will also not +be able to use an accelerator table. + +Option 3 is the most flexible. The dialog template must have the `WS_CHILD` flag, +and it must not have a `CAPTION`. Proper sizing must be done manually. The application +must also coordinate certain events (like window closing) between the main window +procedure and the dialog window procedure. + +Option 2 is a common compromise. One unfortunate flaw is that you cannot use +`CW_USEDEFAULT` upon window creation to let the system decide a good spot for +the window on the screen. This example works around this by creating an invisible +dummy window on startup just to get a suitable position. Alternatively, `DS_CENTER` +can be used to make it pop up in the center of the screen. + +**Odin context propagation** + +The Odin `context` gets lost when control goes through a callback chain without +the Odin calling convention, such as `WNDPROC` or `DLGPROC`. + + - `runtime.default_context()` would discard anything that the application has + set up in `main()`, such as logging. + - Passing a context to the window through `lpParam`/`CREATESTRUCT` into the + `WM_CREATE` message can work sometimes, but not in general. For example, the + board control in this application is created by the dialog manager functions. + It is not possible to hook into this mechanism in a way that would let the + caller pass in a contextual runtime value to specific children. + - Using a global (or TLS "pseudo-global") is straight forward, but not across + module boundaries. In such a case, the module containing the control would + need to have an explicit `init` call. Additionally, memory allocated using + the context's allocator must be freed with that same allocator. Because the + code that captures the `context` is decoupled from the window or dialog + procedure callback which uses the captured value, this relationship is not + as obvious as it would normally be in Odin. + +This example stores the `context` in TLS. + +**String resource IDs** + +String resources (declared in a `STRINGTABLE`) are grouped into blocks of 16. + +When using `LoadStringW`, the actual internal resource ID is the provided ID, +divided by 16. Then, `LoadStringW` will skip over N strings, where N is the +remainder of the division. + +Normally, this is not a problem, because resource IDs of different resource +types (e.g. `ICON`, `BITMAP`, `DIALOG`, etc.) do not collide with each other. +However, by convention programs use the *same* resource ID for menu items +and their corresponding help text strings. This lets the main window select +the appropriate help text automatically when a menu item is being hovered +over. In this case, it may be necessary to avoid ID conflicts. diff --git a/win32/fentool/about.bmp b/win32/fentool/about.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b2ffa0be2a35e0b3a62c239da8cc377f752b210a GIT binary patch literal 27702 zcmeI2QBE5%5Qdiv^tngi1RS96+<^o1Mggf6k%&a4@{mW`2U>vyNbmp;g~PbBJIT!K ztiw8DC1U%JHk+~I^?JU{jK^7jIXw85FUcqA;VUUoc&NS-F_i}dF1l4()YAweIzbMV=;KDDci&c9as6E0`8#iiF_{$3{PC>+jwcz%B) z{xBusL;G8ZPajdHHe-h!;lnF0nZHKoci!J3e9RUyd!tO{I3I?Sm>&wCZhxJ1u}qoB zumuhY>R&HQThFq4gb``L_S5Y`wBO3Tauh_7b}6wAJ5uQcLFDSC$omi5b15#SsmIEq-L&WSR8F5_sUTi>T7F8b*ILoZF0n7$83RES=!i2Cy%y~ z1`t311V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;K;Q!j{0EG^ B4(tE` literal 0 HcmV?d00001 diff --git a/win32/fentool/about.odin b/win32/fentool/about.odin new file mode 100644 index 0000000..c00dd8c --- /dev/null +++ b/win32/fentool/about.odin @@ -0,0 +1,57 @@ +package fen_tool + +import log "core:log" +import win "core:sys/windows" + +about_show_dialog :: proc(owner: win.HWND) { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + win.DialogBoxW(hInstance, "IDD_ABOUT", owner, about_proc) +} + +about_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.INT_PTR { + switch msg { + case win.WM_INITDIALOG: + return 1 + + case win.WM_COMMAND: + context = window_context_tls + id := win.LOWORD(wparam) + nc := win.HIWORD(wparam) + log.debugf("About WM_COMMAND: %v %v", id, nc) + switch id + { + case win.IDOK: + win.EndDialog(hdlg, 0) + return 1 + case win.IDCANCEL: + win.EndDialog(hdlg, 0) + return 1 + } + + case win.WM_PAINT: + ps: win.PAINTSTRUCT + hdc := win.BeginPaint(hdlg, &ps) + defer win.EndPaint(hdlg, &ps) + + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + hbitmap := win.LoadImageW(hInstance, "IDB_ABOUT", win.IMAGE_BITMAP, 0, 0, win.LR_CREATEDIBSECTION) + defer win.DeleteObject(auto_cast hbitmap) + bm: win.BITMAP + win.GetObjectW(hbitmap, size_of(bm), &bm) + + hdc_src := win.CreateCompatibleDC(hdc) + defer win.DeleteDC(hdc_src) + previous_bitmap := cast(win.HBITMAP)win.SelectObject(hdc_src, auto_cast hbitmap) + defer win.SelectObject(hdc_src, auto_cast previous_bitmap) + + transparency_key := win.GetPixel(hdc_src, 0, 0) + + win.TransparentBlt( + hdc, 0, 0, bm.bmWidth, bm.bmHeight, + hdc_src, 0, 0, bm.bmWidth, bm.bmHeight, + transparency_key, + ) + } + + return 0 +} diff --git a/win32/fentool/app.manifest b/win32/fentool/app.manifest new file mode 100644 index 0000000..628b626 --- /dev/null +++ b/win32/fentool/app.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + true + PerMonitorV2 + + + \ No newline at end of file diff --git a/win32/fentool/billchess.png b/win32/fentool/billchess.png new file mode 100644 index 0000000000000000000000000000000000000000..53602c4f2a794c4e50980f4c63132972c2a643cf GIT binary patch literal 7505 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 N7@s!N&)36;{})+otBe2u literal 0 HcmV?d00001 diff --git a/win32/fentool/board.odin b/win32/fentool/board.odin new file mode 100644 index 0000000..13b0003 --- /dev/null +++ b/win32/fentool/board.odin @@ -0,0 +1,134 @@ +package fen_tool + +import fmt "core:fmt" +import strings "core:strings" + +board :: struct { + pieces: board_pieces, // Rank-major, file-minor. + turn_player: board_piece_color, + white_can_castle_kingside: bool, + white_can_castle_queenside: bool, + black_can_castle_kingside: bool, + black_can_castle_queenside: bool, + en_passant_target_square: square_specifier, + halfmove_clock: i32, + fullmove_number: i32, +} + +square_specifier :: struct { + file: i8, + rank: i8, +} + +board_piece :: struct { + type: board_piece_type, + color: board_piece_color, +} + +board_pieces :: distinct [64]board_piece + +board_piece_type :: enum i8 { + none, + rook, + FIRST = rook, + knight, + bishop, + queen, + king, + pawn, + LAST = pawn, +} + +board_piece_color :: enum i8 { + white, + FIRST = white, + black, + LAST = black, +} + +board_format_full :: proc(board: board, allocator := context.temp_allocator) -> string { + sb := strings.builder_make_len_cap(0, 1000, allocator) + fmt.sbprintfln(&sb, "%v", board_format_pieces(board, allocator)) + fmt.sbprintfln(&sb, "White can castle kingside: %v", board_format_yesno(board.white_can_castle_kingside)) + fmt.sbprintfln(&sb, "White can castle queenside: %v", board_format_yesno(board.white_can_castle_queenside)) + fmt.sbprintfln(&sb, "Black can castle kingside: %v", board_format_yesno(board.black_can_castle_kingside)) + fmt.sbprintfln(&sb, "Black can castle queenside: %v", board_format_yesno(board.black_can_castle_queenside)) + fmt.sbprintfln(&sb, "En passant target square: %v", board_format_en_passant_target_square(board.en_passant_target_square)) + fmt.sbprintfln(&sb, "Halfmove clock: %v", board.halfmove_clock) + fmt.sbprintfln(&sb, "Fullmove number: %v", board.fullmove_number) + return strings.to_string(sb) +} + +board_format_position :: proc { board_format_position_file_rank, board_format_position_square } + +board_format_position_file_rank :: proc(#any_int file: i32, #any_int rank: i32, allocator := context.temp_allocator) -> string { + file_string := cast(rune)('A' + file) + rank_string := cast(rune)('1' + rank) + sb := strings.builder_make_len_cap(0, 4, context.temp_allocator) + strings.write_rune(&sb, file_string) + strings.write_rune(&sb, rank_string) + return strings.to_string(sb) +} + +board_format_position_square :: proc(square: square_specifier, allocator := context.temp_allocator) -> string { + return board_format_position_file_rank(square.file, square.rank, allocator) +} + +board_format_yesno :: proc(b: bool) -> string { + return b ? "yes" : "no" +} + +board_format_en_passant_target_square :: proc(ts: square_specifier, allocator := context.temp_allocator) -> string { + if ts == {} { + return "-" + } + return board_format_position(ts, allocator) +} + +board_format_pieces :: proc(board: board, allocator := context.temp_allocator) -> string { + // Construct the string like this: + // 8 | rnbqkbnr + // 7 | pppppppp + // 6 | ........ + // 5 | ........ + // 4 | ........ + // 3 | ........ + // 2 | PPPPPPPP + // 1 | RNBQKBNR + // +--------- + // ABCDEFGH + sb := strings.builder_make_len_cap(0, 140, allocator) + for rank := 7; rank >= 0; rank -= 1 { + rank_name := cast(rune)('1' + rank) + fmt.sbprintf(&sb, "%v | ", rank_name) + for file in 0 ..< 8 { + boardwhere := rank * 8 + file + piece := board.pieces[boardwhere] + r := get_piece_rune(piece) + strings.write_rune(&sb, r) + } + fmt.sbprintln(&sb) + } + fmt.sbprintln(&sb, " +---------") + fmt.sbprint(&sb, " ABCDEFGH") + return strings.to_string(sb) +} + +get_piece_rune :: proc(piece: board_piece) -> rune { + switch { + case piece.type == .none: return '.' + case piece == { .rook, .white }: return 'R' + case piece == { .knight, .white }: return 'N' + case piece == { .bishop, .white }: return 'B' + case piece == { .queen, .white }: return 'Q' + case piece == { .king, .white }: return 'K' + case piece == { .pawn, .white }: return 'P' + case piece == { .rook, .black }: return 'r' + case piece == { .knight, .black }: return 'n' + case piece == { .bishop, .black }: return 'b' + case piece == { .queen, .black }: return 'q' + case piece == { .king, .black }: return 'k' + case piece == { .pawn, .black }: return 'p' + case: return '?' + } +} diff --git a/win32/fentool/board_control.odin b/win32/fentool/board_control.odin new file mode 100644 index 0000000..6850c38 --- /dev/null +++ b/win32/fentool/board_control.odin @@ -0,0 +1,820 @@ +package fen_tool + +import runtime "base:runtime" +import log "core:log" +import strings "core:strings" +import win "core:sys/windows" + +BOARD_CONTROL_CLASS_NAME :: "board_control" + +board_control_register_class :: proc(instance: win.HINSTANCE) { + wndclass := win.WNDCLASSEXW { + cbSize = size_of(win.WNDCLASSEXW), + style = win.CS_HREDRAW | win.CS_VREDRAW, + lpfnWndProc = board_control_wndproc, + cbWndExtra = size_of(rawptr), + hInstance = instance, + hCursor = win.LoadCursorA(nil, win.IDC_ARROW), + hbrBackground = cast(win.HBRUSH)cast(uintptr)(win.COLOR_WINDOW + 1), + lpszClassName = BOARD_CONTROL_CLASS_NAME, + } + win.RegisterClassExW(&wndclass) +} + +board_control_set_board :: proc(hwnd: win.HWND, board: board) { + ctx := board_control_get_ctx(hwnd) + if ctx.board != board { + ctx.board = board + win.InvalidateRect(hwnd, nil, true) + log.debugf("Board changed.") + } else { + log.debugf("No board change detected.") + } +} + +board_control_set_spritemap :: proc(hwnd: win.HWND, spritemap: board_control_spritemap) { + ctx := board_control_get_ctx(hwnd) + if ctx.spritemap != spritemap { + ctx.spritemap = spritemap + // Invalidate the cached spritemap bitmap. + win.DeleteObject(auto_cast ctx.spritemap_cache) + ctx.spritemap_cache = nil + win.InvalidateRect(hwnd, nil, true) + } +} + +board_control_spritemap :: enum { + normal, + ginger, +} + +board_control_get_spritemap_resource_name :: proc(spritemap: board_control_spritemap) -> string { + switch spritemap { + case .normal: return "IDR_PIECES_NORMAL" + case .ginger: return "IDR_PIECES_GINGER" + case: return "IDR_PIECES_NORMAL" + } +} + +@(private="file") +board_control_ctx :: struct { + runtime_context: ^runtime.Context, + font: win.HFONT, + board: board, + + spritemap: board_control_spritemap, + spritemap_cache: win.HBITMAP, + + // Last known mouse client coordinates; needed for piece drag and drop. + mouse_client: [2]i32, + + hover_mode: board_control_hover_mode, + hovered_square: square_specifier, + + menu_open: bool, + menu_square: square_specifier, + + pending_castling_right: castling_right, + pending_castling_right_state: castling_right_state, + + dragging: bool, + drag_from: square_specifier, +} + +@(private="file") +board_control_hover_mode :: enum { + none, + // User is currently using the mouse to highlight squares. + mouse, + // User is currently using the keyboard (arrow keys) to highlight squares. + keyboard, +} + +castling_right :: enum { + white_kingside, + white_queenside, + black_kingside, + black_queenside, +} + +castling_right_state :: enum { + allow, + disallow, +} + +@(private="file") +board_control_get_ctx :: proc "contextless" (hwnd: win.HWND) -> ^board_control_ctx { + return cast(^board_control_ctx)cast(uintptr)win.GetWindowLongPtrW(hwnd, 0) +} + +board_control_notification_code :: enum win.UINT { + none, + hover, + place_piece, + set_en_passant_target_square, + set_castling_right, + exchange_pieces, +} + +NM_BOARD_CONTROL_NOTIFICATION :: struct { + hdr: win.NMHDR, + hovering: bool, + square: square_specifier, + piece: board_piece, + castling_right: castling_right, + castling_right_state: castling_right_state, + square2: square_specifier, +} + +board_control_set_hovered_square :: proc(hwnd: win.HWND, ctx: ^board_control_ctx, mode: board_control_hover_mode, square: square_specifier) { + if ctx.hover_mode == mode && ctx.hovered_square == square { + // No change. + return + } + //log.debugf("Hover state changed to %v - %v, %v", mode, square.rank, square.file) + ctx.hover_mode = mode + ctx.hovered_square = square + is_hovering := mode != .none + board_control_notify_cell_hover(hwnd, is_hovering, square) + win.InvalidateRect(hwnd, nil, true) +} + +board_control_notify_cell_hover :: proc(hwnd: win.HWND, hovering: bool, square: square_specifier) { + parent := win.GetParent(hwnd) + if parent == nil { + return + } + nm := NM_BOARD_CONTROL_NOTIFICATION { + hdr = { + hwndFrom = hwnd, + idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), + code = cast(win.UINT)board_control_notification_code.hover, + }, + hovering = hovering, + square = square, + } + win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) +} + +board_control_notify_piece :: proc(hwnd: win.HWND, square: square_specifier, piece: board_piece) { + parent := win.GetParent(hwnd) + if parent == nil { + return + } + nm := NM_BOARD_CONTROL_NOTIFICATION { + hdr = { + hwndFrom = hwnd, + idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), + code = cast(win.UINT)board_control_notification_code.place_piece, + }, + square = square, + piece = piece, + } + win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) +} + +board_control_notify_exchange_pieces :: proc(hwnd: win.HWND, square1, square2: square_specifier) { + parent := win.GetParent(hwnd) + if parent == nil { + return + } + nm := NM_BOARD_CONTROL_NOTIFICATION { + hdr = { + hwndFrom = hwnd, + idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), + code = cast(win.UINT)board_control_notification_code.exchange_pieces, + }, + square = square1, + square2 = square2, + } + win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) +} + +board_control_notify_set_en_passant_target_square :: proc(hwnd: win.HWND, square: square_specifier) { + parent := win.GetParent(hwnd) + if parent == nil { + return + } + nm := NM_BOARD_CONTROL_NOTIFICATION { + hdr = { + hwndFrom = hwnd, + idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), + code = cast(win.UINT)board_control_notification_code.set_en_passant_target_square, + }, + square = square, + } + win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) +} + +board_control_notify_set_castling_right :: proc(hwnd: win.HWND, castling_right: castling_right, state: castling_right_state) { + parent := win.GetParent(hwnd) + if parent == nil { + return + } + nm := NM_BOARD_CONTROL_NOTIFICATION { + hdr = { + hwndFrom = hwnd, + idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), + code = cast(win.UINT)board_control_notification_code.set_castling_right, + }, + castling_right = castling_right, + castling_right_state = state, + } + win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) +} + +@(private="file") +board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.LRESULT { + + switch msg { + + case win.WM_CREATE: + context = window_context_tls + ctx := new(board_control_ctx) + ctx.runtime_context = &window_context_tls + win.SetWindowLongPtrW(hwnd, 0, cast(win.LONG_PTR)cast(uintptr)ctx) + + case win.WM_NCDESTROY: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + win.DeleteObject(auto_cast ctx.spritemap_cache) + ctx.spritemap_cache = nil + free(ctx) + + case win.WM_SETFONT: + ctx := board_control_get_ctx(hwnd) + ctx.font = cast(win.HFONT)wparam + + case win.WM_GETFONT: + ctx := board_control_get_ctx(hwnd) + return cast(win.LRESULT)cast(uintptr)ctx.font + + case win.WM_SIZE: + // Make sure that the control stays a square. + rc: win.RECT + win.GetWindowRect(hwnd, &rc) + w := rc.right - rc.left + h := rc.bottom - rc.top + if w != h { + win.SetWindowPos(hwnd, nil, 0, 0, w, w, win.SWP_NOZORDER | win.SWP_NOMOVE) + return 0 + } + + case win.WM_MOUSEMOVE: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + x_client := win.GET_X_LPARAM(lparam) + y_client := win.GET_Y_LPARAM(lparam) + board_control_set_drag_target_coords(hwnd, ctx, x_client, y_client) + square, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) + board_control_set_hovered_square(hwnd, ctx, ok ? .mouse : .none, square) + tme := win.TRACKMOUSEEVENT { + cbSize = size_of(win.TRACKMOUSEEVENT), + hwndTrack = hwnd, + dwFlags = win.TME_LEAVE, + } + win.TrackMouseEvent(&tme) + + case win.WM_MOUSELEAVE: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + board_control_set_hovered_square(hwnd, ctx, .none, {}) + + case win.WM_LBUTTONDOWN: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + + win.SetCapture(hwnd) + + // Setup piece drag operation + x_client := win.GET_X_LPARAM(lparam) + y_client := win.GET_Y_LPARAM(lparam) + square, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) + if ok { + win.SetFocus(hwnd) + // You can only drag if there is a piece on that square. + if ctx.board.pieces[square.rank * 8 + square.file].type != .none { + ctx.dragging = true + ctx.mouse_client = { x_client, y_client } + ctx.drag_from = square + win.InvalidateRect(hwnd, nil, true) + } + } + + case win.WM_LBUTTONUP: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + + // Release mouse capture. + if win.GetCapture() == hwnd { + win.SetCapture(nil) + } + + if ctx.dragging { + x_client := win.GET_X_LPARAM(lparam) + y_client := win.GET_Y_LPARAM(lparam) + ctx.dragging = false + square_to, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) + square_from := ctx.drag_from + if ok { + if square_to == square_from { + // Released over the same square. Do nothing. + } else { + // Exchange these 2 pieces. This also handles the case where the target square is empty. + board_control_notify_exchange_pieces(hwnd, square_from, square_to) + } + } else { + // Was released somewhere not over the board. + // Delete the piece from the "from" square, but don't put it anywhere. + board_control_notify_piece(hwnd, square_from, {}) + } + win.InvalidateRect(hwnd, nil, true) + } + + case win.WM_CONTEXTMENU: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + x_screen := win.GET_X_LPARAM(lparam) + y_screen := win.GET_Y_LPARAM(lparam) + x_client: i32 + y_client: i32 + if x_screen == -1 && y_screen == -1 { + // This was triggered by a non-mouse event (e.g. the "VK_APPS" key on the keyboard). + // NOTE: This is ambiguous. (-1, -1) could technically be valid screen coordinates + // in a multi-monitor scenario. This is an API flaw. + // In this case, we open the context menu at the center of the currently hovered square. + target_square := ctx.hovered_square + target_file := cast(i32)target_square.file + target_rank := cast(i32)target_square.rank + client: win.RECT + win.GetClientRect(hwnd, &client) + x, y, w, h := board_control_get_board_bounds(hwnd, client) + square_rect := win.RECT { + left = auto_cast (x + (w * (target_file )) / 8), + top = auto_cast (y + (h * (7-target_rank)) / 8), + right = auto_cast (x + (w * (target_file+1)) / 8), + bottom = auto_cast (y + (h * (8-target_rank)) / 8), + } + x_client = square_rect.left + (square_rect.right - square_rect.left) / 2 + y_client = square_rect.top + (square_rect.bottom - square_rect.top) / 2 + pt := win.POINT { x_client, y_client } + win.ClientToScreen(hwnd, &pt) + x_screen = pt.x + y_screen = pt.y + } else { + pt := win.POINT { x_screen, y_screen } + win.ScreenToClient(hwnd, &pt) + x_client = pt.x + y_client = pt.y + } + log.debugf("lparam: 0x%8x - screen x, y: %v, %v - client x, y: %v %v", lparam, x_screen, y_screen, x_client, y_client) + square, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) + if ok { + hInstance := cast(win.HINSTANCE)cast(uintptr)win.GetWindowLongPtrW(hwnd, win.GWLP_HINSTANCE) + menu := win.LoadMenuW(hInstance, "IDM_BOARD_CONTEXT_MENU") + if menu == nil { + error := win.GetLastError() + msg := get_system_error_message(error) + log.errorf("Could not load menu: %v", msg) + break + } + defer win.DestroyMenu(menu) + popup := win.GetSubMenu(menu, 0) + if popup == nil { + error := win.GetLastError() + msg := get_system_error_message(error) + log.errorf("Could not popup menu: %v", msg) + break + } + + // Display a string like "E4" in the square info text. + square_info_text := board_format_position(square, context.temp_allocator) + set_menu_item_text(popup, IDM_SQUARE_INFO, square_info_text) + + // Allow setting the en passant target square if rank is eligible for it (on the "3" or "6" rank). + allow_set_en_passant_target_square := square.rank == 2 || square.rank == 5 + win.EnableMenuItem(popup, IDM_SET_EN_PASSANT_TARGET_SQUARE, allow_set_en_passant_target_square ? win.MF_ENABLED : win.MF_GRAYED) + + // Allow toggling castling rights if the clicked square is a rook square. + // This menu item has dynamic functionality, depending on the clicked square and the previous castling rights. + castling_right_toggle, castling_right_toggle_state, castling_right_toggle_enable := board_control_get_castling_right_toggle_action(ctx, square) + win.EnableMenuItem(popup, IDM_TOGGLE_CASTLING_RIGHT, castling_right_toggle_enable ? win.MF_ENABLED : win.MF_GRAYED) + if castling_right_toggle_enable { + ctx.pending_castling_right = castling_right_toggle + ctx.pending_castling_right_state = castling_right_toggle_state + string_resource_id: win.UINT + switch castling_right_toggle { + case .white_kingside: + string_resource_id = castling_right_toggle_state == .allow ? IDS_ALLOW_CASTLING_WHITE_KINGSIDE : IDS_DISALLOW_CASTLING_WHITE_KINGSIDE + case .white_queenside: + string_resource_id = castling_right_toggle_state == .allow ? IDS_ALLOW_CASTLING_WHITE_QUEENSIDE : IDS_DISALLOW_CASTLING_WHITE_QUEENSIDE + case .black_kingside: + string_resource_id = castling_right_toggle_state == .allow ? IDS_ALLOW_CASTLING_BLACK_KINGSIDE : IDS_DISALLOW_CASTLING_BLACK_KINGSIDE + case .black_queenside: + string_resource_id = castling_right_toggle_state == .allow ? IDS_ALLOW_CASTLING_BLACK_QUEENSIDE : IDS_DISALLOW_CASTLING_BLACK_QUEENSIDE + } + label := load_string_resource(hInstance, string_resource_id, context.temp_allocator) + set_menu_item_text(popup, IDM_TOGGLE_CASTLING_RIGHT, label) + } + + ctx.menu_open = true + ctx.menu_square = square + defer { + ctx.menu_open = false + win.InvalidateRect(hwnd, nil, true) + } + + win.TrackPopupMenu(popup, 0, x_screen, y_screen, 0, hwnd, nil) + } + + case win.WM_MENUSELECT: + parent := win.GetParent(hwnd) + if parent != nil { + return win.SendMessageW(parent, msg, wparam, lparam) + } + + case win.WM_GETDLGCODE: + return win.DLGC_WANTARROWS + + case win.WM_SETFOCUS: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + if ctx.hover_mode != .keyboard { + ctx.hover_mode = .keyboard + win.InvalidateRect(hwnd, nil, true) + } + + case win.WM_KILLFOCUS: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + board_control_set_hovered_square(hwnd, ctx, .none, {}) + win.InvalidateRect(hwnd, nil, true) + + case win.WM_KEYDOWN: + // Change selected square with the arrow keys. + vk := wparam + switch vk { + case win.VK_LEFT: keyboard_navigate_square(hwnd, -1, 0); return 0 + case win.VK_UP: keyboard_navigate_square(hwnd, 0, 1); return 0 + case win.VK_RIGHT: keyboard_navigate_square(hwnd, 1, 0); return 0 + case win.VK_DOWN: keyboard_navigate_square(hwnd, 0, -1); return 0 + } + + keyboard_navigate_square :: proc "contextless" (hwnd: win.HWND, delta_file, delta_rank: i8) { + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + // NOTE: We could use independent state variables for the "keyboard-hovered" square. + rank := clamp(ctx.hovered_square.rank + delta_rank, 0, 7) + file := clamp(ctx.hovered_square.file + delta_file, 0, 7) + square := square_specifier { rank = rank, file = file } + board_control_set_hovered_square(hwnd, ctx, .keyboard, square) + } + + case win.WM_COMMAND: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + id := win.LOWORD(wparam) + nc := win.HIWORD(wparam) + log.debugf("Board control command: id: %v - nc: %v", id, nc) + + switch id { + case IDM_CLEAR_SQUARE: board_control_notify_piece(hwnd, ctx.menu_square, { .none, nil }) + + case IDM_ROOK_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .rook, .white }) + case IDM_KNIGHT_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .knight, .white }) + case IDM_BISHOP_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .bishop, .white }) + case IDM_QUEEN_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .queen, .white }) + case IDM_KING_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .king, .white }) + case IDM_PAWN_WHITE: board_control_notify_piece(hwnd, ctx.menu_square, { .pawn, .white }) + case IDM_ROOK_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .rook, .black }) + case IDM_KNIGHT_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .knight, .black }) + case IDM_BISHOP_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .bishop, .black }) + case IDM_QUEEN_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .queen, .black }) + case IDM_KING_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .king, .black }) + case IDM_PAWN_BLACK: board_control_notify_piece(hwnd, ctx.menu_square, { .pawn, .black }) + + case IDM_SET_EN_PASSANT_TARGET_SQUARE: + board_control_notify_set_en_passant_target_square(hwnd, ctx.menu_square) + case IDM_TOGGLE_CASTLING_RIGHT: + board_control_notify_set_castling_right(hwnd, ctx.pending_castling_right, ctx.pending_castling_right_state) + } + + case win.WM_ERASEBKGND: + return 1 + + case win.WM_PAINT: + ctx := board_control_get_ctx(hwnd) + context = ctx.runtime_context^ + + // Setup WM_PAINT infrastructure + ps: win.PAINTSTRUCT + hdc0 := win.BeginPaint(hwnd, &ps) + defer win.EndPaint(hwnd, &ps) + hdc: win.HDC + paintbuffer := win.BeginBufferedPaint(hdc0, &ps.rcPaint, .BPBF_TOPDOWNDIB, nil, &hdc) + defer win.EndBufferedPaint(paintbuffer, true) + + // Fill background + { + brush := win.CreateSolidBrush(win.RGB(255, 255, 255)) + defer win.DeleteObject(auto_cast brush) + win.FillRect(hdc, &ps.rcPaint, brush) + } + + // Prepare board squares + + client: win.RECT + win.GetClientRect(hwnd, &client) + x, y, w, h := board_control_get_board_bounds(hwnd, client) + //log.debugf("Board bounds: %v, %v, %v, %v", x, y, w, h) + + dark_square_brush := win.CreateSolidBrush(win.RGB(175, 185, 170)) + defer win.DeleteObject(auto_cast dark_square_brush) + light_square_brush := win.CreateSolidBrush(win.RGB(205, 215, 200)) + defer win.DeleteObject(auto_cast light_square_brush) + highlight_square_brush: win.HBRUSH + if ctx.hover_mode != .none || ctx.menu_open { + // NOTE: This brush does not need to be freed. + highlight_square_brush = win.GetSysColorBrush(win.COLOR_HIGHLIGHT) + } + en_passant_highlight_color := win.RGB(255, 127, 0) + castling_right_highlight_color := win.RGB(0, 255, 255) + + highlight_thickness := adjust_for_dpi(hwnd, 3) + + should_highlight_square :: proc(ctx: ^board_control_ctx, square: square_specifier) -> bool { + if ctx.menu_open { + return square == ctx.menu_square + } else if ctx.hover_mode != .none { + return square == ctx.hovered_square + } else { + return false + } + } + + // Paint board squares + + // rank index 0 is rank "1" + // file index 0 is file "A" + for rank in 0 ..< i32(8) { + for file in 0 ..< i32(8) { + square_rect := win.RECT { + left = auto_cast (x + (w * (file )) / 8), + top = auto_cast (y + (h * (7-rank)) / 8), + right = auto_cast (x + (w * (file+1)) / 8), + bottom = auto_cast (y + (h * (8-rank)) / 8), + } + square := square_specifier { rank = cast(i8)rank, file = cast(i8)file } + brush: win.HBRUSH + if should_highlight_square(ctx, square) { + brush = highlight_square_brush + } else { + is_dark_square := (rank + file) % 2 != 0 + brush = is_dark_square ? dark_square_brush : light_square_brush + } + win.FillRect(hdc, &square_rect, brush) + + // Highlight square if en passant + if ctx.board.en_passant_target_square != {} && ctx.board.en_passant_target_square == square { + stroke_rectangle_precise(hdc, square_rect, en_passant_highlight_color, thickness = highlight_thickness) + } + + // Highlight square if castling right + highlight_for_castling := (ctx.board.white_can_castle_queenside && file == 0 && rank == 0) || + (ctx.board.white_can_castle_kingside && file == 7 && rank == 0) || + (ctx.board.black_can_castle_queenside && file == 0 && rank == 7) || + (ctx.board.black_can_castle_kingside && file == 7 && rank == 7) + if highlight_for_castling { + stroke_rectangle_precise(hdc, square_rect, castling_right_highlight_color, thickness = highlight_thickness) + } + } + } + + // Paint border (board frame) + + stroke_rectangle_precise(hdc, client, win.RGB(0, 0, 0)) + board_rc := win.RECT { x, y, x + w, y + h } + stroke_rectangle_precise(hdc, board_rc, win.RGB(0, 0, 0), -1) + + // Paint file and rank names + + previous_font := cast(win.HFONT)win.SelectObject(hdc, auto_cast ctx.font) + defer win.SelectObject(hdc, auto_cast previous_font) + + for file in 0 ..< i32(8) { + label_rect := win.RECT { + left = auto_cast (x + (w * (file )) / 8), + top = board_rc.bottom, + right = auto_cast (x + (w * (file+1)) / 8), + bottom = client.bottom, + } + file_string := cast(rune)('A' + file) + sb := strings.builder_make_len_cap(0, 4, context.temp_allocator) + strings.write_rune(&sb, file_string) + win.DrawTextExW(hdc, win.utf8_to_wstring(strings.to_string(sb), context.temp_allocator), -1, &label_rect, .DT_SINGLELINE | .DT_CENTER | .DT_VCENTER | .DT_NOPREFIX, nil) + } + + for rank in 0 ..< i32(8) { + label_rect := win.RECT { + left = client.left, + top = auto_cast (y + (h * (7-rank)) / 8), + right = board_rc.left, + bottom = auto_cast (y + (h * (8-rank)) / 8), + } + rank_string := cast(rune)('1' + rank) + sb := strings.builder_make_len_cap(0, 4, context.temp_allocator) + strings.write_rune(&sb, rank_string) + win.DrawTextExW(hdc, win.utf8_to_wstring(strings.to_string(sb), context.temp_allocator), -1, &label_rect, .DT_SINGLELINE | .DT_CENTER | .DT_VCENTER | .DT_NOPREFIX, nil) + } + + // Prepare pieces + + if ctx.spritemap_cache == nil { + // We cache the image so we don't have to load it every time. + ctx.spritemap_cache = load_pieces_png(ctx.spritemap) + log.debugf("Spritemap '%v' loaded: %v", ctx.spritemap, ctx.spritemap_cache) + } + piece_sprites := ctx.spritemap_cache + if piece_sprites == nil { + err := win.GetLastError() + msg := get_system_error_message(err, allocator = context.temp_allocator) + log.errorf("Error loading piece sprite bitmap: %v", msg) + } + + load_pieces_png :: proc(spritemap: board_control_spritemap) -> win.HBITMAP { + resource_name := board_control_get_spritemap_resource_name(spritemap) + hModule := win.GetModuleHandleW(nil) + return load_hbitmap_from_png_rcdata(hModule, win.utf8_to_wstring(resource_name, context.temp_allocator)) + } + + // Paint pieces + + if piece_sprites != nil { + hdc_src := win.CreateCompatibleDC(hdc) + defer win.DeleteDC(hdc_src) + previous_bitmap := win.SelectObject(hdc_src, auto_cast piece_sprites) + defer win.SelectObject(hdc_src, previous_bitmap) + for rank in 0 ..< i32(8) { + for file in 0 ..< i32(8) { + if ctx.dragging && ctx.drag_from.file == auto_cast file && ctx.drag_from.rank == auto_cast rank { + // Skip this; it's the piece currently being dragged. + continue + } + piece := ctx.board.pieces[rank * 8 + file] + if piece.type != .none { + square_rect := win.RECT { + left = auto_cast (x + (w * (file )) / 8), + top = auto_cast (y + (h * (7-rank)) / 8), + right = auto_cast (x + (w * (file+1)) / 8), + bottom = auto_cast (y + (h * (8-rank)) / 8), + } + xd := square_rect.left + yd := square_rect.top + wd := square_rect.right - square_rect.left + hd := square_rect.bottom - square_rect.top + sx, sy, sw, sh := board_control_get_sprite_src(piece) + bf := win.BLENDFUNCTION { + BlendOp = win.AC_SRC_OVER, + AlphaFormat = win.AC_SRC_ALPHA, + SourceConstantAlpha = 255, + } + win.GdiAlphaBlend( + hdc, xd, yd, wd, hd, + hdc_src, sx, sy, sw, sh, + bf, + ) + } + } + } + + // Paint the dragged piece, if any. + if ctx.dragging { + from_rank := cast(i32)ctx.drag_from.rank + from_file := cast(i32)ctx.drag_from.file + piece := ctx.board.pieces[from_rank * 8 + from_file] + square_rect := win.RECT { + left = auto_cast (x + (w * (from_file )) / 8), + top = auto_cast (y + (h * (7-from_rank)) / 8), + right = auto_cast (x + (w * (from_file+1)) / 8), + bottom = auto_cast (y + (h * (8-from_rank)) / 8), + } + wd := square_rect.right - square_rect.left + hd := square_rect.bottom - square_rect.top + sx, sy, sw, sh := board_control_get_sprite_src(piece) + xd := ctx.mouse_client[0] - wd / 2 + yd := ctx.mouse_client[1] - hd / 2 + bf := win.BLENDFUNCTION { + BlendOp = win.AC_SRC_OVER, + AlphaFormat = win.AC_SRC_ALPHA, + SourceConstantAlpha = 255, + } + win.GdiAlphaBlend( + hdc, xd, yd, wd, hd, + hdc_src, sx, sy, sw, sh, + bf, + ) + } + } + } + + return win.DefWindowProcW(hwnd, msg, wparam, lparam) +} + +adjust_for_dpi :: proc(hwnd: win.HWND, i: i32) -> i32 { + lpy := win.GetDpiForWindow(hwnd) + return cast(i32)(cast(f64)lpy / 96.0 * cast(f64)i) +} + +// Returns the castling right toggle action that is appropriate for clicking on the specified square. +// E.g. if the user clicks on A1, they can toggle the castling right for white queenside. +// The `toggle_state` will be the opposite state (allow/disallow) of the current board state. +board_control_get_castling_right_toggle_action :: proc(ctx: ^board_control_ctx, square: square_specifier) -> (castling_right: castling_right, toggle_state: castling_right_state, enable: bool) { + currently_allowed: bool + switch { + case square.rank == 0 && square.file == 0: + castling_right = .white_queenside + currently_allowed = ctx.board.white_can_castle_queenside + case square.rank == 0 && square.file == 7: + castling_right = .white_kingside + currently_allowed = ctx.board.white_can_castle_kingside + case square.rank == 7 && square.file == 0: + castling_right = .black_queenside + currently_allowed = ctx.board.black_can_castle_queenside + case square.rank == 7 && square.file == 7: + castling_right = .black_kingside + currently_allowed = ctx.board.black_can_castle_kingside + case: + enable = false + return + } + enable = true + if currently_allowed { + toggle_state = .disallow + } else { + toggle_state = .allow + } + return +} + +board_control_get_board_bounds :: proc(hwnd: win.HWND, client: win.RECT) -> (x, y, w, h: i32) { + BORDER_WIDTH_PX :: 18 + border_thickness := adjust_for_dpi(hwnd, BORDER_WIDTH_PX) + x = client.left + border_thickness + y = client.top + border_thickness + w = client.right - x - border_thickness + h = client.bottom - y - border_thickness + min_wh := min(w, h) + w = min_wh + h = min_wh + return +} + +board_control_get_square_from_client_coords :: proc(hwnd: win.HWND, client_x, client_y: i32) -> (square: square_specifier, ok: bool) { + client: win.RECT + win.GetClientRect(hwnd, &client) + x, y, w, h := board_control_get_board_bounds(hwnd, client) + X := client_x - x + Y := client_y - y + if X < 0 || Y < 0 || X >= w || Y >= h { + ok = false + return + } + file := cast(i8)(cast(f64)X / cast(f64)w * 8.0) + rank := 7 - cast(i8)(cast(f64)Y / cast(f64)h * 8.0) + if file < 0 || file >= 8 || rank < 0 || rank >= 8 { + ok = false + return + } + return { file = file, rank = rank }, true +} + +// Get the source rectangle for the sprite for `piece` in the sprite map. +board_control_get_sprite_src :: proc(piece: board_piece) -> (x, y, w, h: i32) { + if piece.type == .none { + return + } + if piece.type < .FIRST || piece.type > .LAST { + return + } + if piece.color < .FIRST || piece.color > .LAST { + return + } + SQUARE_SIZE_PX :: 128 + w = SQUARE_SIZE_PX + h = SQUARE_SIZE_PX + col := cast(i32)piece.type - 1 + row := cast(i32)piece.color + x = col * SQUARE_SIZE_PX + y = row * SQUARE_SIZE_PX + return +} + +// Stores the specified mouse client coordinates for the visual position of the currently dragged piece. +// +// Coordinates values (-1, -1) mean that the mouse is not over the board. +board_control_set_drag_target_coords :: proc(hwnd: win.HWND, ctx: ^board_control_ctx, x_client, y_client: i32) { + if ctx.dragging && ctx.mouse_client != { x_client, y_client } { + ctx.mouse_client = { x_client, y_client } + win.InvalidateRect(hwnd, nil, true) + } +} diff --git a/win32/fentool/clack.wav b/win32/fentool/clack.wav new file mode 100644 index 0000000000000000000000000000000000000000..3f45d3275ef790b86b298d3856b6d2ec5bed4610 GIT binary patch literal 7362 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 A!TW;PhUEHzMx;Kv;sY*PvYkvaxe#%4B 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�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 bVFuMExJRhCXD$5#{)1^6XcSz$9{hg*1-$8a literal 0 HcmV?d00001 diff --git a/win32/fentool/doc/screenshot2.png b/win32/fentool/doc/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..60f38ced428010a77b6b9a2ab93142271c3fc100 GIT binary patch literal 29507 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 literal 0 HcmV?d00001 diff --git a/win32/fentool/fen.odin b/win32/fentool/fen.odin new file mode 100644 index 0000000..eeb2e06 --- /dev/null +++ b/win32/fentool/fen.odin @@ -0,0 +1,471 @@ +package fen_tool + +import runtime "base:runtime" +import log "core:log" +import fmt "core:fmt" +import strings "core:strings" +import strconv "core:strconv" +import win "core:sys/windows" + +parse_fen_error :: struct { + is_error: bool, + cursor: int, + display_message: string, +} + +// Retrieves the (possibly) localized error string with the specified ID from the embedded resource, and formats +// the error with the given arguments. +make_fen_errorf :: proc(ctx: parse_ctx, allocator: runtime.Allocator, string_resource_id: win.UINT, args: ..any) -> parse_fen_error { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + format := load_string_resource(hInstance, string_resource_id) + message := fmt.aprintf(format, args = args, allocator = allocator) + return parse_fen_error { + is_error = true, + cursor = ctx.c, + display_message = message, + } +} + +parse_fen :: proc(s: string, allocator := context.temp_allocator) -> (board: board, error: parse_fen_error) { + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + + ctx := parse_ctx { s = s } + eat_spaces(&ctx) + + // rank index 0 is the "1" rank on the board. + for rank := 7; rank >= 0; rank -= 1 { + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + // file index 0 is the "A" file on the board. + file := 0 + rank_loop: for { + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + piece: Maybe(board_piece) + switch c { + case 'r': piece = board_piece { .rook, .black } + case 'n': piece = board_piece { .knight, .black } + case 'b': piece = board_piece { .bishop, .black } + case 'q': piece = board_piece { .queen, .black } + case 'k': piece = board_piece { .king, .black } + case 'p': piece = board_piece { .pawn, .black } + case 'R': piece = board_piece { .rook, .white } + case 'N': piece = board_piece { .knight, .white } + case 'B': piece = board_piece { .bishop, .white } + case 'Q': piece = board_piece { .queen, .white } + case 'K': piece = board_piece { .king, .white } + case 'P': piece = board_piece { .pawn, .white } + case: + if c == '/' { + if file != 8 || rank == 0 { + log.warnf("Rank separator '/' at invalid position: %v", board_format_position(file, rank)) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_RANK_SEPARATOR_AT_INVALID_POSITION_0, board_format_position(file, rank)) + return + } + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + log.debugf("Rank %v complete - next rank.", rank) + break rank_loop + } else if c >= '1' && c <= '8' { + spaces := cast(int)(c - '0') + if file + spaces > 8 { + log.warnf("Rank overflow (cannot advance by %v empty squares): %v", spaces, board_format_position(file, rank)) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_RANK_OVERFLOW_ADVANCE_0_1, spaces, board_format_position(file, rank)) + return + } + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + log.debugf("Advance %v empty squares at %v.", spaces, board_format_position(file, rank)) + file += spaces + } else if c == ' ' { + if file != 8 || rank != 0 { + log.warnf("Board separator ' ' at invalid position: %v", board_format_position(file, rank)) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_BOARD_SEPARATOR_AT_INVALID_POSITION_0, board_format_position(file, rank)) + return + } + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + log.debugf("Pieces done.") + break rank_loop + } else { + log.warnf("Invalid character '%v' at position: %v", c, board_format_position(file, rank)) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_INVALID_CHARACTER_0_AT_POSITION_1, c, board_format_position(file, rank)) + return + } + } + + if piece != nil { + if file >= 8 { + log.warnf("Rank overflow (cannot place %v %v) at %v", piece.?.color, piece.?.type, board_format_position(file, rank)) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_RANK_OVERFLOW_CANNOT_PLACE_0_1_AT_2, piece.?.color, piece.?.type, board_format_position(file, rank)) + return + } + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + boardwhere := rank * 8 + file + log.debugf("Placed %v %v at %v", piece.?.color, piece.?.type, board_format_position(file, rank)) + ctx.board.pieces[boardwhere] = piece.? + file += 1 + } + } + } + + eat_spaces(&ctx) + + // Which player is to move? + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + { + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + switch c { + case 'w': + ctx.board.turn_player = .white + ctx.c += 1 + case 'b': + ctx.board.turn_player = .black + ctx.c += 1 + case: + log.warnf("Expected player to move (w/b), got %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_PLAYER_TO_MOVE_0, c) + return + } + } + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + { + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c != ' ' { + log.warnf("Expected space, got %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_SPACE_0, c) + return + } + ctx.c += 1 + } + + eat_spaces(&ctx) + + // Castling rights + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + castling_rights_loop: for { + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + switch c { + case 'K': + if ctx.board.white_can_castle_kingside { + log.warnf("Duplicate castling right specifier: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0, c) + return + } + ctx.board.white_can_castle_kingside = true + case 'Q': + if ctx.board.white_can_castle_queenside { + log.warnf("Duplicate castling right specifier: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0, c) + return + } + ctx.board.white_can_castle_queenside = true + case 'k': + if ctx.board.black_can_castle_kingside { + log.warnf("Duplicate castling right specifier: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0, c) + return + } + ctx.board.black_can_castle_kingside = true + case 'q': + if ctx.board.black_can_castle_queenside { + log.warnf("Duplicate castling right specifier: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0, c) + return + } + ctx.board.black_can_castle_queenside = true + case '-': + // No castling rights. This is only valid + // if no castling rights are being specified. + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1 + // ^ + if ctx.board.white_can_castle_kingside || + ctx.board.white_can_castle_queenside || + ctx.board.black_can_castle_kingside || + ctx.board.black_can_castle_queenside { + log.warnf("Castling right specifier conflict") + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_CASTLING_RIGHT_SPECIFIER_CONFLICT) + return + } + case ' ': + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + break castling_rights_loop + case: + log.warnf("Expected castling rights specifier, got %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_CASTLING_RIGHTS_SPECIFIER_0, c) + return + } + ctx.c += 1 + } + + eat_spaces(&ctx) + + // En passant target square + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq e3 0 1 + // ^ + + { + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c == '-' { + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + ctx.c += 1 + } else { + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq e3 0 1 + // ^ + if c < 'a' || c > 'h' { + log.warnf("Invalid en passant target square file: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_FILE_0, c) + return + } + file := c - 'a' + ctx.c += 1 + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq e3 0 1 + // ^ + + c = cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c != '3' && c != '6' { + log.warnf("Invalid en passant target square rank: %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_RANK_0, c) + return + } + rank := c - '1' + ctx.c += 1 + ctx.board.en_passant_target_square = { cast(i8)file, cast(i8)rank } + } + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + c = cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c != ' ' { + log.warnf("Expected space, got %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_SPACE_0, c) + return + } + ctx.c += 1 + } + + eat_spaces(&ctx) + + // Halfmove clock + + { + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + number_string := read_number(&ctx) + n, n_ok := strconv.parse_int(number_string, 10) + if !n_ok { + log.warnf("Invalid halfmove clock: %v", number_string) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_INVALID_HALFMOVE_CLOCK_0, number_string) + return + } + ctx.board.halfmove_clock = cast(i32)n + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c != ' ' { + log.warnf("Expected space, got %v", c) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_SPACE_0, c) + return + } + ctx.c += 1 + } + + eat_spaces(&ctx) + + // Fullmove number + + { + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + number_string := read_number(&ctx) + n, n_ok := strconv.parse_int(number_string, 10) + if !n_ok { + log.warnf("Invalid fullmove number: %v", number_string) + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_INVALID_FULLMOVE_NUMBER_0, number_string) + return + } + ctx.board.fullmove_number = cast(i32)n + + // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + // ^ + + eat_spaces(&ctx) + if ctx.c < len(ctx.s) { + log.warnf("Expected end of string") + error = make_fen_errorf(ctx, allocator, IDS_FEN_ERR_EXPECTED_END_OF_STRING) + return + } + } + + log.debugf("FEN string parsed successfully: %v", s) + + board = ctx.board + return +} + +format_fen :: proc(board: board, allocator := context.temp_allocator) -> string { + sb := strings.builder_make_len_cap(0, 200, allocator) + + fen_write_pieces(&sb, board.pieces) + + strings.write_string(&sb, " ") + + switch board.turn_player { + case .white: + strings.write_string(&sb, "w") + case .black: + strings.write_string(&sb, "b") + case: + strings.write_string(&sb, "?") + } + + strings.write_string(&sb, " ") + + if !board.white_can_castle_kingside && + !board.white_can_castle_queenside && + !board.black_can_castle_kingside && + !board.black_can_castle_queenside { + strings.write_string(&sb, "-") + } else { + if board.white_can_castle_kingside { strings.write_string(&sb, "K") } + if board.white_can_castle_queenside { strings.write_string(&sb, "Q") } + if board.black_can_castle_kingside { strings.write_string(&sb, "k") } + if board.black_can_castle_queenside { strings.write_string(&sb, "q") } + } + + strings.write_string(&sb, " ") + + fen_write_en_passant_target_square(&sb, board.en_passant_target_square) + + strings.write_string(&sb, " ") + + strings.write_int(&sb, auto_cast board.halfmove_clock) + + strings.write_string(&sb, " ") + + strings.write_int(&sb, auto_cast board.fullmove_number) + + return strings.to_string(sb) +} + +fen_write_pieces :: proc(sb: ^strings.Builder, pieces: board_pieces) { + assert(len(pieces) == 64) + + // file index 0 is the 'A' file. + // rank index 0 is the '1' rank. + for rank := 7; rank >= 0; rank -= 1 { + + file := 0 + + file_loop: for { + if file == 8 { + break file_loop + } + assert(file < 8) + p := pieces[rank * 8 + file] + if p.type == .none { + // Consolidate consecutive empty squares within a rank. + num_consecutive_empty_squares: int + for file < 8 && pieces[rank * 8 + file].type == .none { + num_consecutive_empty_squares += 1 + file += 1 + } + assert(num_consecutive_empty_squares > 0) + assert(num_consecutive_empty_squares <= 8) + strings.write_rune(sb, cast(rune)('0' + num_consecutive_empty_squares)) + } else { + switch { + case p == { .rook, .white }: strings.write_rune(sb, 'R') + case p == { .knight, .white }: strings.write_rune(sb, 'N') + case p == { .bishop, .white }: strings.write_rune(sb, 'B') + case p == { .queen, .white }: strings.write_rune(sb, 'Q') + case p == { .king, .white }: strings.write_rune(sb, 'K') + case p == { .pawn, .white }: strings.write_rune(sb, 'P') + case p == { .rook, .black }: strings.write_rune(sb, 'r') + case p == { .knight, .black }: strings.write_rune(sb, 'n') + case p == { .bishop, .black }: strings.write_rune(sb, 'b') + case p == { .queen, .black }: strings.write_rune(sb, 'q') + case p == { .king, .black }: strings.write_rune(sb, 'k') + case p == { .pawn, .black }: strings.write_rune(sb, 'p') + case: strings.write_rune(sb, '?') + } + file += 1 + } + } + + assert(file == 8) + + if rank != 0 { + strings.write_rune(sb, '/') + } + } +} + +fen_write_en_passant_target_square :: proc(sb: ^strings.Builder, square: square_specifier) { + if square == {} { + // NOTE: This works because (0, 0) is never a valid en passant square. + strings.write_string(sb, "-") + } else { + if square.file < 0 || square.file >= 8 { + strings.write_string(sb, "?") + } else { + strings.write_rune(sb, cast(rune)('a' + square.file)) + } + + if square.rank < 0 || square.rank >= 8 { + strings.write_string(sb, "?") + } else { + strings.write_rune(sb, cast(rune)('1' + square.rank)) + } + } +} + +@(private="file") +parse_ctx :: struct { + s: string, + c: int, + board: board, +} + +@(private="file") +eat_spaces :: proc(ctx: ^parse_ctx) { + for ctx.c < len(ctx.s) && strings.is_ascii_space(cast(rune)ctx.s[ctx.c]) { + ctx.c += 1 + } +} + +@(private="file") +read_number :: proc(ctx: ^parse_ctx) -> string { + start := ctx.c + for { + c := ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 + if c >= '0' && c <= '9' { + ctx.c += 1 + } else { + break + } + } + return ctx.s[start:ctx.c] +} diff --git a/win32/fentool/file_dialog.odin b/win32/fentool/file_dialog.odin new file mode 100644 index 0000000..125815e --- /dev/null +++ b/win32/fentool/file_dialog.odin @@ -0,0 +1,127 @@ +package fen_tool + +import runtime "base:runtime" +import strings "core:strings" +import log "core:log" +import win "core:sys/windows" + +file_dialog_mode :: enum { + open, + save, +} + +file_dialog_filter :: struct { + // The full line of text displayed to the user in the selection box. + // + // Examples: + // - `"Text files (*.txt)"` + // - `"All files (*.*)"` + display_text: string, + + // The internal search pattern used to filter the files. + // + // Examples: + // - `"*.txt"` + // - `"*.*"` // NOTE: This includes files without an extension. + filter: string, +} + +file_dialog :: proc( + owner: win.HWND, + mode: file_dialog_mode, + initial_file: Maybe(string) = nil, + filters: []file_dialog_filter = nil, + #any_int selected_filter_index: u32 = 0, + dialog_caption: Maybe(string) = nil, + initial_directory: Maybe(string) = nil, + allocator: runtime.Allocator = context.temp_allocator, +) -> ( + dialog_confirmed: bool, + selected_file: string, +) { + file_buf := make([]u16, win.MAX_PATH_WIDE, context.temp_allocator) + file_buf[0] = 0 + + if initial_file != nil { + converted := win.utf8_to_wstring(file_buf, initial_file.?) + if converted == nil { + // Conversion error. + log.warnf("Unable to convert initial file name string.") + file_buf[0] = 0 + } + } + + ofn := win.OPENFILENAMEW { + lStructSize = size_of(win.OPENFILENAMEW), + hwndOwner = owner, + lpstrFilter = file_dialog_build_filter_string(filters), + nFilterIndex = selected_filter_index + 1, + lpstrFile = cast(win.LPWSTR)raw_data(file_buf[:]), + nMaxFile = win.MAX_PATH_WIDE, + } + + if dialog_caption != nil { + ofn.lpstrTitle = win.utf8_to_wstring(dialog_caption.?, context.temp_allocator) + } + if initial_directory != nil { + ofn.lpstrInitialDir = win.utf8_to_wstring(initial_directory.?, context.temp_allocator) + } + + confirmed: win.BOOL + + switch mode { + case .open: + ofn.Flags = win.OFN_PATHMUSTEXIST | win.OFN_FILEMUSTEXIST | win.OFN_EXPLORER + confirmed = win.GetOpenFileNameW(&ofn) + case .save: + ofn.Flags = win.OFN_OVERWRITEPROMPT | win.OFN_EXPLORER + confirmed = win.GetSaveFileNameW(&ofn) + case: + unreachable() + } + + if confirmed { + // Convert result to string. + path := cast(win.wstring)ofn.lpstrFile + path_str, err1 := win.wstring_to_utf8(path, N = -1, allocator = allocator) + if err1 != nil { + log.warnf("File dialog confirmed - string conversion error: %v", err1) + return false, "" + } + return true, path_str + } else { + return false, "" + } +} + +// Converts the `filters` slice to the string format required by `OPENFILENAMEW.lpstrFilter`. +// +// Returns `nil` if `filters` is an empty slice. +file_dialog_build_filter_string :: proc(filters: []file_dialog_filter, allocator: runtime.Allocator = context.temp_allocator) -> win.LPCWSTR { + + if len(filters) == 0 { + return nil + } + + needed_capacity := 0 + for f in filters { + needed_capacity += len(f.display_text) + needed_capacity += 1 + needed_capacity += len(f.filter) + needed_capacity += 1 + } + needed_capacity += 1 + + sb: strings.Builder + strings.builder_init_len_cap(&sb, 0, needed_capacity, context.temp_allocator) + + for f in filters { + strings.write_string(&sb, f.display_text) + strings.write_string(&sb, "\x00") + strings.write_string(&sb, f.filter) + strings.write_string(&sb, "\x00") + } + strings.write_string(&sb, "\x00") + + return win.utf8_to_wstring(strings.to_string(sb), allocator) +} diff --git a/win32/fentool/icon.ico b/win32/fentool/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..67f1f373f5f0794bbc167910d6f8c770e9ed8eef GIT binary patch literal 82214 zcmeI4J#!pI5QgU@QxfNbvm+7$1q2TL6DS2_$`w*LAwjXpTxCkR5~@ro!vPVFaYTS) z6X*vJ0EwzFBq}N4Jac<@+UeExT&q>u4?RD!sEvww(aoK z$02+X-|g(o``aP>6SqBhQ1!q5AcXHfN$*qJ4v#(?h5tVd;Zf|RLwp$@qqu$6KCE75 z&!hO99mBpe(Zgm0fd4c|R~9$x+t*Ax8daE zB>es7U*YuhH2nAO-*9$z7S7Mls{`>20SNSlz;rt8;#wcB-Ky2EJ^kUe=a1HCaT&|w zuQ7e(oUdCQ7a)#*3-j;S+GefAQ(w8B+VuOi>6^^2aNRXNbq&;QU!@1;SCrsW#Y^O{ zs$H3XR!xrE{qy4y^cK%@YxP30v_7<4Pkp2E{(AGPy*lz7uYqQlLGtyFw7#tnc)b}8 zlJBCQKBw-)$LH~}{G56(b{0*1PF;RR{igbidU$vkj*gDP4=-M1pH<)d_GbC{^y1=T zaWq~b0D+znm?dcUnkPz22UmtC)%dD`Y{I3(pU=6UNTTlE;m)E|u)XTTh9;^R*X>@JT*q;-?|2l!3);Rl?WldJSc`NPF zt#9W4-Wa-Kq1c}j!2dddoEGuFBCL9|S^87=KKW1b>W!mUEL7Tq|8)YA*9xq?*(~kR zttb8^uiiL%#X_Y$UjNJEb*(Qk$n{~rPC)W1@s#u1?w8i9TVKw9ZQEBmKlbMYBrp80 z6Og=enxoo3*~*#S=$F>3TTlE;FP3=9`EB=0>&5>%0qMUIPdUHserdhB^~AsQUx}xj z-*&&WUi_~Ukp3(2l=Iu}m)5IWPy9>&m3XG#NAa}un<}1WUGGi%-(!2)*JFFy7yEMp zCB5Q*oj^{r`S(9Lj{bcFh<_gesYl{JCm{Lg(Qm*0@u{Es=TkrRU+OzofAfAN`qv3q zc@qCV0>rYg%SoNx_pZag*=~F-T-=}`+zg4fq zzfQo)llb=$ApU&>ta{bePyM&@^r@fv?^8eZ->O&QUngMYN&Nc=5dS^`R=w)#r~X@c z`n)7iiGLpf z;@?NWs#jh8_Rl{)?^FML-lzVd|C|8Vzd3=N&Uj4z(+NmkInCzp>5%_@1jzqB0#?21 z>gW2$%G0NQuK#@M=laj8SK?nMVC6~t`v?&KJ_1&~>guQdTY37_PyP3)pZagrEAg)r zu<|7SeFTVq9|5agb@fyKtvr3|r~doYPyM&*mH5{QSa}lvJ_5wQkAPLLy85aAR-Qif zQ~!PHr~X^@O8n~ttUQT-9|7XuN5HCAUH#O5D^H*LssBFpQ~#}cCH{2+R-VMaj{x!S zBVg65u72vjm8Vbr)PJA)+pqspFKzcr-&eos_;1@@d4BBA2}nNpUnd}Wx7{zTSGS({m;Nj9l=Iu}m)7g`zdU}|`_doSuM?2Gy6)f7`lLO&_2v9mI$zhu zu|Fq(|8)X6t>*8^bxq%;^+xqBKtHihKn}>((<00SG_<0uX=z1Rwwb z2tWV=5P-lC2~25soIp@oLZb`^#^HlLRudpTh>z9k2;@)d3-whA5b`JaQ;`Sxll)2k zR04$jN&ZyiLH;Cvl0TIIA%BuT6?u?9$)Ds;B|ylZ ^main_ctx { + return cast(^main_ctx)cast(uintptr)win.GetWindowLongPtrW(hdlg, win.DWLP_USER) +} + +dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.INT_PTR { + + switch msg { + + case win.WM_INITDIALOG: + // Setup main window context + context = window_context_tls + ctx := new(main_ctx) + ctx.runtime_context = &window_context_tls + ctx.hdlg = hdlg + win.SetWindowLongPtrW(hdlg, win.DWLP_USER, cast(win.LONG_PTR)cast(uintptr)ctx) + + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + idi_main := cast(rawptr)cast(uintptr)IDI_MAIN + icon_big := win.LoadImageW(hInstance, cast(win.LPCWSTR)idi_main, win.IMAGE_ICON, win.GetSystemMetrics(win.SM_CXICON), win.GetSystemMetrics(win.SM_CYICON), win.LR_DEFAULTCOLOR | win.LR_SHARED) + icon_small := win.LoadImageW(hInstance, cast(win.LPCWSTR)idi_main, win.IMAGE_ICON, win.GetSystemMetrics(win.SM_CXSMICON), win.GetSystemMetrics(win.SM_CYSMICON), win.LR_DEFAULTCOLOR | win.LR_SHARED) + win.SendMessageW(hdlg, win.WM_SETICON, win.ICON_BIG, cast(win.LPARAM)cast(uintptr)icon_big) + win.SendMessageW(hdlg, win.WM_SETICON, win.ICON_SMALL, cast(win.LPARAM)cast(uintptr)icon_small) + + hmenu := win.GetMenu(hdlg) + win.CheckMenuRadioItem(hmenu, IDM_VISUALS_NORMAL, IDM_VISUALS_GINGER, IDM_VISUALS_NORMAL, win.MF_BYCOMMAND) + + reset_to_starting_position(hdlg, ctx, set_dirty = false) + + case win.WM_NCDESTROY: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + free(ctx) + + case win.WM_DESTROY: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + win.PostQuitMessage(0) + // Return 1 to stop DefDlgProc message handling. + return 1 + + case win.WM_CLOSE: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + if prompt_if_project_dirty(hdlg, ctx) { + win.DestroyWindow(hdlg) + } + // Return 1 to stop DefDlgProc message handling. + return 1 + + case win.WM_SIZE: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + // The status bar will adjust itself, but it does need a message to trigger it. + status_bar := win.GetDlgItem(hdlg, IDC_STATUSBAR) + win.SendMessageW(status_bar, win.WM_SIZE, 0, 0) + + case win.WM_COMMAND: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + id := win.LOWORD(wparam) + nc := win.HIWORD(wparam) + + switch id { + + case IDM_RESET_TO_STARTING_POSITION: + reset_to_starting_position(ctx.hdlg, ctx, prompt_for_confirmation = true) + + case IDM_NEW: + file_new(hdlg, ctx) + + case IDM_OPEN: + file_open(hdlg, ctx) + + case IDM_SAVE: + file_save(hdlg, ctx) + + case IDM_SAVE_AS: + file_save_as(hdlg, ctx) + + case IDM_EXIT: + if prompt_if_project_dirty(hdlg, ctx) { + win.DestroyWindow(hdlg) + } + + case IDM_PLAY_SOUND: + // Toggle Play Sound option. + hmenu := win.GetMenu(hdlg) + mii: win.MENUITEMINFOW + mii.cbSize = size_of(win.MENUITEMINFOW) + mii.fMask = win.MIIM_STATE + win.GetMenuItemInfoW(hmenu, IDM_PLAY_SOUND, false, &mii) + checked := (mii.fState & win.MFS_CHECKED) != 0 + if checked { + win.CheckMenuItem(hmenu, IDM_PLAY_SOUND, win.MF_BYCOMMAND | win.MF_UNCHECKED) + } else { + win.CheckMenuItem(hmenu, IDM_PLAY_SOUND, win.MF_BYCOMMAND | win.MF_CHECKED) + } + + case IDM_VISUALS_NORMAL: + hmenu := win.GetMenu(hdlg) + win.CheckMenuRadioItem(hmenu, IDM_VISUALS_NORMAL, IDM_VISUALS_GINGER, IDM_VISUALS_NORMAL, win.MF_BYCOMMAND) + board_control := win.GetDlgItem(hdlg, IDC_BOARD_CONTROL) + board_control_set_spritemap(board_control, .normal) + + case IDM_VISUALS_GINGER: + hmenu := win.GetMenu(hdlg) + win.CheckMenuRadioItem(hmenu, IDM_VISUALS_NORMAL, IDM_VISUALS_GINGER, IDM_VISUALS_GINGER, win.MF_BYCOMMAND) + board_control := win.GetDlgItem(hdlg, IDC_BOARD_CONTROL) + board_control_set_spritemap(board_control, .ginger) + + case IDM_ABOUT: + about_show_dialog(hdlg) + + case IDC_TEXTBOX_FEN: + if nc == win.EN_CHANGE { + if ctx.updating == 0 { + reparse_fen(hdlg, ctx, interactive = true) + set_project_dirty(ctx) + } + } + + case IDC_RADIOBUTTON_WHITE_TO_MOVE, + IDC_RADIOBUTTON_BLACK_TO_MOVE: + if nc == win.BN_CLICKED { + win.CheckRadioButton(hdlg, IDC_RADIOBUTTON_WHITE_TO_MOVE, IDC_RADIOBUTTON_BLACK_TO_MOVE, auto_cast id) + update_from_control_states(hdlg, ctx) + } + + case IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE, + IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE, + IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, + IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE: + if nc == win.BN_CLICKED { + // Toggle the check state. + current_state := win.IsDlgButtonChecked(hdlg, auto_cast id) == win.BST_CHECKED + new_state := current_state ? win.BST_UNCHECKED : win.BST_CHECKED + win.CheckDlgButton(hdlg, auto_cast id, auto_cast new_state) + update_from_control_states(hdlg, ctx) + } + + case IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE: + if nc == win.EN_CHANGE { + if ctx.updating == 0 { + show_en_passant_validation_tip(hdlg) + update_from_control_states(hdlg, ctx) + } + } + + case IDC_TEXTBOX_HALFMOVE_CLOCK, + IDC_TEXTBOX_FULLMOVE_NUMBER: + if nc == win.EN_CHANGE { + if ctx.updating == 0 { + update_from_control_states(hdlg, ctx) + } + } + + case IDC_BUTTON_CLEAR_EN_PASSANT: + ctx.updating += 1 + defer ctx.updating -= 1 + set_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, "-") + update_from_control_states(hdlg, ctx) + } + + case win.WM_NOTIFY: + switch wparam { + case IDC_BOARD_CONTROL: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + nm := cast(^NM_BOARD_CONTROL_NOTIFICATION)cast(uintptr)lparam + code := cast(board_control_notification_code)nm.hdr.code + switch code { + case .none: + case .hover: + if nm.hovering { + // Build a string like "A1 - Rook (White)" + square := nm.square + rank := square.rank + file := square.file + piece := ctx.current_pieces[rank * 8 + file] + piece_str := format_piece_display_text(piece, context.temp_allocator) + pos_str := board_format_position(square, context.temp_allocator) + status_text: string + if piece_str != "" { + status_text = fmt.tprintf("%v - %v", pos_str, piece_str) + } else { + status_text = pos_str + } + set_dialog_item_text(hdlg, IDC_STATUSBAR, status_text) + } else { + set_dialog_item_text(hdlg, IDC_STATUSBAR, "") + } + case .place_piece: + rank := nm.square.rank + file := nm.square.file + piece := nm.piece + ctx.current_pieces[rank * 8 + file] = piece + update_from_control_states(hdlg, ctx) + play_piece_placing_sound_if_enabled(hdlg) + case .set_en_passant_target_square: + square := nm.square + set_en_passant_target_square(hdlg, square) + update_from_control_states(hdlg, ctx) + case .set_castling_right: + castling_right := nm.castling_right + state := nm.castling_right_state + button_state: win.UINT + switch state { + case .allow: button_state = win.BST_CHECKED + case .disallow: button_state = win.BST_UNCHECKED + case: unreachable() + } + switch castling_right { + case .white_kingside: win.CheckDlgButton(hdlg, IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE, button_state) + case .white_queenside: win.CheckDlgButton(hdlg, IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE, button_state) + case .black_kingside: win.CheckDlgButton(hdlg, IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, button_state) + case .black_queenside: win.CheckDlgButton(hdlg, IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE, button_state) + } + update_from_control_states(hdlg, ctx) + case .exchange_pieces: + rank1 := nm.square.rank + file1 := nm.square.file + rank2 := nm.square2.rank + file2 := nm.square2.file + tmp := ctx.current_pieces[rank1 * 8 + file1] + ctx.current_pieces[rank1 * 8 + file1] = ctx.current_pieces[rank2 * 8 + file2] + ctx.current_pieces[rank2 * 8 + file2] = tmp + update_from_control_states(hdlg, ctx) + play_piece_placing_sound_if_enabled(hdlg) + } + } + + case win.WM_MENUSELECT: + ctx := get_main_ctx(hdlg) + context = ctx.runtime_context^ + menu_item_id := cast(win.UINT)win.LOWORD(wparam) + flags := win.HIWORD(wparam) + hmenu := cast(win.HMENU)cast(uintptr)lparam + if flags == 0xffff && hmenu == nil { + set_dialog_item_text(hdlg, IDC_STATUSBAR, "") + } else { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + // NOTE: If no matching string exists for the specified menu item ID, this will return "", + // in which case the status bar shows nothing. + string_resource, _ := load_string_resource(hInstance, menu_item_id, context.temp_allocator) + set_dialog_item_text(hdlg, IDC_STATUSBAR, string_resource) + } + } + + return 0 +} + +file_new :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { + if !prompt_if_project_dirty(hdlg, ctx) { + return false + } + delete(ctx.active_project_file_path) + ctx.active_project_file_path = "" + reset_to_starting_position(hdlg, ctx, set_dirty = false) + set_project_dirty(ctx, false) + return true +} + +file_open :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { + if !prompt_if_project_dirty(hdlg, ctx) { + return false + } + filters := get_file_dialog_filters() + dialog_confirmed, file_path := file_dialog( + owner = hdlg, + mode = .open, + filters = filters[:], + allocator = context.allocator, + ) + if !dialog_confirmed { + return false + } + data, error := os.read_entire_file_from_path(file_path, context.temp_allocator) + if error != nil { + // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + error_message := os.error_string(error) + error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) + win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) + return false + } + fen := transmute(string)data + set_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, fen) + delete(ctx.active_project_file_path) + ctx.active_project_file_path = file_path + set_project_dirty(ctx, false) + return true +} + +file_save :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { + if ctx.active_project_file_path == "" { + return file_save_as(hdlg, ctx) + } else { + fen := get_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, 512) + error := os.write_entire_file(ctx.active_project_file_path, fen) + if error != nil { + // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. + error_message := os.error_string(error) + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) + win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) + return false + } + set_project_dirty(ctx, false) + return true + } +} + +file_save_as :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + filters := get_file_dialog_filters() + dialog_confirmed, file_path := file_dialog( + owner = hdlg, + mode = .save, + filters = filters[:], + allocator = context.allocator, + ) + if !dialog_confirmed { + return false + } + fen := get_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, 512) + error := os.write_entire_file(file_path, fen) + if error != nil { + // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. + error_message := os.error_string(error) + error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) + win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) + return false + } + delete(ctx.active_project_file_path) + ctx.active_project_file_path = file_path + set_project_dirty(ctx, false) + return true +} + +get_file_dialog_filters :: proc() -> [2]file_dialog_filter { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + return [2]file_dialog_filter { + { display_text = load_string_resource(hInstance, IDS_FEN_FILES_FILTER), filter = "*.fen" }, + { display_text = load_string_resource(hInstance, IDS_ALL_FILES_FILTER), filter = "*.*" }, + } +} + +play_piece_placing_sound_if_enabled :: proc(hdlg: win.HWND) { + hmenu := win.GetMenu(hdlg) + mii: win.MENUITEMINFOW + mii.cbSize = size_of(win.MENUITEMINFOW) + mii.fMask = win.MIIM_STATE + win.GetMenuItemInfoW(hmenu, IDM_PLAY_SOUND, false, &mii) + checked := (mii.fState & win.MFS_CHECKED) != 0 + if checked { + win.PlaySoundW("IDW_CLACK", win.GetModuleHandleW(nil), win.SND_ASYNC | win.SND_RESOURCE) + } +} + +prompt_if_project_dirty :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { + if ctx.project_dirty { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + text := load_string_resource(hInstance, IDS_UNSAVED_CHANGES_MESSAGE) + caption := load_string_resource(hInstance, IDS_UNSAVED_CHANGES_CAPTION) + choice := win.MessageBoxW(hdlg, win.utf8_to_wstring(text), win.utf8_to_wstring(caption), win.MB_YESNOCANCEL) + switch choice { + case win.IDYES: + if !file_save(hdlg, ctx) { + // Saving failed or got canceled by user. + return false + } + case win.IDNO: + // OK, discard. + case: + return false + } + } + return true +} + +set_project_dirty :: proc(ctx: ^main_ctx, dirty := true) { + if ctx.project_dirty != dirty { + ctx.project_dirty = dirty + if dirty { + // Append "*" to the main window caption. + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + caption := load_string_resource(hInstance, IDS_TITLE, context.temp_allocator) + caption_dirty := strings.concatenate({ caption, "*" }, context.temp_allocator) + win.SetWindowTextW(ctx.hdlg, win.utf8_to_wstring(caption_dirty, context.temp_allocator)) + } else { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + caption := load_string_resource(hInstance, IDS_TITLE, context.temp_allocator) + win.SetWindowTextW(ctx.hdlg, win.utf8_to_wstring(caption, context.temp_allocator)) + } + } +} + +show_en_passant_validation_tip :: proc(hdlg: win.HWND) { + + text := get_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, 512, context.temp_allocator) + trimmed := strings.trim_space(text) + if len(trimmed) < 2 { + return + } + + _, ok := parse_en_passant_target_square(trimmed) + if ok { + return + } + + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + + tip := win.EDITBALLOONTIP { + cbStruct = size_of(win.EDITBALLOONTIP), + pszTitle = win.utf8_to_wstring_alloc(load_string_resource(hInstance, IDS_INVALID_EN_PASSANT_TARGET_SQUARE, context.temp_allocator), context.temp_allocator), + pszText = win.utf8_to_wstring_alloc(load_string_resource(hInstance, IDS_INVALID_EN_PASSANT_TARGET_SQUARE_HINT, context.temp_allocator), context.temp_allocator), + ttiIcon = win.TTI_ERROR, + } + + // NOTE: We send the message to display the balloon tip, but we do not prevent user input here, because that would be irritating. + win.SendDlgItemMessageW(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, win.EM_SHOWBALLOONTIP, 0, cast(win.LPARAM)cast(uintptr)&tip) +} + +format_piece_display_text :: proc(piece: board_piece, allocator := context.temp_allocator) -> string { + + type_str: string + + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + + switch piece.type { + case .none: return "" + case .rook: type_str = load_string_resource(hInstance, IDS_ROOK, context.temp_allocator) + case .knight: type_str = load_string_resource(hInstance, IDS_KNIGHT, context.temp_allocator) + case .bishop: type_str = load_string_resource(hInstance, IDS_BISHOP, context.temp_allocator) + case .queen: type_str = load_string_resource(hInstance, IDS_QUEEN, context.temp_allocator) + case .king: type_str = load_string_resource(hInstance, IDS_KING, context.temp_allocator) + case .pawn: type_str = load_string_resource(hInstance, IDS_PAWN, context.temp_allocator) + case: type_str = "?" + } + + color_str: string + + switch piece.color { + case .white: color_str = load_string_resource(hInstance, IDS_WHITE, context.temp_allocator) + case .black: color_str = load_string_resource(hInstance, IDS_BLACK, context.temp_allocator) + case: color_str = "?" + } + + return fmt.aprintf("%v (%v)", type_str, color_str, allocator = allocator) +} + +STARTING_POSITION_FEN :: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + +reset_to_starting_position :: proc(hdlg: win.HWND, ctx: ^main_ctx, set_dirty := true, prompt_for_confirmation := false) { + + if prompt_for_confirmation { + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + message := win.utf8_to_wstring(load_string_resource(hInstance, IDS_CONFIRM_RESET_MESSAGE, context.temp_allocator)) + caption := win.utf8_to_wstring(load_string_resource(hInstance, IDS_CONFIRM_RESET_CAPTION, context.temp_allocator)) + choice := win.MessageBoxW(hdlg, message, caption, win.MB_YESNO) + if choice != win.IDYES { + return + } + } + + { + ctx.updating += 1 + defer ctx.updating -= 1 + set_dialog_item_text_cstring16(hdlg, IDC_TEXTBOX_FEN, STARTING_POSITION_FEN) + if set_dirty { + set_project_dirty(ctx) + } + } + + reparse_fen(hdlg, ctx, interactive = false) +} + +// Parses the contents of the FEN string text box, and updates all +// controls accordingly to the board state it denotes. +reparse_fen :: proc(hdlg: win.HWND, ctx: ^main_ctx, interactive: bool) { + fen_string := get_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, 512, context.temp_allocator) + if fen_string == "" { + fmt.printfln("FEN string is fucked.") + return + } + + board, error := parse_fen(fen_string) + if error.is_error { + log.debugf("FEN string is invalid.") + if interactive { + // Display balloon tip with the error message. + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + tip := win.EDITBALLOONTIP { + cbStruct = size_of(win.EDITBALLOONTIP), + pszTitle = win.utf8_to_wstring_alloc(load_string_resource(hInstance, IDS_INVALID_FEN_STRING, context.temp_allocator), context.temp_allocator), + pszText = win.utf8_to_wstring_alloc(error.display_message, context.temp_allocator), + ttiIcon = win.TTI_ERROR, + } + win.SendDlgItemMessageW(hdlg, IDC_TEXTBOX_FEN, win.EM_SHOWBALLOONTIP, 0, cast(win.LPARAM)cast(uintptr)&tip) + } + return + } + + fmt.printfln("Board:") + fmt.printfln("%v", board_format_full(board)) + + ctx.updating += 1 + defer ctx.updating -= 1 + + ctx.current_pieces = board.pieces + + board_control := win.GetDlgItem(hdlg, IDC_BOARD_CONTROL) + board_control_set_board(board_control, board) + + check_button: win.c_int + switch board.turn_player { + case .white: + check_button = IDC_RADIOBUTTON_WHITE_TO_MOVE + case .black: + check_button = IDC_RADIOBUTTON_BLACK_TO_MOVE + } + win.CheckRadioButton(hdlg, IDC_RADIOBUTTON_WHITE_TO_MOVE, IDC_RADIOBUTTON_BLACK_TO_MOVE, check_button) + + win.CheckDlgButton(hdlg, IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE, board.white_can_castle_kingside ? win.BST_CHECKED : win.BST_UNCHECKED) + win.CheckDlgButton(hdlg, IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE, board.white_can_castle_queenside ? win.BST_CHECKED : win.BST_UNCHECKED) + win.CheckDlgButton(hdlg, IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, board.black_can_castle_kingside ? win.BST_CHECKED : win.BST_UNCHECKED) + win.CheckDlgButton(hdlg, IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE, board.black_can_castle_queenside ? win.BST_CHECKED : win.BST_UNCHECKED) + + set_en_passant_target_square(hdlg, board.en_passant_target_square) + + win.SetDlgItemInt(hdlg, IDC_TEXTBOX_HALFMOVE_CLOCK, cast(win.UINT)board.halfmove_clock, true) + win.SetDlgItemInt(hdlg, IDC_TEXTBOX_FULLMOVE_NUMBER, cast(win.UINT)board.fullmove_number, true) +} + +set_en_passant_target_square :: proc(hdlg: win.HWND, square: square_specifier) { + square_text := board_format_en_passant_target_square(square) + set_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, square_text) +} + +// Queries the state of all controls, and then displays the +// corresponding FEN string in the FEN string text box, and the +// board state in the board control. +update_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) { + + // Construct board from control states. + board, board_ok := get_board_from_control_states(hdlg, ctx) + if !board_ok { + log.warn("Could not construct board from control states.") + return + } + + // Display the board state in the board control. + board_control := win.GetDlgItem(hdlg, IDC_BOARD_CONTROL) + board_control_set_board(board_control, board) + + // Convert board info to FEN string. + fen_string, fen_string_ok := construct_fen_string_from_board(board) + if !fen_string_ok { + log.warn("Could not construct FEN string from board.") + return + } + + // Put the constructed FEN string the FEN string text box. + old_fen_string := get_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, 512, context.temp_allocator) + if fen_string != old_fen_string { + ctx.updating += 1 + defer ctx.updating -= 1 + log.debugf("FEN string changing from %q to %q", old_fen_string, fen_string) + fen_wstr := win.utf8_to_wstring_alloc(fen_string, context.temp_allocator) + win.SetDlgItemTextW(hdlg, IDC_TEXTBOX_FEN, fen_wstr) + set_project_dirty(ctx) + } +} + +get_board_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> (result: board, ok: bool) { + + log.debugf("Constructing board from control states...") + + // Collect all information in this board instance. + board: board + + board.pieces = ctx.current_pieces + + if win.IsDlgButtonChecked(hdlg, IDC_RADIOBUTTON_WHITE_TO_MOVE) == win.BST_CHECKED { + board.turn_player = .white + } else if win.IsDlgButtonChecked(hdlg, IDC_RADIOBUTTON_BLACK_TO_MOVE) == win.BST_CHECKED { + board.turn_player = .black + } else { + ok = false + return + } + + board.white_can_castle_kingside = win.IsDlgButtonChecked(hdlg, IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE) == win.BST_CHECKED + board.white_can_castle_queenside = win.IsDlgButtonChecked(hdlg, IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE) == win.BST_CHECKED + board.black_can_castle_kingside = win.IsDlgButtonChecked(hdlg, IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE) == win.BST_CHECKED + board.black_can_castle_queenside = win.IsDlgButtonChecked(hdlg, IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE) == win.BST_CHECKED + + en_passant_target_square_text := get_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, 10, context.temp_allocator) + en_passant_target_square, en_passant_target_square_ok := parse_en_passant_target_square(en_passant_target_square_text) + if !en_passant_target_square_ok { + ok = false + return + } + board.en_passant_target_square = en_passant_target_square + + { + success: win.BOOL + board.halfmove_clock = cast(i32)win.GetDlgItemInt(hdlg, IDC_TEXTBOX_HALFMOVE_CLOCK, &success, bSigned = false) + if !success { + ok = false + return + } + } + + { + success: win.BOOL + board.fullmove_number = cast(i32)win.GetDlgItemInt(hdlg, IDC_TEXTBOX_FULLMOVE_NUMBER, &success, bSigned = false) + if !success { + ok = false + return + } + } + + return board, true +} + +construct_fen_string_from_board :: proc(board: board) -> (result: string, ok: bool) { + fen_string := format_fen(board) + return fen_string, true +} + +parse_en_passant_target_square :: proc(s0: string) -> (result: square_specifier, ok: bool) { + s := strings.trim_space(s0) + if s == "" || s == "-" { + // We interpret an empty string as "no en passant". + return {}, true + } + if len(s) != 2 { + log.warnf("Invalid en passant target square (string length must be 2): %v", s) + return {}, false + } + + // NOTE: While FEN only allows lowercase letters here, the text box accepts both. + file: int + if s[0] >= 'A' && s[0] <= 'H' { + file = cast(int)s[0] - 'A' + } else if s[0] >= 'a' && s[0] <= 'h' { + file = cast(int)s[0] - 'a' + } else { + log.warnf("Invalid file in en passant target square: %v", s) + return {}, false + } + + rank := cast(int)s[1] - '1' + if rank < 0 || rank >= 8 { + log.warnf("Invalid rank in en passant target square: %v", s) + return {}, false + } + + return { file = cast(i8)file, rank = cast(i8)rank }, true +} diff --git a/win32/fentool/piecesnormal.png b/win32/fentool/piecesnormal.png new file mode 100644 index 0000000000000000000000000000000000000000..cd89893ca5203dc7844f6c8a356a1dc86777bd22 GIT binary patch literal 31133 zcma(3Wn5J6_dO1up+UL?>FyHg?nWBv6b!nXA*2Na6_9R_R9b2P1yoYHL%LHM{ul4h z@B8?^eGxJY=bX9DzV=>wt+gjwOH&C4>k$?NK{zVP@;VTN4E_s)Fwnp^6l#Lv`=8u( zlw@I3KRJW%AG4Rykb$5t2@kHVP{Gf%Hp)605ah=ULGW-0x&#lwHz3H97lO7dAxJb8 zf*!hLeA5;Ozd*BAQ<4YY!M_W+=KG^zx+ojDLl8H^{U^+)P|6EDi0+}Hp@6=HNrgv< zEcufh{EikK8gO{vT1X*tp!`@8{46=zgzB^eK`5h-kfq?a2*83RonDI^gkDe*UBTIm9f z1g3lG*5=-3+GbXe)ZhqSL31&W{+Ry0$*DOMN{jK|heMdWg7hn z^s91We`gYO66#h^OxjD3J1rjEOlSYrPA~5M6l@TRmD8)ytkl$0GIdPhtqW#Tz4DLr zB!~(q0(QQSU&#+cNR{4Vb{-MInjmT@aigw%T5Sm(t_o%QFeBf;tk`!LQtny(=|cQW z#>U2zD?gJJill8yl)3nO@nF?h2+442!g}}S@R*OZoEqNHbN2SLCOy+|vs`)0@Lgq9 z)y}^OQrx1~gm8Yk^d$VXLJIf|2z^V>-=J#ExtA|6EG4wj>~zCN$5jhaO> z15s_&6%a2ZAV9~&gbMztrlkFy5;ekDq=n-orlkDToc&HrN=h##MuCimEp5I0l7F$j zUQka@53%Wy_w_4b!@M5{38~CvN}wJJSiEt$&>iTJ<;=yp)Sc0bvnHD!+-@=(?v&Wp z%dIj$#~=2vUX(=K#}8xBL|Xie+5$vgJ2~wgow+obmgnc^zjkjjHS3w4p7uHY29Fs( z5r$5XUE&wUd5{+JP-u}64p8vF=`6GZ3aCL^
mC}{W2?CzioZ>KmrXgP3yUE39e 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 HGYk43W+8)F literal 0 HcmV?d00001 diff --git a/win32/fentool/resource.h b/win32/fentool/resource.h new file mode 100644 index 0000000..3200d0f --- /dev/null +++ b/win32/fentool/resource.h @@ -0,0 +1,86 @@ +#define IDC_STATIC -1 + +#define IDI_MAIN 1 + +#define IDC_TEXTBOX_FEN 102 +#define IDC_BOARD_CONTROL 104 +#define IDC_RADIOBUTTON_WHITE_TO_MOVE 105 +#define IDC_RADIOBUTTON_BLACK_TO_MOVE 106 +#define IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE 107 +#define IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE 108 +#define IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE 109 +#define IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE 110 +#define IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE 111 +#define IDC_TEXTBOX_HALFMOVE_CLOCK 112 +#define IDC_TEXTBOX_FULLMOVE_NUMBER 113 +#define IDC_STATUSBAR 114 +#define IDM_SQUARE_INFO 115 +#define IDM_CLEAR_SQUARE 116 +#define IDM_ROOK_WHITE 117 +#define IDM_KNIGHT_WHITE 118 +#define IDM_BISHOP_WHITE 119 +#define IDM_QUEEN_WHITE 120 +#define IDM_KING_WHITE 121 +#define IDM_PAWN_WHITE 122 +#define IDM_ROOK_BLACK 123 +#define IDM_KNIGHT_BLACK 124 +#define IDM_BISHOP_BLACK 125 +#define IDM_QUEEN_BLACK 126 +#define IDM_KING_BLACK 127 +#define IDM_PAWN_BLACK 128 +#define IDM_SET_EN_PASSANT_TARGET_SQUARE 129 +#define IDM_TOGGLE_CASTLING_RIGHT 130 +#define IDM_RESET_TO_STARTING_POSITION 131 +#define IDM_EXIT 132 +#define IDM_ABOUT 133 +#define IDC_BUTTON_CLEAR_EN_PASSANT 134 +#define IDM_OPEN 135 +#define IDM_SAVE_AS 136 +#define IDM_SAVE 137 +#define IDM_NEW 138 +#define IDM_PLAY_SOUND 139 +#define IDM_VISUALS_NORMAL 140 +#define IDM_VISUALS_GINGER 141 + +#define IDS_FEN_ERR_RANK_SEPARATOR_AT_INVALID_POSITION_0 400 +#define IDS_FEN_ERR_RANK_OVERFLOW_ADVANCE_0_1 401 +#define IDS_FEN_ERR_BOARD_SEPARATOR_AT_INVALID_POSITION_0 402 +#define IDS_FEN_ERR_INVALID_CHARACTER_0_AT_POSITION_1 403 +#define IDS_FEN_ERR_RANK_OVERFLOW_CANNOT_PLACE_0_1_AT_2 404 +#define IDS_FEN_ERR_EXPECTED_PLAYER_TO_MOVE_0 405 +#define IDS_FEN_ERR_EXPECTED_SPACE_0 406 +#define IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0 407 +#define IDS_FEN_ERR_CASTLING_RIGHT_SPECIFIER_CONFLICT 408 +#define IDS_FEN_ERR_EXPECTED_CASTLING_RIGHTS_SPECIFIER_0 409 +#define IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_FILE_0 410 +#define IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_RANK_0 411 +#define IDS_FEN_ERR_INVALID_HALFMOVE_CLOCK_0 412 +#define IDS_FEN_ERR_INVALID_FULLMOVE_NUMBER_0 413 +#define IDS_FEN_ERR_EXPECTED_END_OF_STRING 414 +#define IDS_INVALID_EN_PASSANT_TARGET_SQUARE 415 +#define IDS_INVALID_EN_PASSANT_TARGET_SQUARE_HINT 416 +#define IDS_INVALID_FEN_STRING 417 +#define IDS_ROOK 418 +#define IDS_KNIGHT 419 +#define IDS_BISHOP 420 +#define IDS_QUEEN 421 +#define IDS_KING 422 +#define IDS_PAWN 423 +#define IDS_WHITE 424 +#define IDS_BLACK 425 +#define IDS_TITLE 426 +#define IDS_CONFIRM_RESET_CAPTION 427 +#define IDS_CONFIRM_RESET_MESSAGE 428 +#define IDS_ERROR_CAPTION 429 +#define IDS_FEN_FILES_FILTER 430 +#define IDS_ALL_FILES_FILTER 431 +#define IDS_UNSAVED_CHANGES_CAPTION 432 +#define IDS_UNSAVED_CHANGES_MESSAGE 433 +#define IDS_ALLOW_CASTLING_WHITE_KINGSIDE 434 +#define IDS_DISALLOW_CASTLING_WHITE_KINGSIDE 435 +#define IDS_ALLOW_CASTLING_WHITE_QUEENSIDE 436 +#define IDS_DISALLOW_CASTLING_WHITE_QUEENSIDE 437 +#define IDS_ALLOW_CASTLING_BLACK_KINGSIDE 438 +#define IDS_DISALLOW_CASTLING_BLACK_KINGSIDE 439 +#define IDS_ALLOW_CASTLING_BLACK_QUEENSIDE 440 +#define IDS_DISALLOW_CASTLING_BLACK_QUEENSIDE 441 diff --git a/win32/fentool/resource.odin b/win32/fentool/resource.odin new file mode 100644 index 0000000..508f935 --- /dev/null +++ b/win32/fentool/resource.odin @@ -0,0 +1,89 @@ +package fen_tool + +IDI_MAIN :: 1 + +IDC_TEXTBOX_FEN :: 102 +IDC_BOARD_CONTROL :: 104 +IDC_RADIOBUTTON_WHITE_TO_MOVE :: 105 +IDC_RADIOBUTTON_BLACK_TO_MOVE :: 106 +IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE :: 107 +IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE :: 108 +IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE :: 109 +IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE :: 110 +IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE :: 111 +IDC_TEXTBOX_HALFMOVE_CLOCK :: 112 +IDC_TEXTBOX_FULLMOVE_NUMBER :: 113 +IDC_STATUSBAR :: 114 +IDM_SQUARE_INFO :: 115 +IDM_CLEAR_SQUARE :: 116 +IDM_ROOK_WHITE :: 117 +IDM_KNIGHT_WHITE :: 118 +IDM_BISHOP_WHITE :: 119 +IDM_QUEEN_WHITE :: 120 +IDM_KING_WHITE :: 121 +IDM_PAWN_WHITE :: 122 +IDM_ROOK_BLACK :: 123 +IDM_KNIGHT_BLACK :: 124 +IDM_BISHOP_BLACK :: 125 +IDM_QUEEN_BLACK :: 126 +IDM_KING_BLACK :: 127 +IDM_PAWN_BLACK :: 128 +IDM_SET_EN_PASSANT_TARGET_SQUARE :: 129 +IDM_TOGGLE_CASTLING_RIGHT :: 130 +IDM_RESET_TO_STARTING_POSITION :: 131 +IDM_EXIT :: 132 +IDM_ABOUT :: 133 +IDC_BUTTON_CLEAR_EN_PASSANT :: 134 +IDM_OPEN :: 135 +IDM_SAVE_AS :: 136 +IDM_SAVE :: 137 +IDM_NEW :: 138 +IDM_PLAY_SOUND :: 139 +IDM_VISUALS_NORMAL :: 140 +IDM_VISUALS_GINGER :: 141 + +// String resource IDs +// Convention: The numbers correspond to numbered argument inserts for aprintf `{0}`, `{1}` etc. + +IDS_FEN_ERR_RANK_SEPARATOR_AT_INVALID_POSITION_0 :: 400 +IDS_FEN_ERR_RANK_OVERFLOW_ADVANCE_0_1 :: 401 +IDS_FEN_ERR_BOARD_SEPARATOR_AT_INVALID_POSITION_0 :: 402 +IDS_FEN_ERR_INVALID_CHARACTER_0_AT_POSITION_1 :: 403 +IDS_FEN_ERR_RANK_OVERFLOW_CANNOT_PLACE_0_1_AT_2 :: 404 +IDS_FEN_ERR_EXPECTED_PLAYER_TO_MOVE_0 :: 405 +IDS_FEN_ERR_EXPECTED_SPACE_0 :: 406 +IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0 :: 407 +IDS_FEN_ERR_CASTLING_RIGHT_SPECIFIER_CONFLICT :: 408 +IDS_FEN_ERR_EXPECTED_CASTLING_RIGHTS_SPECIFIER_0 :: 409 +IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_FILE_0 :: 410 +IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_RANK_0 :: 411 +IDS_FEN_ERR_INVALID_HALFMOVE_CLOCK_0 :: 412 +IDS_FEN_ERR_INVALID_FULLMOVE_NUMBER_0 :: 413 +IDS_FEN_ERR_EXPECTED_END_OF_STRING :: 414 +IDS_INVALID_EN_PASSANT_TARGET_SQUARE :: 415 +IDS_INVALID_EN_PASSANT_TARGET_SQUARE_HINT :: 416 +IDS_INVALID_FEN_STRING :: 417 +IDS_ROOK :: 418 +IDS_KNIGHT :: 419 +IDS_BISHOP :: 420 +IDS_QUEEN :: 421 +IDS_KING :: 422 +IDS_PAWN :: 423 +IDS_WHITE :: 424 +IDS_BLACK :: 425 +IDS_TITLE :: 426 +IDS_CONFIRM_RESET_CAPTION :: 427 +IDS_CONFIRM_RESET_MESSAGE :: 428 +IDS_ERROR_CAPTION :: 429 +IDS_FEN_FILES_FILTER :: 430 +IDS_ALL_FILES_FILTER :: 431 +IDS_UNSAVED_CHANGES_CAPTION :: 432 +IDS_UNSAVED_CHANGES_MESSAGE :: 433 +IDS_ALLOW_CASTLING_WHITE_KINGSIDE :: 434 +IDS_DISALLOW_CASTLING_WHITE_KINGSIDE :: 435 +IDS_ALLOW_CASTLING_WHITE_QUEENSIDE :: 436 +IDS_DISALLOW_CASTLING_WHITE_QUEENSIDE :: 437 +IDS_ALLOW_CASTLING_BLACK_KINGSIDE :: 438 +IDS_DISALLOW_CASTLING_BLACK_KINGSIDE :: 439 +IDS_ALLOW_CASTLING_BLACK_QUEENSIDE :: 440 +IDS_DISALLOW_CASTLING_BLACK_QUEENSIDE :: 441 diff --git a/win32/fentool/resource.rc b/win32/fentool/resource.rc new file mode 100644 index 0000000..98cb311 --- /dev/null +++ b/win32/fentool/resource.rc @@ -0,0 +1,416 @@ +#pragma code_page(65001) + +#include +#include +#include "resource.h" + +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "app.manifest" + +IDI_MAIN ICON "icon.ico" +IDB_ABOUT BITMAP "about.bmp" +IDR_PIECES_NORMAL RCDATA "piecesnormal.png" +IDR_PIECES_GINGER RCDATA "billchess.png" + +IDW_CLACK WAVE "clack.wav" + +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +IDM_MAIN_MENU MENU +BEGIN + POPUP "&File" + BEGIN + MENUITEM "&Reset to starting position", IDM_RESET_TO_STARTING_POSITION + MENUITEM SEPARATOR + MENUITEM "&New\tCtrl+N", IDM_NEW + MENUITEM "&Open...\tCtrl+O", IDM_OPEN + MENUITEM "&Save\tCtrl+S", IDM_SAVE + MENUITEM "Save &As...\tCtrl+Shift+S", IDM_SAVE_AS + MENUITEM SEPARATOR + MENUITEM "E&xit", IDM_EXIT + END + POPUP "&Options" + BEGIN + MENUITEM "Play &sound", IDM_PLAY_SOUND, CHECKED + MENUITEM SEPARATOR + MENUITEM "&Normal visuals", IDM_VISUALS_NORMAL + MENUITEM "&Ginger visuals", IDM_VISUALS_GINGER + END + POPUP "&Help" + BEGIN + MENUITEM "&About\tF12", IDM_ABOUT + END +END + +STRINGTABLE +BEGIN + IDM_NEW "Creates a new position." + IDM_OPEN "Opens a position from a file." + IDM_SAVE "Saves the current position to the current file." + IDM_SAVE_AS "Saves the current position to a new file." + IDM_EXIT "Closes the application." + IDM_RESET_TO_STARTING_POSITION "Discards the current position and reloads the starting position." + IDM_ABOUT "Shows information about this application." + IDM_PLAY_SOUND "Toggles whether to play a sound when a piece is placed." +END + +IDD_MAIN DIALOGEX 0, 0, 410, 320 +CAPTION "FEN tool" +STYLE WS_OVERLAPPEDWINDOW | DS_SHELLFONT | DS_CENTER +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +MENU IDM_MAIN_MENU +BEGIN + LTEXT "FEN:", IDC_STATIC, 11, 10, 248, 10 + EDITTEXT IDC_TEXTBOX_FEN, 10, 22, 390, 12, ES_AUTOHSCROLL + + RADIOBUTTON "White to move", IDC_RADIOBUTTON_WHITE_TO_MOVE, 10, 50, 120, 12 + RADIOBUTTON "Black to move", IDC_RADIOBUTTON_BLACK_TO_MOVE, 10, 62, 120, 12 + + CHECKBOX "White can castle kingside", IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE, 10, 80, 120, 12 + CHECKBOX "White can castle queenside", IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE, 10, 92, 120, 12 + CHECKBOX "Black can castle kingside", IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, 10, 104, 120, 12 + CHECKBOX "Black can castle queenside", IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE, 10, 116, 120, 12 + + LTEXT "En passant target square:", IDC_STATIC, 11, 134, 120, 12 + EDITTEXT IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, 10, 146, 90, 12 + PUSHBUTTON "❌", IDC_BUTTON_CLEAR_EN_PASSANT, 110, 146, 16, 12 + + LTEXT "Halfmove clock:", IDC_STATIC, 11, 160, 120, 12 + EDITTEXT IDC_TEXTBOX_HALFMOVE_CLOCK, 10, 172, 120, 12, ES_NUMBER + + LTEXT "Fullmove number:", IDC_STATIC, 11, 190, 120, 12 + EDITTEXT IDC_TEXTBOX_FULLMOVE_NUMBER, 10, 202, 120, 12, ES_NUMBER + + CONTROL "", IDC_BOARD_CONTROL, "board_control", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 140, 50, 260, 260 + + CONTROL "", IDC_STATUSBAR, STATUSCLASSNAME, WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP, 0, 115, 300, 15 +END + +IDM_BOARD_CONTEXT_MENU MENU +BEGIN + POPUP "" + BEGIN + MENUITEM "xx", IDM_SQUARE_INFO, INACTIVE + MENUITEM SEPARATOR + MENUITEM "&Clear square", IDM_CLEAR_SQUARE + MENUITEM SEPARATOR + POPUP "&Pawn" + BEGIN + MENUITEM "&White", IDM_PAWN_WHITE + MENUITEM "&Black", IDM_PAWN_BLACK + END + POPUP "&Bishop" + BEGIN + MENUITEM "&White", IDM_BISHOP_WHITE + MENUITEM "&Black", IDM_BISHOP_BLACK + END + POPUP "K&night" + BEGIN + MENUITEM "&White", IDM_KNIGHT_WHITE + MENUITEM "&Black", IDM_KNIGHT_BLACK + END + POPUP "&Rook" + BEGIN + MENUITEM "&White", IDM_ROOK_WHITE + MENUITEM "&Black", IDM_ROOK_BLACK + END + POPUP "&Queen" + BEGIN + MENUITEM "&White", IDM_QUEEN_WHITE + MENUITEM "&Black", IDM_QUEEN_BLACK + END + POPUP "&King" + BEGIN + MENUITEM "&White", IDM_KING_WHITE + MENUITEM "&Black", IDM_KING_BLACK + END + MENUITEM SEPARATOR + MENUITEM "Set &En Passant target square", IDM_SET_EN_PASSANT_TARGET_SQUARE + MENUITEM "Toggle castling right", IDM_TOGGLE_CASTLING_RIGHT + END +END + +STRINGTABLE +BEGIN + IDM_SQUARE_INFO "Displays the selected square." + IDM_CLEAR_SQUARE "Removes the piece from the selected square." + IDM_ROOK_WHITE "Sets a rook for white on the selected square." + IDM_KNIGHT_WHITE "Sets a knight for white on the selected square." + IDM_BISHOP_WHITE "Sets a bishop for white on the selected square." + IDM_QUEEN_WHITE "Sets a queen for white on the selected square." + IDM_KING_WHITE "Sets a king for white on the selected square." + IDM_PAWN_WHITE "Sets a pawn for white on the selected square." + IDM_ROOK_BLACK "Sets a rook for black on the selected square." + IDM_KNIGHT_BLACK "Sets a knight for black on the selected square." + IDM_BISHOP_BLACK "Sets a bishop for black on the selected square." + IDM_QUEEN_BLACK "Sets a queen for black on the selected square." + IDM_KING_BLACK "Sets a king for black on the selected square." + IDM_PAWN_BLACK "Sets a pawn for black on the selected square." + IDM_SET_EN_PASSANT_TARGET_SQUARE "Sets this square as the target for a possible en passant capture." + IDM_TOGGLE_CASTLING_RIGHT "Toggles the castling right associated with the rook on the selected square." +END + +IDD_ABOUT DIALOGEX 0, 0, 150, 70 +CAPTION "About" +STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_MODALFRAME | DS_SHELLFONT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK", IDOK, 80, 45, 60, 15 + RTEXT "FEN tool\r\nDemo project", IDC_STATIC, 80, 10, 60, 20 +END + +STRINGTABLE +BEGIN + IDS_FEN_ERR_RANK_SEPARATOR_AT_INVALID_POSITION_0 "Rank separator '/' at invalid position: {0}" + IDS_FEN_ERR_RANK_OVERFLOW_ADVANCE_0_1 "Rank overflow (cannot advance by {0} empty squares): {1}" + IDS_FEN_ERR_BOARD_SEPARATOR_AT_INVALID_POSITION_0 "Board separator ' ' at invalid position: {0}" + IDS_FEN_ERR_INVALID_CHARACTER_0_AT_POSITION_1 "Invalid character '{0}' at position: {1}" + IDS_FEN_ERR_RANK_OVERFLOW_CANNOT_PLACE_0_1_AT_2 "Rank overflow (cannot place {0} {1}) at {2}" + IDS_FEN_ERR_EXPECTED_PLAYER_TO_MOVE_0 "Expected player to move (w/b), got {0}" + IDS_FEN_ERR_EXPECTED_SPACE_0 "Expected space, got {0}" + IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0 "Duplicate castling right specifier: {0}" + IDS_FEN_ERR_CASTLING_RIGHT_SPECIFIER_CONFLICT "Castling right specifier conflict" + IDS_FEN_ERR_EXPECTED_CASTLING_RIGHTS_SPECIFIER_0 "Expected castling rights specifier, got {0}" + IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_FILE_0 "Invalid en passant target square file: {0}" + IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_RANK_0 "Invalid en passant target square rank: {0}" + IDS_FEN_ERR_INVALID_HALFMOVE_CLOCK_0 "Invalid halfmove clock: {0}" + IDS_FEN_ERR_INVALID_FULLMOVE_NUMBER_0 "Invalid fullmove number: {0}" + IDS_FEN_ERR_EXPECTED_END_OF_STRING "Expected end of string" + IDS_INVALID_EN_PASSANT_TARGET_SQUARE "Invalid en passant target square" + IDS_INVALID_EN_PASSANT_TARGET_SQUARE_HINT "Enter a square such as ""e3"", or ""-""." + IDS_INVALID_FEN_STRING "Invalid FEN string" + IDS_ROOK "Rook" + IDS_KNIGHT "Knight" + IDS_BISHOP "Bishop" + IDS_QUEEN "Queen" + IDS_KING "King" + IDS_PAWN "Pawn" + IDS_WHITE "White" + IDS_BLACK "Black" + IDS_TITLE "FEN tool" + IDS_CONFIRM_RESET_CAPTION "Confirm reset" + IDS_CONFIRM_RESET_MESSAGE "Really discard the current position and reset to starting position?" + IDS_ERROR_CAPTION "Error" + IDS_FEN_FILES_FILTER "FEN files (*.fen)" + IDS_ALL_FILES_FILTER "All files" + IDS_UNSAVED_CHANGES_CAPTION "Unsaved changes" + IDS_UNSAVED_CHANGES_MESSAGE "There are unsaved changes. Do you want to save?" + IDS_ALLOW_CASTLING_WHITE_KINGSIDE "Allow &castling kingside for white" + IDS_DISALLOW_CASTLING_WHITE_KINGSIDE "Disallow &castling kingside for white" + IDS_ALLOW_CASTLING_WHITE_QUEENSIDE "Allow &castling queenside for white" + IDS_DISALLOW_CASTLING_WHITE_QUEENSIDE "Disallow &castling queenside for white" + IDS_ALLOW_CASTLING_BLACK_KINGSIDE "Allow &castling kingside for black" + IDS_DISALLOW_CASTLING_BLACK_KINGSIDE "Disallow &castling kingside for black" + IDS_ALLOW_CASTLING_BLACK_QUEENSIDE "Allow &castling queenside for black" + IDS_DISALLOW_CASTLING_BLACK_QUEENSIDE "Disallow &castling queenside for black" +END + +IDC_ACCELERATOR_TABLE ACCELERATORS +BEGIN + "N", IDM_NEW, VIRTKEY, CONTROL + "O", IDM_OPEN, VIRTKEY, CONTROL + "S", IDM_SAVE, VIRTKEY, CONTROL + "S", IDM_SAVE_AS, VIRTKEY, CONTROL, SHIFT + VK_F12, IDM_ABOUT, VIRTKEY +END + +LANGUAGE LANG_DUTCH, SUBLANG_DUTCH + +IDM_MAIN_MENU MENU +BEGIN + POPUP "&Bestand" + BEGIN + MENUITEM "&Reset naar beginpositie", IDM_RESET_TO_STARTING_POSITION + MENUITEM SEPARATOR + MENUITEM "&Nieuw\tCtrl+N", IDM_NEW + MENUITEM "&Openen...\tCtrl+O", IDM_OPEN + MENUITEM "&Opslaan\tCtrl+S", IDM_SAVE + MENUITEM "Opslaan &als...\tCtrl+Shift+S", IDM_SAVE_AS + MENUITEM SEPARATOR + MENUITEM "A&fsluiten", IDM_EXIT + END + POPUP "&Opties" + BEGIN + MENUITEM "Geluid a&fspelen", IDM_PLAY_SOUND, CHECKED + MENUITEM SEPARATOR + MENUITEM "&Normale vormgeving", IDM_VISUALS_NORMAL + MENUITEM "&Ginger-vormgeving", IDM_VISUALS_GINGER + END + POPUP "&Help" + BEGIN + MENUITEM "&Over\tF12", IDM_ABOUT + END +END + +STRINGTABLE +BEGIN + IDM_NEW "Maakt een nieuwe positie aan." + IDM_OPEN "Opent een positie uit een bestand." + IDM_SAVE "Slaat de huidige positie op in het huidige bestand." + IDM_SAVE_AS "Slaat de huidige positie op in een nieuw bestand." + IDM_EXIT "Sluit de toepassing." + IDM_RESET_TO_STARTING_POSITION "Verwerpt de huidige positie en laadt de beginpositie opnieuw." + IDM_ABOUT "Toont informatie over deze toepassing." + IDM_PLAY_SOUND "Schakelt het geluid in of uit wanneer een stuk wordt geplaatst." + IDS_FEN_FILES_FILTER "FEN-bestanden (*.fen)" + IDS_ALL_FILES_FILTER "Alle bestanden" + IDS_UNSAVED_CHANGES_CAPTION "Niet-opgeslagen wijzigingen" + IDS_UNSAVED_CHANGES_MESSAGE "Er zijn niet-opgeslagen wijzigingen. Wilt u deze opslaan?" + IDS_ALLOW_CASTLING_WHITE_KINGSIDE "Korte &rokade voor wit toestaan" + IDS_DISALLOW_CASTLING_WHITE_KINGSIDE "Korte &rokade voor wit verbieden" + IDS_ALLOW_CASTLING_WHITE_QUEENSIDE "Lange &rokade voor wit toestaan" + IDS_DISALLOW_CASTLING_WHITE_QUEENSIDE "Lange &rokade voor wit verbieden" + IDS_ALLOW_CASTLING_BLACK_KINGSIDE "Korte &rokade voor zwart toestaan" + IDS_DISALLOW_CASTLING_BLACK_KINGSIDE "Korte &rokade voor zwart verbieden" + IDS_ALLOW_CASTLING_BLACK_QUEENSIDE "Lange &rokade voor zwart toestaan" + IDS_DISALLOW_CASTLING_BLACK_QUEENSIDE "Lange &rokade voor zwart verbieden" +END + +IDD_MAIN DIALOGEX 0, 0, 410, 320 +CAPTION "FEN tool" +STYLE WS_OVERLAPPEDWINDOW | DS_SHELLFONT | DS_CENTER +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +MENU IDM_MAIN_MENU +BEGIN + LTEXT "FEN:", IDC_STATIC, 11, 10, 248, 10 + EDITTEXT IDC_TEXTBOX_FEN, 10, 22, 390, 12, ES_AUTOHSCROLL + + RADIOBUTTON "Wit aan zet", IDC_RADIOBUTTON_WHITE_TO_MOVE, 10, 50, 120, 12 + RADIOBUTTON "Zwart aan zet", IDC_RADIOBUTTON_BLACK_TO_MOVE, 10, 62, 120, 12 + + CHECKBOX "Wit mag kort rokeren", IDC_CHECKBOX_WHITE_CASTLE_KINGSIDE, 10, 80, 120, 12 + CHECKBOX "Wit mag lang rokeren", IDC_CHECKBOX_WHITE_CASTLE_QUEENSIDE, 10, 92, 120, 12 + CHECKBOX "Zwart mag kort rokeren", IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, 10, 104, 120, 12 + CHECKBOX "Zwart mag lang rokeren", IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE, 10, 116, 120, 12 + + LTEXT "En-passantdoelveld:", IDC_STATIC, 11, 134, 120, 12 + EDITTEXT IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, 10, 146, 90, 12 + PUSHBUTTON "❌", IDC_BUTTON_CLEAR_EN_PASSANT, 110, 146, 16, 12 + + LTEXT "Halfzettenklok:", IDC_STATIC, 11, 160, 120, 12 + EDITTEXT IDC_TEXTBOX_HALFMOVE_CLOCK, 10, 172, 120, 12, ES_NUMBER + + LTEXT "Volledige zet:", IDC_STATIC, 11, 190, 120, 12 + EDITTEXT IDC_TEXTBOX_FULLMOVE_NUMBER, 10, 202, 120, 12, ES_NUMBER + + CONTROL "", IDC_BOARD_CONTROL, "board_control", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 140, 50, 260, 260 + + CONTROL "", IDC_STATUSBAR, STATUSCLASSNAME, WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP, 0, 115, 300, 15 +END + +IDM_BOARD_CONTEXT_MENU MENU +BEGIN + POPUP "" + BEGIN + MENUITEM "xx", IDM_SQUARE_INFO, INACTIVE + MENUITEM SEPARATOR + MENUITEM "Vak &leegmaken", IDM_CLEAR_SQUARE + MENUITEM SEPARATOR + POPUP "&Pion" + BEGIN + MENUITEM "&Wit", IDM_PAWN_WHITE + MENUITEM "&Zwart", IDM_PAWN_BLACK + END + POPUP "&Loper" + BEGIN + MENUITEM "&Wit", IDM_BISHOP_WHITE + MENUITEM "&Zwart", IDM_BISHOP_BLACK + END + POPUP "&Paard" + BEGIN + MENUITEM "&Wit", IDM_KNIGHT_WHITE + MENUITEM "&Zwart", IDM_KNIGHT_BLACK + END + POPUP "&Toren" + BEGIN + MENUITEM "&Wit", IDM_ROOK_WHITE + MENUITEM "&Zwart", IDM_ROOK_BLACK + END + POPUP "&Dame" + BEGIN + MENUITEM "&Wit", IDM_QUEEN_WHITE + MENUITEM "&Zwart", IDM_QUEEN_BLACK + END + POPUP "K&oning" + BEGIN + MENUITEM "&Wit", IDM_KING_WHITE + MENUITEM "&Zwart", IDM_KING_BLACK + END + MENUITEM SEPARATOR + MENUITEM "En-passantdoelveld in&stellen", IDM_SET_EN_PASSANT_TARGET_SQUARE + MENUITEM "Rokaderecht omschakelen", IDM_TOGGLE_CASTLING_RIGHT + END +END + +STRINGTABLE +BEGIN + IDM_SQUARE_INFO "Toont het geselecteerde veld." + IDM_CLEAR_SQUARE "Verwijdert het stuk van het geselecteerde veld." + IDM_ROOK_WHITE "Plaatst een witte toren op het geselecteerde veld." + IDM_KNIGHT_WHITE "Plaatst een wit paard op het geselecteerde veld." + IDM_BISHOP_WHITE "Plaatst een witte loper op het geselecteerde veld." + IDM_QUEEN_WHITE "Plaatst een witte dame op het geselecteerde veld." + IDM_KING_WHITE "Plaatst een witte koning op het geselecteerde veld." + IDM_PAWN_WHITE "Plaatst een witte pion op het geselecteerde veld." + IDM_ROOK_BLACK "Plaatst een zwarte toren op het geselecteerde veld." + IDM_KNIGHT_BLACK "Plaatst een zwart paard op het geselecteerde veld." + IDM_BISHOP_BLACK "Plaatst een zwarte loper op het geselecteerde veld." + IDM_QUEEN_BLACK "Plaatst een zwarte dame op het geselecteerde veld." + IDM_KING_BLACK "Plaatst een zwarte koning op het geselecteerde veld." + IDM_PAWN_BLACK "Plaatst een zwarte pion op het geselecteerde veld." + IDM_SET_EN_PASSANT_TARGET_SQUARE "Stelt dit veld in als doel voor een mogelijke en-passantslag." + IDM_TOGGLE_CASTLING_RIGHT "Schakelt het rokaderecht van de toren op dit veld om." +END + +IDD_ABOUT DIALOGEX 0, 0, 150, 70 +CAPTION "Over" +STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_MODALFRAME | DS_SHELLFONT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK", IDOK, 80, 45, 60, 15 + RTEXT "FEN tool\r\nDemoproject", IDC_STATIC, 80, 10, 60, 20 +END + +STRINGTABLE +BEGIN + IDS_FEN_ERR_RANK_SEPARATOR_AT_INVALID_POSITION_0 "Rangscheidingsteken '/' op ongeldige positie: {0}" + IDS_FEN_ERR_RANK_OVERFLOW_ADVANCE_0_1 "Rij loopt over (kan niet {0} lege velden opschuiven): {1}" + IDS_FEN_ERR_BOARD_SEPARATOR_AT_INVALID_POSITION_0 "Bordscheidingsteken ' ' op ongeldige positie: {0}" + IDS_FEN_ERR_INVALID_CHARACTER_0_AT_POSITION_1 "Ongeldig teken '{0}' op positie: {1}" + IDS_FEN_ERR_RANK_OVERFLOW_CANNOT_PLACE_0_1_AT_2 "Rij loopt over (kan {0} {1} niet plaatsen) op {2}" + IDS_FEN_ERR_EXPECTED_PLAYER_TO_MOVE_0 "Speler aan zet verwacht (w/b), kreeg {0}" + IDS_FEN_ERR_EXPECTED_SPACE_0 "Spatie verwacht, kreeg {0}" + IDS_FEN_ERR_DUPLICATE_CASTLING_RIGHT_SPECIFIER_0 "Dubbele aanduiding voor rokaderecht: {0}" + IDS_FEN_ERR_CASTLING_RIGHT_SPECIFIER_CONFLICT "Tegenstrijdige aanduidingen voor rokaderecht" + IDS_FEN_ERR_EXPECTED_CASTLING_RIGHTS_SPECIFIER_0 "Aanduiding voor rokaderechten verwacht, kreeg {0}" + IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_FILE_0 "Ongeldige kolom van het en-passantdoelveld: {0}" + IDS_FEN_ERR_INVALID_EN_PASSANT_TARGET_SQUARE_RANK_0 "Ongeldige rij van het en-passantdoelveld: {0}" + IDS_FEN_ERR_INVALID_HALFMOVE_CLOCK_0 "Ongeldige halfzettenklok: {0}" + IDS_FEN_ERR_INVALID_FULLMOVE_NUMBER_0 "Ongeldig volledig zetnummer: {0}" + IDS_FEN_ERR_EXPECTED_END_OF_STRING "Einde van de tekenreeks verwacht" + IDS_INVALID_EN_PASSANT_TARGET_SQUARE "Ongeldig en-passantdoelveld" + IDS_INVALID_EN_PASSANT_TARGET_SQUARE_HINT "Voer een veld in, zoals ""e3"", of ""-""." + IDS_INVALID_FEN_STRING "Ongeldige FEN-tekenreeks" + IDS_ROOK "Toren" + IDS_KNIGHT "Paard" + IDS_BISHOP "Loper" + IDS_QUEEN "Dame" + IDS_KING "Koning" + IDS_PAWN "Pion" + IDS_WHITE "Wit" + IDS_BLACK "Zwart" + IDS_TITLE "FEN tool" + IDS_CONFIRM_RESET_CAPTION "Beginpositie herstellen?" + IDS_CONFIRM_RESET_MESSAGE "Wilt u de huidige positie echt verwerpen en de beginpositie herstellen?" + IDS_ERROR_CAPTION "Fout" +END + +IDC_ACCELERATOR_TABLE ACCELERATORS +BEGIN + "N", IDM_NEW, VIRTKEY, CONTROL + "O", IDM_OPEN, VIRTKEY, CONTROL + "S", IDM_SAVE, VIRTKEY, CONTROL + "S", IDM_SAVE_AS, VIRTKEY, CONTROL, SHIFT + VK_F12, IDM_ABOUT, VIRTKEY +END diff --git a/win32/fentool/util.odin b/win32/fentool/util.odin new file mode 100644 index 0000000..d9a9f39 --- /dev/null +++ b/win32/fentool/util.odin @@ -0,0 +1,227 @@ +package fen_tool + +import runtime "base:runtime" +import log "core:log" +import png "core:image/png" +import win "core:sys/windows" + +system_error_message_default_language := win.MAKELANGID(win.LANG_NEUTRAL, win.SUBLANG_DEFAULT) + +get_system_error_message :: proc(error_code: win.DWORD, language_id: win.DWORD = system_error_message_default_language, allocator := context.allocator) -> (result: string, error: runtime.Allocator_Error) #optional_allocator_error { + // Let Windows allocate the buffer internally, because only it knows how big it needs to be. + // It is temporary anyway because it will be converted to UTF-8. + message_buffer: win.wstring + rc := win.FormatMessageW(win.FORMAT_MESSAGE_FROM_SYSTEM | win.FORMAT_MESSAGE_IGNORE_INSERTS | win.FORMAT_MESSAGE_ALLOCATE_BUFFER, + nil, error_code, language_id, cast(win.LPWSTR)&message_buffer, 0, nil) + if rc == 0 { + // FormatMessageW failed. The most likely cause is that the caller gave us an invalid error code. + // Could try to build a string containing the error code? + return "", nil + } + + defer win.LocalFree(cast(win.LPVOID)message_buffer) + + return win.wstring_to_utf8(message_buffer, N = -1, allocator = allocator) +} + +get_dialog_item_text :: proc(hdlg: win.HWND, id: win.c_int, capacity: int, allocator := context.temp_allocator) -> (result: string, error: runtime.Allocator_Error) #optional_allocator_error { + buf := make([]win.WCHAR, capacity, allocator) + if win.GetDlgItemTextW(hdlg, id, auto_cast raw_data(buf), auto_cast len(buf)) <= 0 { + return "", nil + } + return win.wstring_to_utf8(cast(win.wstring)raw_data(buf), N = -1, allocator = allocator) +} + +set_dialog_item_text_string :: proc(hdlg: win.HWND, id: win.c_int, text: string) -> bool { + wstr := win.utf8_to_wstring(text, context.temp_allocator) + return cast(bool)win.SetDlgItemTextW(hdlg, id, wstr) +} + +set_dialog_item_text_cstring16 :: proc(hdlg: win.HWND, id: win.c_int, text: cstring16) -> bool { + return cast(bool)win.SetDlgItemTextW(hdlg, id, text) +} + +set_dialog_item_text :: proc { set_dialog_item_text_string, set_dialog_item_text_cstring16 } + +set_menu_item_text :: proc(hMenu: win.HMENU, itemID: win.UINT, text: string) { + wstr := win.utf8_to_wstring_alloc(text, context.temp_allocator) + if wstr == nil { + return + } + mii := win.MENUITEMINFOW { + cbSize = size_of(win.MENUITEMINFOW), + fMask = win.MIIM_STRING, + dwTypeData = cast(win.LPWSTR)wstr, + } + win.SetMenuItemInfoW(hMenu, itemID, false, &mii) +} + +// This procedure is "needed" because painting a rectangle with gid32's `Rectangle` API function +// is inherently inaccurate if the pen thickness is not equal to 1. +// +// A positive thickness will paint an inset rectangle (rect defines its outer border), a negative +// thickness will paint an outset rectangle (rect defines its inner border). +stroke_rectangle_precise :: proc(hdc: win.HDC, rc: win.RECT, color: win.COLORREF, #any_int thickness: i32 = 1) -> bool { + if thickness == 0 { + return true + } + + thickness := thickness + rc := rc + if thickness < 0 { + // Outset rectangle. We do this by inflating `rc` by the thickness, then + // paint an inset rectangle. + thickness *= -1 + rc.left -= thickness + rc.top -= thickness + rc.right += thickness + rc.bottom += thickness + } + + brush := win.CreateSolidBrush(color) + if brush == nil { + return false + } + defer win.DeleteObject(auto_cast brush) + + part: win.RECT + + // Top + part.left = rc.left + part.top = rc.top + part.right = rc.right + part.bottom = min(rc.bottom, rc.top + thickness) + win.FillRect(hdc, &part, brush) + + // Left + part.right = min(rc.right, rc.left + thickness) + part.bottom = rc.bottom + win.FillRect(hdc, &part, brush) + + // Bottom + part.top = max(rc.top, rc.bottom - thickness) + part.right = rc.right + win.FillRect(hdc, &part, brush) + + // Right + part.left = max(rc.left, rc.right - thickness) + part.top = rc.top + win.FillRect(hdc, &part, brush) + + return true +} + +load_string_resource :: proc(hInstance: win.HINSTANCE, id: win.UINT, allocator := context.temp_allocator) -> (result: string, error: runtime.Allocator_Error) #optional_allocator_error { + buffer: win.LPWSTR + // This particular usage (cchBufferMax = 0) makes it so that we receive a read-only + // pointer to the string resource. We then have to copy it out. + string_length := win.LoadStringW(hInstance, id, cast(win.LPWSTR)&buffer, 0) + if string_length <= 0 || buffer == nil { + return "", nil + } + return win.utf16_to_utf8((cast([^]win.WCHAR)buffer)[:string_length], allocator = allocator) +} + +// Loads a PNG image stored as an `RCDATA` type resource, and converts it into a GDI compatible `HBITMAP`. +load_hbitmap_from_png_rcdata :: proc { + load_hbitmap_from_png_rcdata_int, + load_hbitmap_from_png_rcdata_wstr, + load_hbitmap_from_png_rcdata_hrsrc, +} + +load_hbitmap_from_png_rcdata_int :: proc(hModule: win.HMODULE, resource_id: int) -> win.HBITMAP { + rt_rcdata := win.RT_RCDATA + resource_info := win.FindResourceW(hModule, cast(win.LPCWSTR)cast(rawptr)cast(uintptr)resource_id, cast(win.LPCWSTR)rt_rcdata) + if resource_info == nil { + log.errorf("Could not find resource: %v", resource_id) + return nil + } + return load_hbitmap_from_png_rcdata(hModule, resource_info) +} + +load_hbitmap_from_png_rcdata_wstr :: proc(hModule: win.HMODULE, resource_id: win.wstring) -> win.HBITMAP { + rt_rcdata := win.RT_RCDATA + resource_info := win.FindResourceW(hModule, resource_id, cast(win.LPCWSTR)rt_rcdata) + if resource_info == nil { + log.errorf("Could not find resource: %v", resource_id) + return nil + } + return load_hbitmap_from_png_rcdata(hModule, resource_info) +} + +load_hbitmap_from_png_rcdata_hrsrc :: proc(hModule: win.HMODULE, resource_info: win.HRSRC) -> win.HBITMAP { + resource_size := win.SizeofResource(hModule, resource_info) + if resource_size == 0 { + error := win.GetLastError() + log.errorf("Could not load resource: %v", get_system_error_message(error)) + return nil + } + hresource := win.LoadResource(hModule, resource_info) + if hresource == nil { + error := win.GetLastError() + log.errorf("Could not load resource: %v", get_system_error_message(error)) + return nil + } + defer win.FreeResource(hresource) + data := (cast([^]byte)win.LockResource(hresource))[:resource_size] + img, err := png.load_from_bytes(data, allocator = context.temp_allocator) + if err != nil { + log.errorf("png.load_from_bytes failed: %v", err) + return nil + } + // Declare a specialized `BITMAPINFO` variant with no color table entries. + BITMAPINFO :: struct { + hdr: win.BITMAPINFOHEADER, + } + bmi := BITMAPINFO { + hdr = { + biSize = size_of(win.BITMAPV5HEADER), + biWidth = cast(win.LONG)img.width, + biHeight = cast(win.LONG)-img.height, + biPlanes = 1, + biBitCount = 32, + biCompression = win.BI_RGB, + }, + } + pixels: [^]win.RGBQUAD + dib := win.CreateDIBSection(nil, cast(^win.BITMAPINFO)&bmi, win.DIB_RGB_COLORS, cast(^rawptr)&pixels, nil, 0) + if dib == nil { + error := win.GetLastError() + log.errorf("CreateDIBSection failed: %v", get_system_error_message(error)) + return nil + } + // We need to convert the pixel format that `png` gives us from RGBA to BGRA. + num_pixels := img.width * img.height // NOTE: This only works because `stride == width` here. + src := cast([^][4]win.BYTE)raw_data(img.pixels.buf) + for i in 0 ..< num_pixels { + pixels[i] = win.RGBQUAD { + rgbBlue = src[i].b, + rgbGreen = src[i].g, + rgbRed = src[i].r, + rgbReserved = src[i].a, + } + } + return dib +} + +fudge_window_starting_position :: proc(hwnd: win.HWND) { + // Create an invisible dummy window to make the operating system generate a valid starting position + rc: win.RECT + if !win.GetWindowRect(hwnd, &rc) { + return + } + w := rc.right - rc.left + h := rc.bottom - rc.top + dummy := win.CreateWindowW("STATIC", "", win.WS_OVERLAPPEDWINDOW, win.CW_USEDEFAULT, 0, w, h, nil, nil, nil, nil) + if dummy == nil { + return + } + defer win.DestroyWindow(dummy) + dummy_rc: win.RECT + if !win.GetWindowRect(dummy, &dummy_rc) { + return + } + x := dummy_rc.left + y := dummy_rc.top + win.SetWindowPos(hwnd, nil, x, y, 0, 0, win.SWP_NOZORDER | win.SWP_NOSIZE) +} From 7979a35386081a011888c418e37b85a37eedf6f9 Mon Sep 17 00:00:00 2001 From: bplu4t2f Date: Wed, 30 Sep 2026 12:11:13 +0200 Subject: [PATCH 2/3] Cleanup, code style, naming style --- win32/fentool/README.md | 15 +++ win32/fentool/about.odin | 20 +++- win32/fentool/board.odin | 42 +++---- win32/fentool/board_control.odin | 196 +++++++++++++++++++------------ win32/fentool/fen.odin | 50 ++++---- win32/fentool/file_dialog.odin | 12 +- win32/fentool/main.odin | 185 ++++++++++++++++++----------- win32/fentool/resource.rc | 17 ++- 8 files changed, 333 insertions(+), 204 deletions(-) diff --git a/win32/fentool/README.md b/win32/fentool/README.md index 22726d5..e480b8b 100644 --- a/win32/fentool/README.md +++ b/win32/fentool/README.md @@ -54,6 +54,21 @@ be a somewhat self-contained control class. Notes ----- +**Resource IDs** + +The resource compiler (`rc.exe`) understands C preprocessor directives. Thus, in a +C program, you would include the same `resource.h` header file from the `.rc` file +as in the main C program. In Odin, the resource ID definitions must be synchronized +explicitly (see `resource.odin`). This is a matter of simple text transformation, +so it can be automated (not done in this project for simplicity). + +Not all resources need to have numeric IDs. For example, `DIALOG[EX]` resource IDs +can be strings. The associated resource loading functions (e.g. `CreateDialog...`) +accept either a pointer to a null-terminated string, or a 16-bit integer. + +This can occasionally be problematic in Odin currently, because the compiler will +not let you cast an arbitrary constant integer value to a `cstring16`. + **DPI awareness** DPI awareness is a large topic. This example relies on Windows already doing most diff --git a/win32/fentool/about.odin b/win32/fentool/about.odin index c00dd8c..4219138 100644 --- a/win32/fentool/about.odin +++ b/win32/fentool/about.odin @@ -9,8 +9,11 @@ about_show_dialog :: proc(owner: win.HWND) { } about_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.INT_PTR { + switch msg { + case win.WM_INITDIALOG: + // Instruct the system to use default keyboard focus handling. return 1 case win.WM_COMMAND: @@ -21,9 +24,11 @@ about_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, switch id { case win.IDOK: + // Generated by the default dialog proc on Enter. win.EndDialog(hdlg, 0) return 1 case win.IDCANCEL: + // Generated by the default dialog proc when the window is closed ([x] or Esc). win.EndDialog(hdlg, 0) return 1 } @@ -36,21 +41,26 @@ about_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) hbitmap := win.LoadImageW(hInstance, "IDB_ABOUT", win.IMAGE_BITMAP, 0, 0, win.LR_CREATEDIBSECTION) defer win.DeleteObject(auto_cast hbitmap) + + // Retrieve w/h information about the bitmap. bm: win.BITMAP win.GetObjectW(hbitmap, size_of(bm), &bm) - + bitmap_w := bm.bmWidth + bitmap_h := bm.bmHeight + hdc_src := win.CreateCompatibleDC(hdc) defer win.DeleteDC(hdc_src) previous_bitmap := cast(win.HBITMAP)win.SelectObject(hdc_src, auto_cast hbitmap) defer win.SelectObject(hdc_src, auto_cast previous_bitmap) - + + // Arbitrarily declare the top-left corner pixel as the transparency key. transparency_key := win.GetPixel(hdc_src, 0, 0) win.TransparentBlt( - hdc, 0, 0, bm.bmWidth, bm.bmHeight, - hdc_src, 0, 0, bm.bmWidth, bm.bmHeight, + hdc, 0, 0, bitmap_w, bitmap_h, + hdc_src, 0, 0, bitmap_w, bitmap_h, transparency_key, - ) + ) } return 0 diff --git a/win32/fentool/board.odin b/win32/fentool/board.odin index 13b0003..ba3eca8 100644 --- a/win32/fentool/board.odin +++ b/win32/fentool/board.odin @@ -3,31 +3,33 @@ package fen_tool import fmt "core:fmt" import strings "core:strings" -board :: struct { - pieces: board_pieces, // Rank-major, file-minor. - turn_player: board_piece_color, +Board :: struct { + pieces: Board_Pieces, + turn_player: Board_Piece_Color, white_can_castle_kingside: bool, white_can_castle_queenside: bool, black_can_castle_kingside: bool, black_can_castle_queenside: bool, - en_passant_target_square: square_specifier, + en_passant_target_square: Square_Specifier, halfmove_clock: i32, fullmove_number: i32, } -square_specifier :: struct { +Square_Specifier :: struct { file: i8, rank: i8, } -board_piece :: struct { - type: board_piece_type, - color: board_piece_color, +Board_Piece :: struct { + type: Board_Piece_Type, + color: Board_Piece_Color, } -board_pieces :: distinct [64]board_piece +// Rank-major, file-minor. +// Starting position has white rook at index 0, white knight at index 1, and so on. +Board_Pieces :: distinct [64]Board_Piece -board_piece_type :: enum i8 { +Board_Piece_Type :: enum i8 { none, rook, FIRST = rook, @@ -39,14 +41,14 @@ board_piece_type :: enum i8 { LAST = pawn, } -board_piece_color :: enum i8 { +Board_Piece_Color :: enum i8 { white, FIRST = white, black, LAST = black, } -board_format_full :: proc(board: board, allocator := context.temp_allocator) -> string { +board_format_full :: proc(board: Board, allocator := context.temp_allocator) -> string { sb := strings.builder_make_len_cap(0, 1000, allocator) fmt.sbprintfln(&sb, "%v", board_format_pieces(board, allocator)) fmt.sbprintfln(&sb, "White can castle kingside: %v", board_format_yesno(board.white_can_castle_kingside)) @@ -62,15 +64,15 @@ board_format_full :: proc(board: board, allocator := context.temp_allocator) -> board_format_position :: proc { board_format_position_file_rank, board_format_position_square } board_format_position_file_rank :: proc(#any_int file: i32, #any_int rank: i32, allocator := context.temp_allocator) -> string { - file_string := cast(rune)('A' + file) - rank_string := cast(rune)('1' + rank) + file_rune := cast(rune)('A' + file) + rank_rune := cast(rune)('1' + rank) sb := strings.builder_make_len_cap(0, 4, context.temp_allocator) - strings.write_rune(&sb, file_string) - strings.write_rune(&sb, rank_string) + strings.write_rune(&sb, file_rune) + strings.write_rune(&sb, rank_rune) return strings.to_string(sb) } -board_format_position_square :: proc(square: square_specifier, allocator := context.temp_allocator) -> string { +board_format_position_square :: proc(square: Square_Specifier, allocator := context.temp_allocator) -> string { return board_format_position_file_rank(square.file, square.rank, allocator) } @@ -78,14 +80,14 @@ board_format_yesno :: proc(b: bool) -> string { return b ? "yes" : "no" } -board_format_en_passant_target_square :: proc(ts: square_specifier, allocator := context.temp_allocator) -> string { +board_format_en_passant_target_square :: proc(ts: Square_Specifier, allocator := context.temp_allocator) -> string { if ts == {} { return "-" } return board_format_position(ts, allocator) } -board_format_pieces :: proc(board: board, allocator := context.temp_allocator) -> string { +board_format_pieces :: proc(board: Board, allocator := context.temp_allocator) -> string { // Construct the string like this: // 8 | rnbqkbnr // 7 | pppppppp @@ -114,7 +116,7 @@ board_format_pieces :: proc(board: board, allocator := context.temp_allocator) - return strings.to_string(sb) } -get_piece_rune :: proc(piece: board_piece) -> rune { +get_piece_rune :: proc(piece: Board_Piece) -> rune { switch { case piece.type == .none: return '.' case piece == { .rook, .white }: return 'R' diff --git a/win32/fentool/board_control.odin b/win32/fentool/board_control.odin index 6850c38..f50d388 100644 --- a/win32/fentool/board_control.odin +++ b/win32/fentool/board_control.odin @@ -7,6 +7,8 @@ import win "core:sys/windows" BOARD_CONTROL_CLASS_NAME :: "board_control" +// Called on application startup to register the board control window class +// for use in the main dialog template. board_control_register_class :: proc(instance: win.HINSTANCE) { wndclass := win.WNDCLASSEXW { cbSize = size_of(win.WNDCLASSEXW), @@ -21,7 +23,8 @@ board_control_register_class :: proc(instance: win.HINSTANCE) { win.RegisterClassExW(&wndclass) } -board_control_set_board :: proc(hwnd: win.HWND, board: board) { +// Called by the application whenever the board state changes. +board_control_set_board :: proc(hwnd: win.HWND, board: Board) { ctx := board_control_get_ctx(hwnd) if ctx.board != board { ctx.board = board @@ -32,7 +35,8 @@ board_control_set_board :: proc(hwnd: win.HWND, board: board) { } } -board_control_set_spritemap :: proc(hwnd: win.HWND, spritemap: board_control_spritemap) { +// Called by the application when the user selects a different spritemap. +board_control_set_spritemap :: proc(hwnd: win.HWND, spritemap: Board_Control_Spritemap) { ctx := board_control_get_ctx(hwnd) if ctx.spritemap != spritemap { ctx.spritemap = spritemap @@ -43,46 +47,47 @@ board_control_set_spritemap :: proc(hwnd: win.HWND, spritemap: board_control_spr } } -board_control_spritemap :: enum { +Board_Control_Spritemap :: enum { normal, ginger, } -board_control_get_spritemap_resource_name :: proc(spritemap: board_control_spritemap) -> string { +board_control_get_spritemap_resource_name :: proc(spritemap: Board_Control_Spritemap) -> string { switch spritemap { - case .normal: return "IDR_PIECES_NORMAL" - case .ginger: return "IDR_PIECES_GINGER" - case: return "IDR_PIECES_NORMAL" + case .normal: return "IDR_PIECES_NORMAL" + case .ginger: return "IDR_PIECES_GINGER" + case: return "IDR_PIECES_NORMAL" } } @(private="file") -board_control_ctx :: struct { +Board_Control_Ctx :: struct { runtime_context: ^runtime.Context, font: win.HFONT, - board: board, + // Copy of the board state visualized by the board control. + board: Board, - spritemap: board_control_spritemap, + spritemap: Board_Control_Spritemap, spritemap_cache: win.HBITMAP, // Last known mouse client coordinates; needed for piece drag and drop. mouse_client: [2]i32, - hover_mode: board_control_hover_mode, - hovered_square: square_specifier, + hover_mode: Board_Control_Hover_Mode, + hovered_square: Square_Specifier, menu_open: bool, - menu_square: square_specifier, + menu_square: Square_Specifier, - pending_castling_right: castling_right, - pending_castling_right_state: castling_right_state, + pending_castling_right: Castling_Right, + pending_castling_right_state: Castling_Right_State, dragging: bool, - drag_from: square_specifier, + drag_from: Square_Specifier, } @(private="file") -board_control_hover_mode :: enum { +Board_Control_Hover_Mode :: enum { none, // User is currently using the mouse to highlight squares. mouse, @@ -90,24 +95,28 @@ board_control_hover_mode :: enum { keyboard, } -castling_right :: enum { +Castling_Right :: enum { white_kingside, white_queenside, black_kingside, black_queenside, } -castling_right_state :: enum { +Castling_Right_State :: enum { allow, disallow, } -@(private="file") -board_control_get_ctx :: proc "contextless" (hwnd: win.HWND) -> ^board_control_ctx { - return cast(^board_control_ctx)cast(uintptr)win.GetWindowLongPtrW(hwnd, 0) -} +// +// Notifications +// +// When the user interacts with the board control (e.g. place a piece), this control +// will notify the parent. The authority to execute the requested action lies with +// the parent. +// -board_control_notification_code :: enum win.UINT { +// Notification code for `NMHDR.code` +Board_Control_Notification_Code :: enum win.UINT { none, hover, place_piece, @@ -117,21 +126,26 @@ board_control_notification_code :: enum win.UINT { } NM_BOARD_CONTROL_NOTIFICATION :: struct { - hdr: win.NMHDR, - hovering: bool, - square: square_specifier, - piece: board_piece, - castling_right: castling_right, - castling_right_state: castling_right_state, - square2: square_specifier, + // Adhere to standard Win32 notification structure: + // This struct must start with an `NMHDR`. + hdr: win.NMHDR, + hovering: bool, + square: Square_Specifier, + piece: Board_Piece, + castling_right: Castling_Right, + castling_right_state: Castling_Right_State, + square2: Square_Specifier, } -board_control_set_hovered_square :: proc(hwnd: win.HWND, ctx: ^board_control_ctx, mode: board_control_hover_mode, square: square_specifier) { +// Called by the board control when the hovered square has changed, +// either through mouse or keyboard interaction. +// +// Will update state and notify the parent if appropriate. +board_control_set_hovered_square :: proc(hwnd: win.HWND, ctx: ^Board_Control_Ctx, mode: Board_Control_Hover_Mode, square: Square_Specifier) { if ctx.hover_mode == mode && ctx.hovered_square == square { // No change. return } - //log.debugf("Hover state changed to %v - %v, %v", mode, square.rank, square.file) ctx.hover_mode = mode ctx.hovered_square = square is_hovering := mode != .none @@ -139,7 +153,7 @@ board_control_set_hovered_square :: proc(hwnd: win.HWND, ctx: ^board_control_ctx win.InvalidateRect(hwnd, nil, true) } -board_control_notify_cell_hover :: proc(hwnd: win.HWND, hovering: bool, square: square_specifier) { +board_control_notify_cell_hover :: proc(hwnd: win.HWND, hovering: bool, square: Square_Specifier) { parent := win.GetParent(hwnd) if parent == nil { return @@ -148,7 +162,7 @@ board_control_notify_cell_hover :: proc(hwnd: win.HWND, hovering: bool, square: hdr = { hwndFrom = hwnd, idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), - code = cast(win.UINT)board_control_notification_code.hover, + code = cast(win.UINT)Board_Control_Notification_Code.hover, }, hovering = hovering, square = square, @@ -156,7 +170,7 @@ board_control_notify_cell_hover :: proc(hwnd: win.HWND, hovering: bool, square: win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) } -board_control_notify_piece :: proc(hwnd: win.HWND, square: square_specifier, piece: board_piece) { +board_control_notify_piece :: proc(hwnd: win.HWND, square: Square_Specifier, piece: Board_Piece) { parent := win.GetParent(hwnd) if parent == nil { return @@ -165,7 +179,7 @@ board_control_notify_piece :: proc(hwnd: win.HWND, square: square_specifier, pie hdr = { hwndFrom = hwnd, idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), - code = cast(win.UINT)board_control_notification_code.place_piece, + code = cast(win.UINT)Board_Control_Notification_Code.place_piece, }, square = square, piece = piece, @@ -173,7 +187,7 @@ board_control_notify_piece :: proc(hwnd: win.HWND, square: square_specifier, pie win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) } -board_control_notify_exchange_pieces :: proc(hwnd: win.HWND, square1, square2: square_specifier) { +board_control_notify_exchange_pieces :: proc(hwnd: win.HWND, square1, square2: Square_Specifier) { parent := win.GetParent(hwnd) if parent == nil { return @@ -182,7 +196,7 @@ board_control_notify_exchange_pieces :: proc(hwnd: win.HWND, square1, square2: s hdr = { hwndFrom = hwnd, idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), - code = cast(win.UINT)board_control_notification_code.exchange_pieces, + code = cast(win.UINT)Board_Control_Notification_Code.exchange_pieces, }, square = square1, square2 = square2, @@ -190,7 +204,7 @@ board_control_notify_exchange_pieces :: proc(hwnd: win.HWND, square1, square2: s win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) } -board_control_notify_set_en_passant_target_square :: proc(hwnd: win.HWND, square: square_specifier) { +board_control_notify_set_en_passant_target_square :: proc(hwnd: win.HWND, square: Square_Specifier) { parent := win.GetParent(hwnd) if parent == nil { return @@ -199,14 +213,14 @@ board_control_notify_set_en_passant_target_square :: proc(hwnd: win.HWND, square hdr = { hwndFrom = hwnd, idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), - code = cast(win.UINT)board_control_notification_code.set_en_passant_target_square, + code = cast(win.UINT)Board_Control_Notification_Code.set_en_passant_target_square, }, square = square, } win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) } -board_control_notify_set_castling_right :: proc(hwnd: win.HWND, castling_right: castling_right, state: castling_right_state) { +board_control_notify_set_castling_right :: proc(hwnd: win.HWND, castling_right: Castling_Right, state: Castling_Right_State) { parent := win.GetParent(hwnd) if parent == nil { return @@ -215,7 +229,7 @@ board_control_notify_set_castling_right :: proc(hwnd: win.HWND, castling_right: hdr = { hwndFrom = hwnd, idFrom = cast(uintptr)win.GetDlgCtrlID(hwnd), - code = cast(win.UINT)board_control_notification_code.set_castling_right, + code = cast(win.UINT)Board_Control_Notification_Code.set_castling_right, }, castling_right = castling_right, castling_right_state = state, @@ -223,14 +237,20 @@ board_control_notify_set_castling_right :: proc(hwnd: win.HWND, castling_right: win.SendMessageW(parent, win.WM_NOTIFY, nm.hdr.idFrom, cast(win.LPARAM)cast(uintptr)&nm) } +@(private="file") +board_control_get_ctx :: proc "contextless" (hwnd: win.HWND) -> ^Board_Control_Ctx { + return cast(^Board_Control_Ctx)cast(uintptr)win.GetWindowLongPtrW(hwnd, 0) +} + @(private="file") board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.LRESULT { switch msg { case win.WM_CREATE: + // Setup window context. context = window_context_tls - ctx := new(board_control_ctx) + ctx := new(Board_Control_Ctx) ctx.runtime_context = &window_context_tls win.SetWindowLongPtrW(hwnd, 0, cast(win.LONG_PTR)cast(uintptr)ctx) @@ -242,10 +262,13 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w free(ctx) case win.WM_SETFONT: + // Standard Win32 infrastructure for controls that draw text. + // Called by the parent (either explicitly or through DS_SETFONT). ctx := board_control_get_ctx(hwnd) ctx.font = cast(win.HFONT)wparam case win.WM_GETFONT: + // Standard Win32 infrastructure for controls that draw text. ctx := board_control_get_ctx(hwnd) return cast(win.LRESULT)cast(uintptr)ctx.font @@ -256,6 +279,8 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w w := rc.right - rc.left h := rc.bottom - rc.top if w != h { + // Use the width here for both. + // NOTE: min(w,h) could cause the control to shrink unexpectedly on DPI changes. win.SetWindowPos(hwnd, nil, 0, 0, w, w, win.SWP_NOZORDER | win.SWP_NOMOVE) return 0 } @@ -265,9 +290,11 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w context = ctx.runtime_context^ x_client := win.GET_X_LPARAM(lparam) y_client := win.GET_Y_LPARAM(lparam) + // Update drag and drop target coordinates for painting the dragged piece. board_control_set_drag_target_coords(hwnd, ctx, x_client, y_client) square, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) board_control_set_hovered_square(hwnd, ctx, ok ? .mouse : .none, square) + // Setup mouse event tracking so that we get WM_MOUSELEAVE. tme := win.TRACKMOUSEEVENT { cbSize = size_of(win.TRACKMOUSEEVENT), hwndTrack = hwnd, @@ -311,6 +338,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w } if ctx.dragging { + // Complete piece dragging operation. x_client := win.GET_X_LPARAM(lparam) y_client := win.GET_Y_LPARAM(lparam) ctx.dragging = false @@ -332,12 +360,14 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w } case win.WM_CONTEXTMENU: + ctx := board_control_get_ctx(hwnd) context = ctx.runtime_context^ x_screen := win.GET_X_LPARAM(lparam) y_screen := win.GET_Y_LPARAM(lparam) x_client: i32 y_client: i32 + if x_screen == -1 && y_screen == -1 { // This was triggered by a non-mouse event (e.g. the "VK_APPS" key on the keyboard). // NOTE: This is ambiguous. (-1, -1) could technically be valid screen coordinates @@ -367,9 +397,11 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w x_client = pt.x y_client = pt.y } + log.debugf("lparam: 0x%8x - screen x, y: %v, %v - client x, y: %v %v", lparam, x_screen, y_screen, x_client, y_client) square, ok := board_control_get_square_from_client_coords(hwnd, x_client, y_client) if ok { + // Load and open the context menu. hInstance := cast(win.HINSTANCE)cast(uintptr)win.GetWindowLongPtrW(hwnd, win.GWLP_HINSTANCE) menu := win.LoadMenuW(hInstance, "IDM_BOARD_CONTEXT_MENU") if menu == nil { @@ -379,6 +411,8 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w break } defer win.DestroyMenu(menu) + // NOTE: A context menu must be a popup menu specifically. If defined in a resource as a menu template, + // the containing menu is just a dummy "main menu". popup := win.GetSubMenu(menu, 0) if popup == nil { error := win.GetLastError() @@ -424,16 +458,25 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w win.InvalidateRect(hwnd, nil, true) } + // Actually shows the menu, and returns when the menu is closed. + // The action chosen by the user (if any) is sent as a WM_COMMAND message. win.TrackPopupMenu(popup, 0, x_screen, y_screen, 0, hwnd, nil) } case win.WM_MENUSELECT: + // Conventional Win32 infrastructure: + // Notify the parent of the selected (i.e. hovered) menu item so that it can + // update the status bar text with a hint text, if applicable. parent := win.GetParent(hwnd) if parent != nil { return win.SendMessageW(parent, msg, wparam, lparam) } case win.WM_GETDLGCODE: + // Instructs `IsDialogMessageW` in the main message loop that this control + // wants to handle arrow keys. Otherwise, arrow keys would be swallowed by + // keyboard navigation logic in `IsDialogMessageW`. + // We need the arrow keys to navigate through the squares on the board. return win.DLGC_WANTARROWS case win.WM_SETFOCUS: @@ -466,7 +509,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w // NOTE: We could use independent state variables for the "keyboard-hovered" square. rank := clamp(ctx.hovered_square.rank + delta_rank, 0, 7) file := clamp(ctx.hovered_square.file + delta_file, 0, 7) - square := square_specifier { rank = rank, file = file } + square := Square_Specifier { rank = rank, file = file } board_control_set_hovered_square(hwnd, ctx, .keyboard, square) } @@ -500,16 +543,19 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w } case win.WM_ERASEBKGND: + // Suppress this: All painting is done in WM_PAINT through buffered painting to prevent flicker. return 1 case win.WM_PAINT: ctx := board_control_get_ctx(hwnd) context = ctx.runtime_context^ - // Setup WM_PAINT infrastructure + // Setup WM_PAINT infrastructure. ps: win.PAINTSTRUCT hdc0 := win.BeginPaint(hwnd, &ps) defer win.EndPaint(hwnd, &ps) + + // Setup buffered painting (to prevent flicker). hdc: win.HDC paintbuffer := win.BeginBufferedPaint(hdc0, &ps.rcPaint, .BPBF_TOPDOWNDIB, nil, &hdc) defer win.EndBufferedPaint(paintbuffer, true) @@ -525,8 +571,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w client: win.RECT win.GetClientRect(hwnd, &client) - x, y, w, h := board_control_get_board_bounds(hwnd, client) - //log.debugf("Board bounds: %v, %v, %v, %v", x, y, w, h) + bx, by, bw, bh := board_control_get_board_bounds(hwnd, client) dark_square_brush := win.CreateSolidBrush(win.RGB(175, 185, 170)) defer win.DeleteObject(auto_cast dark_square_brush) @@ -542,7 +587,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w highlight_thickness := adjust_for_dpi(hwnd, 3) - should_highlight_square :: proc(ctx: ^board_control_ctx, square: square_specifier) -> bool { + should_highlight_square :: proc(ctx: ^Board_Control_Ctx, square: Square_Specifier) -> bool { if ctx.menu_open { return square == ctx.menu_square } else if ctx.hover_mode != .none { @@ -559,12 +604,12 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w for rank in 0 ..< i32(8) { for file in 0 ..< i32(8) { square_rect := win.RECT { - left = auto_cast (x + (w * (file )) / 8), - top = auto_cast (y + (h * (7-rank)) / 8), - right = auto_cast (x + (w * (file+1)) / 8), - bottom = auto_cast (y + (h * (8-rank)) / 8), + left = auto_cast (bx + (bw * (file )) / 8), + top = auto_cast (by + (bh * (7-rank)) / 8), + right = auto_cast (bx + (bw * (file+1)) / 8), + bottom = auto_cast (by + (bh * (8-rank)) / 8), } - square := square_specifier { rank = cast(i8)rank, file = cast(i8)file } + square := Square_Specifier { rank = cast(i8)rank, file = cast(i8)file } brush: win.HBRUSH if should_highlight_square(ctx, square) { brush = highlight_square_brush @@ -593,7 +638,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w // Paint border (board frame) stroke_rectangle_precise(hdc, client, win.RGB(0, 0, 0)) - board_rc := win.RECT { x, y, x + w, y + h } + board_rc := win.RECT { bx, by, bx + bw, by + bh } stroke_rectangle_precise(hdc, board_rc, win.RGB(0, 0, 0), -1) // Paint file and rank names @@ -603,9 +648,9 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w for file in 0 ..< i32(8) { label_rect := win.RECT { - left = auto_cast (x + (w * (file )) / 8), + left = auto_cast (bx + (bw * (file )) / 8), top = board_rc.bottom, - right = auto_cast (x + (w * (file+1)) / 8), + right = auto_cast (bx + (bw * (file+1)) / 8), bottom = client.bottom, } file_string := cast(rune)('A' + file) @@ -617,9 +662,9 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w for rank in 0 ..< i32(8) { label_rect := win.RECT { left = client.left, - top = auto_cast (y + (h * (7-rank)) / 8), + top = auto_cast (by + (bh * (7-rank)) / 8), right = board_rc.left, - bottom = auto_cast (y + (h * (8-rank)) / 8), + bottom = auto_cast (by + (bh * (8-rank)) / 8), } rank_string := cast(rune)('1' + rank) sb := strings.builder_make_len_cap(0, 4, context.temp_allocator) @@ -641,7 +686,7 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w log.errorf("Error loading piece sprite bitmap: %v", msg) } - load_pieces_png :: proc(spritemap: board_control_spritemap) -> win.HBITMAP { + load_pieces_png :: proc(spritemap: Board_Control_Spritemap) -> win.HBITMAP { resource_name := board_control_get_spritemap_resource_name(spritemap) hModule := win.GetModuleHandleW(nil) return load_hbitmap_from_png_rcdata(hModule, win.utf8_to_wstring(resource_name, context.temp_allocator)) @@ -663,10 +708,10 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w piece := ctx.board.pieces[rank * 8 + file] if piece.type != .none { square_rect := win.RECT { - left = auto_cast (x + (w * (file )) / 8), - top = auto_cast (y + (h * (7-rank)) / 8), - right = auto_cast (x + (w * (file+1)) / 8), - bottom = auto_cast (y + (h * (8-rank)) / 8), + left = auto_cast (bx + (bw * (file )) / 8), + top = auto_cast (by + (bh * (7-rank)) / 8), + right = auto_cast (bx + (bw * (file+1)) / 8), + bottom = auto_cast (by + (bh * (8-rank)) / 8), } xd := square_rect.left yd := square_rect.top @@ -693,10 +738,10 @@ board_control_wndproc :: proc "system" (hwnd: win.HWND, msg: win.UINT, wparam: w from_file := cast(i32)ctx.drag_from.file piece := ctx.board.pieces[from_rank * 8 + from_file] square_rect := win.RECT { - left = auto_cast (x + (w * (from_file )) / 8), - top = auto_cast (y + (h * (7-from_rank)) / 8), - right = auto_cast (x + (w * (from_file+1)) / 8), - bottom = auto_cast (y + (h * (8-from_rank)) / 8), + left = auto_cast (bx + (bw * (from_file )) / 8), + top = auto_cast (by + (bh * (7-from_rank)) / 8), + right = auto_cast (bx + (bw * (from_file+1)) / 8), + bottom = auto_cast (by + (bh * (8-from_rank)) / 8), } wd := square_rect.right - square_rect.left hd := square_rect.bottom - square_rect.top @@ -728,7 +773,14 @@ adjust_for_dpi :: proc(hwnd: win.HWND, i: i32) -> i32 { // Returns the castling right toggle action that is appropriate for clicking on the specified square. // E.g. if the user clicks on A1, they can toggle the castling right for white queenside. // The `toggle_state` will be the opposite state (allow/disallow) of the current board state. -board_control_get_castling_right_toggle_action :: proc(ctx: ^board_control_ctx, square: square_specifier) -> (castling_right: castling_right, toggle_state: castling_right_state, enable: bool) { +board_control_get_castling_right_toggle_action :: proc( + ctx: ^Board_Control_Ctx, + square: Square_Specifier, +) -> ( + castling_right: Castling_Right, + toggle_state: Castling_Right_State, + enable: bool, +) { currently_allowed: bool switch { case square.rank == 0 && square.file == 0: @@ -769,7 +821,7 @@ board_control_get_board_bounds :: proc(hwnd: win.HWND, client: win.RECT) -> (x, return } -board_control_get_square_from_client_coords :: proc(hwnd: win.HWND, client_x, client_y: i32) -> (square: square_specifier, ok: bool) { +board_control_get_square_from_client_coords :: proc(hwnd: win.HWND, client_x, client_y: i32) -> (square: Square_Specifier, ok: bool) { client: win.RECT win.GetClientRect(hwnd, &client) x, y, w, h := board_control_get_board_bounds(hwnd, client) @@ -789,7 +841,7 @@ board_control_get_square_from_client_coords :: proc(hwnd: win.HWND, client_x, cl } // Get the source rectangle for the sprite for `piece` in the sprite map. -board_control_get_sprite_src :: proc(piece: board_piece) -> (x, y, w, h: i32) { +board_control_get_sprite_src :: proc(piece: Board_Piece) -> (x, y, w, h: i32) { if piece.type == .none { return } @@ -812,7 +864,7 @@ board_control_get_sprite_src :: proc(piece: board_piece) -> (x, y, w, h: i32) { // Stores the specified mouse client coordinates for the visual position of the currently dragged piece. // // Coordinates values (-1, -1) mean that the mouse is not over the board. -board_control_set_drag_target_coords :: proc(hwnd: win.HWND, ctx: ^board_control_ctx, x_client, y_client: i32) { +board_control_set_drag_target_coords :: proc(hwnd: win.HWND, ctx: ^Board_Control_Ctx, x_client, y_client: i32) { if ctx.dragging && ctx.mouse_client != { x_client, y_client } { ctx.mouse_client = { x_client, y_client } win.InvalidateRect(hwnd, nil, true) diff --git a/win32/fentool/fen.odin b/win32/fentool/fen.odin index eeb2e06..2fc4af5 100644 --- a/win32/fentool/fen.odin +++ b/win32/fentool/fen.odin @@ -7,7 +7,7 @@ import strings "core:strings" import strconv "core:strconv" import win "core:sys/windows" -parse_fen_error :: struct { +Parse_FEN_Error :: struct { is_error: bool, cursor: int, display_message: string, @@ -15,22 +15,22 @@ parse_fen_error :: struct { // Retrieves the (possibly) localized error string with the specified ID from the embedded resource, and formats // the error with the given arguments. -make_fen_errorf :: proc(ctx: parse_ctx, allocator: runtime.Allocator, string_resource_id: win.UINT, args: ..any) -> parse_fen_error { +make_fen_errorf :: proc(ctx: Parse_Ctx, allocator: runtime.Allocator, string_resource_id: win.UINT, args: ..any) -> Parse_FEN_Error { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) format := load_string_resource(hInstance, string_resource_id) message := fmt.aprintf(format, args = args, allocator = allocator) - return parse_fen_error { + return Parse_FEN_Error { is_error = true, cursor = ctx.c, display_message = message, } } -parse_fen :: proc(s: string, allocator := context.temp_allocator) -> (board: board, error: parse_fen_error) { +parse_fen :: proc(s: string, allocator := context.temp_allocator) -> (board: Board, error: Parse_FEN_Error) { // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 - ctx := parse_ctx { s = s } + ctx := Parse_Ctx { s = s } eat_spaces(&ctx) // rank index 0 is the "1" rank on the board. @@ -41,20 +41,20 @@ parse_fen :: proc(s: string, allocator := context.temp_allocator) -> (board: boa file := 0 rank_loop: for { c := cast(rune)ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 - piece: Maybe(board_piece) + piece: Maybe(Board_Piece) switch c { - case 'r': piece = board_piece { .rook, .black } - case 'n': piece = board_piece { .knight, .black } - case 'b': piece = board_piece { .bishop, .black } - case 'q': piece = board_piece { .queen, .black } - case 'k': piece = board_piece { .king, .black } - case 'p': piece = board_piece { .pawn, .black } - case 'R': piece = board_piece { .rook, .white } - case 'N': piece = board_piece { .knight, .white } - case 'B': piece = board_piece { .bishop, .white } - case 'Q': piece = board_piece { .queen, .white } - case 'K': piece = board_piece { .king, .white } - case 'P': piece = board_piece { .pawn, .white } + case 'r': piece = Board_Piece { .rook, .black } + case 'n': piece = Board_Piece { .knight, .black } + case 'b': piece = Board_Piece { .bishop, .black } + case 'q': piece = Board_Piece { .queen, .black } + case 'k': piece = Board_Piece { .king, .black } + case 'p': piece = Board_Piece { .pawn, .black } + case 'R': piece = Board_Piece { .rook, .white } + case 'N': piece = Board_Piece { .knight, .white } + case 'B': piece = Board_Piece { .bishop, .white } + case 'Q': piece = Board_Piece { .queen, .white } + case 'K': piece = Board_Piece { .king, .white } + case 'P': piece = Board_Piece { .pawn, .white } case: if c == '/' { if file != 8 || rank == 0 { @@ -325,7 +325,7 @@ parse_fen :: proc(s: string, allocator := context.temp_allocator) -> (board: boa return } -format_fen :: proc(board: board, allocator := context.temp_allocator) -> string { +format_fen :: proc(board: Board, allocator := context.temp_allocator) -> string { sb := strings.builder_make_len_cap(0, 200, allocator) fen_write_pieces(&sb, board.pieces) @@ -370,7 +370,7 @@ format_fen :: proc(board: board, allocator := context.temp_allocator) -> string return strings.to_string(sb) } -fen_write_pieces :: proc(sb: ^strings.Builder, pieces: board_pieces) { +fen_write_pieces :: proc(sb: ^strings.Builder, pieces: Board_Pieces) { assert(len(pieces) == 64) // file index 0 is the 'A' file. @@ -423,7 +423,7 @@ fen_write_pieces :: proc(sb: ^strings.Builder, pieces: board_pieces) { } } -fen_write_en_passant_target_square :: proc(sb: ^strings.Builder, square: square_specifier) { +fen_write_en_passant_target_square :: proc(sb: ^strings.Builder, square: Square_Specifier) { if square == {} { // NOTE: This works because (0, 0) is never a valid en passant square. strings.write_string(sb, "-") @@ -443,21 +443,21 @@ fen_write_en_passant_target_square :: proc(sb: ^strings.Builder, square: square_ } @(private="file") -parse_ctx :: struct { +Parse_Ctx :: struct { s: string, c: int, - board: board, + board: Board, } @(private="file") -eat_spaces :: proc(ctx: ^parse_ctx) { +eat_spaces :: proc(ctx: ^Parse_Ctx) { for ctx.c < len(ctx.s) && strings.is_ascii_space(cast(rune)ctx.s[ctx.c]) { ctx.c += 1 } } @(private="file") -read_number :: proc(ctx: ^parse_ctx) -> string { +read_number :: proc(ctx: ^Parse_Ctx) -> string { start := ctx.c for { c := ctx.s[ctx.c] if ctx.c < len(ctx.s) else 0 diff --git a/win32/fentool/file_dialog.odin b/win32/fentool/file_dialog.odin index 125815e..02f1da5 100644 --- a/win32/fentool/file_dialog.odin +++ b/win32/fentool/file_dialog.odin @@ -5,12 +5,12 @@ import strings "core:strings" import log "core:log" import win "core:sys/windows" -file_dialog_mode :: enum { +File_Dialog_Mode :: enum { open, save, } -file_dialog_filter :: struct { +File_Dialog_Filter :: struct { // The full line of text displayed to the user in the selection box. // // Examples: @@ -28,13 +28,13 @@ file_dialog_filter :: struct { file_dialog :: proc( owner: win.HWND, - mode: file_dialog_mode, + mode: File_Dialog_Mode, initial_file: Maybe(string) = nil, - filters: []file_dialog_filter = nil, + filters: []File_Dialog_Filter = nil, #any_int selected_filter_index: u32 = 0, dialog_caption: Maybe(string) = nil, initial_directory: Maybe(string) = nil, - allocator: runtime.Allocator = context.temp_allocator, + allocator: runtime.Allocator = context.allocator, ) -> ( dialog_confirmed: bool, selected_file: string, @@ -97,7 +97,7 @@ file_dialog :: proc( // Converts the `filters` slice to the string format required by `OPENFILENAMEW.lpstrFilter`. // // Returns `nil` if `filters` is an empty slice. -file_dialog_build_filter_string :: proc(filters: []file_dialog_filter, allocator: runtime.Allocator = context.temp_allocator) -> win.LPCWSTR { +file_dialog_build_filter_string :: proc(filters: []File_Dialog_Filter, allocator: runtime.Allocator = context.temp_allocator) -> win.LPCWSTR { if len(filters) == 0 { return nil diff --git a/win32/fentool/main.odin b/win32/fentool/main.odin index 208bed3..e41d673 100644 --- a/win32/fentool/main.odin +++ b/win32/fentool/main.odin @@ -13,6 +13,10 @@ MAIN_WNDCLASS_NAME :: "fen_tool_main" main :: proc() { + startup_info : win.STARTUPINFOW + win.GetStartupInfoW(&startup_info) + nCmdShow := (startup_info.dwFlags & win.STARTF_USESHOWWINDOW) != 0 ? cast(win.c_int)startup_info.wShowWindow : win.SW_SHOWDEFAULT + // For testing purposes - toggles the language of loaded resources. //win.SetThreadUILanguage(0x0413) @@ -31,7 +35,7 @@ main :: proc() { hdlg := win.CreateDialogW(hInstance, "IDD_MAIN", nil, dlg_proc) fudge_window_starting_position(hdlg) - win.ShowWindow(hdlg, win.SW_SHOW) + win.ShowWindow(hdlg, nCmdShow) accelerator_table := win.LoadAcceleratorsW(hInstance, "IDC_ACCELERATOR_TABLE") @@ -50,20 +54,22 @@ main :: proc() { os.exit(cast(int)msg.wParam) } -main_ctx :: struct { +Main_Ctx :: struct { runtime_context: ^runtime.Context, - hdlg: win.HWND, + // Used to suppress any change notifications while we're + // updating control contents programmatically. updating: int, - current_pieces: board_pieces, + current_pieces: Board_Pieces, // Last opened or saved file path. // Used as the save path in `IDM_SAVE`. // When empty, no project file is active. + // NOTE: This instance owns the memory associated with the string. active_project_file_path: string, project_dirty: bool, } -get_main_ctx :: proc "contextless" (hdlg: win.HWND) -> ^main_ctx { - return cast(^main_ctx)cast(uintptr)win.GetWindowLongPtrW(hdlg, win.DWLP_USER) +get_main_ctx :: proc "contextless" (hdlg: win.HWND) -> ^Main_Ctx { + return cast(^Main_Ctx)cast(uintptr)win.GetWindowLongPtrW(hdlg, win.DWLP_USER) } dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lparam: win.LPARAM) -> win.INT_PTR { @@ -73,11 +79,11 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp case win.WM_INITDIALOG: // Setup main window context context = window_context_tls - ctx := new(main_ctx) + ctx := new(Main_Ctx) ctx.runtime_context = &window_context_tls - ctx.hdlg = hdlg win.SetWindowLongPtrW(hdlg, win.DWLP_USER, cast(win.LONG_PTR)cast(uintptr)ctx) + // Load and set the window icon. hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) idi_main := cast(rawptr)cast(uintptr)IDI_MAIN icon_big := win.LoadImageW(hInstance, cast(win.LPCWSTR)idi_main, win.IMAGE_ICON, win.GetSystemMetrics(win.SM_CXICON), win.GetSystemMetrics(win.SM_CYICON), win.LR_DEFAULTCOLOR | win.LR_SHARED) @@ -85,6 +91,7 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp win.SendMessageW(hdlg, win.WM_SETICON, win.ICON_BIG, cast(win.LPARAM)cast(uintptr)icon_big) win.SendMessageW(hdlg, win.WM_SETICON, win.ICON_SMALL, cast(win.LPARAM)cast(uintptr)icon_small) + // Check initial visuals option. hmenu := win.GetMenu(hdlg) win.CheckMenuRadioItem(hmenu, IDM_VISUALS_NORMAL, IDM_VISUALS_GINGER, IDM_VISUALS_NORMAL, win.MF_BYCOMMAND) @@ -93,13 +100,16 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp case win.WM_NCDESTROY: ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ + delete(ctx.active_project_file_path) + ctx.active_project_file_path = "" free(ctx) case win.WM_DESTROY: ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ + // Main window destroyed - quit message loop. win.PostQuitMessage(0) - // Return 1 to stop DefDlgProc message handling. + // Return 1 to stop DefDlgProc message handling - we handle this explicitly because we're the application's main window. return 1 case win.WM_CLOSE: @@ -108,17 +118,18 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp if prompt_if_project_dirty(hdlg, ctx) { win.DestroyWindow(hdlg) } - // Return 1 to stop DefDlgProc message handling. + // Return 1 to stop DefDlgProc message handling - we handle this explicitly because we're the application's main window. return 1 case win.WM_SIZE: ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ - // The status bar will adjust itself, but it does need a message to trigger it. + // The status bar will adjust itself to the window size, it just needs a WM_SIZE message to trigger it. status_bar := win.GetDlgItem(hdlg, IDC_STATUSBAR) win.SendMessageW(status_bar, win.WM_SIZE, 0, 0) case win.WM_COMMAND: + ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ id := win.LOWORD(wparam) @@ -127,7 +138,7 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp switch id { case IDM_RESET_TO_STARTING_POSITION: - reset_to_starting_position(ctx.hdlg, ctx, prompt_for_confirmation = true) + reset_to_starting_position(hdlg, ctx, prompt_for_confirmation = true) case IDM_NEW: file_new(hdlg, ctx) @@ -179,13 +190,14 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp if nc == win.EN_CHANGE { if ctx.updating == 0 { reparse_fen(hdlg, ctx, interactive = true) - set_project_dirty(ctx) + set_project_dirty(hdlg, ctx) } } case IDC_RADIOBUTTON_WHITE_TO_MOVE, IDC_RADIOBUTTON_BLACK_TO_MOVE: if nc == win.BN_CLICKED { + // Check the clicked radio button. win.CheckRadioButton(hdlg, IDC_RADIOBUTTON_WHITE_TO_MOVE, IDC_RADIOBUTTON_BLACK_TO_MOVE, auto_cast id) update_from_control_states(hdlg, ctx) } @@ -195,7 +207,7 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp IDC_CHECKBOX_BLACK_CASTLE_KINGSIDE, IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE: if nc == win.BN_CLICKED { - // Toggle the check state. + // Toggle the check state of the clicked check box. current_state := win.IsDlgButtonChecked(hdlg, auto_cast id) == win.BST_CHECKED new_state := current_state ? win.BST_UNCHECKED : win.BST_CHECKED win.CheckDlgButton(hdlg, auto_cast id, auto_cast new_state) @@ -226,14 +238,20 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp } case win.WM_NOTIFY: + switch wparam { + case IDC_BOARD_CONTROL: + ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ nm := cast(^NM_BOARD_CONTROL_NOTIFICATION)cast(uintptr)lparam - code := cast(board_control_notification_code)nm.hdr.code + code := cast(Board_Control_Notification_Code)nm.hdr.code + switch code { + case .none: + case .hover: if nm.hovering { // Build a string like "A1 - Rook (White)" @@ -253,6 +271,7 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp } else { set_dialog_item_text(hdlg, IDC_STATUSBAR, "") } + case .place_piece: rank := nm.square.rank file := nm.square.file @@ -260,10 +279,12 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp ctx.current_pieces[rank * 8 + file] = piece update_from_control_states(hdlg, ctx) play_piece_placing_sound_if_enabled(hdlg) + case .set_en_passant_target_square: square := nm.square set_en_passant_target_square(hdlg, square) update_from_control_states(hdlg, ctx) + case .set_castling_right: castling_right := nm.castling_right state := nm.castling_right_state @@ -280,6 +301,7 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp case .black_queenside: win.CheckDlgButton(hdlg, IDC_CHECKBOX_BLACK_CASTLE_QUEENSIDE, button_state) } update_from_control_states(hdlg, ctx) + case .exchange_pieces: rank1 := nm.square.rank file1 := nm.square.file @@ -294,6 +316,10 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp } case win.WM_MENUSELECT: + // User is hovering over any menu item. + // Try to get a help hint for the status bar by looking for a string resource + // with the same ID as the hovered menu item ID. + // This works because by convention, we share resource IDs in this particular way. ctx := get_main_ctx(hdlg) context = ctx.runtime_context^ menu_item_id := cast(win.UINT)win.LOWORD(wparam) @@ -313,21 +339,27 @@ dlg_proc :: proc "system" (hdlg: win.HWND, msg: win.UINT, wparam: win.WPARAM, lp return 0 } -file_new :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { +// +// File New/Open/Save/SaveAs +// + +file_new :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> bool { if !prompt_if_project_dirty(hdlg, ctx) { return false } delete(ctx.active_project_file_path) ctx.active_project_file_path = "" reset_to_starting_position(hdlg, ctx, set_dirty = false) - set_project_dirty(ctx, false) + set_project_dirty(hdlg, ctx, false) return true } -file_open :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { +file_open :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> bool { + if !prompt_if_project_dirty(hdlg, ctx) { return false } + filters := get_file_dialog_filters() dialog_confirmed, file_path := file_dialog( owner = hdlg, @@ -335,9 +367,11 @@ file_open :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { filters = filters[:], allocator = context.allocator, ) + if !dialog_confirmed { return false } + data, error := os.read_entire_file_from_path(file_path, context.temp_allocator) if error != nil { // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. @@ -347,35 +381,37 @@ file_open :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) return false } + fen := transmute(string)data - set_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, fen) + set_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, fen) delete(ctx.active_project_file_path) ctx.active_project_file_path = file_path - set_project_dirty(ctx, false) + set_project_dirty(hdlg, ctx, false) return true } -file_save :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { +file_save :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> bool { + if ctx.active_project_file_path == "" { return file_save_as(hdlg, ctx) - } else { - fen := get_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, 512) - error := os.write_entire_file(ctx.active_project_file_path, fen) - if error != nil { - // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. - error_message := os.error_string(error) - hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) - error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) - win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) - return false - } - set_project_dirty(ctx, false) - return true } + + fen := get_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, 512) + error := os.write_entire_file(ctx.active_project_file_path, fen) + if error != nil { + // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. + error_message := os.error_string(error) + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) + error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) + win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) + return false + } + set_project_dirty(hdlg, ctx, false) + return true } -file_save_as :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { - hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) +file_save_as :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> bool { + filters := get_file_dialog_filters() dialog_confirmed, file_path := file_dialog( owner = hdlg, @@ -383,45 +419,37 @@ file_save_as :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { filters = filters[:], allocator = context.allocator, ) + if !dialog_confirmed { return false } - fen := get_dialog_item_text(ctx.hdlg, IDC_TEXTBOX_FEN, 512) + + fen := get_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, 512) error := os.write_entire_file(file_path, fen) if error != nil { // NOTE: The Win32 system error message can actually be nicer than what the OS package gives us here. error_message := os.error_string(error) + hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) error_caption := load_string_resource(hInstance, IDS_ERROR_CAPTION, context.temp_allocator) win.MessageBoxW(hdlg, win.utf8_to_wstring(error_message, context.temp_allocator), win.utf8_to_wstring(error_caption, context.temp_allocator), win.MB_OK) return false } + delete(ctx.active_project_file_path) ctx.active_project_file_path = file_path - set_project_dirty(ctx, false) + set_project_dirty(hdlg, ctx, false) return true } -get_file_dialog_filters :: proc() -> [2]file_dialog_filter { +get_file_dialog_filters :: proc() -> [2]File_Dialog_Filter { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) - return [2]file_dialog_filter { + return [2]File_Dialog_Filter { { display_text = load_string_resource(hInstance, IDS_FEN_FILES_FILTER), filter = "*.fen" }, { display_text = load_string_resource(hInstance, IDS_ALL_FILES_FILTER), filter = "*.*" }, } } -play_piece_placing_sound_if_enabled :: proc(hdlg: win.HWND) { - hmenu := win.GetMenu(hdlg) - mii: win.MENUITEMINFOW - mii.cbSize = size_of(win.MENUITEMINFOW) - mii.fMask = win.MIIM_STATE - win.GetMenuItemInfoW(hmenu, IDM_PLAY_SOUND, false, &mii) - checked := (mii.fState & win.MFS_CHECKED) != 0 - if checked { - win.PlaySoundW("IDW_CLACK", win.GetModuleHandleW(nil), win.SND_ASYNC | win.SND_RESOURCE) - } -} - -prompt_if_project_dirty :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { +prompt_if_project_dirty :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> bool { if ctx.project_dirty { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) text := load_string_resource(hInstance, IDS_UNSAVED_CHANGES_MESSAGE) @@ -442,7 +470,7 @@ prompt_if_project_dirty :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> bool { return true } -set_project_dirty :: proc(ctx: ^main_ctx, dirty := true) { +set_project_dirty :: proc(hdlg: win.HWND, ctx: ^Main_Ctx, dirty := true) { if ctx.project_dirty != dirty { ctx.project_dirty = dirty if dirty { @@ -450,15 +478,29 @@ set_project_dirty :: proc(ctx: ^main_ctx, dirty := true) { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) caption := load_string_resource(hInstance, IDS_TITLE, context.temp_allocator) caption_dirty := strings.concatenate({ caption, "*" }, context.temp_allocator) - win.SetWindowTextW(ctx.hdlg, win.utf8_to_wstring(caption_dirty, context.temp_allocator)) + win.SetWindowTextW(hdlg, win.utf8_to_wstring(caption_dirty, context.temp_allocator)) } else { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) caption := load_string_resource(hInstance, IDS_TITLE, context.temp_allocator) - win.SetWindowTextW(ctx.hdlg, win.utf8_to_wstring(caption, context.temp_allocator)) + win.SetWindowTextW(hdlg, win.utf8_to_wstring(caption, context.temp_allocator)) } } } +play_piece_placing_sound_if_enabled :: proc(hdlg: win.HWND) { + // Query the "Play Sound" option directly from the menu. + hmenu := win.GetMenu(hdlg) + mii: win.MENUITEMINFOW + mii.cbSize = size_of(win.MENUITEMINFOW) + mii.fMask = win.MIIM_STATE + win.GetMenuItemInfoW(hmenu, IDM_PLAY_SOUND, false, &mii) + checked := (mii.fState & win.MFS_CHECKED) != 0 + if checked { + // PlaySoundW can play the sound directly from the resource. + win.PlaySoundW("IDW_CLACK", win.GetModuleHandleW(nil), win.SND_ASYNC | win.SND_RESOURCE) + } +} + show_en_passant_validation_tip :: proc(hdlg: win.HWND) { text := get_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, 512, context.temp_allocator) @@ -485,7 +527,7 @@ show_en_passant_validation_tip :: proc(hdlg: win.HWND) { win.SendDlgItemMessageW(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, win.EM_SHOWBALLOONTIP, 0, cast(win.LPARAM)cast(uintptr)&tip) } -format_piece_display_text :: proc(piece: board_piece, allocator := context.temp_allocator) -> string { +format_piece_display_text :: proc(piece: Board_Piece, allocator := context.temp_allocator) -> string { type_str: string @@ -515,7 +557,7 @@ format_piece_display_text :: proc(piece: board_piece, allocator := context.temp_ STARTING_POSITION_FEN :: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" -reset_to_starting_position :: proc(hdlg: win.HWND, ctx: ^main_ctx, set_dirty := true, prompt_for_confirmation := false) { +reset_to_starting_position :: proc(hdlg: win.HWND, ctx: ^Main_Ctx, set_dirty := true, prompt_for_confirmation := false) { if prompt_for_confirmation { hInstance := cast(win.HINSTANCE)win.GetModuleHandleW(nil) @@ -532,7 +574,7 @@ reset_to_starting_position :: proc(hdlg: win.HWND, ctx: ^main_ctx, set_dirty := defer ctx.updating -= 1 set_dialog_item_text_cstring16(hdlg, IDC_TEXTBOX_FEN, STARTING_POSITION_FEN) if set_dirty { - set_project_dirty(ctx) + set_project_dirty(hdlg, ctx) } } @@ -541,10 +583,13 @@ reset_to_starting_position :: proc(hdlg: win.HWND, ctx: ^main_ctx, set_dirty := // Parses the contents of the FEN string text box, and updates all // controls accordingly to the board state it denotes. -reparse_fen :: proc(hdlg: win.HWND, ctx: ^main_ctx, interactive: bool) { +// +// - `interactive`: Displays a balloon tip if the text box contains an invalid FEN string. +// Should only be used if this was called after direct user interaction. +reparse_fen :: proc(hdlg: win.HWND, ctx: ^Main_Ctx, interactive: bool) { + fen_string := get_dialog_item_text(hdlg, IDC_TEXTBOX_FEN, 512, context.temp_allocator) if fen_string == "" { - fmt.printfln("FEN string is fucked.") return } @@ -565,8 +610,7 @@ reparse_fen :: proc(hdlg: win.HWND, ctx: ^main_ctx, interactive: bool) { return } - fmt.printfln("Board:") - fmt.printfln("%v", board_format_full(board)) + log.debugf("Board:\r\n%v", board_format_full(board)) ctx.updating += 1 defer ctx.updating -= 1 @@ -596,7 +640,7 @@ reparse_fen :: proc(hdlg: win.HWND, ctx: ^main_ctx, interactive: bool) { win.SetDlgItemInt(hdlg, IDC_TEXTBOX_FULLMOVE_NUMBER, cast(win.UINT)board.fullmove_number, true) } -set_en_passant_target_square :: proc(hdlg: win.HWND, square: square_specifier) { +set_en_passant_target_square :: proc(hdlg: win.HWND, square: Square_Specifier) { square_text := board_format_en_passant_target_square(square) set_dialog_item_text(hdlg, IDC_TEXTBOX_EN_PASSANT_TARGET_SQUARE, square_text) } @@ -604,7 +648,7 @@ set_en_passant_target_square :: proc(hdlg: win.HWND, square: square_specifier) { // Queries the state of all controls, and then displays the // corresponding FEN string in the FEN string text box, and the // board state in the board control. -update_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) { +update_from_control_states :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) { // Construct board from control states. board, board_ok := get_board_from_control_states(hdlg, ctx) @@ -632,16 +676,16 @@ update_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) { log.debugf("FEN string changing from %q to %q", old_fen_string, fen_string) fen_wstr := win.utf8_to_wstring_alloc(fen_string, context.temp_allocator) win.SetDlgItemTextW(hdlg, IDC_TEXTBOX_FEN, fen_wstr) - set_project_dirty(ctx) + set_project_dirty(hdlg, ctx) } } -get_board_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> (result: board, ok: bool) { +get_board_from_control_states :: proc(hdlg: win.HWND, ctx: ^Main_Ctx) -> (result: Board, ok: bool) { log.debugf("Constructing board from control states...") // Collect all information in this board instance. - board: board + board: Board board.pieces = ctx.current_pieces @@ -688,12 +732,13 @@ get_board_from_control_states :: proc(hdlg: win.HWND, ctx: ^main_ctx) -> (result return board, true } -construct_fen_string_from_board :: proc(board: board) -> (result: string, ok: bool) { +construct_fen_string_from_board :: proc(board: Board) -> (result: string, ok: bool) { fen_string := format_fen(board) return fen_string, true } -parse_en_passant_target_square :: proc(s0: string) -> (result: square_specifier, ok: bool) { +parse_en_passant_target_square :: proc(s0: string) -> (result: Square_Specifier, ok: bool) { + s := strings.trim_space(s0) if s == "" || s == "-" { // We interpret an empty string as "no en passant". diff --git a/win32/fentool/resource.rc b/win32/fentool/resource.rc index 98cb311..472dad8 100644 --- a/win32/fentool/resource.rc +++ b/win32/fentool/resource.rc @@ -1,3 +1,4 @@ +// Read this file as UTF-8 #pragma code_page(65001) #include @@ -8,12 +9,12 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "app.manifest" -IDI_MAIN ICON "icon.ico" -IDB_ABOUT BITMAP "about.bmp" -IDR_PIECES_NORMAL RCDATA "piecesnormal.png" -IDR_PIECES_GINGER RCDATA "billchess.png" - -IDW_CLACK WAVE "clack.wav" +// NOTE: The icon with the lowest ID value becomes the executable's main icon (displayed in Windows Explorer). +IDI_MAIN ICON "icon.ico" +IDB_ABOUT BITMAP "about.bmp" +IDR_PIECES_NORMAL RCDATA "piecesnormal.png" +IDR_PIECES_GINGER RCDATA "billchess.png" +IDW_CLACK WAVE "clack.wav" LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US @@ -43,6 +44,7 @@ BEGIN END END +// Help hints for IDM_MAIN_MENU STRINGTABLE BEGIN IDM_NEW "Creates a new position." @@ -131,6 +133,7 @@ BEGIN END END +// Help hints for IDM_BOARD_CONTEXT_MENU STRINGTABLE BEGIN IDM_SQUARE_INFO "Displays the selected square." @@ -243,6 +246,7 @@ BEGIN END END +// Help hints for IDM_MAIN_MENU STRINGTABLE BEGIN IDM_NEW "Maakt een nieuwe positie aan." @@ -343,6 +347,7 @@ BEGIN END END +// Help hints for IDM_BOARD_CONTEXT_MENU STRINGTABLE BEGIN IDM_SQUARE_INFO "Toont het geselecteerde veld." From 61a3da1d341c7c7be9418c50a610ed71b36836f0 Mon Sep 17 00:00:00 2001 From: bplu4t2f Date: Thu, 1 Oct 2026 11:24:47 +0200 Subject: [PATCH 3/3] Improve error behavior when compiled improperly (without resource script). --- win32/fentool/main.odin | 1 + 1 file changed, 1 insertion(+) diff --git a/win32/fentool/main.odin b/win32/fentool/main.odin index e41d673..00933e8 100644 --- a/win32/fentool/main.odin +++ b/win32/fentool/main.odin @@ -33,6 +33,7 @@ main :: proc() { window_context_tls = context hdlg := win.CreateDialogW(hInstance, "IDD_MAIN", nil, dlg_proc) + assert(hdlg != nil, "Could not create main dialog. Is 'resource.rc' compiled properly?") fudge_window_starting_position(hdlg) win.ShowWindow(hdlg, nCmdShow)