Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions win32/fentool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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
-----

**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
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.
Binary file added win32/fentool/about.bmp
Binary file not shown.
67 changes: 67 additions & 0 deletions win32/fentool/about.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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:
// Instruct the system to use default keyboard focus handling.
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:
// 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
}

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)

// 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, bitmap_w, bitmap_h,
hdc_src, 0, 0, bitmap_w, bitmap_h,
transparency_key,
)
}

return 0
}
20 changes: 20 additions & 0 deletions win32/fentool/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"/>
</dependentAssembly>
</dependency>
<application>
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
Binary file added win32/fentool/billchess.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions win32/fentool/board.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package fen_tool

import fmt "core:fmt"
import strings "core:strings"

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,
halfmove_clock: i32,
fullmove_number: i32,
}

Square_Specifier :: struct {
file: i8,
rank: i8,
}

Board_Piece :: struct {
type: Board_Piece_Type,
color: Board_Piece_Color,
}

// 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 {
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_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_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 {
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 '?'
}
}
Loading
Loading