Repository: leok7v/ui Branch: main Commit: 0f200aeb298c Files: 166 Total size: 2.7 MB Directory structure: gitextract_fo8u23vg/ ├── .github/ │ └── workflows/ │ └── build-on-push.yml ├── .gitignore ├── LICENSE ├── README.md ├── inc/ │ ├── generics.h │ ├── rt/ │ │ ├── rt.h │ │ ├── rt_args.h │ │ ├── rt_atomics.h │ │ ├── rt_backtrace.h │ │ ├── rt_clipboard.h │ │ ├── rt_clock.h │ │ ├── rt_config.h │ │ ├── rt_core.h │ │ ├── rt_debug.h │ │ ├── rt_files.h │ │ ├── rt_glyphs.h │ │ ├── rt_heap.h │ │ ├── rt_loader.h │ │ ├── rt_mem.h │ │ ├── rt_nls.h │ │ ├── rt_num.h │ │ ├── rt_processes.h │ │ ├── rt_static.h │ │ ├── rt_str.h │ │ ├── rt_streams.h │ │ ├── rt_threads.h │ │ ├── rt_vigil.h │ │ ├── rt_work.h │ │ └── version.rc.in │ ├── ui/ │ │ ├── ui.h │ │ ├── ui_app.h │ │ ├── ui_button.h │ │ ├── ui_caption.h │ │ ├── ui_colors.h │ │ ├── ui_containers.h │ │ ├── ui_core.h │ │ ├── ui_edit_doc.h │ │ ├── ui_edit_view.h │ │ ├── ui_fuzzing.h │ │ ├── ui_gdi.h │ │ ├── ui_image.h │ │ ├── ui_label.h │ │ ├── ui_mbx.h │ │ ├── ui_midi.h │ │ ├── ui_slider.h │ │ ├── ui_theme.h │ │ ├── ui_toggle.h │ │ └── ui_view.h │ ├── ut_std.h │ └── ut_win32.h ├── msvc2022/ │ ├── .editorconfig │ ├── amalgamate.vcxproj │ ├── amalgamate.vcxproj.filters │ ├── common.props │ ├── exclusion.dic │ ├── groot.vcxproj │ ├── groot.vcxproj.filters │ ├── manifest.xml │ ├── msvc2022.vssettings │ ├── prebuild.vcxproj │ ├── prebuild.vcxproj.filters │ ├── rt.vcxproj │ ├── rt.vcxproj.filters │ ├── sample1.vcxproj │ ├── sample1.vcxproj.filters │ ├── sample2.vcxproj │ ├── sample2.vcxproj.filters │ ├── sample3.vcxproj │ ├── sample3.vcxproj.filters │ ├── sample4.vcxproj │ ├── sample4.vcxproj.filters │ ├── sample5.vcxproj │ ├── sample5.vcxproj.filters │ ├── sample6.vcxproj │ ├── sample6.vcxproj.filters │ ├── sample7.vcxproj │ ├── sample7.vcxproj.filters │ ├── sample8.vcxproj │ ├── sample8.vcxproj.filters │ ├── sample9.vcxproj │ ├── sample9.vcxproj.filters │ ├── test1.vcxproj │ ├── test1.vcxproj.filters │ ├── test2.vcxproj │ ├── test2.vcxproj.filters │ ├── ui.sln │ ├── ui.vcxproj │ ├── ui.vcxproj.filters │ ├── version.vcxproj │ └── version.vcxproj.filters ├── scripts/ │ ├── clean.bat │ └── prebuild.bat ├── single_file_lib/ │ ├── rt/ │ │ └── rt.h │ └── ui/ │ └── ui.h ├── src/ │ ├── rt/ │ │ ├── rt.c │ │ ├── rt_args.c │ │ ├── rt_atomics.c │ │ ├── rt_backtrace.c │ │ ├── rt_clipboard.c │ │ ├── rt_clock.c │ │ ├── rt_config.c │ │ ├── rt_core.c │ │ ├── rt_debug.c │ │ ├── rt_files.c │ │ ├── rt_generics.c │ │ ├── rt_heap.c │ │ ├── rt_loader.c │ │ ├── rt_mem.c │ │ ├── rt_nls.c │ │ ├── rt_num.c │ │ ├── rt_processes.c │ │ ├── rt_static.c │ │ ├── rt_str.c │ │ ├── rt_streams.c │ │ ├── rt_threads.c │ │ ├── rt_vigil.c │ │ ├── rt_win32.c │ │ └── rt_work.c │ ├── samples/ │ │ ├── edit.test.c │ │ ├── groot/ │ │ │ ├── groot.c │ │ │ ├── groot.h │ │ │ ├── groot.rc │ │ │ └── rocket.h │ │ ├── i18n.h │ │ ├── mr_blue_sky.midi │ │ ├── rt.c │ │ ├── sample.rc │ │ ├── sample1.c │ │ ├── sample2.c │ │ ├── sample3.c │ │ ├── sample4.c │ │ ├── sample5.c │ │ ├── sample6.c │ │ ├── sample6.rc │ │ ├── sample7.c │ │ ├── sample8.c │ │ ├── sample9.c │ │ ├── stb_image.c │ │ ├── stb_image.h │ │ └── ui.c │ ├── tools/ │ │ ├── amalgamate.c │ │ ├── dirent.c │ │ ├── dirent.h │ │ └── version.c │ └── ui/ │ ├── attic/ │ │ └── ui_theme.c │ ├── ui_app.c │ ├── ui_button.c │ ├── ui_caption.c │ ├── ui_colors.c │ ├── ui_containers.c │ ├── ui_core.c │ ├── ui_edit_doc.c │ ├── ui_edit_view.c │ ├── ui_fuzzing.c │ ├── ui_gdi.c │ ├── ui_image.c │ ├── ui_label.c │ ├── ui_mbx.c │ ├── ui_midi.c │ ├── ui_slider.c │ ├── ui_theme.c │ ├── ui_toggle.c │ └── ui_view.c └── test/ ├── test1.c ├── test1.rc └── test2.c ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build-on-push.yml ================================================ name: build-on-push on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: build: runs-on: windows-latest permissions: id-token: write contents: read attestations: write steps: - uses: actions/checkout@v4 with: submodules: recursive - name: setup msbuild uses: microsoft/setup-msbuild@v2 - name: build debug run: msbuild msvc2022\ui.sln -t:rebuild -verbosity:quiet -property:Configuration=debug -property:Platform=x64 - name: build release run: msbuild msvc2022\ui.sln -t:rebuild -verbosity:quiet -property:Configuration=release -property:Platform=x64 - name: short sha id: vars run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: check short sha run: | echo "sha_short: $env:sha_short" env: sha_short: ${{ steps.vars.outputs.sha_short }} - name: run debug tests run: bin\debug\x64\test1.exe --verbosity quiet - name: run release tests run: bin\release\x64\test1.exe --verbosity quiet - name: Attest Build Provenance uses: actions/attest-build-provenance@v1.1.2 with: subject-path: 'bin\**\*.exe' - name: upload release artifact uses: actions/upload-artifact@v4 with: name: ui.release.zip path: | bin\release\**\*.exe retention-days: 5 - name: upload debug artifact uses: actions/upload-artifact@v4 with: name: ui.debug.zip path: | bin\debug\**\*.exe retention-days: 5 ================================================ FILE: .gitignore ================================================ # User files *.vcxproj.user inc/rt/version.h bin/** build/** lib/** msvc2022/.vs/* ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Leo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ui Single header libraries for building primitive UI/UX. Win32 only implementation for now. Posix/MacOS/iOS/Linux/Android - possible [![build-on-push](https://github.com/leok7v/ut/actions/workflows/build-on-push.yml/badge.svg)](https://github.com/leok7v/ut/actions/workflows/build-on-push.yml) # Two Namespaces: ``` rt_ for minimalistic fail fast runtime ui_ for UI types and interfaces ``` # Minimalistic "Hello" Application Example ```c #include "ui/ui.h" #include "rt/rt.h" static ui_label_t label = ui_label(0.0, "Hello"); static void opened(void) { ui_view.add(ui_app.view, &label, null); } static void init(void) { ui_app.title = "Sample"; ui_app.opened = opened; } ui_app_t ui_app = { .class_name = "sample", .init = init, .window_sizing = { .ini_w = 4.0f, // 4x2 inches .ini_h = 2.0f } }; ``` ================================================ FILE: inc/generics.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // Most of ut/ui code is written the way of min(a,b) max(a,b) // not having side effects on the arguments and thus evaluating // them twice ain't a big deal. However, out of curiosity of // usefulness of Generic() in C11 standard here is type-safe // single evaluation of the arguments version of what could // have been simple minimum and maximum macro definitions. // Type safety comes with the cost of complexity in puritan // or stoic language like C: static inline int8_t rt_max_int8(int8_t x, int8_t y) { return x > y ? x : y; } static inline int16_t rt_max_int16(int16_t x, int16_t y) { return x > y ? x : y; } static inline int32_t rt_max_int32(int32_t x, int32_t y) { return x > y ? x : y; } static inline int64_t rt_max_int64(int64_t x, int64_t y) { return x > y ? x : y; } static inline uint8_t rt_max_uint8(uint8_t x, uint8_t y) { return x > y ? x : y; } static inline uint16_t rt_max_uint16(uint16_t x, uint16_t y) { return x > y ? x : y; } static inline uint32_t rt_max_uint32(uint32_t x, uint32_t y) { return x > y ? x : y; } static inline uint64_t rt_max_uint64(uint64_t x, uint64_t y) { return x > y ? x : y; } static inline fp32_t rt_max_fp32(fp32_t x, fp32_t y) { return x > y ? x : y; } static inline fp64_t rt_max_fp64(fp64_t x, fp64_t y) { return x > y ? x : y; } // MS cl.exe version 19.39.33523 has issues with "long": // does not pick up int32_t/uint32_t types for "long" and "unsigned long" // need to handle long / unsigned long separately: static inline long rt_max_long(long x, long y) { return x > y ? x : y; } static inline unsigned long rt_max_ulong(unsigned long x, unsigned long y) { return x > y ? x : y; } static inline int8_t rt_min_int8(int8_t x, int8_t y) { return x < y ? x : y; } static inline int16_t rt_min_int16(int16_t x, int16_t y) { return x < y ? x : y; } static inline int32_t rt_min_int32(int32_t x, int32_t y) { return x < y ? x : y; } static inline int64_t rt_min_int64(int64_t x, int64_t y) { return x < y ? x : y; } static inline uint8_t rt_min_uint8(uint8_t x, uint8_t y) { return x < y ? x : y; } static inline uint16_t rt_min_uint16(uint16_t x, uint16_t y) { return x < y ? x : y; } static inline uint32_t rt_min_uint32(uint32_t x, uint32_t y) { return x < y ? x : y; } static inline uint64_t rt_min_uint64(uint64_t x, uint64_t y) { return x < y ? x : y; } static inline fp32_t rt_min_fp32(fp32_t x, fp32_t y) { return x < y ? x : y; } static inline fp64_t rt_min_fp64(fp64_t x, fp64_t y) { return x < y ? x : y; } static inline long rt_min_long(long x, long y) { return x < y ? x : y; } static inline unsigned long rt_min_ulong(unsigned long x, unsigned long y) { return x < y ? x : y; } static inline void rt_min_undefined(void) { } static inline void rt_max_undefined(void) { } #define rt_max(X, Y) _Generic((X) + (Y), \ int8_t: rt_max_int8, \ int16_t: rt_max_int16, \ int32_t: rt_max_int32, \ int64_t: rt_max_int64, \ uint8_t: rt_max_uint8, \ uint16_t: rt_max_uint16, \ uint32_t: rt_max_uint32, \ uint64_t: rt_max_uint64, \ fp32_t: rt_max_fp32, \ fp64_t: rt_max_fp64, \ long: rt_max_long, \ unsigned long: rt_max_ulong, \ default: rt_max_undefined)(X, Y) #define rt_min(X, Y) _Generic((X) + (Y), \ int8_t: rt_min_int8, \ int16_t: rt_min_int16, \ int32_t: rt_min_int32, \ int64_t: rt_min_int64, \ uint8_t: rt_min_uint8, \ uint16_t: rt_min_uint16, \ uint32_t: rt_min_uint32, \ uint64_t: rt_min_uint64, \ fp32_t: rt_min_fp32, \ fp64_t: rt_min_fp64, \ long: rt_min_long, \ unsigned long: rt_min_ulong, \ default: rt_min_undefined)(X, Y) // The expression (X) + (Y) is used in _Generic primarily for type promotion // and compatibility between different types of the two operands. In C, when // you perform an arithmetic operation like addition between two variables, // the types of these variables undergo implicit conversions to a common type // according to the usual arithmetic conversions defined in the C standard. // This helps ensure that: // // Type Compatibility: The macro works correctly even if X and Y are of // different types. By using (X) + (Y), both X and Y are promoted to a common // type, which ensures that the macro selects the appropriate function // that can handle this common type. // // Type Safety: It ensures that the selected function can handle the type // resulting from the operation, thereby preventing type mismatches that // could lead to undefined behavior or compilation errors. typedef struct { void (*test)(void); } rt_generics_if; extern rt_generics_if rt_generics; rt_end_c ================================================ FILE: inc/rt/rt.h ================================================ #pragma once #include "rt/rt_std.h" // must be first #include "rt/rt_str.h" // defines str_*_t types // the rest is in alphabetical order (no inter dependencies) #include "rt/rt_args.h" #include "rt/rt_backtrace.h" #include "rt/rt_atomics.h" #include "rt/rt_clipboard.h" #include "rt/rt_clock.h" #include "rt/rt_config.h" #include "rt/rt_core.h" #include "rt/rt_debug.h" #include "rt/rt_files.h" #include "rt/rt_generics.h" #include "rt/rt_glyphs.h" #include "rt/rt_heap.h" #include "rt/rt_loader.h" #include "rt/rt_mem.h" #include "rt/rt_nls.h" #include "rt/rt_num.h" #include "rt/rt_static.h" #include "rt/rt_streams.h" #include "rt/rt_processes.h" #include "rt/rt_threads.h" #include "rt/rt_vigil.h" #include "rt/rt_work.h" ================================================ FILE: inc/rt/rt_args.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { // On Unix it is responsibility of the main() to assign these values int32_t c; // argc const char** v; // argv[argc] const char** env; // rt_args.env[] is null-terminated void (*main)(int32_t argc, const char* argv[], const char** env); void (*WinMain)(void); // windows specific int32_t (*option_index)(const char* option); // e.g. option: "--verbosity" or "-v" void (*remove_at)(int32_t ix); /* argc=3 argv={"foo", "--verbose"} -> returns true; argc=1 argv={"foo"} */ bool (*option_bool)(const char* option); /* argc=3 argv={"foo", "--n", "153"} -> value==153, true; argc=1 argv={"foo"} also handles negative values (e.g. "-153") and hex (e.g. 0xBADF00D) */ bool (*option_int)(const char* option, int64_t *value); // for argc=3 argv={"foo", "--path", "bar"} // option_str("--path", option) // returns option: "bar" and argc=1 argv={"foo"} */ const char* (*option_str)(const char* option); // basename() for argc=3 argv={"/bin/foo.exe", ...} returns "foo": const char* (*basename)(void); void (*fini)(void); void (*test)(void); } rt_args_if; extern rt_args_if rt_args; /* Usage: (both main() and WinMain() could be compiled at the same time on Windows): static int run(void); int main(int argc, char* argv[], char* envp[]) { // link.exe /SUBSYSTEM:CONSOLE rt_args.main(argc, argv, envp); // Initialize args with command-line parameters int r = run(); rt_args.fini(); return r; } #include "rt/rt_win32.h" int APIENTRY WinMain(HINSTANCE inst, HINSTANCE prev, char* command, int show) { // link.exe /SUBSYSTEM:WINDOWS rt_args.WinMain(); int r = run(); rt_args.fini(); return 0; } static int run(void) { if (rt_args.option_bool("-v")) { rt_debug.verbosity.level = rt_debug.verbosity.verbose; } int64_t num = 0; if (rt_args.option_int("--number", &num)) { printf("--number: %ld\n", num); } const char* path = rt_args.option_str("--path"); if (path != null) { printf("--path: %s\n", path); } printf("rt_args.basename(): %s\n", rt_args.basename()); printf("rt_args.v[0]: %s\n", rt_args.v[0]); for (int i = 1; i < rt_args.c; i++) { printf("rt_args.v[%d]: %s\n", i, rt_args.v[i]); } return 0; } // Also see: https://github.com/leok7v/ut/blob/main/test/test1.c */ rt_end_c ================================================ FILE: inc/rt/rt_atomics.h ================================================ #pragma once #include "rt/rt_std.h" // Will be deprecated soon after Microsoft fully supports rt_begin_c typedef struct { void* (*exchange_ptr)(volatile void** a, void* v); // retuns previous value int32_t (*increment_int32)(volatile int32_t* a); // returns incremented int32_t (*decrement_int32)(volatile int32_t* a); // returns decremented int64_t (*increment_int64)(volatile int64_t* a); // returns incremented int64_t (*decrement_int64)(volatile int64_t* a); // returns decremented int32_t (*add_int32)(volatile int32_t* a, int32_t v); // returns result of add int64_t (*add_int64)(volatile int64_t* a, int64_t v); // returns result of add // returns the value held previously by "a" address: int32_t (*exchange_int32)(volatile int32_t* a, int32_t v); int64_t (*exchange_int64)(volatile int64_t* a, int64_t v); // compare_exchange functions compare the *a value with the comparand value. // If the *a is equal to the comparand value, the "v" value is stored in the address // specified by "a" otherwise, no operation is performed. // returns true if previous value *a was the same as "comparand" // false if *a was different from "comparand" and "a" was NOT modified. bool (*compare_exchange_int64)(volatile int64_t* a, int64_t comparand, int64_t v); bool (*compare_exchange_int32)(volatile int32_t* a, int32_t comparand, int32_t v); bool (*compare_exchange_ptr)(volatile void** a, void* comparand, void* v); void (*spinlock_acquire)(volatile int64_t* spinlock); void (*spinlock_release)(volatile int64_t* spinlock); int32_t (*load32)(volatile int32_t* a); int64_t (*load64)(volatile int64_t* a); void (*memory_fence)(void); void (*test)(void); } rt_atomics_if; extern rt_atomics_if rt_atomics; rt_end_c ================================================ FILE: inc/rt/rt_backtrace.h ================================================ #pragma once #include "rt/rt_std.h" // "bt" stands for Stack Back Trace (not British Telecom) rt_begin_c enum { rt_backtrace_max_depth = 32 }; // increase if not enough enum { rt_backtrace_max_symbol = 1024 }; // MSFT symbol size limit typedef struct thread_s* rt_thread_t; typedef char rt_backtrace_symbol_t[rt_backtrace_max_symbol]; typedef char rt_backtrace_file_t[512]; typedef struct rt_backtrace_s { int32_t frames; // 0 if capture() failed uint32_t hash; errno_t error; // last error set by capture() or symbolize() void* stack[rt_backtrace_max_depth]; rt_backtrace_symbol_t symbol[rt_backtrace_max_depth]; rt_backtrace_file_t file[rt_backtrace_max_depth]; int32_t line[rt_backtrace_max_depth]; bool symbolized; } rt_backtrace_t; // calling .trace(bt, /*stop:*/"*") // stops backtrace dumping at any of the known Microsoft runtime // symbols: // "main", // "WinMain", // "BaseThreadInitThunk", // "RtlUserThreadStart", // "mainCRTStartup", // "WinMainCRTStartup", // "invoke_main" // .trace(bt, null) // provides complete backtrace to the bottom of stack typedef struct { void (*capture)(rt_backtrace_t *bt, int32_t skip); // number of frames to skip void (*context)(rt_thread_t thread, const void* context, rt_backtrace_t *bt); void (*symbolize)(rt_backtrace_t *bt); // dump backtrace into rt_println(): void (*trace)(const rt_backtrace_t* bt, const char* stop); void (*trace_self)(const char* stop); void (*trace_all_but_self)(void); // trace all threads const char* (*string)(const rt_backtrace_t* bt, char* text, int32_t count); void (*test)(void); } rt_backtrace_if; extern rt_backtrace_if rt_backtrace; #define rt_backtrace_here() do { \ rt_backtrace_t bt_ = {0}; \ rt_backtrace.capture(&bt_, 0); \ rt_backtrace.symbolize(&bt_); \ rt_backtrace.trace(&bt_, "*"); \ } while (0) rt_end_c ================================================ FILE: inc/rt/rt_clipboard.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct ui_bitmap_s ui_bitmap_t; typedef struct { errno_t (*put_text)(const char* s); errno_t (*get_text)(char* text, int32_t* bytes); errno_t (*put_image)(ui_bitmap_t* image); // only for Windows apps void (*test)(void); } rt_clipboard_if; extern rt_clipboard_if rt_clipboard; rt_end_c ================================================ FILE: inc/rt/rt_clock.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { int32_t const nsec_in_usec; // nano in micro second int32_t const nsec_in_msec; // nano in milli int32_t const nsec_in_sec; int32_t const usec_in_msec; // micro in milli int32_t const msec_in_sec; // milli in sec int32_t const usec_in_sec; // micro in sec fp64_t (*seconds)(void); // since boot uint64_t (*nanoseconds)(void); // since boot overflows in about 584.5 years uint64_t (*unix_microseconds)(void); // since January 1, 1970 uint64_t (*unix_seconds)(void); // since January 1, 1970 uint64_t (*microseconds)(void); // NOT monotonic(!) UTC since epoch January 1, 1601 uint64_t (*localtime)(void); // local time microseconds since epoch void (*utc)(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc); void (*local)(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc); void (*test)(void); } rt_clock_if; extern rt_clock_if rt_clock; rt_end_c ================================================ FILE: inc/rt/rt_config.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // Persistent storage for configuration and other small data // related to specific application. // on Unix-like system ~/.name/key files are used. // On Window User registry (could be .dot files/folders). // "name" is customary basename of "rt_args.v[0]" typedef struct { errno_t (*save)(const char* name, const char* key, const void* data, int32_t bytes); int32_t (*size)(const char* name, const char* key); // load() returns number of actual loaded bytes: int32_t (*load)(const char* name, const char* key, void* data, int32_t bytes); errno_t (*remove)(const char* name, const char* key); errno_t (*clean)(const char* name); // remove all subkeys void (*test)(void); } rt_config_if; extern rt_config_if rt_config; rt_end_c ================================================ FILE: inc/rt/rt_core.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { errno_t (*err)(void); // errno or GetLastError() void (*set_err)(errno_t err); // errno = err or SetLastError() void (*abort)(void); void (*exit)(int32_t exit_code); // only 8 bits on posix void (*test)(void); struct { // posix errno_t const access_denied; // EACCES errno_t const bad_file; // EBADF errno_t const broken_pipe; // EPIPE errno_t const device_not_ready; // ENXIO errno_t const directory_not_empty; // ENOTEMPTY errno_t const disk_full; // ENOSPC errno_t const file_exists; // EEXIST errno_t const file_not_found; // ENOENT errno_t const insufficient_buffer; // E2BIG errno_t const interrupted; // EINTR errno_t const invalid_data; // EINVAL errno_t const invalid_handle; // EBADF errno_t const invalid_parameter; // EINVAL errno_t const io_error; // EIO errno_t const more_data; // ENOBUFS errno_t const name_too_long; // ENAMETOOLONG errno_t const no_child_process; // ECHILD errno_t const not_a_directory; // ENOTDIR errno_t const not_empty; // ENOTEMPTY errno_t const out_of_memory; // ENOMEM errno_t const path_not_found; // ENOENT errno_t const pipe_not_connected; // EPIPE errno_t const read_only_file; // EROFS errno_t const resource_deadlock; // EDEADLK errno_t const too_many_open_files; // EMFILE } const error; } rt_core_if; extern rt_core_if rt_core; rt_end_c ================================================ FILE: inc/rt/rt_debug.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // debug interface essentially is: // vfprintf(stderr, format, va) // fprintf(stderr, format, ...) // with the additional convience: // 1. writing to system log (e.g. OutputDebugString() on Windows) // 2. always appending \n at the end of the line and thus flushing buffer // Warning: on Windows it is not real-time and subject to 30ms delays // that may or may not happen on some calls typedef struct { int32_t level; // global verbosity (interpretation may vary) int32_t const quiet; // 0 int32_t const info; // 1 basic information (errors and warnings) int32_t const verbose; // 2 detailed diagnostic int32_t const debug; // 3 including debug messages int32_t const trace; // 4 everything (may include nested calls) } rt_verbosity_if; typedef struct { rt_verbosity_if verbosity; int32_t (*verbosity_from_string)(const char* s); // "T" connector for outside write; return false to proceed with default bool (*tee)(const char* s, int32_t count); // return true to intercept void (*output)(const char* s, int32_t count); void (*println_va)(const char* file, int32_t line, const char* func, const char* format, va_list va); void (*println)(const char* file, int32_t line, const char* func, const char* format, ...); void (*perrno)(const char* file, int32_t line, const char* func, int32_t err_no, const char* format, ...); void (*perror)(const char* file, int32_t line, const char* func, int32_t error, const char* format, ...); bool (*is_debugger_present)(void); void (*breakpoint)(void); // no-op if debugger is not present errno_t (*raise)(uint32_t exception); struct { uint32_t const access_violation; uint32_t const datatype_misalignment; uint32_t const breakpoint; uint32_t const single_step; uint32_t const array_bounds; uint32_t const float_denormal_operand; uint32_t const float_divide_by_zero; uint32_t const float_inexact_result; uint32_t const float_invalid_operation; uint32_t const float_overflow; uint32_t const float_stack_check; uint32_t const float_underflow; uint32_t const int_divide_by_zero; uint32_t const int_overflow; uint32_t const priv_instruction; uint32_t const in_page_error; uint32_t const illegal_instruction; uint32_t const noncontinuable; uint32_t const stack_overflow; uint32_t const invalid_disposition; uint32_t const guard_page; uint32_t const invalid_handle; uint32_t const possible_deadlock; } exception; void (*test)(void); } rt_debug_if; #define rt_println(...) rt_debug.println(__FILE__, __LINE__, __func__, "" __VA_ARGS__) extern rt_debug_if rt_debug; rt_end_c ================================================ FILE: inc/rt/rt_files.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // space for "short" 260 utf-16 characters filename in utf-8 format: typedef struct rt_file_name_s { char s[1024]; } rt_file_name_t; enum { rt_files_max_path = (int32_t)sizeof(rt_file_name_t) }; // *) typedef struct rt_file_s rt_file_t; typedef struct rt_files_stat_s { uint64_t created; uint64_t accessed; uint64_t updated; int64_t size; // bytes int64_t type; // device / folder / symlink } rt_files_stat_t; typedef struct rt_folder_s { uint8_t data[512]; // implementation specific } rt_folder_t; typedef struct { rt_file_t* const invalid; // (rt_file_t*)-1 // rt_files_stat_t.type: int32_t const type_folder; int32_t const type_symlink; int32_t const type_device; // seek() methods: int32_t const seek_set; int32_t const seek_cur; int32_t const seek_end; // open() flags int32_t const o_rd; // read only int32_t const o_wr; // write only int32_t const o_rw; // != (o_rd | o_wr) int32_t const o_append; int32_t const o_create; // opens existing or creates new int32_t const o_excl; // create fails if file exists int32_t const o_trunc; // open always truncates to empty int32_t const o_sync; // known folders ids: struct { // known folders: int32_t const home; // "c:\Users\" or "/users/" int32_t const desktop; int32_t const documents; int32_t const downloads; int32_t const music; int32_t const pictures; int32_t const videos; int32_t const shared; // "c:\Users\Public" int32_t const bin; // "c:\Program Files" aka "/bin" aka "/Applications" int32_t const data; // "c:\ProgramData" aka "/var" aka "/data" } const folder; errno_t (*open)(rt_file_t* *file, const char* filename, int32_t flags); bool (*is_valid)(rt_file_t* file); // checks both null and invalid errno_t (*seek)(rt_file_t* file, int64_t *position, int32_t method); errno_t (*stat)(rt_file_t* file, rt_files_stat_t* stat, bool follow_symlink); errno_t (*read)(rt_file_t* file, void* data, int64_t bytes, int64_t *transferred); errno_t (*write)(rt_file_t* file, const void* data, int64_t bytes, int64_t *transferred); errno_t (*flush)(rt_file_t* file); void (*close)(rt_file_t* file); errno_t (*write_fully)(const char* filename, const void* data, int64_t bytes, int64_t *transferred); bool (*exists)(const char* pathname); // does not guarantee any access writes bool (*is_folder)(const char* pathname); bool (*is_symlink)(const char* pathname); const char* (*basename)(const char* pathname); // c:\foo\bar.ext -> bar.ext errno_t (*mkdirs)(const char* pathname); // tries to deep create all folders in pathname errno_t (*rmdirs)(const char* pathname); // tries to remove folder and its subtree errno_t (*create_tmp)(char* file, int32_t count); // create temporary file errno_t (*chmod777)(const char* pathname); // and whole subtree new files and folders errno_t (*symlink)(const char* from, const char* to); // sym link "ln -s" **) errno_t (*link)(const char* from, const char* to); // hard link like "ln" errno_t (*unlink)(const char* pathname); // delete file or empty folder errno_t (*copy)(const char* from, const char* to); // allows overwriting errno_t (*move)(const char* from, const char* to); // allows overwriting errno_t (*cwd)(char* folder, int32_t count); // get current working dir errno_t (*chdir)(const char* folder); // set working directory const char* (*known_folder)(int32_t kf_id); const char* (*bin)(void); // Windows: "c:\Program Files" Un*x: "/bin" const char* (*data)(void); // Windows: "c:\ProgramData" Un*x: /data or /var const char* (*tmp)(void); // temporary folder (system or user) // There are better, native, higher performance ways to iterate thru // folders in Posix, Linux and Windows. The following is minimalistic // approach to folder content reading: errno_t (*opendir)(rt_folder_t* folder, const char* folder_name); const char* (*readdir)(rt_folder_t* folder, rt_files_stat_t* optional); void (*closedir)(rt_folder_t* folder); void (*test)(void); } rt_files_if; // *) rt_files_max_path is a compromise - way longer than Windows MAX_PATH of 260 // and somewhat shorter than 32 * 1024 Windows long path. // Use with caution understanding that it is a limitation and where it is // important heap may and should be used. Do not to rely on thread stack size // (default: 1MB on Windows, Android Linux 64KB, 512 KB on MacOS, Ubuntu 8MB) // // **) symlink on Win32 is only allowed in Admin elevated // processes and in Developer mode. extern rt_files_if rt_files; rt_end_c ================================================ FILE: inc/rt/rt_glyphs.h ================================================ #pragma once #include "rt/rt_std.h" // Square Four Corners https://www.compart.com/en/unicode/U+26F6 #define rt_glyph_square_four_corners "\xE2\x9B\xB6" // Circled Cross Formee // https://codepoints.net/U+1F902 #define rt_glyph_circled_cross_formee "\xF0\x9F\xA4\x82" // White Large Square https://www.compart.com/en/unicode/U+2B1C #define rt_glyph_white_large_square "\xE2\xAC\x9C" // N-Ary Times Operator https://www.compart.com/en/unicode/U+2A09 #define rt_glyph_n_ary_times_operator "\xE2\xA8\x89" // Heavy Minus Sign https://www.compart.com/en/unicode/U+2796 #define rt_glyph_heavy_minus_sign "\xE2\x9E\x96" // Heavy Plus Sign https://www.compart.com/en/unicode/U+2795 #define rt_glyph_heavy_plus_sign "\xE2\x9E\x95" // Clockwise Rightwards and Leftwards Open Circle Arrows with Circled One Overlay // https://www.compart.com/en/unicode/U+1F502 // rt_glyph_clockwise_rightwards_and_leftwards_open_circle_arrows_with_circled_one_overlay... #define rt_glyph_open_circle_arrows_one_overlay "\xF0\x9F\x94\x82" // Halfwidth Katakana-Hiragana Prolonged Sound Mark https://www.compart.com/en/unicode/U+FF70 #define rt_glyph_prolonged_sound_mark "\xEF\xBD\xB0" // Fullwidth Plus Sign https://www.compart.com/en/unicode/U+FF0B #define rt_glyph_fullwidth_plus_sign "\xEF\xBC\x8B" // Fullwidth Hyphen-Minus https://www.compart.com/en/unicode/U+FF0D #define rt_glyph_fullwidth_hyphen_minus "\xEF\xBC\x8D" // Heavy Multiplication X https://www.compart.com/en/unicode/U+2716 #define rt_glyph_heavy_multiplication_x "\xE2\x9C\x96" // Multiplication Sign https://www.compart.com/en/unicode/U+00D7 #define rt_glyph_multiplication_sign "\xC3\x97" // Trigram For Heaven (caption menu button) https://www.compart.com/en/unicode/U+2630 #define rt_glyph_trigram_for_heaven "\xE2\x98\xB0" // (tool bar drag handle like: msvc toolbars) // Braille Pattern Dots-12345678 https://www.compart.com/en/unicode/U+28FF #define rt_glyph_braille_pattern_dots_12345678 "\xE2\xA3\xBF" // White Square Containing Black Medium Square // https://compart.com/en/unicode/U+1F795 #define rt_glyph_white_square_containing_black_medium_square "\xF0\x9F\x9E\x95" // White Medium Square // https://compart.com/en/unicode/U+25FB #define rt_glyph_white_medium_square "\xE2\x97\xBB" // White Square with Upper Right Quadrant // https://compart.com/en/unicode/U+25F3 #define rt_glyph_white_square_with_upper_right_quadrant "\xE2\x97\xB3" // White Square with Upper Left Quadrant https://www.compart.com/en/unicode/U+25F0 #define rt_glyph_white_square_with_upper_left_quadrant "\xE2\x97\xB0" // White Square with Lower Left Quadrant https://www.compart.com/en/unicode/U+25F1 #define rt_glyph_white_square_with_lower_left_quadrant "\xE2\x97\xB1" // White Square with Lower Right Quadrant https://www.compart.com/en/unicode/U+25F2 #define rt_glyph_white_square_with_lower_right_quadrant "\xE2\x97\xB2" // White Square with Upper Right Quadrant https://www.compart.com/en/unicode/U+25F3 #define rt_glyph_white_square_with_upper_right_quadrant "\xE2\x97\xB3" // White Square with Vertical Bisecting Line https://www.compart.com/en/unicode/U+25EB #define rt_glyph_white_square_with_vertical_bisecting_line "\xE2\x97\xAB" // (White Square with Horizontal Bisecting Line) // Squared Minus https://www.compart.com/en/unicode/U+229F #define rt_glyph_squared_minus "\xE2\x8A\x9F" // North East and South West Arrow https://www.compart.com/en/unicode/U+2922 #define rt_glyph_north_east_and_south_west_arrow "\xE2\xA4\xA2" // South East Arrow to Corner https://www.compart.com/en/unicode/U+21F2 #define rt_glyph_south_east_white_arrow_to_corner "\xE2\x87\xB2" // North West Arrow to Corner https://www.compart.com/en/unicode/U+21F1 #define rt_glyph_north_west_white_arrow_to_corner "\xE2\x87\xB1" // Leftwards Arrow to Bar https://www.compart.com/en/unicode/U+21E6 #define rt_glyph_leftwards_white_arrow_to_bar "\xE2\x87\xA6" // Rightwards Arrow to Bar https://www.compart.com/en/unicode/U+21E8 #define rt_glyph_rightwards_white_arrow_to_bar "\xE2\x87\xA8" // Upwards White Arrow https://www.compart.com/en/unicode/U+21E7 #define rt_glyph_upwards_white_arrow "\xE2\x87\xA7" // Downwards White Arrow https://www.compart.com/en/unicode/U+21E9 #define rt_glyph_downwards_white_arrow "\xE2\x87\xA9" // Leftwards White Arrow https://www.compart.com/en/unicode/U+21E4 #define rt_glyph_leftwards_white_arrow "\xE2\x87\xA4" // Rightwards White Arrow https://www.compart.com/en/unicode/U+21E5 #define rt_glyph_rightwards_white_arrow "\xE2\x87\xA5" // Upwards White Arrow on Pedestal https://www.compart.com/en/unicode/U+21EB #define rt_glyph_upwards_white_arrow_on_pedestal "\xE2\x87\xAB" // Braille Pattern Dots-678 https://www.compart.com/en/unicode/U+28E0 #define rt_glyph_3_dots_tiny_right_bottom_triangle "\xE2\xA3\xA0" // Braille Pattern Dots-2345678 https://www.compart.com/en/unicode/U+28FE // Combining the two into: #define rt_glyph_dotted_right_bottom_triangle "\xE2\xA3\xA0\xE2\xA3\xBE" // Upper Right Drop-Shadowed White Square https://www.compart.com/en/unicode/U+2750 #define rt_glyph_upper_right_drop_shadowed_white_square "\xE2\x9D\x90" // No-Break Space (NBSP) // https://www.compart.com/en/unicode/U+00A0 #define rt_glyph_nbsp "\xC2\xA0" // Word Joiner (WJ) // https://compart.com/en/unicode/U+2060 #define rt_glyph_word_joiner "\xE2\x81\xA0" // Zero Width Space (ZWSP) // https://compart.com/en/unicode/U+200B #define rt_glyph_zwsp "\xE2\x80\x8B" // Zero Width Joiner (ZWJ) // https://compart.com/en/unicode/u+200D #define rt_glyph_zwj "\xE2\x80\x8D" // En Quad // https://compart.com/en/unicode/U+2000 #define rt_glyph_en_quad "\xE2\x80\x80" // Em Quad // https://compart.com/en/unicode/U+2001 #define rt_glyph_em_quad "\xE2\x80\x81" // En Space // https://compart.com/en/unicode/U+2002 #define rt_glyph_en_space "\xE2\x80\x82" // Em Space // https://compart.com/en/unicode/U+2003 #define rt_glyph_em_space "\xE2\x80\x83" // Hyphen https://www.compart.com/en/unicode/U+2010 #define rt_glyph_hyphen "\xE2\x80\x90" // Non-Breaking Hyphen https://www.compart.com/en/unicode/U+2011 #define rt_glyph_non_breaking_hyphen "\xE2\x80\x91" // Fullwidth Low Line https://www.compart.com/en/unicode/U+FF3F #define rt_glyph_fullwidth_low_line "\xEF\xBC\xBF" // #define rt_glyph_light_horizontal "\xE2\x94\x80" // Light Horizontal https://www.compart.com/en/unicode/U+2500 #define rt_glyph_light_horizontal "\xE2\x94\x80" // Three-Em Dash https://www.compart.com/en/unicode/U+2E3B #define rt_glyph_three_em_dash "\xE2\xB8\xBB" // Infinity https://www.compart.com/en/unicode/U+221E #define rt_glyph_infinity "\xE2\x88\x9E" // Black Large Circle https://www.compart.com/en/unicode/U+2B24 #define rt_glyph_black_large_circle "\xE2\xAC\xA4" // Large Circle https://www.compart.com/en/unicode/U+25EF #define rt_glyph_large_circle "\xE2\x97\xAF" // Heavy Leftwards Arrow with Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F818 #define rt_glyph_heavy_leftwards_arrow_with_equilateral_arrowhead "\xF0\x9F\xA0\x98" // Heavy Rightwards Arrow with Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81A #define rt_glyph_heavy_rightwards_arrow_with_equilateral_arrowhead "\xF0\x9F\xA0\x9A" // Heavy Leftwards Arrow with Large Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81C #define rt_glyph_heavy_leftwards_arrow_with_large_equilateral_arrowhead "\xF0\x9F\xA0\x9C" // Heavy Rightwards Arrow with Large Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81E #define rt_glyph_heavy_rightwards_arrow_with_large_equilateral_arrowhead "\xF0\x9F\xA0\x9E" // CJK Unified Ideograph-5973: Kanji Onna "Female" https://www.compart.com/en/unicode/U+5973 #define rt_glyph_kanji_onna_female "\xE2\xBC\xA5" // Leftwards Arrow https://www.compart.com/en/unicode/U+2190 #define rt_glyph_leftward_arrow "\xE2\x86\x90" // Upwards Arrow https://www.compart.com/en/unicode/U+2191 #define rt_glyph_upwards_arrow "\xE2\x86\x91" // Rightwards Arrow // https://www.compart.com/en/unicode/U+2192 #define rt_glyph_rightwards_arrow "\xE2\x86\x92" // Downwards Arrow https://www.compart.com/en/unicode/U+2193 #define rt_glyph_downwards_arrow "\xE2\x86\x93" // Thin Space https://www.compart.com/en/unicode/U+2009 #define rt_glyph_thin_space "\xE2\x80\x89" // Medium Mathematical Space (MMSP) https://www.compart.com/en/unicode/U+205F #define rt_glyph_mmsp "\xE2\x81\x9F" // Three-Per-Em Space https://www.compart.com/en/unicode/U+2004 #define rt_glyph_three_per_em "\xE2\x80\x84" // Six-Per-Em Space https://www.compart.com/en/unicode/U+2006 #define rt_glyph_six_per_em "\xE2\x80\x86" // Punctuation Space https://www.compart.com/en/unicode/U+2008 #define rt_glyph_punctuation "\xE2\x80\x88" // Hair Space https://www.compart.com/en/unicode/U+200A #define rt_glyph_hair_space "\xE2\x80\x8A" // Chinese "jin4" https://www.compart.com/en/unicode/U+58F9 #define rt_glyph_chinese_jin4 "\xE5\xA3\xB9" // Chinese "gong" https://www.compart.com/en/unicode/U+8D70 #define rt_glyph_chinese_gong "\xE8\xB5\xB0" // https://www.compart.com/en/unicode/U+1F9F8 #define rt_glyph_teddy_bear "\xF0\x9F\xA7\xB8" // https://www.compart.com/en/unicode/U+1F9CA #define rt_glyph_ice_cube "\xF0\x9F\xA7\x8A" // Speaker https://www.compart.com/en/unicode/U+1F508 #define rt_glyph_speaker "\xF0\x9F\x94\x88" // Speaker with Cancellation Stroke https://www.compart.com/en/unicode/U+1F507 #define rt_glyph_mute "\xF0\x9F\x94\x87" // TODO: this is used for Font Metric Visualization // Full Block https://www.compart.com/en/unicode/U+2588 #define rt_glyph_full_block "\xE2\x96\x88" // Black Square https://www.compart.com/en/unicode/U+25A0 #define rt_glyph_black_square "\xE2\x96\xA0" // the appearance of a dragon walking // CJK Unified Ideograph-9F98 https://www.compart.com/en/unicode/U+9F98 #define rt_glyph_walking_dragon "\xE9\xBE\x98" // possibly highest "diacritical marks" character (Vietnamese) // Latin Small Letter U with Horn and Hook Above https://www.compart.com/en/unicode/U+1EED #define rt_glyph_u_with_horn_and_hook_above "\xC7\xAD" // possibly "long descender" character // Latin Small Letter Qp Digraph https://www.compart.com/en/unicode/U+0239 #define rt_glyph_qp_digraph "\xC9\xB9" // another possibly "long descender" character // Cyrillic Small Letter Shha with Descender https://www.compart.com/en/unicode/U+0527 #define rt_glyph_shha_with_descender "\xD4\xA7" // a"very long descender" character // Tibetan Mark Caret Yig Mgo Phur Shad Ma https://www.compart.com/en/unicode/U+0F06 #define rt_glyph_caret_yig_mgo_phur_shad_ma "\xE0\xBC\x86" // Tibetan Vowel Sign Vocalic Ll https://www.compart.com/en/unicode/U+0F79 #define rt_glyph_vocalic_ll "\xE0\xBD\xB9" // https://www.compart.com/en/unicode/U+1F4A3 #define rt_glyph_bomb "\xF0\x9F\x92\xA3" // https://www.compart.com/en/unicode/U+1F4A1 #define rt_glyph_electric_light_bulb "\xF0\x9F\x92\xA1" // https://www.compart.com/en/unicode/U+1F4E2 #define rt_glyph_public_address_loudspeaker "\xF0\x9F\x93\xA2" // https://www.compart.com/en/unicode/U+1F517 #define rt_glyph_link_symbol "\xF0\x9F\x94\x97" // https://www.compart.com/en/unicode/U+1F571 #define rt_glyph_black_skull_and_crossbones "\xF0\x9F\x95\xB1" // https://www.compart.com/en/unicode/U+1F5B5 #define rt_glyph_screen "\xF0\x9F\x96\xB5" // https://www.compart.com/en/unicode/U+1F5D7 #define rt_glyph_overlap "\xF0\x9F\x97\x97" // https://www.compart.com/en/unicode/U+1F5D6 #define rt_glyph_maximize "\xF0\x9F\x97\x96" // https://www.compart.com/en/unicode/U+1F5D5 #define rt_glyph_minimize "\xF0\x9F\x97\x95" // Desktop Window // https://compart.com/en/unicode/U+1F5D4 #define rt_glyph_desktop_window "\xF0\x9F\x97\x94" // https://www.compart.com/en/unicode/U+1F5D9 #define rt_glyph_cancellation_x "\xF0\x9F\x97\x99" // https://www.compart.com/en/unicode/U+1F5DF #define rt_glyph_page_with_circled_text "\xF0\x9F\x97\x9F" // https://www.compart.com/en/unicode/U+1F533 #define rt_glyph_white_square_button "\xF0\x9F\x94\xB3" // https://www.compart.com/en/unicode/U+1F532 #define rt_glyph_black_square_button "\xF0\x9F\x94\xB2" // https://www.compart.com/en/unicode/U+1F5F9 #define rt_glyph_ballot_box_with_bold_check "\xF0\x9F\x97\xB9" // https://www.compart.com/en/unicode/U+1F5F8 #define rt_glyph_light_check_mark "\xF0\x9F\x97\xB8" // https://compart.com/en/unicode/U+1F4BB #define rt_glyph_personal_computer "\xF0\x9F\x92\xBB" // https://compart.com/en/unicode/U+1F4DC #define rt_glyph_desktop_computer "\xF0\x9F\x93\x9C" // https://compart.com/en/unicode/U+1F4DD #define rt_glyph_printer "\xF0\x9F\x93\x9D" // https://compart.com/en/unicode/U+1F4F9 #define rt_glyph_video_camera "\xF0\x9F\x93\xB9" // https://compart.com/en/unicode/U+1F4F8 #define rt_glyph_camera "\xF0\x9F\x93\xB8" // https://compart.com/en/unicode/U+1F505 #define rt_glyph_high_brightness "\xF0\x9F\x94\x85" // https://compart.com/en/unicode/U+1F506 #define rt_glyph_low_brightness "\xF0\x9F\x94\x86" // https://compart.com/en/unicode/U+1F507 #define rt_glyph_speaker_with_cancellation_stroke "\xF0\x9F\x94\x87" // https://compart.com/en/unicode/U+1F509 #define rt_glyph_speaker_with_one_sound_wave "\xF0\x9F\x94\x89" // Right-Pointing Magnifying Glass // https://compart.com/en/unicode/U+1F50E #define rt_glyph_right_pointing_magnifying_glass "\xF0\x9F\x94\x8E" // Radio Button // https://compart.com/en/unicode/U+1F518 #define rt_glyph_radio_button "\xF0\x9F\x94\x98" // https://compart.com/en/unicode/U+1F525 #define rt_glyph_fire "\xF0\x9F\x94\xA5" // Gear // https://compart.com/en/unicode/U+2699 #define rt_glyph_gear "\xE2\x9A\x99" // Nut and Bolt // https://compart.com/en/unicode/U+1F529 #define rt_glyph_nut_and_bolt "\xF0\x9F\x94\xA9" // Hammer and Wrench // https://compart.com/en/unicode/U+1F6E0 #define rt_glyph_hammer_and_wrench "\xF0\x9F\x9B\xA0" // https://compart.com/en/unicode/U+1F53E #define rt_glyph_upwards_button "\xF0\x9F\x94\xBE" // https://compart.com/en/unicode/U+1F53F #define rt_glyph_downwards_button "\xF0\x9F\x94\xBF" // https://compart.com/en/unicode/U+1F5C7 #define rt_glyph_litter_in_bin_sign "\xF0\x9F\x97\x87" // Checker Board // https://compart.com/en/unicode/U+1F67E #define rt_glyph_checker_board "\xF0\x9F\x9A\xBE" // Reverse Checker Board // https://compart.com/en/unicode/U+1F67F #define rt_glyph_reverse_checker_board "\xF0\x9F\x9A\xBF" // Clipboard // https://compart.com/en/unicode/U+1F4CB #define rt_glyph_clipboard "\xF0\x9F\x93\x8B" // Two Joined Squares https://www.compart.com/en/unicode/U+29C9 #define rt_glyph_two_joined_squares "\xE2\xA7\x89" // White Heavy Check Mark // https://compart.com/en/unicode/U+2705 #define rt_glyph_white_heavy_check_mark "\xE2\x9C\x85" // Negative Squared Cross Mark // https://compart.com/en/unicode/U+274E #define rt_glyph_negative_squared_cross_mark "\xE2\x9D\x8E" // Lower Right Drop-Shadowed White Square // https://compart.com/en/unicode/U+274F #define rt_glyph_lower_right_drop_shadowed_white_square "\xE2\x9D\x8F" // Upper Right Drop-Shadowed White Square // https://compart.com/en/unicode/U+2750 #define rt_glyph_upper_right_drop_shadowed_white_square "\xE2\x9D\x90" // Lower Right Shadowed White Square // https://compart.com/en/unicode/U+2751 #define rt_glyph_lower_right_shadowed_white_square "\xE2\x9D\x91" // Upper Right Shadowed White Square // https://compart.com/en/unicode/U+2752 #define rt_glyph_upper_right_shadowed_white_square "\xE2\x9D\x92" // Left Double Wiggly Fence // https://compart.com/en/unicode/U+29DA #define rt_glyph_left_double_wiggly_fence "\xE2\xA7\x9A" // Right Double Wiggly Fence // https://compart.com/en/unicode/U+29DB #define rt_glyph_right_double_wiggly_fence "\xE2\xA7\x9B" // Logical Or // https://compart.com/en/unicode/U+2228 #define rt_glyph_logical_or "\xE2\x88\xA8" // Logical And // https://compart.com/en/unicode/U+2227 #define rt_glyph_logical_and "\xE2\x88\xA7" // Double Vertical Bar (Pause) // https://compart.com/en/unicode/U+23F8 #define rt_glyph_double_vertical_bar "\xE2\x8F\xB8" // Black Square For Stop // https://compart.com/en/unicode/U+23F9 #define rt_glyph_black_square_for_stop "\xE2\x8F\xB9" // Black Circle For Record // https://compart.com/en/unicode/U+23FA #define rt_glyph_black_circle_for_record "\xE2\x8F\xBA" // Negative Squared Latin Capital Letter "I" // https://compart.com/en/unicode/U+1F158 #define rt_glyph_negative_squared_latin_capital_letter_i "\xF0\x9F\x85\x98" #define rt_glyph_info rt_glyph_negative_squared_latin_capital_letter_i // Circled Information Source // https://compart.com/en/unicode/U+1F6C8 #define rt_glyph_circled_information_source "\xF0\x9F\x9B\x88" // Information Source // https://compart.com/en/unicode/U+2139 #define rt_glyph_information_source "\xE2\x84\xB9" // Squared Cool // https://compart.com/en/unicode/U+1F192 #define rt_glyph_squared_cool "\xF0\x9F\x86\x92" // Squared OK // https://compart.com/en/unicode/U+1F197 #define rt_glyph_squared_ok "\xF0\x9F\x86\x97" // Squared Free // https://compart.com/en/unicode/U+1F193 #define rt_glyph_squared_free "\xF0\x9F\x86\x93" // Squared New // https://compart.com/en/unicode/U+1F195 #define rt_glyph_squared_new "\xF0\x9F\x86\x95" // Lady Beetle // https://compart.com/en/unicode/U+1F41E #define rt_glyph_lady_beetle "\xF0\x9F\x90\x9E" // Brain // https://compart.com/en/unicode/U+1F9E0 #define rt_glyph_brain "\xF0\x9F\xA7\xA0" // South West Arrow with Hook // https://www.compart.com/en/unicode/U+2926 #define rt_glyph_south_west_arrow_with_hook "\xE2\xA4\xA6" // North West Arrow with Hook // https://www.compart.com/en/unicode/U+2923 #define rt_glyph_north_west_arrow_with_hook "\xE2\xA4\xA3" // White Sun with Rays // https://www.compart.com/en/unicode/U+263C #define rt_glyph_white_sun_with_rays "\xE2\x98\xBC" // Black Sun with Rays // https://www.compart.com/en/unicode/U+2600 #define rt_glyph_black_sun_with_rays "\xE2\x98\x80" // Sun Behind Cloud // https://www.compart.com/en/unicode/U+26C5 #define rt_glyph_sun_behind_cloud "\xE2\x9B\x85" // White Sun // https://www.compart.com/en/unicode/U+1F323 #define rt_glyph_white_sun "\xF0\x9F\x8C\xA3" // Crescent Moon // https://www.compart.com/en/unicode/U+1F319 #define rt_glyph_crescent_moon "\xF0\x9F\x8C\x99" // Latin Capital Letter E with Cedilla and Breve // https://compart.com/en/unicode/U+1E1C #define rt_glyph_E_with_cedilla_and_breve "\xE1\xB8\x9C" // Box Drawings Heavy Vertical and Horizontal // https://compart.com/en/unicode/U+254B #define rt_glyph_box_drawings_heavy_vertical_and_horizontal "\xE2\x95\x8B" // Box Drawings Light Diagonal Cross // https://compart.com/en/unicode/U+2573 #define rt_glyph_box_drawings_light_diagonal_cross "\xE2\x95\xB3" // Combining Enclosing Square // https://compart.com/en/unicode/U+20DE #define rt_glyph_combining_enclosing_square "\xE2\x83\x9E" // Combining Enclosing Screen // https://compart.com/en/unicode/U+20E2 #define rt_glyph_combining_enclosing_screen "\xE2\x83\xA2" // Combining Enclosing Keycap // https://compart.com/en/unicode/U+20E3 #define rt_glyph_combining_enclosing_keycap "\xE2\x83\xA3" // Combining Enclosing Circle // https://compart.com/en/unicode/U+20DD #define rt_glyph_combining_enclosing_circle "\xE2\x83\x9D" // Frame with Picture // https://compart.com/en/unicode/U+1F5BC #define rt_glyph_frame_with_picture "\xF0\x9F\x96\xBC" // with emoji variation selector: "\xF0\x9F\x96\xBC\xEF\xB8\x8F" // Document with Picture // https://compart.com/en/unicode/U+1F5BB #define rt_glyph_document_with_picture "\xF0\x9F\x96\xBB" // Frame with Tiles // https://compart.com/en/unicode/U+1F5BD #define rt_glyph_frame_with_tiles "\xF0\x9F\x96\xBD" // Frame with an X // https://compart.com/en/unicode/U+1F5BE #define rt_glyph_frame_with_an_x "\xF0\x9F\x96\xBE" // Left Right Arrow // https://compart.com/en/unicode/U+2194 #define rt_glyph_left_right_arrow "\xE2\x86\x94" // Up Down Arrow // https://compart.com/en/unicode/U+2195 #define rt_glyph_up_down_arrow "\xE2\x86\x95" ================================================ FILE: inc/rt/rt_heap.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // It is absolutely OK to use posix compliant // malloc()/calloc()/realloc()/free() function calls with understanding // that they introduce serialization points in multi-threaded applications // and may be induce wait states that under pressure (all cores busy) may // result in prolonged wait which may not be acceptable for real time // processing pipelines. // // heap_if.functions may or may not be faster than malloc()/free() ... // // Some callers may find realloc() parameters more convenient to avoid // anti-pattern // void* reallocated = realloc(p, new_size); // if (reallocated != null) { p = reallocated; } // and avoid never ending discussion of legality and implementation // compliance of the situation: // realloc(p /* when p == null */, ...) // // zero: true initializes allocated or reallocated tail memory to 0x00 // be careful with zeroing heap memory. It will result in virtual // to physical memory mapping and may be expensive. typedef struct rt_heap_s rt_heap_t; typedef struct { // heap == null uses process serialized LFH errno_t (*alloc)(void* *a, int64_t bytes); errno_t (*alloc_zero)(void* *a, int64_t bytes); errno_t (*realloc)(void* *a, int64_t bytes); errno_t (*realloc_zero)(void* *a, int64_t bytes); void (*free)(void* a); // heaps: rt_heap_t* (*create)(bool serialized); errno_t (*allocate)(rt_heap_t* heap, void* *a, int64_t bytes, bool zero); // reallocate may return ERROR_OUTOFMEMORY w/o changing 'a' *) errno_t (*reallocate)(rt_heap_t* heap, void* *a, int64_t bytes, bool zero); void (*deallocate)(rt_heap_t* heap, void* a); int64_t (*bytes)(rt_heap_t* heap, void* a); // actual allocated size void (*dispose)(rt_heap_t* heap); void (*test)(void); } rt_heap_if; extern rt_heap_if rt_heap; // *) zero in reallocate applies to the newly appended bytes // On Windows rt_mem.heap is based on serialized LFH returned by GetProcessHeap() // https://learn.microsoft.com/en-us/windows/win32/memory/low-fragmentation-heap // threads can benefit from not serialized, not LFH if they allocate and free // memory in time critical loops. rt_end_c ================================================ FILE: inc/rt/rt_loader.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // see: // https://pubs.opengroup.org/onlinepubs/7908799/xsh/dlfcn.h.html typedef struct { // mode: int32_t const local; int32_t const lazy; int32_t const now; int32_t const global; // "If the value of file is null, dlopen() provides a handle on a global // symbol object." posix void* (*open)(const char* filename, int32_t mode); void* (*sym)(void* handle, const char* name); void (*close)(void* handle); void (*test)(void); } rt_loader_if; extern rt_loader_if rt_loader; rt_end_c ================================================ FILE: inc/rt/rt_mem.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { // whole file read only errno_t (*map_ro)(const char* filename, void** data, int64_t* bytes); // whole file read-write errno_t (*map_rw)(const char* filename, void** data, int64_t* bytes); void (*unmap)(void* data, int64_t bytes); // map_resource() maps data from resources, do NOT unmap! errno_t (*map_resource)(const char* label, void** data, int64_t* bytes); int32_t (*page_size)(void); // 4KB or 64KB on Windows int32_t (*large_page_size)(void); // 2MB on Windows // allocate() contiguous reserved virtual address range, // if possible committed to physical memory. // Memory guaranteed to be aligned to page boundary. // Memory is guaranteed to be initialized to zero on access. void* (*allocate)(int64_t bytes_multiple_of_page_size); void (*deallocate)(void* a, int64_t bytes_multiple_of_page_size); void (*test)(void); } rt_mem_if; extern rt_mem_if rt_mem; rt_end_c ================================================ FILE: inc/rt/rt_nls.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { // i18n national language support void (*init)(void); const char* (*locale)(void); // "en-US" "zh-CN" etc... // force locale for debugging and testing: errno_t (*set_locale)(const char* locale); // only for calling thread // nls(s) is same as string(strid(s), s) const char* (*str)(const char* defau1t); // returns localized string // strid("foo") returns -1 if there is no matching // ENGLISH NEUTRAL STRINGTABLE entry int32_t (*strid)(const char* s); // given strid > 0 returns localized string or default value const char* (*string)(int32_t strid, const char* defau1t); } rt_nls_if; extern rt_nls_if rt_nls; rt_end_c ================================================ FILE: inc/rt/rt_num.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct { uint64_t lo; uint64_t hi; } rt_num128_t; // uint128_t may be supported by compiler typedef struct { rt_num128_t (*add128)(const rt_num128_t a, const rt_num128_t b); rt_num128_t (*sub128)(const rt_num128_t a, const rt_num128_t b); rt_num128_t (*mul64x64)(uint64_t a, uint64_t b); uint64_t (*muldiv128)(uint64_t a, uint64_t b, uint64_t d); uint32_t (*gcd32)(uint32_t u, uint32_t v); // greatest common denominator // non-crypto strong pseudo-random number generators (thread safe) uint32_t (*random32)(uint32_t *state); // "Mulberry32" uint64_t (*random64)(uint64_t *state); // "Trust" // "FNV-1a" hash functions (if bytes == 0 expects zero terminated string) uint32_t (*hash32)(const char* s, int64_t bytes); uint64_t (*hash64)(const char* s, int64_t bytes); void (*test)(void); } rt_num_if; extern rt_num_if rt_num; rt_end_c ================================================ FILE: inc/rt/rt_processes.h ================================================ #pragma once #include "rt/rt_streams.h" rt_begin_c typedef struct { const char* command; rt_stream_if* in; rt_stream_if* out; rt_stream_if* err; uint32_t exit_code; fp64_t timeout; // seconds } rt_processes_child_t; // Process name may be an the executable filename with // full, partial or absent pathname. // Case insensitive on Windows. typedef struct { const char* (*name)(void); // argv[0] like but full path uint64_t (*pid)(const char* name); // 0 if process not found errno_t (*pids)(const char* name, uint64_t* pids/*[size]*/, int32_t size, int32_t *count); // return 0, error or ERROR_MORE_DATA errno_t (*nameof)(uint64_t pid, char* name, int32_t count); // pathname bool (*present)(uint64_t pid); errno_t (*kill)(uint64_t pid, fp64_t timeout_seconds); errno_t (*kill_all)(const char* name, fp64_t timeout_seconds); bool (*is_elevated)(void); // Is process running as root/ Admin / System? errno_t (*restart_elevated)(void); // retuns error or exits on success errno_t (*run)(rt_processes_child_t* child); errno_t (*popen)(const char* command, int32_t *xc, rt_stream_if* output, fp64_t timeout_seconds); // <= 0 infinite // popen() does NOT guarantee stream zero termination on errors errno_t (*spawn)(const char* command); // spawn fully detached process void (*test)(void); } rt_processes_if; extern rt_processes_if rt_processes; rt_end_c ================================================ FILE: inc/rt/rt_static.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // rt_static_init(unique_name) { code_to_execute_before_main } #if defined(_MSC_VER) #if defined(_WIN64) || defined(_M_X64) #define _msvc_symbol_prefix_ "" #else #define _msvc_symbol_prefix_ "_" #endif #pragma comment(linker, "/include:rt_force_symbol_reference") void* rt_force_symbol_reference(void* symbol); #define _msvc_ctor_(sym_prefix, func) \ void func(void); \ int32_t (* rt_array ## func)(void); \ int32_t func ## _wrapper(void); \ int32_t func ## _wrapper(void) { func(); \ rt_force_symbol_reference((void*)rt_array ## func); \ rt_force_symbol_reference((void*)func ## _wrapper); return 0; } \ extern int32_t (* rt_array ## func)(void); \ __pragma(comment(linker, "/include:" sym_prefix # func "_wrapper")) \ __pragma(section(".CRT$XCU", read)) \ __declspec(allocate(".CRT$XCU")) \ int32_t (* rt_array ## func)(void) = func ## _wrapper; #define rt_static_init2_(func, line) _msvc_ctor_(_msvc_symbol_prefix_, \ func ## _constructor_##line) \ void func ## _constructor_##line(void) #define rt_static_init1_(func, line) rt_static_init2_(func, line) #define rt_static_init(func) rt_static_init1_(func, __LINE__) #else #define rt_static_init(n) __attribute__((constructor)) \ static void _init_ ## n ## __LINE__ ## _ctor(void) #endif void rt_static_init_test(void); rt_end_c ================================================ FILE: inc/rt/rt_str.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct rt_str64_t { char s[64]; } rt_str64_t; typedef struct rt_str128_t { char s[128]; } rt_str128_t; typedef struct rt_str1024_t { char s[1024]; } rt_str1024_t; typedef struct rt_str32K_t { char s[32 * 1024]; } rt_str32K_t; // truncating string printf: // char s[100]; rt_str_printf(s, "Hello %s", "world"); // do not use with char* and char s[] parameters // because rt_countof(s) will be sizeof(char*) not the size of the buffer. #define rt_str_printf(s, ...) rt_str.format((s), rt_countof(s), "" __VA_ARGS__) #define rt_strerr(r) (rt_str.error((r)).s) // use only as rt_str_printf() parameter // The strings are expected to be UTF-8 encoded. // Copy functions fatal fail if the destination buffer is too small. // It is responsibility of the caller to make sure it won't happen. typedef struct { char* (*drop_const)(const char* s); // because of strstr() and alike int32_t (*len)(const char* s); int32_t (*len16)(const uint16_t* utf16); int32_t (*utf8bytes)(const char* utf8, int32_t bytes); // 0 on error int32_t (*glyphs)(const char* utf8, int32_t bytes); // -1 on error bool (*starts)(const char* s1, const char* s2); // s1 starts with s2 bool (*ends)(const char* s1, const char* s2); // s1 ends with s2 bool (*istarts)(const char* s1, const char* s2); // ignore case bool (*iends)(const char* s1, const char* s2); // ignore case // string truncation is fatal use strlen() to check at call site void (*lower)(char* d, int32_t capacity, const char* s); // ASCII only void (*upper)(char* d, int32_t capacity, const char* s); // ASCII only // utf8/utf16 conversion // If `chars` argument is -1, the function utf8_bytes includes the zero // terminating character in the conversion and the returned byte count. int32_t (*utf8_bytes)(const uint16_t* utf16, int32_t bytes); // bytes count // If `bytes` argument is -1, the function utf16_chars() includes the zero // terminating character in the conversion and the returned character count. int32_t (*utf16_chars)(const char* utf8, int32_t bytes); // chars count // utf8_bytes() and utf16_chars() return -1 on invalid UTF-8/UTF-16 // utf8_bytes(L"", -1) returns 1 for zero termination // utf16_chars("", -1) returns 1 for zero termination // chars: -1 means both source and destination are zero terminated errno_t (*utf16to8)(char* utf8, int32_t capacity, const uint16_t* utf16, int32_t chars); // bytes: -1 means both source and destination are zero terminated errno_t (*utf8to16)(uint16_t* utf16, int32_t capacity, const char* utf8, int32_t bytes); // https://compart.com/en/unicode/U+1F41E // Lady Beetle: utf16 L"\xD83D\xDC1E" utf8 "\xF0\x9F\x90\x9E" // surrogates: high low bool (*utf16_is_low_surrogate)(uint16_t utf16char); bool (*utf16_is_high_surrogate)(uint16_t utf16char); uint32_t (*utf32)(const char* utf8, int32_t bytes); // single codepoint // string formatting printf style: void (*format_va)(char* utf8, int32_t count, const char* format, va_list va); void (*format)(char* utf8, int32_t count, const char* format, ...); // format "dg" digit grouped; see below for known grouping separators: const char* (*grouping_separator)(void); // locale // Returned const char* pointer is short-living thread local and // intended to be used in the arguments list of .format() or .printf() // like functions, not stored or passed for prolonged call chains. // See implementation for details. rt_str64_t (*int64_dg)(int64_t v, bool uint, const char* gs); rt_str64_t (*int64)(int64_t v); // with UTF-8 thin space rt_str64_t (*uint64)(uint64_t v); // with UTF-8 thin space rt_str64_t (*int64_lc)(int64_t v); // with locale separator rt_str64_t (*uint64_lc)(uint64_t v); // with locale separator rt_str128_t (*fp)(const char* format, fp64_t v); // respects locale // errors to strings rt_str1024_t (*error)(int32_t error); // en-US rt_str1024_t (*error_nls)(int32_t error); // national locale string void (*test)(void); } rt_str_if; // Known grouping separators // https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping // coma "," separated decimal // other commonly used separators: // underscore "_" (Fortran, Kotlin) // apostrophe "'" (C++14, Rebol, Red) // backtick "`" // space "\x20" // thin_space "\xE2\x80\x89" Unicode: U+2009 extern rt_str_if rt_str; rt_end_c ================================================ FILE: inc/rt/rt_streams.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct rt_stream_if rt_stream_if; typedef struct rt_stream_if { errno_t (*read)(rt_stream_if* s, void* data, int64_t bytes, int64_t *transferred); errno_t (*write)(rt_stream_if* s, const void* data, int64_t bytes, int64_t *transferred); void (*close)(rt_stream_if* s); // optional } rt_stream_if; typedef struct { rt_stream_if stream; const void* data_read; int64_t bytes_read; int64_t pos_read; void* data_write; int64_t bytes_write; int64_t pos_write; } rt_stream_memory_if; typedef struct { void (*read_only)(rt_stream_memory_if* s, const void* data, int64_t bytes); void (*write_only)(rt_stream_memory_if* s, void* data, int64_t bytes); void (*read_write)(rt_stream_memory_if* s, const void* read, int64_t read_bytes, void* write, int64_t write_bytes); void (*test)(void); } rt_streams_if; extern rt_streams_if rt_streams; rt_end_c ================================================ FILE: inc/rt/rt_threads.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct rt_event_s* rt_event_t; typedef struct { rt_event_t (*create)(void); // never returns null rt_event_t (*create_manual)(void); // never returns null void (*set)(rt_event_t e); void (*reset)(rt_event_t e); void (*wait)(rt_event_t e); // returns 0 on success or -1 on timeout int32_t (*wait_or_timeout)(rt_event_t e, fp64_t seconds); // seconds < 0 forever // returns event index or -1 on timeout or -2 on abandon int32_t (*wait_any)(int32_t n, rt_event_t events[]); // -1 on abandon int32_t (*wait_any_or_timeout)(int32_t n, rt_event_t e[], fp64_t seconds); void (*dispose)(rt_event_t e); void (*test)(void); } rt_event_if; extern rt_event_if rt_event; typedef struct rt_aligned_8 rt_mutex_s { uint8_t content[40]; } rt_mutex_t; typedef struct { void (*init)(rt_mutex_t* m); void (*lock)(rt_mutex_t* m); void (*unlock)(rt_mutex_t* m); void (*dispose)(rt_mutex_t* m); void (*test)(void); } rt_mutex_if; extern rt_mutex_if rt_mutex; typedef struct thread_s* rt_thread_t; typedef struct { rt_thread_t (*start)(void (*func)(void*), void* p); // never returns null errno_t (*join)(rt_thread_t thread, fp64_t timeout_seconds); // < 0 forever void (*detach)(rt_thread_t thread); // closes handle. thread is not joinable void (*name)(const char* name); // names the thread void (*realtime)(void); // bumps calling thread priority void (*yield)(void); // pthread_yield() / Win32: SwitchToThread() void (*sleep_for)(fp64_t seconds); uint64_t (*id_of)(rt_thread_t t); uint64_t (*id)(void); // gettid() rt_thread_t (*self)(void); // Pseudo Handle may differ in access to .open(.id()) errno_t (*open)(rt_thread_t* t, uint64_t id); void (*close)(rt_thread_t t); void (*test)(void); } rt_thread_if; extern rt_thread_if rt_thread; rt_end_c ================================================ FILE: inc/rt/rt_vigil.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // better rt_assert() - augmented with printf format and parameters // rt_swear() - release configuration rt_assert() in honor of: // https://github.com/munificent/vigil #define rt_static_assertion(condition) static_assert(condition, #condition) typedef struct { int32_t (*failed_assertion)(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...); int32_t (*fatal_termination)(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...); int32_t (*fatal_if_error)(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, ...); void (*test)(void); } rt_vigil_if; extern rt_vigil_if rt_vigil; #if defined(DEBUG) #define rt_assert(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.failed_assertion(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #else #define rt_assert(b, ...) ((void)0) #endif // rt_swear() is runtime rt_assert() for both debug and release configurations #define rt_swear(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.failed_assertion(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_fatal(...) (void)(rt_vigil.fatal_termination( \ __FILE__, __LINE__, __func__, "", "" __VA_ARGS__)) #define rt_fatal_if(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!(b)) || rt_vigil.fatal_termination(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_fatal_if_not(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.fatal_termination(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_not_null(e, ...) rt_fatal_if((e) == null, "" __VA_ARGS__) #define rt_fatal_if_error(r, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)(rt_vigil.fatal_if_error(__FILE__, __LINE__, __func__, \ #r, r, "" __VA_ARGS__)) #define rt_fatal_win32err(c, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)(rt_vigil.fatal_if_error(__FILE__, __LINE__, __func__, \ #c, rt_b2e(c), "" __VA_ARGS__)) rt_end_c ================================================ FILE: inc/rt/rt_work.h ================================================ #pragma once #include "rt/rt.h" rt_begin_c // Minimalistic "react"-like work_queue or work items and // a thread based workers. See rt_worker_test() for usage. typedef struct rt_event_s* rt_event_t; typedef struct rt_work_s rt_work_t; typedef struct rt_work_queue_s rt_work_queue_t; typedef struct rt_work_s { rt_work_queue_t* queue; // queue where the call is or was last scheduled fp64_t when; // proc() call will be made after or at this time void (*work)(rt_work_t* c); void* data; // extra data that will be passed to proc() call rt_event_t done; // if not null signalled after calling proc() or canceling rt_work_t* next; // next element in the queue (implementation detail) bool canceled; // set to true inside .cancel() call } rt_work_t; typedef struct rt_work_queue_s { rt_work_t* head; int64_t lock; // spinlock rt_event_t changed; // if not null will be signaled when head changes } rt_work_queue_t; typedef struct rt_work_queue_if { void (*post)(rt_work_t* c); bool (*get)(rt_work_queue_t*, rt_work_t* *c); void (*call)(rt_work_t* c); void (*dispatch)(rt_work_queue_t* q); // all ready messages void (*cancel)(rt_work_t* c); void (*flush)(rt_work_queue_t* q); // cancel all requests in the queue } rt_work_queue_if; extern rt_work_queue_if rt_work_queue; typedef struct rt_worker_s { rt_work_queue_t queue; rt_thread_t thread; rt_event_t wake; volatile bool quit; } rt_worker_t; typedef struct rt_worker_if { void (*start)(rt_worker_t* tq); void (*post)(rt_worker_t* tq, rt_work_t* w); errno_t (*join)(rt_worker_t* tq, fp64_t timeout); void (*test)(void); } rt_worker_if; extern rt_worker_if rt_worker; // worker thread waits for a queue's `wake` event with the timeout // infinity or if queue is not empty delta time till the head // item of the queue. // // Upon post() call the `wake` event is set and the worker thread // wakes up and dispatches all the items with .when less then now // calling function work() if it is not null and optionally signaling // .done event if it is not null. // // When all ready items in the queue are processed worker thread locks // the queue and if the head is present calculates next timeout based // on .when time of the head or sets timeout to infinity if the queue // is empty. // // Function .join() sets .quit to true signals .wake event and attempt // to join the worker .thread with specified timeout. // It is the responsibility of the caller to ensure that no other // work is posted after calling .join() because it will be lost. rt_end_c /* Usage examples: // The dispatch_until() is just for testing purposes. // Usually rt_work_queue.dispatch(q) will be called inside each // iteration of message loop of a dispatch [UI] thread. static void dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n); // simple way of passing a single pointer to call_later static void every_100ms(rt_work_t* w) { int32_t* i = (int32_t*)w->data; rt_println("i: %d", *i); (*i)++; w->when = rt_clock.seconds() + 0.100; rt_work_queue.post(w); } static void example_1(void) { rt_work_queue_t queue = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.100, .work = every_100ms, .data = &i }; rt_work_queue.post(&work); dispatch_until(&queue, &i, 4); } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void every_200ms(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; rt_println("ex { .i: %d, .s.a: %d .s.b: %d}", ex->i, ex->s.a, ex->s.b); ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; w->when = rt_clock.seconds() + 0.200; rt_work_queue.post(w); } static void example_2(void) { rt_work_queue_t queue = {0}; rt_work_ex_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.200, .work = every_200ms, .data = null, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&work.base); dispatch_until(&queue, &work.i, 4); } static void dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n) { while (q->head != null && *i < n) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(q); } rt_work_queue.flush(q); } // worker: static void do_work(rt_work_t* w) { // TODO: something useful } static void worker_test(void) { rt_worker_t worker = { 0 }; rt_worker.start(&worker); rt_work_t work = { .when = rt_clock.seconds() + 0.010, // 10ms .done = rt_event.create(), .work = do_work }; rt_worker.post(&worker, &work); rt_event.wait(work.done); // await(work) rt_event.dispose(work.done); // responsibility of the caller rt_fatal_if_error(rt_worker.join(&worker, -1.0)); } // Hint: // To monitor timing turn on MSVC Output / Show Timestamp (clock button) */ ================================================ FILE: inc/rt/version.rc.in ================================================ #include "..\inc\rt\version.h" // rc.exe does not grok forward slashes VS_VERSION_INFO VERSIONINFO FILEVERSION version_yy, version_mm, version_dd, version_hh PRODUCTVERSION version_yy, version_mm, version_dd, version_hh FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x4L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", company_name VALUE "FileDescription", file_description VALUE "FileVersion", version_str VALUE "InternalName", original_file_name VALUE "LegalCopyright", copyright VALUE "OriginalFilename", original_file_name VALUE "ProductName", product_name VALUE "ProductVersion", version_str END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END ================================================ FILE: inc/ui/ui.h ================================================ #pragma once // alphabetical order is not possible because of headers interdependencies #include "ui/rt_std.h" #include "ui/ui_core.h" #include "ui/ui_colors.h" #include "ui/ui_fuzzing.h" #include "ui/ui_gdi.h" #include "ui/ui_view.h" #include "ui/ui_containers.h" #include "ui/ui_edit_doc.h" #include "ui/ui_edit_view.h" #include "ui/ui_label.h" #include "ui/ui_button.h" #include "ui/ui_image.h" #include "ui/ui_midi.h" #include "ui/ui_slider.h" #include "ui/ui_theme.h" #include "ui/ui_toggle.h" #include "ui/ui_mbx.h" #include "ui/ui_caption.h" #include "ui/ui_app.h" ================================================ FILE: inc/ui/ui_app.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // link.exe /SUBSYSTEM:WINDOWS single window application typedef struct ui_app_message_handler_s ui_app_message_handler_t; typedef struct ui_app_message_handler_s { void* that; ui_app_message_handler_t* next; bool (*callback)(ui_app_message_handler_t* handler, int32_t m, int64_t wp, int64_t lp, int64_t* rt); } ui_app_message_handler_t; typedef struct ui_dpi_s { // max(dpi_x, dpi_y) int32_t system; // system dpi int32_t process; // process dpi // 15" diagonal monitor 3840x2160 175% scaled // monitor dpi effective 168, angular 248 raw 284 int32_t monitor_effective; // effective with regard of user scaling int32_t monitor_raw; // with regard of physical screen size int32_t monitor_angular; // diagonal raw int32_t monitor_max; // maximum of effective,raw,angular int32_t window; // main window dpi } ui_dpi_t; // in inches (because monitors customary are) // it is not in points (1/72 inch) like font size // because it is awkward to express large area // size in typography measurements. typedef struct ui_window_sizing_s { fp32_t ini_w; // initial window width in inches fp32_t ini_h; // 0,0 means set to min_w, min_h fp32_t min_w; // minimum window width in inches fp32_t min_h; // 0,0 means - do not care use content size fp32_t max_w; // maximum window width in inches fp32_t max_h; // 0,0 means as big as user wants // "sizing" "estimate or measure something's dimensions." // initial window sizing only used on the first invocation // actual user sizing is stored in the configuration and used // on all launches except the very first. } ui_window_sizing_t; typedef struct ui_fms_s { // when font handles are re-created on system scaling change // metrics "em" and font geometry filled ui_fm_t normal; // regular UI font ~ 11-12pt ui_fm_t tiny; // small UI font ~ 8pt ui_fm_t title; // Largest Title font ui_fm_t rubric; // Subtitle font ui_fm_t H1; // bolder header font ui_fm_t H2; ui_fm_t H3; } ui_fms_t; typedef struct { // TODO: split to ui_app_t and ui_app_if, move data after methods // implemented by client: const char* class_name; // called before creating main window void (*init)(void); // called instead of init() for console apps and when .no_ui=true int (*main)(void); // class_name and init must be set before main() void (*opened)(void); // window has been created and shown void (*every_sec)(void); // if not null called ~ once a second void (*every_100ms)(void); // called ~10 times per second // .can_close() called before window is closed and can be // used in a meaning of .closing() bool (*can_close)(void); // window can be closed void (*closed)(void); // window has been closed void (*fini)(void); // called before WinMain() return // must be filled by application: const char* title; ui_window_sizing_t const window_sizing; // TODO: struct {} visibility; // see: ui.visibility.* int32_t visibility; // initial window_visibility state int32_t last_visibility; // last window_visibility state from last run int32_t startup_visibility; // window_visibility from parent process ui_canvas_t canvas; // set by message.paint // ui flags: bool is_full_screen; bool no_ui; // do not create application window at all bool dark_mode; // forced dark mode for the whole application bool light_mode; // forced light mode for the whole application bool no_decor; // window w/o title bar, min/max close buttons bool no_min; // window w/o minimize button on title bar and sys menu bool no_max; // window w/o maximize button on title bar bool no_size; // window w/o maximize button on title bar bool no_clip; // allows to resize window above hosting monitor size bool hide_on_minimize; // like task manager minimize means hide ui_window_t window; ui_icon_t icon; // may be null uint64_t tid; // main thread id int32_t exit_code; // application exit code ui_dpi_t dpi; ui_rect_t wrc; // window rectangle including non-client area ui_rect_t crc; // client rectangle ui_rect_t mrc; // monitor rectangle ui_rect_t prc; // previously invalidated paint rectangle inside crc ui_rect_t work_area; // current monitor work area int32_t caption_height; // caption height ui_wh_t border; // frame border size // not to call rt_clock.seconds() too often: fp64_t now; // ssb "seconds since boot" updated on each message ui_view_t* root; // show_window() changes ui.hidden ui_view_t* content; ui_view_t* caption; ui_view_t* focus; // does not affect message routing struct { // font metrics and handles ui_fms_t prop; // proportional fonts ui_fms_t mono; // monospaced fonts } fm; // TODO: struct {} keyboard // keyboard state now: bool alt; bool ctrl; bool shift; // TODO: struct {} mouse // mouse buttons state bool mouse_swapped; bool mouse_left; // left or if buttons are swapped - right button pressed bool mouse_middle; // rarely useful bool mouse_right; // context button pressed ui_point_t mouse; // mouse/touchpad pointer ui_cursor_t cursor; // current cursor struct { ui_cursor_t arrow; ui_cursor_t wait; ui_cursor_t ibeam; ui_cursor_t size_nwse; // north west - south east ui_cursor_t size_nesw; // north east - south west ui_cursor_t size_we; // west - east ui_cursor_t size_ns; // north - south ui_cursor_t size_all; // north - south } cursors; struct { // animated_groot state ui_view_t* view; ui_view_t* focused; // focused view before animated_groot started int32_t step; fp64_t time; // closing time or zero int32_t x; // (x,y) for tooltip (-1,y) for toast int32_t y; // screen coordinates for tooltip } animating; ui_app_message_handler_t* handlers; // post(..., delay_in_seconds, ...) can be scheduled from any thread executed // on UI thread void (*post)(rt_work_t* work); // work.when == 0 meaning ASAP void (*request_redraw)(void); // very fast <2 microseconds void (*draw)(void); // paint window now - bad idea do not use // inch to pixels and reverse translation via ui_app.dpi.window fp32_t (*px2in)(int32_t pixels); int32_t (*in2px)(fp32_t inches); errno_t (*set_layered_window)(ui_color_t color, float alpha); bool (*is_active)(void); // is application window active bool (*is_minimized)(void); bool (*is_maximized)(void); bool (*focused)(void); // application window has keyboard focus void (*activate)(void); // request application window activation void (*set_title)(const char* title); void (*capture_mouse)(bool on); // capture mouse global input on/of void (*move_and_resize)(const ui_rect_t* rc); void (*bring_to_foreground)(void); // not necessary topmost void (*make_topmost)(void); // in foreground hierarchy of windows void (*request_focus)(void); // request application window keyboard focus void (*bring_to_front)(void); // activate() + bring_to_foreground() + // make_topmost() + request_focus() // measure and layout: void (*request_layout)(void); // requests layout on UI tree before paint() void (*invalidate)(const ui_rect_t* rc); void (*full_screen)(bool on); void (*set_cursor)(ui_cursor_t c); void (*close)(void); // attempts to close (can_close() permitting) // forced quit() even if can_close() returns false void (*quit)(int32_t ec); // ui_app.exit_code = ec; PostQuitMessage(ec); ui_timer_t (*set_timer)(uintptr_t id, int32_t milliseconds); // see notes void (*kill_timer)(ui_timer_t id); void (*show_window)(int32_t show); // see show_window enum void (*show_toast)(ui_view_t* toast, fp64_t seconds); // toast(null) to cancel void (*show_hint)(ui_view_t* tooltip, int32_t x, int32_t y, fp64_t seconds); void (*toast_va)(fp64_t seconds, const char* format, va_list va); void (*toast)(fp64_t seconds, const char* format, ...); // caret calls must be balanced by caller void (*create_caret)(int32_t w, int32_t h); void (*show_caret)(void); void (*move_caret)(int32_t x, int32_t y); void (*hide_caret)(void); void (*destroy_caret)(void); // beep sounds: void (*beep)(int32_t kind); // registry interface: void (*data_save)(const char* name, const void* data, int32_t bytes); int32_t (*data_size)(const char* name); int32_t (*data_load)(const char* name, void* data, int32_t bytes); // returns bytes read // filename dialog: // const char* filter[] = // {"Text Files", ".txt;.doc;.ini", // "Executables", ".exe", // "All Files", "*"}; // const char* fn = ui_app.open_filename("C:\\", filter, rt_countof(filter)); const char* (*open_file)(const char* folder, const char* filter[], int32_t n); bool (*is_stdout_redirected)(void); bool (*is_console_visible)(void); int (*console_attach)(void); // attempts to attach to parent terminal int (*console_create)(void); // allocates new console void (*console_show)(bool b); // stats: int32_t paint_count; // number of paint calls fp64_t paint_time; // last paint duration in seconds fp64_t paint_max; // max of last 128 paint fp64_t paint_avg; // EMA of last 128 paints fp64_t paint_fps; // EMA of last 128 paints fp64_t paint_last; // rt_clock.seconds() of last paint fp64_t paint_dt_min; // minimum time between 2 paints } ui_app_t; extern ui_app_t ui_app; rt_end_c ================================================ FILE: inc/ui/ui_button.h ================================================ #pragma once #include "rt/rt_std.h" #include "ui/ui_view.h" rt_begin_c typedef ui_view_t ui_button_t; void ui_view_init_button(ui_view_t* v); void ui_button_init(ui_button_t* b, const char* label, fp32_t min_width_em, void (*callback)(ui_button_t* b)); // ui_button_clicked can only be used on static button variables #define ui_button_clicked(name, s, min_width_em, ...) \ static void name ## _clicked(ui_button_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_button_t name = { \ .type = ui_view_button, \ .init = ui_view_init_button, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = name ## _clicked, \ .color_id = ui_color_id_button_text, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } #define ui_button(s, min_width_em, clicked) { \ .type = ui_view_button, \ .init = ui_view_init_button, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = clicked, \ .color_id = ui_color_id_button_text, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } // usage: // // ui_button_clicked(button, "&Button", 7.0, { // if (button->state.pressed) { // // do something on click that happens on release mouse button // } // }) // // or: // // static void button_flipped(ui_button_t* b) { // swear(b->flip == true); // 2 state button, clicked on mouse press button // if (b->state.pressed) { // // show something: // } else { // // show something else: // } // } // // ui_button_t button = ui_button(7.0, "&Button", button_flipped); // // or // // ui_button_t button = ui_view)button(button); // ui_view.set_text(button.text, "&Button"); // button.min_w_em = 7.0; // button.callback = button_flipped; // // Note: // ui_button_clicked(button, "&Button", 7.0, { // button->state.pressed = !button->state.pressed; // // is similar to: button.flip = true but it leads thru // // multiple button paint and click happens on mouse button // // release not press // } rt_end_c ================================================ FILE: inc/ui/ui_caption.h ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "ui/ui.h" rt_begin_c typedef struct ui_caption_s { ui_view_t view; // caption`s children: ui_button_t icon; ui_label_t title; ui_view_t spacer; ui_button_t menu; // use: ui_caption.button_menu.cb := your callback ui_button_t mode; // switch between dark/light mode ui_button_t mini; ui_button_t maxi; ui_button_t full; ui_button_t quit; } ui_caption_t; extern ui_caption_t ui_caption; rt_end_c ================================================ FILE: inc/ui/ui_colors.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef uint64_t ui_color_t; // top 2 bits determine color format /* TODO: make ui_color_t uint64_t RGBA or better yet fp32_t RGBA support upto 16-16-16-14(A)bit per pixel color components with 'transparent' aka 'hollow' bit */ #define ui_color_mask ((ui_color_t)0xC000000000000000ULL) #define ui_color_undefined ((ui_color_t)0x8000000000000000ULL) #define ui_color_transparent ((ui_color_t)0x4000000000000000ULL) #define ui_color_hdr ((ui_color_t)0xC000000000000000ULL) #define ui_color_is_8bit(c) ( ((c) & ui_color_mask) == 0) #define ui_color_is_hdr(c) ( ((c) & ui_color_mask) == ui_color_hdr) #define ui_color_is_undefined(c) ( ((c) & ui_color_mask) == ui_color_undefined) #define ui_color_is_transparent(c) ((((c) & ui_color_mask) == ui_color_transparent) && \ ( ((c) & ~ui_color_mask) == 0)) // if any other special colors or formats need to be introduced // (c) & ~ui_color_mask) has 2^62 possible extensions bits // ui_color_hdr A - 14 bit, R,G,B - 16 bit, all in range [0..0xFFFF] #define ui_color_hdr_a(c) ((uint16_t)((((c) >> 48) & 0x3FFF) << 2)) #define ui_color_hdr_r(c) ((uint16_t)( ((c) >> 0) & 0xFFFF)) #define ui_color_hdr_g(c) ((uint16_t)( ((c) >> 16) & 0xFFFF)) #define ui_color_hdr_b(c) ((uint16_t)( ((c) >> 32) & 0xFFFF)) #define ui_color_a(c) ((uint8_t)(((c) >> 24) & 0xFFU)) #define ui_color_r(c) ((uint8_t)(((c) >> 0) & 0xFFU)) #define ui_color_g(c) ((uint8_t)(((c) >> 8) & 0xFFU)) #define ui_color_b(c) ((uint8_t)(((c) >> 16) & 0xFFU)) #define ui_color_is_rgb(c) ((uint32_t)( (c) & 0x00FFFFFFU)) #define ui_color_is_rgba(c) ((uint32_t)( (c) & 0xFFFFFFFFU)) #define ui_color_is_rgbFF(c) ((uint32_t)(((c) & 0x00FFFFFFU)) | 0xFF000000U) #define ui_color_rgb(r, g, b) ((ui_color_t)( \ (((uint32_t)(uint8_t)(r)) ) | \ (((uint32_t)(uint8_t)(g)) << 8) | \ (((uint32_t)(uint8_t)(b)) << 16))) #define ui_color_rgba(r, g, b, a) \ ( (ui_color_t)( \ (ui_color_rgb(r, g, b)) | \ ((ui_color_t)((uint32_t)((uint8_t)(a))) << 24)) \ ) enum { ui_color_id_undefined = 0, ui_color_id_active_title = 1, ui_color_id_button_face = 2, ui_color_id_button_text = 3, ui_color_id_gray_text = 4, ui_color_id_highlight = 5, ui_color_id_highlight_text = 6, ui_color_id_hot_tracking = 7, ui_color_id_inactive_title = 8, ui_color_id_inactive_title_text = 9, ui_color_id_menu_highlight = 10, ui_color_id_title_text = 11, ui_color_id_window = 12, ui_color_id_window_text = 13, ui_color_id_accent = 14 }; typedef struct ui_control_colors_s { ui_color_t text; ui_color_t background; ui_color_t border; ui_color_t accent; // aka highlight ui_color_t gradient_top; ui_color_t gradient_bottom; } control_colors_t; typedef struct ui_control_state_colors_s { control_colors_t disabled; control_colors_t enabled; control_colors_t hover; control_colors_t armed; control_colors_t pressed; } ui_control_state_colors_t; typedef struct ui_colors_s { ui_color_t (*get_color)(int32_t color_id); // ui.colors.* void (*rgb_to_hsi)(fp64_t r, fp64_t g, fp64_t b, fp64_t *h, fp64_t *s, fp64_t *i); ui_color_t (*hsi_to_rgb)(fp64_t h, fp64_t s, fp64_t i, uint8_t a); // interpolate(): // 0.0 < multiplier < 1.0 excluding boundaries // alpha is interpolated as well ui_color_t (*interpolate)(ui_color_t c0, ui_color_t c1, fp32_t multiplier); ui_color_t (*gray_with_same_intensity)(ui_color_t c); // multiplier ]0.0..1.0] excluding zero // lighten() and darken() ignore alpha (use interpolate for alpha colors) ui_color_t (*lighten)(ui_color_t rgb, fp32_t multiplier); // interpolate toward white ui_color_t (*darken)(ui_color_t rgb, fp32_t multiplier); // interpolate toward black ui_color_t (*adjust_saturation)(ui_color_t c, fp32_t multiplier); ui_color_t (*multiply_brightness)(ui_color_t c, fp32_t multiplier); ui_color_t (*multiply_saturation)(ui_color_t c, fp32_t multiplier); ui_control_state_colors_t* controls; // colors for UI controls ui_color_t const transparent; ui_color_t const none; // aka CLR_INVALID in wingdi.h ui_color_t const text; ui_color_t const white; ui_color_t const black; ui_color_t const red; ui_color_t const green; ui_color_t const blue; ui_color_t const yellow; ui_color_t const cyan; ui_color_t const magenta; ui_color_t const gray; // tone down RGB colors: ui_color_t const tone_white; ui_color_t const tone_red; ui_color_t const tone_green; ui_color_t const tone_blue; ui_color_t const tone_yellow; ui_color_t const tone_cyan; ui_color_t const tone_magenta; // miscellaneous: ui_color_t const orange; ui_color_t const dark_green; ui_color_t const pink; ui_color_t const ochre; ui_color_t const gold; ui_color_t const teal; ui_color_t const wheat; ui_color_t const tan; ui_color_t const brown; ui_color_t const maroon; ui_color_t const barbie_pink; ui_color_t const steel_pink; ui_color_t const salmon_pink; ui_color_t const gainsboro; ui_color_t const light_gray; ui_color_t const silver; ui_color_t const dark_gray; ui_color_t const dim_gray; ui_color_t const light_slate_gray; ui_color_t const slate_gray; /* Named colors */ /* Main Panel Backgrounds */ ui_color_t const ennui_black; // rgb(18, 18, 18) 0x121212 ui_color_t const charcoal; ui_color_t const onyx; ui_color_t const gunmetal; ui_color_t const jet_black; ui_color_t const outer_space; ui_color_t const eerie_black; ui_color_t const oil; ui_color_t const black_coral; ui_color_t const obsidian; /* Secondary Panels or Sidebars */ ui_color_t const raisin_black; ui_color_t const dark_charcoal; ui_color_t const dark_jungle_green; ui_color_t const pine_tree; ui_color_t const rich_black; ui_color_t const eclipse; ui_color_t const cafe_noir; /* Flat Buttons */ ui_color_t const prussian_blue; ui_color_t const midnight_green; ui_color_t const charleston_green; ui_color_t const rich_black_fogra; ui_color_t const dark_liver; ui_color_t const dark_slate_gray; ui_color_t const black_olive; ui_color_t const cadet; /* Button highlights (hover) */ ui_color_t const dark_sienna; ui_color_t const bistre_brown; ui_color_t const dark_puce; ui_color_t const wenge; /* Raised button effects */ ui_color_t const dark_scarlet; ui_color_t const burnt_umber; ui_color_t const caput_mortuum; ui_color_t const barn_red; /* Text and Icons */ ui_color_t const platinum; ui_color_t const anti_flash_white; ui_color_t const silver_sand; ui_color_t const quick_silver; /* Links and Selections */ ui_color_t const dark_powder_blue; ui_color_t const sapphire_blue; ui_color_t const international_klein_blue; ui_color_t const zaffre; /* Additional Colors */ ui_color_t const fish_belly; ui_color_t const rusty_red; ui_color_t const falu_red; ui_color_t const cordovan; ui_color_t const dark_raspberry; ui_color_t const deep_magenta; ui_color_t const byzantium; ui_color_t const amethyst; ui_color_t const wisteria; ui_color_t const lavender_purple; ui_color_t const opera_mauve; ui_color_t const mauve_taupe; ui_color_t const rich_lavender; ui_color_t const pansy_purple; ui_color_t const violet_eggplant; ui_color_t const jazzberry_jam; ui_color_t const dark_orchid; ui_color_t const electric_purple; ui_color_t const sky_magenta; ui_color_t const brilliant_rose; ui_color_t const fuchsia_purple; ui_color_t const french_raspberry; ui_color_t const wild_watermelon; ui_color_t const neon_carrot; ui_color_t const burnt_orange; ui_color_t const carrot_orange; ui_color_t const tiger_orange; ui_color_t const giant_onion; ui_color_t const rust; ui_color_t const copper_red; ui_color_t const dark_tangerine; ui_color_t const bright_marigold; ui_color_t const bone; /* Earthy Tones */ ui_color_t const sienna; ui_color_t const sandy_brown; ui_color_t const golden_brown; ui_color_t const camel; ui_color_t const burnt_sienna; ui_color_t const khaki; ui_color_t const dark_khaki; /* Greens */ ui_color_t const fern_green; ui_color_t const moss_green; ui_color_t const myrtle_green; ui_color_t const pine_green; ui_color_t const jungle_green; ui_color_t const sacramento_green; /* Blues */ ui_color_t const yale_blue; ui_color_t const cobalt_blue; ui_color_t const persian_blue; ui_color_t const royal_blue; ui_color_t const iceberg; ui_color_t const blue_yonder; /* Miscellaneous */ ui_color_t const cocoa_brown; ui_color_t const cinnamon_satin; ui_color_t const fallow; ui_color_t const cafe_au_lait; ui_color_t const liver; ui_color_t const shadow; ui_color_t const cool_grey; ui_color_t const payne_grey; /* Lighter Tones for Contrast */ ui_color_t const timberwolf; ui_color_t const silver_chalice; ui_color_t const roman_silver; /* Dark Mode Specific Highlights */ ui_color_t const electric_lavender; ui_color_t const magenta_haze; ui_color_t const cyber_grape; ui_color_t const purple_navy; ui_color_t const liberty; ui_color_t const purple_mountain_majesty; ui_color_t const ceil; ui_color_t const moonstone_blue; ui_color_t const independence; } ui_colors_if; extern ui_colors_if ui_colors; // TODO: // https://ankiewicz.com/colors/ // https://htmlcolorcodes.com/color-names/ // it would be super cool to implement a plethora of palettes // with named colors and app "themes" that can be switched rt_end_c ================================================ FILE: inc/ui/ui_containers.h ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "ui/ui.h" rt_begin_c typedef struct ui_view_s ui_view_t; // Usage: // // ui_view_t* stack = ui_view(stack); // ui_view_t* horizontal = ui_view(ui_view_span); // ui_view_t* vertical = ui_view(ui_view_list); // // containers automatically layout child views // similar to SwiftUI HStack and VStack taking .align // .insets and .padding into account. // // Container positions every child views in the center, // top bottom left right edge or any of 4 corners // depending on .align values. // if child view has .max_w or .max_h set to ui.infinity == INT32_MAX // the views are expanded to fill the container in specified // direction. If child .max_w or .max_h is set to > .w or .h // the child view .w .h measurement are expanded accordingly. // // All containers are transparent and inset by 1/4 of an "em" // Except ui_app.root,caption,content which are also containers // but are not inset or padded and have default background color. // // Application implementer can override this after // // void opened(void) { // ui_view.add(ui_app.view, ..., null); // ui_app.view->insets = (ui_margins_t) { // .left = 0.25, .top = 0.25, // .right = 0.25, .bottom = 0.25 }; // ui_app.view->color = ui_colors.dark_scarlet; // } typedef struct ui_view_s ui_view_t; #define ui_view(view_type) { \ .type = (ui_view_ ## view_type), \ .init = ui_view_init_ ## view_type, \ .fm = &ui_app.fm.prop.normal, \ .color = ui_color_transparent, \ .color_id = 0 \ } void ui_view_init_stack(ui_view_t* v); void ui_view_init_span(ui_view_t* v); void ui_view_init_list(ui_view_t* v); void ui_view_init_spacer(ui_view_t* v); rt_end_c ================================================ FILE: inc/ui/ui_core.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c typedef struct ui_point_s { int32_t x, y; } ui_point_t; typedef struct ui_rect_s { int32_t x, y, w, h; } ui_rect_t; typedef struct ui_ltbr_s { int32_t left, top, right, bottom; } ui_ltrb_t; typedef struct ui_wh_s { int32_t w, h; } ui_wh_t; typedef struct ui_window_s* ui_window_t; typedef struct ui_icon_s* ui_icon_t; typedef struct ui_canvas_s* ui_canvas_t; typedef struct ui_texture_s* ui_texture_t; typedef struct ui_font_s* ui_font_t; typedef struct ui_brush_s* ui_brush_t; typedef struct ui_pen_s* ui_pen_t; typedef struct ui_cursor_s* ui_cursor_t; typedef struct ui_region_s* ui_region_t; typedef uintptr_t ui_timer_t; // timer not the same as "id" in set_timer()! typedef struct ui_bitmap_s { // TODO: ui_ namespace void* pixels; int32_t w; // width int32_t h; // height int32_t bpp; // "components" bytes per pixel int32_t stride; // bytes per scanline rounded up to: (w * bpp + 3) & ~3 ui_texture_t texture; // device allocated texture handle } ui_bitmap_t; // ui_margins_t are used for padding and insets and expressed // in partial "em"s not in pixels, inches or points. // Pay attention that "em" is not square. "M" measurement // for most fonts are em.w = 0.5 * em.h // .em square pixel size of glyph "m" // https://en.wikipedia.org/wiki/Em_(typography) typedef struct ui_gaps_s { // in partial "em"s fp32_t left; fp32_t top; fp32_t right; fp32_t bottom; } ui_margins_t; typedef struct ui_s { bool (*point_in_rect)(const ui_point_t* p, const ui_rect_t* r); // intersect_rect(null, r0, r1) and intersect_rect(r0, r0, r1) supported. bool (*intersect_rect)(ui_rect_t* destination, const ui_rect_t* r0, const ui_rect_t* r1); ui_rect_t (*combine_rect)(const ui_rect_t* r0, const ui_rect_t* r1); const int32_t infinity; // = INT32_MAX, look better struct { // align bitset int32_t const center; // = 0, default int32_t const left; // left|top, left|bottom, right|bottom int32_t const top; int32_t const right; // right|top, right|bottom int32_t const bottom; } const align; struct { // window visibility int32_t const hide; int32_t const normal; // should be use for first .show() int32_t const minimize; // activate and minimize int32_t const maximize; // activate and maximize int32_t const normal_na;// same as .normal but no activate int32_t const show; // shows and activates in current size and position int32_t const min_next; // minimize and activate next window in Z order int32_t const min_na; // minimize but do not activate int32_t const show_na; // same as .show but no activate int32_t const restore; // from min/max to normal window size/pos int32_t const defau1t; // use Windows STARTUPINFO value int32_t const force_min;// minimize even if dispatch thread not responding } const visibility; // TODO: remove or move inside app struct { // message: int32_t const animate; int32_t const opening; int32_t const closing; } const message; // TODO: remove or move inside app struct { // mouse buttons bitset mask struct { int32_t const left; int32_t const right; } button; } const mouse; struct { // window decorations hit test results int32_t const error; // -2 int32_t const transparent; // -1 int32_t const nowhere; // 0 int32_t const client; // 1 int32_t const caption; // 2 int32_t const system_menu; // 3 int32_t const grow_box; // 4 int32_t const menu; // 5 int32_t const horizontal_scroll;// 6 int32_t const vertical_scroll; // 7 int32_t const min_button; // 8 int32_t const max_button; // 9 int32_t const left; // 10 int32_t const right; // 11 int32_t const top; // 12 int32_t const top_left; // 13 int32_t const top_right; // 14 int32_t const bottom; // 15 int32_t const bottom_left; // 16 int32_t const bottom_right; // 17 int32_t const border; // 18 int32_t const object; // 19 int32_t const close; // 20 int32_t const help; // 21 } const hit_test; struct { // virtual keyboard keys int32_t const up; int32_t const down; int32_t const left; int32_t const right; int32_t const home; int32_t const end; int32_t const page_up; int32_t const page_down; int32_t const insert; int32_t const del; int32_t const back; int32_t const escape; int32_t const enter; int32_t const plus; int32_t const minus; int32_t const f1; int32_t const f2; int32_t const f3; int32_t const f4; int32_t const f5; int32_t const f6; int32_t const f7; int32_t const f8; int32_t const f9; int32_t const f10; int32_t const f11; int32_t const f12; int32_t const f13; int32_t const f14; int32_t const f15; int32_t const f16; int32_t const f17; int32_t const f18; int32_t const f19; int32_t const f20; int32_t const f21; int32_t const f22; int32_t const f23; int32_t const f24; } const key; struct { int32_t const ok; int32_t const info; int32_t const question; int32_t const warning; int32_t const error; } beep; } ui_if; extern ui_if ui; // ui_margins_t in "em"s: // // The reason is that UI fonts may become larger smaller // for accessibility reasons with the same display // density in DPIs. Humanoid would expect the margins around // larger font text to grow with font size increase. // SwingUI and MacOS is using "pt" for padding which does // not account to font size changes. MacOS does weird stuff // with font increase - it actually decreases GPU resolution. // Android uses "dp" which is pretty much the same as scaled // "pixels" on MacOS. Windows used to use "dialog units" which // is font size based and this is where the idea is inherited from. rt_end_c ================================================ FILE: inc/ui/ui_edit_doc.h ================================================ #pragma once /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" rt_begin_c typedef struct ui_edit_str_s ui_edit_str_t; typedef struct ui_edit_doc_s ui_edit_doc_t; typedef struct ui_edit_notify_s ui_edit_notify_t; typedef struct ui_edit_to_do_s ui_edit_to_do_t; typedef struct ui_edit_pg_s { // page/glyph coordinates // humans used to line:column coordinates in text int32_t pn; // zero based paragraph number ("line number") int32_t gp; // zero based glyph position ("column") } ui_edit_pg_t; typedef union rt_begin_packed ui_edit_range_s { struct { ui_edit_pg_t from; ui_edit_pg_t to; }; ui_edit_pg_t a[2]; } rt_end_packed ui_edit_range_t; // "from"[0] "to"[1] typedef struct ui_edit_text_s { int32_t np; // number of paragraphs ui_edit_str_t* ps; // ps[np] paragraphs } ui_edit_text_t; typedef struct ui_edit_notify_info_s { bool ok; // false if ui_edit_view.replace() failed (bad utf8 or no memory) const ui_edit_doc_t* const d; const ui_edit_range_t* const r; // range to be replaced const ui_edit_range_t* const x; // extended range (replacement) const ui_edit_text_t* const t; // replacement text // d->text.np number of paragraphs may change after replace // before/after: [pnf..pnt] is inside [0..d->text.np-1] int32_t const pnf; // paragraph number from int32_t const pnt; // paragraph number to. (inclusive) // one can safely assume that ps[pnf] was modified // except empty range replace with empty text (which shouldn't be) // d->text.ps[pnf..pnf + deleted] were deleted // d->text.ps[pnf..pnf + inserted] were inserted int32_t const deleted; // number of deleted paragraphs (before: 0) int32_t const inserted; // paragraph inserted paragraphs (before: 0) } ui_edit_notify_info_t; typedef struct ui_edit_notify_s { // called before and after replace() void (*before)(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni); // after() is called even if replace() failed with ok: false void (*after)(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni); } ui_edit_notify_t; typedef struct ui_edit_listener_s ui_edit_listener_t; typedef struct ui_edit_listener_s { ui_edit_notify_t* notify; ui_edit_listener_t* prev; ui_edit_listener_t* next; } ui_edit_listener_t; typedef struct ui_edit_to_do_s { // undo/redo action ui_edit_range_t range; ui_edit_text_t text; ui_edit_to_do_t* next; // inside undo or redo list } ui_edit_to_do_t; typedef struct ui_edit_doc_s { ui_edit_text_t text; ui_edit_to_do_t* undo; // undo stack ui_edit_to_do_t* redo; // redo stack ui_edit_listener_t* listeners; } ui_edit_doc_t; typedef struct ui_edit_doc_if { // init(utf8, bytes, heap:false) must have longer lifetime // than document, otherwise use heap: true to copy bool (*init)(ui_edit_doc_t* d, const char* utf8_or_null, int32_t bytes, bool heap); bool (*replace)(ui_edit_doc_t* d, const ui_edit_range_t* r, const char* utf8, int32_t bytes); int32_t (*bytes)(const ui_edit_doc_t* d, const ui_edit_range_t* range); bool (*copy_text)(ui_edit_doc_t* d, const ui_edit_range_t* range, ui_edit_text_t* text); // retrieves range into string int32_t (*utf8bytes)(const ui_edit_doc_t* d, const ui_edit_range_t* range); // utf8 must be at least ui_edit_doc.utf8bytes() void (*copy)(ui_edit_doc_t* d, const ui_edit_range_t* range, char* utf8, int32_t bytes); // undo() and push reverse into redo stack bool (*undo)(ui_edit_doc_t* d); // false if there is nothing to redo // redo() and push reverse into undo stack bool (*redo)(ui_edit_doc_t* d); // false if there is nothing to undo bool (*subscribe)(ui_edit_doc_t* d, ui_edit_notify_t* notify); void (*unsubscribe)(ui_edit_doc_t* d, ui_edit_notify_t* notify); void (*dispose_to_do)(ui_edit_to_do_t* to_do); void (*dispose)(ui_edit_doc_t* d); void (*test)(void); } ui_edit_doc_if; extern ui_edit_doc_if ui_edit_doc; typedef struct ui_edit_range_if { int (*compare)(const ui_edit_pg_t pg1, const ui_edit_pg_t pg2); ui_edit_range_t (*order)(const ui_edit_range_t r); bool (*is_valid)(const ui_edit_range_t r); bool (*is_empty)(const ui_edit_range_t r); uint64_t (*uint64)(const ui_edit_pg_t pg); // (p << 32 | g) ui_edit_pg_t (*pg)(uint64_t ui64); // p: (ui64 >> 32) g: (int32_t)ui64 bool (*inside)(const ui_edit_text_t* t, const ui_edit_range_t r); ui_edit_range_t (*intersect)(const ui_edit_range_t r1, const ui_edit_range_t r2); const ui_edit_range_t* const invalid_range; // {{-1,-1},{-1,-1}} } ui_edit_range_if; extern ui_edit_range_if ui_edit_range; typedef struct ui_edit_text_if { bool (*init)(ui_edit_text_t* t, const char* utf, int32_t b, bool heap); int32_t (*bytes)(const ui_edit_text_t* t, const ui_edit_range_t* r); // end() last paragraph, last glyph in text ui_edit_pg_t (*end)(const ui_edit_text_t* t); ui_edit_range_t (*end_range)(const ui_edit_text_t* t); ui_edit_range_t (*all_on_null)(const ui_edit_text_t* t, const ui_edit_range_t* r); ui_edit_range_t (*ordered)(const ui_edit_text_t* t, const ui_edit_range_t* r); bool (*dup)(ui_edit_text_t* t, const ui_edit_text_t* s); bool (*equal)(const ui_edit_text_t* t1, const ui_edit_text_t* t2); bool (*copy_text)(const ui_edit_text_t* t, const ui_edit_range_t* range, ui_edit_text_t* to); void (*copy)(const ui_edit_text_t* t, const ui_edit_range_t* range, char* to, int32_t bytes); bool (*replace)(ui_edit_text_t* t, const ui_edit_range_t* r, const ui_edit_text_t* text, ui_edit_to_do_t* undo_or_null); bool (*replace_utf8)(ui_edit_text_t* t, const ui_edit_range_t* r, const char* utf8, int32_t bytes, ui_edit_to_do_t* undo_or_null); void (*dispose)(ui_edit_text_t* t); } ui_edit_text_if; extern ui_edit_text_if ui_edit_text; typedef struct rt_begin_packed ui_edit_str_s { char* u; // always correct utf8 bytes not zero terminated(!) sequence // s.g2b[s.g + 1] glyph to byte position inside s.u[] // s.g2b[0] == 0, s.g2b[s.glyphs] == s.bytes int32_t* g2b; // g2b_0 or heap allocated glyphs to bytes indices int32_t b; // number of bytes int32_t c; // when capacity is zero .u is not heap allocated int32_t g; // number of glyphs } rt_end_packed ui_edit_str_t; typedef struct ui_edit_str_if { bool (*init)(ui_edit_str_t* s, const char* utf8, int32_t bytes, bool heap); void (*swap)(ui_edit_str_t* s1, ui_edit_str_t* s2); int32_t (*gp_to_bp)(const char* s, int32_t bytes, int32_t gp); // or -1 int32_t (*bytes)(ui_edit_str_t* s, int32_t from, int32_t to); // glyphs bool (*expand)(ui_edit_str_t* s, int32_t capacity); // reallocate void (*shrink)(ui_edit_str_t* s); // get rid of extra heap memory bool (*replace)(ui_edit_str_t* s, int32_t from, int32_t to, // glyphs const char* utf8, int32_t bytes); // [from..to[ exclusive bool (*is_zwj)(uint32_t utf32); // zero width joiner bool (*is_letter)(uint32_t utf32); // in European Alphabets bool (*is_digit)(uint32_t utf32); bool (*is_symbol)(uint32_t utf32); bool (*is_alphanumeric)(uint32_t utf32); bool (*is_blank)(uint32_t utf32); // white space bool (*is_punctuation)(uint32_t utf32); bool (*is_combining)(uint32_t utf32); bool (*is_spacing)(uint32_t utf32); // spacing modifiers bool (*is_cjk_or_emoji)(uint32_t utf32); bool (*can_break)(uint32_t cp1, uint32_t cp2); void (*test)(void); void (*free)(ui_edit_str_t* s); const ui_edit_str_t* const empty; } ui_edit_str_if; extern ui_edit_str_if ui_edit_str; /* For caller convenience the bytes parameter in all calls can be set to -1 for zero terminated utf8 strings which results in treating strlen(utf8) as number of bytes. ui_edit_str.init() initializes not zero terminated utf8 string that may be allocated on the heap or point out to an outside memory location that should have longer lifetime and will be treated as read only. init() may return false if heap.alloc() returns null or the utf8 bytes sequence is invalid. s.b is number of bytes in the initialized string; s.c is set to heap allocated capacity is set to zero for strings that are not allocated on the heap; s.g is number of the utf8 glyphs (aka Unicode codepoints) in the string; s.g2b[] is an array of s.g + 1 integers that maps glyph positions to byte positions in the utf8 string. The last element is number of bytes in the s.u memory. Called must zero out the string struct before calling init(). ui_edit_str.bytes() returns number of bytes in utf8 string in the exclusive range [from..to[ between string glyphs. ui_edit_str.replace() replaces utf8 string in the exclusive range [from..to[ with the new utf8 string. The new string may be longer or shorter than the replaced string. The function returns false if the new string is invalid utf8 sequence or heap allocation fails. The called must ensure that the range [from..to[ is valid, failure to do so is a fatal error. ui_edit_str.replace() moves string content to the heap. ui_edit_str.free() deallocates all heap allocated memory and zero out string struct. It is incorrect to call free() on the string that was not initialized or already freed. All ui_edit_str_t keep "precise" number of utf8 bytes. Caller may allocate extra byte and set it to 0x00 after retrieving and copying data from ui_edit_str if the string content is intended to be used by any other API that expects zero terminated strings. */ rt_end_c ================================================ FILE: inc/ui/ui_edit_view.h ================================================ #pragma once /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" rt_begin_c // important ui_edit_view_t will refuse to layout into a box smaller than // width 3 x fm->em.w height 1 x fm->em.h typedef struct ui_edit_view_s ui_edit_view_t; typedef struct ui_edit_str_s ui_edit_str_t; typedef struct ui_edit_doc_s ui_edit_doc_t; typedef struct ui_edit_notify_s ui_edit_notify_t; typedef struct ui_edit_to_do_s ui_edit_to_do_t; typedef struct ui_edit_pr_s { // page/run coordinates int32_t pn; // paragraph number int32_t rn; // run number inside paragraph } ui_edit_pr_t; typedef struct ui_edit_run_s { int32_t bp; // position in bytes since start of the paragraph int32_t gp; // position in glyphs since start of the paragraph int32_t bytes; // number of bytes in this `run` int32_t glyphs; // number of glyphs in this `run` int32_t pixels; // width in pixels } ui_edit_run_t; // ui_edit_paragraph_t.initially text will point to readonly memory // with .allocated == 0; as text is modified it is copied to // heap and reallocated there. typedef struct ui_edit_paragraph_s { // "paragraph" view consists of wrapped runs int32_t runs; // number of runs in this paragraph ui_edit_run_t* run; // heap allocated array[runs] } ui_edit_paragraph_t; typedef struct ui_edit_notify_view_s { ui_edit_notify_t notify; void* that; // specific for listener uintptr_t data; // before -> after listener data } ui_edit_notify_view_t; typedef struct ui_edit_view_s { union { ui_view_t view; struct ui_view_s; }; ui_edit_doc_t* doc; // document ui_edit_notify_view_t listener; ui_edit_range_t selection; // "from" selection[0] "to" selection[1] ui_point_t caret; // (-1, -1) off int32_t caret_width; // in pixels ui_edit_pr_t scroll; // left top corner paragraph/run coordinates int32_t last_x; // last_x for up/down caret movement ui_ltrb_t inside; // inside insets space struct { int32_t w; // inside.right - inside.left int32_t h; // inside.bottom - inside.top int32_t buttons; // bit 0 and bit 1 for LEFT and RIGHT mouse buttons down } edit; // number of fully (not partially clipped) visible `runs' from top to bottom: int32_t visible_runs; // TODO: remove focused because it is the same as caret != (-1, -1) bool focused; // is focused and created caret bool ro; // Read Only bool sle; // Single Line Edit bool hide_word_wrap; // do not paint word wrap int32_t shown; // debug: caret show/hide counter 0|1 // paragraphs memory: ui_edit_paragraph_t* para; // para[e->doc->text.np] } ui_edit_view_t; typedef struct ui_edit_view_if { void (*init)(ui_edit_view_t* e, ui_edit_doc_t* d); void (*set_font)(ui_edit_view_t* e, ui_fm_t* fm); // see notes below (*) void (*move)(ui_edit_view_t* e, ui_edit_pg_t pg); // move caret clear selection // replace selected text. If bytes < 0 text is treated as zero terminated void (*replace)(ui_edit_view_t* e, const char* text, int32_t bytes); // call save(e, null, &bytes) to retrieve number of utf8 // bytes required to save whole text including 0x00 terminating bytes errno_t (*save)(ui_edit_view_t* e, char* text, int32_t* bytes); void (*copy)(ui_edit_view_t* e); // to clipboard void (*cut)(ui_edit_view_t* e); // to clipboard // replace selected text with content of clipboard: void (*paste)(ui_edit_view_t* e); // from clipboard void (*select_all)(ui_edit_view_t* e); // select whole text void (*erase)(ui_edit_view_t* e); // delete selected text // keyboard actions dispatcher: void (*key_down)(ui_edit_view_t* e); void (*key_up)(ui_edit_view_t* e); void (*key_left)(ui_edit_view_t* e); void (*key_right)(ui_edit_view_t* e); void (*key_page_up)(ui_edit_view_t* e); void (*key_page_down)(ui_edit_view_t* e); void (*key_home)(ui_edit_view_t* e); void (*key_end)(ui_edit_view_t* e); void (*key_delete)(ui_edit_view_t* e); void (*key_backspace)(ui_edit_view_t* e); void (*key_enter)(ui_edit_view_t* e); // called when ENTER keyboard key is pressed in single line mode void (*enter)(ui_edit_view_t* e); // fuzzer test: void (*fuzz)(ui_edit_view_t* e); // start/stop fuzzing test void (*dispose)(ui_edit_view_t* e); } ui_edit_view_if; extern ui_edit_view_if ui_edit_view; /* Notes: set_font() neither edit.view.font = font nor measure()/layout() functions do NOT dispose paragraphs layout unless geometry changed because it is quite expensive operation. But choosing different font on the fly needs to re-layout all paragraphs. Thus caller needs to set font via this function instead which also requests edit UI element re-layout. .ro readonly edit->ro is used to control readonly mode. If edit control is readonly its appearance does not change but it refuses to accept any changes to the rendered text. .wb wordbreak this attribute was removed as poor UX human experience along with single line scroll editing. See note below about .sle. .sle single line edit control. Edit UI element does NOT support horizontal scroll and breaking words semantics as it is poor UX human experience. This is not how humans (apart of software developers) edit text. If content of the edit UI element is wider than the bounding box width the content is broken on word boundaries and vertical scrolling semantics is supported. Layouts containing edit control of the single line height are strongly encouraged to enlarge edit control layout vertically on as needed basis similar to Google Search Box behavior change implemented in 2023. If multiline is set to true by the callers code the edit UI layout snaps text to the top of x,y,w,h box otherwise the vertical space is distributed evenly between single line of text and top bottom margins. IMPORTANT: SLE resizes itself vertically to accommodate for input that is too wide. If caller wants to limit vertical space it will need to hook .measure() function of SLE and do the math there. */ /* For caller convenience the bytes parameter in all calls can be set to -1 for zero terminated utf8 strings which results in treating strlen(utf8) as number of bytes. ui_edit_str.init() initializes not zero terminated utf8 string that may be allocated on the heap or point out to an outside memory location that should have longer lifetime and will be treated as read only. init() may return false if heap.alloc() returns null or the utf8 bytes sequence is invalid. s.b is number of bytes in the initialized string; s.c is set to heap allocated capacity is set to zero for strings that are not allocated on the heap; s.g is number of the utf8 glyphs (aka Unicode codepoints) in the string; s.g2b[] is an array of s.g + 1 integers that maps glyph positions to byte positions in the utf8 string. The last element is number of bytes in the s.u memory. Called must zero out the string struct before calling init(). ui_edit_str.bytes() returns number of bytes in utf8 string in the exclusive range [from..to[ between string glyphs. ui_edit_str.replace() replaces utf8 string in the exclusive range [from..to[ with the new utf8 string. The new string may be longer or shorter than the replaced string. The function returns false if the new string is invalid utf8 sequence or heap allocation fails. The called must ensure that the range [from..to[ is valid, failure to do so is a fatal error. ui_edit_str.replace() moves string content to the heap. ui_edit_str.free() deallocates all heap allocated memory and zero out string struct. It is incorrect to call free() on the string that was not initialized or already freed. All ui_edit_str_t keep "precise" number of utf8 bytes. Caller may allocate extra byte and set it to 0x00 after retrieving and copying data from ui_edit_str if the string content is intended to be used by any other API that expects zero terminated strings. */ rt_end_c ================================================ FILE: inc/ui/ui_fuzzing.h ================================================ #pragma once /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" rt_begin_c // https://en.wikipedia.org/wiki/Fuzzing // aka "Monkey" testing typedef struct ui_fuzzing_s { rt_work_t base; const char* utf8; // .character(utf8) int32_t key; // .key_pressed(key)/.key_released(key) ui_point_t* pt; // .move_move() // key_press and character bool alt; bool ctrl; bool shift; // mouse modifiers bool left; // tap() buttons: bool right; bool double_tap; bool long_press; // custom int32_t op; void* data; } ui_fuzzing_t; typedef struct ui_fuzzing_if { void (*start)(uint32_t seed); bool (*is_running)(void); bool (*from_inside)(void); // true if called originated inside fuzzing void (*next_random)(ui_fuzzing_t* f); // called if `next` is null void (*dispatch)(ui_fuzzing_t* f); // dispatch work // next() called instead of random if not null void (*next)(ui_fuzzing_t* f); // custom() called instead of dispatch() if not null void (*custom)(ui_fuzzing_t* f); void (*stop)(void); } ui_fuzzing_if; extern ui_fuzzing_if ui_fuzzing; rt_end_c ================================================ FILE: inc/ui/ui_gdi.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c // Graphic Device Interface (selected parts of Windows GDI) enum { // TODO: into gdi int32_t const ui_gdi_font_quality_default = 0, ui_gdi_font_quality_draft = 1, ui_gdi_font_quality_proof = 2, // anti-aliased w/o ClearType rainbows ui_gdi_font_quality_nonantialiased = 3, ui_gdi_font_quality_antialiased = 4, ui_gdi_font_quality_cleartype = 5, ui_gdi_font_quality_cleartype_natural = 6 }; typedef struct ui_fm_s { // font metrics ui_font_t font; ui_wh_t em; // "em" square point size expressed in pixels *) // https://learn.microsoft.com/en-us/windows/win32/gdi/string-widths-and-heights int32_t height; // font height in pixels int32_t baseline; // bottom of the glyphs sans descenders (align of multi-font text) int32_t ascent; // the maximum glyphs extend above the baseline int32_t descent; // maximum height of descenders int32_t x_height; // small letters height int32_t cap_em_height; // Capital letter "M" height int32_t internal_leading; // accents and diacritical marks goes there int32_t external_leading; int32_t average_char_width; int32_t max_char_width; int32_t line_gap; // gap between lines of text ui_wh_t subscript; // height ui_point_t subscript_offset; ui_wh_t superscript; // height ui_point_t superscript_offset; int32_t underscore; // height int32_t underscore_position; int32_t strike_through; // height int32_t strike_through_position; int32_t design_units_per_em; // aka EM square ~ 2048 ui_rect_t box; // bounding box of the glyphs in design units bool mono; } ui_fm_t; /* see: https://github.com/leok7v/ui/wiki/Typography-Line-Terms https://en.wikipedia.org/wiki/Typeface#Font_metrics Example em55x55 H1 font @ 192dpi: _ _ _ ___ <- y:0 (_)_(_) | | ___ /\ "diacritics circumflex" / \ __ _ _ _ _ __ | |_ ___ _ __ || / _ \ / _` | | | | '_ \| __/ _ \ '_ \ || .ascend:30 / ___ \ (_| | |_| | |_) | || __/ | | | || max extend above baseline /_/ \_\__, |\__, | .__/ \__\___|_| |_| ___ || <- .baseline:44 __/ | __/ | | || .descend:11 |___/ |___/|_| ___ \/ max height of descenders <- .height:55 em: 55x55 ascender for "diacritics circumflex" is (h:55 - a:30 - d:11) = 14 */ typedef struct ui_gdi_ta_s { // text attributes const ui_fm_t* fm; // font metrics int32_t color_id; // <= 0 use color ui_color_t color; // ui_colors.undefined() use color_id bool measure; // measure only do not draw } ui_gdi_ta_t; typedef struct { struct { struct { ui_gdi_ta_t const normal; ui_gdi_ta_t const title; ui_gdi_ta_t const rubric; ui_gdi_ta_t const H1; ui_gdi_ta_t const H2; ui_gdi_ta_t const H3; } prop; struct { ui_gdi_ta_t const normal; ui_gdi_ta_t const title; ui_gdi_ta_t const rubric; ui_gdi_ta_t const H1; ui_gdi_ta_t const H2; ui_gdi_ta_t const H3; } mono; } const ta; void (*init)(void); void (*fini)(void); void (*begin)(ui_bitmap_t* bitmap_or_null); // all paint must be done in between void (*end)(void); // TODO: move to ui_colors uint32_t (*color_rgb)(ui_color_t c); // rgb color // bpp bytes (not bits!) per pixel. bpp = -3 or -4 does not swap RGB to BRG: void (*bitmap_init)(ui_bitmap_t* bitmap, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels); void (*bitmap_init_rgbx)(ui_bitmap_t* bitmap, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels); // sets all alphas to 0xFF void (*bitmap_dispose)(ui_bitmap_t* bitmap); void (*set_clip)(int32_t x, int32_t y, int32_t w, int32_t h); // use set_clip(0, 0, 0, 0) to clear clip region void (*pixel)(int32_t x, int32_t y, ui_color_t c); void (*line)(int32_t x0, int32_t y1, int32_t x2, int32_t y2, ui_color_t c); void (*frame)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c); void (*rect)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t border, ui_color_t fill); void (*fill)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c); void (*poly)(ui_point_t* points, int32_t count, ui_color_t c); void (*circle)(int32_t center_x, int32_t center_y, int32_t odd_radius, ui_color_t border, ui_color_t fill); void (*rounded)(int32_t x, int32_t y, int32_t w, int32_t h, int32_t odd_radius, ui_color_t border, ui_color_t fill); void (*gradient)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t rgba_from, ui_color_t rgba_to, bool vertical); // dx, dy, dw, dh destination rectangle // ix, iy, iw, ih rectangle inside pixels[height][width] void (*pixels)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, int32_t bpp, const uint8_t* pixels); // bytes per pixel void (*greyscale)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); void (*bgr)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); void (*bgrx)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t x, int32_t y, int32_t w, int32_t h, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); // alpha() blend only works with device allocated bitmaps void (*alpha)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* bitmap, fp64_t alpha); // alpha blend // bitmap() only works with device allocated bitmaps void (*bitmap)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* bitmap); void (*icon)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, ui_icon_t icon); // text: void (*cleartype)(bool on); // system wide change: don't use void (*font_smoothing_contrast)(int32_t c); // [1000..2202] or -1 for 1400 default ui_font_t (*create_font)(const char* family, int32_t height, int32_t quality); // custom font, quality: -1 "as is" ui_font_t (*font)(ui_font_t f, int32_t height, int32_t quality); void (*delete_font)(ui_font_t f); void (*dump_fm)(ui_font_t f); // dump font metrics void (*update_fm)(ui_fm_t* fm, ui_font_t f); // fills font metrics ui_wh_t (*text_va)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, va_list va); ui_wh_t (*text)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, ...); ui_wh_t (*multiline_va)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va); // "w" can be zero ui_wh_t (*multiline)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, ...); // x[rt_str.glyphs(utf8, bytes)] = {x0, x1, x2, ...} ui_wh_t (*glyphs_placement)(const ui_gdi_ta_t* ta, const char* utf8, int32_t bytes, int32_t x[/*glyphs + 1*/], int32_t glyphs); } ui_gdi_if; extern ui_gdi_if ui_gdi; rt_end_c ================================================ FILE: inc/ui/ui_image.h ================================================ #include "rt/rt.h" #include "ui/ui.h" rt_begin_c // "image view" // To enable zoom/pan make view focusable: // iv.focusable = true; // Field .image may have .pixels pointer and .bitmap == null. // If this is the case the direct pixels transfer to the // device is used. RGBA bitmaps must be allocated on the // device otherwise ui_gdi.rgbx() call is used and alpha // is ignored. typedef struct ui_image_s ui_image_t; typedef struct ui_image_s { union { ui_view_t view; struct ui_view_s; }; ui_bitmap_t image; // view does NOT own or dispose image->bitmap fp64_t alpha; // for rgba images // actual scale() is: z = 2 ^ (zn - 1) / 2 ^ (zd - 1) int32_t zoom; // 0..8 // 0=16:1 1=8:1 2=4:1 3=2:1 4=1:1 5=1:2 6=1:4 7=1:8 8=1:16 int32_t zn; // zoom nominator (1, 2, 3, ...) int32_t zd; // zoom denominator (1, 2, 3, ...) fp64_t sx; // shift x [0..1.0] in view coordinates fp64_t sy; // shift y [0..1.0] struct { // only visible when focused ui_view_t bar; // ui_view(span) {zoom in, zoom 1:1, zoom out, help} ui_button_t copy; // copy image to clipboard ui_button_t zoom_in; ui_button_t zoom_1t1; // 1:1 ui_button_t zoom_out; ui_button_t fit; ui_button_t fill; ui_button_t help; ui_label_t ratio; } tool; ui_point_t drag_start; fp64_t when; // to hide toolbar bool fit; // best fit into view bool fill; // fill entire view // fit and fill cannot be true at the same time // when fit: false and fill: false the zoom ratio is in effect } ui_image_t; typedef struct ui_image_if { void (*init)(ui_image_t* iv); void (*init_with)(ui_image_t* iv, const uint8_t* pixels, int32_t width, int32_t height, int32_t bpp, int32_t stride); // ration can only be: 16:1 8:1 4:1 2:1 1:1 1:2 1:4 1:8 1:16 // but ignored if .fit or .fill is true void (*ratio)(ui_image_t* iv, int32_t nominator, int32_t denominator); fp64_t (*scale)(ui_image_t* iv); // 2 ^ (zn - 1) / 2 ^ (zd - 1) ui_rect_t (*position)(ui_image_t* iv); } ui_image_if; extern ui_image_if ui_image; rt_end_c ================================================ FILE: inc/ui/ui_label.h ================================================ #pragma once #include "rt/rt_std.h" #include "ui/ui_view.h" rt_begin_c typedef ui_view_t ui_label_t; void ui_view_init_label(ui_view_t* v); // label insets and padding left/right are intentionally // smaller than button/slider/toggle controls #define ui_label(min_width_em, s) { \ .type = ui_view_label, .init = ui_view_init_label, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } // text with "&" keyboard shortcuts: void ui_label_init(ui_label_t* t, fp32_t min_w_em, const char* format, ...); void ui_label_init_va(ui_label_t* t, fp32_t min_w_em, const char* format, va_list va); // use this macro for initialization: // ui_label_t label = ui_label(min_width_em, s); // or: // label = (ui_label_t)ui_label(min_width_em, s); // which is subtle C difference of constant and // variable initialization and I did not find universal way rt_end_c ================================================ FILE: inc/ui/ui_mbx.h ================================================ #pragma once #include "rt/rt_std.h" #include "ui/ui_view.h" rt_begin_c // Options like: // "Yes"|"No"|"Abort"|"Retry"|"Ignore"|"Cancel"|"Try"|"Continue" // maximum number of choices presentable to human is 4. typedef struct { union { ui_view_t view; struct ui_view_s; }; ui_label_t label; ui_button_t button[4]; int32_t option; // -1 or option chosen by user const char** options; } ui_mbx_t; void ui_view_init_mbx(ui_view_t* v); void ui_mbx_init(ui_mbx_t* mx, const char* option[], const char* format, ...); // ui_mbx_on_choice can only be used on static mbx variables #define ui_mbx_chosen(name, s, code, ...) \ \ static char* name ## _options[] = { __VA_ARGS__, null }; \ \ static void name ## _chosen(ui_mbx_t* m, int32_t option) { \ (void)m; (void)option; /* no warnings if unused */ \ code \ } \ static \ ui_mbx_t name = { \ .view = { \ .type = ui_view_mbx, \ .init = ui_view_init_mbx, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = name ## _chosen, \ .padding = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 }, \ .insets = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 } \ }, \ .options = name ## _options \ } #define ui_mbx(s, chosen, ...) { \ .view = { \ .type = ui_view_mbx, .init = ui_view_init_mbx, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = chosen, \ .padding = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 }, \ .insets = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 } \ }, \ .options = (const char*[]){ __VA_ARGS__, null }, \ } rt_end_c ================================================ FILE: inc/ui/ui_midi.h ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include #include #ifdef __cplusplus extern "C" { #endif typedef struct ui_midi_s ui_midi_t; typedef struct ui_midi_s { uint8_t data[16 * 8]; // opaque implementation data // must return 0 if successful or error otherwise: int64_t (*notify)(ui_midi_t* midi, int64_t flags); } ui_midi_t; typedef struct { // flags bitset: int32_t const success; // when the clip is done playing int32_t const failure; // on error playing media int32_t const aborted; // on stop() call int32_t const superseded; // midi has it's own section of legacy error messages void (*error)(errno_t r, char* s, int32_t count); errno_t (*open)(ui_midi_t* midi, const char* filename); errno_t (*play)(ui_midi_t* midi); errno_t (*rewind)(ui_midi_t* midi); errno_t (*stop)(ui_midi_t* midi); errno_t (*get_volume)(ui_midi_t* midi, fp64_t *volume); errno_t (*set_volume)(ui_midi_t* midi, fp64_t volume); bool (*is_open)(ui_midi_t* midi); bool (*is_playing)(ui_midi_t* midi); void (*close)(ui_midi_t* midi); } ui_midi_if; extern ui_midi_if ui_midi; /* success: "The conditions initiating the callback function have been met." I guess meaning media is done playing... failure: "A device error occurred while the device was executing the command." aborted: "The device received a command that prevented the current conditions for initiating the callback function from being met. If a new command interrupts the current command and it also requests notification, the device sends this message only and not `superseded`". I guess meaning media is stopped playing... superseded: "The device received another command with the "notify" flag set and the current conditions for initiating the callback function have been superseded." */ #ifdef __cplusplus } #endif ================================================ FILE: inc/ui/ui_slider.h ================================================ #pragma once #include "rt/rt_std.h" #include "ui/ui_button.h" rt_begin_c typedef struct ui_slider_s ui_slider_t; typedef struct ui_slider_s { union { ui_view_t view; struct ui_view_s; }; int32_t step; fp64_t time; // time last button was pressed ui_wh_t wh; // text measurement (special case for %0*d) ui_button_t inc; // can be hidden ui_button_t dec; // can be hidden int32_t value; // for ui_slider_t range slider control int32_t value_min; int32_t value_max; // style: bool notched; // true if marked with a notches and has a thumb } ui_slider_t; void ui_view_init_slider(ui_view_t* v); void ui_slider_init(ui_slider_t* r, const char* label, fp32_t min_w_em, int32_t value_min, int32_t value_max, void (*callback)(ui_view_t* r)); // ui_slider_changed can only be used on static slider variables #define ui_slider_changed(name, s, min_width_em, mn, mx, fmt, ...) \ static void name ## _changed(ui_slider_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_slider_t name = { \ .view = { \ .type = ui_view_slider, \ .init = ui_view_init_slider, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .format = fmt, \ .callback = name ## _changed, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ }, \ .value_min = mn, .value_max = mx, .value = mn, \ } #define ui_slider(s, min_width_em, mn, mx, fmt, changed) { \ .view = { \ .type = ui_view_slider, \ .init = ui_view_init_slider, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = changed, \ .format = fmt, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ }, \ .value_min = mn, .value_max = mx, .value = mn, \ } rt_end_c ================================================ FILE: inc/ui/ui_theme.h ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "ui/ui.h" rt_begin_c enum { ui_theme_app_mode_default = 0, ui_theme_app_mode_allow_dark = 1, ui_theme_app_mode_force_dark = 2, ui_theme_app_mode_force_light = 3 }; typedef struct { bool (*is_app_dark)(void); bool (*is_system_dark)(void); bool (*are_apps_dark)(void); void (*set_preferred_app_mode)(int32_t mode); void (*flush_menu_themes)(void); void (*allow_dark_mode_for_app)(bool allow); void (*allow_dark_mode_for_window)(bool allow); void (*refresh)(void); void (*test)(void); } ui_theme_if; extern ui_theme_if ui_theme; rt_end_c ================================================ FILE: inc/ui/ui_toggle.h ================================================ #pragma once #include "rt/rt_std.h" #include "ui/ui_view.h" rt_begin_c typedef ui_view_t ui_toggle_t; // label may contain "___" which will be replaced with "On" / "Off" void ui_toggle_init(ui_toggle_t* b, const char* label, fp32_t ems, void (*callback)(ui_toggle_t* b)); void ui_view_init_toggle(ui_view_t* v); // ui_toggle_on_off can only be used on static toggle variables #define ui_toggle_on_off(name, s, min_width_em, ...) \ static void name ## _on_off(ui_toggle_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_toggle_t name = { \ .type = ui_view_toggle, \ .init = ui_view_init_toggle, \ .fm = &ui_app.fm.prop.normal, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .p.text = s, \ .callback = name ## _on_off, \ .insets = { \ .left = 1.75f, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } #define ui_toggle(s, min_width_em, on_off) { \ .type = ui_view_toggle, \ .init = ui_view_init_toggle, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = on_off, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = 1.75f, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } rt_end_c ================================================ FILE: inc/ui/ui_view.h ================================================ #pragma once #include "rt/rt_std.h" rt_begin_c enum ui_view_type_t { ui_view_stack = 'vwst', ui_view_label = 'vwlb', ui_view_mbx = 'vwmb', ui_view_button = 'vwbt', ui_view_toggle = 'vwtg', ui_view_slider = 'vwsl', ui_view_image = 'vwiv', ui_view_text = 'vwtx', ui_view_span = 'vwhs', ui_view_list = 'vwvs', ui_view_spacer = 'vwsp', ui_view_scroll = 'vwsc' }; typedef struct ui_view_s ui_view_t; typedef struct ui_view_private_s { // do not access directly char text[1024]; // utf8 zero terminated int32_t strid; // 0 for not yet localized, -1 no localization fp64_t armed_until; // rt_clock.seconds() - when to release fp64_t hover_when; // time in seconds when to call hovered() // use: ui_view.string(v) and ui_view.set_string() } ui_view_private_t; typedef struct ui_view_text_metrics_s { // ui_view.measure_text() fills these attributes: ui_wh_t wh; // text width and height ui_point_t xy; // text offset inside view bool multiline; // text contains "\n" } ui_view_text_metrics_t; typedef struct ui_view_s { enum ui_view_type_t type; ui_view_private_t p; // private void (*init)(ui_view_t* v); // called once before first layout ui_view_t* parent; ui_view_t* child; // first child, circular doubly linked list ui_view_t* prev; // left or top sibling ui_view_t* next; // right or top sibling int32_t x; int32_t y; int32_t w; int32_t h; ui_margins_t insets; ui_margins_t padding; ui_view_text_metrics_t text; // see ui.alignment values int32_t align; // align inside parent int32_t text_align; // align of the text inside control int32_t max_w; // > 0 maximum width in pixels the view agrees to int32_t max_h; // > 0 maximum height in pixels fp32_t min_w_em; // > 0 minimum width of a view in "em"s fp32_t min_h_em; // > 0 minimum height of a view in "em"s ui_icon_t icon; // used instead of text if != null // updated on layout() call const ui_fm_t* fm; // font metrics int32_t shortcut; // keyboard shortcut void* that; // for the application use void (*notify)(ui_view_t* v, void* p); // for the application use // two pass layout: measure() .w, .h layout() .x .y // first measure() bottom up - children.layout before parent.layout // second layout() top down - parent.layout before children.layout // before methods: called before measure()/layout()/paint() void (*prepare)(ui_view_t* v); // called before measure() void (*measure)(ui_view_t* v); // determine w, h (bottom up) void (*measured)(ui_view_t* v); // called after measure() void (*layout)(ui_view_t* v); // set x, y possibly adjust w, h (top down) void (*composed)(ui_view_t* v); // after layout() is done (laid out) void (*erase)(ui_view_t* v); // called before paint() void (*paint)(ui_view_t* v); void (*painted)(ui_view_t* v); // called after paint() // composed() is effectively called right before paint() and // can be used to prepare for painting w/o need to override paint() void (*debug_paint)(ui_view_t* v); // called if .debug is set to true // any message: bool (*message)(ui_view_t* v, int32_t message, int64_t wp, int64_t lp, int64_t* rt); // return true and value in rt to stop processing void (*click)(ui_view_t* v); // ui click callback - view action void (*format)(ui_view_t* v); // format a value to text (e.g. slider) void (*callback)(ui_view_t* v); // state change callback void (*mouse_scroll)(ui_view_t* v, ui_point_t dx_dy); // touchpad scroll void (*mouse_hover)(ui_view_t* v); // hover over void (*mouse_move)(ui_view_t* v); void (*double_click)(ui_view_t* v, int32_t ix); // tap(ui, button_index) press(ui, button_index) see note below // button index 0: left, 1: middle, 2: right // bottom up (leaves to root or children to parent) // return true if consumed (halts further calls up the tree) bool (*tap)(ui_view_t* v, int32_t ix, bool pressed); // single click/tap inside ui bool (*long_press)(ui_view_t* v, int32_t ix); // two finger click/tap or long press bool (*double_tap)(ui_view_t* v, int32_t ix); // legacy double click bool (*context_menu)(ui_view_t* v); // right mouse click or long press void (*focus_gained)(ui_view_t* v); void (*focus_lost)(ui_view_t* v); // translated from key pressed/released to utf8: void (*character)(ui_view_t* v, const char* utf8); bool (*key_pressed)(ui_view_t* v, int64_t key); // return true to stop bool (*key_released)(ui_view_t* v, int64_t key); // processing // timer() every_100ms() and every_sec() called // even for hidden and disabled views void (*timer)(ui_view_t* v, ui_timer_t id); void (*every_100ms)(ui_view_t* v); // ~10 x times per second void (*every_sec)(ui_view_t* v); // ~once a second int64_t (*hit_test)(const ui_view_t* v, ui_point_t pt); struct { bool hidden; // measure()/ layout() paint() is not called on bool disabled; // mouse, keyboard, key_up/down not called on bool armed; // button is pressed but not yet released bool hover; // cursor is hovering over the control bool pressed; // for ui_button_t and ui_toggle_t } state; // TODO: instead of flat color scheme: undefined colors for // border rounded gradient etc. bool flat; // no-border appearance of controls bool flip; // flip button pressed / released bool focusable; // can be target for keyboard focus bool highlightable; // paint highlight rectangle when hover over label ui_color_t color; // interpretation depends on view type int32_t color_id; // 0 is default meaning use color ui_color_t background; // interpretation depends on view type int32_t background_id; // 0 is default meaning use background char hint[256]; // tooltip hint text (to be shown while hovering over view) struct { struct { bool prc; // paint rect bool mt; // measure text } trace; struct { // after painted(): bool call; // v->debug_paint() bool margins; // call debug_paint_margins() bool fm; // paint font metrics } paint; const char* id; // for debugging purposes } debug; // debug flags } ui_view_t; // tap() / press() APIs guarantee that single tap() is not coming // before fp64_t tap/click in expense of fp64_t click delay (0.5 seconds) // which is OK for buttons and many other UI controls but absolutely not // OK for text editing. Thus edit uses raw mouse events to react // on clicks and fp64_t clicks. typedef struct ui_view_if { // children va_args must be null terminated ui_view_t* (*add)(ui_view_t* parent, ...); void (*add_first)(ui_view_t* parent, ui_view_t* child); void (*add_last)(ui_view_t* parent, ui_view_t* child); void (*add_after)(ui_view_t* child, ui_view_t* after); void (*add_before)(ui_view_t* child, ui_view_t* before); void (*remove)(ui_view_t* v); // removes view from it`s parent void (*remove_all)(ui_view_t* parent); // removes all children void (*disband)(ui_view_t* parent); // removes all children recursively bool (*is_parent_of)(const ui_view_t* p, const ui_view_t* c); bool (*inside)(const ui_view_t* v, const ui_point_t* pt); ui_ltrb_t (*margins)(const ui_view_t* v, const ui_margins_t* g); // to pixels void (*inbox)(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* insets); void (*outbox)(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* padding); void (*set_text)(ui_view_t* v, const char* format, ...); void (*set_text_va)(ui_view_t* v, const char* format, va_list va); // ui_view.invalidate() prone to 30ms delays don't use in r/t video code // ui_view.invalidate(v, ui_app.crc) invalidates whole client rect but // ui_view.redraw() (fast non blocking) is much better instead void (*invalidate)(const ui_view_t* v, const ui_rect_t* rect_or_null); bool (*is_orphan)(const ui_view_t* v); // view parent chain has null bool (*is_hidden)(const ui_view_t* v); // view or any parent is hidden bool (*is_disabled)(const ui_view_t* v); // view or any parent is disabled bool (*is_control)(const ui_view_t* v); bool (*is_container)(const ui_view_t* v); bool (*is_spacer)(const ui_view_t* v); const char* (*string)(ui_view_t* v); // returns localized text void (*timer)(ui_view_t* v, ui_timer_t id); void (*every_sec)(ui_view_t* v); void (*every_100ms)(ui_view_t* v); int64_t (*hit_test)(const ui_view_t* v, ui_point_t pt); // key_pressed() key_released() return true to stop further processing bool (*key_pressed)(ui_view_t* v, int64_t v_key); bool (*key_released)(ui_view_t* v, int64_t v_key); void (*character)(ui_view_t* v, const char* utf8); void (*paint)(ui_view_t* v); bool (*has_focus)(const ui_view_t* v); // ui_app.focused() && ui_app.focus == v void (*set_focus)(ui_view_t* view_or_null); void (*lose_hidden_focus)(ui_view_t* v); void (*hovering)(ui_view_t* v, bool start); void (*mouse_hover)(ui_view_t* v); // hover over void (*mouse_move)(ui_view_t* v); void (*mouse_scroll)(ui_view_t* v, ui_point_t dx_dy); // touchpad scroll ui_wh_t (*text_metrics_va)(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, va_list va); ui_wh_t (*text_metrics)(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, ...); void (*text_measure)(ui_view_t* v, const char* s, ui_view_text_metrics_t* tm); void (*text_align)(ui_view_t* v, ui_view_text_metrics_t* tm); void (*measure_text)(ui_view_t* v); // fills v->text.mt and .xy // measure_control(): control is special case with v->text.mt and .xy void (*measure_control)(ui_view_t* v); void (*measure_children)(ui_view_t* v); void (*layout_children)(ui_view_t* v); void (*measure)(ui_view_t* v); void (*layout)(ui_view_t* v); void (*hover_changed)(ui_view_t* v); bool (*is_shortcut_key)(ui_view_t* v, int64_t key); bool (*context_menu)(ui_view_t* v); // `ix` 0: left 1: middle 2: right bool (*tap)(ui_view_t* v, int32_t ix, bool pressed); bool (*long_press)(ui_view_t* v, int32_t ix); bool (*double_tap)(ui_view_t* v, int32_t ix); bool (*message)(ui_view_t* v, int32_t m, int64_t wp, int64_t lp, int64_t* ret); void (*debug_paint_margins)(ui_view_t* v); // insets padding void (*debug_paint_fm)(ui_view_t* v); // text font metrics void (*test)(void); } ui_view_if; extern ui_view_if ui_view; // view children iterator: #define ui_view_for_each_begin(v, it) do { \ ui_view_t* it = (v)->child; \ if (it != null) { \ do { \ #define ui_view_for_each_end(v, it) \ it = it->next; \ } while (it != (v)->child); \ } \ } while (0) #define ui_view_for_each(v, it, ...) \ ui_view_for_each_begin(v, it) \ { __VA_ARGS__ } \ ui_view_for_each_end(v, it) #define ui_view_debug_id(v) \ ((v)->debug.id != null ? (v)->debug.id : (v)->p.text) // #define code(statements) statements // // used as: // { // macro({ // foo(); // bar(); // }) // } // // except in m4 preprocessor loses new line // between foo() and bar() and makes debugging and // using __LINE__ difficult to impossible. // // Also // #define code(...) { __VA_ARGS__ } // is way easier on preprocessor // ui_view_insets (fractions of 1/2 to keep float calculations precise): #define ui_view_i_lr (0.750f) // 3/4 of "em.w" on left and right #define ui_view_i_tb (0.125f) // 1/8 em // ui_view_padding #define ui_view_p_lr (0.375f) #define ui_view_p_tb (0.250f) #define ui_view_call_init(v) do { \ if ((v)->init != null) { \ void (*_init_)(ui_view_t* _v_) = (v)->init; \ (v)->init = null; /* before! call */ \ _init_((v)); \ } \ } while (0) rt_end_c ================================================ FILE: inc/ut_std.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #define rt_stringify(x) #x #define rt_tostring(x) rt_stringify(x) #define rt_pragma(x) _Pragma(rt_tostring(x)) #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #pragma GCC diagnostic ignored "-Wfour-char-constants" #pragma GCC diagnostic ignored "-Wmissing-field-initializers" #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wmissing-noreturn" #pragma GCC diagnostic ignored "-Wdouble-promotion" #pragma GCC diagnostic ignored "-Wcast-align" #pragma GCC diagnostic ignored "-Waddress-of-packed-member" #pragma GCC diagnostic ignored "-Wused-but-marked-unused" // because in debug only #define rt_msvc_pragma(x) #define rt_gcc_pragma(x) rt_pragma(x) #else #define rt_gcc_pragma(x) #define rt_msvc_pragma(x) rt_pragma(x) #endif #ifdef _MSC_VER #define rt_suppress_constant_cond_exp _Pragma("warning(suppress: 4127)") #else #define rt_suppress_constant_cond_exp #endif // Type aliases for floating-point types similar to typedef float fp32_t; typedef double fp64_t; // "long fp64_t" is required by C standard but the bitness // of it is not specified. #ifdef __cplusplus #define rt_begin_c extern "C" { #define rt_end_c } // extern "C" #else #define rt_begin_c // C headers compiled as C++ #define rt_end_c #endif // rt_countof() and rt_countof() are suitable for // small < 2^31 element arrays #define rt_countof(a) ((int32_t)((int)(sizeof(a) / sizeof((a)[0])))) #if defined(__GNUC__) || defined(__clang__) #define rt_force_inline __attribute__((always_inline)) #elif defined(_MSC_VER) #define rt_force_inline __forceinline #endif #ifndef __cplusplus #define null ((void*)0) // better than NULL which is zero #else #define null nullptr #endif #if defined(_MSC_VER) #define rt_thread_local __declspec(thread) #else #ifndef __cplusplus #define rt_thread_local _Thread_local // C99 #else // C++ supports rt_thread_local keyword #endif #endif // rt_begin_packed rt_end_packed // usage: typedef rt_begin_packed struct foo_s { ... } rt_end_packed foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_attribute_packed __attribute__((packed)) #define rt_begin_packed #define rt_end_packed rt_attribute_packed #else #define rt_begin_packed rt_pragma( pack(push, 1) ) #define rt_end_packed rt_pragma( pack(pop) ) #define rt_attribute_packed #endif // usage: typedef struct rt_aligned_8 foo_s { ... } foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_aligned_8 __attribute__((aligned(8))) #elif defined(_MSC_VER) #define rt_aligned_8 __declspec(align(8)) #else #define rt_aligned_8 #endif // In callbacks the formal parameters are // frequently unused. Also sometimes parameters // are used in debug configuration only (e.g. rt_assert() checks) // but not in release. // C does not have anonymous parameters like C++ // Instead of: // void foo(param_type_t param) { (void)param; / *unused */ } // use: // vod foo(param_type_t rt_unused(param)) { } #if defined(__GNUC__) || defined(__clang__) #define rt_unused(name) name __attribute__((unused)) #elif defined(_MSC_VER) #define rt_unused(name) _Pragma("warning(suppress: 4100)") name #else #define rt_unused(name) name #endif // Because MS C compiler is unhappy about alloca() and // does not implement (C99 optional) dynamic arrays on the stack: #define rt_stackalloc(n) (_Pragma("warning(suppress: 6255 6263)") alloca(n)) // alloca() is messy and in general is a not a good idea. // try to avoid if possible. Stack sizes vary from 64KB to 8MB in 2024. ================================================ FILE: inc/ut_win32.h ================================================ #pragma once #ifdef WIN32 #pragma warning(push) #pragma warning(disable: 4255) // no function prototype: '()' to '(void)' #pragma warning(disable: 4459) // declaration of '...' hides global declaration #pragma push_macro("UNICODE") #define UNICODE // always because otherwise IME does not work // ut: #include // used by: #include // both rt_loader.c and rt_processes.c #include // rt_processes.c #include // rt_processes.c #include // for knownfolders #include // rt_files.c #include // rt_files.c #include // rt_files.c #include // rt_files.c // ui: #include #include #include #include #include #include #include #include #include #pragma pop_macro("UNICODE") #pragma warning(pop) #include #define rt_export __declspec(dllexport) // Win32 API BOOL -> errno_t translation #define rt_b2e(call) ((errno_t)(call ? 0 : GetLastError())) void rt_win32_close_handle(void* h); /* translate ix to error */ errno_t rt_wait_ix2e(uint32_t r); #endif // WIN32 ================================================ FILE: msvc2022/.editorconfig ================================================ # top-most EditorConfig file for this level root = true [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] spelling_languages = en-us spelling_checkable_types = strings,identifiers,comments spelling_exclusion_path = .\exclusion.dic spelling_error_severity = hint # Visual C++ Code Style settings cpp_generate_documentation_comments = none # Visual C++ Formatting settings cpp_indent_braces = false cpp_indent_multi_line_relative_to = innermost_parenthesis cpp_indent_within_parentheses = indent cpp_indent_preserve_within_parentheses = true cpp_indent_case_contents = true cpp_indent_case_labels = true cpp_indent_case_contents_when_block = false cpp_indent_lambda_braces_when_parameter = true cpp_indent_goto_labels = one_left cpp_indent_preprocessor = none cpp_indent_access_specifiers = false cpp_indent_namespace_contents = true cpp_indent_preserve_comments = true cpp_new_line_before_open_brace_namespace = ignore cpp_new_line_before_open_brace_type = ignore cpp_new_line_before_open_brace_function = ignore cpp_new_line_before_open_brace_block = ignore cpp_new_line_before_open_brace_lambda = ignore cpp_new_line_scope_braces_on_separate_lines = false cpp_new_line_close_brace_same_line_empty_type = true cpp_new_line_close_brace_same_line_empty_function = true cpp_new_line_before_catch = false cpp_new_line_before_else = false cpp_new_line_before_while_in_do_while = false cpp_space_before_function_open_parenthesis = remove cpp_space_within_parameter_list_parentheses = false cpp_space_between_empty_parameter_list_parentheses = false cpp_space_after_keywords_in_control_flow_statements = true cpp_space_within_control_flow_statement_parentheses = false cpp_space_before_lambda_open_parenthesis = false cpp_space_within_cast_parentheses = false cpp_space_after_cast_close_parenthesis = false cpp_space_within_expression_parentheses = false cpp_space_before_block_open_brace = true cpp_space_between_empty_braces = false cpp_space_before_initializer_list_open_brace = false cpp_space_within_initializer_list_braces = true cpp_space_preserve_in_initializer_list = true cpp_space_before_open_square_bracket = false cpp_space_within_square_brackets = false cpp_space_before_empty_square_brackets = false cpp_space_between_empty_square_brackets = false cpp_space_group_square_brackets = true cpp_space_within_lambda_brackets = false cpp_space_between_empty_lambda_brackets = false cpp_space_before_comma = false cpp_space_after_comma = true cpp_space_remove_around_member_operators = true cpp_space_before_inheritance_colon = true cpp_space_before_constructor_colon = true cpp_space_remove_before_semicolon = true cpp_space_after_semicolon = true cpp_space_remove_around_unary_operator = true cpp_space_around_binary_operator = insert cpp_space_around_assignment_operator = insert cpp_space_pointer_reference_alignment = left cpp_space_around_ternary_operator = insert cpp_use_unreal_engine_macro_formatting = true cpp_wrap_preserve_blocks = one_liners # Visual C++ Inlcude Cleanup settings cpp_include_cleanup_add_missing_error_tag_type = suggestion cpp_include_cleanup_remove_unused_error_tag_type = dimmed cpp_include_cleanup_optimize_unused_error_tag_type = suggestion cpp_include_cleanup_sort_after_edits = false cpp_sort_includes_error_tag_type = none cpp_sort_includes_priority_case_sensitive = false cpp_sort_includes_priority_style = quoted cpp_includes_style = default cpp_includes_use_forward_slash = true ================================================ FILE: msvc2022/amalgamate.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9} Win32Proj amalgamate amalgamate v143 v143 v143 v143 Console rem pushd $(ProjectDir).. && $(TargetPath) ut > $(ProjectDir)..\sfh\ut.h && popd Console rem pushd $(ProjectDir).. && $(TargetPath) ut > $(ProjectDir)..\sfh\ut.h && popd Console rem pushd $(ProjectDir).. && $(TargetPath) ut > $(ProjectDir)..\sfh\ut.h && popd Console rem pushd $(ProjectDir).. && $(TargetPath) ut > $(ProjectDir)..\sfh\ut.h && popd ================================================ FILE: msvc2022/amalgamate.vcxproj.filters ================================================  ================================================ FILE: msvc2022/common.props ================================================ $(ProjectDir)..;%(AdditionalIncludeDirectories) $(ProjectDir)..\inc;%(AdditionalIncludeDirectories) $(ProjectDir)..\src;%(AdditionalIncludeDirectories) $(ProjectDir)..\src\samples;%(AdditionalIncludeDirectories) WIN32;%(PreprocessorDefinitions) WINDOWS;%(PreprocessorDefinitions) STRICT;%(PreprocessorDefinitions); WIN32_LEAN_AND_MEAN;%(PreprocessorDefinitions) VC_EXTRALEAN;%(PreprocessorDefinitions) _CRT_NONSTDC_NO_WARNINGS;%(PreprocessorDefinitions) CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions) _CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions) _SCL_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) _CRT_SECURE_NO_WARNINGS=1;%(PreprocessorDefinitions) EnableAllWarnings 4710;%(DisableSpecificWarnings) 4711;%(DisableSpecificWarnings) 4820;%(DisableSpecificWarnings) 5045;%(DisableSpecificWarnings) 6262;%(DisableSpecificWarnings) 4746;%(DisableSpecificWarnings) 4133;%(TreatSpecificWarningsAsErrors) 4020;%(TreatSpecificWarningsAsErrors) 4716;%(TreatSpecificWarningsAsErrors) 4013;%(TreatSpecificWarningsAsErrors) 4312;%(TreatSpecificWarningsAsErrors) 4087;%(TreatSpecificWarningsAsErrors) 4033;%(TreatSpecificWarningsAsErrors) 4098;%(TreatSpecificWarningsAsErrors) 4047;%(TreatSpecificWarningsAsErrors) 4057;%(TreatSpecificWarningsAsErrors) 4113;%(TreatSpecificWarningsAsErrors) 4700;%(TreatSpecificWarningsAsErrors) 4431;%(TreatSpecificWarningsAsErrors) OldStyle /std:c17 /volatile:ms /experimental:c11atomics %(AdditionalOptions) $(OutDir)$(TargetName)$(TargetExt) $(CoreLibraryDependencies);%(AdditionalDependencies) $(ProjectDir)..\inc;$(ProjectDir)..\src;$(ProjectDir)..\src\samples NDEBUG;$(DefineConstants);%(PreprocessorDefinitions) $(ProjectDir)..\bin\$(Configuration)\$(Platform)\ $(ProjectDir)..\build\$(Configuration)\$(Platform)\$(ProjectName)\ false true Disabled MultiByte $(ProjectDir)..\bin\$(Configuration)\$(Platform)\ $(ProjectDir)..\build\$(Configuration)\$(Platform)\$(ProjectName)\ false false Full MultiByte false _DEBUG;DEBUG;%(PreprocessorDefinitions) MultiThreadedDebug stdc17 stdcpp20 false Disabled false _DEBUG;DEBUG;$(DefineConstants);%(PreprocessorDefinitions) true false false true NDEBUG;%(PreprocessorDefinitions) MultiThreaded stdc17 stdcpp20 Full true AnySuitable true NDEBUG true UseLinkTimeCodeGeneration true false true ================================================ FILE: msvc2022/exclusion.dic ================================================ #### .editorconfig seems to be ignored #### merge manually for not with: #### C:\Users\leo\AppData\Local\Microsoft\VisualStudio\17.0_0790c7c1\exclusion.dic Dacl Hant Hilight Hola Inteli Intelisense Intelli dxgi dwrite swapchain rect traceln idof dism segoe Phur ntdll doserrno countof hcore shcore fdips fdip bgra alice indian navajo tabstop gainsboro peru argb zoomer ltrb printfs debugln Kuznetsov trols println perrno muldiv dbghelp comctl comdlg dwmapi msimg uxtheme Toolhelp strpintf Latn defau Formee cleartype rgbx bgrx Hilight wingdi prussian charleston fogra klein zaffre falu byzantium jazzberry french sacramento persian timberwolf payne ibeam nwse nesw relayout fuzzer advapi psapi shlwapi imagehlp posix mkdirs rmdirs unmap hwair dword sqlite otms rgrc Intelli egypt sfence respone abcdefghi mollit anim cupidatat commodo aute amet consectetur adipiscing elit eiusmod tempor laborum deserunt occaecat voluptate veniam quis incididunt nostrud ullamco aliqua laboris aliquip consequat irure reprehenderit enim velit proident esse cillum fugiat Excepteur nulla officia pariatur pellentesque sapien faucibus quisque pretium duis lacus fringilla vivamus placerat tellus nisl massa iaculis metus egestas bibendum urna curabitur facilisi cubilia curae habitasse platea dictumst morbi senectus netus suscipit feugiat tristique accumsan maecenas potenti ultricies praesent felis venenatis ultrices proin primis vulputate ornare sagittis vehicula ullamcorper rutrum cras eleifend turpis imperdiet mollis nullam volutpat porttitor lectus augue arcu dignissim aliquam elementum euismod quam justo congue sollicitudin erat viverra tincidunt facilisis dapibus etiam interdum condimentum neque tortor luctus nibh finibus aenean malesuada scelerisque nunc posuere hendrerit aptent taciti sociosqu litora torquent conubia inceptos himenaeos orci varius purus natoque penatibus magnis montes nascetur ridiculus donec rhoncus lobortis molestie mattis eget odio phasellus efficitur laoreet mauris fusce risus blandit suspendisse aliquet sodales pgup pgdw istarts iends chinese ================================================ FILE: msvc2022/groot.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3} Win32Proj groot groot Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug Windows Windows MultiThreaded Windows Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {8b9ac256-a764-474a-ad7a-31411fe694e1} {9b9ac256-a764-474a-ad7a-31411fe694e2} true ================================================ FILE: msvc2022/groot.vcxproj.filters ================================================  ================================================ FILE: msvc2022/manifest.xml ================================================ UTF-8 ================================================ FILE: msvc2022/msvc2022.vssettings ================================================ falsetruetruefalsetruefalsetruetruetrue00truetruefalsefalsefalse0%vsspv_vs_localappdata_dir%\settings\CurrentSettings.vssettingsInstallWhileDownloadingtrue%vsspv_user_appdata%\source\repostruefalsetrue21true%vsspv_visualstudio_dir%\Templates\ItemTemplatestruetrue101%vsspv_visualstudio_dir%\Templates\ProjectTemplatesfalse4truefalsetrue1true3falseHACK:2TODO:2UNDONE:2UnresolvedMergeConflict:3falsefalse https://go.microsoft.com/fwlink/?LinkId=32722&clcid=%VSSPV_LCID_HEX% https://go.microsoft.com/fwlink/?LinkId=32722&clcid=%VSSPV_LCID_HEX% %systemroot%\system32\notepad.exe 1 0 1 1 0 .cpp;.cxx;.cc;.c;.c++;.cppm;.ixx;.inl;.ipp;.h;.hh;.hpp;.hxx;.h++;.hm;.inc;.rc;.resx;.idl;.rc2;.def;.odl;.asm;.asmx;.xsd;.bin;.rgs;.html;.htm;.manifesttrue0false.suo;.sln;.ncb;.sdf;.vcxproj;.csproj;.user;.vbproj;.scc;.vsscc;.vspscc;.old;.filtersfalse0false0true4truetruetruetrue0truefalsetruefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue true true true -1 true true true true false true true -1 true true true true true <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /> <CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /> 4truetruetruetrue0truetruetruefalsefalsetruetruefalsetruetruetrue58truefalse4truetruefalsetruefalsefalsetrue4truetruetruefalse72truetrue10falsefalsefalsetruetrue02false3truefalsetruefalsefalse0truefalse2true50truefalse0truetrue21truefalsefalsefalse0false0false1truetruetruefalsefalsefalsetruefalse{}[]().,:;+-*/%&|^!=<>?@#\1truetruefalse0falsefalsetruetruefalsefalsefalsetruetruefalsetruefalse0truetruefalse2true0truefalsetrue0truetruefalse0true2falsefalsefalsetrue12true0falsetruefalse5120falsefalsefalsefalsetruefalsetruefalse0falsefalsefalsefalsefalsefalse20falsefalsefalsetrue1falsefalsetruetruetruetruetruefalsetruefalsefalse2true1falsefalse5falsefalse1falsefalse0false1falsefalsefalsefalsetrue4096true3truetruetruefalsefalse0falsefalse1falsefalsefalse60false0truefalsetruefalsefalsefalsefalsefalsefalsetruetruefalsefalsefalsetrue55truefalse1falsefalsefalsetrue0falsefalsetruetruetruetrue11truetruefalsefalse2truefalsefalsefalsetrue1falsefalsefalse02truefalsetruetruefalsefalsefalsetruefalsefalsefalsefalsefalsefalse0211falsefalsefalsefalsefalse0truefalse1falsefalse16384true1https://www.bing.com/search?q={0}11falsefalsefalsefalsefalse24truetruetruetrue0truetruetruefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue4truetruetruetrue0truetruetruefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" />100001<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" />111111-1<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />110<CodeStyleOption SerializationVersion="1" Type="String" Value="public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Int32" Value="2" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />11100<CodeStyleOption SerializationVersion="1" Type="Int32" Value="2" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />1011111<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />111011000<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" />00000<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" />100<NamingPreferencesInfo SerializationVersion="5"> <SymbolSpecifications> <SymbolSpecification ID="5c545a62-b14d-460a-88d8-e936c0a39316" Name="Class"> <ApplicableSymbolKindList> <TypeKind>Class</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="23d856b4-5089-4405-83ce-749aada99153" Name="Interface"> <ApplicableSymbolKindList> <TypeKind>Interface</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="d1796e78-ff66-463f-8576-eb46416060c0" Name="Struct"> <ApplicableSymbolKindList> <TypeKind>Struct</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="d8af8dc6-1ade-441d-9947-8946922e198a" Name="Enum"> <ApplicableSymbolKindList> <TypeKind>Enum</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="408a3347-b908-4b54-a954-1355e64c1de3" Name="Delegate"> <ApplicableSymbolKindList> <TypeKind>Delegate</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="830657f6-e7e5-4830-b328-f109d3b6c165" Name="Event"> <ApplicableSymbolKindList> <SymbolKind>Event</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="390caed4-f0a9-42bb-adbb-b44c4a302a22" Name="Method"> <ApplicableSymbolKindList> <MethodKind>Ordinary</MethodKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="af410767-f189-47c6-b140-aeccf1ff242e" Name="Private Method"> <ApplicableSymbolKindList> <MethodKind>Ordinary</MethodKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Private</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="8076757e-6a4a-47f1-9b4b-ae8a3284e987" Name="Abstract Method"> <ApplicableSymbolKindList> <MethodKind>Ordinary</MethodKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList> <ModifierKind>IsAbstract</ModifierKind> </RequiredModifierList> </SymbolSpecification> <SymbolSpecification ID="16133061-a8e7-4392-92c3-1d93cd54c218" Name="Static Method"> <ApplicableSymbolKindList> <MethodKind>Ordinary</MethodKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList> <ModifierKind>IsStatic</ModifierKind> </RequiredModifierList> </SymbolSpecification> <SymbolSpecification ID="da6a2919-5aa6-4ad1-a24d-576776ed3974" Name="Property"> <ApplicableSymbolKindList> <SymbolKind>Property</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="b24a91ce-3501-4799-b6df-baf044156c83" Name="Public or Protected Field"> <ApplicableSymbolKindList> <SymbolKind>Field</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="70af42cb-1741-4027-969c-9edc4877d965" Name="Static Field"> <ApplicableSymbolKindList> <SymbolKind>Field</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList> <ModifierKind>IsStatic</ModifierKind> </RequiredModifierList> </SymbolSpecification> <SymbolSpecification ID="10790aa6-0a0b-432d-a52d-d252ca92302b" Name="Private or Internal Field"> <ApplicableSymbolKindList> <SymbolKind>Field</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="ac995be4-88de-4771-9dcc-a456a7c02d89" Name="Private or Internal Static Field"> <ApplicableSymbolKindList> <SymbolKind>Field</SymbolKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList> <ModifierKind>IsStatic</ModifierKind> </RequiredModifierList> </SymbolSpecification> <SymbolSpecification ID="2c07f5bf-bc81-4c2b-82b4-ae9b3ffd0ba4" Name="Types"> <ApplicableSymbolKindList> <TypeKind>Class</TypeKind> <TypeKind>Struct</TypeKind> <TypeKind>Interface</TypeKind> <TypeKind>Enum</TypeKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> <SymbolSpecification ID="5f3ddba1-279f-486c-801e-5c097c36dd85" Name="Non-Field Members"> <ApplicableSymbolKindList> <SymbolKind>Property</SymbolKind> <SymbolKind>Event</SymbolKind> <MethodKind>Ordinary</MethodKind> </ApplicableSymbolKindList> <ApplicableAccessibilityList> <AccessibilityKind>Public</AccessibilityKind> <AccessibilityKind>Internal</AccessibilityKind> <AccessibilityKind>Private</AccessibilityKind> <AccessibilityKind>Protected</AccessibilityKind> <AccessibilityKind>ProtectedOrInternal</AccessibilityKind> </ApplicableAccessibilityList> <RequiredModifierList /> </SymbolSpecification> </SymbolSpecifications> <NamingStyles> <NamingStyle ID="87e7c501-9948-4b53-b1eb-a6cbe918feee" Name="Pascal Case" Prefix="" Suffix="" WordSeparator="" CapitalizationScheme="PascalCase" /> <NamingStyle ID="1ecc5eb6-b5fc-49a5-a9f1-a980f3e48c92" Name="Begins with I" Prefix="I" Suffix="" WordSeparator="" CapitalizationScheme="PascalCase" /> </NamingStyles> <NamingRules> <SerializableNamingRule SymbolSpecificationID="23d856b4-5089-4405-83ce-749aada99153" NamingStyleID="1ecc5eb6-b5fc-49a5-a9f1-a980f3e48c92" EnforcementLevel="Info" /> <SerializableNamingRule SymbolSpecificationID="2c07f5bf-bc81-4c2b-82b4-ae9b3ffd0ba4" NamingStyleID="87e7c501-9948-4b53-b1eb-a6cbe918feee" EnforcementLevel="Info" /> <SerializableNamingRule SymbolSpecificationID="5f3ddba1-279f-486c-801e-5c097c36dd85" NamingStyleID="87e7c501-9948-4b53-b1eb-a6cbe918feee" EnforcementLevel="Info" /> </NamingRules> </NamingPreferencesInfo>0<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" />000001111<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Int32" Value="2" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" />011<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />11<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />0000011<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />1101001-1<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" />1110<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" />011<CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Info" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="true" DiagnosticSeverity="Hidden" /><CodeStyleOption SerializationVersion="1" Type="Boolean" Value="false" DiagnosticSeverity="Hidden" />0111110010001114truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue4truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue4truetruetruetrue0truetruetruefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue4truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truetrue4truetruetruetrue4truetruetruetrue0truetruefalsetruefalsetruetruefalsefalsefalsetrue58falsefalse4truetruetruetrue4truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue4truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4falsetruetruetrue4truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4truetruetruetrue16244681013027014130030571648769114413810003575607202202157245271502431557746900202000-1-17257087123839990128jpg080gif8015000-12083886165-1750-11317273516765887200-1-10214748364722551001128954521521676031116750848210033012865075201800-114145511216768975072570870text/html;text/x-jquery-tmpl;text/template;text/x-handlebars;text/x-handlebars-template;text/x-jsrender1673925816777215090HTML5010500-120111190170100112167508481200-14truetruetruetrue0truetruefalsefalsefalsetruetruefalsetruetruetrue58truefalse4falsetruetruetruetruefalsetruetruetruetruefalsefalsefalsetruefalsefalsefalsefalsetruetruetruetruefalsefalsetruetruefalsetruefalsefalsetruefalse00True<ExtensionRepositoryConfigList Capacity="0" xmlns="clr-namespace:Microsoft.VisualStudio.ExtensionManager;assembly=Microsoft.VisualStudio.ExtensionEngineContract" />Truefalsetruefalse2{A5A527EA-CF0A-4ABF-B501-EAFE6B3BA5C6}021010truetruefalsetrue131071truetruetruetruetrue17.0.0.0F7Shift+F7Visual C++ 6Alt+F5Alt+F8Ctrl+-Ctrl+1, Ctrl+FCtrl+1, FCtrl+R, Ctrl+RCtrl+Shift+-Ctrl+Shift+Left ArrowCtrl+Shift+Right ArrowCtrl+Shift+TAlt+Left ArrowCtrl+K, Ctrl+FCtrl+0, MCtrl+0, Ctrl+MCtrl+0, PCtrl+0, Ctrl+PCtrl+0, DCtrl+0, Ctrl+DCtrl+0, BCtrl+0, Ctrl+BCtrl+0, CCtrl+0, Ctrl+CCtrl+0, ACtrl+0, Ctrl+ACtrl+0, SCtrl+0, Ctrl+SCtrl+0, HCtrl+0, Ctrl+HCtrl+0, RCtrl+0, Ctrl+RCtrl+0, GCtrl+0, Ctrl+GCtrl+0, YCtrl+0, Ctrl+YCtrl+0, Ctrl+ECtrl+0, WCtrl+0, Ctrl+WCtrl+0, EAlt+Right ArrowCtrl+Alt+Right ArrowCtrl+Alt+Left ArrowCtrl+Shift+F7F6Shift+F6Ctrl+]Ctrl+[Ctrl+Shift+F5Alt+F7Ctrl+Shift+Gfalsefalsefalsefalsetruefalsefalsefalsetruefalsefalsefalsefalsetruetruetruetruefalsefalsetruetruefalsetruefalse{B1BA9461-FC54-45B3-A484-CB6DD0B95C94}0falseTrue01falsetruefalsefalsefalseTrueFalse15146f07-d8dd-40a1-8b10-dd4ba62587cbDefaultTopTrueFalseTrueTrueTrueFalseTrueFalseFalse21560FalseTrueTrueFalseFalseTrueFalseAlphabetical 0 0 0 false 0 1 MatchCase=0 WholeWord=0 Hidden=0 Up=0 Selection=0 Block=0 KeepCase=0 SubFolders=1 KeepOpen=0 NameOnly=0 Append=0 Plain Files FindMatchCase=0 WholeWord=0 Hidden=0 Up=0 Selection=0 Block=0 KeepCase=0 SubFolders=1 KeepOpen=0 NameOnly=0 Append=0 Plain Files FindMatchCase=0 WholeWord=0 Hidden=1 Up=0 Selection=0 Block=0 KeepCase=0 SubFolders=0 KeepOpen=0 NameOnly=0 Append=0 Plain Document Find1101100111101Regex07Design|debug|NoToolWin True False true11010111101011111000100010101010011010110000001675773604736002101101110000500015000150010001000100020002000500010000100002501000500125100011001100110111000011100100110000000010120482530001001110000010100110101011032768819201011010000000115010111011031101110001101110010011110010011https://referencesource.microsoft.com/symbolshttps://msdl.microsoft.com/download/symbolsFunction: $FUNCTION, Thread: $TID $TNAMEhttps://symbols.nuget.org/download/symbols8 .\node_modules\.bin;$(DevEnvDir)\Extensions\Microsoft\Web Tools\External;$(PATH);$(DevEnvDir)\Extensions\Microsoft\Web Tools\External\git True True true false False false -1 false True True True 25 False 1falsetrue1true0truefalsetruetruetrue10240true-131000falsefalsefalsetruetruetruetruefalsefalse2falsefalse50truetruefalsefalsetruetruetruetruetruetruetrue1truetruetruetruetruetrue true false true false false True False 120 True True 8, 8 SnapLines None Warnings True True True True True None False True True True True True True True True %VsInstallDir%\xml\Schemas ================================================ FILE: msvc2022/prebuild.vcxproj ================================================ debug arm64 debug x64 release arm64 release x64 16.0 Win32Proj {9f53c795-2a93-4154-8b04-bb1829d67602} prebuild Utility v143 Utility v143 Utility v143 Utility v143 Level3 true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true Level3 true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true Level3 true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true Level3 true true true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Console true true true Document $(SolutionDir)..\scripts\prebuild.bat $(Platform) $(SolutionDir)..\scripts\prebuild.bat $(Platform) $(SolutionDir)..\scripts\prebuild.bat $(Platform) $(SolutionDir)..\scripts\prebuild.bat $(Platform) $(SolutionDir)..\inc\rt\version.h $(SolutionDir)..\inc\rt\version.h $(SolutionDir)..\inc\rt\version.h $(SolutionDir)..\inc\rt\version.h false false false false prebuild prebuild prebuild prebuild {0ea9bf0c-402b-4852-bd16-644244f0d1b8} ================================================ FILE: msvc2022/prebuild.vcxproj.filters ================================================  ================================================ FILE: msvc2022/rt.vcxproj ================================================ debug arm64 debug x64 release arm64 release x64 {1ea9bf0c-402b-4852-bd16-644244f0d1b9} 17.0 Win32Proj {8b9ac256-a764-474a-ad7a-31411fe694e1} rt StaticLibrary v143 StaticLibrary v143 StaticLibrary v143 StaticLibrary v143 true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h RT_TESTS;_DEBUG;DEBUG;%(PreprocessorDefinitions) true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h RT_TESTS;_DEBUG;DEBUG;%(PreprocessorDefinitions) MultiThreadedDebug Disabled Disabled true true true true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h RT_TESTS;NDEBUG;%(PreprocessorDefinitions) true true true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h RT_TESTS;NDEBUG;%(PreprocessorDefinitions) MultiThreaded ================================================ FILE: msvc2022/rt.vcxproj.filters ================================================  {4c769adc-9483-4a7d-86f9-288a6f0c0b7e} {024f6238-556d-4e5c-80a5-c4e0c49f8ce2} {a4450c50-a72b-444f-be12-c4ebf4fe6b4a} {c6383183-3d91-4a7b-9c26-cb1405bf1216} inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt inc\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt src\rt ================================================ FILE: msvc2022/sample1.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2} Win32Proj sample sample1 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug Windows Windows MultiThreaded Windows Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {8b9ac256-a764-474a-ad7a-31411fe694e1} {9b9ac256-a764-474a-ad7a-31411fe694e2} true ================================================ FILE: msvc2022/sample1.vcxproj.filters ================================================  ================================================ FILE: msvc2022/sample2.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43} Win32Proj sample2 sample2 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} ================================================ FILE: msvc2022/sample2.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {933dd3c6-58dd-453e-9d20-0c2269f78756} {111da25f-48b8-4da1-91c2-4b99ca8f45bb} {37839352-8a1d-4f6f-90d1-cac9f87295ae} res res ================================================ FILE: msvc2022/sample3.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43} Win32Proj sample3 sample3 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} true false ================================================ FILE: msvc2022/sample3.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {a68dae01-11a7-4240-9c5e-fe8e52b54a09} {41933a71-6dc4-43f3-bdc6-a6455b161d33} {71fad776-115b-499a-ac1d-020e1111e537} res res ================================================ FILE: msvc2022/sample4.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4} Win32Proj sample sample4 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} false ================================================ FILE: msvc2022/sample4.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {6af6d069-4c05-45fb-aa18-19f2b315b0f2} {7891cc09-ee32-488d-a56a-9d5497eb8eb4} {ab08c9f3-f30a-47cc-a6ac-e8111b064190} res res res ================================================ FILE: msvc2022/sample5.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2} Win32Proj sample sample5 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {8b9ac256-a764-474a-ad7a-31411fe694e1} false {9b9ac256-a764-474a-ad7a-31411fe694e2} false ================================================ FILE: msvc2022/sample5.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {611fddac-73f7-4eca-bb48-4cdd4434cd95} {8afe0792-28fa-4e94-8862-1af8b9675554} {0ea3b32f-55db-4ed9-82d8-a178df0955c9} res res ================================================ FILE: msvc2022/sample6.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C} Win32Proj sample sample6 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} false ================================================ FILE: msvc2022/sample6.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {85eb12e3-d695-4bdf-bdf0-18e9b0664a88} {8e38243b-66e1-443c-8007-2e77d12bbf58} {88bb860e-71d0-4b1b-a521-c1c35ba5ce20} res res res res res res ================================================ FILE: msvc2022/sample7.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D} Win32Proj sample7 sample7 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} false ================================================ FILE: msvc2022/sample7.vcxproj.filters ================================================  res res single_file_lib\ui single_file_lib\rt {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {0891e55a-05d8-46ba-b881-db39fe379194} {78dd48ce-ee1d-4d53-a958-b06af38243cc} {5b6cb884-4d07-44cd-b604-88b712143ff8} res res ================================================ FILE: msvc2022/sample8.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE} Win32Proj sample sample8 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug Windows MultiThreadedDebug Windows MultiThreaded AnySuitable true Windows MultiThreaded AnySuitable true Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} true ================================================ FILE: msvc2022/sample8.vcxproj.filters ================================================  ================================================ FILE: msvc2022/sample9.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34} Win32Proj sample9 sample9 Application true v143 MultiByte Application true v143 MultiByte Application false v143 true MultiByte Application false v143 true MultiByte NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false NativeMinimumRules.ruleset false MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreadedDebug $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows MultiThreaded $(ProjectDir)..\src\samples;$(ProjectDir)..\single_file_lib;%(AdditionalIncludeDirectories) Windows {9f53c795-2a93-4154-8b04-bb1829d67602} {9b9ac256-a764-474a-ad7a-31411fe694e2} false ================================================ FILE: msvc2022/sample9.vcxproj.filters ================================================  res res single_file_lib\rt single_file_lib\ui {c60b96ea-15fe-4796-86c9-3b9428dc82c7} {0b936939-cf81-415a-861e-4c0d87625788} {96a3d1bd-6d12-44d3-9226-8ee7bd0e87f8} {4d363118-820b-458d-b1c4-9b3cdfa6a3be} res res ================================================ FILE: msvc2022/test1.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7} Win32Proj test1 test1 v143 v143 v143 v143 true true true true Console /NOIMPLIB %(AdditionalOptions) _DEBUG;DEBUG;%(PreprocessorDefinitions) Console /NOIMPLIB %(AdditionalOptions) MSVCRT _DEBUG;DEBUG;%(PreprocessorDefinitions) MultiThreadedDebug Disabled Disabled true Console /NOIMPLIB %(AdditionalOptions) NDEBUG;%(PreprocessorDefinitions) Console /NOIMPLIB %(AdditionalOptions) NDEBUG;%(PreprocessorDefinitions) MultiThreaded {9f53c795-2a93-4154-8b04-bb1829d67602} {8b9ac256-a764-474a-ad7a-31411fe694e1} ================================================ FILE: msvc2022/test1.vcxproj.filters ================================================  {be5e3ac4-0df0-4828-adc5-d9e917138711} rc rc ================================================ FILE: msvc2022/test2.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8} Win32Proj test2 test2 v143 v143 v143 v143 true true true true Console /NOIMPLIB %(AdditionalOptions) RT_TESTS;_DEBUG;DEBUG;%(PreprocessorDefinitions) Console /NOIMPLIB %(AdditionalOptions) RT_TESTS;_DEBUG;DEBUG;%(PreprocessorDefinitions) Console /NOIMPLIB %(AdditionalOptions) RT_TESTS;NDEBUG;%(PreprocessorDefinitions) Console /NOIMPLIB %(AdditionalOptions) RT_TESTS;NDEBUG;%(PreprocessorDefinitions) {1ea9bf0c-402b-4852-bd16-644244f0d1b9} false {9f53c795-2a93-4154-8b04-bb1829d67602} {8b9ac256-a764-474a-ad7a-31411fe694e1} false ================================================ FILE: msvc2022/test2.vcxproj.filters ================================================  {965f87a5-7aa4-48a6-8f61-70479fd74ef6} {5fb84469-51a5-4305-9c2e-20874e45dff6} single_file_lib\rt ================================================ FILE: msvc2022/ui.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32804.467 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".misc", ".misc", "{FA83895A-1CED-4FC5-9F11-6AFA109CFC7D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig ..\.gitignore = ..\.gitignore ..\.github\workflows\build-on-push.yml = ..\.github\workflows\build-on-push.yml common.props = common.props exclusion.dic = exclusion.dic ..\README.md = ..\README.md EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test1", "test1.vcxproj", "{3EA9BF0C-402B-4852-BD16-644255F0D1B7}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "version", "version.vcxproj", "{0EA9BF0C-402B-4852-BD16-644244F0D1B8}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "amalgamate", "amalgamate.vcxproj", "{1EA9BF0C-402B-4852-BD16-644244F0D1B9}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "prebuild", "prebuild.vcxproj", "{9F53C795-2A93-4154-8B04-BB1829D67602}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test2", "test2.vcxproj", "{4EA9BF0C-402B-4852-BD61-644255F0D1B8}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample1", "sample1.vcxproj", "{4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ui", "ui.vcxproj", "{9B9AC256-A764-474A-AD7A-31411FE694E2}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rt", "rt.vcxproj", "{8B9AC256-A764-474A-AD7A-31411FE694E1}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample2", "sample2.vcxproj", "{0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample3", "sample3.vcxproj", "{0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample4", "sample4.vcxproj", "{4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample5", "sample5.vcxproj", "{4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample6", "sample6.vcxproj", "{8930DB4B-FF85-4434-A9FD-4251F6B0749C}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample9", "sample9.vcxproj", "{1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample7", "sample7.vcxproj", "{1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample8", "sample8.vcxproj", "{7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9CACB4C0-49E4-4B37-B45A-B36F2CAA7A15}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "groot", "groot.vcxproj", "{4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution debug|arm64 = debug|arm64 debug|x64 = debug|x64 release|arm64 = release|arm64 release|x64 = release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.debug|arm64.ActiveCfg = debug|arm64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.debug|arm64.Build.0 = debug|arm64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.debug|x64.ActiveCfg = debug|x64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.debug|x64.Build.0 = debug|x64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.release|arm64.ActiveCfg = release|arm64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.release|arm64.Build.0 = release|arm64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.release|x64.ActiveCfg = release|x64 {3EA9BF0C-402B-4852-BD16-644255F0D1B7}.release|x64.Build.0 = release|x64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.debug|arm64.ActiveCfg = debug|arm64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.debug|arm64.Build.0 = debug|arm64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.debug|x64.ActiveCfg = debug|x64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.debug|x64.Build.0 = debug|x64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.release|arm64.ActiveCfg = release|arm64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.release|arm64.Build.0 = release|arm64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.release|x64.ActiveCfg = release|x64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8}.release|x64.Build.0 = release|x64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.debug|arm64.ActiveCfg = debug|arm64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.debug|arm64.Build.0 = debug|arm64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.debug|x64.ActiveCfg = debug|x64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.debug|x64.Build.0 = debug|x64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.release|arm64.ActiveCfg = release|arm64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.release|arm64.Build.0 = release|arm64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.release|x64.ActiveCfg = release|x64 {1EA9BF0C-402B-4852-BD16-644244F0D1B9}.release|x64.Build.0 = release|x64 {9F53C795-2A93-4154-8B04-BB1829D67602}.debug|arm64.ActiveCfg = debug|arm64 {9F53C795-2A93-4154-8B04-BB1829D67602}.debug|arm64.Build.0 = debug|arm64 {9F53C795-2A93-4154-8B04-BB1829D67602}.debug|x64.ActiveCfg = debug|x64 {9F53C795-2A93-4154-8B04-BB1829D67602}.debug|x64.Build.0 = debug|x64 {9F53C795-2A93-4154-8B04-BB1829D67602}.release|arm64.ActiveCfg = release|arm64 {9F53C795-2A93-4154-8B04-BB1829D67602}.release|arm64.Build.0 = release|arm64 {9F53C795-2A93-4154-8B04-BB1829D67602}.release|x64.ActiveCfg = release|x64 {9F53C795-2A93-4154-8B04-BB1829D67602}.release|x64.Build.0 = release|x64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.debug|arm64.ActiveCfg = debug|arm64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.debug|arm64.Build.0 = debug|arm64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.debug|x64.ActiveCfg = debug|x64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.debug|x64.Build.0 = debug|x64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.release|arm64.ActiveCfg = release|arm64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.release|arm64.Build.0 = release|arm64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.release|x64.ActiveCfg = release|x64 {4EA9BF0C-402B-4852-BD61-644255F0D1B8}.release|x64.Build.0 = release|x64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.debug|arm64.ActiveCfg = Debug|arm64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.debug|arm64.Build.0 = Debug|arm64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.debug|x64.ActiveCfg = Debug|x64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.debug|x64.Build.0 = Debug|x64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.release|arm64.ActiveCfg = Release|arm64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.release|arm64.Build.0 = Release|arm64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.release|x64.ActiveCfg = Release|x64 {4A21BE1F-678C-4733-A9F0-A7BFFFCF3CC2}.release|x64.Build.0 = Release|x64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.debug|arm64.ActiveCfg = debug|arm64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.debug|arm64.Build.0 = debug|arm64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.debug|x64.ActiveCfg = debug|x64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.debug|x64.Build.0 = debug|x64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.release|arm64.ActiveCfg = release|arm64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.release|arm64.Build.0 = release|arm64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.release|x64.ActiveCfg = release|x64 {9B9AC256-A764-474A-AD7A-31411FE694E2}.release|x64.Build.0 = release|x64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.debug|arm64.ActiveCfg = debug|arm64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.debug|arm64.Build.0 = debug|arm64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.debug|x64.ActiveCfg = debug|x64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.debug|x64.Build.0 = debug|x64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.release|arm64.ActiveCfg = release|arm64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.release|arm64.Build.0 = release|arm64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.release|x64.ActiveCfg = release|x64 {8B9AC256-A764-474A-AD7A-31411FE694E1}.release|x64.Build.0 = release|x64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.debug|arm64.ActiveCfg = debug|arm64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.debug|arm64.Build.0 = debug|arm64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.debug|x64.ActiveCfg = debug|x64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.debug|x64.Build.0 = debug|x64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.release|arm64.ActiveCfg = release|arm64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.release|arm64.Build.0 = release|arm64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.release|x64.ActiveCfg = release|x64 {0B45C2EE-3A7E-46B0-8B8D-5DB62BF75A43}.release|x64.Build.0 = release|x64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.debug|arm64.ActiveCfg = debug|arm64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.debug|arm64.Build.0 = debug|arm64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.debug|x64.ActiveCfg = debug|x64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.debug|x64.Build.0 = debug|x64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.release|arm64.ActiveCfg = release|arm64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.release|arm64.Build.0 = release|arm64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.release|x64.ActiveCfg = release|x64 {0B45C2EE-3A7E-46B0-3B3D-5DB62BF75A43}.release|x64.Build.0 = release|x64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.debug|arm64.ActiveCfg = debug|arm64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.debug|arm64.Build.0 = debug|arm64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.debug|x64.ActiveCfg = debug|x64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.debug|x64.Build.0 = debug|x64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.release|arm64.ActiveCfg = release|arm64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.release|arm64.Build.0 = release|arm64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.release|x64.ActiveCfg = release|x64 {4A12BE1F-678C-4733-A9F0-A7BFFFCF3CC4}.release|x64.Build.0 = release|x64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.debug|arm64.ActiveCfg = debug|arm64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.debug|arm64.Build.0 = debug|arm64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.debug|x64.ActiveCfg = debug|x64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.debug|x64.Build.0 = debug|x64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.release|arm64.ActiveCfg = release|arm64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.release|arm64.Build.0 = release|arm64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.release|x64.ActiveCfg = release|x64 {4A21BE1F-678C-4733-A9F1-A8BFFFCF3CC2}.release|x64.Build.0 = release|x64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.debug|arm64.ActiveCfg = debug|arm64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.debug|arm64.Build.0 = debug|arm64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.debug|x64.ActiveCfg = debug|x64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.debug|x64.Build.0 = debug|x64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.release|arm64.ActiveCfg = release|arm64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.release|arm64.Build.0 = release|arm64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.release|x64.ActiveCfg = release|x64 {8930DB4B-FF85-4434-A9FD-4251F6B0749C}.release|x64.Build.0 = release|x64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.debug|arm64.ActiveCfg = debug|arm64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.debug|arm64.Build.0 = debug|arm64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.debug|x64.ActiveCfg = debug|x64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.debug|x64.Build.0 = debug|x64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.release|arm64.ActiveCfg = release|arm64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.release|arm64.Build.0 = release|arm64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.release|x64.ActiveCfg = release|x64 {1B99C2EE-3A7E-46B0-3B3D-5DB99BF75A34}.release|x64.Build.0 = release|x64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.debug|arm64.ActiveCfg = debug|arm64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.debug|arm64.Build.0 = debug|arm64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.debug|x64.ActiveCfg = debug|x64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.debug|x64.Build.0 = debug|x64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.release|arm64.ActiveCfg = release|arm64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.release|arm64.Build.0 = release|arm64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.release|x64.ActiveCfg = release|x64 {1C3416BF-18B1-5CB1-8D33-A3DACA40AB5D}.release|x64.Build.0 = release|x64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.debug|arm64.ActiveCfg = Debug|arm64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.debug|arm64.Build.0 = Debug|arm64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.debug|x64.ActiveCfg = Debug|x64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.debug|x64.Build.0 = Debug|x64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.release|arm64.ActiveCfg = Release|arm64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.release|arm64.Build.0 = Release|arm64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.release|x64.ActiveCfg = Release|x64 {7A21BE1F-678C-4733-A9F0-A7BEEECF3CCE}.release|x64.Build.0 = Release|x64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.debug|arm64.ActiveCfg = debug|arm64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.debug|arm64.Build.0 = debug|arm64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.debug|x64.ActiveCfg = debug|x64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.debug|x64.Build.0 = debug|x64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.release|arm64.ActiveCfg = release|arm64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.release|arm64.Build.0 = release|arm64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.release|x64.ActiveCfg = release|x64 {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3}.release|x64.Build.0 = release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {4A21BE1F-678C-4733-ABF0-A7BFFFCF2CC3} = {9CACB4C0-49E4-4B37-B45A-B36F2CAA7A15} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4903FF9C-ADBE-4753-BB20-C4EEE5D83493} EndGlobalSection EndGlobal ================================================ FILE: msvc2022/ui.vcxproj ================================================ debug arm64 debug x64 release arm64 release x64 {1ea9bf0c-402b-4852-bd16-644244f0d1b9} {8b9ac256-a764-474a-ad7a-31411fe694e1} 17.0 Win32Proj {9b9ac256-a764-474a-ad7a-31411fe694e2} ui StaticLibrary v143 StaticLibrary v143 StaticLibrary v143 StaticLibrary v143 true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h true true true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h true true true pushd $(ProjectDir).. && $(ProjectDir)..\bin\$(Configuration)\$(Platform)\amalgamate.exe $(ProjectName) > single_file_lib\$(ProjectName)\$(ProjectName).h && popd amalgamate "$(ProjectName)" into single_file_lib\$(ProjectName)\$(ProjectName).h ================================================ FILE: msvc2022/ui.vcxproj.filters ================================================  {4c769adc-9483-4a7d-86f9-288a6f0c0b9e} {24026435-1a60-4f78-a1fd-e3c4f12e5885} {a4450c50-a72b-444f-be12-c4ebf4fe5b4a} {72fd0a31-5562-44eb-b991-4130091643f4} inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui inc\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui src\ui ================================================ FILE: msvc2022/version.vcxproj ================================================  debug arm64 debug x64 release arm64 release x64 {0EA9BF0C-402B-4852-BD16-644244F0D1B8} Win32Proj version version v143 v143 v143 v143 Console $(CoreLibraryDependencies);%(AdditionalDependencies) Console $(CoreLibraryDependencies);%(AdditionalDependencies) Console Console ================================================ FILE: msvc2022/version.vcxproj.filters ================================================  ================================================ FILE: scripts/clean.bat ================================================ @echo off rmdir /s /q ..\bin rmdir /s /q ..\build rmdir /s /q ..\lib rmdir /s /q ..\msvc2022\.vs del /q ..\inc\ut\version.h ================================================ FILE: scripts/prebuild.bat ================================================ @echo off pushd .. if exist bin\release\%1\version.exe ( bin\release\%1\version.exe > inc/rt/version.h 2>nul goto version_done ) if exist bin\debug\%1\version.exe ( bin\debug\%1\version.exe > inc/rt/version.h 2>nul goto version_done ) :version_done popd exit /b 0 ================================================ FILE: single_file_lib/rt/rt.h ================================================ #ifndef rt_definition #define rt_definition // _________________________________ rt_std.h _________________________________ #include #include #include #include #include #include #include #include #include #include #include #include #include #define rt_stringify(x) #x #define rt_tostring(x) rt_stringify(x) #define rt_pragma(x) _Pragma(rt_tostring(x)) #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #pragma GCC diagnostic ignored "-Wfour-char-constants" #pragma GCC diagnostic ignored "-Wmissing-field-initializers" #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wmissing-noreturn" #pragma GCC diagnostic ignored "-Wdouble-promotion" #pragma GCC diagnostic ignored "-Wcast-align" #pragma GCC diagnostic ignored "-Waddress-of-packed-member" #pragma GCC diagnostic ignored "-Wused-but-marked-unused" // because in debug only #define rt_msvc_pragma(x) #define rt_gcc_pragma(x) rt_pragma(x) #else #define rt_gcc_pragma(x) #define rt_msvc_pragma(x) rt_pragma(x) #endif #ifdef _MSC_VER #define rt_suppress_constant_cond_exp _Pragma("warning(suppress: 4127)") #else #define rt_suppress_constant_cond_exp #endif // Type aliases for floating-point types similar to typedef float fp32_t; typedef double fp64_t; // "long fp64_t" is required by C standard but the bitness // of it is not specified. #ifdef __cplusplus #define rt_begin_c extern "C" { #define rt_end_c } // extern "C" #else #define rt_begin_c // C headers compiled as C++ #define rt_end_c #endif // rt_countof() and rt_countof() are suitable for // small < 2^31 element arrays #define rt_countof(a) ((int32_t)((int)(sizeof(a) / sizeof((a)[0])))) #if defined(__GNUC__) || defined(__clang__) #define rt_force_inline __attribute__((always_inline)) #elif defined(_MSC_VER) #define rt_force_inline __forceinline #endif #ifndef __cplusplus #define null ((void*)0) // better than NULL which is zero #else #define null nullptr #endif #if defined(_MSC_VER) #define rt_thread_local __declspec(thread) #else #ifndef __cplusplus #define rt_thread_local _Thread_local // C99 #else // C++ supports rt_thread_local keyword #endif #endif // rt_begin_packed rt_end_packed // usage: typedef rt_begin_packed struct foo_s { ... } rt_end_packed foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_attribute_packed __attribute__((packed)) #define rt_begin_packed #define rt_end_packed rt_attribute_packed #else #define rt_begin_packed rt_pragma( pack(push, 1) ) #define rt_end_packed rt_pragma( pack(pop) ) #define rt_attribute_packed #endif // usage: typedef struct rt_aligned_8 foo_s { ... } foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_aligned_8 __attribute__((aligned(8))) #elif defined(_MSC_VER) #define rt_aligned_8 __declspec(align(8)) #else #define rt_aligned_8 #endif // In callbacks the formal parameters are // frequently unused. Also sometimes parameters // are used in debug configuration only (e.g. rt_assert() checks) // but not in release. // C does not have anonymous parameters like C++ // Instead of: // void foo(param_type_t param) { (void)param; / *unused */ } // use: // vod foo(param_type_t rt_unused(param)) { } #if defined(__GNUC__) || defined(__clang__) #define rt_unused(name) name __attribute__((unused)) #elif defined(_MSC_VER) #define rt_unused(name) _Pragma("warning(suppress: 4100)") name #else #define rt_unused(name) name #endif // Because MS C compiler is unhappy about alloca() and // does not implement (C99 optional) dynamic arrays on the stack: #define rt_stackalloc(n) (_Pragma("warning(suppress: 6255 6263)") alloca(n)) // alloca() is messy and in general is a not a good idea. // try to avoid if possible. Stack sizes vary from 64KB to 8MB in 2024. // _________________________________ rt_str.h _________________________________ rt_begin_c typedef struct rt_str64_t { char s[64]; } rt_str64_t; typedef struct rt_str128_t { char s[128]; } rt_str128_t; typedef struct rt_str1024_t { char s[1024]; } rt_str1024_t; typedef struct rt_str32K_t { char s[32 * 1024]; } rt_str32K_t; // truncating string printf: // char s[100]; rt_str_printf(s, "Hello %s", "world"); // do not use with char* and char s[] parameters // because rt_countof(s) will be sizeof(char*) not the size of the buffer. #define rt_str_printf(s, ...) rt_str.format((s), rt_countof(s), "" __VA_ARGS__) #define rt_strerr(r) (rt_str.error((r)).s) // use only as rt_str_printf() parameter // The strings are expected to be UTF-8 encoded. // Copy functions fatal fail if the destination buffer is too small. // It is responsibility of the caller to make sure it won't happen. typedef struct { char* (*drop_const)(const char* s); // because of strstr() and alike int32_t (*len)(const char* s); int32_t (*len16)(const uint16_t* utf16); int32_t (*utf8bytes)(const char* utf8, int32_t bytes); // 0 on error int32_t (*glyphs)(const char* utf8, int32_t bytes); // -1 on error bool (*starts)(const char* s1, const char* s2); // s1 starts with s2 bool (*ends)(const char* s1, const char* s2); // s1 ends with s2 bool (*istarts)(const char* s1, const char* s2); // ignore case bool (*iends)(const char* s1, const char* s2); // ignore case // string truncation is fatal use strlen() to check at call site void (*lower)(char* d, int32_t capacity, const char* s); // ASCII only void (*upper)(char* d, int32_t capacity, const char* s); // ASCII only // utf8/utf16 conversion // If `chars` argument is -1, the function utf8_bytes includes the zero // terminating character in the conversion and the returned byte count. int32_t (*utf8_bytes)(const uint16_t* utf16, int32_t bytes); // bytes count // If `bytes` argument is -1, the function utf16_chars() includes the zero // terminating character in the conversion and the returned character count. int32_t (*utf16_chars)(const char* utf8, int32_t bytes); // chars count // utf8_bytes() and utf16_chars() return -1 on invalid UTF-8/UTF-16 // utf8_bytes(L"", -1) returns 1 for zero termination // utf16_chars("", -1) returns 1 for zero termination // chars: -1 means both source and destination are zero terminated errno_t (*utf16to8)(char* utf8, int32_t capacity, const uint16_t* utf16, int32_t chars); // bytes: -1 means both source and destination are zero terminated errno_t (*utf8to16)(uint16_t* utf16, int32_t capacity, const char* utf8, int32_t bytes); // https://compart.com/en/unicode/U+1F41E // Lady Beetle: utf16 L"\xD83D\xDC1E" utf8 "\xF0\x9F\x90\x9E" // surrogates: high low bool (*utf16_is_low_surrogate)(uint16_t utf16char); bool (*utf16_is_high_surrogate)(uint16_t utf16char); uint32_t (*utf32)(const char* utf8, int32_t bytes); // single codepoint // string formatting printf style: void (*format_va)(char* utf8, int32_t count, const char* format, va_list va); void (*format)(char* utf8, int32_t count, const char* format, ...); // format "dg" digit grouped; see below for known grouping separators: const char* (*grouping_separator)(void); // locale // Returned const char* pointer is short-living thread local and // intended to be used in the arguments list of .format() or .printf() // like functions, not stored or passed for prolonged call chains. // See implementation for details. rt_str64_t (*int64_dg)(int64_t v, bool uint, const char* gs); rt_str64_t (*int64)(int64_t v); // with UTF-8 thin space rt_str64_t (*uint64)(uint64_t v); // with UTF-8 thin space rt_str64_t (*int64_lc)(int64_t v); // with locale separator rt_str64_t (*uint64_lc)(uint64_t v); // with locale separator rt_str128_t (*fp)(const char* format, fp64_t v); // respects locale // errors to strings rt_str1024_t (*error)(int32_t error); // en-US rt_str1024_t (*error_nls)(int32_t error); // national locale string void (*test)(void); } rt_str_if; // Known grouping separators // https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping // coma "," separated decimal // other commonly used separators: // underscore "_" (Fortran, Kotlin) // apostrophe "'" (C++14, Rebol, Red) // backtick "`" // space "\x20" // thin_space "\xE2\x80\x89" Unicode: U+2009 extern rt_str_if rt_str; rt_end_c // ___________________________________ rt.h ___________________________________ // the rest is in alphabetical order (no inter dependencies) // ________________________________ rt_args.h _________________________________ rt_begin_c typedef struct { // On Unix it is responsibility of the main() to assign these values int32_t c; // argc const char** v; // argv[argc] const char** env; // rt_args.env[] is null-terminated void (*main)(int32_t argc, const char* argv[], const char** env); void (*WinMain)(void); // windows specific int32_t (*option_index)(const char* option); // e.g. option: "--verbosity" or "-v" void (*remove_at)(int32_t ix); /* argc=3 argv={"foo", "--verbose"} -> returns true; argc=1 argv={"foo"} */ bool (*option_bool)(const char* option); /* argc=3 argv={"foo", "--n", "153"} -> value==153, true; argc=1 argv={"foo"} also handles negative values (e.g. "-153") and hex (e.g. 0xBADF00D) */ bool (*option_int)(const char* option, int64_t *value); // for argc=3 argv={"foo", "--path", "bar"} // option_str("--path", option) // returns option: "bar" and argc=1 argv={"foo"} */ const char* (*option_str)(const char* option); // basename() for argc=3 argv={"/bin/foo.exe", ...} returns "foo": const char* (*basename)(void); void (*fini)(void); void (*test)(void); } rt_args_if; extern rt_args_if rt_args; /* Usage: (both main() and WinMain() could be compiled at the same time on Windows): static int run(void); int main(int argc, char* argv[], char* envp[]) { // link.exe /SUBSYSTEM:CONSOLE rt_args.main(argc, argv, envp); // Initialize args with command-line parameters int r = run(); rt_args.fini(); return r; } #include "rt/rt_win32.h" int APIENTRY WinMain(HINSTANCE inst, HINSTANCE prev, char* command, int show) { // link.exe /SUBSYSTEM:WINDOWS rt_args.WinMain(); int r = run(); rt_args.fini(); return 0; } static int run(void) { if (rt_args.option_bool("-v")) { rt_debug.verbosity.level = rt_debug.verbosity.verbose; } int64_t num = 0; if (rt_args.option_int("--number", &num)) { printf("--number: %ld\n", num); } const char* path = rt_args.option_str("--path"); if (path != null) { printf("--path: %s\n", path); } printf("rt_args.basename(): %s\n", rt_args.basename()); printf("rt_args.v[0]: %s\n", rt_args.v[0]); for (int i = 1; i < rt_args.c; i++) { printf("rt_args.v[%d]: %s\n", i, rt_args.v[i]); } return 0; } // Also see: https://github.com/leok7v/ut/blob/main/test/test1.c */ rt_end_c // ______________________________ rt_backtrace.h ______________________________ // "bt" stands for Stack Back Trace (not British Telecom) rt_begin_c enum { rt_backtrace_max_depth = 32 }; // increase if not enough enum { rt_backtrace_max_symbol = 1024 }; // MSFT symbol size limit typedef struct thread_s* rt_thread_t; typedef char rt_backtrace_symbol_t[rt_backtrace_max_symbol]; typedef char rt_backtrace_file_t[512]; typedef struct rt_backtrace_s { int32_t frames; // 0 if capture() failed uint32_t hash; errno_t error; // last error set by capture() or symbolize() void* stack[rt_backtrace_max_depth]; rt_backtrace_symbol_t symbol[rt_backtrace_max_depth]; rt_backtrace_file_t file[rt_backtrace_max_depth]; int32_t line[rt_backtrace_max_depth]; bool symbolized; } rt_backtrace_t; // calling .trace(bt, /*stop:*/"*") // stops backtrace dumping at any of the known Microsoft runtime // symbols: // "main", // "WinMain", // "BaseThreadInitThunk", // "RtlUserThreadStart", // "mainCRTStartup", // "WinMainCRTStartup", // "invoke_main" // .trace(bt, null) // provides complete backtrace to the bottom of stack typedef struct { void (*capture)(rt_backtrace_t *bt, int32_t skip); // number of frames to skip void (*context)(rt_thread_t thread, const void* context, rt_backtrace_t *bt); void (*symbolize)(rt_backtrace_t *bt); // dump backtrace into rt_println(): void (*trace)(const rt_backtrace_t* bt, const char* stop); void (*trace_self)(const char* stop); void (*trace_all_but_self)(void); // trace all threads const char* (*string)(const rt_backtrace_t* bt, char* text, int32_t count); void (*test)(void); } rt_backtrace_if; extern rt_backtrace_if rt_backtrace; #define rt_backtrace_here() do { \ rt_backtrace_t bt_ = {0}; \ rt_backtrace.capture(&bt_, 0); \ rt_backtrace.symbolize(&bt_); \ rt_backtrace.trace(&bt_, "*"); \ } while (0) rt_end_c // _______________________________ rt_atomics.h _______________________________ // Will be deprecated soon after Microsoft fully supports rt_begin_c typedef struct { void* (*exchange_ptr)(volatile void** a, void* v); // retuns previous value int32_t (*increment_int32)(volatile int32_t* a); // returns incremented int32_t (*decrement_int32)(volatile int32_t* a); // returns decremented int64_t (*increment_int64)(volatile int64_t* a); // returns incremented int64_t (*decrement_int64)(volatile int64_t* a); // returns decremented int32_t (*add_int32)(volatile int32_t* a, int32_t v); // returns result of add int64_t (*add_int64)(volatile int64_t* a, int64_t v); // returns result of add // returns the value held previously by "a" address: int32_t (*exchange_int32)(volatile int32_t* a, int32_t v); int64_t (*exchange_int64)(volatile int64_t* a, int64_t v); // compare_exchange functions compare the *a value with the comparand value. // If the *a is equal to the comparand value, the "v" value is stored in the address // specified by "a" otherwise, no operation is performed. // returns true if previous value *a was the same as "comparand" // false if *a was different from "comparand" and "a" was NOT modified. bool (*compare_exchange_int64)(volatile int64_t* a, int64_t comparand, int64_t v); bool (*compare_exchange_int32)(volatile int32_t* a, int32_t comparand, int32_t v); bool (*compare_exchange_ptr)(volatile void** a, void* comparand, void* v); void (*spinlock_acquire)(volatile int64_t* spinlock); void (*spinlock_release)(volatile int64_t* spinlock); int32_t (*load32)(volatile int32_t* a); int64_t (*load64)(volatile int64_t* a); void (*memory_fence)(void); void (*test)(void); } rt_atomics_if; extern rt_atomics_if rt_atomics; rt_end_c // ______________________________ rt_clipboard.h ______________________________ rt_begin_c typedef struct ui_bitmap_s ui_bitmap_t; typedef struct { errno_t (*put_text)(const char* s); errno_t (*get_text)(char* text, int32_t* bytes); errno_t (*put_image)(ui_bitmap_t* image); // only for Windows apps void (*test)(void); } rt_clipboard_if; extern rt_clipboard_if rt_clipboard; rt_end_c // ________________________________ rt_clock.h ________________________________ rt_begin_c typedef struct { int32_t const nsec_in_usec; // nano in micro second int32_t const nsec_in_msec; // nano in milli int32_t const nsec_in_sec; int32_t const usec_in_msec; // micro in milli int32_t const msec_in_sec; // milli in sec int32_t const usec_in_sec; // micro in sec fp64_t (*seconds)(void); // since boot uint64_t (*nanoseconds)(void); // since boot overflows in about 584.5 years uint64_t (*unix_microseconds)(void); // since January 1, 1970 uint64_t (*unix_seconds)(void); // since January 1, 1970 uint64_t (*microseconds)(void); // NOT monotonic(!) UTC since epoch January 1, 1601 uint64_t (*localtime)(void); // local time microseconds since epoch void (*utc)(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc); void (*local)(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc); void (*test)(void); } rt_clock_if; extern rt_clock_if rt_clock; rt_end_c // _______________________________ rt_config.h ________________________________ rt_begin_c // Persistent storage for configuration and other small data // related to specific application. // on Unix-like system ~/.name/key files are used. // On Window User registry (could be .dot files/folders). // "name" is customary basename of "rt_args.v[0]" typedef struct { errno_t (*save)(const char* name, const char* key, const void* data, int32_t bytes); int32_t (*size)(const char* name, const char* key); // load() returns number of actual loaded bytes: int32_t (*load)(const char* name, const char* key, void* data, int32_t bytes); errno_t (*remove)(const char* name, const char* key); errno_t (*clean)(const char* name); // remove all subkeys void (*test)(void); } rt_config_if; extern rt_config_if rt_config; rt_end_c // ________________________________ rt_core.h _________________________________ rt_begin_c typedef struct { errno_t (*err)(void); // errno or GetLastError() void (*set_err)(errno_t err); // errno = err or SetLastError() void (*abort)(void); void (*exit)(int32_t exit_code); // only 8 bits on posix void (*test)(void); struct { // posix errno_t const access_denied; // EACCES errno_t const bad_file; // EBADF errno_t const broken_pipe; // EPIPE errno_t const device_not_ready; // ENXIO errno_t const directory_not_empty; // ENOTEMPTY errno_t const disk_full; // ENOSPC errno_t const file_exists; // EEXIST errno_t const file_not_found; // ENOENT errno_t const insufficient_buffer; // E2BIG errno_t const interrupted; // EINTR errno_t const invalid_data; // EINVAL errno_t const invalid_handle; // EBADF errno_t const invalid_parameter; // EINVAL errno_t const io_error; // EIO errno_t const more_data; // ENOBUFS errno_t const name_too_long; // ENAMETOOLONG errno_t const no_child_process; // ECHILD errno_t const not_a_directory; // ENOTDIR errno_t const not_empty; // ENOTEMPTY errno_t const out_of_memory; // ENOMEM errno_t const path_not_found; // ENOENT errno_t const pipe_not_connected; // EPIPE errno_t const read_only_file; // EROFS errno_t const resource_deadlock; // EDEADLK errno_t const too_many_open_files; // EMFILE } const error; } rt_core_if; extern rt_core_if rt_core; rt_end_c // ________________________________ rt_debug.h ________________________________ rt_begin_c // debug interface essentially is: // vfprintf(stderr, format, va) // fprintf(stderr, format, ...) // with the additional convience: // 1. writing to system log (e.g. OutputDebugString() on Windows) // 2. always appending \n at the end of the line and thus flushing buffer // Warning: on Windows it is not real-time and subject to 30ms delays // that may or may not happen on some calls typedef struct { int32_t level; // global verbosity (interpretation may vary) int32_t const quiet; // 0 int32_t const info; // 1 basic information (errors and warnings) int32_t const verbose; // 2 detailed diagnostic int32_t const debug; // 3 including debug messages int32_t const trace; // 4 everything (may include nested calls) } rt_verbosity_if; typedef struct { rt_verbosity_if verbosity; int32_t (*verbosity_from_string)(const char* s); // "T" connector for outside write; return false to proceed with default bool (*tee)(const char* s, int32_t count); // return true to intercept void (*output)(const char* s, int32_t count); void (*println_va)(const char* file, int32_t line, const char* func, const char* format, va_list va); void (*println)(const char* file, int32_t line, const char* func, const char* format, ...); void (*perrno)(const char* file, int32_t line, const char* func, int32_t err_no, const char* format, ...); void (*perror)(const char* file, int32_t line, const char* func, int32_t error, const char* format, ...); bool (*is_debugger_present)(void); void (*breakpoint)(void); // no-op if debugger is not present errno_t (*raise)(uint32_t exception); struct { uint32_t const access_violation; uint32_t const datatype_misalignment; uint32_t const breakpoint; uint32_t const single_step; uint32_t const array_bounds; uint32_t const float_denormal_operand; uint32_t const float_divide_by_zero; uint32_t const float_inexact_result; uint32_t const float_invalid_operation; uint32_t const float_overflow; uint32_t const float_stack_check; uint32_t const float_underflow; uint32_t const int_divide_by_zero; uint32_t const int_overflow; uint32_t const priv_instruction; uint32_t const in_page_error; uint32_t const illegal_instruction; uint32_t const noncontinuable; uint32_t const stack_overflow; uint32_t const invalid_disposition; uint32_t const guard_page; uint32_t const invalid_handle; uint32_t const possible_deadlock; } exception; void (*test)(void); } rt_debug_if; #define rt_println(...) rt_debug.println(__FILE__, __LINE__, __func__, "" __VA_ARGS__) extern rt_debug_if rt_debug; rt_end_c // ________________________________ rt_files.h ________________________________ rt_begin_c // space for "short" 260 utf-16 characters filename in utf-8 format: typedef struct rt_file_name_s { char s[1024]; } rt_file_name_t; enum { rt_files_max_path = (int32_t)sizeof(rt_file_name_t) }; // *) typedef struct rt_file_s rt_file_t; typedef struct rt_files_stat_s { uint64_t created; uint64_t accessed; uint64_t updated; int64_t size; // bytes int64_t type; // device / folder / symlink } rt_files_stat_t; typedef struct rt_folder_s { uint8_t data[512]; // implementation specific } rt_folder_t; typedef struct { rt_file_t* const invalid; // (rt_file_t*)-1 // rt_files_stat_t.type: int32_t const type_folder; int32_t const type_symlink; int32_t const type_device; // seek() methods: int32_t const seek_set; int32_t const seek_cur; int32_t const seek_end; // open() flags int32_t const o_rd; // read only int32_t const o_wr; // write only int32_t const o_rw; // != (o_rd | o_wr) int32_t const o_append; int32_t const o_create; // opens existing or creates new int32_t const o_excl; // create fails if file exists int32_t const o_trunc; // open always truncates to empty int32_t const o_sync; // known folders ids: struct { // known folders: int32_t const home; // "c:\Users\" or "/users/" int32_t const desktop; int32_t const documents; int32_t const downloads; int32_t const music; int32_t const pictures; int32_t const videos; int32_t const shared; // "c:\Users\Public" int32_t const bin; // "c:\Program Files" aka "/bin" aka "/Applications" int32_t const data; // "c:\ProgramData" aka "/var" aka "/data" } const folder; errno_t (*open)(rt_file_t* *file, const char* filename, int32_t flags); bool (*is_valid)(rt_file_t* file); // checks both null and invalid errno_t (*seek)(rt_file_t* file, int64_t *position, int32_t method); errno_t (*stat)(rt_file_t* file, rt_files_stat_t* stat, bool follow_symlink); errno_t (*read)(rt_file_t* file, void* data, int64_t bytes, int64_t *transferred); errno_t (*write)(rt_file_t* file, const void* data, int64_t bytes, int64_t *transferred); errno_t (*flush)(rt_file_t* file); void (*close)(rt_file_t* file); errno_t (*write_fully)(const char* filename, const void* data, int64_t bytes, int64_t *transferred); bool (*exists)(const char* pathname); // does not guarantee any access writes bool (*is_folder)(const char* pathname); bool (*is_symlink)(const char* pathname); const char* (*basename)(const char* pathname); // c:\foo\bar.ext -> bar.ext errno_t (*mkdirs)(const char* pathname); // tries to deep create all folders in pathname errno_t (*rmdirs)(const char* pathname); // tries to remove folder and its subtree errno_t (*create_tmp)(char* file, int32_t count); // create temporary file errno_t (*chmod777)(const char* pathname); // and whole subtree new files and folders errno_t (*symlink)(const char* from, const char* to); // sym link "ln -s" **) errno_t (*link)(const char* from, const char* to); // hard link like "ln" errno_t (*unlink)(const char* pathname); // delete file or empty folder errno_t (*copy)(const char* from, const char* to); // allows overwriting errno_t (*move)(const char* from, const char* to); // allows overwriting errno_t (*cwd)(char* folder, int32_t count); // get current working dir errno_t (*chdir)(const char* folder); // set working directory const char* (*known_folder)(int32_t kf_id); const char* (*bin)(void); // Windows: "c:\Program Files" Un*x: "/bin" const char* (*data)(void); // Windows: "c:\ProgramData" Un*x: /data or /var const char* (*tmp)(void); // temporary folder (system or user) // There are better, native, higher performance ways to iterate thru // folders in Posix, Linux and Windows. The following is minimalistic // approach to folder content reading: errno_t (*opendir)(rt_folder_t* folder, const char* folder_name); const char* (*readdir)(rt_folder_t* folder, rt_files_stat_t* optional); void (*closedir)(rt_folder_t* folder); void (*test)(void); } rt_files_if; // *) rt_files_max_path is a compromise - way longer than Windows MAX_PATH of 260 // and somewhat shorter than 32 * 1024 Windows long path. // Use with caution understanding that it is a limitation and where it is // important heap may and should be used. Do not to rely on thread stack size // (default: 1MB on Windows, Android Linux 64KB, 512 KB on MacOS, Ubuntu 8MB) // // **) symlink on Win32 is only allowed in Admin elevated // processes and in Developer mode. extern rt_files_if rt_files; rt_end_c // ______________________________ rt_generics.h _______________________________ rt_begin_c // Most of ut/ui code is written the way of min(a,b) max(a,b) // not having side effects on the arguments and thus evaluating // them twice ain't a big deal. However, out of curiosity of // usefulness of Generic() in C11 standard here is type-safe // single evaluation of the arguments version of what could // have been simple minimum and maximum macro definitions. // Type safety comes with the cost of complexity in puritan // or stoic language like C: static inline int8_t rt_max_int8(int8_t x, int8_t y) { return x > y ? x : y; } static inline int16_t rt_max_int16(int16_t x, int16_t y) { return x > y ? x : y; } static inline int32_t rt_max_int32(int32_t x, int32_t y) { return x > y ? x : y; } static inline int64_t rt_max_int64(int64_t x, int64_t y) { return x > y ? x : y; } static inline uint8_t rt_max_uint8(uint8_t x, uint8_t y) { return x > y ? x : y; } static inline uint16_t rt_max_uint16(uint16_t x, uint16_t y) { return x > y ? x : y; } static inline uint32_t rt_max_uint32(uint32_t x, uint32_t y) { return x > y ? x : y; } static inline uint64_t rt_max_uint64(uint64_t x, uint64_t y) { return x > y ? x : y; } static inline fp32_t rt_max_fp32(fp32_t x, fp32_t y) { return x > y ? x : y; } static inline fp64_t rt_max_fp64(fp64_t x, fp64_t y) { return x > y ? x : y; } // MS cl.exe version 19.39.33523 has issues with "long": // does not pick up int32_t/uint32_t types for "long" and "unsigned long" // need to handle long / unsigned long separately: static inline long rt_max_long(long x, long y) { return x > y ? x : y; } static inline unsigned long rt_max_ulong(unsigned long x, unsigned long y) { return x > y ? x : y; } static inline int8_t rt_min_int8(int8_t x, int8_t y) { return x < y ? x : y; } static inline int16_t rt_min_int16(int16_t x, int16_t y) { return x < y ? x : y; } static inline int32_t rt_min_int32(int32_t x, int32_t y) { return x < y ? x : y; } static inline int64_t rt_min_int64(int64_t x, int64_t y) { return x < y ? x : y; } static inline uint8_t rt_min_uint8(uint8_t x, uint8_t y) { return x < y ? x : y; } static inline uint16_t rt_min_uint16(uint16_t x, uint16_t y) { return x < y ? x : y; } static inline uint32_t rt_min_uint32(uint32_t x, uint32_t y) { return x < y ? x : y; } static inline uint64_t rt_min_uint64(uint64_t x, uint64_t y) { return x < y ? x : y; } static inline fp32_t rt_min_fp32(fp32_t x, fp32_t y) { return x < y ? x : y; } static inline fp64_t rt_min_fp64(fp64_t x, fp64_t y) { return x < y ? x : y; } static inline long rt_min_long(long x, long y) { return x < y ? x : y; } static inline unsigned long rt_min_ulong(unsigned long x, unsigned long y) { return x < y ? x : y; } static inline void rt_min_undefined(void) { } static inline void rt_max_undefined(void) { } #define rt_max(X, Y) _Generic((X) + (Y), \ int8_t: rt_max_int8, \ int16_t: rt_max_int16, \ int32_t: rt_max_int32, \ int64_t: rt_max_int64, \ uint8_t: rt_max_uint8, \ uint16_t: rt_max_uint16, \ uint32_t: rt_max_uint32, \ uint64_t: rt_max_uint64, \ fp32_t: rt_max_fp32, \ fp64_t: rt_max_fp64, \ long: rt_max_long, \ unsigned long: rt_max_ulong, \ default: rt_max_undefined)(X, Y) #define rt_min(X, Y) _Generic((X) + (Y), \ int8_t: rt_min_int8, \ int16_t: rt_min_int16, \ int32_t: rt_min_int32, \ int64_t: rt_min_int64, \ uint8_t: rt_min_uint8, \ uint16_t: rt_min_uint16, \ uint32_t: rt_min_uint32, \ uint64_t: rt_min_uint64, \ fp32_t: rt_min_fp32, \ fp64_t: rt_min_fp64, \ long: rt_min_long, \ unsigned long: rt_min_ulong, \ default: rt_min_undefined)(X, Y) // The expression (X) + (Y) is used in _Generic primarily for type promotion // and compatibility between different types of the two operands. In C, when // you perform an arithmetic operation like addition between two variables, // the types of these variables undergo implicit conversions to a common type // according to the usual arithmetic conversions defined in the C standard. // This helps ensure that: // // Type Compatibility: The macro works correctly even if X and Y are of // different types. By using (X) + (Y), both X and Y are promoted to a common // type, which ensures that the macro selects the appropriate function // that can handle this common type. // // Type Safety: It ensures that the selected function can handle the type // resulting from the operation, thereby preventing type mismatches that // could lead to undefined behavior or compilation errors. typedef struct { void (*test)(void); } rt_generics_if; extern rt_generics_if rt_generics; rt_end_c // _______________________________ rt_glyphs.h ________________________________ // Square Four Corners https://www.compart.com/en/unicode/U+26F6 #define rt_glyph_square_four_corners "\xE2\x9B\xB6" // Circled Cross Formee // https://codepoints.net/U+1F902 #define rt_glyph_circled_cross_formee "\xF0\x9F\xA4\x82" // White Large Square https://www.compart.com/en/unicode/U+2B1C #define rt_glyph_white_large_square "\xE2\xAC\x9C" // N-Ary Times Operator https://www.compart.com/en/unicode/U+2A09 #define rt_glyph_n_ary_times_operator "\xE2\xA8\x89" // Heavy Minus Sign https://www.compart.com/en/unicode/U+2796 #define rt_glyph_heavy_minus_sign "\xE2\x9E\x96" // Heavy Plus Sign https://www.compart.com/en/unicode/U+2795 #define rt_glyph_heavy_plus_sign "\xE2\x9E\x95" // Clockwise Rightwards and Leftwards Open Circle Arrows with Circled One Overlay // https://www.compart.com/en/unicode/U+1F502 // rt_glyph_clockwise_rightwards_and_leftwards_open_circle_arrows_with_circled_one_overlay... #define rt_glyph_open_circle_arrows_one_overlay "\xF0\x9F\x94\x82" // Halfwidth Katakana-Hiragana Prolonged Sound Mark https://www.compart.com/en/unicode/U+FF70 #define rt_glyph_prolonged_sound_mark "\xEF\xBD\xB0" // Fullwidth Plus Sign https://www.compart.com/en/unicode/U+FF0B #define rt_glyph_fullwidth_plus_sign "\xEF\xBC\x8B" // Fullwidth Hyphen-Minus https://www.compart.com/en/unicode/U+FF0D #define rt_glyph_fullwidth_hyphen_minus "\xEF\xBC\x8D" // Heavy Multiplication X https://www.compart.com/en/unicode/U+2716 #define rt_glyph_heavy_multiplication_x "\xE2\x9C\x96" // Multiplication Sign https://www.compart.com/en/unicode/U+00D7 #define rt_glyph_multiplication_sign "\xC3\x97" // Trigram For Heaven (caption menu button) https://www.compart.com/en/unicode/U+2630 #define rt_glyph_trigram_for_heaven "\xE2\x98\xB0" // (tool bar drag handle like: msvc toolbars) // Braille Pattern Dots-12345678 https://www.compart.com/en/unicode/U+28FF #define rt_glyph_braille_pattern_dots_12345678 "\xE2\xA3\xBF" // White Square Containing Black Medium Square // https://compart.com/en/unicode/U+1F795 #define rt_glyph_white_square_containing_black_medium_square "\xF0\x9F\x9E\x95" // White Medium Square // https://compart.com/en/unicode/U+25FB #define rt_glyph_white_medium_square "\xE2\x97\xBB" // White Square with Upper Right Quadrant // https://compart.com/en/unicode/U+25F3 #define rt_glyph_white_square_with_upper_right_quadrant "\xE2\x97\xB3" // White Square with Upper Left Quadrant https://www.compart.com/en/unicode/U+25F0 #define rt_glyph_white_square_with_upper_left_quadrant "\xE2\x97\xB0" // White Square with Lower Left Quadrant https://www.compart.com/en/unicode/U+25F1 #define rt_glyph_white_square_with_lower_left_quadrant "\xE2\x97\xB1" // White Square with Lower Right Quadrant https://www.compart.com/en/unicode/U+25F2 #define rt_glyph_white_square_with_lower_right_quadrant "\xE2\x97\xB2" // White Square with Upper Right Quadrant https://www.compart.com/en/unicode/U+25F3 #define rt_glyph_white_square_with_upper_right_quadrant "\xE2\x97\xB3" // White Square with Vertical Bisecting Line https://www.compart.com/en/unicode/U+25EB #define rt_glyph_white_square_with_vertical_bisecting_line "\xE2\x97\xAB" // (White Square with Horizontal Bisecting Line) // Squared Minus https://www.compart.com/en/unicode/U+229F #define rt_glyph_squared_minus "\xE2\x8A\x9F" // North East and South West Arrow https://www.compart.com/en/unicode/U+2922 #define rt_glyph_north_east_and_south_west_arrow "\xE2\xA4\xA2" // South East Arrow to Corner https://www.compart.com/en/unicode/U+21F2 #define rt_glyph_south_east_white_arrow_to_corner "\xE2\x87\xB2" // North West Arrow to Corner https://www.compart.com/en/unicode/U+21F1 #define rt_glyph_north_west_white_arrow_to_corner "\xE2\x87\xB1" // Leftwards Arrow to Bar https://www.compart.com/en/unicode/U+21E6 #define rt_glyph_leftwards_white_arrow_to_bar "\xE2\x87\xA6" // Rightwards Arrow to Bar https://www.compart.com/en/unicode/U+21E8 #define rt_glyph_rightwards_white_arrow_to_bar "\xE2\x87\xA8" // Upwards White Arrow https://www.compart.com/en/unicode/U+21E7 #define rt_glyph_upwards_white_arrow "\xE2\x87\xA7" // Downwards White Arrow https://www.compart.com/en/unicode/U+21E9 #define rt_glyph_downwards_white_arrow "\xE2\x87\xA9" // Leftwards White Arrow https://www.compart.com/en/unicode/U+21E4 #define rt_glyph_leftwards_white_arrow "\xE2\x87\xA4" // Rightwards White Arrow https://www.compart.com/en/unicode/U+21E5 #define rt_glyph_rightwards_white_arrow "\xE2\x87\xA5" // Upwards White Arrow on Pedestal https://www.compart.com/en/unicode/U+21EB #define rt_glyph_upwards_white_arrow_on_pedestal "\xE2\x87\xAB" // Braille Pattern Dots-678 https://www.compart.com/en/unicode/U+28E0 #define rt_glyph_3_dots_tiny_right_bottom_triangle "\xE2\xA3\xA0" // Braille Pattern Dots-2345678 https://www.compart.com/en/unicode/U+28FE // Combining the two into: #define rt_glyph_dotted_right_bottom_triangle "\xE2\xA3\xA0\xE2\xA3\xBE" // Upper Right Drop-Shadowed White Square https://www.compart.com/en/unicode/U+2750 #define rt_glyph_upper_right_drop_shadowed_white_square "\xE2\x9D\x90" // No-Break Space (NBSP) // https://www.compart.com/en/unicode/U+00A0 #define rt_glyph_nbsp "\xC2\xA0" // Word Joiner (WJ) // https://compart.com/en/unicode/U+2060 #define rt_glyph_word_joiner "\xE2\x81\xA0" // Zero Width Space (ZWSP) // https://compart.com/en/unicode/U+200B #define rt_glyph_zwsp "\xE2\x80\x8B" // Zero Width Joiner (ZWJ) // https://compart.com/en/unicode/u+200D #define rt_glyph_zwj "\xE2\x80\x8D" // En Quad // https://compart.com/en/unicode/U+2000 #define rt_glyph_en_quad "\xE2\x80\x80" // Em Quad // https://compart.com/en/unicode/U+2001 #define rt_glyph_em_quad "\xE2\x80\x81" // En Space // https://compart.com/en/unicode/U+2002 #define rt_glyph_en_space "\xE2\x80\x82" // Em Space // https://compart.com/en/unicode/U+2003 #define rt_glyph_em_space "\xE2\x80\x83" // Hyphen https://www.compart.com/en/unicode/U+2010 #define rt_glyph_hyphen "\xE2\x80\x90" // Non-Breaking Hyphen https://www.compart.com/en/unicode/U+2011 #define rt_glyph_non_breaking_hyphen "\xE2\x80\x91" // Fullwidth Low Line https://www.compart.com/en/unicode/U+FF3F #define rt_glyph_fullwidth_low_line "\xEF\xBC\xBF" // #define rt_glyph_light_horizontal "\xE2\x94\x80" // Light Horizontal https://www.compart.com/en/unicode/U+2500 #define rt_glyph_light_horizontal "\xE2\x94\x80" // Three-Em Dash https://www.compart.com/en/unicode/U+2E3B #define rt_glyph_three_em_dash "\xE2\xB8\xBB" // Infinity https://www.compart.com/en/unicode/U+221E #define rt_glyph_infinity "\xE2\x88\x9E" // Black Large Circle https://www.compart.com/en/unicode/U+2B24 #define rt_glyph_black_large_circle "\xE2\xAC\xA4" // Large Circle https://www.compart.com/en/unicode/U+25EF #define rt_glyph_large_circle "\xE2\x97\xAF" // Heavy Leftwards Arrow with Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F818 #define rt_glyph_heavy_leftwards_arrow_with_equilateral_arrowhead "\xF0\x9F\xA0\x98" // Heavy Rightwards Arrow with Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81A #define rt_glyph_heavy_rightwards_arrow_with_equilateral_arrowhead "\xF0\x9F\xA0\x9A" // Heavy Leftwards Arrow with Large Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81C #define rt_glyph_heavy_leftwards_arrow_with_large_equilateral_arrowhead "\xF0\x9F\xA0\x9C" // Heavy Rightwards Arrow with Large Equilateral Arrowhead https://www.compart.com/en/unicode/U+1F81E #define rt_glyph_heavy_rightwards_arrow_with_large_equilateral_arrowhead "\xF0\x9F\xA0\x9E" // CJK Unified Ideograph-5973: Kanji Onna "Female" https://www.compart.com/en/unicode/U+5973 #define rt_glyph_kanji_onna_female "\xE2\xBC\xA5" // Leftwards Arrow https://www.compart.com/en/unicode/U+2190 #define rt_glyph_leftward_arrow "\xE2\x86\x90" // Upwards Arrow https://www.compart.com/en/unicode/U+2191 #define rt_glyph_upwards_arrow "\xE2\x86\x91" // Rightwards Arrow // https://www.compart.com/en/unicode/U+2192 #define rt_glyph_rightwards_arrow "\xE2\x86\x92" // Downwards Arrow https://www.compart.com/en/unicode/U+2193 #define rt_glyph_downwards_arrow "\xE2\x86\x93" // Thin Space https://www.compart.com/en/unicode/U+2009 #define rt_glyph_thin_space "\xE2\x80\x89" // Medium Mathematical Space (MMSP) https://www.compart.com/en/unicode/U+205F #define rt_glyph_mmsp "\xE2\x81\x9F" // Three-Per-Em Space https://www.compart.com/en/unicode/U+2004 #define rt_glyph_three_per_em "\xE2\x80\x84" // Six-Per-Em Space https://www.compart.com/en/unicode/U+2006 #define rt_glyph_six_per_em "\xE2\x80\x86" // Punctuation Space https://www.compart.com/en/unicode/U+2008 #define rt_glyph_punctuation "\xE2\x80\x88" // Hair Space https://www.compart.com/en/unicode/U+200A #define rt_glyph_hair_space "\xE2\x80\x8A" // Chinese "jin4" https://www.compart.com/en/unicode/U+58F9 #define rt_glyph_chinese_jin4 "\xE5\xA3\xB9" // Chinese "gong" https://www.compart.com/en/unicode/U+8D70 #define rt_glyph_chinese_gong "\xE8\xB5\xB0" // https://www.compart.com/en/unicode/U+1F9F8 #define rt_glyph_teddy_bear "\xF0\x9F\xA7\xB8" // https://www.compart.com/en/unicode/U+1F9CA #define rt_glyph_ice_cube "\xF0\x9F\xA7\x8A" // Speaker https://www.compart.com/en/unicode/U+1F508 #define rt_glyph_speaker "\xF0\x9F\x94\x88" // Speaker with Cancellation Stroke https://www.compart.com/en/unicode/U+1F507 #define rt_glyph_mute "\xF0\x9F\x94\x87" // TODO: this is used for Font Metric Visualization // Full Block https://www.compart.com/en/unicode/U+2588 #define rt_glyph_full_block "\xE2\x96\x88" // Black Square https://www.compart.com/en/unicode/U+25A0 #define rt_glyph_black_square "\xE2\x96\xA0" // the appearance of a dragon walking // CJK Unified Ideograph-9F98 https://www.compart.com/en/unicode/U+9F98 #define rt_glyph_walking_dragon "\xE9\xBE\x98" // possibly highest "diacritical marks" character (Vietnamese) // Latin Small Letter U with Horn and Hook Above https://www.compart.com/en/unicode/U+1EED #define rt_glyph_u_with_horn_and_hook_above "\xC7\xAD" // possibly "long descender" character // Latin Small Letter Qp Digraph https://www.compart.com/en/unicode/U+0239 #define rt_glyph_qp_digraph "\xC9\xB9" // another possibly "long descender" character // Cyrillic Small Letter Shha with Descender https://www.compart.com/en/unicode/U+0527 #define rt_glyph_shha_with_descender "\xD4\xA7" // a"very long descender" character // Tibetan Mark Caret Yig Mgo Phur Shad Ma https://www.compart.com/en/unicode/U+0F06 #define rt_glyph_caret_yig_mgo_phur_shad_ma "\xE0\xBC\x86" // Tibetan Vowel Sign Vocalic Ll https://www.compart.com/en/unicode/U+0F79 #define rt_glyph_vocalic_ll "\xE0\xBD\xB9" // https://www.compart.com/en/unicode/U+1F4A3 #define rt_glyph_bomb "\xF0\x9F\x92\xA3" // https://www.compart.com/en/unicode/U+1F4A1 #define rt_glyph_electric_light_bulb "\xF0\x9F\x92\xA1" // https://www.compart.com/en/unicode/U+1F4E2 #define rt_glyph_public_address_loudspeaker "\xF0\x9F\x93\xA2" // https://www.compart.com/en/unicode/U+1F517 #define rt_glyph_link_symbol "\xF0\x9F\x94\x97" // https://www.compart.com/en/unicode/U+1F571 #define rt_glyph_black_skull_and_crossbones "\xF0\x9F\x95\xB1" // https://www.compart.com/en/unicode/U+1F5B5 #define rt_glyph_screen "\xF0\x9F\x96\xB5" // https://www.compart.com/en/unicode/U+1F5D7 #define rt_glyph_overlap "\xF0\x9F\x97\x97" // https://www.compart.com/en/unicode/U+1F5D6 #define rt_glyph_maximize "\xF0\x9F\x97\x96" // https://www.compart.com/en/unicode/U+1F5D5 #define rt_glyph_minimize "\xF0\x9F\x97\x95" // Desktop Window // https://compart.com/en/unicode/U+1F5D4 #define rt_glyph_desktop_window "\xF0\x9F\x97\x94" // https://www.compart.com/en/unicode/U+1F5D9 #define rt_glyph_cancellation_x "\xF0\x9F\x97\x99" // https://www.compart.com/en/unicode/U+1F5DF #define rt_glyph_page_with_circled_text "\xF0\x9F\x97\x9F" // https://www.compart.com/en/unicode/U+1F533 #define rt_glyph_white_square_button "\xF0\x9F\x94\xB3" // https://www.compart.com/en/unicode/U+1F532 #define rt_glyph_black_square_button "\xF0\x9F\x94\xB2" // https://www.compart.com/en/unicode/U+1F5F9 #define rt_glyph_ballot_box_with_bold_check "\xF0\x9F\x97\xB9" // https://www.compart.com/en/unicode/U+1F5F8 #define rt_glyph_light_check_mark "\xF0\x9F\x97\xB8" // https://compart.com/en/unicode/U+1F4BB #define rt_glyph_personal_computer "\xF0\x9F\x92\xBB" // https://compart.com/en/unicode/U+1F4DC #define rt_glyph_desktop_computer "\xF0\x9F\x93\x9C" // https://compart.com/en/unicode/U+1F4DD #define rt_glyph_printer "\xF0\x9F\x93\x9D" // https://compart.com/en/unicode/U+1F4F9 #define rt_glyph_video_camera "\xF0\x9F\x93\xB9" // https://compart.com/en/unicode/U+1F4F8 #define rt_glyph_camera "\xF0\x9F\x93\xB8" // https://compart.com/en/unicode/U+1F505 #define rt_glyph_high_brightness "\xF0\x9F\x94\x85" // https://compart.com/en/unicode/U+1F506 #define rt_glyph_low_brightness "\xF0\x9F\x94\x86" // https://compart.com/en/unicode/U+1F507 #define rt_glyph_speaker_with_cancellation_stroke "\xF0\x9F\x94\x87" // https://compart.com/en/unicode/U+1F509 #define rt_glyph_speaker_with_one_sound_wave "\xF0\x9F\x94\x89" // Right-Pointing Magnifying Glass // https://compart.com/en/unicode/U+1F50E #define rt_glyph_right_pointing_magnifying_glass "\xF0\x9F\x94\x8E" // Radio Button // https://compart.com/en/unicode/U+1F518 #define rt_glyph_radio_button "\xF0\x9F\x94\x98" // https://compart.com/en/unicode/U+1F525 #define rt_glyph_fire "\xF0\x9F\x94\xA5" // Gear // https://compart.com/en/unicode/U+2699 #define rt_glyph_gear "\xE2\x9A\x99" // Nut and Bolt // https://compart.com/en/unicode/U+1F529 #define rt_glyph_nut_and_bolt "\xF0\x9F\x94\xA9" // Hammer and Wrench // https://compart.com/en/unicode/U+1F6E0 #define rt_glyph_hammer_and_wrench "\xF0\x9F\x9B\xA0" // https://compart.com/en/unicode/U+1F53E #define rt_glyph_upwards_button "\xF0\x9F\x94\xBE" // https://compart.com/en/unicode/U+1F53F #define rt_glyph_downwards_button "\xF0\x9F\x94\xBF" // https://compart.com/en/unicode/U+1F5C7 #define rt_glyph_litter_in_bin_sign "\xF0\x9F\x97\x87" // Checker Board // https://compart.com/en/unicode/U+1F67E #define rt_glyph_checker_board "\xF0\x9F\x9A\xBE" // Reverse Checker Board // https://compart.com/en/unicode/U+1F67F #define rt_glyph_reverse_checker_board "\xF0\x9F\x9A\xBF" // Clipboard // https://compart.com/en/unicode/U+1F4CB #define rt_glyph_clipboard "\xF0\x9F\x93\x8B" // Two Joined Squares https://www.compart.com/en/unicode/U+29C9 #define rt_glyph_two_joined_squares "\xE2\xA7\x89" // White Heavy Check Mark // https://compart.com/en/unicode/U+2705 #define rt_glyph_white_heavy_check_mark "\xE2\x9C\x85" // Negative Squared Cross Mark // https://compart.com/en/unicode/U+274E #define rt_glyph_negative_squared_cross_mark "\xE2\x9D\x8E" // Lower Right Drop-Shadowed White Square // https://compart.com/en/unicode/U+274F #define rt_glyph_lower_right_drop_shadowed_white_square "\xE2\x9D\x8F" // Upper Right Drop-Shadowed White Square // https://compart.com/en/unicode/U+2750 #define rt_glyph_upper_right_drop_shadowed_white_square "\xE2\x9D\x90" // Lower Right Shadowed White Square // https://compart.com/en/unicode/U+2751 #define rt_glyph_lower_right_shadowed_white_square "\xE2\x9D\x91" // Upper Right Shadowed White Square // https://compart.com/en/unicode/U+2752 #define rt_glyph_upper_right_shadowed_white_square "\xE2\x9D\x92" // Left Double Wiggly Fence // https://compart.com/en/unicode/U+29DA #define rt_glyph_left_double_wiggly_fence "\xE2\xA7\x9A" // Right Double Wiggly Fence // https://compart.com/en/unicode/U+29DB #define rt_glyph_right_double_wiggly_fence "\xE2\xA7\x9B" // Logical Or // https://compart.com/en/unicode/U+2228 #define rt_glyph_logical_or "\xE2\x88\xA8" // Logical And // https://compart.com/en/unicode/U+2227 #define rt_glyph_logical_and "\xE2\x88\xA7" // Double Vertical Bar (Pause) // https://compart.com/en/unicode/U+23F8 #define rt_glyph_double_vertical_bar "\xE2\x8F\xB8" // Black Square For Stop // https://compart.com/en/unicode/U+23F9 #define rt_glyph_black_square_for_stop "\xE2\x8F\xB9" // Black Circle For Record // https://compart.com/en/unicode/U+23FA #define rt_glyph_black_circle_for_record "\xE2\x8F\xBA" // Negative Squared Latin Capital Letter "I" // https://compart.com/en/unicode/U+1F158 #define rt_glyph_negative_squared_latin_capital_letter_i "\xF0\x9F\x85\x98" #define rt_glyph_info rt_glyph_negative_squared_latin_capital_letter_i // Circled Information Source // https://compart.com/en/unicode/U+1F6C8 #define rt_glyph_circled_information_source "\xF0\x9F\x9B\x88" // Information Source // https://compart.com/en/unicode/U+2139 #define rt_glyph_information_source "\xE2\x84\xB9" // Squared Cool // https://compart.com/en/unicode/U+1F192 #define rt_glyph_squared_cool "\xF0\x9F\x86\x92" // Squared OK // https://compart.com/en/unicode/U+1F197 #define rt_glyph_squared_ok "\xF0\x9F\x86\x97" // Squared Free // https://compart.com/en/unicode/U+1F193 #define rt_glyph_squared_free "\xF0\x9F\x86\x93" // Squared New // https://compart.com/en/unicode/U+1F195 #define rt_glyph_squared_new "\xF0\x9F\x86\x95" // Lady Beetle // https://compart.com/en/unicode/U+1F41E #define rt_glyph_lady_beetle "\xF0\x9F\x90\x9E" // Brain // https://compart.com/en/unicode/U+1F9E0 #define rt_glyph_brain "\xF0\x9F\xA7\xA0" // South West Arrow with Hook // https://www.compart.com/en/unicode/U+2926 #define rt_glyph_south_west_arrow_with_hook "\xE2\xA4\xA6" // North West Arrow with Hook // https://www.compart.com/en/unicode/U+2923 #define rt_glyph_north_west_arrow_with_hook "\xE2\xA4\xA3" // White Sun with Rays // https://www.compart.com/en/unicode/U+263C #define rt_glyph_white_sun_with_rays "\xE2\x98\xBC" // Black Sun with Rays // https://www.compart.com/en/unicode/U+2600 #define rt_glyph_black_sun_with_rays "\xE2\x98\x80" // Sun Behind Cloud // https://www.compart.com/en/unicode/U+26C5 #define rt_glyph_sun_behind_cloud "\xE2\x9B\x85" // White Sun // https://www.compart.com/en/unicode/U+1F323 #define rt_glyph_white_sun "\xF0\x9F\x8C\xA3" // Crescent Moon // https://www.compart.com/en/unicode/U+1F319 #define rt_glyph_crescent_moon "\xF0\x9F\x8C\x99" // Latin Capital Letter E with Cedilla and Breve // https://compart.com/en/unicode/U+1E1C #define rt_glyph_E_with_cedilla_and_breve "\xE1\xB8\x9C" // Box Drawings Heavy Vertical and Horizontal // https://compart.com/en/unicode/U+254B #define rt_glyph_box_drawings_heavy_vertical_and_horizontal "\xE2\x95\x8B" // Box Drawings Light Diagonal Cross // https://compart.com/en/unicode/U+2573 #define rt_glyph_box_drawings_light_diagonal_cross "\xE2\x95\xB3" // Combining Enclosing Square // https://compart.com/en/unicode/U+20DE #define rt_glyph_combining_enclosing_square "\xE2\x83\x9E" // Combining Enclosing Screen // https://compart.com/en/unicode/U+20E2 #define rt_glyph_combining_enclosing_screen "\xE2\x83\xA2" // Combining Enclosing Keycap // https://compart.com/en/unicode/U+20E3 #define rt_glyph_combining_enclosing_keycap "\xE2\x83\xA3" // Combining Enclosing Circle // https://compart.com/en/unicode/U+20DD #define rt_glyph_combining_enclosing_circle "\xE2\x83\x9D" // Frame with Picture // https://compart.com/en/unicode/U+1F5BC #define rt_glyph_frame_with_picture "\xF0\x9F\x96\xBC" // with emoji variation selector: "\xF0\x9F\x96\xBC\xEF\xB8\x8F" // Document with Picture // https://compart.com/en/unicode/U+1F5BB #define rt_glyph_document_with_picture "\xF0\x9F\x96\xBB" // Frame with Tiles // https://compart.com/en/unicode/U+1F5BD #define rt_glyph_frame_with_tiles "\xF0\x9F\x96\xBD" // Frame with an X // https://compart.com/en/unicode/U+1F5BE #define rt_glyph_frame_with_an_x "\xF0\x9F\x96\xBE" // Left Right Arrow // https://compart.com/en/unicode/U+2194 #define rt_glyph_left_right_arrow "\xE2\x86\x94" // Up Down Arrow // https://compart.com/en/unicode/U+2195 #define rt_glyph_up_down_arrow "\xE2\x86\x95" // ________________________________ rt_heap.h _________________________________ rt_begin_c // It is absolutely OK to use posix compliant // malloc()/calloc()/realloc()/free() function calls with understanding // that they introduce serialization points in multi-threaded applications // and may be induce wait states that under pressure (all cores busy) may // result in prolonged wait which may not be acceptable for real time // processing pipelines. // // heap_if.functions may or may not be faster than malloc()/free() ... // // Some callers may find realloc() parameters more convenient to avoid // anti-pattern // void* reallocated = realloc(p, new_size); // if (reallocated != null) { p = reallocated; } // and avoid never ending discussion of legality and implementation // compliance of the situation: // realloc(p /* when p == null */, ...) // // zero: true initializes allocated or reallocated tail memory to 0x00 // be careful with zeroing heap memory. It will result in virtual // to physical memory mapping and may be expensive. typedef struct rt_heap_s rt_heap_t; typedef struct { // heap == null uses process serialized LFH errno_t (*alloc)(void* *a, int64_t bytes); errno_t (*alloc_zero)(void* *a, int64_t bytes); errno_t (*realloc)(void* *a, int64_t bytes); errno_t (*realloc_zero)(void* *a, int64_t bytes); void (*free)(void* a); // heaps: rt_heap_t* (*create)(bool serialized); errno_t (*allocate)(rt_heap_t* heap, void* *a, int64_t bytes, bool zero); // reallocate may return ERROR_OUTOFMEMORY w/o changing 'a' *) errno_t (*reallocate)(rt_heap_t* heap, void* *a, int64_t bytes, bool zero); void (*deallocate)(rt_heap_t* heap, void* a); int64_t (*bytes)(rt_heap_t* heap, void* a); // actual allocated size void (*dispose)(rt_heap_t* heap); void (*test)(void); } rt_heap_if; extern rt_heap_if rt_heap; // *) zero in reallocate applies to the newly appended bytes // On Windows rt_mem.heap is based on serialized LFH returned by GetProcessHeap() // https://learn.microsoft.com/en-us/windows/win32/memory/low-fragmentation-heap // threads can benefit from not serialized, not LFH if they allocate and free // memory in time critical loops. rt_end_c // _______________________________ rt_loader.h ________________________________ rt_begin_c // see: // https://pubs.opengroup.org/onlinepubs/7908799/xsh/dlfcn.h.html typedef struct { // mode: int32_t const local; int32_t const lazy; int32_t const now; int32_t const global; // "If the value of file is null, dlopen() provides a handle on a global // symbol object." posix void* (*open)(const char* filename, int32_t mode); void* (*sym)(void* handle, const char* name); void (*close)(void* handle); void (*test)(void); } rt_loader_if; extern rt_loader_if rt_loader; rt_end_c // _________________________________ rt_mem.h _________________________________ rt_begin_c typedef struct { // whole file read only errno_t (*map_ro)(const char* filename, void** data, int64_t* bytes); // whole file read-write errno_t (*map_rw)(const char* filename, void** data, int64_t* bytes); void (*unmap)(void* data, int64_t bytes); // map_resource() maps data from resources, do NOT unmap! errno_t (*map_resource)(const char* label, void** data, int64_t* bytes); int32_t (*page_size)(void); // 4KB or 64KB on Windows int32_t (*large_page_size)(void); // 2MB on Windows // allocate() contiguous reserved virtual address range, // if possible committed to physical memory. // Memory guaranteed to be aligned to page boundary. // Memory is guaranteed to be initialized to zero on access. void* (*allocate)(int64_t bytes_multiple_of_page_size); void (*deallocate)(void* a, int64_t bytes_multiple_of_page_size); void (*test)(void); } rt_mem_if; extern rt_mem_if rt_mem; rt_end_c // _________________________________ rt_nls.h _________________________________ rt_begin_c typedef struct { // i18n national language support void (*init)(void); const char* (*locale)(void); // "en-US" "zh-CN" etc... // force locale for debugging and testing: errno_t (*set_locale)(const char* locale); // only for calling thread // nls(s) is same as string(strid(s), s) const char* (*str)(const char* defau1t); // returns localized string // strid("foo") returns -1 if there is no matching // ENGLISH NEUTRAL STRINGTABLE entry int32_t (*strid)(const char* s); // given strid > 0 returns localized string or default value const char* (*string)(int32_t strid, const char* defau1t); } rt_nls_if; extern rt_nls_if rt_nls; rt_end_c // _________________________________ rt_num.h _________________________________ rt_begin_c typedef struct { uint64_t lo; uint64_t hi; } rt_num128_t; // uint128_t may be supported by compiler typedef struct { rt_num128_t (*add128)(const rt_num128_t a, const rt_num128_t b); rt_num128_t (*sub128)(const rt_num128_t a, const rt_num128_t b); rt_num128_t (*mul64x64)(uint64_t a, uint64_t b); uint64_t (*muldiv128)(uint64_t a, uint64_t b, uint64_t d); uint32_t (*gcd32)(uint32_t u, uint32_t v); // greatest common denominator // non-crypto strong pseudo-random number generators (thread safe) uint32_t (*random32)(uint32_t *state); // "Mulberry32" uint64_t (*random64)(uint64_t *state); // "Trust" // "FNV-1a" hash functions (if bytes == 0 expects zero terminated string) uint32_t (*hash32)(const char* s, int64_t bytes); uint64_t (*hash64)(const char* s, int64_t bytes); void (*test)(void); } rt_num_if; extern rt_num_if rt_num; rt_end_c // _______________________________ rt_static.h ________________________________ rt_begin_c // rt_static_init(unique_name) { code_to_execute_before_main } #if defined(_MSC_VER) #if defined(_WIN64) || defined(_M_X64) #define _msvc_symbol_prefix_ "" #else #define _msvc_symbol_prefix_ "_" #endif #pragma comment(linker, "/include:rt_force_symbol_reference") void* rt_force_symbol_reference(void* symbol); #define _msvc_ctor_(sym_prefix, func) \ void func(void); \ int32_t (* rt_array ## func)(void); \ int32_t func ## _wrapper(void); \ int32_t func ## _wrapper(void) { func(); \ rt_force_symbol_reference((void*)rt_array ## func); \ rt_force_symbol_reference((void*)func ## _wrapper); return 0; } \ extern int32_t (* rt_array ## func)(void); \ __pragma(comment(linker, "/include:" sym_prefix # func "_wrapper")) \ __pragma(section(".CRT$XCU", read)) \ __declspec(allocate(".CRT$XCU")) \ int32_t (* rt_array ## func)(void) = func ## _wrapper; #define rt_static_init2_(func, line) _msvc_ctor_(_msvc_symbol_prefix_, \ func ## _constructor_##line) \ void func ## _constructor_##line(void) #define rt_static_init1_(func, line) rt_static_init2_(func, line) #define rt_static_init(func) rt_static_init1_(func, __LINE__) #else #define rt_static_init(n) __attribute__((constructor)) \ static void _init_ ## n ## __LINE__ ## _ctor(void) #endif void rt_static_init_test(void); rt_end_c // _______________________________ rt_streams.h _______________________________ rt_begin_c typedef struct rt_stream_if rt_stream_if; typedef struct rt_stream_if { errno_t (*read)(rt_stream_if* s, void* data, int64_t bytes, int64_t *transferred); errno_t (*write)(rt_stream_if* s, const void* data, int64_t bytes, int64_t *transferred); void (*close)(rt_stream_if* s); // optional } rt_stream_if; typedef struct { rt_stream_if stream; const void* data_read; int64_t bytes_read; int64_t pos_read; void* data_write; int64_t bytes_write; int64_t pos_write; } rt_stream_memory_if; typedef struct { void (*read_only)(rt_stream_memory_if* s, const void* data, int64_t bytes); void (*write_only)(rt_stream_memory_if* s, void* data, int64_t bytes); void (*read_write)(rt_stream_memory_if* s, const void* read, int64_t read_bytes, void* write, int64_t write_bytes); void (*test)(void); } rt_streams_if; extern rt_streams_if rt_streams; rt_end_c // ______________________________ rt_processes.h ______________________________ rt_begin_c typedef struct { const char* command; rt_stream_if* in; rt_stream_if* out; rt_stream_if* err; uint32_t exit_code; fp64_t timeout; // seconds } rt_processes_child_t; // Process name may be an the executable filename with // full, partial or absent pathname. // Case insensitive on Windows. typedef struct { const char* (*name)(void); // argv[0] like but full path uint64_t (*pid)(const char* name); // 0 if process not found errno_t (*pids)(const char* name, uint64_t* pids/*[size]*/, int32_t size, int32_t *count); // return 0, error or ERROR_MORE_DATA errno_t (*nameof)(uint64_t pid, char* name, int32_t count); // pathname bool (*present)(uint64_t pid); errno_t (*kill)(uint64_t pid, fp64_t timeout_seconds); errno_t (*kill_all)(const char* name, fp64_t timeout_seconds); bool (*is_elevated)(void); // Is process running as root/ Admin / System? errno_t (*restart_elevated)(void); // retuns error or exits on success errno_t (*run)(rt_processes_child_t* child); errno_t (*popen)(const char* command, int32_t *xc, rt_stream_if* output, fp64_t timeout_seconds); // <= 0 infinite // popen() does NOT guarantee stream zero termination on errors errno_t (*spawn)(const char* command); // spawn fully detached process void (*test)(void); } rt_processes_if; extern rt_processes_if rt_processes; rt_end_c // _______________________________ rt_threads.h _______________________________ rt_begin_c typedef struct rt_event_s* rt_event_t; typedef struct { rt_event_t (*create)(void); // never returns null rt_event_t (*create_manual)(void); // never returns null void (*set)(rt_event_t e); void (*reset)(rt_event_t e); void (*wait)(rt_event_t e); // returns 0 on success or -1 on timeout int32_t (*wait_or_timeout)(rt_event_t e, fp64_t seconds); // seconds < 0 forever // returns event index or -1 on timeout or -2 on abandon int32_t (*wait_any)(int32_t n, rt_event_t events[]); // -1 on abandon int32_t (*wait_any_or_timeout)(int32_t n, rt_event_t e[], fp64_t seconds); void (*dispose)(rt_event_t e); void (*test)(void); } rt_event_if; extern rt_event_if rt_event; typedef struct rt_aligned_8 rt_mutex_s { uint8_t content[40]; } rt_mutex_t; typedef struct { void (*init)(rt_mutex_t* m); void (*lock)(rt_mutex_t* m); void (*unlock)(rt_mutex_t* m); void (*dispose)(rt_mutex_t* m); void (*test)(void); } rt_mutex_if; extern rt_mutex_if rt_mutex; typedef struct thread_s* rt_thread_t; typedef struct { rt_thread_t (*start)(void (*func)(void*), void* p); // never returns null errno_t (*join)(rt_thread_t thread, fp64_t timeout_seconds); // < 0 forever void (*detach)(rt_thread_t thread); // closes handle. thread is not joinable void (*name)(const char* name); // names the thread void (*realtime)(void); // bumps calling thread priority void (*yield)(void); // pthread_yield() / Win32: SwitchToThread() void (*sleep_for)(fp64_t seconds); uint64_t (*id_of)(rt_thread_t t); uint64_t (*id)(void); // gettid() rt_thread_t (*self)(void); // Pseudo Handle may differ in access to .open(.id()) errno_t (*open)(rt_thread_t* t, uint64_t id); void (*close)(rt_thread_t t); void (*test)(void); } rt_thread_if; extern rt_thread_if rt_thread; rt_end_c // ________________________________ rt_vigil.h ________________________________ rt_begin_c // better rt_assert() - augmented with printf format and parameters // rt_swear() - release configuration rt_assert() in honor of: // https://github.com/munificent/vigil #define rt_static_assertion(condition) static_assert(condition, #condition) typedef struct { int32_t (*failed_assertion)(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...); int32_t (*fatal_termination)(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...); int32_t (*fatal_if_error)(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, ...); void (*test)(void); } rt_vigil_if; extern rt_vigil_if rt_vigil; #if defined(DEBUG) #define rt_assert(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.failed_assertion(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #else #define rt_assert(b, ...) ((void)0) #endif // rt_swear() is runtime rt_assert() for both debug and release configurations #define rt_swear(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.failed_assertion(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_fatal(...) (void)(rt_vigil.fatal_termination( \ __FILE__, __LINE__, __func__, "", "" __VA_ARGS__)) #define rt_fatal_if(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!(b)) || rt_vigil.fatal_termination(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_fatal_if_not(b, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)((!!(b)) || rt_vigil.fatal_termination(__FILE__, __LINE__, \ __func__, #b, "" __VA_ARGS__)) #define rt_not_null(e, ...) rt_fatal_if((e) == null, "" __VA_ARGS__) #define rt_fatal_if_error(r, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)(rt_vigil.fatal_if_error(__FILE__, __LINE__, __func__, \ #r, r, "" __VA_ARGS__)) #define rt_fatal_win32err(c, ...) rt_suppress_constant_cond_exp \ /* const cond */ \ (void)(rt_vigil.fatal_if_error(__FILE__, __LINE__, __func__, \ #c, rt_b2e(c), "" __VA_ARGS__)) rt_end_c // ___________________________________ rt.h ___________________________________ // the rest is in alphabetical order (no inter dependencies) // ________________________________ rt_work.h _________________________________ rt_begin_c // Minimalistic "react"-like work_queue or work items and // a thread based workers. See rt_worker_test() for usage. typedef struct rt_event_s* rt_event_t; typedef struct rt_work_s rt_work_t; typedef struct rt_work_queue_s rt_work_queue_t; typedef struct rt_work_s { rt_work_queue_t* queue; // queue where the call is or was last scheduled fp64_t when; // proc() call will be made after or at this time void (*work)(rt_work_t* c); void* data; // extra data that will be passed to proc() call rt_event_t done; // if not null signalled after calling proc() or canceling rt_work_t* next; // next element in the queue (implementation detail) bool canceled; // set to true inside .cancel() call } rt_work_t; typedef struct rt_work_queue_s { rt_work_t* head; int64_t lock; // spinlock rt_event_t changed; // if not null will be signaled when head changes } rt_work_queue_t; typedef struct rt_work_queue_if { void (*post)(rt_work_t* c); bool (*get)(rt_work_queue_t*, rt_work_t* *c); void (*call)(rt_work_t* c); void (*dispatch)(rt_work_queue_t* q); // all ready messages void (*cancel)(rt_work_t* c); void (*flush)(rt_work_queue_t* q); // cancel all requests in the queue } rt_work_queue_if; extern rt_work_queue_if rt_work_queue; typedef struct rt_worker_s { rt_work_queue_t queue; rt_thread_t thread; rt_event_t wake; volatile bool quit; } rt_worker_t; typedef struct rt_worker_if { void (*start)(rt_worker_t* tq); void (*post)(rt_worker_t* tq, rt_work_t* w); errno_t (*join)(rt_worker_t* tq, fp64_t timeout); void (*test)(void); } rt_worker_if; extern rt_worker_if rt_worker; // worker thread waits for a queue's `wake` event with the timeout // infinity or if queue is not empty delta time till the head // item of the queue. // // Upon post() call the `wake` event is set and the worker thread // wakes up and dispatches all the items with .when less then now // calling function work() if it is not null and optionally signaling // .done event if it is not null. // // When all ready items in the queue are processed worker thread locks // the queue and if the head is present calculates next timeout based // on .when time of the head or sets timeout to infinity if the queue // is empty. // // Function .join() sets .quit to true signals .wake event and attempt // to join the worker .thread with specified timeout. // It is the responsibility of the caller to ensure that no other // work is posted after calling .join() because it will be lost. rt_end_c /* Usage examples: // The dispatch_until() is just for testing purposes. // Usually rt_work_queue.dispatch(q) will be called inside each // iteration of message loop of a dispatch [UI] thread. static void dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n); // simple way of passing a single pointer to call_later static void every_100ms(rt_work_t* w) { int32_t* i = (int32_t*)w->data; rt_println("i: %d", *i); (*i)++; w->when = rt_clock.seconds() + 0.100; rt_work_queue.post(w); } static void example_1(void) { rt_work_queue_t queue = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.100, .work = every_100ms, .data = &i }; rt_work_queue.post(&work); dispatch_until(&queue, &i, 4); } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void every_200ms(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; rt_println("ex { .i: %d, .s.a: %d .s.b: %d}", ex->i, ex->s.a, ex->s.b); ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; w->when = rt_clock.seconds() + 0.200; rt_work_queue.post(w); } static void example_2(void) { rt_work_queue_t queue = {0}; rt_work_ex_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.200, .work = every_200ms, .data = null, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&work.base); dispatch_until(&queue, &work.i, 4); } static void dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n) { while (q->head != null && *i < n) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(q); } rt_work_queue.flush(q); } // worker: static void do_work(rt_work_t* w) { // TODO: something useful } static void worker_test(void) { rt_worker_t worker = { 0 }; rt_worker.start(&worker); rt_work_t work = { .when = rt_clock.seconds() + 0.010, // 10ms .done = rt_event.create(), .work = do_work }; rt_worker.post(&worker, &work); rt_event.wait(work.done); // await(work) rt_event.dispose(work.done); // responsibility of the caller rt_fatal_if_error(rt_worker.join(&worker, -1.0)); } // Hint: // To monitor timing turn on MSVC Output / Show Timestamp (clock button) */ #endif // rt_definition #ifdef rt_implementation // ________________________________ rt_win32.h ________________________________ #ifdef WIN32 #pragma warning(push) #pragma warning(disable: 4255) // no function prototype: '()' to '(void)' #pragma warning(disable: 4459) // declaration of '...' hides global declaration #pragma push_macro("UNICODE") #define UNICODE // always because otherwise IME does not work // ut: #include // used by: #include // both rt_loader.c and rt_processes.c #include // rt_processes.c #include // rt_processes.c #include // for knownfolders #include // rt_files.c #include // rt_files.c #include // rt_files.c #include // rt_files.c // ui: #include #include #include #include #include #include #include #include #include #pragma pop_macro("UNICODE") #pragma warning(pop) #include #define rt_export __declspec(dllexport) // Win32 API BOOL -> errno_t translation #define rt_b2e(call) ((errno_t)(call ? 0 : GetLastError())) void rt_win32_close_handle(void* h); /* translate ix to error */ errno_t rt_wait_ix2e(uint32_t r); #endif // WIN32 // ___________________________________ rt.c ___________________________________ // #include "ut/macos.h" // TODO // #include "ut/linux.h" // TODO // ________________________________ rt_args.c _________________________________ static void* rt_args_memory; static void rt_args_main(int32_t argc, const char* argv[], const char** env) { rt_swear(rt_args.c == 0 && rt_args.v == null && rt_args.env == null); rt_swear(rt_args_memory == null); rt_args.c = argc; rt_args.v = argv; rt_args.env = env; } static int32_t rt_args_option_index(const char* option) { for (int32_t i = 1; i < rt_args.c; i++) { if (strcmp(rt_args.v[i], "--") == 0) { break; } // no options after '--' if (strcmp(rt_args.v[i], option) == 0) { return i; } } return -1; } static void rt_args_remove_at(int32_t ix) { // returns new argc rt_assert(0 < rt_args.c); rt_assert(0 < ix && ix < rt_args.c); // cannot remove rt_args.v[0] for (int32_t i = ix; i < rt_args.c; i++) { rt_args.v[i] = rt_args.v[i + 1]; } rt_args.v[rt_args.c - 1] = ""; rt_args.c--; } static bool rt_args_option_bool(const char* option) { int32_t ix = rt_args_option_index(option); if (ix > 0) { rt_args_remove_at(ix); } return ix > 0; } static bool rt_args_option_int(const char* option, int64_t *value) { int32_t ix = rt_args_option_index(option); if (ix > 0 && ix < rt_args.c - 1) { const char* s = rt_args.v[ix + 1]; int32_t base = (strstr(s, "0x") == s || strstr(s, "0X") == s) ? 16 : 10; const char* b = s + (base == 10 ? 0 : 2); char* e = null; errno = 0; int64_t v = strtoll(b, &e, base); if (errno == 0 && e > b && *e == 0) { *value = v; } else { ix = -1; } } else { ix = -1; } if (ix > 0) { rt_args_remove_at(ix); // remove option rt_args_remove_at(ix); // remove following number } return ix > 0; } static const char* rt_args_option_str(const char* option) { int32_t ix = rt_args_option_index(option); const char* s = null; if (ix > 0 && ix < rt_args.c - 1) { s = rt_args.v[ix + 1]; } else { ix = -1; } if (ix > 0) { rt_args_remove_at(ix); // remove option rt_args_remove_at(ix); // remove following string } return ix > 0 ? s : null; } // Terminology: "quote" in the code and comments below // actually refers to "fp64_t quote mark" and used for brevity. // TODO: posix like systems // Looks like all shells support quote marks but // AFAIK MacOS bash and zsh also allow (and prefer) backslash escaped // space character. Unclear what other escaping shell and posix compliant // parser should support. // Lengthy discussion here: // https://stackoverflow.com/questions/1706551/parse-string-into-argv-argc // Microsoft specific argument parsing: // https://web.archive.org/web/20231115181633/http://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=msvc-170 // Alternative: just use CommandLineToArgvW() typedef struct { const char* s; char* d; const char* e; } rt_args_pair_t; static rt_args_pair_t rt_args_parse_backslashes(rt_args_pair_t p) { enum { quote = '"', backslash = '\\' }; const char* s = p.s; char* d = p.d; rt_swear(*s == backslash); int32_t bsc = 0; // number of backslashes while (*s == backslash) { s++; bsc++; } if (*s == quote) { while (bsc > 1 && d < p.e) { *d++ = backslash; bsc -= 2; } if (bsc == 1 && d < p.e) { *d++ = *s++; } } else { // Backslashes are interpreted literally, // unless they immediately precede a quote: while (bsc > 0 && d < p.e) { *d++ = backslash; bsc--; } } return (rt_args_pair_t){ .s = s, .d = d, .e = p.e }; } static rt_args_pair_t rt_args_parse_quoted(rt_args_pair_t p) { enum { quote = '"', backslash = '\\' }; const char* s = p.s; char* d = p.d; rt_swear(*s == quote); s++; // opening quote (skip) while (*s != 0x00) { if (*s == backslash) { p = rt_args_parse_backslashes((rt_args_pair_t){ .s = s, .d = d, .e = p.e }); s = p.s; d = p.d; } else if (*s == quote && s[1] == quote) { // Within a quoted string, a pair of quote is // interpreted as a single escaped quote. if (d < p.e) { *d++ = *s++; } s++; // 1 for 2 quotes } else if (*s == quote) { s++; // closing quote (skip) break; } else if (d < p.e) { *d++ = *s++; } } return (rt_args_pair_t){ .s = s, .d = d, .e = p.e }; } static void rt_args_parse(const char* s) { rt_swear(s[0] != 0, "cannot parse empty string"); rt_swear(rt_args.c == 0); rt_swear(rt_args.v == null); rt_swear(rt_args_memory == null); enum { quote = '"', backslash = '\\', tab = '\t', space = 0x20 }; const int32_t len = (int32_t)strlen(s); // Worst-case scenario (possible to optimize with dry run of parse) // at least 2 characters per token in "a b c d e" plush null at the end: const int32_t k = ((len + 2) / 2 + 1) * (int32_t)sizeof(void*) + (int32_t)sizeof(void*); const int32_t n = k + (len + 2) * (int32_t)sizeof(char); rt_fatal_if_error(rt_heap.allocate(null, &rt_args_memory, n, true)); rt_args.c = 0; rt_args.v = (const char**)rt_args_memory; char* d = (char*)(((char*)rt_args.v) + k); char* e = d + n; // end of memory // special rules for 1st argument: if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } if (*s == quote) { s++; while (*s != 0x00 && *s != quote && d < e) { *d++ = *s++; } if (*s == quote) { // // closing quote s++; // skip closing quote *d++ = 0x00; } else { while (*s != 0x00) { s++; } } } else { while (*s != 0x00 && *s != space && *s != tab && d < e) { *d++ = *s++; } } if (d < e) { *d++ = 0; } while (d < e) { while (*s == space || *s == tab) { s++; } if (*s == 0) { break; } if (*s == quote && s[1] == 0 && d < e) { // unbalanced single quote if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } // spec does not say what to do *d++ = *s++; } else if (*s == quote) { // quoted arg if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } rt_args_pair_t p = rt_args_parse_quoted( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else { // non-quoted arg (that can have quoted strings inside) if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } while (*s != 0) { if (*s == backslash) { rt_args_pair_t p = rt_args_parse_backslashes( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else if (*s == quote) { rt_args_pair_t p = rt_args_parse_quoted( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else if (*s == tab || *s == space) { break; } else if (d < e) { *d++ = *s++; } } } if (d < e) { *d++ = 0; } } if (rt_args.c < n) { rt_args.v[rt_args.c] = null; } rt_swear(rt_args.c < n, "not enough memory - adjust guestimates"); rt_swear(d <= e, "not enough memory - adjust guestimates"); } static const char* rt_args_basename(void) { static char basename[260]; rt_swear(rt_args.c > 0); if (basename[0] == 0) { const char* s = rt_args.v[0]; const char* b = s; while (*s != 0) { if (*s == '\\' || *s == '/') { b = s + 1; } s++; } int32_t n = rt_str.len(b); rt_swear(n < rt_countof(basename)); strncpy(basename, b, rt_countof(basename) - 1); char* d = basename + n - 1; while (d > basename && *d != '.') { d--; } if (*d == '.') { *d = 0x00; } } return basename; } static void rt_args_fini(void) { rt_heap.deallocate(null, rt_args_memory); // can be null is parse() was not called rt_args_memory = null; rt_args.c = 0; rt_args.v = null; } static void rt_args_WinMain(void) { rt_swear(rt_args.c == 0 && rt_args.v == null && rt_args.env == null); rt_swear(rt_args_memory == null); const uint16_t* wcl = GetCommandLineW(); int32_t n = (int32_t)rt_str.len16(wcl); char* cl = null; rt_fatal_if_error(rt_heap.allocate(null, (void**)&cl, n * 2 + 1, false)); rt_str.utf16to8(cl, n * 2 + 1, wcl, -1); rt_args_parse(cl); rt_heap.deallocate(null, cl); rt_args.env = (const char**)(void*)_environ; } #ifdef RT_TESTS // https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments // Command-line input argv[1] argv[2] argv[3] // "a b c" d e a b c d e // "ab\"c" "\\" d ab"c \ d // a\\\b d"e f"g h a\\\b de fg h // a\\\"b c d a\"b c d // a\\\\"b c" d e a\\b c d e // a"b"" c d ab" c d #ifndef __INTELLISENSE__ // confused data analysis static void rt_args_test_verify(const char* cl, int32_t expected, ...) { if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { rt_println("cl: `%s`", cl); } int32_t argc = rt_args.c; const char** argv = rt_args.v; void* memory = rt_args_memory; rt_args.c = 0; rt_args.v = null; rt_args_memory = null; rt_args_parse(cl); va_list va; va_start(va, expected); for (int32_t i = 0; i < expected; i++) { const char* s = va_arg(va, const char*); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("rt_args.v[%d]: `%s` expected: `%s`", i, rt_args.v[i], s); // } // Warning 6385: reading data outside of array const char* ai = _Pragma("warning(suppress: 6385)")rt_args.v[i]; rt_swear(strcmp(ai, s) == 0, "rt_args.v[%d]: `%s` expected: `%s`", i, ai, s); } va_end(va); rt_args.fini(); // restore command line arguments: rt_args.c = argc; rt_args.v = argv; rt_args_memory = memory; } #endif // __INTELLISENSE__ static void rt_args_test(void) { // The first argument (rt_args.v[0]) is treated specially. // It represents the program name. Because it must be a valid pathname, // parts surrounded by quote (") are allowed. The quote aren't included // in the rt_args.v[0] output. The parts surrounded by quote prevent // interpretation of a space or tab character as the end of the argument. // The escaping rules don't apply. rt_args_test_verify("\"c:\\foo\\bar\\snafu.exe\"", 1, "c:\\foo\\bar\\snafu.exe"); rt_args_test_verify("c:\\foo\\bar\\snafu.exe", 1, "c:\\foo\\bar\\snafu.exe"); rt_args_test_verify("foo.exe \"a b c\" d e", 4, "foo.exe", "a b c", "d", "e"); rt_args_test_verify("foo.exe \"ab\\\"c\" \"\\\\\" d", 4, "foo.exe", "ab\"c", "\\", "d"); rt_args_test_verify("foo.exe a\\\\\\b d\"e f\"g h", 4, "foo.exe", "a\\\\\\b", "de fg", "h"); rt_args_test_verify("foo.exe a\\\\\\b d\"e f\"g h", 4, "foo.exe", "a\\\\\\b", "de fg", "h"); rt_args_test_verify("foo.exe a\"b\"\" c d", 2, // unmatched quote "foo.exe", "ab\" c d"); // unbalanced quote and backslash: rt_args_test_verify("foo.exe \"", 2, "foo.exe", "\""); rt_args_test_verify("foo.exe \\", 2, "foo.exe", "\\"); rt_args_test_verify("foo.exe \\\\", 2, "foo.exe", "\\\\"); rt_args_test_verify("foo.exe \\\\\\", 2, "foo.exe", "\\\\\\"); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_args_test(void) {} #endif rt_args_if rt_args = { .main = rt_args_main, .WinMain = rt_args_WinMain, .option_index = rt_args_option_index, .remove_at = rt_args_remove_at, .option_bool = rt_args_option_bool, .option_int = rt_args_option_int, .option_str = rt_args_option_str, .basename = rt_args_basename, .fini = rt_args_fini, .test = rt_args_test }; // _______________________________ rt_atomics.c _______________________________ #include // needs cl.exe /experimental:c11atomics command line // see: https://developercommunity.visualstudio.com/t/C11--C17-include-stdatomich-issue/10620622 #pragma warning(push) #pragma warning(disable: 4746) // volatile access of 'int32_var' is subject to /volatile: setting; consider using __iso_volatile_load/store intrinsic functions #ifndef UT_ATOMICS_HAS_STDATOMIC_H static int32_t rt_atomics_increment_int32(volatile int32_t* a) { return InterlockedIncrement((volatile LONG*)a); } static int32_t rt_atomics_decrement_int32(volatile int32_t* a) { return InterlockedDecrement((volatile LONG*)a); } static int64_t rt_atomics_increment_int64(volatile int64_t* a) { return InterlockedIncrement64((__int64 volatile *)a); } static int64_t rt_atomics_decrement_int64(volatile int64_t* a) { return InterlockedDecrement64((__int64 volatile *)a); } static int32_t rt_atomics_add_int32(volatile int32_t* a, int32_t v) { return InterlockedAdd((LONG volatile *)a, v); } static int64_t rt_atomics_add_int64(volatile int64_t* a, int64_t v) { return InterlockedAdd64((__int64 volatile *)a, v); } static int64_t rt_atomics_exchange_int64(volatile int64_t* a, int64_t v) { return (int64_t)InterlockedExchange64((LONGLONG*)a, (LONGLONG)v); } static int32_t rt_atomics_exchange_int32(volatile int32_t* a, int32_t v) { rt_assert(sizeof(int32_t) == sizeof(unsigned long)); return (int32_t)InterlockedExchange((volatile LONG*)a, (unsigned long)v); } static bool rt_atomics_compare_exchange_int64(volatile int64_t* a, int64_t comparand, int64_t v) { return (int64_t)InterlockedCompareExchange64((LONGLONG*)a, (LONGLONG)v, (LONGLONG)comparand) == comparand; } static bool rt_atomics_compare_exchange_int32(volatile int32_t* a, int32_t comparand, int32_t v) { return (int64_t)InterlockedCompareExchange((LONG*)a, (LONG)v, (LONG)comparand) == comparand; } static void memory_fence(void) { #ifdef _M_ARM64 atomic_thread_fence(memory_order_seq_cst); #else _mm_mfence(); #endif } #else // stdatomic.h version: #ifndef __INTELLISENSE__ // IntelliSense chokes on _Atomic(_Type) // __INTELLISENSE__ Defined as 1 during an IntelliSense compiler pass // in the Visual Studio IDE. Otherwise, undefined. You can use this macro // to guard code the IntelliSense compiler doesn't understand, // or use it to toggle between the build and IntelliSense compiler. // _strong() operations are the same as _explicit(..., memory_order_seq_cst) // memory_order_seq_cst stands for Sequentially Consistent Ordering // // This is the strongest memory order, providing the guarantee that // all sequentially consistent operations appear to be executed in // the same order on all threads (cores) // // int_fast32_t: Fastest integer type with at least 32 bits. // int_least32_t: Smallest integer type with at least 32 bits. rt_static_assertion(sizeof(int32_t) == sizeof(int_fast32_t)); rt_static_assertion(sizeof(int32_t) == sizeof(int_least32_t)); static int32_t rt_atomics_increment_int32(volatile int32_t* a) { return atomic_fetch_add((volatile atomic_int_fast32_t*)a, 1) + 1; } static int32_t rt_atomics_decrement_int32(volatile int32_t* a) { return atomic_fetch_sub((volatile atomic_int_fast32_t*)a, 1) - 1; } static int64_t rt_atomics_increment_int64(volatile int64_t* a) { return atomic_fetch_add((volatile atomic_int_fast64_t*)a, 1) + 1; } static int64_t rt_atomics_decrement_int64(volatile int64_t* a) { return atomic_fetch_sub((volatile atomic_int_fast64_t*)a, 1) - 1; } static int32_t rt_atomics_add_int32(volatile int32_t* a, int32_t v) { return atomic_fetch_add((volatile atomic_int_fast32_t*)a, v) + v; } static int64_t rt_atomics_add_int64(volatile int64_t* a, int64_t v) { return atomic_fetch_add((volatile atomic_int_fast64_t*)a, v) + v; } static int64_t rt_atomics_exchange_int64(volatile int64_t* a, int64_t v) { return atomic_exchange((volatile atomic_int_fast64_t*)a, v); } static int32_t rt_atomics_exchange_int32(volatile int32_t* a, int32_t v) { return atomic_exchange((volatile atomic_int_fast32_t*)a, v); } static bool rt_atomics_compare_exchange_int64(volatile int64_t* a, int64_t comparand, int64_t v) { return atomic_compare_exchange_strong((volatile atomic_int_fast64_t*)a, &comparand, v); } // Code here is not "seen" by IntelliSense but is compiled normally. static bool rt_atomics_compare_exchange_int32(volatile int32_t* a, int32_t comparand, int32_t v) { return atomic_compare_exchange_strong((volatile atomic_int_fast32_t*)a, &comparand, v); } static void memory_fence(void) { atomic_thread_fence(memory_order_seq_cst); } #endif // __INTELLISENSE__ #endif // UT_ATOMICS_HAS_STDATOMIC_H static int32_t rt_atomics_load_int32(volatile int32_t* a) { return rt_atomics.add_int32(a, 0); } static int64_t rt_atomics_load_int64(volatile int64_t* a) { return rt_atomics.add_int64(a, 0); } static void* rt_atomics_exchange_ptr(volatile void* *a, void* v) { rt_static_assertion(sizeof(void*) == sizeof(uint64_t)); return (void*)(intptr_t)rt_atomics.exchange_int64((int64_t*)a, (int64_t)v); } static bool rt_atomics_compare_exchange_ptr(volatile void* *a, void* comparand, void* v) { rt_static_assertion(sizeof(void*) == sizeof(int64_t)); return rt_atomics.compare_exchange_int64((int64_t*)a, (int64_t)comparand, (int64_t)v); } #pragma push_macro("rt_sync_bool_compare_and_swap") #pragma push_macro("rt_builtin_cpu_pause") // https://en.wikipedia.org/wiki/Spinlock #define rt_sync_bool_compare_and_swap(p, old_val, new_val) \ (_InterlockedCompareExchange64(p, new_val, old_val) == old_val) // https://stackoverflow.com/questions/37063700/mm-pause-usage-in-gcc-on-intel #define rt_builtin_cpu_pause() do { YieldProcessor(); } while (0) static void spinlock_acquire(volatile int64_t* spinlock) { // Very basic implementation of a spinlock. This is currently // only used to guarantee thread-safety during context initialization // and shutdown (which are both executed very infrequently and // have minimal thread contention). // Not a performance champion (because of mem_fence()) but serves // the purpose. mem_fence() can be reduced to mem_sfence()... sigh while (!rt_sync_bool_compare_and_swap(spinlock, 0, 1)) { while (*spinlock) { rt_builtin_cpu_pause(); } } rt_atomics.memory_fence(); // not strictly necessary on strong mem model Intel/AMD but // see: https://cfsamsonbooks.gitbook.io/explaining-atomics-in-rust/ // Fig 2 Inconsistent C11 execution of SB and 2+2W rt_assert(*spinlock == 1); } #pragma pop_macro("rt_builtin_cpu_pause") #pragma pop_macro("rt_sync_bool_compare_and_swap") static void spinlock_release(volatile int64_t* spinlock) { rt_assert(*spinlock == 1); *spinlock = 0; // tribute to lengthy Linus discussion going since 2006: rt_atomics.memory_fence(); } static void rt_atomics_test(void) { #ifdef RT_TESTS volatile int32_t int32_var = 0; volatile int64_t int64_var = 0; volatile void* ptr_var = null; int64_t spinlock = 0; void* old_ptr = rt_atomics.exchange_ptr(&ptr_var, (void*)123); rt_swear(old_ptr == null); rt_swear(ptr_var == (void*)123); int32_t incremented_int32 = rt_atomics.increment_int32(&int32_var); rt_swear(incremented_int32 == 1); rt_swear(int32_var == 1); int32_t decremented_int32 = rt_atomics.decrement_int32(&int32_var); rt_swear(decremented_int32 == 0); rt_swear(int32_var == 0); int64_t incremented_int64 = rt_atomics.increment_int64(&int64_var); rt_swear(incremented_int64 == 1); rt_swear(int64_var == 1); int64_t decremented_int64 = rt_atomics.decrement_int64(&int64_var); rt_swear(decremented_int64 == 0); rt_swear(int64_var == 0); int32_t added_int32 = rt_atomics.add_int32(&int32_var, 5); rt_swear(added_int32 == 5); rt_swear(int32_var == 5); int64_t added_int64 = rt_atomics.add_int64(&int64_var, 10); rt_swear(added_int64 == 10); rt_swear(int64_var == 10); int32_t old_int32 = rt_atomics.exchange_int32(&int32_var, 3); rt_swear(old_int32 == 5); rt_swear(int32_var == 3); int64_t old_int64 = rt_atomics.exchange_int64(&int64_var, 6); rt_swear(old_int64 == 10); rt_swear(int64_var == 6); bool int32_exchanged = rt_atomics.compare_exchange_int32(&int32_var, 3, 4); rt_swear(int32_exchanged); rt_swear(int32_var == 4); bool int64_exchanged = rt_atomics.compare_exchange_int64(&int64_var, 6, 7); rt_swear(int64_exchanged); rt_swear(int64_var == 7); ptr_var = (void*)0x123; bool ptr_exchanged = rt_atomics.compare_exchange_ptr(&ptr_var, (void*)0x123, (void*)0x456); rt_swear(ptr_exchanged); rt_swear(ptr_var == (void*)0x456); rt_atomics.spinlock_acquire(&spinlock); rt_swear(spinlock == 1); rt_atomics.spinlock_release(&spinlock); rt_swear(spinlock == 0); int32_t loaded_int32 = rt_atomics.load32(&int32_var); rt_swear(loaded_int32 == int32_var); int64_t loaded_int64 = rt_atomics.load64(&int64_var); rt_swear(loaded_int64 == int64_var); rt_atomics.memory_fence(); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } #ifndef __INTELLISENSE__ // IntelliSense chokes on _Atomic(_Type) rt_static_assertion(sizeof(void*) == sizeof(int64_t)); rt_static_assertion(sizeof(void*) == sizeof(uintptr_t)); rt_atomics_if rt_atomics = { .exchange_ptr = rt_atomics_exchange_ptr, .increment_int32 = rt_atomics_increment_int32, .decrement_int32 = rt_atomics_decrement_int32, .increment_int64 = rt_atomics_increment_int64, .decrement_int64 = rt_atomics_decrement_int64, .add_int32 = rt_atomics_add_int32, .add_int64 = rt_atomics_add_int64, .exchange_int32 = rt_atomics_exchange_int32, .exchange_int64 = rt_atomics_exchange_int64, .compare_exchange_int64 = rt_atomics_compare_exchange_int64, .compare_exchange_int32 = rt_atomics_compare_exchange_int32, .compare_exchange_ptr = rt_atomics_compare_exchange_ptr, .load32 = rt_atomics_load_int32, .load64 = rt_atomics_load_int64, .spinlock_acquire = spinlock_acquire, .spinlock_release = spinlock_release, .memory_fence = memory_fence, .test = rt_atomics_test }; #endif // __INTELLISENSE__ // 2024-03-20 latest windows runtime and toolchain cl.exe // ... VC\Tools\MSVC\14.39.33519\include // see: // vcruntime_c11_atomic_support.h // vcruntime_c11_stdatomic.h // stdatomic.h // https://developercommunity.visualstudio.com/t/C11--C17-include--issue/10620622 // cl.exe /std:c11 /experimental:c11atomics // command line option are required // even in C17 mode in spring of 2024 #pragma warning(pop) // ______________________________ rt_backtrace.c ______________________________ static void* rt_backtrace_process; static DWORD rt_backtrace_pid; typedef rt_begin_packed struct symbol_info_s { SYMBOL_INFO info; char name[rt_backtrace_max_symbol]; } rt_end_packed symbol_info_t; #pragma push_macro("rt_backtrace_load_dll") #define rt_backtrace_load_dll(fn) do { \ if (GetModuleHandleA(fn) == null) { \ rt_fatal_win32err(LoadLibraryA(fn)); \ } \ } while (0) static void rt_backtrace_init(void) { if (rt_backtrace_process == null) { rt_backtrace_load_dll("dbghelp.dll"); rt_backtrace_load_dll("imagehlp.dll"); DWORD options = SymGetOptions(); // options |= SYMOPT_DEBUG; options |= SYMOPT_NO_PROMPTS; options |= SYMOPT_LOAD_LINES; options |= SYMOPT_UNDNAME; options |= SYMOPT_LOAD_ANYTHING; rt_swear(SymSetOptions(options)); rt_backtrace_pid = GetProcessId(GetCurrentProcess()); rt_swear(rt_backtrace_pid != 0); rt_backtrace_process = OpenProcess(PROCESS_ALL_ACCESS, false, rt_backtrace_pid); rt_swear(rt_backtrace_process != null); rt_swear(SymInitialize(rt_backtrace_process, null, true), "%s", rt_str.error(rt_core.err())); } } #pragma pop_macro("rt_backtrace_load_dll") static void rt_backtrace_capture(rt_backtrace_t* bt, int32_t skip) { rt_backtrace_init(); SetLastError(0); bt->frames = CaptureStackBackTrace(1 + skip, rt_countof(bt->stack), bt->stack, (DWORD*)&bt->hash); bt->error = rt_core.err(); } static bool rt_backtrace_function(DWORD64 pc, SYMBOL_INFO* si) { // find DLL exported function bool found = false; const DWORD64 module_base = SymGetModuleBase64(rt_backtrace_process, pc); if (module_base != 0) { const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS; HMODULE module_handle = null; if (GetModuleHandleExA(flags, (const char*)pc, &module_handle)) { DWORD bytes = 0; IMAGE_EXPORT_DIRECTORY* dir = (IMAGE_EXPORT_DIRECTORY*) ImageDirectoryEntryToDataEx(module_handle, true, IMAGE_DIRECTORY_ENTRY_EXPORT, &bytes, null); if (dir) { uint8_t* m = (uint8_t*)module_handle; DWORD* functions = (DWORD*)(m + dir->AddressOfFunctions); DWORD* names = (DWORD*)(m + dir->AddressOfNames); WORD* ordinals = (WORD*)(m + dir->AddressOfNameOrdinals); DWORD64 address = 0; // closest address DWORD64 min_distance = (DWORD64)-1; const char* function = NULL; // closest function name for (DWORD i = 0; i < dir->NumberOfNames; i++) { // function address DWORD64 fa = (DWORD64)(m + functions[ordinals[i]]); if (fa <= pc) { DWORD64 distance = pc - fa; if (distance < min_distance) { min_distance = distance; address = fa; function = (const char*)(m + names[i]); } } } if (function != null) { si->ModBase = (uint64_t)m; snprintf(si->Name, si->MaxNameLen - 1, "%s", function); si->Name[si->MaxNameLen - 1] = 0x00; si->NameLen = (DWORD)strlen(si->Name); si->Address = address; found = true; } } } } return found; } // SimpleStackWalker::showVariablesAt() can be implemented if needed like this: // https://accu.org/journals/overload/29/165/orr/ // https://github.com/rogerorr/articles/tree/main/Debugging_Optimised_Code // https://github.com/rogerorr/articles/blob/main/Debugging_Optimised_Code/SimpleStackWalker.cpp#L301 static const void rt_backtrace_symbolize_inline_frame(rt_backtrace_t* bt, int32_t i, DWORD64 pc, DWORD inline_context, symbol_info_t* si) { si->info.Name[0] = 0; si->info.NameLen = 0; bt->file[i][0] = 0; bt->line[i] = 0; bt->symbol[i][0] = 0; DWORD64 displacement = 0; if (SymFromInlineContext(rt_backtrace_process, pc, inline_context, &displacement, &si->info)) { rt_str_printf(bt->symbol[i], "%s", si->info.Name); } else { bt->error = rt_core.err(); } IMAGEHLP_LINE64 li = { .SizeOfStruct = sizeof(IMAGEHLP_LINE64) }; DWORD offset = 0; if (SymGetLineFromInlineContext(rt_backtrace_process, pc, inline_context, 0, &offset, &li)) { rt_str_printf(bt->file[i], "%s", li.FileName); bt->line[i] = li.LineNumber; } } // Too see kernel addresses in Stack Back Traces: // // Windows Registry Editor Version 5.00 // [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management] // "DisablePagingExecutive"=dword:00000001 // // https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc757875(v=ws.10) static int32_t rt_backtrace_symbolize_frame(rt_backtrace_t* bt, int32_t i) { const DWORD64 pc = (DWORD64)bt->stack[i]; symbol_info_t si = { .info = { .SizeOfStruct = sizeof(SYMBOL_INFO), .MaxNameLen = rt_countof(si.name) } }; bt->file[i][0] = 0; bt->line[i] = 0; bt->symbol[i][0] = 0; DWORD64 offsetFromSymbol = 0; const DWORD inline_count = SymAddrIncludeInlineTrace(rt_backtrace_process, pc); if (inline_count > 0) { DWORD ic = 0; // inline context DWORD fi = 0; // frame index if (SymQueryInlineTrace(rt_backtrace_process, pc, 0, pc, pc, &ic, &fi)) { for (DWORD k = 0; k < inline_count; k++, ic++) { rt_backtrace_symbolize_inline_frame(bt, i, pc, ic, &si); i++; } } } else { if (SymFromAddr(rt_backtrace_process, pc, &offsetFromSymbol, &si.info)) { rt_str_printf(bt->symbol[i], "%s", si.info.Name); DWORD d = 0; // displacement IMAGEHLP_LINE64 ln = { .SizeOfStruct = sizeof(IMAGEHLP_LINE64) }; if (SymGetLineFromAddr64(rt_backtrace_process, pc, &d, &ln)) { bt->line[i] = ln.LineNumber; rt_str_printf(bt->file[i], "%s", ln.FileName); } else { bt->error = rt_core.err(); if (rt_backtrace_function(pc, &si.info)) { GetModuleFileNameA((HANDLE)si.info.ModBase, bt->file[i], rt_countof(bt->file[i]) - 1); bt->file[i][rt_countof(bt->file[i]) - 1] = 0; bt->line[i] = 0; } else { bt->file[i][0] = 0x00; bt->line[i] = 0; } } i++; } else { bt->error = rt_core.err(); if (rt_backtrace_function(pc, &si.info)) { rt_str_printf(bt->symbol[i], "%s", si.info.Name); GetModuleFileNameA((HANDLE)si.info.ModBase, bt->file[i], rt_countof(bt->file[i]) - 1); bt->file[i][rt_countof(bt->file[i]) - 1] = 0; bt->error = 0; i++; } else { // will not do i++ } } } return i; } static void rt_backtrace_symbolize_backtrace(rt_backtrace_t* bt) { rt_assert(!bt->symbolized); bt->error = 0; rt_backtrace_init(); // rt_backtrace_symbolize_frame() may produce zero, one or many frames int32_t n = bt->frames; void* stack[rt_countof(bt->stack)]; memcpy(stack, bt->stack, n * sizeof(stack[0])); bt->frames = 0; for (int32_t i = 0; i < n && bt->frames < rt_countof(bt->stack); i++) { bt->stack[bt->frames] = stack[i]; bt->frames = rt_backtrace_symbolize_frame(bt, i); } bt->symbolized = true; } static void rt_backtrace_symbolize(rt_backtrace_t* bt) { if (!bt->symbolized) { rt_backtrace_symbolize_backtrace(bt); } } static const char* rt_backtrace_stops[] = { "main", "WinMain", "BaseThreadInitThunk", "RtlUserThreadStart", "mainCRTStartup", "WinMainCRTStartup", "invoke_main", "NdrInterfacePointerMemorySize", null }; static void rt_backtrace_trace(const rt_backtrace_t* bt, const char* stop) { #pragma push_macro("rt_backtrace_glyph_called_from") #define rt_backtrace_glyph_called_from rt_glyph_north_west_arrow_with_hook rt_assert(bt->symbolized, "need rt_backtrace.symbolize(bt)"); const char** alt = stop != null && strcmp(stop, "*") == 0 ? rt_backtrace_stops : null; for (int32_t i = 0; i < bt->frames; i++) { rt_debug.println(bt->file[i], bt->line[i], bt->symbol[i], rt_backtrace_glyph_called_from "%s", i == i < bt->frames - 1 ? "\n" : ""); // extra \n for last line if (stop != null && strcmp(bt->symbol[i], stop) == 0) { break; } const char** s = alt; while (s != null && *s != null && strcmp(bt->symbol[i], *s) != 0) { s++; } if (s != null && *s != null) { break; } } #pragma pop_macro("rt_backtrace_glyph_called_from") } static const char* rt_backtrace_string(const rt_backtrace_t* bt, char* text, int32_t count) { rt_assert(bt->symbolized, "need rt_backtrace.symbolize(bt)"); char s[1024]; char* p = text; int32_t n = count; for (int32_t i = 0; i < bt->frames && n > 128; i++) { int32_t line = bt->line[i]; const char* file = bt->file[i]; const char* name = bt->symbol[i]; if (file[0] != 0 && name[0] != 0) { rt_str_printf(s, "%s(%d): %s\n", file, line, name); } else if (file[0] == 0 && name[0] != 0) { rt_str_printf(s, "%s\n", name); } s[rt_countof(s) - 1] = 0; int32_t k = (int32_t)strlen(s); if (k < n) { memcpy(p, s, (size_t)k + 1); p += k; n -= k; } } return text; } typedef struct { char name[32]; } rt_backtrace_thread_name_t; static rt_backtrace_thread_name_t rt_backtrace_thread_name(HANDLE thread) { rt_backtrace_thread_name_t tn; tn.name[0] = 0; wchar_t* thread_name = null; if (SUCCEEDED(GetThreadDescription(thread, &thread_name))) { rt_str.utf16to8(tn.name, rt_countof(tn.name), thread_name, -1); LocalFree(thread_name); } return tn; } static void rt_backtrace_context(rt_thread_t thread, const void* ctx, rt_backtrace_t* bt) { CONTEXT* context = (CONTEXT*)ctx; STACKFRAME64 stack_frame = { 0 }; int machine_type = IMAGE_FILE_MACHINE_UNKNOWN; #if defined(_M_IX86) #error "Unsupported platform" #elif defined(_M_ARM64) machine_type = IMAGE_FILE_MACHINE_ARM64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Pc, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Fp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Sp, .Mode = AddrModeFlat} }; #elif defined(_M_X64) machine_type = IMAGE_FILE_MACHINE_AMD64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Rip, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Rbp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Rsp, .Mode = AddrModeFlat} }; #elif defined(_M_IA64) int machine_type = IMAGE_FILE_MACHINE_IA64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->StIIP, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->IntSp, .Mode = AddrModeFlat}, .AddrBStore = {.Offset = context->RsBSP, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->IntSp, .Mode = AddrModeFlat} } #elif defined(_M_ARM64) machine_type = IMAGE_FILE_MACHINE_ARM64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Pc, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Fp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Sp, .Mode = AddrModeFlat} }; #else #error "Unsupported platform" #endif rt_backtrace_init(); while (StackWalk64(machine_type, rt_backtrace_process, (HANDLE)thread, &stack_frame, context, null, SymFunctionTableAccess64, SymGetModuleBase64, null)) { DWORD64 pc = stack_frame.AddrPC.Offset; if (pc == 0) { break; } if (bt->frames < rt_countof(bt->stack)) { bt->stack[bt->frames] = (void*)pc; bt->frames = rt_backtrace_symbolize_frame(bt, bt->frames); } } bt->symbolized = true; } static void rt_backtrace_thread(HANDLE thread, rt_backtrace_t* bt) { bt->frames = 0; // cannot suspend callers thread rt_swear(rt_thread.id_of(thread) != rt_thread.id()); if (SuspendThread(thread) != (DWORD)-1) { CONTEXT context = { .ContextFlags = CONTEXT_FULL }; GetThreadContext(thread, &context); rt_backtrace.context(thread, &context, bt); if (ResumeThread(thread) == (DWORD)-1) { rt_println("ResumeThread() failed %s", rt_str.error(rt_core.err())); ExitProcess(0xBD); } } } static void rt_backtrace_trace_self(const char* stop) { rt_backtrace_t bt = {{0}}; rt_backtrace.capture(&bt, 2); rt_backtrace.symbolize(&bt); rt_backtrace.trace(&bt, stop); } static void rt_backtrace_trace_all_but_self(void) { rt_backtrace_init(); rt_assert(rt_backtrace_process != null && rt_backtrace_pid != 0); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (snapshot == INVALID_HANDLE_VALUE) { rt_println("CreateToolhelp32Snapshot failed %s", rt_str.error(rt_core.err())); } else { THREADENTRY32 te = { .dwSize = sizeof(THREADENTRY32) }; if (!Thread32First(snapshot, &te)) { rt_println("Thread32First failed %s", rt_str.error(rt_core.err())); } else { do { if (te.th32OwnerProcessID == rt_backtrace_pid) { static const DWORD flags = THREAD_ALL_ACCESS | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT; uint32_t tid = te.th32ThreadID; if (tid != (uint32_t)rt_thread.id()) { HANDLE thread = OpenThread(flags, false, tid); if (thread != null) { rt_backtrace_t bt = {0}; rt_backtrace_thread(thread, &bt); rt_backtrace_thread_name_t tn = rt_backtrace_thread_name(thread); rt_debug.println(">Thread", tid, tn.name, "id 0x%08X (%d)", tid, tid); if (bt.frames > 0) { rt_backtrace.trace(&bt, "*"); } rt_debug.println(" 0 && s[count - 1] == 0) { // zero terminated int32_t k = (int32_t)(uintptr_t)( rt_backtrace_test_output_p - rt_backtrace_test_output); int32_t space = rt_countof(rt_backtrace_test_output) - k; if (count < space) { memcpy(rt_backtrace_test_output_p, s, count); rt_backtrace_test_output_p += count - 1; // w/o 0x00 } } else { rt_debug.breakpoint(); // incorrect output() cannot append } return true; // intercepted, do not do OutputDebugString() } static void rt_backtrace_test_thread(void* e) { rt_event.wait(*(rt_event_t*)e); } static void rt_backtrace_test(void) { rt_backtrace_debug_tee = rt_debug.tee; rt_backtrace_test_output_p = rt_backtrace_test_output; rt_backtrace_test_output[0] = 0x00; rt_debug.tee = rt_backtrace_tee; rt_backtrace_t bt = {{0}}; rt_backtrace.capture(&bt, 0); // rt_backtrace_test <- rt_core_test <- run <- main rt_swear(bt.frames >= 3); rt_backtrace.symbolize(&bt); rt_backtrace.trace(&bt, null); rt_backtrace.trace(&bt, "main"); rt_backtrace.trace(&bt, null); rt_backtrace.trace(&bt, "main"); rt_event_t e = rt_event.create(); rt_thread_t thread = rt_thread.start(rt_backtrace_test_thread, &e); rt_backtrace.trace_all_but_self(); rt_event.set(e); rt_thread.join(thread, -1.0); rt_event.dispose(e); rt_debug.tee = rt_backtrace_debug_tee; if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { rt_debug.output(rt_backtrace_test_output, (int32_t)strlen(rt_backtrace_test_output) + 1); } rt_swear(strstr(rt_backtrace_test_output, "rt_backtrace_test") != null, "%s", rt_backtrace_test_output); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_backtrace_test(void) { } #endif rt_backtrace_if rt_backtrace = { .capture = rt_backtrace_capture, .context = rt_backtrace_context, .symbolize = rt_backtrace_symbolize, .trace = rt_backtrace_trace, .trace_self = rt_backtrace_trace_self, .trace_all_but_self = rt_backtrace_trace_all_but_self, .string = rt_backtrace_string, .test = rt_backtrace_test }; // ______________________________ rt_clipboard.c ______________________________ static errno_t rt_clipboard_put_text(const char* utf8) { int32_t chars = rt_str.utf16_chars(utf8, -1); int32_t bytes = (chars + 1) * 2; uint16_t* utf16 = null; errno_t r = rt_heap.alloc((void**)&utf16, (size_t)bytes); if (utf16 != null) { rt_str.utf8to16(utf16, bytes, utf8, -1); rt_assert(utf16[chars - 1] == 0); const int32_t n = (int32_t)rt_str.len16(utf16) + 1; r = OpenClipboard(GetDesktopWindow()) ? 0 : rt_core.err(); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { r = EmptyClipboard() ? 0 : rt_core.err(); if (r != 0) { rt_println("EmptyClipboard() failed %s", rt_strerr(r)); } } void* global = null; if (r == 0) { global = GlobalAlloc(GMEM_MOVEABLE, (size_t)n * 2); r = global != null ? 0 : rt_core.err(); if (r != 0) { rt_println("GlobalAlloc() failed %s", rt_strerr(r)); } } if (r == 0) { char* d = (char*)GlobalLock(global); rt_not_null(d); memcpy(d, utf16, (size_t)n * 2); r = rt_b2e(SetClipboardData(CF_UNICODETEXT, global)); GlobalUnlock(global); if (r != 0) { rt_println("SetClipboardData() failed %s", rt_strerr(r)); GlobalFree(global); } else { // do not free global memory. It's owned by system clipboard now } } if (r == 0) { r = rt_b2e(CloseClipboard()); if (r != 0) { rt_println("CloseClipboard() failed %s", rt_strerr(r)); } } rt_heap.free(utf16); } return r; } static errno_t rt_clipboard_get_text(char* utf8, int32_t* bytes) { rt_not_null(bytes); errno_t r = rt_b2e(OpenClipboard(GetDesktopWindow())); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { HANDLE global = GetClipboardData(CF_UNICODETEXT); if (global == null) { r = rt_core.err(); } else { uint16_t* utf16 = (uint16_t*)GlobalLock(global); if (utf16 != null) { int32_t utf8_bytes = rt_str.utf8_bytes(utf16, -1); if (utf8 != null) { char* decoded = (char*)malloc((size_t)utf8_bytes); if (decoded == null) { r = ERROR_OUTOFMEMORY; } else { rt_str.utf16to8(decoded, utf8_bytes, utf16, -1); int32_t n = rt_min(*bytes, utf8_bytes); memcpy(utf8, decoded, (size_t)n); free(decoded); if (n < utf8_bytes) { r = ERROR_INSUFFICIENT_BUFFER; } } } *bytes = utf8_bytes; GlobalUnlock(global); } } r = rt_b2e(CloseClipboard()); } return r; } #ifdef RT_TESTS static void rt_clipboard_test(void) { rt_fatal_if_error(rt_clipboard.put_text("Hello Clipboard")); char text[256]; int32_t bytes = rt_countof(text); rt_fatal_if_error(rt_clipboard.get_text(text, &bytes)); rt_swear(strcmp(text, "Hello Clipboard") == 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_clipboard_test(void) { } #endif rt_clipboard_if rt_clipboard = { .put_text = rt_clipboard_put_text, .get_text = rt_clipboard_get_text, .put_image = null, // implemented in ui.app .test = rt_clipboard_test }; // ________________________________ rt_clock.c ________________________________ enum { rt_clock_nsec_in_usec = 1000, // nano in micro rt_clock_nsec_in_msec = rt_clock_nsec_in_usec * 1000, // nano in milli rt_clock_nsec_in_sec = rt_clock_nsec_in_msec * 1000, rt_clock_usec_in_msec = 1000, // micro in mill rt_clock_msec_in_sec = 1000, // milli in sec rt_clock_usec_in_sec = rt_clock_usec_in_msec * rt_clock_msec_in_sec // micro in sec }; static uint64_t rt_clock_microseconds_since_epoch(void) { // NOT monotonic FILETIME ft; // time in 100ns interval (tenth of microsecond) // since 12:00 A.M. January 1, 1601 Coordinated Universal Time (UTC) GetSystemTimePreciseAsFileTime(&ft); uint64_t microseconds = (((uint64_t)ft.dwHighDateTime) << 32 | ft.dwLowDateTime) / 10; rt_assert(microseconds > 0); return microseconds; } static uint64_t rt_clock_localtime(void) { TIME_ZONE_INFORMATION tzi; // UTC = local time + bias GetTimeZoneInformation(&tzi); uint64_t bias = (uint64_t)tzi.Bias * 60LL * 1000 * 1000; // in microseconds return rt_clock_microseconds_since_epoch() - bias; } static void rt_clock_utc(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc) { uint64_t time_in_100ns = microseconds * 10; FILETIME mst = { (DWORD)(time_in_100ns & 0xFFFFFFFF), (DWORD)(time_in_100ns >> 32) }; SYSTEMTIME utc; FileTimeToSystemTime(&mst, &utc); *year = utc.wYear; *month = utc.wMonth; *day = utc.wDay; *hh = utc.wHour; *mm = utc.wMinute; *ss = utc.wSecond; *ms = utc.wMilliseconds; *mc = microseconds % 1000; } static void rt_clock_local(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc) { uint64_t time_in_100ns = microseconds * 10; FILETIME mst = { (DWORD)(time_in_100ns & 0xFFFFFFFF), (DWORD)(time_in_100ns >> 32) }; SYSTEMTIME utc; FileTimeToSystemTime(&mst, &utc); DYNAMIC_TIME_ZONE_INFORMATION tzi; GetDynamicTimeZoneInformation(&tzi); SYSTEMTIME lt = {0}; SystemTimeToTzSpecificLocalTimeEx(&tzi, &utc, <); *year = lt.wYear; *month = lt.wMonth; *day = lt.wDay; *hh = lt.wHour; *mm = lt.wMinute; *ss = lt.wSecond; *ms = lt.wMilliseconds; *mc = microseconds % 1000; } static fp64_t rt_clock_seconds(void) { // since_boot LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); static fp64_t one_over_freq; if (one_over_freq == 0) { LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); one_over_freq = 1.0 / (fp64_t)frequency.QuadPart; } return (fp64_t)qpc.QuadPart * one_over_freq; } // Max duration in nanoseconds=2^64 - 1 nanoseconds // 2^64 - 1 ns 1 sec 1 min // Max Duration in Hours = ----------- x ------------ x ------------- // 10^9 ns / s 60 sec / min 60 min / hour // // 1 hour // Max Duration in Days = --------------- // 24 hours / day // // it would take approximately 213,503 days (or about 584.5 years) // for rt_clock.nanoseconds() to overflow // // for divider = rt_num.gcd32(nsec_in_sec, freq) below and 10MHz timer // the actual duration is shorter because of (mul == 100) // (uint64_t)qpc.QuadPart * mul // 64 bit overflow and is about 5.8 years. // // In a long running code like services is advisable to use // rt_clock.nanoseconds() to measure only deltas and pay close attention // to the wrap around despite of 5 years monotony static uint64_t rt_clock_nanoseconds(void) { LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); static uint32_t freq; static uint32_t mul = rt_clock_nsec_in_sec; if (freq == 0) { LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); rt_assert(frequency.HighPart == 0); // even 1GHz frequency should fit into 32 bit unsigned rt_assert(frequency.HighPart == 0, "%08lX%%08lX", frequency.HighPart, frequency.LowPart); // known values: 10,000,000 and 3,000,000 10MHz, 3MHz rt_assert(frequency.LowPart % (1000 * 1000) == 0); // if we start getting weird frequencies not // multiples of MHz rt_num.gcd() approach may need // to be revised in favor of rt_num.muldiv64x64() freq = frequency.LowPart; rt_assert(freq != 0 && freq < (uint32_t)rt_clock.nsec_in_sec); // to avoid rt_num.muldiv128: uint32_t divider = rt_num.gcd32((uint32_t)rt_clock.nsec_in_sec, freq); freq /= divider; mul /= divider; } uint64_t ns_mul_freq = (uint64_t)qpc.QuadPart * mul; return freq == 1 ? ns_mul_freq : ns_mul_freq / freq; } // Difference between 1601 and 1970 in microseconds: static const uint64_t rt_clock_epoch_diff_usec = 11644473600000000ULL; static uint64_t rt_clock_unix_microseconds(void) { return rt_clock.microseconds() - rt_clock_epoch_diff_usec; } static uint64_t rt_clock_unix_seconds(void) { return rt_clock.unix_microseconds() / (uint64_t)rt_clock.usec_in_sec; } static void rt_clock_test(void) { #ifdef RT_TESTS // TODO: implement more tests uint64_t t0 = rt_clock.nanoseconds(); uint64_t t1 = rt_clock.nanoseconds(); int32_t count = 0; while (t0 == t1 && count < 1024) { t1 = rt_clock.nanoseconds(); count++; } rt_swear(t0 != t1, "count: %d t0: %lld t1: %lld", count, t0, t1); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_clock_if rt_clock = { .nsec_in_usec = rt_clock_nsec_in_usec, .nsec_in_msec = rt_clock_nsec_in_msec, .nsec_in_sec = rt_clock_nsec_in_sec, .usec_in_msec = rt_clock_usec_in_msec, .msec_in_sec = rt_clock_msec_in_sec, .usec_in_sec = rt_clock_usec_in_sec, .seconds = rt_clock_seconds, .nanoseconds = rt_clock_nanoseconds, .unix_microseconds = rt_clock_unix_microseconds, .unix_seconds = rt_clock_unix_seconds, .microseconds = rt_clock_microseconds_since_epoch, .localtime = rt_clock_localtime, .utc = rt_clock_utc, .local = rt_clock_local, .test = rt_clock_test }; // _______________________________ rt_config.c ________________________________ // On Unix the implementation should keep KV pairs in // key-named files inside .name/ folder static const char* rt_config_apps = "Software\\ui\\apps"; static const DWORD rt_config_access = KEY_READ|KEY_WRITE|KEY_SET_VALUE|KEY_QUERY_VALUE| KEY_ENUMERATE_SUB_KEYS|DELETE; static errno_t rt_config_get_reg_key(const char* name, HKEY *key) { char path[256] = {0}; rt_str_printf(path, "%s\\%s", rt_config_apps, name); errno_t r = RegOpenKeyExA(HKEY_CURRENT_USER, path, 0, rt_config_access, key); if (r != 0) { const DWORD option = REG_OPTION_NON_VOLATILE; r = RegCreateKeyExA(HKEY_CURRENT_USER, path, 0, null, option, rt_config_access, null, key, null); } return r; } static errno_t rt_config_save(const char* name, const char* key, const void* data, int32_t bytes) { errno_t r = 0; HKEY k = null; r = rt_config_get_reg_key(name, &k); if (k != null) { r = RegSetValueExA(k, key, 0, REG_BINARY, (const uint8_t*)data, (DWORD)bytes); rt_fatal_if_error(RegCloseKey(k)); } return r; } static errno_t rt_config_remove(const char* name, const char* key) { errno_t r = 0; HKEY k = null; r = rt_config_get_reg_key(name, &k); if (k != null) { r = RegDeleteValueA(k, key); rt_fatal_if_error(RegCloseKey(k)); } return r; } static errno_t rt_config_clean(const char* name) { errno_t r = 0; HKEY k = null; if (RegOpenKeyExA(HKEY_CURRENT_USER, rt_config_apps, 0, rt_config_access, &k) == 0) { r = RegDeleteTreeA(k, name); rt_fatal_if_error(RegCloseKey(k)); } return r; } static int32_t rt_config_size(const char* name, const char* key) { int32_t bytes = -1; HKEY k = null; errno_t r = rt_config_get_reg_key(name, &k); if (k != null) { DWORD type = REG_BINARY; DWORD cb = 0; r = RegQueryValueExA(k, key, null, &type, null, &cb); if (r == ERROR_FILE_NOT_FOUND) { bytes = 0; // do not report data_size() often used this way } else if (r != 0) { rt_println("%s.RegQueryValueExA(\"%s\") failed %s", name, key, rt_strerr(r)); bytes = 0; // on any error behave as empty data } else { bytes = (int32_t)cb; } rt_fatal_if_error(RegCloseKey(k)); } return bytes; } static int32_t rt_config_load(const char* name, const char* key, void* data, int32_t bytes) { int32_t read = -1; HKEY k = null; errno_t r = rt_config_get_reg_key(name, &k); if (k != null) { DWORD type = REG_BINARY; DWORD cb = (DWORD)bytes; r = RegQueryValueExA(k, key, null, &type, (uint8_t*)data, &cb); if (r == ERROR_MORE_DATA) { // returns -1 ui_app.data_size() should be used } else if (r != 0) { if (r != ERROR_FILE_NOT_FOUND) { rt_println("%s.RegQueryValueExA(\"%s\") failed %s", name, key, rt_strerr(r)); } read = 0; // on any error behave as empty data } else { read = (int32_t)cb; } rt_fatal_if_error(RegCloseKey(k)); } return read; } #ifdef RT_TESTS static void rt_config_test(void) { const char* name = strrchr(rt_args.v[0], '\\'); if (name == null) { name = strrchr(rt_args.v[0], '/'); } name = name != null ? name + 1 : rt_args.v[0]; rt_swear(name != null); const char* key = "test"; const char data[] = "data"; int32_t bytes = sizeof(data); rt_swear(rt_config.save(name, key, data, bytes) == 0); char read[256]; rt_swear(rt_config.load(name, key, read, bytes) == bytes); int32_t size = rt_config.size(name, key); rt_swear(size == bytes); rt_swear(rt_config.remove(name, key) == 0); rt_swear(rt_config.clean(name) == 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_config_test(void) { } #endif rt_config_if rt_config = { .save = rt_config_save, .size = rt_config_size, .load = rt_config_load, .remove = rt_config_remove, .clean = rt_config_clean, .test = rt_config_test }; // ________________________________ rt_core.c _________________________________ // abort does NOT call atexit() functions and // does NOT flush rt_streams. Also Win32 runtime // abort() attempt to show Abort/Retry/Ignore // MessageBox - thus ExitProcess() static void rt_core_abort(void) { ExitProcess(ERROR_FATAL_APP_EXIT); } static void rt_core_exit(int32_t exit_code) { exit(exit_code); } // TODO: consider r = HRESULT_FROM_WIN32() and r = HRESULT_CODE(hr); // this separates posix error codes from win32 error codes static errno_t rt_core_err(void) { return (errno_t)GetLastError(); } static void rt_core_seterr(errno_t err) { SetLastError((DWORD)err); } rt_static_init(runtime) { SetErrorMode( // The system does not display the critical-error-handler message box. // Instead, the system sends the error to the calling process: SEM_FAILCRITICALERRORS| // The system automatically fixes memory alignment faults and // makes them invisible to the application. SEM_NOALIGNMENTFAULTEXCEPT| // The system does not display the Windows Error Reporting dialog. SEM_NOGPFAULTERRORBOX| // The OpenFile function does not display a message box when it fails // to find a file. Instead, the error is returned to the caller. // This error mode overrides the OF_PROMPT flag. SEM_NOOPENFILEERRORBOX); } #ifdef RT_TESTS static void rt_core_test(void) { // in alphabetical order rt_args.test(); rt_atomics.test(); rt_backtrace.test(); rt_clipboard.test(); rt_clock.test(); rt_config.test(); rt_debug.test(); rt_event.test(); rt_files.test(); rt_generics.test(); rt_heap.test(); rt_loader.test(); rt_mem.test(); rt_mutex.test(); rt_num.test(); rt_processes.test(); rt_static_init_test(); rt_str.test(); rt_streams.test(); rt_thread.test(); rt_vigil.test(); rt_worker.test(); } #else static void rt_core_test(void) { } #endif rt_core_if rt_core = { .err = rt_core_err, .set_err = rt_core_seterr, .abort = rt_core_abort, .exit = rt_core_exit, .test = rt_core_test, .error = { // posix .access_denied = ERROR_ACCESS_DENIED, // EACCES .bad_file = ERROR_BAD_FILE_TYPE, // EBADF .broken_pipe = ERROR_BROKEN_PIPE, // EPIPE .device_not_ready = ERROR_NOT_READY, // ENXIO .directory_not_empty = ERROR_DIR_NOT_EMPTY, // ENOTEMPTY .disk_full = ERROR_DISK_FULL, // ENOSPC .file_exists = ERROR_FILE_EXISTS, // EEXIST .file_not_found = ERROR_FILE_NOT_FOUND, // ENOENT .insufficient_buffer = ERROR_INSUFFICIENT_BUFFER, // E2BIG .interrupted = ERROR_OPERATION_ABORTED, // EINTR .invalid_data = ERROR_INVALID_DATA, // EINVAL .invalid_handle = ERROR_INVALID_HANDLE, // EBADF .invalid_parameter = ERROR_INVALID_PARAMETER, // EINVAL .io_error = ERROR_IO_DEVICE, // EIO .more_data = ERROR_MORE_DATA, // ENOBUFS .name_too_long = ERROR_FILENAME_EXCED_RANGE, // ENAMETOOLONG .no_child_process = ERROR_NO_PROC_SLOTS, // ECHILD .not_a_directory = ERROR_DIRECTORY, // ENOTDIR .not_empty = ERROR_DIR_NOT_EMPTY, // ENOTEMPTY .out_of_memory = ERROR_OUTOFMEMORY, // ENOMEM .path_not_found = ERROR_PATH_NOT_FOUND, // ENOENT .pipe_not_connected = ERROR_PIPE_NOT_CONNECTED, // EPIPE .read_only_file = ERROR_WRITE_PROTECT, // EROFS .resource_deadlock = ERROR_LOCK_VIOLATION, // EDEADLK .too_many_open_files = ERROR_TOO_MANY_OPEN_FILES, // EMFILE } }; #pragma comment(lib, "advapi32") #pragma comment(lib, "ntdll") #pragma comment(lib, "psapi") #pragma comment(lib, "shell32") #pragma comment(lib, "shlwapi") #pragma comment(lib, "kernel32") #pragma comment(lib, "user32") // clipboard #pragma comment(lib, "imm32") // Internationalization input method #pragma comment(lib, "ole32") // rt_files.known_folder CoMemFree #pragma comment(lib, "dbghelp") #pragma comment(lib, "imagehlp") // ________________________________ rt_debug.c ________________________________ static const char* rt_debug_abbreviate(const char* file) { const char* fn = strrchr(file, '\\'); if (fn == null) { fn = strrchr(file, '/'); } return fn != null ? fn + 1 : file; } #ifdef WINDOWS static int32_t rt_debug_max_file_line; static int32_t rt_debug_max_function; static void rt_debug_output(const char* s, int32_t count) { bool intercepted = false; if (rt_debug.tee != null) { intercepted = rt_debug.tee(s, count); } if (!intercepted) { // For link.exe /Subsystem:Windows code stdout/stderr are often closed if (stderr != null && fileno(stderr) >= 0) { fprintf(stderr, "%s", s); } // SetConsoleCP(CP_UTF8) is not guaranteed to be called uint16_t* wide = rt_stackalloc((count + 1) * sizeof(uint16_t)); rt_str.utf8to16(wide, count, s, -1); OutputDebugStringW(wide); } } static void rt_debug_println_va(const char* file, int32_t line, const char* func, const char* format, va_list va) { if (func == null) { func = ""; } char file_line[1024]; if (line == 0 && file == null || file[0] == 0x00) { file_line[0] = 0x00; } else { if (file == null) { file = ""; } // backtrace can have null files // full path is useful in MSVC debugger output pane (clickable) // for all other scenarios short filename without path is preferable: const char* name = IsDebuggerPresent() ? file : rt_files.basename(file); snprintf(file_line, rt_countof(file_line) - 1, "%s(%d):", name, line); } file_line[rt_countof(file_line) - 1] = 0x00; // always zero terminated' rt_debug_max_file_line = rt_max(rt_debug_max_file_line, (int32_t)strlen(file_line)); rt_debug_max_function = rt_max(rt_debug_max_function, (int32_t)strlen(func)); char prefix[2 * 1024]; // snprintf() does not guarantee zero termination on truncation snprintf(prefix, rt_countof(prefix) - 1, "%-*s %-*s", rt_debug_max_file_line, file_line, rt_debug_max_function, func); prefix[rt_countof(prefix) - 1] = 0; // zero terminated char text[2 * 1024]; if (format != null && format[0] != 0) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif vsnprintf(text, rt_countof(text) - 1, format, va); text[rt_countof(text) - 1] = 0; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } else { text[0] = 0; } char output[4 * 1024]; snprintf(output, rt_countof(output) - 1, "%s %s", prefix, text); output[rt_countof(output) - 2] = 0; // strip trailing \n which can be remnant of fprintf("...\n") int32_t n = (int32_t)strlen(output); while (n > 0 && (output[n - 1] == '\n' || output[n - 1] == '\r')) { output[n - 1] = 0; n--; } rt_assert(n + 1 < rt_countof(output)); // Win32 OutputDebugString() needs \n output[n + 0] = '\n'; output[n + 1] = 0; rt_debug.output(output, n + 2); // including 0x00 } #else // posix version: static void rt_debug_vprintf(const char* file, int32_t line, const char* func, const char* format, va_list va) { fprintf(stderr, "%s(%d): %s ", file, line, func); vfprintf(stderr, format, va); fprintf(stderr, "\n"); } #endif static void rt_debug_perrno(const char* file, int32_t line, const char* func, int32_t err_no, const char* format, ...) { if (err_no != 0) { if (format != null && format[0] != 0) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } rt_debug.println(file, line, func, "errno: %d %s", err_no, strerror(err_no)); } } static void rt_debug_perror(const char* file, int32_t line, const char* func, int32_t error, const char* format, ...) { if (error != 0) { if (format != null && format[0] != 0) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } rt_debug.println(file, line, func, "error: %s", rt_strerr(error)); } } static void rt_debug_println(const char* file, int32_t line, const char* func, const char* format, ...) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } static bool rt_debug_is_debugger_present(void) { return IsDebuggerPresent(); } static void rt_debug_breakpoint(void) { if (rt_debug.is_debugger_present()) { DebugBreak(); } } static errno_t rt_debug_raise(uint32_t exception) { rt_core.set_err(0); RaiseException(exception, EXCEPTION_NONCONTINUABLE, 0, null); return rt_core.err(); } static int32_t rt_debug_verbosity_from_string(const char* s) { char* n = null; long v = strtol(s, &n, 10); if (stricmp(s, "quiet") == 0) { return rt_debug.verbosity.quiet; } else if (stricmp(s, "info") == 0) { return rt_debug.verbosity.info; } else if (stricmp(s, "verbose") == 0) { return rt_debug.verbosity.verbose; } else if (stricmp(s, "debug") == 0) { return rt_debug.verbosity.debug; } else if (stricmp(s, "trace") == 0) { return rt_debug.verbosity.trace; } else if (n > s && rt_debug.verbosity.quiet <= v && v <= rt_debug.verbosity.trace) { return v; } else { rt_fatal("invalid verbosity: %s", s); return rt_debug.verbosity.quiet; } } static void rt_debug_test(void) { #ifdef RT_TESTS // not clear what can be tested here if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } #ifndef STATUS_POSSIBLE_DEADLOCK #define STATUS_POSSIBLE_DEADLOCK 0xC0000194uL #endif rt_debug_if rt_debug = { .verbosity = { .level = 0, .quiet = 0, .info = 1, .verbose = 2, .debug = 3, .trace = 4, }, .verbosity_from_string = rt_debug_verbosity_from_string, .tee = null, .output = rt_debug_output, .println = rt_debug_println, .println_va = rt_debug_println_va, .perrno = rt_debug_perrno, .perror = rt_debug_perror, .is_debugger_present = rt_debug_is_debugger_present, .breakpoint = rt_debug_breakpoint, .raise = rt_debug_raise, .exception = { .access_violation = EXCEPTION_ACCESS_VIOLATION, .datatype_misalignment = EXCEPTION_DATATYPE_MISALIGNMENT, .breakpoint = EXCEPTION_BREAKPOINT, .single_step = EXCEPTION_SINGLE_STEP, .array_bounds = EXCEPTION_ARRAY_BOUNDS_EXCEEDED, .float_denormal_operand = EXCEPTION_FLT_DENORMAL_OPERAND, .float_divide_by_zero = EXCEPTION_FLT_DIVIDE_BY_ZERO, .float_inexact_result = EXCEPTION_FLT_INEXACT_RESULT, .float_invalid_operation = EXCEPTION_FLT_INVALID_OPERATION, .float_overflow = EXCEPTION_FLT_OVERFLOW, .float_stack_check = EXCEPTION_FLT_STACK_CHECK, .float_underflow = EXCEPTION_FLT_UNDERFLOW, .int_divide_by_zero = EXCEPTION_INT_DIVIDE_BY_ZERO, .int_overflow = EXCEPTION_INT_OVERFLOW, .priv_instruction = EXCEPTION_PRIV_INSTRUCTION, .in_page_error = EXCEPTION_IN_PAGE_ERROR, .illegal_instruction = EXCEPTION_ILLEGAL_INSTRUCTION, .noncontinuable = EXCEPTION_NONCONTINUABLE_EXCEPTION, .stack_overflow = EXCEPTION_STACK_OVERFLOW, .invalid_disposition = EXCEPTION_INVALID_DISPOSITION, .guard_page = EXCEPTION_GUARD_PAGE, .invalid_handle = EXCEPTION_INVALID_HANDLE, .possible_deadlock = EXCEPTION_POSSIBLE_DEADLOCK }, .test = rt_debug_test }; // ________________________________ rt_files.c ________________________________ // TODO: test FILE_APPEND_DATA // https://learn.microsoft.com/en-us/windows/win32/fileio/appending-one-file-to-another-file?redirectedfrom=MSDN // are posix and Win32 seek in agreement? rt_static_assertion(SEEK_SET == FILE_BEGIN); rt_static_assertion(SEEK_CUR == FILE_CURRENT); rt_static_assertion(SEEK_END == FILE_END); #ifndef O_SYNC #define O_SYNC (0x10000) #endif static errno_t rt_files_open(rt_file_t* *file, const char* fn, int32_t f) { DWORD access = (f & rt_files.o_wr) ? GENERIC_WRITE : (f & rt_files.o_rw) ? GENERIC_READ | GENERIC_WRITE : GENERIC_READ; access |= (f & rt_files.o_append) ? FILE_APPEND_DATA : 0; DWORD disposition = (f & rt_files.o_create) ? ((f & rt_files.o_excl) ? CREATE_NEW : (f & rt_files.o_trunc) ? CREATE_ALWAYS : OPEN_ALWAYS) : (f & rt_files.o_trunc) ? TRUNCATE_EXISTING : OPEN_EXISTING; const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; DWORD attr = FILE_ATTRIBUTE_NORMAL; attr |= (f & O_SYNC) ? FILE_FLAG_WRITE_THROUGH : 0; *file = CreateFileA(fn, access, share, null, disposition, attr, null); return *file != INVALID_HANDLE_VALUE ? 0 : rt_core.err(); } static bool rt_files_is_valid(rt_file_t* file) { // both null and rt_files.invalid return file != rt_files.invalid && file != null; } static errno_t rt_files_seek(rt_file_t* file, int64_t *position, int32_t method) { LARGE_INTEGER distance_to_move = { .QuadPart = *position }; LARGE_INTEGER p = { 0 }; // pointer errno_t r = rt_b2e(SetFilePointerEx(file, distance_to_move, &p, (DWORD)method)); if (r == 0) { *position = p.QuadPart; } return r; } static inline uint64_t rt_files_ft_to_us(FILETIME ft) { // us (microseconds) return (ft.dwLowDateTime | (((uint64_t)ft.dwHighDateTime) << 32)) / 10; } static int64_t rt_files_a2t(DWORD a) { int64_t type = 0; if (a & FILE_ATTRIBUTE_REPARSE_POINT) { type |= rt_files.type_symlink; } if (a & FILE_ATTRIBUTE_DIRECTORY) { type |= rt_files.type_folder; } if (a & FILE_ATTRIBUTE_DEVICE) { type |= rt_files.type_device; } return type; } #ifdef FILES_LINUX_PATH_BY_FD static int get_final_path_name_by_fd(int fd, char *buffer, int32_t bytes) { swear(bytes >= 0); char fd_path[16 * 1024]; // /proc/self/fd/* is a symbolic link snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd); size_t len = readlink(fd_path, buffer, bytes - 1); if (len != -1) { buffer[len] = 0x00; } // Null-terminate the result return len == -1 ? errno : 0; } #endif static errno_t rt_files_stat(rt_file_t* file, rt_files_stat_t* s, bool follow_symlink) { errno_t r = 0; BY_HANDLE_FILE_INFORMATION fi; rt_fatal_win32err(GetFileInformationByHandle(file, &fi)); const bool symlink = (fi.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; if (follow_symlink && symlink) { const DWORD flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS; DWORD n = GetFinalPathNameByHandleA(file, null, 0, flags); if (n == 0) { r = rt_core.err(); } else { char* name = null; r = rt_heap.allocate(null, (void**)&name, (int64_t)n + 2, false); if (r == 0) { n = GetFinalPathNameByHandleA(file, name, n + 1, flags); if (n == 0) { r = rt_core.err(); } else { rt_file_t* f = rt_files.invalid; r = rt_files.open(&f, name, rt_files.o_rd); if (r == 0) { // keep following: r = rt_files.stat(f, s, follow_symlink); rt_files.close(f); } } rt_heap.deallocate(null, name); } } } else { s->size = (int64_t)((uint64_t)fi.nFileSizeLow | (((uint64_t)fi.nFileSizeHigh) << 32)); s->created = rt_files_ft_to_us(fi.ftCreationTime); // since epoch s->accessed = rt_files_ft_to_us(fi.ftLastAccessTime); s->updated = rt_files_ft_to_us(fi.ftLastWriteTime); s->type = rt_files_a2t(fi.dwFileAttributes); } return r; } static errno_t rt_files_read(rt_file_t* file, void* data, int64_t bytes, int64_t *transferred) { errno_t r = 0; *transferred = 0; while (bytes > 0 && r == 0) { DWORD chunk_size = (DWORD)(bytes > UINT32_MAX ? UINT32_MAX : bytes); DWORD bytes_read = 0; r = rt_b2e(ReadFile(file, data, chunk_size, &bytes_read, null)); if (r == 0) { *transferred += bytes_read; bytes -= bytes_read; data = (uint8_t*)data + bytes_read; } } return r; } static errno_t rt_files_write(rt_file_t* file, const void* data, int64_t bytes, int64_t *transferred) { errno_t r = 0; *transferred = 0; while (bytes > 0 && r == 0) { DWORD chunk_size = (DWORD)(bytes > UINT32_MAX ? UINT32_MAX : bytes); DWORD bytes_read = 0; r = rt_b2e(WriteFile(file, data, chunk_size, &bytes_read, null)); if (r == 0) { *transferred += bytes_read; bytes -= bytes_read; data = (const uint8_t*)data + bytes_read; } } return r; } static errno_t rt_files_flush(rt_file_t* file) { return rt_b2e(FlushFileBuffers(file)); } static void rt_files_close(rt_file_t* file) { rt_win32_close_handle(file); } static errno_t rt_files_write_fully(const char* filename, const void* data, int64_t bytes, int64_t *transferred) { if (transferred != null) { *transferred = 0; } errno_t r = 0; const DWORD access = GENERIC_WRITE; const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; const DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH; HANDLE file = CreateFileA(filename, access, share, null, CREATE_ALWAYS, flags, null); if (file == INVALID_HANDLE_VALUE) { r = rt_core.err(); } else { int64_t written = 0; const uint8_t* p = (const uint8_t*)data; while (r == 0 && bytes > 0) { uint64_t write = bytes >= UINT32_MAX ? (uint64_t)(UINT32_MAX) - 0xFFFFuLL : (uint64_t)bytes; rt_assert(0 < write && write < (uint64_t)UINT32_MAX); DWORD chunk = 0; r = rt_b2e(WriteFile(file, p, (DWORD)write, &chunk, null)); written += chunk; bytes -= chunk; } if (transferred != null) { *transferred = written; } errno_t rc = rt_b2e(FlushFileBuffers(file)); if (r == 0) { r = rc; } rt_win32_close_handle(file); } return r; } static errno_t rt_files_unlink(const char* pathname) { if (rt_files.is_folder(pathname)) { return rt_b2e(RemoveDirectoryA(pathname)); } else { return rt_b2e(DeleteFileA(pathname)); } } static errno_t rt_files_create_tmp(char* fn, int32_t count) { // create temporary file (not folder!) see folders_test() about racing rt_swear(fn != null && count > 0); const char* tmp = rt_files.tmp(); errno_t r = 0; if (count < (int32_t)strlen(tmp) + 8) { r = ERROR_BUFFER_OVERFLOW; } else { rt_assert(count > (int32_t)strlen(tmp) + 8); // If GetTempFileNameA() succeeds, the return value is the length, // in chars, of the string copied to lpBuffer, not including the // terminating null character.If the function fails, // the return value is zero. if (count > (int32_t)strlen(tmp) + 8) { char prefix[4] = { 0 }; r = GetTempFileNameA(tmp, prefix, 0, fn) == 0 ? rt_core.err() : 0; if (r == 0) { rt_assert(rt_files.exists(fn) && !rt_files.is_folder(fn)); } else { rt_println("GetTempFileNameA() failed %s", rt_strerr(r)); } } else { r = ERROR_BUFFER_OVERFLOW; } } return r; } #pragma push_macro("files_acl_args") #pragma push_macro("files_get_acl") #pragma push_macro("files_set_acl") #define rt_files_acl_args(acl) DACL_SECURITY_INFORMATION, null, null, acl, null #define rt_files_get_acl(obj, type, acl, sd) (errno_t)( \ (type == SE_FILE_OBJECT ? GetNamedSecurityInfoA((char*)obj, \ SE_FILE_OBJECT, rt_files_acl_args(acl), &sd) : \ (type == SE_KERNEL_OBJECT) ? GetSecurityInfo((HANDLE)obj, \ SE_KERNEL_OBJECT, rt_files_acl_args(acl), &sd) : \ ERROR_INVALID_PARAMETER)) #define rt_files_set_acl(obj, type, acl) (errno_t)( \ (type == SE_FILE_OBJECT ? SetNamedSecurityInfoA((char*)obj, \ SE_FILE_OBJECT, rt_files_acl_args(acl)) : \ (type == SE_KERNEL_OBJECT) ? SetSecurityInfo((HANDLE)obj, \ SE_KERNEL_OBJECT, rt_files_acl_args(acl)) : \ ERROR_INVALID_PARAMETER)) static errno_t rt_files_acl_add_ace(ACL* acl, SID* sid, uint32_t mask, ACL** free_me, byte flags) { ACL_SIZE_INFORMATION info = {0}; ACL* bigger = null; uint32_t bytes_needed = sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(sid) - sizeof(DWORD); errno_t r = rt_b2e(GetAclInformation(acl, &info, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation)); if (r == 0 && info.AclBytesFree < bytes_needed) { const int64_t bytes = (int64_t)(info.AclBytesInUse + bytes_needed); r = rt_heap.allocate(null, (void**)&bigger, bytes, true); if (r == 0) { r = rt_b2e(InitializeAcl((ACL*)bigger, info.AclBytesInUse + bytes_needed, ACL_REVISION)); } } if (r == 0 && bigger != null) { for (int32_t i = 0; i < (int32_t)info.AceCount; i++) { ACCESS_ALLOWED_ACE* ace = null; r = rt_b2e(GetAce(acl, (DWORD)i, (void**)&ace)); if (r != 0) { break; } r = rt_b2e(AddAce(bigger, ACL_REVISION, MAXDWORD, ace, ace->Header.AceSize)); if (r != 0) { break; } } } if (r == 0) { ACCESS_ALLOWED_ACE* ace = null; r = rt_heap.allocate(null, (void**)&ace, bytes_needed, true); if (r == 0) { ace->Header.AceFlags = flags; ace->Header.AceType = ACCESS_ALLOWED_ACE_TYPE; ace->Header.AceSize = (WORD)bytes_needed; ace->Mask = mask; ace->SidStart = sizeof(ACCESS_ALLOWED_ACE); memcpy(&ace->SidStart, sid, GetLengthSid(sid)); r = rt_b2e(AddAce(bigger != null ? bigger : acl, ACL_REVISION, MAXDWORD, ace, bytes_needed)); rt_heap.deallocate(null, ace); } } *free_me = bigger; return r; } static errno_t rt_files_lookup_sid(ACCESS_ALLOWED_ACE* ace) { // handy for debugging SID* sid = (SID*)&ace->SidStart; DWORD l1 = 128, l2 = 128; char account[128]; char group[128]; SID_NAME_USE use; errno_t r = rt_b2e(LookupAccountSidA(null, sid, account, &l1, group, &l2, &use)); if (r == 0) { rt_println("%s/%s: type: %d, mask: 0x%X, flags:%d", group, account, ace->Header.AceType, ace->Mask, ace->Header.AceFlags); } else { rt_println("LookupAccountSidA() failed %s", rt_strerr(r)); } return r; } static errno_t rt_files_add_acl_ace(void* obj, int32_t obj_type, int32_t sid_type, uint32_t mask) { uint8_t stack[SECURITY_MAX_SID_SIZE] = {0}; DWORD n = rt_countof(stack); SID* sid = (SID*)stack; errno_t r = rt_b2e(CreateWellKnownSid((WELL_KNOWN_SID_TYPE)sid_type, null, sid, &n)); if (r != 0) { return ERROR_INVALID_PARAMETER; } ACL* acl = null; void* sd = null; r = rt_files_get_acl(obj, obj_type, &acl, sd); if (r == 0) { ACCESS_ALLOWED_ACE* found = null; for (int32_t i = 0; i < acl->AceCount; i++) { ACCESS_ALLOWED_ACE* ace = null; r = rt_b2e(GetAce(acl, (DWORD)i, (void**)&ace)); if (r != 0) { break; } if (EqualSid((SID*)&ace->SidStart, sid)) { if (ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && (ace->Header.AceFlags & INHERITED_ACE) == 0) { found = ace; } else if (ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE) { rt_println("%d ACE_TYPE is not supported.", ace->Header.AceType); r = ERROR_INVALID_PARAMETER; } break; } } if (r == 0 && found) { if ((found->Mask & mask) != mask) { // rt_println("updating existing ace"); found->Mask |= mask; r = rt_files_set_acl(obj, obj_type, acl); } else { // rt_println("desired access is already allowed by ace"); } } else if (r == 0) { // rt_println("inserting new ace"); ACL* new_acl = null; byte flags = obj_type == SE_FILE_OBJECT ? CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE : 0; r = rt_files_acl_add_ace(acl, sid, mask, &new_acl, flags); if (r == 0) { r = rt_files_set_acl(obj, obj_type, (new_acl != null ? new_acl : acl)); } if (new_acl != null) { rt_heap.deallocate(null, new_acl); } } } if (sd != null) { LocalFree(sd); } return r; } #pragma pop_macro("files_set_acl") #pragma pop_macro("files_get_acl") #pragma pop_macro("files_acl_args") static errno_t rt_files_chmod777(const char* pathname) { SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; PSID everyone = null; // Create a well-known SID for the Everyone group. rt_fatal_win32err(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyone)); EXPLICIT_ACCESSA ea[1] = { { 0 } }; // Initialize an EXPLICIT_ACCESS structure for an ACE. ea[0].grfAccessPermissions = 0xFFFFFFFF; ea[0].grfAccessMode = GRANT_ACCESS; // The ACE will allow everyone all access. ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ea[0].Trustee.ptstrName = (LPSTR)everyone; // Create a new ACL that contains the new ACEs. ACL* acl = null; rt_fatal_if_error(SetEntriesInAclA(1, ea, null, &acl)); // Initialize a security descriptor. uint8_t stack[SECURITY_DESCRIPTOR_MIN_LENGTH] = {0}; SECURITY_DESCRIPTOR* sd = (SECURITY_DESCRIPTOR*)stack; rt_fatal_win32err(InitializeSecurityDescriptor(sd, SECURITY_DESCRIPTOR_REVISION)); // Add the ACL to the security descriptor. rt_fatal_win32err(SetSecurityDescriptorDacl(sd, /* present flag: */ true, acl, /* not a default DACL: */ false)); // Change the security attributes errno_t r = rt_b2e(SetFileSecurityA(pathname, DACL_SECURITY_INFORMATION, sd)); if (r != 0) { rt_println("chmod777(%s) failed %s", pathname, rt_strerr(r)); } if (everyone != null) { FreeSid(everyone); } if (acl != null) { LocalFree(acl); } return r; } // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectorya // "If lpSecurityAttributes is null, the directory gets a default security // descriptor. The ACLs in the default security descriptor for a directory // are inherited from its parent directory." static errno_t rt_files_mkdirs(const char* dir) { const int32_t n = (int32_t)strlen(dir) + 1; char* s = null; errno_t r = rt_heap.allocate(null, (void**)&s, n, true); const char* next = strchr(dir, '\\'); if (next == null) { next = strchr(dir, '/'); } while (r == 0 && next != null) { if (next > dir && *(next - 1) != ':') { memcpy(s, dir, (size_t)(next - dir)); r = rt_b2e(CreateDirectoryA(s, null)); if (r == ERROR_ALREADY_EXISTS) { r = 0; } } if (r == 0) { const char* prev = ++next; next = strchr(prev, '\\'); if (next == null) { next = strchr(prev, '/'); } } } if (r == 0) { r = rt_b2e(CreateDirectoryA(dir, null)); } rt_heap.deallocate(null, s); return r == ERROR_ALREADY_EXISTS ? 0 : r; } #pragma push_macro("rt_files_realloc_path") #pragma push_macro("rt_files_append_name") #define rt_files_realloc_path(r, pn, pnc, fn, name) do { \ const int32_t bytes = (int32_t)(strlen(fn) + strlen(name) + 3); \ if (bytes > pnc) { \ r = rt_heap.reallocate(null, (void**)&pn, bytes, false); \ if (r != 0) { \ pnc = bytes; \ } else { \ rt_heap.deallocate(null, pn); \ pn = null; \ } \ } \ } while (0) #define rt_files_append_name(pn, pnc, fn, name) do { \ if (strcmp(fn, "\\") == 0 || strcmp(fn, "/") == 0) { \ rt_str.format(pn, pnc, "\\%s", name); \ } else { \ rt_str.format(pn, pnc, "%.*s\\%s", k, fn, name); \ } \ } while (0) static errno_t rt_files_rmdirs(const char* fn) { rt_files_stat_t st; rt_folder_t folder; errno_t r = rt_files.opendir(&folder, fn); if (r == 0) { int32_t k = (int32_t)strlen(fn); // remove trailing backslash (except if it is root: "/" or "\\") if (k > 1 && (fn[k - 1] == '/' || fn[k - 1] == '\\')) { k--; } int32_t pnc = 64 * 1024; // pathname "pn" capacity in bytes char* pn = null; r = rt_heap.allocate(null, (void**)&pn, pnc, false); while (r == 0) { // recurse into sub folders and remove them first // do NOT follow symlinks - it could be disastrous const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && (st.type & rt_files.type_symlink) == 0 && (st.type & rt_files.type_folder) != 0) { rt_files_realloc_path(r, pn, pnc, fn, name); if (r == 0) { rt_files_append_name(pn, pnc, fn, name); r = rt_files.rmdirs(pn); } } } rt_files.closedir(&folder); r = rt_files.opendir(&folder, fn); while (r == 0) { const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } // symlinks are already removed as normal files if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && (st.type & rt_files.type_folder) == 0) { rt_files_realloc_path(r, pn, pnc, fn, name); if (r == 0) { rt_files_append_name(pn, pnc, fn, name); r = rt_files.unlink(pn); if (r != 0) { rt_println("remove(%s) failed %s", pn, rt_strerr(r)); } } } } rt_heap.deallocate(null, pn); rt_files.closedir(&folder); } if (r == 0) { r = rt_files.unlink(fn); } return r; } #pragma pop_macro("rt_files_append_name") #pragma pop_macro("rt_files_realloc_path") static bool rt_files_exists(const char* path) { return PathFileExistsA(path); } static bool rt_files_is_folder(const char* path) { return PathIsDirectoryA(path); } static bool rt_files_is_symlink(const char* filename) { DWORD attributes = GetFileAttributesA(filename); return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; } static const char* rt_files_basename(const char* pathname) { const char* bn = strrchr(pathname, '\\'); if (bn == null) { bn = strrchr(pathname, '/'); } return bn != null ? bn + 1 : pathname; } static errno_t rt_files_copy(const char* s, const char* d) { return rt_b2e(CopyFileA(s, d, false)); } static errno_t rt_files_move(const char* s, const char* d) { static const DWORD flags = MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH; return rt_b2e(MoveFileExA(s, d, flags)); } static errno_t rt_files_link(const char* from, const char* to) { // note reverse order of parameters: return rt_b2e(CreateHardLinkA(to, from, null)); } static errno_t rt_files_symlink(const char* from, const char* to) { // The correct order of parameters for CreateSymbolicLinkA is: // CreateSymbolicLinkA(symlink_to_create, existing_file, flags); DWORD flags = rt_files.is_folder(from) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; return rt_b2e(CreateSymbolicLinkA(to, from, flags)); } static const char* rt_files_known_folder(int32_t kf) { // known folder ids order must match enum see: static const GUID* kf_ids[] = { &FOLDERID_Profile, &FOLDERID_Desktop, &FOLDERID_Documents, &FOLDERID_Downloads, &FOLDERID_Music, &FOLDERID_Pictures, &FOLDERID_Videos, &FOLDERID_Public, &FOLDERID_ProgramFiles, &FOLDERID_ProgramData }; static rt_file_name_t known_folders[rt_countof(kf_ids)]; rt_fatal_if(!(0 <= kf && kf < rt_countof(kf_ids)), "invalid kf=%d", kf); if (known_folders[kf].s[0] == 0) { uint16_t* path = null; rt_fatal_if_error(SHGetKnownFolderPath(kf_ids[kf], 0, null, &path)); const int32_t n = rt_countof(known_folders[kf].s); rt_str.utf16to8(known_folders[kf].s, n, path, -1); CoTaskMemFree(path); } return known_folders[kf].s; } static const char* rt_files_bin(void) { return rt_files_known_folder(rt_files.folder.bin); } static const char* rt_files_data(void) { return rt_files_known_folder(rt_files.folder.data); } static const char* rt_files_tmp(void) { static char tmp[rt_files_max_path]; if (tmp[0] == 0) { // If GetTempPathA() succeeds, the return value is the length, // in chars, of the string copied to lpBuffer, not including // the terminating null character. If the function fails, the // return value is zero. errno_t r = GetTempPathA(rt_countof(tmp), tmp) == 0 ? rt_core.err() : 0; rt_fatal_if(r != 0, "GetTempPathA() failed %s", rt_strerr(r)); } return tmp; } static errno_t rt_files_cwd(char* fn, int32_t count) { rt_swear(count > 1); DWORD bytes = (DWORD)(count - 1); errno_t r = rt_b2e(GetCurrentDirectoryA(bytes, fn)); fn[count - 1] = 0; // always return r; } static errno_t rt_files_chdir(const char* fn) { return rt_b2e(SetCurrentDirectoryA(fn)); } typedef struct rt_files_dir_s { HANDLE handle; WIN32_FIND_DATAA find; // On Win64: 320 bytes } rt_files_dir_t; rt_static_assertion(sizeof(rt_files_dir_t) <= sizeof(rt_folder_t)); static errno_t rt_files_opendir(rt_folder_t* folder, const char* folder_name) { rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; int32_t n = (int32_t)strlen(folder_name); char* fn = null; // extra room for "\*" suffix errno_t r = rt_heap.allocate(null, (void**)&fn, (int64_t)n + 3, false); if (r == 0) { rt_str.format(fn, n + 3, "%s\\*", folder_name); fn[n + 2] = 0; d->handle = FindFirstFileA(fn, &d->find); if (d->handle == INVALID_HANDLE_VALUE) { r = rt_core.err(); } rt_heap.deallocate(null, fn); } return r; } static uint64_t rt_files_ft2us(FILETIME* ft) { // 100ns units to microseconds: return (((uint64_t)ft->dwHighDateTime) << 32 | ft->dwLowDateTime) / 10; } static const char* rt_files_readdir(rt_folder_t* folder, rt_files_stat_t* s) { const char* fn = null; rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; if (FindNextFileA(d->handle, &d->find)) { fn = d->find.cFileName; // Ensure zero termination d->find.cFileName[rt_countof(d->find.cFileName) - 1] = 0x00; if (s != null) { s->accessed = rt_files_ft2us(&d->find.ftLastAccessTime); s->created = rt_files_ft2us(&d->find.ftCreationTime); s->updated = rt_files_ft2us(&d->find.ftLastWriteTime); s->type = rt_files_a2t(d->find.dwFileAttributes); s->size = (int64_t)((((uint64_t)d->find.nFileSizeHigh) << 32) | (uint64_t)d->find.nFileSizeLow); } } return fn; } static void rt_files_closedir(rt_folder_t* folder) { rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; rt_fatal_win32err(FindClose(d->handle)); } #pragma push_macro("files_test_failed") #ifdef RT_TESTS // TODO: change rt_fatal_if() to swear() #define rt_files_test_failed " failed %s", rt_strerr(rt_core.err()) #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void folders_dump_time(const char* label, uint64_t us) { int32_t year = 0; int32_t month = 0; int32_t day = 0; int32_t hh = 0; int32_t mm = 0; int32_t ss = 0; int32_t ms = 0; int32_t mc = 0; rt_clock.local(us, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); rt_println("%-7s: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d", label, year, month, day, hh, mm, ss, ms, mc); } static void folders_test(void) { uint64_t now = rt_clock.microseconds(); // microseconds since epoch uint64_t before = now - 1 * (uint64_t)rt_clock.usec_in_sec; // one second earlier uint64_t after = now + 2 * (uint64_t)rt_clock.usec_in_sec; // two seconds later int32_t year = 0; int32_t month = 0; int32_t day = 0; int32_t hh = 0; int32_t mm = 0; int32_t ss = 0; int32_t ms = 0; int32_t mc = 0; rt_clock.local(now, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); verbose("now: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d", year, month, day, hh, mm, ss, ms, mc); // Test cwd, setcwd const char* tmp = rt_files.tmp(); char cwd[256] = { 0 }; rt_fatal_if(rt_files.cwd(cwd, sizeof(cwd)) != 0, "rt_files.cwd() failed"); rt_fatal_if(rt_files.chdir(tmp) != 0, "rt_files.chdir(\"%s\") failed %s", tmp, rt_strerr(rt_core.err())); // there is no racing free way to create temporary folder // without having a temporary file for the duration of folder usage: char tmp_file[rt_files_max_path]; // create_tmp() is thread safe race free: errno_t r = rt_files.create_tmp(tmp_file, rt_countof(tmp_file)); rt_fatal_if(r != 0, "rt_files.create_tmp() failed %s", rt_strerr(r)); char tmp_dir[rt_files_max_path]; rt_str_printf(tmp_dir, "%s.dir", tmp_file); r = rt_files.mkdirs(tmp_dir); rt_fatal_if(r != 0, "rt_files.mkdirs(%s) failed %s", tmp_dir, rt_strerr(r)); verbose("%s", tmp_dir); rt_folder_t folder; char pn[rt_files_max_path] = { 0 }; rt_str_printf(pn, "%s/file", tmp_dir); // cannot test symlinks because they are only // available to Administrators and in Developer mode // char sym[rt_files_max_path] = { 0 }; char hard[rt_files_max_path] = { 0 }; char sub[rt_files_max_path] = { 0 }; rt_str_printf(hard, "%s/hard", tmp_dir); rt_str_printf(sub, "%s/subd", tmp_dir); const char* content = "content"; int64_t transferred = 0; r = rt_files.write_fully(pn, content, (int64_t)strlen(content), &transferred); rt_fatal_if(r != 0, "rt_files.write_fully(\"%s\") failed %s", pn, rt_strerr(r)); rt_swear(transferred == (int64_t)strlen(content)); r = rt_files.link(pn, hard); rt_fatal_if(r != 0, "rt_files.link(\"%s\", \"%s\") failed %s", pn, hard, rt_strerr(r)); r = rt_files.mkdirs(sub); rt_fatal_if(r != 0, "rt_files.mkdirs(\"%s\") failed %s", sub, rt_strerr(r)); r = rt_files.opendir(&folder, tmp_dir); rt_fatal_if(r != 0, "rt_files.opendir(\"%s\") failed %s", tmp_dir, rt_strerr(r)); for (;;) { rt_files_stat_t st = { 0 }; const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } uint64_t at = st.accessed; uint64_t ct = st.created; uint64_t ut = st.updated; rt_swear(ct <= at && ct <= ut); rt_clock.local(ct, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); bool is_folder = st.type & rt_files.type_folder; bool is_symlink = st.type & rt_files.type_symlink; int64_t bytes = st.size; verbose("%s: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d %lld bytes %s%s", name, year, month, day, hh, mm, ss, ms, mc, bytes, is_folder ? "[folder]" : "", is_symlink ? "[symlink]" : ""); if (strcmp(name, "file") == 0 || strcmp(name, "hard") == 0) { rt_swear(bytes == (int64_t)strlen(content), "size of \"%s\": %lld is incorrect expected: %d", name, bytes, transferred); } if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { rt_swear(is_folder, "\"%s\" is_folder: %d", name, is_folder); } else { rt_swear((strcmp(name, "subd") == 0) == is_folder, "\"%s\" is_folder: %d", name, is_folder); // empirically timestamps are imprecise on NTFS rt_swear(at >= before, "access: %lld >= %lld", at, before); if (ct < before || ut < before || at >= after || ct >= after || ut >= after) { rt_println("file: %s", name); folders_dump_time("before", before); folders_dump_time("create", ct); folders_dump_time("update", ut); folders_dump_time("access", at); } rt_swear(ct >= before, "create: %lld >= %lld", ct, before); rt_swear(ut >= before, "update: %lld >= %lld", ut, before); // and no later than 2 seconds since folders_test() rt_swear(at < after, "access: %lld < %lld", at, after); rt_swear(ct < after, "create: %lld < %lld", ct, after); rt_swear(at < after, "update: %lld < %lld", ut, after); } } rt_files.closedir(&folder); r = rt_files.rmdirs(tmp_dir); rt_fatal_if(r != 0, "rt_files.rmdirs(\"%s\") failed %s", tmp_dir, rt_strerr(r)); r = rt_files.unlink(tmp_file); rt_fatal_if(r != 0, "rt_files.unlink(\"%s\") failed %s", tmp_file, rt_strerr(r)); rt_fatal_if(rt_files.chdir(cwd) != 0, "rt_files.chdir(\"%s\") failed %s", cwd, rt_strerr(rt_core.err())); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("verbose") static void rt_files_test_append_thread(void* p) { rt_file_t* f = (rt_file_t*)p; uint8_t data[256] = {0}; for (int i = 0; i < 256; i++) { data[i] = (uint8_t)i; } int64_t transferred = 0; rt_fatal_if(rt_files.write(f, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write()" rt_files_test_failed); } static void rt_files_test(void) { folders_test(); uint64_t now = rt_clock.microseconds(); // epoch time char tf[256]; // temporary file rt_fatal_if(rt_files.create_tmp(tf, rt_countof(tf)) != 0, "rt_files.create_tmp()" rt_files_test_failed); uint8_t data[256] = {0}; int64_t transferred = 0; for (int i = 0; i < 256; i++) { data[i] = (uint8_t)i; } { rt_file_t* f = rt_files.invalid; rt_fatal_if(rt_files.open(&f, tf, rt_files.o_wr | rt_files.o_create | rt_files.o_trunc) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_fatal_if(rt_files.write_fully(tf, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write_fully()" rt_files_test_failed); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rd) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); for (int32_t i = 0; i < 256; i++) { for (int32_t j = 1; j < 256 - i; j++) { uint8_t test[rt_countof(data)] = { 0 }; int64_t position = i; rt_fatal_if(rt_files.seek(f, &position, rt_files.seek_set) != 0 || position != i, "rt_files.seek(position: %lld) failed %s", position, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.read(f, test, j, &transferred) != 0 || transferred != j, "rt_files.read() transferred: %lld failed %s", transferred, rt_strerr(rt_core.err())); for (int32_t k = 0; k < j; k++) { rt_swear(test[k] == data[i + k], "Data mismatch at position: %d, length %d" "test[%d]: 0x%02X != data[%d + %d]: 0x%02X ", i, j, k, test[k], i, k, data[i + k]); } } } rt_swear((rt_files.o_rd | rt_files.o_wr) != rt_files.o_rw); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rw) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); for (int32_t i = 0; i < 256; i++) { uint8_t val = ~data[i]; int64_t pos = i; rt_fatal_if(rt_files.seek(f, &pos, rt_files.seek_set) != 0 || pos != i, "rt_files.seek() failed %s", rt_core.err()); rt_fatal_if(rt_files.write(f, &val, 1, &transferred) != 0 || transferred != 1, "rt_files.write()" rt_files_test_failed); pos = i; rt_fatal_if(rt_files.seek(f, &pos, rt_files.seek_set) != 0 || pos != i, "rt_files.seek(pos: %lld i: %d) failed %s", pos, i, rt_core.err()); uint8_t read_val = 0; rt_fatal_if(rt_files.read(f, &read_val, 1, &transferred) != 0 || transferred != 1, "rt_files.read()" rt_files_test_failed); rt_swear(read_val == val, "Data mismatch at position %d", i); } rt_files_stat_t s = { 0 }; rt_files.stat(f, &s, false); uint64_t before = now - 1 * (uint64_t)rt_clock.usec_in_sec; // one second before now uint64_t after = now + 2 * (uint64_t)rt_clock.usec_in_sec; // two seconds after rt_swear(before <= s.created && s.created <= after, "before: %lld created: %lld after: %lld", before, s.created, after); rt_swear(before <= s.accessed && s.accessed <= after, "before: %lld created: %lld accessed: %lld", before, s.accessed, after); rt_swear(before <= s.updated && s.updated <= after, "before: %lld created: %lld updated: %lld", before, s.updated, after); rt_files.close(f); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_wr | rt_files.o_create | rt_files.o_trunc) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_files.stat(f, &s, false); rt_swear(s.size == 0, "File is not empty after truncation. .size: %lld", s.size); rt_files.close(f); } { // Append test with threads rt_file_t* f = rt_files.invalid; rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rw | rt_files.o_append) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_thread_t thread1 = rt_thread.start(rt_files_test_append_thread, f); rt_thread_t thread2 = rt_thread.start(rt_files_test_append_thread, f); rt_thread.join(thread1, -1); rt_thread.join(thread2, -1); rt_files.close(f); } { // write_fully, exists, is_folder, mkdirs, rmdirs, create_tmp, chmod777 rt_fatal_if(rt_files.write_fully(tf, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write_fully() failed %s", rt_core.err()); rt_fatal_if(!rt_files.exists(tf), "file \"%s\" does not exist", tf); rt_fatal_if(rt_files.is_folder(tf), "%s is a folder", tf); rt_fatal_if(rt_files.chmod777(tf) != 0, "rt_files.chmod777(\"%s\") failed %s", tf, rt_strerr(rt_core.err())); char folder[256] = { 0 }; rt_str_printf(folder, "%s.folder\\subfolder", tf); rt_fatal_if(rt_files.mkdirs(folder) != 0, "rt_files.mkdirs(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.is_folder(folder), "\"%s\" is not a folder", folder); rt_fatal_if(rt_files.chmod777(folder) != 0, "rt_files.chmod777(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.rmdirs(folder) != 0, "rt_files.rmdirs(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists(folder), "folder \"%s\" still exists", folder); } { // getcwd, chdir const char* tmp = rt_files.tmp(); char cwd[256] = { 0 }; rt_fatal_if(rt_files.cwd(cwd, sizeof(cwd)) != 0, "rt_files.cwd() failed"); rt_fatal_if(rt_files.chdir(tmp) != 0, "rt_files.chdir(\"%s\") failed %s", tmp, rt_strerr(rt_core.err())); // symlink if (rt_processes.is_elevated()) { char sym_link[rt_files_max_path]; rt_str_printf(sym_link, "%s.sym_link", tf); rt_fatal_if(rt_files.symlink(tf, sym_link) != 0, "rt_files.symlink(\"%s\", \"%s\") failed %s", tf, sym_link, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.is_symlink(sym_link), "\"%s\" is not a sym_link", sym_link); rt_fatal_if(rt_files.unlink(sym_link) != 0, "rt_files.unlink(\"%s\") failed %s", sym_link, rt_strerr(rt_core.err())); } else { rt_println("Skipping rt_files.symlink test: process is not elevated"); } // hard link char hard_link[rt_files_max_path]; rt_str_printf(hard_link, "%s.hard_link", tf); rt_fatal_if(rt_files.link(tf, hard_link) != 0, "rt_files.link(\"%s\", \"%s\") failed %s", tf, hard_link, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.exists(hard_link), "\"%s\" does not exist", hard_link); rt_fatal_if(rt_files.unlink(hard_link) != 0, "rt_files.unlink(\"%s\") failed %s", hard_link, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists(hard_link), "\"%s\" still exists", hard_link); // copy, move: rt_fatal_if(rt_files.copy(tf, "copied_file") != 0, "rt_files.copy(\"%s\", 'copied_file') failed %s", tf, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.exists("copied_file"), "'copied_file' does not exist"); rt_fatal_if(rt_files.move("copied_file", "moved_file") != 0, "rt_files.move('copied_file', 'moved_file') failed %s", rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists("copied_file"), "'copied_file' still exists"); rt_fatal_if(!rt_files.exists("moved_file"), "'moved_file' does not exist"); rt_fatal_if(rt_files.unlink("moved_file") != 0, "rt_files.unlink('moved_file') failed %s", rt_strerr(rt_core.err())); rt_fatal_if(rt_files.chdir(cwd) != 0, "rt_files.chdir(\"%s\") failed %s", cwd, rt_strerr(rt_core.err())); } rt_fatal_if(rt_files.unlink(tf) != 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_files_test(void) {} #endif // RT_TESTS #pragma pop_macro("files_test_failed") rt_files_if rt_files = { .invalid = (rt_file_t*)INVALID_HANDLE_VALUE, // rt_files_stat_t.type: .type_folder = 0x00000010, // FILE_ATTRIBUTE_DIRECTORY .type_symlink = 0x00000400, // FILE_ATTRIBUTE_REPARSE_POINT .type_device = 0x00000040, // FILE_ATTRIBUTE_DEVICE // seek() methods: .seek_set = SEEK_SET, .seek_cur = SEEK_CUR, .seek_end = SEEK_END, // open() flags: missing O_RSYNC, O_DSYNC, O_NONBLOCK, O_NOCTTY .o_rd = O_RDONLY, .o_wr = O_WRONLY, .o_rw = O_RDWR, .o_append = O_APPEND, .o_create = O_CREAT, .o_excl = O_EXCL, .o_trunc = O_TRUNC, .o_sync = O_SYNC, // known folders ids: .folder = { .home = 0, // c:\Users\ .desktop = 1, .documents = 2, .downloads = 3, .music = 4, .pictures = 5, .videos = 6, .shared = 7, // c:\Users\Public .bin = 8, // c:\Program Files .data = 9 // c:\ProgramData }, // methods: .open = rt_files_open, .is_valid = rt_files_is_valid, .seek = rt_files_seek, .stat = rt_files_stat, .read = rt_files_read, .write = rt_files_write, .flush = rt_files_flush, .close = rt_files_close, .write_fully = rt_files_write_fully, .exists = rt_files_exists, .is_folder = rt_files_is_folder, .is_symlink = rt_files_is_symlink, .mkdirs = rt_files_mkdirs, .rmdirs = rt_files_rmdirs, .create_tmp = rt_files_create_tmp, .chmod777 = rt_files_chmod777, .unlink = rt_files_unlink, .link = rt_files_link, .symlink = rt_files_symlink, .basename = rt_files_basename, .copy = rt_files_copy, .move = rt_files_move, .cwd = rt_files_cwd, .chdir = rt_files_chdir, .known_folder = rt_files_known_folder, .bin = rt_files_bin, .data = rt_files_data, .tmp = rt_files_tmp, .opendir = rt_files_opendir, .readdir = rt_files_readdir, .closedir = rt_files_closedir, .test = rt_files_test }; // ______________________________ rt_generics.c _______________________________ #ifdef RT_TESTS static void rt_generics_test(void) { { int8_t a = 10, b = 20; rt_swear(rt_max(a++, b++) == 20); rt_swear(rt_min(a++, b++) == 11); } { int32_t a = 10, b = 20; rt_swear(rt_max(a++, b++) == 20); rt_swear(rt_min(a++, b++) == 11); } { fp32_t a = 1.1f, b = 2.2f; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp64_t a = 1.1, b = 2.2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp32_t a = 1.1f, b = 2.2f; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp64_t a = 1.1, b = 2.2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { char a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { unsigned char a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } // MS cl.exe version 19.39.33523 has issues with "long": // does not pick up int32_t/uint32_t types for "long" and "unsigned long" { long int a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { unsigned long a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { long long a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_generics_test(void) { } #endif rt_generics_if rt_generics = { .test = rt_generics_test }; // ________________________________ rt_heap.c _________________________________ static errno_t rt_heap_alloc(void* *a, int64_t bytes) { return rt_heap.allocate(null, a, bytes, false); } static errno_t rt_heap_alloc_zero(void* *a, int64_t bytes) { return rt_heap.allocate(null, a, bytes, true); } static errno_t rt_heap_realloc(void* *a, int64_t bytes) { return rt_heap.reallocate(null, a, bytes, false); } static errno_t rt_heap_realloc_zero(void* *a, int64_t bytes) { return rt_heap.reallocate(null, a, bytes, true); } static void rt_heap_free(void* a) { rt_heap.deallocate(null, a); } static rt_heap_t* rt_heap_create(bool serialized) { const DWORD options = serialized ? 0 : HEAP_NO_SERIALIZE; return (rt_heap_t*)HeapCreate(options, 0, 0); } static void rt_heap_dispose(rt_heap_t* h) { rt_fatal_win32err(HeapDestroy((HANDLE)h)); } static inline HANDLE rt_heap_or_process_heap(rt_heap_t* h) { static HANDLE process_heap; if (process_heap == null) { process_heap = GetProcessHeap(); } return h != null ? (HANDLE)h : process_heap; } static errno_t rt_heap_allocate(rt_heap_t* h, void* *p, int64_t bytes, bool zero) { rt_swear(bytes > 0); #ifdef DEBUG static bool enabled; if (!enabled) { enabled = true; HeapSetInformation(null, HeapEnableTerminationOnCorruption, null, 0); } #endif const DWORD flags = zero ? HEAP_ZERO_MEMORY : 0; *p = HeapAlloc(rt_heap_or_process_heap(h), flags, (SIZE_T)bytes); return *p == null ? ERROR_OUTOFMEMORY : 0; } static errno_t rt_heap_reallocate(rt_heap_t* h, void* *p, int64_t bytes, bool zero) { rt_swear(bytes > 0); const DWORD flags = zero ? HEAP_ZERO_MEMORY : 0; void* a = *p == null ? // HeapReAlloc(..., null, bytes) may not work HeapAlloc(rt_heap_or_process_heap(h), flags, (SIZE_T)bytes) : HeapReAlloc(rt_heap_or_process_heap(h), flags, *p, (SIZE_T)bytes); if (a != null) { *p = a; } return a == null ? ERROR_OUTOFMEMORY : 0; } static void rt_heap_deallocate(rt_heap_t* h, void* a) { rt_fatal_win32err(HeapFree(rt_heap_or_process_heap(h), 0, a)); } static int64_t rt_heap_bytes(rt_heap_t* h, void* a) { SIZE_T bytes = HeapSize(rt_heap_or_process_heap(h), 0, a); rt_fatal_if(bytes == (SIZE_T)-1); return (int64_t)bytes; } #ifdef RT_TESTS static void rt_heap_test(void) { // TODO: allocate, reallocate deallocate, create, dispose void* a[1024]; // addresses int32_t b[1024]; // bytes uint32_t seed = 0x1; for (int i = 0; i < 1024; i++) { b[i] = (int32_t)(rt_num.random32(&seed) % 1024) + 1; errno_t r = rt_heap.alloc(&a[i], b[i]); rt_swear(r == 0); } for (int i = 0; i < 1024; i++) { rt_heap.free(a[i]); } HeapCompact(rt_heap_or_process_heap(null), 0); // "There is no extended error information for HeapValidate; // do not call GetLastError." rt_swear(HeapValidate(rt_heap_or_process_heap(null), 0, null)); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_heap_test(void) { } #endif rt_heap_if rt_heap = { .alloc = rt_heap_alloc, .alloc_zero = rt_heap_alloc_zero, .realloc = rt_heap_realloc, .realloc_zero = rt_heap_realloc_zero, .free = rt_heap_free, .create = rt_heap_create, .allocate = rt_heap_allocate, .reallocate = rt_heap_reallocate, .deallocate = rt_heap_deallocate, .bytes = rt_heap_bytes, .dispose = rt_heap_dispose, .test = rt_heap_test }; // _______________________________ rt_loader.c ________________________________ // This is oversimplified Win32 version completely ignoring mode. // I bit more Posix compliant version is here: // https://github.com/dlfcn-win32/dlfcn-win32/blob/master/src/dlfcn.c // POSIX says that if the value of file is NULL, a handle on a global // symbol object must be provided. That object must be able to access // all symbols from the original program file, and any objects loaded // with the RTLD_GLOBAL flag. // The return value from GetModuleHandle( ) allows us to retrieve // symbols only from the original program file. EnumProcessModules() is // used to access symbols from other libraries. For objects loaded // with the RTLD_LOCAL flag, we create our own list later on. They are // excluded from EnumProcessModules() iteration. static void* rt_loader_all; static void* rt_loader_sym_all(const char* name) { void* sym = null; DWORD bytes = 0; rt_fatal_win32err(EnumProcessModules(GetCurrentProcess(), null, 0, &bytes)); rt_assert(bytes % sizeof(HMODULE) == 0); rt_assert(bytes / sizeof(HMODULE) < 1024); // OK to allocate 8KB on stack HMODULE* modules = null; rt_fatal_if_error(rt_heap.allocate(null, (void**)&modules, bytes, false)); rt_fatal_win32err(EnumProcessModules(GetCurrentProcess(), modules, bytes, &bytes)); const int32_t n = bytes / (int32_t)sizeof(HMODULE); for (int32_t i = 0; i < n && sym != null; i++) { sym = rt_loader.sym(modules[i], name); } if (sym == null) { sym = rt_loader.sym(GetModuleHandleA(null), name); } rt_heap.deallocate(null, modules); return sym; } static void* rt_loader_open(const char* filename, int32_t rt_unused(mode)) { return filename == null ? &rt_loader_all : (void*)LoadLibraryA(filename); } static void* rt_loader_sym(void* handle, const char* name) { return handle == &rt_loader_all ? (void*)rt_loader_sym_all(name) : (void*)GetProcAddress((HMODULE)handle, name); } static void rt_loader_close(void* handle) { if (handle != &rt_loader_all) { rt_fatal_win32err(FreeLibrary(handle)); } } #ifdef RT_TESTS // manually test exported function once and comment out because of // creating .lib out of each .exe is annoying #undef RT_LOADER_TEST_EXPORTED_FUNCTION #ifdef RT_LOADER_TEST_EXPORTED_FUNCTION static int32_t rt_loader_test_calls_count; rt_export void rt_loader_test_exported_function(void); void rt_loader_test_exported_function(void) { rt_loader_test_calls_count++; } #endif static void rt_loader_test(void) { void* global = rt_loader.open(null, rt_loader.local); rt_loader.close(global); // NtQueryTimerResolution - http://undocumented.ntinternals.net/ typedef long (__stdcall *query_timer_resolution_t)( long* minimum_resolution, long* maximum_resolution, long* current_resolution); void* nt_dll = rt_loader.open("ntdll", rt_loader.local); query_timer_resolution_t query_timer_resolution = (query_timer_resolution_t)rt_loader.sym(nt_dll, "NtQueryTimerResolution"); // in 100ns = 0.1us units long min_resolution = 0; long max_resolution = 0; // lowest possible delay between timer events long cur_resolution = 0; rt_fatal_if(query_timer_resolution( &min_resolution, &max_resolution, &cur_resolution) != 0); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f millisecond", // min_resolution / 10.0 / 1000.0, // max_resolution / 10.0 / 1000.0, // cur_resolution / 10.0 / 1000.0); // // Interesting observation cur_resolution sometimes 15.625ms or 1.0ms // } rt_loader.close(nt_dll); #ifdef RT_LOADER_TEST_EXPORTED_FUNCTION rt_loader_test_calls_count = 0; rt_loader_test_exported_function(); // to make sure it is linked in rt_swear(rt_loader_test_calls_count == 1); typedef void (*foo_t)(void); foo_t foo = (foo_t)rt_loader.sym(global, "rt_loader_test_exported_function"); foo(); rt_swear(rt_loader_test_calls_count == 2); #endif if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_loader_test(void) {} #endif enum { rt_loader_local = 0, // RTLD_LOCAL All symbols are not made available for relocation processing by other modules. rt_loader_lazy = 1, // RTLD_LAZY Relocations are performed at an implementation-dependent time. rt_loader_now = 2, // RTLD_NOW Relocations are performed when the object is loaded. rt_loader_global = 0x00100, // RTLD_GLOBAL All symbols are available for relocation processing of other modules. }; rt_loader_if rt_loader = { .local = rt_loader_local, .lazy = rt_loader_lazy, .now = rt_loader_now, .global = rt_loader_global, .open = rt_loader_open, .sym = rt_loader_sym, .close = rt_loader_close, .test = rt_loader_test }; // _________________________________ rt_mem.c _________________________________ static errno_t rt_mem_map_view_of_file(HANDLE file, void* *data, int64_t *bytes, bool rw) { errno_t r = 0; void* address = null; HANDLE mapping = CreateFileMapping(file, null, rw ? PAGE_READWRITE : PAGE_READONLY, (uint32_t)(*bytes >> 32), (uint32_t)*bytes, null); if (mapping == null) { r = rt_core.err(); } else { DWORD access = rw ? FILE_MAP_ALL_ACCESS : FILE_MAP_READ; address = MapViewOfFile(mapping, access, 0, 0, (SIZE_T)*bytes); if (address == null) { r = rt_core.err(); } rt_win32_close_handle(mapping); } if (r == 0) { *data = address; } else { *data = null; *bytes = 0; } return r; } // see: https://learn.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c-- static errno_t rt_mem_set_token_privilege(void* token, const char* name, bool e) { TOKEN_PRIVILEGES tp = { .PrivilegeCount = 1 }; tp.Privileges[0].Attributes = e ? SE_PRIVILEGE_ENABLED : 0; rt_fatal_win32err(LookupPrivilegeValueA(null, name, &tp.Privileges[0].Luid)); return rt_b2e(AdjustTokenPrivileges(token, false, &tp, sizeof(TOKEN_PRIVILEGES), null, null)); } static errno_t rt_mem_adjust_process_privilege_manage_volume_name(void) { // see: https://devblogs.microsoft.com/oldnewthing/20160603-00/?p=93565 const uint32_t access = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; const HANDLE process = GetCurrentProcess(); HANDLE token = null; errno_t r = rt_b2e(OpenProcessToken(process, access, &token)); if (r == 0) { const char* se_manage_volume_name = "SeManageVolumePrivilege"; r = rt_mem_set_token_privilege(token, se_manage_volume_name, true); rt_win32_close_handle(token); } return r; } static errno_t rt_mem_map_file(const char* filename, void* *data, int64_t *bytes, bool rw) { if (rw) { // for SetFileValidData() call: (void)rt_mem_adjust_process_privilege_manage_volume_name(); } errno_t r = 0; const DWORD access = GENERIC_READ | (rw ? GENERIC_WRITE : 0); const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; const DWORD disposition = rw ? OPEN_ALWAYS : OPEN_EXISTING; const DWORD flags = FILE_ATTRIBUTE_NORMAL; HANDLE file = CreateFileA(filename, access, share, null, disposition, flags, null); if (file == INVALID_HANDLE_VALUE) { r = rt_core.err(); } else { LARGE_INTEGER eof = { .QuadPart = 0 }; rt_fatal_win32err(GetFileSizeEx(file, &eof)); if (rw && *bytes > eof.QuadPart) { // increase file size const LARGE_INTEGER size = { .QuadPart = *bytes }; r = r != 0 ? r : (rt_b2e(SetFilePointerEx(file, size, null, FILE_BEGIN))); r = r != 0 ? r : (rt_b2e(SetEndOfFile(file))); // the following not guaranteed to work but helps with sparse files r = r != 0 ? r : (rt_b2e(SetFileValidData(file, *bytes))); // SetFileValidData() only works for Admin (verified) or System accounts if (r == ERROR_PRIVILEGE_NOT_HELD) { r = 0; } // ignore // SetFileValidData() is also semi-security hole because it allows to read // previously not zeroed disk content of other files const LARGE_INTEGER zero = { .QuadPart = 0 }; // rewind stream: r = r != 0 ? r : (rt_b2e(SetFilePointerEx(file, zero, null, FILE_BEGIN))); } else { *bytes = eof.QuadPart; } r = r != 0 ? r : rt_mem_map_view_of_file(file, data, bytes, rw); rt_win32_close_handle(file); } return r; } static errno_t rt_mem_map_ro(const char* filename, void* *data, int64_t *bytes) { return rt_mem_map_file(filename, data, bytes, false); } static errno_t rt_mem_map_rw(const char* filename, void* *data, int64_t *bytes) { return rt_mem_map_file(filename, data, bytes, true); } static void rt_mem_unmap(void* data, int64_t bytes) { rt_assert(data != null && bytes > 0); (void)bytes; /* unused only need for posix version */ if (data != null && bytes > 0) { rt_fatal_win32err(UnmapViewOfFile(data)); } } static errno_t rt_mem_map_resource(const char* label, void* *data, int64_t *bytes) { HRSRC res = FindResourceA(null, label, (const char*)RT_RCDATA); // "LockResource does not actually lock memory; it is just used to // obtain a pointer to the memory containing the resource data. // The name of the function comes from versions prior to Windows XP, // when it was used to lock a global memory block allocated by LoadResource." if (res != null) { *bytes = SizeofResource(null, res); } HGLOBAL g = res != null ? LoadResource(null, res) : null; *data = g != null ? LockResource(g) : null; return *data != null ? 0 : rt_core.err(); } static int32_t rt_mem_page_size(void) { static SYSTEM_INFO system_info; if (system_info.dwPageSize == 0) { GetSystemInfo(&system_info); } return (int32_t)system_info.dwPageSize; } static int rt_mem_large_page_size(void) { static SIZE_T large_page_minimum = 0; if (large_page_minimum == 0) { large_page_minimum = GetLargePageMinimum(); } return (int32_t)large_page_minimum; } static void* rt_mem_allocate(int64_t bytes_multiple_of_page_size) { rt_assert(bytes_multiple_of_page_size > 0); SIZE_T bytes = (SIZE_T)bytes_multiple_of_page_size; SIZE_T page_size = (SIZE_T)rt_mem_page_size(); rt_assert(bytes % page_size == 0); errno_t r = 0; void* a = null; if (bytes_multiple_of_page_size < 0 || bytes % page_size != 0) { SetLastError(ERROR_INVALID_PARAMETER); r = EINVAL; } else { const DWORD type = MEM_COMMIT | MEM_RESERVE; const DWORD physical = type | MEM_PHYSICAL; a = VirtualAlloc(null, bytes, physical, PAGE_READWRITE); if (a == null) { a = VirtualAlloc(null, bytes, type, PAGE_READWRITE); } if (a == null) { r = rt_core.err(); if (r != 0) { rt_println("VirtualAlloc(%lld) failed %s", bytes, rt_strerr(r)); } } else { r = VirtualLock(a, bytes) ? 0 : rt_core.err(); if (r == ERROR_WORKING_SET_QUOTA) { // The default size is 345 pages (for example, // this is 1,413,120 bytes on systems with a 4K page size). SIZE_T min_mem = 0, max_mem = 0; r = rt_b2e(GetProcessWorkingSetSize(GetCurrentProcess(), &min_mem, &max_mem)); if (r != 0) { rt_println("GetProcessWorkingSetSize() failed %s", rt_strerr(r)); } else { max_mem = max_mem + bytes * 2LL; max_mem = (max_mem + page_size - 1) / page_size * page_size + page_size * 16; if (min_mem < max_mem) { min_mem = max_mem; } r = rt_b2e(SetProcessWorkingSetSize(GetCurrentProcess(), min_mem, max_mem)); if (r != 0) { rt_println("SetProcessWorkingSetSize(%lld, %lld) failed %s", (uint64_t)min_mem, (uint64_t)max_mem, rt_strerr(r)); } else { r = rt_b2e(VirtualLock(a, bytes)); } } } if (r != 0) { rt_println("VirtualLock(%lld) failed %s", bytes, rt_strerr(r)); } } } if (r != 0) { rt_println("mem_alloc_pages(%lld) failed %s", bytes, rt_strerr(r)); rt_assert(a == null); } return a; } static void rt_mem_deallocate(void* a, int64_t bytes_multiple_of_page_size) { rt_assert(bytes_multiple_of_page_size > 0); SIZE_T bytes = (SIZE_T)bytes_multiple_of_page_size; errno_t r = 0; SIZE_T page_size = (SIZE_T)rt_mem_page_size(); if (bytes_multiple_of_page_size < 0 || bytes % page_size != 0) { r = EINVAL; rt_println("failed %s", rt_strerr(r)); } else { if (a != null) { // in case it was successfully locked r = rt_b2e(VirtualUnlock(a, bytes)); if (r != 0) { rt_println("VirtualUnlock() failed %s", rt_strerr(r)); } // If the "dwFreeType" parameter is MEM_RELEASE, "dwSize" parameter // must be the base address returned by the VirtualAlloc function when // the region of pages is reserved. r = rt_b2e(VirtualFree(a, 0, MEM_RELEASE)); if (r != 0) { rt_println("VirtuaFree() failed %s", rt_strerr(r)); } } } } static void rt_mem_test(void) { #ifdef RT_TESTS rt_swear(rt_args.c > 0); void* data = null; int64_t bytes = 0; rt_swear(rt_mem.map_ro(rt_args.v[0], &data, &bytes) == 0); rt_swear(data != null && bytes != 0); rt_mem.unmap(data, bytes); // TODO: page_size large_page_size allocate deallocate // TODO: test heap functions if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_mem_if rt_mem = { .map_ro = rt_mem_map_ro, .map_rw = rt_mem_map_rw, .unmap = rt_mem_unmap, .map_resource = rt_mem_map_resource, .page_size = rt_mem_page_size, .large_page_size = rt_mem_large_page_size, .allocate = rt_mem_allocate, .deallocate = rt_mem_deallocate, .test = rt_mem_test }; // _________________________________ rt_nls.c _________________________________ // Simplistic Win32 implementation of national language support. // Windows NLS family of functions is very complicated and has // difficult history of LANGID vs LCID etc... See: // ResolveLocaleName() // GetThreadLocale() // SetThreadLocale() // GetUserDefaultLocaleName() // WM_SETTINGCHANGE lParam="intl" // and many others... enum { rt_nls_str_count_max = 1024, rt_nls_str_mem_max = 64 * rt_nls_str_count_max }; static char rt_nls_strings_memory[rt_nls_str_mem_max]; // increase if overflows static char* rt_nls_strings_free = rt_nls_strings_memory; static int32_t rt_nls_strings_count; static const char* rt_nls_ls[rt_nls_str_count_max]; // localized strings static const char* rt_nls_ns[rt_nls_str_count_max]; // neutral language strings static uint16_t* rt_nls_load_string(int32_t strid, LANGID lang_id) { rt_assert(0 <= strid && strid < rt_countof(rt_nls_ns)); uint16_t* r = null; int32_t block = strid / 16 + 1; int32_t index = strid % 16; HRSRC res = FindResourceExW(((HMODULE)null), RT_STRING, MAKEINTRESOURCEW(block), lang_id); // rt_println("FindResourceExA(block=%d lang_id=%04X)=%p", block, lang_id, res); uint8_t* memory = res == null ? null : (uint8_t*)LoadResource(null, res); uint16_t* ws = memory == null ? null : (uint16_t*)LockResource(memory); // rt_println("LockResource(block=%d lang_id=%04X)=%p", block, lang_id, ws); if (ws != null) { for (int32_t i = 0; i < 16 && r == null; i++) { if (ws[0] != 0) { int32_t count = (int32_t)ws[0]; // String size in characters. ws++; rt_assert(ws[count - 1] == 0, "use rc.exe /n command line option"); if (i == index) { // the string has been found // rt_println("%04X found %s", lang_id, utf16to8(ws)); r = ws; } ws += count; } else { ws++; } } } return r; } static const char* rt_nls_save_string(uint16_t* utf16) { const int32_t bytes = rt_str.utf8_bytes(utf16, -1); rt_swear(bytes > 1); char* s = rt_nls_strings_free; uintptr_t left = (uintptr_t)rt_countof(rt_nls_strings_memory) - (uintptr_t)(rt_nls_strings_free - rt_nls_strings_memory); rt_fatal_if(left < (uintptr_t)bytes, "string_memory[] overflow"); rt_str.utf16to8(s, (int32_t)left, utf16, -1); rt_assert((int32_t)strlen(s) == bytes - 1, "utf16to8() does not truncate"); rt_nls_strings_free += bytes; return s; } static const char* rt_nls_localized_string(int32_t strid) { rt_swear(0 < strid && strid < rt_countof(rt_nls_ns)); const char* s = null; if (0 < strid && strid < rt_countof(rt_nls_ns)) { if (rt_nls_ls[strid] != null) { s = rt_nls_ls[strid]; } else { LCID lc_id = GetThreadLocale(); LANGID lang_id = LANGIDFROMLCID(lc_id); uint16_t* utf16 = rt_nls_load_string(strid, lang_id); if (utf16 == null) { // try default dialect: LANGID primary = PRIMARYLANGID(lang_id); lang_id = MAKELANGID(primary, SUBLANG_NEUTRAL); utf16 = rt_nls_load_string(strid, lang_id); } if (utf16 != null && utf16[0] != 0x0000) { s = rt_nls_save_string(utf16); rt_nls_ls[strid] = s; } } } return s; } static int32_t rt_nls_strid(const char* s) { int32_t strid = -1; for (int32_t i = 1; i < rt_nls_strings_count && strid == -1; i++) { if (rt_nls_ns[i] != null && strcmp(s, rt_nls_ns[i]) == 0) { strid = i; rt_nls_localized_string(strid); // to save it, ignore result } } return strid; } static const char* rt_nls_string(int32_t strid, const char* defau1t) { const char* r = rt_nls_localized_string(strid); return r == null ? defau1t : r; } static const char* rt_nls_str(const char* s) { int32_t id = rt_nls_strid(s); return id < 0 ? s : rt_nls_string(id, s); } static const char* rt_nls_locale(void) { uint16_t utf16[LOCALE_NAME_MAX_LENGTH + 1]; LCID lc_id = GetThreadLocale(); int32_t n = LCIDToLocaleName(lc_id, utf16, rt_countof(utf16), LOCALE_ALLOW_NEUTRAL_NAMES); static char ln[LOCALE_NAME_MAX_LENGTH * 4 + 1]; ln[0] = 0; if (n == 0) { errno_t r = rt_core.err(); rt_println("LCIDToLocaleName(0x%04X) failed %s", lc_id, rt_str.error(r)); } else { rt_str.utf16to8(ln, rt_countof(ln), utf16, -1); } return ln; } static errno_t rt_nls_set_locale(const char* locale) { errno_t r = 0; uint16_t utf16[LOCALE_NAME_MAX_LENGTH + 1]; rt_str.utf8to16(utf16, rt_countof(utf16), locale, -1); uint16_t rln[LOCALE_NAME_MAX_LENGTH + 1]; // resolved locale name int32_t n = (int32_t)ResolveLocaleName(utf16, rln, (DWORD)rt_countof(rln)); if (n == 0) { r = rt_core.err(); rt_println("ResolveLocaleName(\"%s\") failed %s", locale, rt_str.error(r)); } else { LCID lc_id = LocaleNameToLCID(rln, LOCALE_ALLOW_NEUTRAL_NAMES); if (lc_id == 0) { r = rt_core.err(); rt_println("LocaleNameToLCID(\"%s\") failed %s", locale, rt_str.error(r)); } else { rt_fatal_win32err(SetThreadLocale(lc_id)); memset((void*)rt_nls_ls, 0, sizeof(rt_nls_ls)); // start all over } } return r; } static void rt_nls_init(void) { static_assert(rt_countof(rt_nls_ns) % 16 == 0, "rt_countof(ns) must be multiple of 16"); LANGID lang_id = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL); for (int32_t strid = 0; strid < rt_countof(rt_nls_ns); strid += 16) { int32_t block = strid / 16 + 1; HRSRC res = FindResourceExW(((HMODULE)null), RT_STRING, MAKEINTRESOURCEW(block), lang_id); uint8_t* memory = res == null ? null : (uint8_t*)LoadResource(null, res); uint16_t* ws = memory == null ? null : (uint16_t*)LockResource(memory); if (ws == null) { break; } for (int32_t i = 0; i < 16; i++) { int32_t ix = strid + i; uint16_t count = ws[0]; if (count > 0) { ws++; rt_fatal_if(ws[count - 1] != 0, "use rc.exe /n"); rt_nls_ns[ix] = rt_nls_save_string(ws); rt_nls_strings_count = ix + 1; // rt_println("ns[%d] := %d \"%s\"", ix, strlen(rt_nls_ns[ix]), rt_nls_ns[ix]); ws += count; } else { ws++; } } } } rt_nls_if rt_nls = { .init = rt_nls_init, .strid = rt_nls_strid, .str = rt_nls_str, .string = rt_nls_string, .locale = rt_nls_locale, .set_locale = rt_nls_set_locale, }; // _________________________________ rt_num.c _________________________________ #include //#include // _tzcnt_u32 static inline rt_num128_t rt_num_add128_inline(const rt_num128_t a, const rt_num128_t b) { rt_num128_t r = a; r.hi += b.hi; r.lo += b.lo; if (r.lo < b.lo) { r.hi++; } // carry return r; } static inline rt_num128_t rt_num_sub128_inline(const rt_num128_t a, const rt_num128_t b) { rt_num128_t r = a; r.hi -= b.hi; if (r.lo < b.lo) { r.hi--; } // borrow r.lo -= b.lo; return r; } static rt_num128_t rt_num_add128(const rt_num128_t a, const rt_num128_t b) { return rt_num_add128_inline(a, b); } static rt_num128_t rt_num_sub128(const rt_num128_t a, const rt_num128_t b) { return rt_num_sub128_inline(a, b); } static rt_num128_t rt_num_mul64x64(uint64_t a, uint64_t b) { uint64_t a_lo = (uint32_t)a; uint64_t a_hi = a >> 32; uint64_t b_lo = (uint32_t)b; uint64_t b_hi = b >> 32; uint64_t low = a_lo * b_lo; uint64_t cross1 = a_hi * b_lo; uint64_t cross2 = a_lo * b_hi; uint64_t high = a_hi * b_hi; // this cannot overflow as (2^32-1)^2 + 2^32-1 < 2^64-1 cross1 += low >> 32; // this one can overflow cross1 += cross2; // propagate the carry if any high += ((uint64_t)(cross1 < cross2 != 0)) << 32; high = high + (cross1 >> 32); low = ((cross1 & 0xFFFFFFFF) << 32) + (low & 0xFFFFFFFF); return (rt_num128_t){.lo = low, .hi = high }; } static inline void rt_num_shift128_left_inline(rt_num128_t* n) { const uint64_t top = (1ULL << 63); n->hi = (n->hi << 1) | ((n->lo & top) ? 1 : 0); n->lo = (n->lo << 1); } static inline void rt_num_shift128_right_inline(rt_num128_t* n) { const uint64_t top = (1ULL << 63); n->lo = (n->lo >> 1) | ((n->hi & 0x1) ? top : 0); n->hi = (n->hi >> 1); } static inline bool rt_num_less128_inline(const rt_num128_t a, const rt_num128_t b) { return a.hi < b.hi || (a.hi == b.hi && a.lo < b.lo); } static inline bool rt_num_uint128_high_bit(const rt_num128_t a) { return (int64_t)a.hi < 0; } static uint64_t rt_num_muldiv128(uint64_t a, uint64_t b, uint64_t divisor) { rt_swear(divisor > 0, "divisor: %lld", divisor); rt_num128_t r = rt_num.mul64x64(a, b); // reminder: a * b uint64_t q = 0; // quotient if (r.hi >= divisor) { q = UINT64_MAX; // overflow } else { int32_t shift = 0; rt_num128_t d = { .hi = 0, .lo = divisor }; while (!rt_num_uint128_high_bit(d) && rt_num_less128_inline(d, r)) { rt_num_shift128_left_inline(&d); shift++; } rt_assert(shift <= 64); while (shift >= 0 && (d.hi != 0 || d.lo != 0)) { if (!rt_num_less128_inline(r, d)) { r = rt_num_sub128_inline(r, d); rt_assert(shift < 64); q |= (1ULL << shift); } rt_num_shift128_right_inline(&d); shift--; } } return q; } static uint32_t rt_num_gcd32(uint32_t u, uint32_t v) { #pragma push_macro("rt_trailing_zeros") #ifdef _M_ARM64 #define rt_trailing_zeros(x) (_CountTrailingZeros(x)) #else #define rt_trailing_zeros(x) ((int32_t)_tzcnt_u32(x)) #endif if (u == 0) { return v; } else if (v == 0) { return u; } uint32_t i = rt_trailing_zeros(u); u >>= i; uint32_t j = rt_trailing_zeros(v); v >>= j; uint32_t k = rt_min(i, j); for (;;) { rt_assert(u % 2 == 1, "u = %d should be odd", u); rt_assert(v % 2 == 1, "v = %d should be odd", v); if (u > v) { uint32_t swap = u; u = v; v = swap; } v -= u; if (v == 0) { return u << k; } v >>= rt_trailing_zeros(v); } #pragma pop_macro("rt_trailing_zeros") } static uint32_t rt_num_random32(uint32_t* state) { // https://gist.github.com/tommyettinger/46a874533244883189143505d203312c static rt_thread_local bool started; // first seed must be odd if (!started) { started = true; *state |= 1; } uint32_t z = (*state += 0x6D2B79F5UL); z = (z ^ (z >> 15)) * (z | 1UL); z ^= z + (z ^ (z >> 7)) * (z | 61UL); return z ^ (z >> 14); } static uint64_t rt_num_random64(uint64_t *state) { // https://gist.github.com/tommyettinger/e6d3e8816da79b45bfe582384c2fe14a static rt_thread_local bool started; // first seed must be odd if (!started) { started = true; *state |= 1; } const uint64_t s = *state; const uint64_t z = (s ^ s >> 25) * (*state += 0x6A5D39EAE12657AAULL); return z ^ (z >> 22); } // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function static uint32_t rt_num_hash32(const char *data, int64_t len) { uint32_t hash = 0x811c9dc5; // FNV_offset_basis for 32-bit uint32_t prime = 0x01000193; // FNV_prime for 32-bit if (len > 0) { for (int64_t i = 1; i < len; i++) { hash ^= (uint32_t)data[i]; hash *= prime; } } else { for (int64_t i = 0; data[i] != 0; i++) { hash ^= (uint32_t)data[i]; hash *= prime; } } return hash; } static uint64_t rt_num_hash64(const char *data, int64_t len) { uint64_t hash = 0xcbf29ce484222325; // FNV_offset_basis for 64-bit uint64_t prime = 0x100000001b3; // FNV_prime for 64-bit if (len > 0) { for (int64_t i = 0; i < len; i++) { hash ^= (uint64_t)data[i]; hash *= prime; } } else { for (int64_t i = 0; data[i] != 0; i++) { hash ^= (uint64_t)data[i]; hash *= prime; } } return hash; } static uint32_t ctz_2(uint32_t x) { if (x == 0) return 32; unsigned n = 0; while ((x & 1) == 0) { x >>= 1; n++; } return n; } static void rt_num_test(void) { #ifdef RT_TESTS { rt_swear(rt_num.gcd32(1000000000, 24000000) == 8000000); // https://asecuritysite.com/encryption/nprimes?y=64 // https://www.rapidtables.com/convert/number/decimal-to-hex.html uint64_t p = 15843490434539008357u; // prime uint64_t q = 16304766625841520833u; // prime // pq: 258324414073910997987910483408576601381 // 0xC25778F20853A9A1EC0C27C467C45D25 rt_num128_t pq = {.hi = 0xC25778F20853A9A1uLL, .lo = 0xEC0C27C467C45D25uLL }; rt_num128_t p_q = rt_num.mul64x64(p, q); rt_swear(p_q.hi == pq.hi && pq.lo == pq.lo); uint64_t p1 = rt_num.muldiv128(p, q, q); uint64_t q1 = rt_num.muldiv128(p, q, p); rt_swear(p1 == p); rt_swear(q1 == q); } #ifdef DEBUG enum { n = 100 }; #else enum { n = 10000 }; #endif uint64_t seed64 = 1; for (int32_t i = 0; i < n; i++) { uint64_t p = rt_num.random64(&seed64); uint64_t q = rt_num.random64(&seed64); uint64_t p1 = rt_num.muldiv128(p, q, q); uint64_t q1 = rt_num.muldiv128(p, q, p); rt_swear(p == p1, "0%16llx (0%16llu) != 0%16llx (0%16llu)", p, p1); rt_swear(q == q1, "0%16llx (0%16llu) != 0%16llx (0%16llu)", p, p1); } uint32_t seed32 = 1; for (int32_t i = 0; i < n; i++) { uint64_t p = rt_num.random32(&seed32); uint64_t q = rt_num.random32(&seed32); uint64_t r = rt_num.muldiv128(p, q, 1); rt_swear(r == p * q); // division by the maximum uint64_t value: r = rt_num.muldiv128(p, q, UINT64_MAX); rt_swear(r == 0); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_num_if rt_num = { .add128 = rt_num_add128, .sub128 = rt_num_sub128, .mul64x64 = rt_num_mul64x64, .muldiv128 = rt_num_muldiv128, .gcd32 = rt_num_gcd32, .random32 = rt_num_random32, .random64 = rt_num_random64, .hash32 = rt_num_hash32, .hash64 = rt_num_hash64, .test = rt_num_test }; // ______________________________ rt_processes.c ______________________________ typedef struct rt_processes_pidof_lambda_s rt_processes_pidof_lambda_t; typedef struct rt_processes_pidof_lambda_s { bool (*each)(rt_processes_pidof_lambda_t* p, uint64_t pid); // returns true to continue uint64_t* pids; size_t size; // pids[size] size_t count; // number of valid pids in the pids fp64_t timeout; errno_t error; } rt_processes_pidof_lambda_t; static int32_t rt_processes_for_each_pidof(const char* pname, rt_processes_pidof_lambda_t* la) { char stack[1024]; // avoid alloca() int32_t n = rt_str.len(pname); rt_fatal_if(n + 5 >= rt_countof(stack), "name is too long: %s", pname); const char* name = pname; // append ".exe" if not present: if (!rt_str.iends(pname, ".exe")) { int32_t k = (int32_t)strlen(pname) + 5; char* exe = stack; rt_str.format(exe, k, "%s.exe", pname); name = exe; } const char* base = strrchr(name, '\\'); if (base != null) { base++; // advance past "\\" } else { base = name; } uint16_t wn[1024]; rt_fatal_if(strlen(base) >= rt_countof(wn), "name too long: %s", base); rt_str.utf8to16(wn, rt_countof(wn), base, -1); size_t count = 0; uint64_t pid = 0; uint8_t* data = null; ULONG bytes = 0; errno_t r = NtQuerySystemInformation(SystemProcessInformation, data, 0, &bytes); #pragma push_macro("STATUS_INFO_LENGTH_MISMATCH") #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004 while (r == (errno_t)STATUS_INFO_LENGTH_MISMATCH) { // bytes == 420768 on Windows 11 which may be a bit // too much for stack alloca() // add little extra if new process is spawned in between calls. bytes += sizeof(SYSTEM_PROCESS_INFORMATION) * 32; r = rt_heap.reallocate(null, (void**)&data, bytes, false); if (r == 0) { r = NtQuerySystemInformation(SystemProcessInformation, data, bytes, &bytes); } else { rt_assert(r == (errno_t)ERROR_NOT_ENOUGH_MEMORY); } } #pragma pop_macro("STATUS_INFO_LENGTH_MISMATCH") if (r == 0 && data != null) { SYSTEM_PROCESS_INFORMATION* proc = (SYSTEM_PROCESS_INFORMATION*)data; while (proc != null) { uint16_t* img = proc->ImageName.Buffer; // last name only, not a pathname! bool match = img != null && wcsicmp(img, wn) == 0; if (match) { pid = (uint64_t)proc->UniqueProcessId; // HANDLE .UniqueProcessId if (base != name) { char path[rt_files_max_path]; match = rt_processes.nameof(pid, path, rt_countof(path)) == 0 && rt_str.iends(path, name); // rt_println("\"%s\" -> \"%s\" match: %d", name, path, match); } } if (match) { if (la != null && count < la->size && la->pids != null) { la->pids[count] = pid; } count++; if (la != null && la->each != null && !la->each(la, pid)) { break; } } proc = proc->NextEntryOffset != 0 ? (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*)proc + proc->NextEntryOffset) : null; } } if (data != null) { rt_heap.deallocate(null, data); } rt_assert(count <= (uint64_t)INT32_MAX); return (int32_t)count; } static errno_t rt_processes_nameof(uint64_t pid, char* name, int32_t count) { rt_assert(name != null && count > 0); errno_t r = 0; name[0] = 0; HANDLE p = OpenProcess(PROCESS_ALL_ACCESS, false, (DWORD)pid); if (p != null) { r = rt_b2e(GetModuleFileNameExA(p, null, name, count)); name[count - 1] = 0; // ensure zero termination rt_win32_close_handle(p); } else { r = ERROR_NOT_FOUND; } return r; } static bool rt_processes_present(uint64_t pid) { void* h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, (DWORD)pid); bool b = h != null; if (h != null) { rt_win32_close_handle(h); } return b; } static bool rt_processes_first_pid(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { lambda->pids[0] = pid; return false; } static uint64_t rt_processes_pid(const char* pname) { uint64_t first[1] = {0}; rt_processes_pidof_lambda_t lambda = { .each = rt_processes_first_pid, .pids = first, .size = 1, .count = 0, .timeout = 0, .error = 0 }; rt_processes_for_each_pidof(pname, &lambda); return first[0]; } static bool rt_processes_store_pid(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { if (lambda->pids != null && lambda->count < lambda->size) { lambda->pids[lambda->count++] = pid; } return true; // always - need to count all } static errno_t rt_processes_pids(const char* pname, uint64_t* pids/*[size]*/, int32_t size, int32_t *count) { *count = 0; rt_processes_pidof_lambda_t lambda = { .each = rt_processes_store_pid, .pids = pids, .size = (size_t)size, .count = 0, .timeout = 0, .error = 0 }; *count = rt_processes_for_each_pidof(pname, &lambda); return (int32_t)lambda.count == *count ? 0 : ERROR_MORE_DATA; } static errno_t rt_processes_kill(uint64_t pid, fp64_t timeout) { DWORD milliseconds = timeout < 0 ? INFINITE : (DWORD)(timeout * 1000); enum { access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | SYNCHRONIZE }; rt_assert((DWORD)pid == pid); // Windows... HANDLE vs DWORD in different APIs errno_t r = ERROR_NOT_FOUND; HANDLE h = OpenProcess(access, 0, (DWORD)pid); if (h != null) { char path[rt_files_max_path]; path[0] = 0; r = rt_b2e(TerminateProcess(h, ERROR_PROCESS_ABORTED)); if (r == 0) { DWORD ix = WaitForSingleObject(h, milliseconds); r = rt_wait_ix2e(ix); } else { DWORD bytes = rt_countof(path); errno_t rq = rt_b2e(QueryFullProcessImageNameA(h, 0, path, &bytes)); if (rq != 0) { rt_println("QueryFullProcessImageNameA(pid=%d, h=%p) " "failed %s", pid, h, rt_strerr(rq)); } } rt_win32_close_handle(h); if (r == ERROR_ACCESS_DENIED) { // special case rt_thread.sleep_for(0.015); // need to wait a bit HANDLE retry = OpenProcess(access, 0, (DWORD)pid); // process may have died before we have chance to terminate it: if (retry == null) { rt_println("TerminateProcess(pid=%d, h=%p, im=%s) " "failed but zombie died after: %s", pid, h, path, rt_strerr(r)); r = 0; } else { rt_win32_close_handle(retry); } } if (r != 0) { rt_println("TerminateProcess(pid=%d, h=%p, im=%s) failed %s", pid, h, path, rt_strerr(r)); } } if (r != 0) { errno = r; } return r; } static bool rt_processes_kill_one(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { errno_t r = rt_processes_kill(pid, lambda->timeout); if (r != 0) { lambda->error = r; } return true; // keep going } static errno_t rt_processes_kill_all(const char* name, fp64_t timeout) { rt_processes_pidof_lambda_t lambda = { .each = rt_processes_kill_one, .pids = null, .size = 0, .count = 0, .timeout = timeout, .error = 0 }; int32_t c = rt_processes_for_each_pidof(name, &lambda); return c == 0 ? ERROR_NOT_FOUND : lambda.error; } static bool rt_processes_is_elevated(void) { // Is process running as Admin / System ? BOOL elevated = false; PSID administrators_group = null; // Allocate and initialize a SID of the administrators group. SID_IDENTIFIER_AUTHORITY administrators_group_authority = SECURITY_NT_AUTHORITY; errno_t r = rt_b2e(AllocateAndInitializeSid(&administrators_group_authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administrators_group)); if (r != 0) { rt_println("AllocateAndInitializeSid() failed %s", rt_strerr(r)); } PSID system_ops = null; SID_IDENTIFIER_AUTHORITY system_ops_authority = SECURITY_NT_AUTHORITY; r = rt_b2e(AllocateAndInitializeSid(&system_ops_authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_SYSTEM_OPS, 0, 0, 0, 0, 0, 0, &system_ops)); if (r != 0) { rt_println("AllocateAndInitializeSid() failed %s", rt_strerr(r)); } if (administrators_group != null) { r = rt_b2e(CheckTokenMembership(null, administrators_group, &elevated)); } if (system_ops != null && !elevated) { r = rt_b2e(CheckTokenMembership(null, administrators_group, &elevated)); } if (administrators_group != null) { FreeSid(administrators_group); } if (system_ops != null) { FreeSid(system_ops); } if (r != 0) { rt_println("failed %s", rt_strerr(r)); } return elevated; } static errno_t rt_processes_restart_elevated(void) { errno_t r = 0; if (!rt_processes.is_elevated()) { const char* path = rt_processes.name(); SHELLEXECUTEINFOA sei = { sizeof(sei) }; sei.lpVerb = "runas"; sei.lpFile = path; sei.hwnd = null; sei.nShow = SW_NORMAL; r = rt_b2e(ShellExecuteExA(&sei)); if (r == ERROR_CANCELLED) { rt_println("The user unable or refused to allow privileges elevation"); } else if (r == 0) { rt_core.exit(0); // second copy of the app is running now } } return r; } static void rt_processes_close_pipes(STARTUPINFOA* si, HANDLE *read_out, HANDLE *read_err, HANDLE *write_in) { if (si->hStdOutput != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdOutput); } if (si->hStdError != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdError); } if (si->hStdInput != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdInput); } if (*read_out != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*read_out); } if (*read_err != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*read_err); } if (*write_in != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*write_in); } } static errno_t rt_processes_child_read(rt_stream_if* out, HANDLE pipe) { char data[32 * 1024]; // Temporary buffer for reading DWORD available = 0; errno_t r = rt_b2e(PeekNamedPipe(pipe, null, sizeof(data), null, &available, null)); if (r != 0) { if (r != ERROR_BROKEN_PIPE) { // unexpected! // rt_println("PeekNamedPipe() failed %s", rt_strerr(r)); } // process has exited and closed the pipe rt_assert(r == ERROR_BROKEN_PIPE); } else if (available > 0) { DWORD bytes_read = 0; r = rt_b2e(ReadFile(pipe, data, sizeof(data), &bytes_read, null)); // rt_println("r: %d bytes_read: %d", r, bytes_read); if (out != null) { if (r == 0) { r = out->write(out, data, bytes_read, null); } } else { // no one interested - drop on the floor } } return r; } static errno_t rt_processes_child_write(rt_stream_if* in, HANDLE pipe) { errno_t r = 0; if (in != null) { uint8_t memory[32 * 1024]; // Temporary buffer for reading uint8_t* data = memory; int64_t bytes_read = 0; in->read(in, data, sizeof(data), &bytes_read); while (r == 0 && bytes_read > 0) { DWORD bytes_written = 0; r = rt_b2e(WriteFile(pipe, data, (DWORD)bytes_read, &bytes_written, null)); rt_println("r: %d bytes_written: %d", r, bytes_written); rt_assert((int32_t)bytes_written <= bytes_read); data += bytes_written; bytes_read -= bytes_written; } } return r; } static errno_t rt_processes_run(rt_processes_child_t* child) { const fp64_t deadline = rt_clock.seconds() + child->timeout; errno_t r = 0; STARTUPINFOA si = { .cb = sizeof(STARTUPINFOA), .hStdInput = INVALID_HANDLE_VALUE, .hStdOutput = INVALID_HANDLE_VALUE, .hStdError = INVALID_HANDLE_VALUE, .dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES, .wShowWindow = SW_HIDE }; SECURITY_ATTRIBUTES sa = { sizeof(sa), null, true }; // Inheritable handles PROCESS_INFORMATION pi = {0}; HANDLE read_out = INVALID_HANDLE_VALUE; HANDLE read_err = INVALID_HANDLE_VALUE; HANDLE write_in = INVALID_HANDLE_VALUE; errno_t ro = rt_b2e(CreatePipe(&read_out, &si.hStdOutput, &sa, 0)); errno_t re = rt_b2e(CreatePipe(&read_err, &si.hStdError, &sa, 0)); errno_t ri = rt_b2e(CreatePipe(&si.hStdInput, &write_in, &sa, 0)); if (ro != 0 || re != 0 || ri != 0) { rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); if (ro != 0) { rt_println("CreatePipe() failed %s", rt_strerr(ro)); r = ro; } if (re != 0) { rt_println("CreatePipe() failed %s", rt_strerr(re)); r = re; } if (ri != 0) { rt_println("CreatePipe() failed %s", rt_strerr(ri)); r = ri; } } if (r == 0) { r = rt_b2e(CreateProcessA(null, rt_str.drop_const(child->command), null, null, true, CREATE_NO_WINDOW, null, null, &si, &pi)); if (r != 0) { rt_println("CreateProcess() failed %s", rt_strerr(r)); rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); } } if (r == 0) { // not relevant: stdout can be written in other threads rt_win32_close_handle(pi.hThread); pi.hThread = null; // need to close si.hStdO* handles on caller side so, // when the process closes handles of the pipes, EOF happens // on caller side with io result ERROR_BROKEN_PIPE // indicating no more data can be read or written rt_win32_close_handle(si.hStdOutput); rt_win32_close_handle(si.hStdError); rt_win32_close_handle(si.hStdInput); si.hStdOutput = INVALID_HANDLE_VALUE; si.hStdError = INVALID_HANDLE_VALUE; si.hStdInput = INVALID_HANDLE_VALUE; bool done = false; while (!done && r == 0) { if (child->timeout > 0 && rt_clock.seconds() > deadline) { r = rt_b2e(TerminateProcess(pi.hProcess, ERROR_SEM_TIMEOUT)); if (r != 0) { rt_println("TerminateProcess() failed %s", rt_strerr(r)); } else { done = true; } } if (r == 0) { r = rt_processes_child_write(child->in, write_in); } if (r == 0) { r = rt_processes_child_read(child->out, read_out); } if (r == 0) { r = rt_processes_child_read(child->err, read_err); } if (!done) { DWORD ix = WaitForSingleObject(pi.hProcess, 0); // ix == 0 means process has exited (or terminated) // r == ERROR_BROKEN_PIPE process closed one of the handles done = ix == WAIT_OBJECT_0 || r == ERROR_BROKEN_PIPE; } // to avoid tight loop 100% cpu utilization: if (!done) { rt_thread.yield(); } } // broken pipe actually signifies EOF on the pipe if (r == ERROR_BROKEN_PIPE) { r = 0; } // not an error // if (r != 0) { rt_println("pipe loop failed %s", rt_strerr(r));} DWORD xc = 0; errno_t rx = rt_b2e(GetExitCodeProcess(pi.hProcess, &xc)); if (rx == 0) { child->exit_code = xc; } else { rt_println("GetExitCodeProcess() failed %s", rt_strerr(rx)); if (r != 0) { r = rx; } // report earliest error } rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); // expected never to fail rt_win32_close_handle(pi.hProcess); } return r; } typedef struct { rt_stream_if stream; rt_stream_if* output; errno_t error; } rt_processes_io_merge_out_and_err_if; static errno_t rt_processes_merge_write(rt_stream_if* stream, const void* data, int64_t bytes, int64_t* transferred) { if (transferred != null) { *transferred = 0; } rt_processes_io_merge_out_and_err_if* s = (rt_processes_io_merge_out_and_err_if*)stream; if (s->output != null && bytes > 0) { s->error = s->output->write(s->output, data, bytes, transferred); } return s->error; } static errno_t rt_processes_open(const char* command, int32_t *exit_code, rt_stream_if* output, fp64_t timeout) { rt_not_null(output); rt_processes_io_merge_out_and_err_if merge_out_and_err = { .stream ={ .write = rt_processes_merge_write }, .output = output, .error = 0 }; rt_processes_child_t child = { .command = command, .in = null, .out = &merge_out_and_err.stream, .err = &merge_out_and_err.stream, .exit_code = 0, .timeout = timeout }; errno_t r = rt_processes.run(&child); if (exit_code != null) { *exit_code = (int32_t)child.exit_code; } uint8_t zero = 0; // zero termination merge_out_and_err.stream.write(&merge_out_and_err.stream, &zero, 1, null); if (r == 0 && merge_out_and_err.error != 0) { r = merge_out_and_err.error; // zero termination is not guaranteed } return r; } static errno_t rt_processes_spawn(const char* command) { errno_t r = 0; STARTUPINFOA si = { .cb = sizeof(STARTUPINFOA), .dwFlags = STARTF_USESHOWWINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS, .wShowWindow = SW_HIDE, .hStdInput = INVALID_HANDLE_VALUE, .hStdOutput = INVALID_HANDLE_VALUE, .hStdError = INVALID_HANDLE_VALUE }; const DWORD flags = CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS; PROCESS_INFORMATION pi = { .hProcess = null, .hThread = null }; r = rt_b2e(CreateProcessA(null, rt_str.drop_const(command), null, null, /*bInheritHandles:*/false, flags, null, null, &si, &pi)); if (r == 0) { // Close handles immediately rt_win32_close_handle(pi.hProcess); rt_win32_close_handle(pi.hThread); } else { rt_println("CreateProcess() failed %s", rt_strerr(r)); } return r; } static const char* rt_processes_name(void) { static char mn[rt_files_max_path]; if (mn[0] == 0) { rt_fatal_win32err(GetModuleFileNameA(null, mn, rt_countof(mn))); } return mn; } #ifdef RT_TESTS #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void rt_processes_test(void) { #ifdef RT_TESTS // in alphabetical order const char* names[] = { "svchost", "RuntimeBroker", "conhost" }; for (int32_t j = 0; j < rt_countof(names); j++) { int32_t size = 0; int32_t count = 0; uint64_t* pids = null; errno_t r = rt_processes.pids(names[j], null, size, &count); while (r == ERROR_MORE_DATA && count > 0) { size = count * 2; // set of processes may change rapidly r = rt_heap.reallocate(null, (void**)&pids, (int64_t)sizeof(uint64_t) * (int64_t)size, false); if (r == 0) { r = rt_processes.pids(names[j], pids, size, &count); } } if (r == 0 && count > 0) { for (int32_t i = 0; i < count; i++) { char path[256] = {0}; #pragma warning(suppress: 6011) // dereferencing null r = rt_processes.nameof(pids[i], path, rt_countof(path)); if (r != ERROR_NOT_FOUND) { rt_assert(r == 0 && path[0] != 0); verbose("%6d %s %s", pids[i], path, rt_strerr(r)); } } } rt_heap.deallocate(null, pids); } // test popen() int32_t xc = 0; char data[32 * 1024]; rt_stream_memory_if output; rt_streams.write_only(&output, data, rt_countof(data)); const char* cmd = "cmd /c dir 2>nul >nul"; errno_t r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c dir \"folder that does not exist\\\""; r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c dir"; r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c timeout 1"; r = rt_processes.popen(cmd, &xc, &output.stream, 1.0E-9); verbose("r: %d xc: %d output:\n%s", r, xc, data); #endif } #pragma pop_macro("verbose") #else static void rt_processes_test(void) { } #endif rt_processes_if rt_processes = { .pid = rt_processes_pid, .pids = rt_processes_pids, .nameof = rt_processes_nameof, .present = rt_processes_present, .kill = rt_processes_kill, .kill_all = rt_processes_kill_all, .is_elevated = rt_processes_is_elevated, .restart_elevated = rt_processes_restart_elevated, .run = rt_processes_run, .popen = rt_processes_open, .spawn = rt_processes_spawn, .name = rt_processes_name, .test = rt_processes_test }; // _______________________________ rt_static.c ________________________________ static void* rt_static_symbol_reference[1024]; static int32_t rt_static_symbol_reference_count; void* rt_force_symbol_reference(void* symbol) { rt_assert(rt_static_symbol_reference_count <= rt_countof(rt_static_symbol_reference), "increase size of rt_static_symbol_reference[%d] to at least %d", rt_countof(rt_static_symbol_reference), rt_static_symbol_reference); if (rt_static_symbol_reference_count < rt_countof(rt_static_symbol_reference)) { rt_static_symbol_reference[rt_static_symbol_reference_count] = symbol; // rt_println("rt_static_symbol_reference[%d] = %p", rt_static_symbol_reference_count, // rt_static_symbol_reference[symbol_reference_count]); rt_static_symbol_reference_count++; } return symbol; } // test rt_static_init() { code } that will be executed in random // order but before main() #ifdef RT_TESTS static int32_t rt_static_init_function_called; static void rt_force_inline rt_static_init_function(void) { rt_static_init_function_called = 1; } rt_static_init(static_init_test) { rt_static_init_function(); } void rt_static_init_test(void) { rt_fatal_if(rt_static_init_function_called != 1, "static_init_function() expected to be called before main()"); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else void rt_static_init_test(void) {} #endif // _________________________________ rt_str.c _________________________________ static char* rt_str_drop_const(const char* s) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-qual" #endif return (char*)s; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } static int32_t rt_str_len(const char* s) { return (int32_t)strlen(s); } static int32_t rt_str_utf16len(const uint16_t* utf16) { return (int32_t)wcslen(utf16); } static int32_t rt_str_utf8bytes(const char* s, int32_t b) { rt_assert(b >= 1, "should not be called with bytes < 1"); const uint8_t* const u = (const uint8_t*)s; // based on: // https://stackoverflow.com/questions/66715611/check-for-valid-utf-8-encoding-in-c if (b >= 1 && (u[0] & 0x80u) == 0x00u) { return 1; } else if (b > 1) { uint32_t c = (u[0] << 8) | u[1]; // TODO: 0xC080 is a hack - consider removing if (c == 0xC080) { return 2; } // 0xC080 as not zero terminating '\0' if (0xC280 <= c && c <= 0xDFBF && (c & 0xE0C0) == 0xC080) { return 2; } if (b > 2) { c = (c << 8) | u[2]; // reject utf16 surrogates: if (0xEDA080 <= c && c <= 0xEDBFBF) { return 0; } if (0xE0A080 <= c && c <= 0xEFBFBF && (c & 0xF0C0C0) == 0xE08080) { return 3; } if (b > 3) { c = (c << 8) | u[3]; if (0xF0908080 <= c && c <= 0xF48FBFBF && (c & 0xF8C0C0C0) == 0xF0808080) { return 4; } } } } return 0; // invalid utf8 sequence } static int32_t rt_str_glyphs(const char* utf8, int32_t bytes) { rt_swear(bytes >= 0); bool ok = true; int32_t i = 0; int32_t k = 1; while (i < bytes && ok) { const int32_t b = rt_str.utf8bytes(utf8 + i, bytes - i); ok = 0 < b && i + b <= bytes; if (ok) { i += b; k++; } } return ok ? k - 1 : -1; } static void rt_str_lower(char* d, int32_t capacity, const char* s) { int32_t n = rt_str.len(s); rt_swear(capacity > n); for (int32_t i = 0; i < n; i++) { d[i] = (char)tolower(s[i]); } d[n] = 0; } static void rt_str_upper(char* d, int32_t capacity, const char* s) { int32_t n = rt_str.len(s); rt_swear(capacity > n); for (int32_t i = 0; i < n; i++) { d[i] = (char)toupper(s[i]); } d[n] = 0; } static bool rt_str_starts(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && memcmp(s1, s2, n2) == 0; } static bool rt_str_ends(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && memcmp(s1 + n1 - n2, s2, n2) == 0; } static bool rt_str_i_starts(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && strnicmp(s1, s2, n2) == 0; } static bool rt_str_i_ends(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && strnicmp(s1 + n1 - n2, s2, n2) == 0; } static int32_t rt_str_utf8_bytes(const uint16_t* utf16, int32_t chars) { // If `chars` argument is -1, the function utf8_bytes includes the zero // terminating character in the conversion and the returned byte count. // Function will fail (return 0) on incomplete surrogate pairs like // 0xD83D without following 0xDC1E https://compart.com/en/unicode/U+1F41E if (chars == 0) { return 0; } if (chars < 0 && utf16[0] == 0x0000) { return 1; } const int32_t required_bytes_count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16, chars, null, 0, null, null); if (required_bytes_count == 0) { errno_t r = rt_core.err(); rt_println("WideCharToMultiByte() failed %s", rt_strerr(r)); rt_core.set_err(r); } return required_bytes_count == 0 ? -1 : required_bytes_count; } static int32_t rt_str_utf16_chars(const char* utf8, int32_t bytes) { // If `bytes` argument is -1, the function utf16_chars() includes the zero // terminating character in the conversion and the returned character count. if (bytes == 0) { return 0; } if (bytes < 0 && utf8[0] == 0x00) { return 1; } const int32_t required_wide_chars_count = MultiByteToWideChar(CP_UTF8, 0, utf8, bytes, null, 0); if (required_wide_chars_count == 0) { errno_t r = rt_core.err(); rt_println("MultiByteToWideChar() failed %s", rt_strerr(r)); rt_core.set_err(r); } return required_wide_chars_count == 0 ? -1 : required_wide_chars_count; } static errno_t rt_str_utf16to8(char* utf8, int32_t capacity, const uint16_t* utf16, int32_t chars) { if (chars == 0) { return 0; } if (chars < 0 && utf16[0] == 0x0000) { rt_swear(capacity >= 1); utf8[0] = 0x00; return 0; } const int32_t required = rt_str.utf8_bytes(utf16, chars); errno_t r = required < 0 ? rt_core.err() : 0; if (r == 0) { rt_swear(required > 0 && capacity >= required); int32_t bytes = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16, chars, utf8, capacity, null, null); rt_swear(required == bytes); } return r; } static errno_t rt_str_utf8to16(uint16_t* utf16, int32_t capacity, const char* utf8, int32_t bytes) { const int32_t required = rt_str.utf16_chars(utf8, bytes); errno_t r = required < 0 ? rt_core.err() : 0; if (r == 0) { rt_swear(required >= 0 && capacity >= required); int32_t count = MultiByteToWideChar(CP_UTF8, 0, utf8, bytes, utf16, capacity); rt_swear(required == count); #if 0 // TODO: incorrect need output != input if (count > 0 && !IsNormalizedString(NormalizationC, utf16, count)) { rt_core.set_err(0); int32_t n = NormalizeString(NormalizationC, utf16, count, utf16, count); if (n <= 0) { r = rt_core.err(); rt_println("NormalizeString() failed %s", rt_strerr(r)); } } #endif } return r; } static bool rt_str_utf16_is_low_surrogate(uint16_t utf16char) { return 0xDC00 <= utf16char && utf16char <= 0xDFFF; } static bool rt_str_utf16_is_high_surrogate(uint16_t utf16char) { return 0xD800 <= utf16char && utf16char <= 0xDBFF; } static uint32_t rt_str_utf32(const char* utf8, int32_t bytes) { uint32_t utf32 = 0; if ((utf8[0] & 0x80) == 0) { utf32 = utf8[0]; rt_swear(bytes == 1); } else if ((utf8[0] & 0xE0) == 0xC0) { utf32 = (utf8[0] & 0x1F) << 6; utf32 |= (utf8[1] & 0x3F); rt_swear(bytes == 2); } else if ((utf8[0] & 0xF0) == 0xE0) { utf32 = (utf8[0] & 0x0F) << 12; utf32 |= (utf8[1] & 0x3F) << 6; utf32 |= (utf8[2] & 0x3F); rt_swear(bytes == 3); } else if ((utf8[0] & 0xF8) == 0xF0) { utf32 = (utf8[0] & 0x07) << 18; utf32 |= (utf8[1] & 0x3F) << 12; utf32 |= (utf8[2] & 0x3F) << 6; utf32 |= (utf8[3] & 0x3F); rt_swear(bytes == 4); } else { rt_swear(false); } return utf32; } static void rt_str_format_va(char* utf8, int32_t count, const char* format, va_list va) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif vsnprintf(utf8, (size_t)count, format, va); utf8[count - 1] = 0; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } static void rt_str_format(char* utf8, int32_t count, const char* format, ...) { va_list va; va_start(va, format); rt_str.format_va(utf8, count, format, va); va_end(va); } static rt_str1024_t rt_str_error_for_language(int32_t error, LANGID language) { DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; HMODULE module = null; HRESULT hr = 0 <= error && error <= 0xFFFF ? HRESULT_FROM_WIN32((uint32_t)error) : (HRESULT)error; if ((error & 0xC0000000U) == 0xC0000000U) { // https://stackoverflow.com/questions/25566234/how-to-convert-specific-ntstatus-value-to-the-hresult static HMODULE ntdll; // RtlNtStatusToDosError implies linking to ntdll if (ntdll == null) { ntdll = GetModuleHandleA("ntdll.dll"); } if (ntdll == null) { ntdll = LoadLibraryA("ntdll.dll"); } module = ntdll; hr = HRESULT_FROM_WIN32(RtlNtStatusToDosError((NTSTATUS)error)); flags |= FORMAT_MESSAGE_FROM_HMODULE; } rt_str1024_t text; uint16_t utf16[rt_countof(text.s)]; DWORD count = FormatMessageW(flags, module, hr, language, utf16, rt_countof(utf16) - 1, (va_list*)null); utf16[rt_countof(utf16) - 1] = 0; // always // If FormatMessageW() succeeds, the return value is the number of utf16 // characters stored in the output buffer, excluding the terminating zero. if (count > 0) { rt_swear(count < rt_countof(utf16)); utf16[count] = 0; // remove trailing '\r\n' int32_t k = count; if (k > 0 && utf16[k - 1] == '\n') { utf16[k - 1] = 0; } k = (int32_t)rt_str.len16(utf16); if (k > 0 && utf16[k - 1] == '\r') { utf16[k - 1] = 0; } char message[rt_countof(text.s)]; const int32_t bytes = rt_str.utf8_bytes(utf16, -1); if (bytes >= rt_countof(message)) { rt_str_printf(message, "error message is too long: %d bytes", bytes); } else { rt_str.utf16to8(message, rt_countof(message), utf16, -1); } // truncating printf to string: rt_str_printf(text.s, "0x%08X(%d) \"%s\"", error, error, message); } else { rt_str_printf(text.s, "0x%08X(%d)", error, error); } return text; } static rt_str1024_t rt_str_error(int32_t error) { const LANGID language = MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT); return rt_str_error_for_language(error, language); } static rt_str1024_t rt_str_error_nls(int32_t error) { const LANGID language = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); return rt_str_error_for_language(error, language); } static const char* rt_str_grouping_separator(void) { #ifdef WINDOWS // en-US Windows 10/11: // grouping_separator == "," // decimal_separator == "." static char grouping_separator[8]; if (grouping_separator[0] == 0x00) { errno_t r = rt_b2e(GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, grouping_separator, sizeof(grouping_separator))); rt_swear(r == 0 && grouping_separator[0] != 0); } return grouping_separator; #else // en-US Windows 10/11: // grouping_separator == "" // decimal_separator == "." struct lconv *locale_info = localeconv(); const char* grouping_separator = null; if (grouping_separator == null) { grouping_separator = locale_info->thousands_sep; swear(grouping_separator != null); } return grouping_separator; #endif } // Posix and Win32 C runtime: // #include // struct lconv *locale_info = localeconv(); // const char* grouping_separator = locale_info->thousands_sep; // const char* decimal_separator = locale_info->decimal_point; // en-US Windows 1x: // grouping_separator == "" // decimal_separator == "." // // Win32 API: // rt_b2e(GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, // grouping_separator, sizeof(grouping_separator))); // rt_b2e(GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, // decimal_separator, sizeof(decimal_separator))); // en-US Windows 1x: // grouping_separator == "," // decimal_separator == "." static rt_str64_t rt_str_int64_dg(int64_t v, // digit_grouped bool uint, const char* gs) { // grouping separator: gs // sprintf format %`lld may not be implemented or // does not respect locale or UI separators... // Do it hard way: const int32_t m = (int32_t)strlen(gs); rt_swear(m < 5); // utf-8 4 bytes max // 64 calls per thread 32 or less bytes each because: // "18446744073709551615" 21 characters + 6x4 groups: // "18'446'744'073'709'551'615" 27 characters rt_str64_t text; enum { max_text_bytes = rt_countof(text.s) }; int64_t abs64 = v < 0 ? -v : v; // incorrect for INT64_MIN uint64_t n = uint ? (uint64_t)v : (v != INT64_MIN ? (uint64_t)abs64 : (uint64_t)INT64_MIN); int32_t i = 0; int32_t groups[8]; // 2^63 - 1 ~= 9 x 10^19 upto 7 groups of 3 digits do { groups[i] = n % 1000; n = n / 1000; i++; } while (n > 0); const int32_t gc = i - 1; // group count char* s = text.s; if (v < 0 && !uint) { *s++ = '-'; } // sign int32_t r = max_text_bytes - 1; while (i > 0) { i--; rt_assert(r > 3 + m); if (i == gc) { rt_str.format(s, r, "%d%s", groups[i], gc > 0 ? gs : ""); } else { rt_str.format(s, r, "%03d%s", groups[i], i > 0 ? gs : ""); } int32_t k = (int32_t)strlen(s); r -= k; s += k; } *s = 0; return text; } static rt_str64_t rt_str_int64(int64_t v) { return rt_str_int64_dg(v, false, rt_glyph_hair_space); } static rt_str64_t rt_str_uint64(uint64_t v) { return rt_str_int64_dg(v, true, rt_glyph_hair_space); } static rt_str64_t rt_str_int64_lc(int64_t v) { return rt_str_int64_dg(v, false, rt_str_grouping_separator()); } static rt_str64_t rt_str_uint64_lc(uint64_t v) { return rt_str_int64_dg(v, true, rt_str_grouping_separator()); } static rt_str128_t rt_str_fp(const char* format, fp64_t v) { static char decimal_separator[8]; if (decimal_separator[0] == 0) { errno_t r = rt_b2e(GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, decimal_separator, sizeof(decimal_separator))); rt_swear(r == 0 && decimal_separator[0] != 0); } rt_swear(strlen(decimal_separator) <= 4); rt_str128_t f; // formatted float point // snprintf format does not handle thousands separators on all know runtimes // and respects setlocale() on Un*x systems but in MS runtime only when // _snprintf_l() is used. f.s[0] = 0x00; rt_str.format(f.s, rt_countof(f.s), format, v); f.s[rt_countof(f.s) - 1] = 0x00; rt_str128_t text; char* s = f.s; char* d = text.s; while (*s != 0x00) { if (*s == '.') { const char* sep = decimal_separator; while (*sep != 0x00) { *d++ = *sep++; } s++; } else { *d++ = *s++; } } *d = 0x00; // TODO: It's possible to handle mantissa grouping but... // Not clear if human expects it in 5 digits or 3 digits chunks // and unfortunately locale does not specify how return text; } #ifdef RT_TESTS static void rt_str_test(void) { rt_swear(rt_str.len("hello") == 5); rt_swear(rt_str.starts("hello world", "hello")); rt_swear(rt_str.ends("hello world", "world")); rt_swear(rt_str.istarts("hello world", "HeLlO")); rt_swear(rt_str.iends("hello world", "WoRlD")); char ls[20] = {0}; rt_str.lower(ls, rt_countof(ls), "HeLlO WoRlD"); rt_swear(strcmp(ls, "hello world") == 0); char upper[11] = {0}; rt_str.upper(upper, rt_countof(upper), "hello12345"); rt_swear(strcmp(upper, "HELLO12345") == 0); #pragma push_macro("glyph_chinese_one") #pragma push_macro("glyph_chinese_two") #pragma push_macro("glyph_teddy_bear") #pragma push_macro("glyph_ice_cube") #define glyph_chinese_one "\xE5\xA3\xB9" #define glyph_chinese_two "\xE8\xB4\xB0" #define glyph_teddy_bear "\xF0\x9F\xA7\xB8" #define glyph_ice_cube "\xF0\x9F\xA7\x8A" const char* utf8_str = glyph_teddy_bear "0" rt_glyph_chinese_jin4 rt_glyph_chinese_gong "3456789 " glyph_ice_cube; rt_swear(rt_str.utf8bytes("\x01", 1) == 1); rt_swear(rt_str.utf8bytes("\x7F", 1) == 1); rt_swear(rt_str.utf8bytes("\x80", 1) == 0); // swear(rt_str.utf8bytes(glyph_chinese_one, 0) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 1) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 2) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 3) == 3); rt_swear(rt_str.utf8bytes(glyph_teddy_bear, 4) == 4); #pragma pop_macro("glyph_ice_cube") #pragma pop_macro("glyph_teddy_bear") #pragma pop_macro("glyph_chinese_two") #pragma pop_macro("glyph_chinese_one") uint16_t wide_str[100] = {0}; rt_str.utf8to16(wide_str, rt_countof(wide_str), utf8_str, -1); char utf8[100] = {0}; rt_str.utf16to8(utf8, rt_countof(utf8), wide_str, -1); uint16_t utf16[100]; rt_str.utf8to16(utf16, rt_countof(utf16), utf8, -1); char narrow_str[100] = {0}; rt_str.utf16to8(narrow_str, rt_countof(narrow_str), utf16, -1); rt_swear(strcmp(narrow_str, utf8_str) == 0); char formatted[100]; rt_str.format(formatted, rt_countof(formatted), "n: %d, s: %s", 42, "test"); rt_swear(strcmp(formatted, "n: 42, s: test") == 0); // numeric values digit grouping format: rt_swear(strcmp("0", rt_str.int64_dg(0, true, ",").s) == 0); rt_swear(strcmp("-1", rt_str.int64_dg(-1, false, ",").s) == 0); rt_swear(strcmp("999", rt_str.int64_dg(999, true, ",").s) == 0); rt_swear(strcmp("-999", rt_str.int64_dg(-999, false, ",").s) == 0); rt_swear(strcmp("1,001", rt_str.int64_dg(1001, true, ",").s) == 0); rt_swear(strcmp("-1,001", rt_str.int64_dg(-1001, false, ",").s) == 0); rt_swear(strcmp("18,446,744,073,709,551,615", rt_str.int64_dg(UINT64_MAX, true, ",").s) == 0 ); rt_swear(strcmp("9,223,372,036,854,775,807", rt_str.int64_dg(INT64_MAX, false, ",").s) == 0 ); rt_swear(strcmp("-9,223,372,036,854,775,808", rt_str.int64_dg(INT64_MIN, false, ",").s) == 0 ); // see: // https://en.wikipedia.org/wiki/Single-precision_floating-point_format uint32_t pi_fp32 = 0x40490FDBULL; // 3.14159274101257324 rt_swear(strcmp("3.141592741", rt_str.fp("%.9f", *(fp32_t*)&pi_fp32).s) == 0, "%s", rt_str.fp("%.9f", *(fp32_t*)&pi_fp32).s ); // 3.141592741 // ********^ (*** true digits ^ first rounded digit) // 123456 (%.6f) // // https://en.wikipedia.org/wiki/Double-precision_floating-point_format uint64_t pi_fp64 = 0x400921FB54442D18ULL; rt_swear(strcmp("3.141592653589793116", rt_str.fp("%.18f", *(fp64_t*)&pi_fp64).s) == 0, "%s", rt_str.fp("%.18f", *(fp64_t*)&pi_fp64).s ); // 3.141592653589793116 // *****************^ (*** true digits ^ first rounded digit) // 123456789012345 (%.15f) // https://en.wikipedia.org/wiki/Double-precision_floating-point_format // // actual "pi" first 64 digits: // 3.1415926535897932384626433832795028841971693993751058209749445923 if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_str_test(void) {} #endif rt_str_if rt_str = { .drop_const = rt_str_drop_const, .len = rt_str_len, .len16 = rt_str_utf16len, .utf8bytes = rt_str_utf8bytes, .glyphs = rt_str_glyphs, .lower = rt_str_lower, .upper = rt_str_upper, .starts = rt_str_starts, .ends = rt_str_ends, .istarts = rt_str_i_starts, .iends = rt_str_i_ends, .utf8_bytes = rt_str_utf8_bytes, .utf16_chars = rt_str_utf16_chars, .utf16to8 = rt_str_utf16to8, .utf8to16 = rt_str_utf8to16, .utf16_is_low_surrogate = rt_str_utf16_is_low_surrogate, .utf16_is_high_surrogate = rt_str_utf16_is_high_surrogate, .utf32 = rt_str_utf32, .format = rt_str_format, .format_va = rt_str_format_va, .error = rt_str_error, .error_nls = rt_str_error_nls, .grouping_separator = rt_str_grouping_separator, .int64_dg = rt_str_int64_dg, .int64 = rt_str_int64, .uint64 = rt_str_uint64, .int64_lc = rt_str_int64, .uint64_lc = rt_str_uint64, .fp = rt_str_fp, .test = rt_str_test }; // _______________________________ rt_streams.c _______________________________ static errno_t rt_streams_memory_read(rt_stream_if* stream, void* data, int64_t bytes, int64_t *transferred) { rt_swear(bytes > 0); rt_stream_memory_if* s = (rt_stream_memory_if*)stream; rt_swear(0 <= s->pos_read && s->pos_read <= s->bytes_read, "bytes: %lld stream .pos: %lld .bytes: %lld", bytes, s->pos_read, s->bytes_read); int64_t transfer = rt_min(bytes, s->bytes_read - s->pos_read); memcpy(data, (const uint8_t*)s->data_read + s->pos_read, (size_t)transfer); s->pos_read += transfer; if (transferred != null) { *transferred = transfer; } return 0; } static errno_t rt_streams_memory_write(rt_stream_if* stream, const void* data, int64_t bytes, int64_t *transferred) { rt_swear(bytes > 0); rt_stream_memory_if* s = (rt_stream_memory_if*)stream; rt_swear(0 <= s->pos_write && s->pos_write <= s->bytes_write, "bytes: %lld stream .pos: %lld .bytes: %lld", bytes, s->pos_write, s->bytes_write); bool overflow = s->bytes_write - s->pos_write <= 0; int64_t transfer = rt_min(bytes, s->bytes_write - s->pos_write); memcpy((uint8_t*)s->data_write + s->pos_write, data, (size_t)transfer); s->pos_write += transfer; if (transferred != null) { *transferred = transfer; } return overflow ? ERROR_INSUFFICIENT_BUFFER : 0; } static void rt_streams_read_only(rt_stream_memory_if* s, const void* data, int64_t bytes) { s->stream.read = rt_streams_memory_read; s->stream.write = null; s->data_read = data; s->bytes_read = bytes; s->pos_read = 0; s->data_write = null; s->bytes_write = 0; s->pos_write = 0; } static void rt_streams_write_only(rt_stream_memory_if* s, void* data, int64_t bytes) { s->stream.read = null; s->stream.write = rt_streams_memory_write; s->data_read = null; s->bytes_read = 0; s->pos_read = 0; s->data_write = data; s->bytes_write = bytes; s->pos_write = 0; } static void rt_streams_read_write(rt_stream_memory_if* s, const void* read, int64_t read_bytes, void* write, int64_t write_bytes) { s->stream.read = rt_streams_memory_read; s->stream.write = rt_streams_memory_write; s->data_read = read; s->bytes_read = read_bytes; s->pos_read = 0; s->pos_read = 0; s->data_write = write; s->bytes_write = write_bytes; s->pos_write = 0; } #ifdef RT_TESTS static void rt_streams_test(void) { { // read test uint8_t memory[256]; for (int32_t i = 0; i < rt_countof(memory); i++) { memory[i] = (uint8_t)i; } for (int32_t i = 1; i < rt_countof(memory) - 1; i++) { rt_stream_memory_if ms; // memory stream rt_streams.read_only(&ms, memory, sizeof(memory)); uint8_t data[256]; for (int32_t j = 0; j < rt_countof(data); j++) { data[j] = 0xFF; } int64_t transferred = 0; errno_t r = ms.stream.read(&ms.stream, data, i, &transferred); rt_swear(r == 0 && transferred == i); for (int32_t j = 0; j < i; j++) { rt_swear(data[j] == memory[j]); } for (int32_t j = i; j < rt_countof(data); j++) { rt_swear(data[j] == 0xFF); } } } { // write test // TODO: implement } { // read/write test // TODO: implement } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_streams_test(void) { } #endif rt_streams_if rt_streams = { .read_only = rt_streams_read_only, .write_only = rt_streams_write_only, .read_write = rt_streams_read_write, .test = rt_streams_test }; // _______________________________ rt_threads.c _______________________________ // events: static rt_event_t rt_event_create(void) { HANDLE e = CreateEvent(null, false, false, null); rt_not_null(e); return (rt_event_t)e; } static rt_event_t rt_event_create_manual(void) { HANDLE e = CreateEvent(null, true, false, null); rt_not_null(e); return (rt_event_t)e; } static void rt_event_set(rt_event_t e) { rt_fatal_win32err(SetEvent((HANDLE)e)); } static void rt_event_reset(rt_event_t e) { rt_fatal_win32err(ResetEvent((HANDLE)e)); } static int32_t rt_event_wait_or_timeout(rt_event_t e, fp64_t seconds) { uint32_t ms = seconds < 0 ? INFINITE : (uint32_t)(seconds * 1000.0 + 0.5); DWORD i = WaitForSingleObject(e, ms); rt_swear(i != WAIT_FAILED, "i: %d", i); errno_t r = rt_wait_ix2e(i); if (r != 0) { rt_swear(i == WAIT_TIMEOUT || i == WAIT_ABANDONED); } return i == WAIT_TIMEOUT ? -1 : (i == WAIT_ABANDONED ? -2 : i); } static void rt_event_wait(rt_event_t e) { rt_event_wait_or_timeout(e, -1); } static int32_t rt_event_wait_any_or_timeout(int32_t n, rt_event_t events[], fp64_t s) { rt_swear(n < 64); // Win32 API limit const uint32_t ms = s < 0 ? INFINITE : (uint32_t)(s * 1000.0 + 0.5); const HANDLE* es = (const HANDLE*)events; DWORD i = WaitForMultipleObjects((DWORD)n, es, false, ms); rt_swear(i != WAIT_FAILED, "i: %d", i); errno_t r = rt_wait_ix2e(i); if (r != 0) { rt_swear(i == WAIT_TIMEOUT || i == WAIT_ABANDONED); } return i == WAIT_TIMEOUT ? -1 : (i == WAIT_ABANDONED ? -2 : i); } static int32_t rt_event_wait_any(int32_t n, rt_event_t e[]) { return rt_event_wait_any_or_timeout(n, e, -1); } static void rt_event_dispose(rt_event_t h) { rt_win32_close_handle(h); } #ifdef RT_TESTS // test: // check if the elapsed time is within the expected range static void rt_event_test_check_time(fp64_t start, fp64_t expected) { fp64_t elapsed = rt_clock.seconds() - start; // Old Windows scheduler is prone to 2x16.6ms ~ 33ms delays (observed) rt_swear(elapsed >= expected - 0.04 && elapsed <= expected + 0.250, "expected: %f elapsed %f seconds", expected, elapsed); } static void rt_event_test(void) { rt_event_t event = rt_event.create(); fp64_t start = rt_clock.seconds(); rt_event.set(event); rt_event.wait(event); rt_event_test_check_time(start, 0); // Event should be immediate rt_event.reset(event); start = rt_clock.seconds(); const fp64_t timeout_seconds = 1.0 / 8.0; int32_t result = rt_event.wait_or_timeout(event, timeout_seconds); rt_event_test_check_time(start, timeout_seconds); rt_swear(result == -1); // Timeout expected enum { count = 5 }; rt_event_t events[count]; for (int32_t i = 0; i < rt_countof(events); i++) { events[i] = rt_event.create_manual(); } start = rt_clock.seconds(); rt_event.set(events[2]); // Set the third event int32_t index = rt_event.wait_any(rt_countof(events), events); rt_swear(index == 2); rt_event_test_check_time(start, 0); rt_swear(index == 2); // Third event should be triggered rt_event.reset(events[2]); // Reset the third event start = rt_clock.seconds(); result = rt_event.wait_any_or_timeout(rt_countof(events), events, timeout_seconds); rt_swear(result == -1); rt_event_test_check_time(start, timeout_seconds); rt_swear(result == -1); // Timeout expected // Clean up rt_event.dispose(event); for (int32_t i = 0; i < rt_countof(events); i++) { rt_event.dispose(events[i]); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_event_test(void) { } #endif rt_event_if rt_event = { .create = rt_event_create, .create_manual = rt_event_create_manual, .set = rt_event_set, .reset = rt_event_reset, .wait = rt_event_wait, .wait_or_timeout = rt_event_wait_or_timeout, .wait_any = rt_event_wait_any, .wait_any_or_timeout = rt_event_wait_any_or_timeout, .dispose = rt_event_dispose, .test = rt_event_test }; // mutexes: rt_static_assertion(sizeof(CRITICAL_SECTION) == sizeof(rt_mutex_t)); static void rt_mutex_init(rt_mutex_t* m) { CRITICAL_SECTION* cs = (CRITICAL_SECTION*)m; rt_fatal_win32err(InitializeCriticalSectionAndSpinCount(cs, 4096)); } static void rt_mutex_lock(rt_mutex_t* m) { EnterCriticalSection((CRITICAL_SECTION*)m); } static void rt_mutex_unlock(rt_mutex_t* m) { LeaveCriticalSection((CRITICAL_SECTION*)m); } static void rt_mutex_dispose(rt_mutex_t* m) { DeleteCriticalSection((CRITICAL_SECTION*)m); } // test: // check if the elapsed time is within the expected range static void rt_mutex_test_check_time(fp64_t start, fp64_t expected) { fp64_t elapsed = rt_clock.seconds() - start; // Old Windows scheduler is prone to 2x16.6ms ~ 33ms delays rt_swear(elapsed >= expected - 0.04 && elapsed <= expected + 0.04, "expected: %f elapsed %f seconds", expected, elapsed); } static void rt_mutex_test_lock_unlock(void* arg) { rt_mutex_t* mutex = (rt_mutex_t*)arg; rt_mutex.lock(mutex); rt_thread.sleep_for(0.01); // Hold the mutex for 10ms rt_mutex.unlock(mutex); } static void rt_mutex_test(void) { rt_mutex_t mutex; rt_mutex.init(&mutex); fp64_t start = rt_clock.seconds(); rt_mutex.lock(&mutex); rt_mutex.unlock(&mutex); // Lock and unlock should be immediate rt_mutex_test_check_time(start, 0); enum { count = 5 }; rt_thread_t ts[count]; for (int32_t i = 0; i < rt_countof(ts); i++) { ts[i] = rt_thread.start(rt_mutex_test_lock_unlock, &mutex); } // Wait for all threads to finish for (int32_t i = 0; i < rt_countof(ts); i++) { rt_thread.join(ts[i], -1); } rt_mutex.dispose(&mutex); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } rt_mutex_if rt_mutex = { .init = rt_mutex_init, .lock = rt_mutex_lock, .unlock = rt_mutex_unlock, .dispose = rt_mutex_dispose, .test = rt_mutex_test }; // threads: static void* rt_thread_ntdll(void) { static HMODULE ntdll; if (ntdll == null) { ntdll = (void*)GetModuleHandleA("ntdll.dll"); } if (ntdll == null) { ntdll = rt_loader.open("ntdll.dll", 0); } rt_not_null(ntdll); return ntdll; } static fp64_t rt_thread_ns2ms(int64_t ns) { return (fp64_t)ns / (fp64_t)rt_clock.nsec_in_msec; } static void rt_thread_set_timer_resolution(uint64_t nanoseconds) { typedef int32_t (*query_timer_resolution_t)(ULONG* minimum_resolution, ULONG* maximum_resolution, ULONG* actual_resolution); typedef int32_t (*set_timer_resolution_t)(ULONG requested_resolution, BOOLEAN set, ULONG* actual_resolution); // ntdll.dll void* nt_dll = rt_thread_ntdll(); query_timer_resolution_t query_timer_resolution = (query_timer_resolution_t) rt_loader.sym(nt_dll, "NtQueryTimerResolution"); set_timer_resolution_t set_timer_resolution = (set_timer_resolution_t) rt_loader.sym(nt_dll, "NtSetTimerResolution"); unsigned long min100ns = 16 * 10 * 1000; unsigned long max100ns = 1 * 10 * 1000; unsigned long cur100ns = 0; rt_fatal_if(query_timer_resolution(&min100ns, &max100ns, &cur100ns) != 0); uint64_t max_ns = max100ns * 100uLL; // uint64_t min_ns = min100ns * 100uLL; // uint64_t cur_ns = cur100ns * 100uLL; // max resolution is lowest possible delay between timer events // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f" // " ms (milliseconds)", // rt_thread_ns2ms(min_ns), // rt_thread_ns2ms(max_ns), // rt_thread_ns2ms(cur_ns)); // } // note that maximum resolution is actually < minimum nanoseconds = rt_max(max_ns, nanoseconds); unsigned long ns = (unsigned long)((nanoseconds + 99) / 100); rt_fatal_if(set_timer_resolution(ns, true, &cur100ns) != 0); rt_fatal_if(query_timer_resolution(&min100ns, &max100ns, &cur100ns) != 0); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // min_ns = min100ns * 100uLL; // max_ns = max100ns * 100uLL; // the smallest interval // cur_ns = cur100ns * 100uLL; // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f ms (milliseconds)", // rt_thread_ns2ms(min_ns), // rt_thread_ns2ms(max_ns), // rt_thread_ns2ms(cur_ns)); // } } static void rt_thread_power_throttling_disable_for_process(void) { static bool disabled_for_the_process; if (!disabled_for_the_process) { PROCESS_POWER_THROTTLING_STATE pt = { 0 }; pt.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; pt.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED; pt.StateMask = 0; rt_fatal_win32err(SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt))); // PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION // does not work on Win10. There is no easy way to // distinguish Windows 11 from 10 (Microsoft great engineering) pt.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; pt.StateMask = 0; // ignore error on Windows 10: (void)SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt)); disabled_for_the_process = true; } } static void rt_thread_power_throttling_disable_for_thread(HANDLE thread) { THREAD_POWER_THROTTLING_STATE pt = { 0 }; pt.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION; pt.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; pt.StateMask = 0; rt_fatal_win32err(SetThreadInformation(thread, ThreadPowerThrottling, &pt, sizeof(pt))); } static void rt_thread_disable_power_throttling(void) { rt_thread_power_throttling_disable_for_process(); rt_thread_power_throttling_disable_for_thread(GetCurrentThread()); } static const char* rt_thread_rel2str(int32_t rel) { switch (rel) { case RelationProcessorCore : return "ProcessorCore "; case RelationNumaNode : return "NumaNode "; case RelationCache : return "Cache "; case RelationProcessorPackage: return "ProcessorPackage"; case RelationGroup : return "Group "; case RelationProcessorDie : return "ProcessorDie "; case RelationNumaNodeEx : return "NumaNodeEx "; case RelationProcessorModule : return "ProcessorModule "; default: rt_assert(false, "fix me"); return "???"; } } static uint64_t rt_thread_next_physical_processor_affinity_mask(void) { static volatile int32_t initialized; static int32_t init; static int32_t next = 1; // next physical core to use static int32_t cores = 0; // number of physical processors (cores) static uint64_t any; static uint64_t affinity[64]; // mask for each physical processor bool set_to_true = rt_atomics.compare_exchange_int32(&init, false, true); if (set_to_true) { // Concept D: 6 cores, 12 logical processors: 27 lpi entries static SYSTEM_LOGICAL_PROCESSOR_INFORMATION lpi[64]; DWORD bytes = 0; GetLogicalProcessorInformation(null, &bytes); rt_assert(bytes % sizeof(lpi[0]) == 0); // number of lpi entries == 27 on 6 core / 12 logical processors system int32_t n = bytes / sizeof(lpi[0]); rt_assert(bytes <= sizeof(lpi), "increase lpi[%d]", n); rt_fatal_win32err(GetLogicalProcessorInformation(&lpi[0], &bytes)); for (int32_t i = 0; i < n; i++) { // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("[%2d] affinity mask 0x%016llX relationship=%d %s", i, // lpi[i].ProcessorMask, lpi[i].Relationship, // rt_thread_rel2str(lpi[i].Relationship)); // } if (lpi[i].Relationship == RelationProcessorCore) { rt_assert(cores < rt_countof(affinity), "increase affinity[%d]", cores); if (cores < rt_countof(affinity)) { any |= lpi[i].ProcessorMask; affinity[cores] = lpi[i].ProcessorMask; cores++; } } } initialized = true; } else { while (initialized == 0) { rt_thread.sleep_for(1 / 1024.0); } rt_assert(any != 0); // should not ever happen if (any == 0) { any = (uint64_t)(-1LL); } } uint64_t mask = next < cores ? affinity[next] : any; rt_assert(mask != 0); // assume last physical core is least popular if (next < cores) { next++; } // not circular return mask; } static void rt_thread_realtime(void) { rt_fatal_win32err(SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)); rt_fatal_win32err(SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)); rt_fatal_win32err(SetThreadPriorityBoost(GetCurrentThread(), /* bDisablePriorityBoost = */ false)); // desired: 0.5ms = 500us (microsecond) = 50,000ns rt_thread_set_timer_resolution((uint64_t)rt_clock.nsec_in_usec * 500ULL); rt_fatal_win32err(SetThreadAffinityMask(GetCurrentThread(), rt_thread_next_physical_processor_affinity_mask())); rt_thread_disable_power_throttling(); } static void rt_thread_yield(void) { SwitchToThread(); } static rt_thread_t rt_thread_start(void (*func)(void*), void* p) { rt_thread_t t = (rt_thread_t)CreateThread(null, 0, (LPTHREAD_START_ROUTINE)(void*)func, p, 0, null); rt_not_null(t); return t; } static bool is_handle_valid(void* h) { DWORD flags = 0; return GetHandleInformation(h, &flags); } static errno_t rt_thread_join(rt_thread_t t, fp64_t timeout) { rt_not_null(t); rt_fatal_if(!is_handle_valid(t)); const uint32_t ms = timeout < 0 ? INFINITE : (uint32_t)(timeout * 1000.0 + 0.5); DWORD ix = WaitForSingleObject(t, (DWORD)ms); errno_t r = rt_wait_ix2e(ix); rt_assert(r != ERROR_REQUEST_ABORTED, "AFAIK thread can`t be ABANDONED"); if (r == 0) { rt_win32_close_handle(t); } else { rt_println("failed to join thread %p %s", t, rt_strerr(r)); } return r; } static void rt_thread_detach(rt_thread_t t) { rt_not_null(t); rt_fatal_if(!is_handle_valid(t)); rt_win32_close_handle(t); } static void rt_thread_name(const char* name) { uint16_t stack[128]; rt_fatal_if(rt_str.len(name) >= rt_countof(stack), "name too long: %s", name); rt_str.utf8to16(stack, rt_countof(stack), name, -1); HRESULT r = SetThreadDescription(GetCurrentThread(), stack); // notoriously returns 0x10000000 for no good reason whatsoever rt_fatal_if(!SUCCEEDED(r)); } static void rt_thread_sleep_for(fp64_t seconds) { rt_assert(seconds >= 0); if (seconds < 0) { seconds = 0; } int64_t ns100 = (int64_t)(seconds * 1.0e+7); // in 0.1 us aka 100ns typedef int32_t (__stdcall *nt_delay_execution_t)(BOOLEAN alertable, PLARGE_INTEGER DelayInterval); static nt_delay_execution_t NtDelayExecution; // delay in 100-ns units. negative value means delay relative to current. LARGE_INTEGER delay = {0}; // delay in 100-ns units. delay.QuadPart = -ns100; // negative value means delay relative to current. if (NtDelayExecution == null) { void* ntdll = rt_thread_ntdll(); NtDelayExecution = (nt_delay_execution_t) rt_loader.sym(ntdll, "NtDelayExecution"); rt_not_null(NtDelayExecution); } // If "alertable" is set, sleep_for() can break earlier // as a result of NtAlertThread call. NtDelayExecution(false, &delay); } static uint64_t rt_thread_id_of(rt_thread_t t) { return (uint64_t)GetThreadId((HANDLE)t); } static uint64_t rt_thread_id(void) { return (uint64_t)GetThreadId(GetCurrentThread()); } static rt_thread_t rt_thread_self(void) { // GetCurrentThread() returns pseudo-handle, not a real handle // if real handle is ever needed may do // rt_thread_t t = rt_thread.open(rt_thread.id()) and // rt_thread.close(t) instead. return (rt_thread_t)GetCurrentThread(); } static errno_t rt_thread_open(rt_thread_t *t, uint64_t id) { // GetCurrentThread() returns pseudo-handle, not a real handle. // if real handle is ever needed do rt_thread_id_of() instead // but don't forget to do rt_thread.close() after that. *t = (rt_thread_t)OpenThread(THREAD_ALL_ACCESS, false, (DWORD)id); return *t == null ? rt_core.err() : 0; } static void rt_thread_close(rt_thread_t t) { rt_not_null(t); rt_win32_close_handle((HANDLE)t); } #ifdef RT_TESTS // test: https://en.wikipedia.org/wiki/Dining_philosophers_problem typedef struct rt_thread_philosophers_s rt_thread_philosophers_t; typedef struct { rt_thread_philosophers_t* ps; rt_mutex_t fork; rt_mutex_t* left_fork; rt_mutex_t* right_fork; rt_thread_t thread; uint64_t id; } rt_thread_philosopher_t; typedef struct rt_thread_philosophers_s { rt_thread_philosopher_t philosopher[3]; rt_event_t fed_up[3]; uint32_t seed; volatile bool enough; } rt_thread_philosophers_t; #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void rt_thread_philosopher_think(rt_thread_philosopher_t* p) { verbose("philosopher %d is thinking.", p->id); // Random think time between .1 and .3 seconds fp64_t seconds = (rt_num.random32(&p->ps->seed) % 30 + 1) / 100.0; rt_thread.sleep_for(seconds); } static void rt_thread_philosopher_eat(rt_thread_philosopher_t* p) { verbose("philosopher %d is eating.", p->id); // Random eat time between .1 and .2 seconds fp64_t seconds = (rt_num.random32(&p->ps->seed) % 20 + 1) / 100.0; rt_thread.sleep_for(seconds); } // To avoid deadlocks in the Three Philosophers problem, we can implement // the Tanenbaum's solution, which ensures that one of the philosophers // (e.g., the last one) tries to pick up the right fork first, while the // others pick up the left fork first. This breaks the circular wait // condition and prevents deadlock. // If the philosopher is the last one (p->id == n - 1) they will try to pick // up the right fork first and then the left fork. All other philosophers will // pick up the left fork first and then the right fork, as before. This change // ensures that at least one philosopher will be able to eat, breaking the // circular wait condition and preventing deadlock. static void rt_thread_philosopher_routine(void* arg) { rt_thread_philosopher_t* p = (rt_thread_philosopher_t*)arg; enum { n = rt_countof(p->ps->philosopher) }; rt_thread.name("philosopher"); rt_thread.realtime(); while (!p->ps->enough) { rt_thread_philosopher_think(p); if (p->id == n - 1) { // Last philosopher picks up the right fork first rt_mutex.lock(p->right_fork); verbose("philosopher %d picked up right fork.", p->id); rt_mutex.lock(p->left_fork); verbose("philosopher %d picked up left fork.", p->id); } else { // Other philosophers pick up the left fork first rt_mutex.lock(p->left_fork); verbose("philosopher %d picked up left fork.", p->id); rt_mutex.lock(p->right_fork); verbose("philosopher %d picked up right fork.", p->id); } rt_thread_philosopher_eat(p); rt_mutex.unlock(p->right_fork); verbose("philosopher %d put down right fork.", p->id); rt_mutex.unlock(p->left_fork); verbose("philosopher %d put down left fork.", p->id); rt_event.set(p->ps->fed_up[p->id]); } } static void rt_thread_detached_sleep(void* rt_unused(p)) { rt_thread.sleep_for(1000.0); // seconds } static void rt_thread_detached_loop(void* rt_unused(p)) { uint64_t sum = 0; for (uint64_t i = 0; i < UINT64_MAX; i++) { sum += i; } // make sure that compiler won't get rid of the loop: rt_swear(sum == 0x8000000000000001ULL, "sum: %llu 0x%16llX", sum, sum); } static void rt_thread_test(void) { rt_thread_philosophers_t ps = { .seed = 1 }; enum { n = rt_countof(ps.philosopher) }; // Initialize mutexes for forks for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; p->id = i; p->ps = &ps; rt_mutex.init(&p->fork); p->left_fork = &p->fork; ps.fed_up[i] = rt_event.create(); } // Create and start philosopher threads for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_thread_philosopher_t* r = &ps.philosopher[(i + 1) % n]; p->right_fork = r->left_fork; p->thread = rt_thread.start(rt_thread_philosopher_routine, p); } // wait for all philosophers being fed up: for (int32_t i = 0; i < n; i++) { rt_event.wait(ps.fed_up[i]); } ps.enough = true; // join all philosopher threads for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_thread.join(p->thread, -1); } // Dispose of mutexes and events for (int32_t i = 0; i < n; ++i) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_mutex.dispose(&p->fork); rt_event.dispose(ps.fed_up[i]); } // detached threads are hacky and not that swell of an idea // but sometimes can be useful for 1. quick hacks 2. threads // that execute blocking calls that e.g. write logs to the // internet service that hangs. // test detached threads rt_thread_t detached_sleep = rt_thread.start(rt_thread_detached_sleep, null); rt_thread.detach(detached_sleep); rt_thread_t detached_loop = rt_thread.start(rt_thread_detached_loop, null); rt_thread.detach(detached_loop); // leave detached threads sleeping and running till ExitProcess(0) // that should NOT hang. if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("verbose") #else static void rt_thread_test(void) { } #endif rt_thread_if rt_thread = { .start = rt_thread_start, .join = rt_thread_join, .detach = rt_thread_detach, .name = rt_thread_name, .realtime = rt_thread_realtime, .yield = rt_thread_yield, .sleep_for = rt_thread_sleep_for, .id_of = rt_thread_id_of, .id = rt_thread_id, .self = rt_thread_self, .open = rt_thread_open, .close = rt_thread_close, .test = rt_thread_test }; // ________________________________ rt_vigil.c ________________________________ #include #include static void rt_vigil_breakpoint_and_abort(void) { rt_debug.breakpoint(); // only if debugger is present rt_debug.raise(rt_debug.exception.noncontinuable); rt_core.abort(); } static int32_t rt_vigil_failed_assertion(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); rt_debug.println(file, line, func, "assertion failed: %s\n", condition); // avoid warnings: conditional expression always true and unreachable code const bool always_true = rt_core.abort != null; if (always_true) { rt_vigil_breakpoint_and_abort(); } return 0; } static int32_t rt_vigil_fatal_termination_va(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, va_list va) { const int32_t er = rt_core.err(); const int32_t en = errno; rt_debug.println_va(file, line, func, format, va); if (r != er && r != 0) { rt_debug.perror(file, line, func, r, ""); } // report last errors: if (er != 0) { rt_debug.perror(file, line, func, er, ""); } if (en != 0) { rt_debug.perrno(file, line, func, en, ""); } if (condition != null && condition[0] != 0) { rt_debug.println(file, line, func, "FATAL: %s\n", condition); } else { rt_debug.println(file, line, func, "FATAL\n"); } const bool always_true = rt_core.abort != null; if (always_true) { rt_vigil_breakpoint_and_abort(); } return 0; } static int32_t rt_vigil_fatal_termination(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { va_list va; va_start(va, format); rt_vigil_fatal_termination_va(file, line, func, condition, 0, format, va); va_end(va); return 0; } static int32_t rt_vigil_fatal_if_error(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, ...) { if (r != 0) { va_list va; va_start(va, format); rt_vigil_fatal_termination_va(file, line, func, condition, r, format, va); va_end(va); } return 0; } #ifdef RT_TESTS static rt_vigil_if rt_vigil_test_saved; static int32_t rt_vigil_test_failed_assertion_count; #pragma push_macro("rt_vigil") // intimate knowledge of vigil.*() functions used in macro definitions #define rt_vigil rt_vigil_test_saved static int32_t rt_vigil_test_failed_assertion(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { rt_fatal_if_not(strcmp(file, __FILE__) == 0, "file: %s", file); rt_fatal_if_not(line > __LINE__, "line: %s", line); rt_assert(strcmp(func, "rt_vigil_test") == 0, "func: %s", func); rt_fatal_if(condition == null || condition[0] == 0); rt_fatal_if(format == null || format[0] == 0); rt_vigil_test_failed_assertion_count++; if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); rt_debug.println(file, line, func, "assertion failed: %s (expected)\n", condition); } return 0; } static int32_t rt_vigil_test_fatal_calls_count; static int32_t rt_vigil_test_fatal_termination(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { const int32_t er = rt_core.err(); const int32_t en = errno; rt_assert(er == 2, "rt_core.err: %d expected 2", er); rt_assert(en == 2, "errno: %d expected 2", en); rt_fatal_if_not(strcmp(file, __FILE__) == 0, "file: %s", file); rt_fatal_if_not(line > __LINE__, "line: %s", line); rt_assert(strcmp(func, "rt_vigil_test") == 0, "func: %s", func); rt_assert(strcmp(condition, "") == 0); // not yet used expected to be "" rt_assert(format != null && format[0] != 0); rt_vigil_test_fatal_calls_count++; if (rt_debug.verbosity.level > rt_debug.verbosity.trace) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); if (er != 0) { rt_debug.perror(file, line, func, er, ""); } if (en != 0) { rt_debug.perrno(file, line, func, en, ""); } if (condition != null && condition[0] != 0) { rt_debug.println(file, line, func, "FATAL: %s (testing)\n", condition); } else { rt_debug.println(file, line, func, "FATAL (testing)\n"); } } return 0; } #pragma pop_macro("rt_vigil") static void rt_vigil_test(void) { rt_vigil_test_saved = rt_vigil; int32_t en = errno; int32_t er = rt_core.err(); errno = 2; // ENOENT rt_core.set_err(2); // ERROR_FILE_NOT_FOUND rt_vigil.failed_assertion = rt_vigil_test_failed_assertion; rt_vigil.fatal_termination = rt_vigil_test_fatal_termination; int32_t count = rt_vigil_test_fatal_calls_count; rt_fatal("testing: %s call", "fatal()"); rt_assert(rt_vigil_test_fatal_calls_count == count + 1); count = rt_vigil_test_failed_assertion_count; rt_assert(false, "testing: rt_assert(%s)", "false"); #ifdef DEBUG // verify that rt_assert() is only compiled in DEBUG: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count + 1); #else // not RELEASE buid: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count); #endif count = rt_vigil_test_failed_assertion_count; rt_swear(false, "testing: swear(%s)", "false"); // swear() is triggered in both debug and release configurations: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count + 1); errno = en; rt_core.set_err(er); rt_vigil = rt_vigil_test_saved; if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_vigil_test(void) { } #endif rt_vigil_if rt_vigil = { .failed_assertion = rt_vigil_failed_assertion, .fatal_termination = rt_vigil_fatal_termination, .fatal_if_error = rt_vigil_fatal_if_error, .test = rt_vigil_test }; // ________________________________ rt_win32.c ________________________________ void rt_win32_close_handle(void* h) { #pragma warning(suppress: 6001) // shut up overzealous IntelliSense rt_fatal_win32err(CloseHandle((HANDLE)h)); } // WAIT_ABANDONED only reported for mutexes not events // WAIT_FAILED means event was invalid handle or was disposed // by another thread while the calling thread was waiting for it. /* translate ix to error */ errno_t rt_wait_ix2e(uint32_t r) { const int32_t ix = (int32_t)r; return (errno_t)( (int32_t)WAIT_OBJECT_0 <= ix && ix <= WAIT_OBJECT_0 + 63 ? 0 : (ix == WAIT_ABANDONED ? ERROR_REQUEST_ABORTED : (ix == WAIT_TIMEOUT ? ERROR_TIMEOUT : (ix == WAIT_FAILED) ? rt_core.err() : ERROR_INVALID_HANDLE ) ) ); } // ________________________________ rt_work.c _________________________________ static void rt_work_queue_no_duplicates(rt_work_t* w) { rt_work_t* e = w->queue->head; bool found = false; while (e != null && !found) { found = e == w; if (!found) { e = e->next; } } rt_swear(!found); } static void rt_work_queue_post(rt_work_t* w) { rt_assert(w->queue != null && w != null && w->when >= 0.0); rt_work_queue_t* q = w->queue; rt_atomics.spinlock_acquire(&q->lock); rt_work_queue_no_duplicates(w); // under lock // Enqueue in time sorted order least ->time first to save // time searching in fetching from queue which is more frequent. rt_work_t* p = null; rt_work_t* e = q->head; while (e != null && e->when <= w->when) { p = e; e = e->next; } w->next = e; bool head = p == null; if (head) { q->head = w; } else { p->next = w; } rt_atomics.spinlock_release(&q->lock); if (head && q->changed != null) { rt_event.set(q->changed); } } static void rt_work_queue_cancel(rt_work_t* w) { rt_swear(!w->canceled && w->queue != null && w->queue->head != null); rt_work_queue_t* q = w->queue; rt_atomics.spinlock_acquire(&q->lock); rt_work_t* p = null; rt_work_t* e = q->head; bool changed = false; // head changed while (e != null && !w->canceled) { if (e == w) { changed = p == null; if (changed) { q->head = e->next; } else { p->next = e->next; } e->next = null; e->canceled = true; } else { p = e; e = e->next; } } rt_atomics.spinlock_release(&q->lock); rt_swear(w->canceled); if (w->done != null) { rt_event.set(w->done); } if (changed && q->changed != null) { rt_event.set(q->changed); } } static void rt_work_queue_flush(rt_work_queue_t* q) { while (q->head != null) { rt_work_queue.cancel(q->head); } } static bool rt_work_queue_get(rt_work_queue_t* q, rt_work_t* *r) { rt_work_t* w = null; rt_atomics.spinlock_acquire(&q->lock); bool changed = q->head != null && q->head->when <= rt_clock.seconds(); if (changed) { w = q->head; q->head = w->next; w->next = null; } rt_atomics.spinlock_release(&q->lock); *r = w; if (changed && q->changed != null) { rt_event.set(q->changed); } return w != null; } static void rt_work_queue_call(rt_work_t* w) { if (w->work != null) { w->work(w); } if (w->done != null) { rt_event.set(w->done); } } static void rt_work_queue_dispatch(rt_work_queue_t* q) { rt_work_t* w = null; while (rt_work_queue.get(q, &w)) { rt_work_queue.call(w); } } rt_work_queue_if rt_work_queue = { .post = rt_work_queue_post, .get = rt_work_queue_get, .call = rt_work_queue_call, .dispatch = rt_work_queue_dispatch, .cancel = rt_work_queue_cancel, .flush = rt_work_queue_flush }; static void rt_worker_thread(void* p) { rt_thread.name("worker"); rt_worker_t* worker = (rt_worker_t*)p; rt_work_queue_t* q = &worker->queue; while (!worker->quit) { rt_work_queue.dispatch(q); fp64_t timeout = -1.0; // forever rt_atomics.spinlock_acquire(&q->lock); if (q->head != null) { timeout = rt_max(0, q->head->when - rt_clock.seconds()); } rt_atomics.spinlock_release(&q->lock); // if another item is inserted into head after unlocking // the `wake` event guaranteed to be signalled if (!worker->quit && timeout != 0) { rt_event.wait_or_timeout(worker->wake, timeout); } } rt_work_queue.dispatch(q); } static void rt_worker_start(rt_worker_t* worker) { rt_assert(worker->wake == null && !worker->quit); worker->wake = rt_event.create(); worker->queue = (rt_work_queue_t){ .head = null, .lock = 0, .changed = worker->wake }; worker->thread = rt_thread.start(rt_worker_thread, worker); } static errno_t rt_worker_join(rt_worker_t* worker, fp64_t to) { worker->quit = true; rt_event.set(worker->wake); errno_t r = rt_thread.join(worker->thread, to); if (r == 0) { rt_event.dispose(worker->wake); worker->wake = null; worker->thread = null; worker->quit = false; rt_swear(worker->queue.head == null); } return r; } static void rt_worker_post(rt_worker_t* worker, rt_work_t* w) { rt_assert(!worker->quit && worker->wake != null && worker->thread != null); w->queue = &worker->queue; rt_work_queue.post(w); } static void rt_worker_test(void); rt_worker_if rt_worker = { .start = rt_worker_start, .post = rt_worker_post, .join = rt_worker_join, .test = rt_worker_test }; #ifdef RT_TESTS // tests: // keep in mind that rt_println() may be blocking and is a subject // of "astronomical" wait state times in order of dozens of ms. static int32_t rt_test_called; static void rt_never_called(rt_work_t* rt_unused(w)) { rt_test_called++; } static void rt_work_queue_test_1(void) { rt_test_called = 0; // testing insertion time ordering of two events into queue const fp64_t now = rt_clock.seconds(); rt_work_queue_t q = {0}; rt_work_t c1 = { .queue = &q, .work = rt_never_called, .when = now + 1.0 }; rt_work_t c2 = { .queue = &q, .work = rt_never_called, .when = now + 0.5 }; rt_work_queue.post(&c1); rt_swear(q.head == &c1 && q.head->next == null); rt_work_queue.post(&c2); rt_swear(q.head == &c2 && q.head->next == &c1); rt_work_queue.flush(&q); // test that canceled events are not dispatched rt_swear(rt_test_called == 0 && c1.canceled && c2.canceled && q.head == null); c1.canceled = false; c2.canceled = false; // test the rt_work_queue.cancel() function rt_work_queue.post(&c1); rt_work_queue.post(&c2); rt_swear(q.head == &c2 && q.head->next == &c1); rt_work_queue.cancel(&c2); rt_swear(c2.canceled && q.head == &c1 && q.head->next == null); c2.canceled = false; rt_work_queue.post(&c2); rt_work_queue.cancel(&c1); rt_swear(c1.canceled && q.head == &c2 && q.head->next == null); rt_work_queue.flush(&q); rt_swear(rt_test_called == 0 && c1.canceled && c2.canceled && q.head == null); } // simple way of passing a single pointer to call_later static fp64_t rt_test_work_start; // makes timing debug traces easier to read static void rt_every_millisecond(rt_work_t* w) { int32_t* i = (int32_t*)w->data; fp64_t now = rt_clock.seconds(); if (rt_debug.verbosity.level > rt_debug.verbosity.info) { const fp64_t since_start = now - rt_test_work_start; const fp64_t dt = w->when - rt_test_work_start; rt_println("%d now: %.6f time: %.6f", *i, since_start, dt); } (*i)++; // read rt_clock.seconds() again because rt_println() above could block w->when = rt_clock.seconds() + 0.001; rt_work_queue.post(w); } static void rt_work_queue_test_2(void) { rt_thread.realtime(); rt_test_work_start = rt_clock.seconds(); rt_work_queue_t q = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t c = { .queue = &q, .work = rt_every_millisecond, .when = rt_test_work_start + 0.001, .data = &i }; rt_work_queue.post(&c); while (q.head != null && i < 8) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(&q); } rt_work_queue.flush(&q); rt_swear(q.head == null); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("called: %d times", i); } } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { // nameless union opens up base fields into rt_work_ex_t // it is not necessary at all union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void rt_every_other_millisecond(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; fp64_t now = rt_clock.seconds(); if (rt_debug.verbosity.level > rt_debug.verbosity.info) { const fp64_t since_start = now - rt_test_work_start; const fp64_t dt = w->when - rt_test_work_start; rt_println(".i: %d .extra: {.a: %d .b: %d} now: %.6f time: %.6f", ex->i, ex->s.a, ex->s.b, since_start, dt); } ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; // read rt_clock.seconds() again because rt_println() above could block w->when = rt_clock.seconds() + 0.002; rt_work_queue.post(w); } static void rt_work_queue_test_3(void) { rt_thread.realtime(); rt_static_assertion(offsetof(rt_work_ex_t, base) == 0); const fp64_t now = rt_clock.seconds(); rt_work_queue_t q = {0}; rt_work_ex_t ex = { .queue = &q, .work = rt_every_other_millisecond, .when = now + 0.002, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&ex.base); while (q.head != null && ex.i < 8) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(&q); } rt_work_queue.flush(&q); rt_swear(q.head == null); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("called: %d times", ex.i); } } static void rt_work_queue_test(void) { rt_work_queue_test_1(); rt_work_queue_test_2(); rt_work_queue_test_3(); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } static int32_t rt_test_do_work_called; static void rt_test_do_work(rt_work_t* rt_unused(w)) { rt_test_do_work_called++; } static void rt_worker_test(void) { // uncomment one of the following lines to see the output // rt_debug.verbosity.level = rt_debug.verbosity.info; // rt_debug.verbosity.level = rt_debug.verbosity.verbose; rt_work_queue_test(); // first test rt_work_queue rt_worker_t worker = { 0 }; rt_worker.start(&worker); rt_work_t asap = { .when = 0, // A.S.A.P. .done = rt_event.create(), .work = rt_test_do_work }; rt_work_t later = { .when = rt_clock.seconds() + 0.010, // 10ms .done = rt_event.create(), .work = rt_test_do_work }; rt_worker.post(&worker, &asap); rt_worker.post(&worker, &later); // because `asap` and `later` are local variables // code needs to wait for them to be processed inside // this function before they goes out of scope rt_event.wait(asap.done); // await(asap) rt_event.dispose(asap.done); // responsibility of the caller // wait for later: rt_event.wait(later.done); // await(later) rt_event.dispose(later.done); // responsibility of the caller // quit the worker thread: rt_fatal_if_error(rt_worker.join(&worker, -1.0)); // does worker respect .when dispatch time? rt_swear(rt_clock.seconds() >= later.when); } #else static void rt_work_queue_test(void) {} static void rt_worker_test(void) {} #endif #endif // rt_implementation ================================================ FILE: single_file_lib/ui/ui.h ================================================ #ifndef ui_definition #define ui_definition // ___________________________________ ui.h ___________________________________ // alphabetical order is not possible because of headers interdependencies // _________________________________ rt_std.h _________________________________ #include #include #include #include #include #include #include #include #include #include #include #include #include #define rt_stringify(x) #x #define rt_tostring(x) rt_stringify(x) #define rt_pragma(x) _Pragma(rt_tostring(x)) #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #pragma GCC diagnostic ignored "-Wfour-char-constants" #pragma GCC diagnostic ignored "-Wmissing-field-initializers" #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wmissing-noreturn" #pragma GCC diagnostic ignored "-Wdouble-promotion" #pragma GCC diagnostic ignored "-Wcast-align" #pragma GCC diagnostic ignored "-Waddress-of-packed-member" #pragma GCC diagnostic ignored "-Wused-but-marked-unused" // because in debug only #define rt_msvc_pragma(x) #define rt_gcc_pragma(x) rt_pragma(x) #else #define rt_gcc_pragma(x) #define rt_msvc_pragma(x) rt_pragma(x) #endif #ifdef _MSC_VER #define rt_suppress_constant_cond_exp _Pragma("warning(suppress: 4127)") #else #define rt_suppress_constant_cond_exp #endif // Type aliases for floating-point types similar to typedef float fp32_t; typedef double fp64_t; // "long fp64_t" is required by C standard but the bitness // of it is not specified. #ifdef __cplusplus #define rt_begin_c extern "C" { #define rt_end_c } // extern "C" #else #define rt_begin_c // C headers compiled as C++ #define rt_end_c #endif // rt_countof() and rt_countof() are suitable for // small < 2^31 element arrays #define rt_countof(a) ((int32_t)((int)(sizeof(a) / sizeof((a)[0])))) #if defined(__GNUC__) || defined(__clang__) #define rt_force_inline __attribute__((always_inline)) #elif defined(_MSC_VER) #define rt_force_inline __forceinline #endif #ifndef __cplusplus #define null ((void*)0) // better than NULL which is zero #else #define null nullptr #endif #if defined(_MSC_VER) #define rt_thread_local __declspec(thread) #else #ifndef __cplusplus #define rt_thread_local _Thread_local // C99 #else // C++ supports rt_thread_local keyword #endif #endif // rt_begin_packed rt_end_packed // usage: typedef rt_begin_packed struct foo_s { ... } rt_end_packed foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_attribute_packed __attribute__((packed)) #define rt_begin_packed #define rt_end_packed rt_attribute_packed #else #define rt_begin_packed rt_pragma( pack(push, 1) ) #define rt_end_packed rt_pragma( pack(pop) ) #define rt_attribute_packed #endif // usage: typedef struct rt_aligned_8 foo_s { ... } foo_t; #if defined(__GNUC__) || defined(__clang__) #define rt_aligned_8 __attribute__((aligned(8))) #elif defined(_MSC_VER) #define rt_aligned_8 __declspec(align(8)) #else #define rt_aligned_8 #endif // In callbacks the formal parameters are // frequently unused. Also sometimes parameters // are used in debug configuration only (e.g. rt_assert() checks) // but not in release. // C does not have anonymous parameters like C++ // Instead of: // void foo(param_type_t param) { (void)param; / *unused */ } // use: // vod foo(param_type_t rt_unused(param)) { } #if defined(__GNUC__) || defined(__clang__) #define rt_unused(name) name __attribute__((unused)) #elif defined(_MSC_VER) #define rt_unused(name) _Pragma("warning(suppress: 4100)") name #else #define rt_unused(name) name #endif // Because MS C compiler is unhappy about alloca() and // does not implement (C99 optional) dynamic arrays on the stack: #define rt_stackalloc(n) (_Pragma("warning(suppress: 6255 6263)") alloca(n)) // alloca() is messy and in general is a not a good idea. // try to avoid if possible. Stack sizes vary from 64KB to 8MB in 2024. // ________________________________ ui_core.h _________________________________ #include "rt/rt_std.h" rt_begin_c typedef struct ui_point_s { int32_t x, y; } ui_point_t; typedef struct ui_rect_s { int32_t x, y, w, h; } ui_rect_t; typedef struct ui_ltbr_s { int32_t left, top, right, bottom; } ui_ltrb_t; typedef struct ui_wh_s { int32_t w, h; } ui_wh_t; typedef struct ui_window_s* ui_window_t; typedef struct ui_icon_s* ui_icon_t; typedef struct ui_canvas_s* ui_canvas_t; typedef struct ui_texture_s* ui_texture_t; typedef struct ui_font_s* ui_font_t; typedef struct ui_brush_s* ui_brush_t; typedef struct ui_pen_s* ui_pen_t; typedef struct ui_cursor_s* ui_cursor_t; typedef struct ui_region_s* ui_region_t; typedef uintptr_t ui_timer_t; // timer not the same as "id" in set_timer()! typedef struct ui_bitmap_s { // TODO: ui_ namespace void* pixels; int32_t w; // width int32_t h; // height int32_t bpp; // "components" bytes per pixel int32_t stride; // bytes per scanline rounded up to: (w * bpp + 3) & ~3 ui_texture_t texture; // device allocated texture handle } ui_bitmap_t; // ui_margins_t are used for padding and insets and expressed // in partial "em"s not in pixels, inches or points. // Pay attention that "em" is not square. "M" measurement // for most fonts are em.w = 0.5 * em.h // .em square pixel size of glyph "m" // https://en.wikipedia.org/wiki/Em_(typography) typedef struct ui_gaps_s { // in partial "em"s fp32_t left; fp32_t top; fp32_t right; fp32_t bottom; } ui_margins_t; typedef struct ui_s { bool (*point_in_rect)(const ui_point_t* p, const ui_rect_t* r); // intersect_rect(null, r0, r1) and intersect_rect(r0, r0, r1) supported. bool (*intersect_rect)(ui_rect_t* destination, const ui_rect_t* r0, const ui_rect_t* r1); ui_rect_t (*combine_rect)(const ui_rect_t* r0, const ui_rect_t* r1); const int32_t infinity; // = INT32_MAX, look better struct { // align bitset int32_t const center; // = 0, default int32_t const left; // left|top, left|bottom, right|bottom int32_t const top; int32_t const right; // right|top, right|bottom int32_t const bottom; } const align; struct { // window visibility int32_t const hide; int32_t const normal; // should be use for first .show() int32_t const minimize; // activate and minimize int32_t const maximize; // activate and maximize int32_t const normal_na;// same as .normal but no activate int32_t const show; // shows and activates in current size and position int32_t const min_next; // minimize and activate next window in Z order int32_t const min_na; // minimize but do not activate int32_t const show_na; // same as .show but no activate int32_t const restore; // from min/max to normal window size/pos int32_t const defau1t; // use Windows STARTUPINFO value int32_t const force_min;// minimize even if dispatch thread not responding } const visibility; // TODO: remove or move inside app struct { // message: int32_t const animate; int32_t const opening; int32_t const closing; } const message; // TODO: remove or move inside app struct { // mouse buttons bitset mask struct { int32_t const left; int32_t const right; } button; } const mouse; struct { // window decorations hit test results int32_t const error; // -2 int32_t const transparent; // -1 int32_t const nowhere; // 0 int32_t const client; // 1 int32_t const caption; // 2 int32_t const system_menu; // 3 int32_t const grow_box; // 4 int32_t const menu; // 5 int32_t const horizontal_scroll;// 6 int32_t const vertical_scroll; // 7 int32_t const min_button; // 8 int32_t const max_button; // 9 int32_t const left; // 10 int32_t const right; // 11 int32_t const top; // 12 int32_t const top_left; // 13 int32_t const top_right; // 14 int32_t const bottom; // 15 int32_t const bottom_left; // 16 int32_t const bottom_right; // 17 int32_t const border; // 18 int32_t const object; // 19 int32_t const close; // 20 int32_t const help; // 21 } const hit_test; struct { // virtual keyboard keys int32_t const up; int32_t const down; int32_t const left; int32_t const right; int32_t const home; int32_t const end; int32_t const page_up; int32_t const page_down; int32_t const insert; int32_t const del; int32_t const back; int32_t const escape; int32_t const enter; int32_t const plus; int32_t const minus; int32_t const f1; int32_t const f2; int32_t const f3; int32_t const f4; int32_t const f5; int32_t const f6; int32_t const f7; int32_t const f8; int32_t const f9; int32_t const f10; int32_t const f11; int32_t const f12; int32_t const f13; int32_t const f14; int32_t const f15; int32_t const f16; int32_t const f17; int32_t const f18; int32_t const f19; int32_t const f20; int32_t const f21; int32_t const f22; int32_t const f23; int32_t const f24; } const key; struct { int32_t const ok; int32_t const info; int32_t const question; int32_t const warning; int32_t const error; } beep; } ui_if; extern ui_if ui; // ui_margins_t in "em"s: // // The reason is that UI fonts may become larger smaller // for accessibility reasons with the same display // density in DPIs. Humanoid would expect the margins around // larger font text to grow with font size increase. // SwingUI and MacOS is using "pt" for padding which does // not account to font size changes. MacOS does weird stuff // with font increase - it actually decreases GPU resolution. // Android uses "dp" which is pretty much the same as scaled // "pixels" on MacOS. Windows used to use "dialog units" which // is font size based and this is where the idea is inherited from. rt_end_c // _______________________________ ui_colors.h ________________________________ rt_begin_c typedef uint64_t ui_color_t; // top 2 bits determine color format /* TODO: make ui_color_t uint64_t RGBA or better yet fp32_t RGBA support upto 16-16-16-14(A)bit per pixel color components with 'transparent' aka 'hollow' bit */ #define ui_color_mask ((ui_color_t)0xC000000000000000ULL) #define ui_color_undefined ((ui_color_t)0x8000000000000000ULL) #define ui_color_transparent ((ui_color_t)0x4000000000000000ULL) #define ui_color_hdr ((ui_color_t)0xC000000000000000ULL) #define ui_color_is_8bit(c) ( ((c) & ui_color_mask) == 0) #define ui_color_is_hdr(c) ( ((c) & ui_color_mask) == ui_color_hdr) #define ui_color_is_undefined(c) ( ((c) & ui_color_mask) == ui_color_undefined) #define ui_color_is_transparent(c) ((((c) & ui_color_mask) == ui_color_transparent) && \ ( ((c) & ~ui_color_mask) == 0)) // if any other special colors or formats need to be introduced // (c) & ~ui_color_mask) has 2^62 possible extensions bits // ui_color_hdr A - 14 bit, R,G,B - 16 bit, all in range [0..0xFFFF] #define ui_color_hdr_a(c) ((uint16_t)((((c) >> 48) & 0x3FFF) << 2)) #define ui_color_hdr_r(c) ((uint16_t)( ((c) >> 0) & 0xFFFF)) #define ui_color_hdr_g(c) ((uint16_t)( ((c) >> 16) & 0xFFFF)) #define ui_color_hdr_b(c) ((uint16_t)( ((c) >> 32) & 0xFFFF)) #define ui_color_a(c) ((uint8_t)(((c) >> 24) & 0xFFU)) #define ui_color_r(c) ((uint8_t)(((c) >> 0) & 0xFFU)) #define ui_color_g(c) ((uint8_t)(((c) >> 8) & 0xFFU)) #define ui_color_b(c) ((uint8_t)(((c) >> 16) & 0xFFU)) #define ui_color_is_rgb(c) ((uint32_t)( (c) & 0x00FFFFFFU)) #define ui_color_is_rgba(c) ((uint32_t)( (c) & 0xFFFFFFFFU)) #define ui_color_is_rgbFF(c) ((uint32_t)(((c) & 0x00FFFFFFU)) | 0xFF000000U) #define ui_color_rgb(r, g, b) ((ui_color_t)( \ (((uint32_t)(uint8_t)(r)) ) | \ (((uint32_t)(uint8_t)(g)) << 8) | \ (((uint32_t)(uint8_t)(b)) << 16))) #define ui_color_rgba(r, g, b, a) \ ( (ui_color_t)( \ (ui_color_rgb(r, g, b)) | \ ((ui_color_t)((uint32_t)((uint8_t)(a))) << 24)) \ ) enum { ui_color_id_undefined = 0, ui_color_id_active_title = 1, ui_color_id_button_face = 2, ui_color_id_button_text = 3, ui_color_id_gray_text = 4, ui_color_id_highlight = 5, ui_color_id_highlight_text = 6, ui_color_id_hot_tracking = 7, ui_color_id_inactive_title = 8, ui_color_id_inactive_title_text = 9, ui_color_id_menu_highlight = 10, ui_color_id_title_text = 11, ui_color_id_window = 12, ui_color_id_window_text = 13, ui_color_id_accent = 14 }; typedef struct ui_control_colors_s { ui_color_t text; ui_color_t background; ui_color_t border; ui_color_t accent; // aka highlight ui_color_t gradient_top; ui_color_t gradient_bottom; } control_colors_t; typedef struct ui_control_state_colors_s { control_colors_t disabled; control_colors_t enabled; control_colors_t hover; control_colors_t armed; control_colors_t pressed; } ui_control_state_colors_t; typedef struct ui_colors_s { ui_color_t (*get_color)(int32_t color_id); // ui.colors.* void (*rgb_to_hsi)(fp64_t r, fp64_t g, fp64_t b, fp64_t *h, fp64_t *s, fp64_t *i); ui_color_t (*hsi_to_rgb)(fp64_t h, fp64_t s, fp64_t i, uint8_t a); // interpolate(): // 0.0 < multiplier < 1.0 excluding boundaries // alpha is interpolated as well ui_color_t (*interpolate)(ui_color_t c0, ui_color_t c1, fp32_t multiplier); ui_color_t (*gray_with_same_intensity)(ui_color_t c); // multiplier ]0.0..1.0] excluding zero // lighten() and darken() ignore alpha (use interpolate for alpha colors) ui_color_t (*lighten)(ui_color_t rgb, fp32_t multiplier); // interpolate toward white ui_color_t (*darken)(ui_color_t rgb, fp32_t multiplier); // interpolate toward black ui_color_t (*adjust_saturation)(ui_color_t c, fp32_t multiplier); ui_color_t (*multiply_brightness)(ui_color_t c, fp32_t multiplier); ui_color_t (*multiply_saturation)(ui_color_t c, fp32_t multiplier); ui_control_state_colors_t* controls; // colors for UI controls ui_color_t const transparent; ui_color_t const none; // aka CLR_INVALID in wingdi.h ui_color_t const text; ui_color_t const white; ui_color_t const black; ui_color_t const red; ui_color_t const green; ui_color_t const blue; ui_color_t const yellow; ui_color_t const cyan; ui_color_t const magenta; ui_color_t const gray; // tone down RGB colors: ui_color_t const tone_white; ui_color_t const tone_red; ui_color_t const tone_green; ui_color_t const tone_blue; ui_color_t const tone_yellow; ui_color_t const tone_cyan; ui_color_t const tone_magenta; // miscellaneous: ui_color_t const orange; ui_color_t const dark_green; ui_color_t const pink; ui_color_t const ochre; ui_color_t const gold; ui_color_t const teal; ui_color_t const wheat; ui_color_t const tan; ui_color_t const brown; ui_color_t const maroon; ui_color_t const barbie_pink; ui_color_t const steel_pink; ui_color_t const salmon_pink; ui_color_t const gainsboro; ui_color_t const light_gray; ui_color_t const silver; ui_color_t const dark_gray; ui_color_t const dim_gray; ui_color_t const light_slate_gray; ui_color_t const slate_gray; /* Named colors */ /* Main Panel Backgrounds */ ui_color_t const ennui_black; // rgb(18, 18, 18) 0x121212 ui_color_t const charcoal; ui_color_t const onyx; ui_color_t const gunmetal; ui_color_t const jet_black; ui_color_t const outer_space; ui_color_t const eerie_black; ui_color_t const oil; ui_color_t const black_coral; ui_color_t const obsidian; /* Secondary Panels or Sidebars */ ui_color_t const raisin_black; ui_color_t const dark_charcoal; ui_color_t const dark_jungle_green; ui_color_t const pine_tree; ui_color_t const rich_black; ui_color_t const eclipse; ui_color_t const cafe_noir; /* Flat Buttons */ ui_color_t const prussian_blue; ui_color_t const midnight_green; ui_color_t const charleston_green; ui_color_t const rich_black_fogra; ui_color_t const dark_liver; ui_color_t const dark_slate_gray; ui_color_t const black_olive; ui_color_t const cadet; /* Button highlights (hover) */ ui_color_t const dark_sienna; ui_color_t const bistre_brown; ui_color_t const dark_puce; ui_color_t const wenge; /* Raised button effects */ ui_color_t const dark_scarlet; ui_color_t const burnt_umber; ui_color_t const caput_mortuum; ui_color_t const barn_red; /* Text and Icons */ ui_color_t const platinum; ui_color_t const anti_flash_white; ui_color_t const silver_sand; ui_color_t const quick_silver; /* Links and Selections */ ui_color_t const dark_powder_blue; ui_color_t const sapphire_blue; ui_color_t const international_klein_blue; ui_color_t const zaffre; /* Additional Colors */ ui_color_t const fish_belly; ui_color_t const rusty_red; ui_color_t const falu_red; ui_color_t const cordovan; ui_color_t const dark_raspberry; ui_color_t const deep_magenta; ui_color_t const byzantium; ui_color_t const amethyst; ui_color_t const wisteria; ui_color_t const lavender_purple; ui_color_t const opera_mauve; ui_color_t const mauve_taupe; ui_color_t const rich_lavender; ui_color_t const pansy_purple; ui_color_t const violet_eggplant; ui_color_t const jazzberry_jam; ui_color_t const dark_orchid; ui_color_t const electric_purple; ui_color_t const sky_magenta; ui_color_t const brilliant_rose; ui_color_t const fuchsia_purple; ui_color_t const french_raspberry; ui_color_t const wild_watermelon; ui_color_t const neon_carrot; ui_color_t const burnt_orange; ui_color_t const carrot_orange; ui_color_t const tiger_orange; ui_color_t const giant_onion; ui_color_t const rust; ui_color_t const copper_red; ui_color_t const dark_tangerine; ui_color_t const bright_marigold; ui_color_t const bone; /* Earthy Tones */ ui_color_t const sienna; ui_color_t const sandy_brown; ui_color_t const golden_brown; ui_color_t const camel; ui_color_t const burnt_sienna; ui_color_t const khaki; ui_color_t const dark_khaki; /* Greens */ ui_color_t const fern_green; ui_color_t const moss_green; ui_color_t const myrtle_green; ui_color_t const pine_green; ui_color_t const jungle_green; ui_color_t const sacramento_green; /* Blues */ ui_color_t const yale_blue; ui_color_t const cobalt_blue; ui_color_t const persian_blue; ui_color_t const royal_blue; ui_color_t const iceberg; ui_color_t const blue_yonder; /* Miscellaneous */ ui_color_t const cocoa_brown; ui_color_t const cinnamon_satin; ui_color_t const fallow; ui_color_t const cafe_au_lait; ui_color_t const liver; ui_color_t const shadow; ui_color_t const cool_grey; ui_color_t const payne_grey; /* Lighter Tones for Contrast */ ui_color_t const timberwolf; ui_color_t const silver_chalice; ui_color_t const roman_silver; /* Dark Mode Specific Highlights */ ui_color_t const electric_lavender; ui_color_t const magenta_haze; ui_color_t const cyber_grape; ui_color_t const purple_navy; ui_color_t const liberty; ui_color_t const purple_mountain_majesty; ui_color_t const ceil; ui_color_t const moonstone_blue; ui_color_t const independence; } ui_colors_if; extern ui_colors_if ui_colors; // TODO: // https://ankiewicz.com/colors/ // https://htmlcolorcodes.com/color-names/ // it would be super cool to implement a plethora of palettes // with named colors and app "themes" that can be switched rt_end_c // _______________________________ ui_fuzzing.h _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" // ___________________________________ ui.h ___________________________________ // alphabetical order is not possible because of headers interdependencies // _________________________________ ui_gdi.h _________________________________ rt_begin_c // Graphic Device Interface (selected parts of Windows GDI) enum { // TODO: into gdi int32_t const ui_gdi_font_quality_default = 0, ui_gdi_font_quality_draft = 1, ui_gdi_font_quality_proof = 2, // anti-aliased w/o ClearType rainbows ui_gdi_font_quality_nonantialiased = 3, ui_gdi_font_quality_antialiased = 4, ui_gdi_font_quality_cleartype = 5, ui_gdi_font_quality_cleartype_natural = 6 }; typedef struct ui_fm_s { // font metrics ui_font_t font; ui_wh_t em; // "em" square point size expressed in pixels *) // https://learn.microsoft.com/en-us/windows/win32/gdi/string-widths-and-heights int32_t height; // font height in pixels int32_t baseline; // bottom of the glyphs sans descenders (align of multi-font text) int32_t ascent; // the maximum glyphs extend above the baseline int32_t descent; // maximum height of descenders int32_t x_height; // small letters height int32_t cap_em_height; // Capital letter "M" height int32_t internal_leading; // accents and diacritical marks goes there int32_t external_leading; int32_t average_char_width; int32_t max_char_width; int32_t line_gap; // gap between lines of text ui_wh_t subscript; // height ui_point_t subscript_offset; ui_wh_t superscript; // height ui_point_t superscript_offset; int32_t underscore; // height int32_t underscore_position; int32_t strike_through; // height int32_t strike_through_position; int32_t design_units_per_em; // aka EM square ~ 2048 ui_rect_t box; // bounding box of the glyphs in design units bool mono; } ui_fm_t; /* see: https://github.com/leok7v/ui/wiki/Typography-Line-Terms https://en.wikipedia.org/wiki/Typeface#Font_metrics Example em55x55 H1 font @ 192dpi: _ _ _ ___ <- y:0 (_)_(_) | | ___ /\ "diacritics circumflex" / \ __ _ _ _ _ __ | |_ ___ _ __ || / _ \ / _` | | | | '_ \| __/ _ \ '_ \ || .ascend:30 / ___ \ (_| | |_| | |_) | || __/ | | | || max extend above baseline /_/ \_\__, |\__, | .__/ \__\___|_| |_| ___ || <- .baseline:44 __/ | __/ | | || .descend:11 |___/ |___/|_| ___ \/ max height of descenders <- .height:55 em: 55x55 ascender for "diacritics circumflex" is (h:55 - a:30 - d:11) = 14 */ typedef struct ui_gdi_ta_s { // text attributes const ui_fm_t* fm; // font metrics int32_t color_id; // <= 0 use color ui_color_t color; // ui_colors.undefined() use color_id bool measure; // measure only do not draw } ui_gdi_ta_t; typedef struct { struct { struct { ui_gdi_ta_t const normal; ui_gdi_ta_t const title; ui_gdi_ta_t const rubric; ui_gdi_ta_t const H1; ui_gdi_ta_t const H2; ui_gdi_ta_t const H3; } prop; struct { ui_gdi_ta_t const normal; ui_gdi_ta_t const title; ui_gdi_ta_t const rubric; ui_gdi_ta_t const H1; ui_gdi_ta_t const H2; ui_gdi_ta_t const H3; } mono; } const ta; void (*init)(void); void (*fini)(void); void (*begin)(ui_bitmap_t* bitmap_or_null); // all paint must be done in between void (*end)(void); // TODO: move to ui_colors uint32_t (*color_rgb)(ui_color_t c); // rgb color // bpp bytes (not bits!) per pixel. bpp = -3 or -4 does not swap RGB to BRG: void (*bitmap_init)(ui_bitmap_t* bitmap, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels); void (*bitmap_init_rgbx)(ui_bitmap_t* bitmap, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels); // sets all alphas to 0xFF void (*bitmap_dispose)(ui_bitmap_t* bitmap); void (*set_clip)(int32_t x, int32_t y, int32_t w, int32_t h); // use set_clip(0, 0, 0, 0) to clear clip region void (*pixel)(int32_t x, int32_t y, ui_color_t c); void (*line)(int32_t x0, int32_t y1, int32_t x2, int32_t y2, ui_color_t c); void (*frame)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c); void (*rect)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t border, ui_color_t fill); void (*fill)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c); void (*poly)(ui_point_t* points, int32_t count, ui_color_t c); void (*circle)(int32_t center_x, int32_t center_y, int32_t odd_radius, ui_color_t border, ui_color_t fill); void (*rounded)(int32_t x, int32_t y, int32_t w, int32_t h, int32_t odd_radius, ui_color_t border, ui_color_t fill); void (*gradient)(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t rgba_from, ui_color_t rgba_to, bool vertical); // dx, dy, dw, dh destination rectangle // ix, iy, iw, ih rectangle inside pixels[height][width] void (*pixels)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, int32_t bpp, const uint8_t* pixels); // bytes per pixel void (*greyscale)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); void (*bgr)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); void (*bgrx)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t x, int32_t y, int32_t w, int32_t h, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels); // alpha() blend only works with device allocated bitmaps void (*alpha)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* bitmap, fp64_t alpha); // alpha blend // bitmap() only works with device allocated bitmaps void (*bitmap)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* bitmap); void (*icon)(int32_t dx, int32_t dy, int32_t dw, int32_t dh, ui_icon_t icon); // text: void (*cleartype)(bool on); // system wide change: don't use void (*font_smoothing_contrast)(int32_t c); // [1000..2202] or -1 for 1400 default ui_font_t (*create_font)(const char* family, int32_t height, int32_t quality); // custom font, quality: -1 "as is" ui_font_t (*font)(ui_font_t f, int32_t height, int32_t quality); void (*delete_font)(ui_font_t f); void (*dump_fm)(ui_font_t f); // dump font metrics void (*update_fm)(ui_fm_t* fm, ui_font_t f); // fills font metrics ui_wh_t (*text_va)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, va_list va); ui_wh_t (*text)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, ...); ui_wh_t (*multiline_va)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va); // "w" can be zero ui_wh_t (*multiline)(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, ...); // x[rt_str.glyphs(utf8, bytes)] = {x0, x1, x2, ...} ui_wh_t (*glyphs_placement)(const ui_gdi_ta_t* ta, const char* utf8, int32_t bytes, int32_t x[/*glyphs + 1*/], int32_t glyphs); } ui_gdi_if; extern ui_gdi_if ui_gdi; rt_end_c // ________________________________ ui_view.h _________________________________ rt_begin_c enum ui_view_type_t { ui_view_stack = 'vwst', ui_view_label = 'vwlb', ui_view_mbx = 'vwmb', ui_view_button = 'vwbt', ui_view_toggle = 'vwtg', ui_view_slider = 'vwsl', ui_view_image = 'vwiv', ui_view_text = 'vwtx', ui_view_span = 'vwhs', ui_view_list = 'vwvs', ui_view_spacer = 'vwsp', ui_view_scroll = 'vwsc' }; typedef struct ui_view_s ui_view_t; typedef struct ui_view_private_s { // do not access directly char text[1024]; // utf8 zero terminated int32_t strid; // 0 for not yet localized, -1 no localization fp64_t armed_until; // rt_clock.seconds() - when to release fp64_t hover_when; // time in seconds when to call hovered() // use: ui_view.string(v) and ui_view.set_string() } ui_view_private_t; typedef struct ui_view_text_metrics_s { // ui_view.measure_text() fills these attributes: ui_wh_t wh; // text width and height ui_point_t xy; // text offset inside view bool multiline; // text contains "\n" } ui_view_text_metrics_t; typedef struct ui_view_s { enum ui_view_type_t type; ui_view_private_t p; // private void (*init)(ui_view_t* v); // called once before first layout ui_view_t* parent; ui_view_t* child; // first child, circular doubly linked list ui_view_t* prev; // left or top sibling ui_view_t* next; // right or top sibling int32_t x; int32_t y; int32_t w; int32_t h; ui_margins_t insets; ui_margins_t padding; ui_view_text_metrics_t text; // see ui.alignment values int32_t align; // align inside parent int32_t text_align; // align of the text inside control int32_t max_w; // > 0 maximum width in pixels the view agrees to int32_t max_h; // > 0 maximum height in pixels fp32_t min_w_em; // > 0 minimum width of a view in "em"s fp32_t min_h_em; // > 0 minimum height of a view in "em"s ui_icon_t icon; // used instead of text if != null // updated on layout() call const ui_fm_t* fm; // font metrics int32_t shortcut; // keyboard shortcut void* that; // for the application use void (*notify)(ui_view_t* v, void* p); // for the application use // two pass layout: measure() .w, .h layout() .x .y // first measure() bottom up - children.layout before parent.layout // second layout() top down - parent.layout before children.layout // before methods: called before measure()/layout()/paint() void (*prepare)(ui_view_t* v); // called before measure() void (*measure)(ui_view_t* v); // determine w, h (bottom up) void (*measured)(ui_view_t* v); // called after measure() void (*layout)(ui_view_t* v); // set x, y possibly adjust w, h (top down) void (*composed)(ui_view_t* v); // after layout() is done (laid out) void (*erase)(ui_view_t* v); // called before paint() void (*paint)(ui_view_t* v); void (*painted)(ui_view_t* v); // called after paint() // composed() is effectively called right before paint() and // can be used to prepare for painting w/o need to override paint() void (*debug_paint)(ui_view_t* v); // called if .debug is set to true // any message: bool (*message)(ui_view_t* v, int32_t message, int64_t wp, int64_t lp, int64_t* rt); // return true and value in rt to stop processing void (*click)(ui_view_t* v); // ui click callback - view action void (*format)(ui_view_t* v); // format a value to text (e.g. slider) void (*callback)(ui_view_t* v); // state change callback void (*mouse_scroll)(ui_view_t* v, ui_point_t dx_dy); // touchpad scroll void (*mouse_hover)(ui_view_t* v); // hover over void (*mouse_move)(ui_view_t* v); void (*double_click)(ui_view_t* v, int32_t ix); // tap(ui, button_index) press(ui, button_index) see note below // button index 0: left, 1: middle, 2: right // bottom up (leaves to root or children to parent) // return true if consumed (halts further calls up the tree) bool (*tap)(ui_view_t* v, int32_t ix, bool pressed); // single click/tap inside ui bool (*long_press)(ui_view_t* v, int32_t ix); // two finger click/tap or long press bool (*double_tap)(ui_view_t* v, int32_t ix); // legacy double click bool (*context_menu)(ui_view_t* v); // right mouse click or long press void (*focus_gained)(ui_view_t* v); void (*focus_lost)(ui_view_t* v); // translated from key pressed/released to utf8: void (*character)(ui_view_t* v, const char* utf8); bool (*key_pressed)(ui_view_t* v, int64_t key); // return true to stop bool (*key_released)(ui_view_t* v, int64_t key); // processing // timer() every_100ms() and every_sec() called // even for hidden and disabled views void (*timer)(ui_view_t* v, ui_timer_t id); void (*every_100ms)(ui_view_t* v); // ~10 x times per second void (*every_sec)(ui_view_t* v); // ~once a second int64_t (*hit_test)(const ui_view_t* v, ui_point_t pt); struct { bool hidden; // measure()/ layout() paint() is not called on bool disabled; // mouse, keyboard, key_up/down not called on bool armed; // button is pressed but not yet released bool hover; // cursor is hovering over the control bool pressed; // for ui_button_t and ui_toggle_t } state; // TODO: instead of flat color scheme: undefined colors for // border rounded gradient etc. bool flat; // no-border appearance of controls bool flip; // flip button pressed / released bool focusable; // can be target for keyboard focus bool highlightable; // paint highlight rectangle when hover over label ui_color_t color; // interpretation depends on view type int32_t color_id; // 0 is default meaning use color ui_color_t background; // interpretation depends on view type int32_t background_id; // 0 is default meaning use background char hint[256]; // tooltip hint text (to be shown while hovering over view) struct { struct { bool prc; // paint rect bool mt; // measure text } trace; struct { // after painted(): bool call; // v->debug_paint() bool margins; // call debug_paint_margins() bool fm; // paint font metrics } paint; const char* id; // for debugging purposes } debug; // debug flags } ui_view_t; // tap() / press() APIs guarantee that single tap() is not coming // before fp64_t tap/click in expense of fp64_t click delay (0.5 seconds) // which is OK for buttons and many other UI controls but absolutely not // OK for text editing. Thus edit uses raw mouse events to react // on clicks and fp64_t clicks. typedef struct ui_view_if { // children va_args must be null terminated ui_view_t* (*add)(ui_view_t* parent, ...); void (*add_first)(ui_view_t* parent, ui_view_t* child); void (*add_last)(ui_view_t* parent, ui_view_t* child); void (*add_after)(ui_view_t* child, ui_view_t* after); void (*add_before)(ui_view_t* child, ui_view_t* before); void (*remove)(ui_view_t* v); // removes view from it`s parent void (*remove_all)(ui_view_t* parent); // removes all children void (*disband)(ui_view_t* parent); // removes all children recursively bool (*is_parent_of)(const ui_view_t* p, const ui_view_t* c); bool (*inside)(const ui_view_t* v, const ui_point_t* pt); ui_ltrb_t (*margins)(const ui_view_t* v, const ui_margins_t* g); // to pixels void (*inbox)(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* insets); void (*outbox)(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* padding); void (*set_text)(ui_view_t* v, const char* format, ...); void (*set_text_va)(ui_view_t* v, const char* format, va_list va); // ui_view.invalidate() prone to 30ms delays don't use in r/t video code // ui_view.invalidate(v, ui_app.crc) invalidates whole client rect but // ui_view.redraw() (fast non blocking) is much better instead void (*invalidate)(const ui_view_t* v, const ui_rect_t* rect_or_null); bool (*is_orphan)(const ui_view_t* v); // view parent chain has null bool (*is_hidden)(const ui_view_t* v); // view or any parent is hidden bool (*is_disabled)(const ui_view_t* v); // view or any parent is disabled bool (*is_control)(const ui_view_t* v); bool (*is_container)(const ui_view_t* v); bool (*is_spacer)(const ui_view_t* v); const char* (*string)(ui_view_t* v); // returns localized text void (*timer)(ui_view_t* v, ui_timer_t id); void (*every_sec)(ui_view_t* v); void (*every_100ms)(ui_view_t* v); int64_t (*hit_test)(const ui_view_t* v, ui_point_t pt); // key_pressed() key_released() return true to stop further processing bool (*key_pressed)(ui_view_t* v, int64_t v_key); bool (*key_released)(ui_view_t* v, int64_t v_key); void (*character)(ui_view_t* v, const char* utf8); void (*paint)(ui_view_t* v); bool (*has_focus)(const ui_view_t* v); // ui_app.focused() && ui_app.focus == v void (*set_focus)(ui_view_t* view_or_null); void (*lose_hidden_focus)(ui_view_t* v); void (*hovering)(ui_view_t* v, bool start); void (*mouse_hover)(ui_view_t* v); // hover over void (*mouse_move)(ui_view_t* v); void (*mouse_scroll)(ui_view_t* v, ui_point_t dx_dy); // touchpad scroll ui_wh_t (*text_metrics_va)(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, va_list va); ui_wh_t (*text_metrics)(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, ...); void (*text_measure)(ui_view_t* v, const char* s, ui_view_text_metrics_t* tm); void (*text_align)(ui_view_t* v, ui_view_text_metrics_t* tm); void (*measure_text)(ui_view_t* v); // fills v->text.mt and .xy // measure_control(): control is special case with v->text.mt and .xy void (*measure_control)(ui_view_t* v); void (*measure_children)(ui_view_t* v); void (*layout_children)(ui_view_t* v); void (*measure)(ui_view_t* v); void (*layout)(ui_view_t* v); void (*hover_changed)(ui_view_t* v); bool (*is_shortcut_key)(ui_view_t* v, int64_t key); bool (*context_menu)(ui_view_t* v); // `ix` 0: left 1: middle 2: right bool (*tap)(ui_view_t* v, int32_t ix, bool pressed); bool (*long_press)(ui_view_t* v, int32_t ix); bool (*double_tap)(ui_view_t* v, int32_t ix); bool (*message)(ui_view_t* v, int32_t m, int64_t wp, int64_t lp, int64_t* ret); void (*debug_paint_margins)(ui_view_t* v); // insets padding void (*debug_paint_fm)(ui_view_t* v); // text font metrics void (*test)(void); } ui_view_if; extern ui_view_if ui_view; // view children iterator: #define ui_view_for_each_begin(v, it) do { \ ui_view_t* it = (v)->child; \ if (it != null) { \ do { \ #define ui_view_for_each_end(v, it) \ it = it->next; \ } while (it != (v)->child); \ } \ } while (0) #define ui_view_for_each(v, it, ...) \ ui_view_for_each_begin(v, it) \ { __VA_ARGS__ } \ ui_view_for_each_end(v, it) #define ui_view_debug_id(v) \ ((v)->debug.id != null ? (v)->debug.id : (v)->p.text) // #define code(statements) statements // // used as: // { // macro({ // foo(); // bar(); // }) // } // // except in m4 preprocessor loses new line // between foo() and bar() and makes debugging and // using __LINE__ difficult to impossible. // // Also // #define code(...) { __VA_ARGS__ } // is way easier on preprocessor // ui_view_insets (fractions of 1/2 to keep float calculations precise): #define ui_view_i_lr (0.750f) // 3/4 of "em.w" on left and right #define ui_view_i_tb (0.125f) // 1/8 em // ui_view_padding #define ui_view_p_lr (0.375f) #define ui_view_p_tb (0.250f) #define ui_view_call_init(v) do { \ if ((v)->init != null) { \ void (*_init_)(ui_view_t* _v_) = (v)->init; \ (v)->init = null; /* before! call */ \ _init_((v)); \ } \ } while (0) rt_end_c // _____________________________ ui_containers.h ______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ rt_begin_c typedef struct ui_view_s ui_view_t; // Usage: // // ui_view_t* stack = ui_view(stack); // ui_view_t* horizontal = ui_view(ui_view_span); // ui_view_t* vertical = ui_view(ui_view_list); // // containers automatically layout child views // similar to SwiftUI HStack and VStack taking .align // .insets and .padding into account. // // Container positions every child views in the center, // top bottom left right edge or any of 4 corners // depending on .align values. // if child view has .max_w or .max_h set to ui.infinity == INT32_MAX // the views are expanded to fill the container in specified // direction. If child .max_w or .max_h is set to > .w or .h // the child view .w .h measurement are expanded accordingly. // // All containers are transparent and inset by 1/4 of an "em" // Except ui_app.root,caption,content which are also containers // but are not inset or padded and have default background color. // // Application implementer can override this after // // void opened(void) { // ui_view.add(ui_app.view, ..., null); // ui_app.view->insets = (ui_margins_t) { // .left = 0.25, .top = 0.25, // .right = 0.25, .bottom = 0.25 }; // ui_app.view->color = ui_colors.dark_scarlet; // } typedef struct ui_view_s ui_view_t; #define ui_view(view_type) { \ .type = (ui_view_ ## view_type), \ .init = ui_view_init_ ## view_type, \ .fm = &ui_app.fm.prop.normal, \ .color = ui_color_transparent, \ .color_id = 0 \ } void ui_view_init_stack(ui_view_t* v); void ui_view_init_span(ui_view_t* v); void ui_view_init_list(ui_view_t* v); void ui_view_init_spacer(ui_view_t* v); rt_end_c // ______________________________ ui_edit_doc.h _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ rt_begin_c typedef struct ui_edit_str_s ui_edit_str_t; typedef struct ui_edit_doc_s ui_edit_doc_t; typedef struct ui_edit_notify_s ui_edit_notify_t; typedef struct ui_edit_to_do_s ui_edit_to_do_t; typedef struct ui_edit_pg_s { // page/glyph coordinates // humans used to line:column coordinates in text int32_t pn; // zero based paragraph number ("line number") int32_t gp; // zero based glyph position ("column") } ui_edit_pg_t; typedef union rt_begin_packed ui_edit_range_s { struct { ui_edit_pg_t from; ui_edit_pg_t to; }; ui_edit_pg_t a[2]; } rt_end_packed ui_edit_range_t; // "from"[0] "to"[1] typedef struct ui_edit_text_s { int32_t np; // number of paragraphs ui_edit_str_t* ps; // ps[np] paragraphs } ui_edit_text_t; typedef struct ui_edit_notify_info_s { bool ok; // false if ui_edit_view.replace() failed (bad utf8 or no memory) const ui_edit_doc_t* const d; const ui_edit_range_t* const r; // range to be replaced const ui_edit_range_t* const x; // extended range (replacement) const ui_edit_text_t* const t; // replacement text // d->text.np number of paragraphs may change after replace // before/after: [pnf..pnt] is inside [0..d->text.np-1] int32_t const pnf; // paragraph number from int32_t const pnt; // paragraph number to. (inclusive) // one can safely assume that ps[pnf] was modified // except empty range replace with empty text (which shouldn't be) // d->text.ps[pnf..pnf + deleted] were deleted // d->text.ps[pnf..pnf + inserted] were inserted int32_t const deleted; // number of deleted paragraphs (before: 0) int32_t const inserted; // paragraph inserted paragraphs (before: 0) } ui_edit_notify_info_t; typedef struct ui_edit_notify_s { // called before and after replace() void (*before)(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni); // after() is called even if replace() failed with ok: false void (*after)(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni); } ui_edit_notify_t; typedef struct ui_edit_listener_s ui_edit_listener_t; typedef struct ui_edit_listener_s { ui_edit_notify_t* notify; ui_edit_listener_t* prev; ui_edit_listener_t* next; } ui_edit_listener_t; typedef struct ui_edit_to_do_s { // undo/redo action ui_edit_range_t range; ui_edit_text_t text; ui_edit_to_do_t* next; // inside undo or redo list } ui_edit_to_do_t; typedef struct ui_edit_doc_s { ui_edit_text_t text; ui_edit_to_do_t* undo; // undo stack ui_edit_to_do_t* redo; // redo stack ui_edit_listener_t* listeners; } ui_edit_doc_t; typedef struct ui_edit_doc_if { // init(utf8, bytes, heap:false) must have longer lifetime // than document, otherwise use heap: true to copy bool (*init)(ui_edit_doc_t* d, const char* utf8_or_null, int32_t bytes, bool heap); bool (*replace)(ui_edit_doc_t* d, const ui_edit_range_t* r, const char* utf8, int32_t bytes); int32_t (*bytes)(const ui_edit_doc_t* d, const ui_edit_range_t* range); bool (*copy_text)(ui_edit_doc_t* d, const ui_edit_range_t* range, ui_edit_text_t* text); // retrieves range into string int32_t (*utf8bytes)(const ui_edit_doc_t* d, const ui_edit_range_t* range); // utf8 must be at least ui_edit_doc.utf8bytes() void (*copy)(ui_edit_doc_t* d, const ui_edit_range_t* range, char* utf8, int32_t bytes); // undo() and push reverse into redo stack bool (*undo)(ui_edit_doc_t* d); // false if there is nothing to redo // redo() and push reverse into undo stack bool (*redo)(ui_edit_doc_t* d); // false if there is nothing to undo bool (*subscribe)(ui_edit_doc_t* d, ui_edit_notify_t* notify); void (*unsubscribe)(ui_edit_doc_t* d, ui_edit_notify_t* notify); void (*dispose_to_do)(ui_edit_to_do_t* to_do); void (*dispose)(ui_edit_doc_t* d); void (*test)(void); } ui_edit_doc_if; extern ui_edit_doc_if ui_edit_doc; typedef struct ui_edit_range_if { int (*compare)(const ui_edit_pg_t pg1, const ui_edit_pg_t pg2); ui_edit_range_t (*order)(const ui_edit_range_t r); bool (*is_valid)(const ui_edit_range_t r); bool (*is_empty)(const ui_edit_range_t r); uint64_t (*uint64)(const ui_edit_pg_t pg); // (p << 32 | g) ui_edit_pg_t (*pg)(uint64_t ui64); // p: (ui64 >> 32) g: (int32_t)ui64 bool (*inside)(const ui_edit_text_t* t, const ui_edit_range_t r); ui_edit_range_t (*intersect)(const ui_edit_range_t r1, const ui_edit_range_t r2); const ui_edit_range_t* const invalid_range; // {{-1,-1},{-1,-1}} } ui_edit_range_if; extern ui_edit_range_if ui_edit_range; typedef struct ui_edit_text_if { bool (*init)(ui_edit_text_t* t, const char* utf, int32_t b, bool heap); int32_t (*bytes)(const ui_edit_text_t* t, const ui_edit_range_t* r); // end() last paragraph, last glyph in text ui_edit_pg_t (*end)(const ui_edit_text_t* t); ui_edit_range_t (*end_range)(const ui_edit_text_t* t); ui_edit_range_t (*all_on_null)(const ui_edit_text_t* t, const ui_edit_range_t* r); ui_edit_range_t (*ordered)(const ui_edit_text_t* t, const ui_edit_range_t* r); bool (*dup)(ui_edit_text_t* t, const ui_edit_text_t* s); bool (*equal)(const ui_edit_text_t* t1, const ui_edit_text_t* t2); bool (*copy_text)(const ui_edit_text_t* t, const ui_edit_range_t* range, ui_edit_text_t* to); void (*copy)(const ui_edit_text_t* t, const ui_edit_range_t* range, char* to, int32_t bytes); bool (*replace)(ui_edit_text_t* t, const ui_edit_range_t* r, const ui_edit_text_t* text, ui_edit_to_do_t* undo_or_null); bool (*replace_utf8)(ui_edit_text_t* t, const ui_edit_range_t* r, const char* utf8, int32_t bytes, ui_edit_to_do_t* undo_or_null); void (*dispose)(ui_edit_text_t* t); } ui_edit_text_if; extern ui_edit_text_if ui_edit_text; typedef struct rt_begin_packed ui_edit_str_s { char* u; // always correct utf8 bytes not zero terminated(!) sequence // s.g2b[s.g + 1] glyph to byte position inside s.u[] // s.g2b[0] == 0, s.g2b[s.glyphs] == s.bytes int32_t* g2b; // g2b_0 or heap allocated glyphs to bytes indices int32_t b; // number of bytes int32_t c; // when capacity is zero .u is not heap allocated int32_t g; // number of glyphs } rt_end_packed ui_edit_str_t; typedef struct ui_edit_str_if { bool (*init)(ui_edit_str_t* s, const char* utf8, int32_t bytes, bool heap); void (*swap)(ui_edit_str_t* s1, ui_edit_str_t* s2); int32_t (*gp_to_bp)(const char* s, int32_t bytes, int32_t gp); // or -1 int32_t (*bytes)(ui_edit_str_t* s, int32_t from, int32_t to); // glyphs bool (*expand)(ui_edit_str_t* s, int32_t capacity); // reallocate void (*shrink)(ui_edit_str_t* s); // get rid of extra heap memory bool (*replace)(ui_edit_str_t* s, int32_t from, int32_t to, // glyphs const char* utf8, int32_t bytes); // [from..to[ exclusive bool (*is_zwj)(uint32_t utf32); // zero width joiner bool (*is_letter)(uint32_t utf32); // in European Alphabets bool (*is_digit)(uint32_t utf32); bool (*is_symbol)(uint32_t utf32); bool (*is_alphanumeric)(uint32_t utf32); bool (*is_blank)(uint32_t utf32); // white space bool (*is_punctuation)(uint32_t utf32); bool (*is_combining)(uint32_t utf32); bool (*is_spacing)(uint32_t utf32); // spacing modifiers bool (*is_cjk_or_emoji)(uint32_t utf32); bool (*can_break)(uint32_t cp1, uint32_t cp2); void (*test)(void); void (*free)(ui_edit_str_t* s); const ui_edit_str_t* const empty; } ui_edit_str_if; extern ui_edit_str_if ui_edit_str; /* For caller convenience the bytes parameter in all calls can be set to -1 for zero terminated utf8 strings which results in treating strlen(utf8) as number of bytes. ui_edit_str.init() initializes not zero terminated utf8 string that may be allocated on the heap or point out to an outside memory location that should have longer lifetime and will be treated as read only. init() may return false if heap.alloc() returns null or the utf8 bytes sequence is invalid. s.b is number of bytes in the initialized string; s.c is set to heap allocated capacity is set to zero for strings that are not allocated on the heap; s.g is number of the utf8 glyphs (aka Unicode codepoints) in the string; s.g2b[] is an array of s.g + 1 integers that maps glyph positions to byte positions in the utf8 string. The last element is number of bytes in the s.u memory. Called must zero out the string struct before calling init(). ui_edit_str.bytes() returns number of bytes in utf8 string in the exclusive range [from..to[ between string glyphs. ui_edit_str.replace() replaces utf8 string in the exclusive range [from..to[ with the new utf8 string. The new string may be longer or shorter than the replaced string. The function returns false if the new string is invalid utf8 sequence or heap allocation fails. The called must ensure that the range [from..to[ is valid, failure to do so is a fatal error. ui_edit_str.replace() moves string content to the heap. ui_edit_str.free() deallocates all heap allocated memory and zero out string struct. It is incorrect to call free() on the string that was not initialized or already freed. All ui_edit_str_t keep "precise" number of utf8 bytes. Caller may allocate extra byte and set it to 0x00 after retrieving and copying data from ui_edit_str if the string content is intended to be used by any other API that expects zero terminated strings. */ rt_end_c // ______________________________ ui_edit_view.h ______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ rt_begin_c // important ui_edit_view_t will refuse to layout into a box smaller than // width 3 x fm->em.w height 1 x fm->em.h typedef struct ui_edit_view_s ui_edit_view_t; typedef struct ui_edit_str_s ui_edit_str_t; typedef struct ui_edit_doc_s ui_edit_doc_t; typedef struct ui_edit_notify_s ui_edit_notify_t; typedef struct ui_edit_to_do_s ui_edit_to_do_t; typedef struct ui_edit_pr_s { // page/run coordinates int32_t pn; // paragraph number int32_t rn; // run number inside paragraph } ui_edit_pr_t; typedef struct ui_edit_run_s { int32_t bp; // position in bytes since start of the paragraph int32_t gp; // position in glyphs since start of the paragraph int32_t bytes; // number of bytes in this `run` int32_t glyphs; // number of glyphs in this `run` int32_t pixels; // width in pixels } ui_edit_run_t; // ui_edit_paragraph_t.initially text will point to readonly memory // with .allocated == 0; as text is modified it is copied to // heap and reallocated there. typedef struct ui_edit_paragraph_s { // "paragraph" view consists of wrapped runs int32_t runs; // number of runs in this paragraph ui_edit_run_t* run; // heap allocated array[runs] } ui_edit_paragraph_t; typedef struct ui_edit_notify_view_s { ui_edit_notify_t notify; void* that; // specific for listener uintptr_t data; // before -> after listener data } ui_edit_notify_view_t; typedef struct ui_edit_view_s { union { ui_view_t view; struct ui_view_s; }; ui_edit_doc_t* doc; // document ui_edit_notify_view_t listener; ui_edit_range_t selection; // "from" selection[0] "to" selection[1] ui_point_t caret; // (-1, -1) off int32_t caret_width; // in pixels ui_edit_pr_t scroll; // left top corner paragraph/run coordinates int32_t last_x; // last_x for up/down caret movement ui_ltrb_t inside; // inside insets space struct { int32_t w; // inside.right - inside.left int32_t h; // inside.bottom - inside.top int32_t buttons; // bit 0 and bit 1 for LEFT and RIGHT mouse buttons down } edit; // number of fully (not partially clipped) visible `runs' from top to bottom: int32_t visible_runs; // TODO: remove focused because it is the same as caret != (-1, -1) bool focused; // is focused and created caret bool ro; // Read Only bool sle; // Single Line Edit bool hide_word_wrap; // do not paint word wrap int32_t shown; // debug: caret show/hide counter 0|1 // paragraphs memory: ui_edit_paragraph_t* para; // para[e->doc->text.np] } ui_edit_view_t; typedef struct ui_edit_view_if { void (*init)(ui_edit_view_t* e, ui_edit_doc_t* d); void (*set_font)(ui_edit_view_t* e, ui_fm_t* fm); // see notes below (*) void (*move)(ui_edit_view_t* e, ui_edit_pg_t pg); // move caret clear selection // replace selected text. If bytes < 0 text is treated as zero terminated void (*replace)(ui_edit_view_t* e, const char* text, int32_t bytes); // call save(e, null, &bytes) to retrieve number of utf8 // bytes required to save whole text including 0x00 terminating bytes errno_t (*save)(ui_edit_view_t* e, char* text, int32_t* bytes); void (*copy)(ui_edit_view_t* e); // to clipboard void (*cut)(ui_edit_view_t* e); // to clipboard // replace selected text with content of clipboard: void (*paste)(ui_edit_view_t* e); // from clipboard void (*select_all)(ui_edit_view_t* e); // select whole text void (*erase)(ui_edit_view_t* e); // delete selected text // keyboard actions dispatcher: void (*key_down)(ui_edit_view_t* e); void (*key_up)(ui_edit_view_t* e); void (*key_left)(ui_edit_view_t* e); void (*key_right)(ui_edit_view_t* e); void (*key_page_up)(ui_edit_view_t* e); void (*key_page_down)(ui_edit_view_t* e); void (*key_home)(ui_edit_view_t* e); void (*key_end)(ui_edit_view_t* e); void (*key_delete)(ui_edit_view_t* e); void (*key_backspace)(ui_edit_view_t* e); void (*key_enter)(ui_edit_view_t* e); // called when ENTER keyboard key is pressed in single line mode void (*enter)(ui_edit_view_t* e); // fuzzer test: void (*fuzz)(ui_edit_view_t* e); // start/stop fuzzing test void (*dispose)(ui_edit_view_t* e); } ui_edit_view_if; extern ui_edit_view_if ui_edit_view; /* Notes: set_font() neither edit.view.font = font nor measure()/layout() functions do NOT dispose paragraphs layout unless geometry changed because it is quite expensive operation. But choosing different font on the fly needs to re-layout all paragraphs. Thus caller needs to set font via this function instead which also requests edit UI element re-layout. .ro readonly edit->ro is used to control readonly mode. If edit control is readonly its appearance does not change but it refuses to accept any changes to the rendered text. .wb wordbreak this attribute was removed as poor UX human experience along with single line scroll editing. See note below about .sle. .sle single line edit control. Edit UI element does NOT support horizontal scroll and breaking words semantics as it is poor UX human experience. This is not how humans (apart of software developers) edit text. If content of the edit UI element is wider than the bounding box width the content is broken on word boundaries and vertical scrolling semantics is supported. Layouts containing edit control of the single line height are strongly encouraged to enlarge edit control layout vertically on as needed basis similar to Google Search Box behavior change implemented in 2023. If multiline is set to true by the callers code the edit UI layout snaps text to the top of x,y,w,h box otherwise the vertical space is distributed evenly between single line of text and top bottom margins. IMPORTANT: SLE resizes itself vertically to accommodate for input that is too wide. If caller wants to limit vertical space it will need to hook .measure() function of SLE and do the math there. */ /* For caller convenience the bytes parameter in all calls can be set to -1 for zero terminated utf8 strings which results in treating strlen(utf8) as number of bytes. ui_edit_str.init() initializes not zero terminated utf8 string that may be allocated on the heap or point out to an outside memory location that should have longer lifetime and will be treated as read only. init() may return false if heap.alloc() returns null or the utf8 bytes sequence is invalid. s.b is number of bytes in the initialized string; s.c is set to heap allocated capacity is set to zero for strings that are not allocated on the heap; s.g is number of the utf8 glyphs (aka Unicode codepoints) in the string; s.g2b[] is an array of s.g + 1 integers that maps glyph positions to byte positions in the utf8 string. The last element is number of bytes in the s.u memory. Called must zero out the string struct before calling init(). ui_edit_str.bytes() returns number of bytes in utf8 string in the exclusive range [from..to[ between string glyphs. ui_edit_str.replace() replaces utf8 string in the exclusive range [from..to[ with the new utf8 string. The new string may be longer or shorter than the replaced string. The function returns false if the new string is invalid utf8 sequence or heap allocation fails. The called must ensure that the range [from..to[ is valid, failure to do so is a fatal error. ui_edit_str.replace() moves string content to the heap. ui_edit_str.free() deallocates all heap allocated memory and zero out string struct. It is incorrect to call free() on the string that was not initialized or already freed. All ui_edit_str_t keep "precise" number of utf8 bytes. Caller may allocate extra byte and set it to 0x00 after retrieving and copying data from ui_edit_str if the string content is intended to be used by any other API that expects zero terminated strings. */ rt_end_c // ________________________________ ui_label.h ________________________________ rt_begin_c typedef ui_view_t ui_label_t; void ui_view_init_label(ui_view_t* v); // label insets and padding left/right are intentionally // smaller than button/slider/toggle controls #define ui_label(min_width_em, s) { \ .type = ui_view_label, .init = ui_view_init_label, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } // text with "&" keyboard shortcuts: void ui_label_init(ui_label_t* t, fp32_t min_w_em, const char* format, ...); void ui_label_init_va(ui_label_t* t, fp32_t min_w_em, const char* format, va_list va); // use this macro for initialization: // ui_label_t label = ui_label(min_width_em, s); // or: // label = (ui_label_t)ui_label(min_width_em, s); // which is subtle C difference of constant and // variable initialization and I did not find universal way rt_end_c // _______________________________ ui_button.h ________________________________ rt_begin_c typedef ui_view_t ui_button_t; void ui_view_init_button(ui_view_t* v); void ui_button_init(ui_button_t* b, const char* label, fp32_t min_width_em, void (*callback)(ui_button_t* b)); // ui_button_clicked can only be used on static button variables #define ui_button_clicked(name, s, min_width_em, ...) \ static void name ## _clicked(ui_button_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_button_t name = { \ .type = ui_view_button, \ .init = ui_view_init_button, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = name ## _clicked, \ .color_id = ui_color_id_button_text, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } #define ui_button(s, min_width_em, clicked) { \ .type = ui_view_button, \ .init = ui_view_init_button, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = clicked, \ .color_id = ui_color_id_button_text, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } // usage: // // ui_button_clicked(button, "&Button", 7.0, { // if (button->state.pressed) { // // do something on click that happens on release mouse button // } // }) // // or: // // static void button_flipped(ui_button_t* b) { // swear(b->flip == true); // 2 state button, clicked on mouse press button // if (b->state.pressed) { // // show something: // } else { // // show something else: // } // } // // ui_button_t button = ui_button(7.0, "&Button", button_flipped); // // or // // ui_button_t button = ui_view)button(button); // ui_view.set_text(button.text, "&Button"); // button.min_w_em = 7.0; // button.callback = button_flipped; // // Note: // ui_button_clicked(button, "&Button", 7.0, { // button->state.pressed = !button->state.pressed; // // is similar to: button.flip = true but it leads thru // // multiple button paint and click happens on mouse button // // release not press // } rt_end_c // ________________________________ ui_image.h ________________________________ rt_begin_c // "image view" // To enable zoom/pan make view focusable: // iv.focusable = true; // Field .image may have .pixels pointer and .bitmap == null. // If this is the case the direct pixels transfer to the // device is used. RGBA bitmaps must be allocated on the // device otherwise ui_gdi.rgbx() call is used and alpha // is ignored. typedef struct ui_image_s ui_image_t; typedef struct ui_image_s { union { ui_view_t view; struct ui_view_s; }; ui_bitmap_t image; // view does NOT own or dispose image->bitmap fp64_t alpha; // for rgba images // actual scale() is: z = 2 ^ (zn - 1) / 2 ^ (zd - 1) int32_t zoom; // 0..8 // 0=16:1 1=8:1 2=4:1 3=2:1 4=1:1 5=1:2 6=1:4 7=1:8 8=1:16 int32_t zn; // zoom nominator (1, 2, 3, ...) int32_t zd; // zoom denominator (1, 2, 3, ...) fp64_t sx; // shift x [0..1.0] in view coordinates fp64_t sy; // shift y [0..1.0] struct { // only visible when focused ui_view_t bar; // ui_view(span) {zoom in, zoom 1:1, zoom out, help} ui_button_t copy; // copy image to clipboard ui_button_t zoom_in; ui_button_t zoom_1t1; // 1:1 ui_button_t zoom_out; ui_button_t fit; ui_button_t fill; ui_button_t help; ui_label_t ratio; } tool; ui_point_t drag_start; fp64_t when; // to hide toolbar bool fit; // best fit into view bool fill; // fill entire view // fit and fill cannot be true at the same time // when fit: false and fill: false the zoom ratio is in effect } ui_image_t; typedef struct ui_image_if { void (*init)(ui_image_t* iv); void (*init_with)(ui_image_t* iv, const uint8_t* pixels, int32_t width, int32_t height, int32_t bpp, int32_t stride); // ration can only be: 16:1 8:1 4:1 2:1 1:1 1:2 1:4 1:8 1:16 // but ignored if .fit or .fill is true void (*ratio)(ui_image_t* iv, int32_t nominator, int32_t denominator); fp64_t (*scale)(ui_image_t* iv); // 2 ^ (zn - 1) / 2 ^ (zd - 1) ui_rect_t (*position)(ui_image_t* iv); } ui_image_if; extern ui_image_if ui_image; rt_end_c // ________________________________ ui_midi.h _________________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include #include #ifdef __cplusplus extern "C" { #endif typedef struct ui_midi_s ui_midi_t; typedef struct ui_midi_s { uint8_t data[16 * 8]; // opaque implementation data // must return 0 if successful or error otherwise: int64_t (*notify)(ui_midi_t* midi, int64_t flags); } ui_midi_t; typedef struct { // flags bitset: int32_t const success; // when the clip is done playing int32_t const failure; // on error playing media int32_t const aborted; // on stop() call int32_t const superseded; // midi has it's own section of legacy error messages void (*error)(errno_t r, char* s, int32_t count); errno_t (*open)(ui_midi_t* midi, const char* filename); errno_t (*play)(ui_midi_t* midi); errno_t (*rewind)(ui_midi_t* midi); errno_t (*stop)(ui_midi_t* midi); errno_t (*get_volume)(ui_midi_t* midi, fp64_t *volume); errno_t (*set_volume)(ui_midi_t* midi, fp64_t volume); bool (*is_open)(ui_midi_t* midi); bool (*is_playing)(ui_midi_t* midi); void (*close)(ui_midi_t* midi); } ui_midi_if; extern ui_midi_if ui_midi; /* success: "The conditions initiating the callback function have been met." I guess meaning media is done playing... failure: "A device error occurred while the device was executing the command." aborted: "The device received a command that prevented the current conditions for initiating the callback function from being met. If a new command interrupts the current command and it also requests notification, the device sends this message only and not `superseded`". I guess meaning media is stopped playing... superseded: "The device received another command with the "notify" flag set and the current conditions for initiating the callback function have been superseded." */ #ifdef __cplusplus } #endif // _______________________________ ui_slider.h ________________________________ rt_begin_c typedef struct ui_slider_s ui_slider_t; typedef struct ui_slider_s { union { ui_view_t view; struct ui_view_s; }; int32_t step; fp64_t time; // time last button was pressed ui_wh_t wh; // text measurement (special case for %0*d) ui_button_t inc; // can be hidden ui_button_t dec; // can be hidden int32_t value; // for ui_slider_t range slider control int32_t value_min; int32_t value_max; // style: bool notched; // true if marked with a notches and has a thumb } ui_slider_t; void ui_view_init_slider(ui_view_t* v); void ui_slider_init(ui_slider_t* r, const char* label, fp32_t min_w_em, int32_t value_min, int32_t value_max, void (*callback)(ui_view_t* r)); // ui_slider_changed can only be used on static slider variables #define ui_slider_changed(name, s, min_width_em, mn, mx, fmt, ...) \ static void name ## _changed(ui_slider_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_slider_t name = { \ .view = { \ .type = ui_view_slider, \ .init = ui_view_init_slider, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .format = fmt, \ .callback = name ## _changed, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ }, \ .value_min = mn, .value_max = mx, .value = mn, \ } #define ui_slider(s, min_width_em, mn, mx, fmt, changed) { \ .view = { \ .type = ui_view_slider, \ .init = ui_view_init_slider, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = changed, \ .format = fmt, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = ui_view_i_lr, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ }, \ .value_min = mn, .value_max = mx, .value = mn, \ } rt_end_c // ________________________________ ui_theme.h ________________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ rt_begin_c enum { ui_theme_app_mode_default = 0, ui_theme_app_mode_allow_dark = 1, ui_theme_app_mode_force_dark = 2, ui_theme_app_mode_force_light = 3 }; typedef struct { bool (*is_app_dark)(void); bool (*is_system_dark)(void); bool (*are_apps_dark)(void); void (*set_preferred_app_mode)(int32_t mode); void (*flush_menu_themes)(void); void (*allow_dark_mode_for_app)(bool allow); void (*allow_dark_mode_for_window)(bool allow); void (*refresh)(void); void (*test)(void); } ui_theme_if; extern ui_theme_if ui_theme; rt_end_c // _______________________________ ui_toggle.h ________________________________ rt_begin_c typedef ui_view_t ui_toggle_t; // label may contain "___" which will be replaced with "On" / "Off" void ui_toggle_init(ui_toggle_t* b, const char* label, fp32_t ems, void (*callback)(ui_toggle_t* b)); void ui_view_init_toggle(ui_view_t* v); // ui_toggle_on_off can only be used on static toggle variables #define ui_toggle_on_off(name, s, min_width_em, ...) \ static void name ## _on_off(ui_toggle_t* name) { \ (void)name; /* no warning if unused */ \ { __VA_ARGS__ } \ } \ static \ ui_toggle_t name = { \ .type = ui_view_toggle, \ .init = ui_view_init_toggle, \ .fm = &ui_app.fm.prop.normal, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .p.text = s, \ .callback = name ## _on_off, \ .insets = { \ .left = 1.75f, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } #define ui_toggle(s, min_width_em, on_off) { \ .type = ui_view_toggle, \ .init = ui_view_init_toggle, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = on_off, \ .min_w_em = min_width_em, .min_h_em = 1.25f, \ .insets = { \ .left = 1.75f, .top = ui_view_i_tb, \ .right = ui_view_i_lr, .bottom = ui_view_i_tb \ }, \ .padding = { \ .left = ui_view_p_lr, .top = ui_view_p_tb, \ .right = ui_view_p_lr, .bottom = ui_view_p_tb, \ } \ } rt_end_c // _________________________________ ui_mbx.h _________________________________ rt_begin_c // Options like: // "Yes"|"No"|"Abort"|"Retry"|"Ignore"|"Cancel"|"Try"|"Continue" // maximum number of choices presentable to human is 4. typedef struct { union { ui_view_t view; struct ui_view_s; }; ui_label_t label; ui_button_t button[4]; int32_t option; // -1 or option chosen by user const char** options; } ui_mbx_t; void ui_view_init_mbx(ui_view_t* v); void ui_mbx_init(ui_mbx_t* mx, const char* option[], const char* format, ...); // ui_mbx_on_choice can only be used on static mbx variables #define ui_mbx_chosen(name, s, code, ...) \ \ static char* name ## _options[] = { __VA_ARGS__, null }; \ \ static void name ## _chosen(ui_mbx_t* m, int32_t option) { \ (void)m; (void)option; /* no warnings if unused */ \ code \ } \ static \ ui_mbx_t name = { \ .view = { \ .type = ui_view_mbx, \ .init = ui_view_init_mbx, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = name ## _chosen, \ .padding = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 }, \ .insets = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 } \ }, \ .options = name ## _options \ } #define ui_mbx(s, chosen, ...) { \ .view = { \ .type = ui_view_mbx, .init = ui_view_init_mbx, \ .fm = &ui_app.fm.prop.normal, \ .p.text = s, \ .callback = chosen, \ .padding = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 }, \ .insets = { .left = 0.125, .top = 0.25, \ .right = 0.125, .bottom = 0.25 } \ }, \ .options = (const char*[]){ __VA_ARGS__, null }, \ } rt_end_c // _______________________________ ui_caption.h _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ rt_begin_c typedef struct ui_caption_s { ui_view_t view; // caption`s children: ui_button_t icon; ui_label_t title; ui_view_t spacer; ui_button_t menu; // use: ui_caption.button_menu.cb := your callback ui_button_t mode; // switch between dark/light mode ui_button_t mini; ui_button_t maxi; ui_button_t full; ui_button_t quit; } ui_caption_t; extern ui_caption_t ui_caption; rt_end_c // _________________________________ ui_app.h _________________________________ rt_begin_c // link.exe /SUBSYSTEM:WINDOWS single window application typedef struct ui_app_message_handler_s ui_app_message_handler_t; typedef struct ui_app_message_handler_s { void* that; ui_app_message_handler_t* next; bool (*callback)(ui_app_message_handler_t* handler, int32_t m, int64_t wp, int64_t lp, int64_t* rt); } ui_app_message_handler_t; typedef struct ui_dpi_s { // max(dpi_x, dpi_y) int32_t system; // system dpi int32_t process; // process dpi // 15" diagonal monitor 3840x2160 175% scaled // monitor dpi effective 168, angular 248 raw 284 int32_t monitor_effective; // effective with regard of user scaling int32_t monitor_raw; // with regard of physical screen size int32_t monitor_angular; // diagonal raw int32_t monitor_max; // maximum of effective,raw,angular int32_t window; // main window dpi } ui_dpi_t; // in inches (because monitors customary are) // it is not in points (1/72 inch) like font size // because it is awkward to express large area // size in typography measurements. typedef struct ui_window_sizing_s { fp32_t ini_w; // initial window width in inches fp32_t ini_h; // 0,0 means set to min_w, min_h fp32_t min_w; // minimum window width in inches fp32_t min_h; // 0,0 means - do not care use content size fp32_t max_w; // maximum window width in inches fp32_t max_h; // 0,0 means as big as user wants // "sizing" "estimate or measure something's dimensions." // initial window sizing only used on the first invocation // actual user sizing is stored in the configuration and used // on all launches except the very first. } ui_window_sizing_t; typedef struct ui_fms_s { // when font handles are re-created on system scaling change // metrics "em" and font geometry filled ui_fm_t normal; // regular UI font ~ 11-12pt ui_fm_t tiny; // small UI font ~ 8pt ui_fm_t title; // Largest Title font ui_fm_t rubric; // Subtitle font ui_fm_t H1; // bolder header font ui_fm_t H2; ui_fm_t H3; } ui_fms_t; typedef struct { // TODO: split to ui_app_t and ui_app_if, move data after methods // implemented by client: const char* class_name; // called before creating main window void (*init)(void); // called instead of init() for console apps and when .no_ui=true int (*main)(void); // class_name and init must be set before main() void (*opened)(void); // window has been created and shown void (*every_sec)(void); // if not null called ~ once a second void (*every_100ms)(void); // called ~10 times per second // .can_close() called before window is closed and can be // used in a meaning of .closing() bool (*can_close)(void); // window can be closed void (*closed)(void); // window has been closed void (*fini)(void); // called before WinMain() return // must be filled by application: const char* title; ui_window_sizing_t const window_sizing; // TODO: struct {} visibility; // see: ui.visibility.* int32_t visibility; // initial window_visibility state int32_t last_visibility; // last window_visibility state from last run int32_t startup_visibility; // window_visibility from parent process ui_canvas_t canvas; // set by message.paint // ui flags: bool is_full_screen; bool no_ui; // do not create application window at all bool dark_mode; // forced dark mode for the whole application bool light_mode; // forced light mode for the whole application bool no_decor; // window w/o title bar, min/max close buttons bool no_min; // window w/o minimize button on title bar and sys menu bool no_max; // window w/o maximize button on title bar bool no_size; // window w/o maximize button on title bar bool no_clip; // allows to resize window above hosting monitor size bool hide_on_minimize; // like task manager minimize means hide ui_window_t window; ui_icon_t icon; // may be null uint64_t tid; // main thread id int32_t exit_code; // application exit code ui_dpi_t dpi; ui_rect_t wrc; // window rectangle including non-client area ui_rect_t crc; // client rectangle ui_rect_t mrc; // monitor rectangle ui_rect_t prc; // previously invalidated paint rectangle inside crc ui_rect_t work_area; // current monitor work area int32_t caption_height; // caption height ui_wh_t border; // frame border size // not to call rt_clock.seconds() too often: fp64_t now; // ssb "seconds since boot" updated on each message ui_view_t* root; // show_window() changes ui.hidden ui_view_t* content; ui_view_t* caption; ui_view_t* focus; // does not affect message routing struct { // font metrics and handles ui_fms_t prop; // proportional fonts ui_fms_t mono; // monospaced fonts } fm; // TODO: struct {} keyboard // keyboard state now: bool alt; bool ctrl; bool shift; // TODO: struct {} mouse // mouse buttons state bool mouse_swapped; bool mouse_left; // left or if buttons are swapped - right button pressed bool mouse_middle; // rarely useful bool mouse_right; // context button pressed ui_point_t mouse; // mouse/touchpad pointer ui_cursor_t cursor; // current cursor struct { ui_cursor_t arrow; ui_cursor_t wait; ui_cursor_t ibeam; ui_cursor_t size_nwse; // north west - south east ui_cursor_t size_nesw; // north east - south west ui_cursor_t size_we; // west - east ui_cursor_t size_ns; // north - south ui_cursor_t size_all; // north - south } cursors; struct { // animated_groot state ui_view_t* view; ui_view_t* focused; // focused view before animated_groot started int32_t step; fp64_t time; // closing time or zero int32_t x; // (x,y) for tooltip (-1,y) for toast int32_t y; // screen coordinates for tooltip } animating; ui_app_message_handler_t* handlers; // post(..., delay_in_seconds, ...) can be scheduled from any thread executed // on UI thread void (*post)(rt_work_t* work); // work.when == 0 meaning ASAP void (*request_redraw)(void); // very fast <2 microseconds void (*draw)(void); // paint window now - bad idea do not use // inch to pixels and reverse translation via ui_app.dpi.window fp32_t (*px2in)(int32_t pixels); int32_t (*in2px)(fp32_t inches); errno_t (*set_layered_window)(ui_color_t color, float alpha); bool (*is_active)(void); // is application window active bool (*is_minimized)(void); bool (*is_maximized)(void); bool (*focused)(void); // application window has keyboard focus void (*activate)(void); // request application window activation void (*set_title)(const char* title); void (*capture_mouse)(bool on); // capture mouse global input on/of void (*move_and_resize)(const ui_rect_t* rc); void (*bring_to_foreground)(void); // not necessary topmost void (*make_topmost)(void); // in foreground hierarchy of windows void (*request_focus)(void); // request application window keyboard focus void (*bring_to_front)(void); // activate() + bring_to_foreground() + // make_topmost() + request_focus() // measure and layout: void (*request_layout)(void); // requests layout on UI tree before paint() void (*invalidate)(const ui_rect_t* rc); void (*full_screen)(bool on); void (*set_cursor)(ui_cursor_t c); void (*close)(void); // attempts to close (can_close() permitting) // forced quit() even if can_close() returns false void (*quit)(int32_t ec); // ui_app.exit_code = ec; PostQuitMessage(ec); ui_timer_t (*set_timer)(uintptr_t id, int32_t milliseconds); // see notes void (*kill_timer)(ui_timer_t id); void (*show_window)(int32_t show); // see show_window enum void (*show_toast)(ui_view_t* toast, fp64_t seconds); // toast(null) to cancel void (*show_hint)(ui_view_t* tooltip, int32_t x, int32_t y, fp64_t seconds); void (*toast_va)(fp64_t seconds, const char* format, va_list va); void (*toast)(fp64_t seconds, const char* format, ...); // caret calls must be balanced by caller void (*create_caret)(int32_t w, int32_t h); void (*show_caret)(void); void (*move_caret)(int32_t x, int32_t y); void (*hide_caret)(void); void (*destroy_caret)(void); // beep sounds: void (*beep)(int32_t kind); // registry interface: void (*data_save)(const char* name, const void* data, int32_t bytes); int32_t (*data_size)(const char* name); int32_t (*data_load)(const char* name, void* data, int32_t bytes); // returns bytes read // filename dialog: // const char* filter[] = // {"Text Files", ".txt;.doc;.ini", // "Executables", ".exe", // "All Files", "*"}; // const char* fn = ui_app.open_filename("C:\\", filter, rt_countof(filter)); const char* (*open_file)(const char* folder, const char* filter[], int32_t n); bool (*is_stdout_redirected)(void); bool (*is_console_visible)(void); int (*console_attach)(void); // attempts to attach to parent terminal int (*console_create)(void); // allocates new console void (*console_show)(bool b); // stats: int32_t paint_count; // number of paint calls fp64_t paint_time; // last paint duration in seconds fp64_t paint_max; // max of last 128 paint fp64_t paint_avg; // EMA of last 128 paints fp64_t paint_fps; // EMA of last 128 paints fp64_t paint_last; // rt_clock.seconds() of last paint fp64_t paint_dt_min; // minimum time between 2 paints } ui_app_t; extern ui_app_t ui_app; rt_end_c rt_begin_c // https://en.wikipedia.org/wiki/Fuzzing // aka "Monkey" testing typedef struct ui_fuzzing_s { rt_work_t base; const char* utf8; // .character(utf8) int32_t key; // .key_pressed(key)/.key_released(key) ui_point_t* pt; // .move_move() // key_press and character bool alt; bool ctrl; bool shift; // mouse modifiers bool left; // tap() buttons: bool right; bool double_tap; bool long_press; // custom int32_t op; void* data; } ui_fuzzing_t; typedef struct ui_fuzzing_if { void (*start)(uint32_t seed); bool (*is_running)(void); bool (*from_inside)(void); // true if called originated inside fuzzing void (*next_random)(ui_fuzzing_t* f); // called if `next` is null void (*dispatch)(ui_fuzzing_t* f); // dispatch work // next() called instead of random if not null void (*next)(ui_fuzzing_t* f); // custom() called instead of dispatch() if not null void (*custom)(ui_fuzzing_t* f); void (*stop)(void); } ui_fuzzing_if; extern ui_fuzzing_if ui_fuzzing; rt_end_c #endif // ui_definition #ifdef ui_implementation // _________________________________ ui_app.c _________________________________ #include "rt/rt.h" #include "rt/rt_win32.h" #pragma push_macro("ui_app_window") #pragma push_macro("ui_app_canvas") static bool ui_app_trace_utf16_keyboard_input; #define ui_app_window() ((HWND)ui_app.window) #define ui_app_canvas() ((HDC)ui_app.canvas) static WNDCLASSW ui_app_wc; // window class static NONCLIENTMETRICSW ui_app_ncm = { sizeof(NONCLIENTMETRICSW) }; static MONITORINFO ui_app_mi = {sizeof(MONITORINFO)}; static rt_event_t ui_app_event_quit; static rt_event_t ui_app_event_invalidate; static rt_event_t ui_app_wt; // waitable timer; static rt_work_queue_t ui_app_queue; static uintptr_t ui_app_timer_1s_id; static uintptr_t ui_app_timer_100ms_id; static bool ui_app_layout_dirty; // call layout() before paint static char ui_app_decoded_pressed[16]; // utf8 of last decoded pressed key static char ui_app_decoded_released[16]; // utf8 of last decoded released key static uint16_t ui_app_high_surrogate; typedef void (*ui_app_animate_function_t)(int32_t step); static struct { ui_app_animate_function_t f; int32_t count; int32_t step; ui_timer_t timer; } ui_app_animate; // Animation timer is Windows minimum of 10ms, but in reality the timer // messages are far from isochronous and more likely to arrive at 16 or // 32ms intervals and can be delayed. static void ui_app_post_message(int32_t m, int64_t wp, int64_t lp) { rt_fatal_win32err(PostMessageA(ui_app_window(), (UINT)m, (WPARAM)wp, (LPARAM)lp)); } static void ui_app_update_wt_timeout(void) { fp64_t next_due_at = -1.0; rt_atomics.spinlock_acquire(&ui_app_queue.lock); if (ui_app_queue.head != null) { next_due_at = ui_app_queue.head->when; } rt_atomics.spinlock_release(&ui_app_queue.lock); if (next_due_at >= 0) { static fp64_t last_next_due_at; fp64_t dt = next_due_at - rt_clock.seconds(); if (dt <= 0) { ui_app_post_message(WM_NULL, 0, 0); } else if (last_next_due_at != next_due_at) { // Negative values indicate relative time in 100ns intervals LARGE_INTEGER rt = {0}; // relative negative time rt.QuadPart = (LONGLONG)(-dt * 1.0E+7); rt_swear(rt.QuadPart < 0, "dt: %.6f %lld", dt, rt.QuadPart); rt_fatal_win32err( SetWaitableTimer(ui_app_wt, &rt, 0, null, null, 0) ); } last_next_due_at = next_due_at; } } static void ui_app_post(rt_work_t* w) { if (w->queue == null) { w->queue = &ui_app_queue; } // work item can be reused but only with the same queue rt_assert(w->queue == &ui_app_queue); rt_work_queue.post(w); ui_app_update_wt_timeout(); } static void ui_app_alarm_thread(void* rt_unused(p)) { rt_thread.realtime(); rt_thread.name("ui_app.alarm"); for (;;) { rt_event_t es[] = { ui_app_wt, ui_app_event_quit }; int32_t ix = rt_event.wait_any(rt_countof(es), es); if (ix == 0) { ui_app_post_message(WM_NULL, 0, 0); } else { break; } } } // InvalidateRect() may wait for up to 30 milliseconds // which is unacceptable for video drawing at monitor // refresh rate static void ui_app_redraw_thread(void* rt_unused(p)) { rt_thread.realtime(); rt_thread.name("ui_app.redraw"); for (;;) { rt_event_t es[] = { ui_app_event_invalidate, ui_app_event_quit }; int32_t ix = rt_event.wait_any(rt_countof(es), es); if (ix == 0) { if (ui_app_window() != null) { InvalidateRect(ui_app_window(), null, false); } } else { break; } } } // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown static void ui_app_alt_ctrl_shift(bool down, int64_t key) { if (key == VK_MENU) { ui_app.alt = down; } if (key == VK_CONTROL) { ui_app.ctrl = down; } if (key == VK_SHIFT) { ui_app.shift = down; } } static inline ui_point_t ui_app_point2ui(const POINT* p) { ui_point_t u = { p->x, p->y }; return u; } static inline POINT ui_app_ui2point(const ui_point_t* u) { POINT p = { u->x, u->y }; return p; } static ui_rect_t ui_app_rect2ui(const RECT* r) { ui_rect_t u = { r->left, r->top, r->right - r->left, r->bottom - r->top }; return u; } static RECT ui_app_ui2rect(const ui_rect_t* u) { RECT r = { u->x, u->y, u->x + u->w, u->y + u->h }; return r; } static void ui_app_update_ncm(int32_t dpi) { // Only UTF-16 version supported SystemParametersInfoForDpi rt_fatal_win32err(SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(ui_app_ncm), &ui_app_ncm, 0, (DWORD)dpi)); } static void ui_app_update_monitor_dpi(HMONITOR monitor, ui_dpi_t* dpi) { dpi->monitor_max = 72; for (int32_t mtd = MDT_EFFECTIVE_DPI; mtd <= MDT_RAW_DPI; mtd++) { uint32_t dpi_x = 0; uint32_t dpi_y = 0; // GetDpiForMonitor() may return ERROR_GEN_FAILURE 0x8007001F when // system wakes up from sleep: // ""A device attached to the system is not functioning." // docs say: // "May be used to indicate that the device has stopped responding // or a general failure has occurred on the device. // The device may need to be manually reset." int32_t r = GetDpiForMonitor(monitor, (MONITOR_DPI_TYPE)mtd, &dpi_x, &dpi_y); if (r != 0) { rt_thread.sleep_for(1.0 / 32); // and retry: r = GetDpiForMonitor(monitor, (MONITOR_DPI_TYPE)mtd, &dpi_x, &dpi_y); } if (r == 0) { // EFFECTIVE_DPI 168 168 (with regard of user scaling) // ANGULAR_DPI 247 248 (diagonal) // RAW_DPI 283 284 (horizontal, vertical) // Parallels Desktop 16.5.0 (49183) on macOS Mac Book Air // EFFECTIVE_DPI 192 192 (with regard of user scaling) // ANGULAR_DPI 224 224 (diagonal) // RAW_DPI 72 72 const int32_t max_xy = (int32_t)rt_max(dpi_x, dpi_y); switch (mtd) { case MDT_EFFECTIVE_DPI: dpi->monitor_effective = max_xy; // rt_println("ui_app.dpi.monitor_effective := max(%d,%d)", dpi_x, dpi_y); break; case MDT_ANGULAR_DPI: dpi->monitor_angular = max_xy; // rt_println("ui_app.dpi.monitor_angular := max(%d,%d)", dpi_x, dpi_y); break; case MDT_RAW_DPI: dpi->monitor_raw = max_xy; // rt_println("ui_app.dpi.monitor_raw := max(%d,%d)", dpi_x, dpi_y); break; default: rt_assert(false); } dpi->monitor_max = rt_max(dpi->monitor_max, max_xy); } } // rt_println("ui_app.dpi.monitor_max := %d", dpi->monitor_max); } #ifndef UI_APP_DEBUG static void ui_app_dump_dpi(void) { rt_println("ui_app.dpi.monitor_effective: %d", ui_app.dpi.monitor_effective ); rt_println("ui_app.dpi.monitor_angular : %d", ui_app.dpi.monitor_angular ); rt_println("ui_app.dpi.monitor_raw : %d", ui_app.dpi.monitor_raw ); rt_println("ui_app.dpi.monitor_max : %d", ui_app.dpi.monitor_max ); rt_println("ui_app.dpi.window : %d", ui_app.dpi.window ); rt_println("ui_app.dpi.system : %d", ui_app.dpi.system ); rt_println("ui_app.dpi.process : %d", ui_app.dpi.process ); rt_println("ui_app.mrc : %d,%d %dx%d", ui_app.mrc.x, ui_app.mrc.y, ui_app.mrc.w, ui_app.mrc.h); rt_println("ui_app.wrc : %d,%d %dx%d", ui_app.wrc.x, ui_app.wrc.y, ui_app.wrc.w, ui_app.wrc.h); rt_println("ui_app.crc : %d,%d %dx%d", ui_app.crc.x, ui_app.crc.y, ui_app.crc.w, ui_app.crc.h); rt_println("ui_app.work_area: %d,%d %dx%d", ui_app.work_area.x, ui_app.work_area.y, ui_app.work_area.w, ui_app.work_area.h); int32_t mxt_x = GetSystemMetrics(SM_CXMAXTRACK); int32_t mxt_y = GetSystemMetrics(SM_CYMAXTRACK); rt_println("MAXTRACK: %d, %d", mxt_x, mxt_y); int32_t scr_x = GetSystemMetrics(SM_CXSCREEN); int32_t scr_y = GetSystemMetrics(SM_CYSCREEN); fp64_t monitor_x = (fp64_t)scr_x / (fp64_t)ui_app.dpi.monitor_max; fp64_t monitor_y = (fp64_t)scr_y / (fp64_t)ui_app.dpi.monitor_max; rt_println("SCREEN: %d, %d %.1fx%.1f\"", scr_x, scr_y, monitor_x, monitor_y); } #endif static bool ui_app_update_mi(const ui_rect_t* r, uint32_t flags) { RECT rc = ui_app_ui2rect(r); HMONITOR monitor = MonitorFromRect(&rc, flags); // TODO: moving between monitors with different DPIs // HMONITOR mw = MonitorFromWindow(ui_app_window(), flags); if (monitor != null) { ui_app_update_monitor_dpi(monitor, &ui_app.dpi); rt_fatal_win32err(GetMonitorInfoA(monitor, &ui_app_mi)); ui_app.work_area = ui_app_rect2ui(&ui_app_mi.rcWork); ui_app.mrc = ui_app_rect2ui(&ui_app_mi.rcMonitor); // ui_app_dump_dpi(); } return monitor != null; } static void ui_app_update_crc(void) { RECT rc = {0}; rt_fatal_win32err(GetClientRect(ui_app_window(), &rc)); ui_app.crc = ui_app_rect2ui(&rc); } static void ui_app_dispose_fonts(void) { ui_gdi.delete_font(ui_app.fm.prop.normal.font); ui_gdi.delete_font(ui_app.fm.prop.tiny.font); ui_gdi.delete_font(ui_app.fm.prop.title.font); ui_gdi.delete_font(ui_app.fm.prop.rubric.font); ui_gdi.delete_font(ui_app.fm.prop.H1.font); ui_gdi.delete_font(ui_app.fm.prop.H2.font); ui_gdi.delete_font(ui_app.fm.prop.H3.font); memset(&ui_app.fm.prop, 0x00, sizeof(ui_app.fm.prop)); ui_gdi.delete_font(ui_app.fm.mono.normal.font); ui_gdi.delete_font(ui_app.fm.mono.tiny.font); ui_gdi.delete_font(ui_app.fm.mono.title.font); ui_gdi.delete_font(ui_app.fm.mono.rubric.font); ui_gdi.delete_font(ui_app.fm.mono.H1.font); ui_gdi.delete_font(ui_app.fm.mono.H2.font); ui_gdi.delete_font(ui_app.fm.mono.H3.font); memset(&ui_app.fm.mono, 0x00, sizeof(ui_app.fm.mono)); } static fp64_t ui_app_px2pt(fp64_t px) { rt_assert(ui_app.dpi.window >= 72.0); return px * 72.0 / (fp64_t)ui_app.dpi.window; } static int32_t ui_app_pt2px(fp64_t pt) { // rounded return (int32_t)(pt * (fp64_t)ui_app.dpi.window / 72.0 + 0.5); } static void ui_app_init_cursors(void) { if (ui_app.cursors.arrow == null) { ui_app.cursors.arrow = (ui_cursor_t)LoadCursorW(null, IDC_ARROW); ui_app.cursors.wait = (ui_cursor_t)LoadCursorW(null, IDC_WAIT); ui_app.cursors.ibeam = (ui_cursor_t)LoadCursorW(null, IDC_IBEAM); ui_app.cursors.size_nwse = (ui_cursor_t)LoadCursorW(null, IDC_SIZENWSE); ui_app.cursors.size_nesw = (ui_cursor_t)LoadCursorW(null, IDC_SIZENESW); ui_app.cursors.size_we = (ui_cursor_t)LoadCursorW(null, IDC_SIZEWE); ui_app.cursors.size_ns = (ui_cursor_t)LoadCursorW(null, IDC_SIZENS); ui_app.cursors.size_all = (ui_cursor_t)LoadCursorW(null, IDC_SIZEALL); ui_app.cursor = ui_app.cursors.arrow; } } static void ui_app_ncm_dump_fonts(void) { // Win10/Win11 all 5 fonts are exactly the same: // Caption : Segoe UI 0x-12 weight: 400 quality: 0 // SmCaption: Segoe UI 0x-12 weight: 400 quality: 0 // Menu : Segoe UI 0x-12 weight: 400 quality: 0 // Status : Segoe UI 0x-12 weight: 400 quality: 0 // Message : Segoe UI 0x-12 weight: 400 quality: 0 #if 0 const LOGFONTW* fonts[] = { &ui_app_ncm.lfCaptionFont, &ui_app_ncm.lfSmCaptionFont, &ui_app_ncm.lfMenuFont, &ui_app_ncm.lfStatusFont, &ui_app_ncm.lfMessageFont }; const char* font_names[] = { "Caption", "SmCaption", "Menu", "Status", "Message" }; for (int32_t i = 0; i < rt_countof(fonts); i++) { const LOGFONTW* lf = fonts[i]; char fn[128]; rt_str.utf16to8(fn, rt_countof(fn), lf->lfFaceName, -1); rt_println("%-9s: %s %dx%d weight: %d quality: %d", font_names[i], fn, lf->lfWidth, lf->lfHeight, lf->lfWeight, lf->lfQuality); } #endif } static void ui_app_dump_font_size(const char* name, const LOGFONTW* lf, ui_fm_t* fm) { rt_swear(abs(lf->lfHeight) == fm->height - fm->internal_leading); rt_swear(fm->external_leading == 0); // "Segoe UI" and "Cascadia Mono" rt_swear(ui_app.dpi.window >= 72); // "The height, in logical units, of the font's character cell or character. // The character height value (also known as the em height) is the // character cell height value minus the internal-leading value." #ifdef UI_APP_DUMP_FONT_SIZE int32_t ascender = fm->baseline - fm->ascent; int32_t cell = fm->height - ascender - fm->descent; fp64_t pt = fm->height * 72.0 / (fp64_t)ui_app.dpi.window; rt_println("%-6s .lfH: %+3d h: %d pt: %6.3f " "a: %2d c: %2d d: %d bl: %2d il: %2d lg: %d", name, lf->lfHeight, fm->height, pt, ascender, cell, fm->descent, fm->baseline, fm->internal_leading, fm->line_gap); #if 0 // TODO: need better understanding of box geometry in // "design units" // box scale factor: design units -> pixels fp64_t sf = pt * 72.0 / (fp64_t)fm->design_units_per_em; sf *= (fp64_t)ui_app.dpi.window / 72.0; // into pixels (unclear???) int32_t bx = (int32_t)(fm->box.x * sf + 0.5); int32_t by = (int32_t)(fm->box.y * sf + 0.5); int32_t bw = (int32_t)(fm->box.w * sf + 0.5); int32_t bh = (int32_t)(fm->box.h * sf + 0.5); rt_println("%-6s .box: %d,%d %dx%d", name, bx, by, bw, bh); #endif #else (void)name; // unused #endif } static void ui_app_init_fms(ui_fms_t* fms, const LOGFONTW* base) { LOGFONTW lf = *base; // lf.lfQuality is zero (DEFAULT_QUALITY) that gets internally // interpreted as CLEARTYPE_QUALITY (if clear type is enabled // system wide and it looks really bad on 4K monitors // Experimentally it looks like Windows UI is using PROOF_QUALITY // which is anti-aliased w/o ClearType rainbows // TODO: maybe DEFAULT_QUALITY on 96DPI, // PROOF_QUALITY below 4K // ANTIALIASED_QUALITY on 4K and ? lf.lfQuality = ANTIALIASED_QUALITY; ui_gdi.update_fm(&fms->normal, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("normal", &lf, &fms->normal); const fp64_t fh = lf.lfHeight; rt_swear(fh != 0); lf.lfHeight = (int32_t)(fh * 8.0 / 11.0 + 0.5); ui_gdi.update_fm(&fms->tiny, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("tiny", &lf, &fms->tiny); lf.lfWeight = FW_SEMIBOLD; lf.lfHeight = (int32_t)(fh * 2.25 + 0.5); ui_gdi.update_fm(&fms->title, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("title", &lf, &fms->title); lf.lfHeight = (int32_t)(fh * 2.00 + 0.5); ui_gdi.update_fm(&fms->rubric, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("rubric", &lf, &fms->rubric); lf.lfHeight = (int32_t)(fh * 1.75 + 0.5); ui_gdi.update_fm(&fms->H1, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H1", &lf, &fms->H1); lf.lfHeight = (int32_t)(fh * 1.4 + 0.5); ui_gdi.update_fm(&fms->H2, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H2", &lf, &fms->H2); lf.lfHeight = (int32_t)(fh * 1.15 + 0.5); ui_gdi.update_fm(&fms->H3, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H3", &lf, &fms->H3); } static void ui_app_init_fonts(int32_t dpi) { ui_app_update_ncm(dpi); ui_app_ncm_dump_fonts(); if (ui_app.fm.prop.normal.font != null) { ui_app_dispose_fonts(); } LOGFONTW mono = ui_app_ncm.lfMessageFont; // TODO: how to get name of monospaced from Win32 API? wcscpy_s(mono.lfFaceName, rt_countof(mono.lfFaceName), L"Cascadia Mono"); mono.lfPitchAndFamily |= FIXED_PITCH; // rt_println("ui_app.fm.mono"); ui_app_init_fms(&ui_app.fm.mono, &mono); LOGFONTW prop = ui_app_ncm.lfMessageFont; prop.lfHeight--; // inc by 1 // rt_println("ui_app.fm.prop"); ui_app_init_fms(&ui_app.fm.prop, &ui_app_ncm.lfMessageFont); } static void ui_app_data_save(const char* name, const void* data, int32_t bytes) { rt_config.save(ui_app.class_name, name, data, bytes); } static int32_t ui_app_data_size(const char* name) { return rt_config.size(ui_app.class_name, name); } static int32_t ui_app_data_load(const char* name, void* data, int32_t bytes) { return rt_config.load(ui_app.class_name, name, data, bytes); } typedef rt_begin_packed struct ui_app_wiw_s { // "where is window" // coordinates in pixels relative (0,0) top left corner // of primary monitor from GetWindowPlacement int32_t bytes; int32_t padding; // to align rectangles and points to 8 bytes ui_rect_t placement; ui_rect_t mrc; // monitor rectangle ui_rect_t work_area; // monitor work area (mrc sans taskbar etc) ui_point_t min_position; // not used (-1, -1) ui_point_t max_position; // not used (-1, -1) ui_point_t max_track; // maximum window size (spawning all monitors) ui_rect_t space; // surrounding rect x,y,w,h of all monitors int32_t dpi; // of the monitor on which window (x,y) is located int32_t flags; // WPF_SETMINPOSITION. WPF_RESTORETOMAXIMIZED int32_t show; // show command } rt_end_packed ui_app_wiw_t; static BOOL CALLBACK ui_app_monitor_enum_proc(HMONITOR monitor, HDC rt_unused(hdc), RECT* rt_unused(rc1), LPARAM that) { ui_app_wiw_t* wiw = (ui_app_wiw_t*)(uintptr_t)that; MONITORINFOEXA mi = { .cbSize = sizeof(MONITORINFOEXA) }; rt_fatal_win32err(GetMonitorInfoA(monitor, (MONITORINFO*)&mi)); // monitors can be in negative coordinate spaces and even rotated upside-down const int32_t min_x = rt_min(mi.rcMonitor.left, mi.rcMonitor.right); const int32_t min_y = rt_min(mi.rcMonitor.top, mi.rcMonitor.bottom); const int32_t max_w = rt_max(mi.rcMonitor.left, mi.rcMonitor.right); const int32_t max_h = rt_max(mi.rcMonitor.top, mi.rcMonitor.bottom); wiw->space.x = rt_min(wiw->space.x, min_x); wiw->space.y = rt_min(wiw->space.y, min_y); wiw->space.w = rt_max(wiw->space.w, max_w); wiw->space.h = rt_max(wiw->space.h, max_h); return true; // keep going } static void ui_app_enum_monitors(ui_app_wiw_t* wiw) { EnumDisplayMonitors(null, null, ui_app_monitor_enum_proc, (LPARAM)(uintptr_t)wiw); // because ui_app_monitor_enum_proc() puts max into w,h: wiw->space.w -= wiw->space.x; wiw->space.h -= wiw->space.y; } static void ui_app_save_window_pos(ui_window_t wnd, const char* name, bool dump) { RECT wr = {0}; rt_fatal_win32err(GetWindowRect((HWND)wnd, &wr)); ui_rect_t wrc = ui_app_rect2ui(&wr); ui_app_update_mi(&wrc, MONITOR_DEFAULTTONEAREST); WINDOWPLACEMENT wpl = { .length = sizeof(wpl) }; rt_fatal_win32err(GetWindowPlacement((HWND)wnd, &wpl)); // note the replacement of wpl.rcNormalPosition with wrc: ui_app_wiw_t wiw = { // where is window .bytes = sizeof(ui_app_wiw_t), .placement = wrc, .mrc = ui_app.mrc, .work_area = ui_app.work_area, .min_position = ui_app_point2ui(&wpl.ptMinPosition), .max_position = ui_app_point2ui(&wpl.ptMaxPosition), .max_track = { .x = GetSystemMetrics(SM_CXMAXTRACK), .y = GetSystemMetrics(SM_CYMAXTRACK) }, .dpi = ui_app.dpi.monitor_max, .flags = (int32_t)wpl.flags, .show = (int32_t)wpl.showCmd }; ui_app_enum_monitors(&wiw); if (dump) { rt_println("wiw.space: %d,%d %dx%d", wiw.space.x, wiw.space.y, wiw.space.w, wiw.space.h); rt_println("MAXTRACK: %d, %d", wiw.max_track.x, wiw.max_track.y); rt_println("wpl.rcNormalPosition: %d,%d %dx%d", wpl.rcNormalPosition.left, wpl.rcNormalPosition.top, wpl.rcNormalPosition.right - wpl.rcNormalPosition.left, wpl.rcNormalPosition.bottom - wpl.rcNormalPosition.top); rt_println("wpl.ptMinPosition: %d,%d", wpl.ptMinPosition.x, wpl.ptMinPosition.y); rt_println("wpl.ptMaxPosition: %d,%d", wpl.ptMaxPosition.x, wpl.ptMaxPosition.y); rt_println("wpl.showCmd: %d", wpl.showCmd); // WPF_SETMINPOSITION. WPF_RESTORETOMAXIMIZED WPF_ASYNCWINDOWPLACEMENT rt_println("wpl.flags: %d", wpl.flags); } // rt_println("%d,%d %dx%d show=%d", wiw.placement.x, wiw.placement.y, // wiw.placement.w, wiw.placement.h, wiw.show); rt_config.save(ui_app.class_name, name, &wiw, sizeof(wiw)); ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); } static void ui_app_save_console_pos(void) { HWND cw = GetConsoleWindow(); if (cw != null) { ui_app_save_window_pos((ui_window_t)cw, "wic", false); HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int32_t r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); if (r != 0) { rt_println("GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); } else { rt_config.save(ui_app.class_name, "console_screen_buffer_infoex", &info, (int32_t)sizeof(info)); // rt_println("info: %dx%d", info.dwSize.X, info.dwSize.Y); // rt_println("%d,%d %dx%d", info.srWindow.Left, info.srWindow.Top, // info.srWindow.Right - info.srWindow.Left, // info.srWindow.Bottom - info.srWindow.Top); } } int32_t v = ui_app.is_console_visible(); // "icv" "is console visible" rt_config.save(ui_app.class_name, "icv", &v, (int32_t)sizeof(v)); } static bool ui_app_is_fully_inside(const ui_rect_t* inner, const ui_rect_t* outer) { return outer->x <= inner->x && inner->x + inner->w <= outer->x + outer->w && outer->y <= inner->y && inner->y + inner->h <= outer->y + outer->h; } static void ui_app_bring_window_inside_monitor(const ui_rect_t* mrc, ui_rect_t* wrc) { rt_assert(mrc->w > 0 && mrc->h > 0); // Check if window rect is inside monitor rect if (!ui_app_is_fully_inside(wrc, mrc)) { // Move window into monitor rect wrc->x = rt_max(mrc->x, rt_min(mrc->x + mrc->w - wrc->w, wrc->x)); wrc->y = rt_max(mrc->y, rt_min(mrc->y + mrc->h - wrc->h, wrc->y)); // Adjust size to fit into monitor rect wrc->w = rt_min(wrc->w, mrc->w); wrc->h = rt_min(wrc->h, mrc->h); } } static bool ui_app_load_window_pos(ui_rect_t* rect, int32_t *visibility) { ui_app_wiw_t wiw = {0}; // where is window bool loaded = rt_config.load(ui_app.class_name, "wiw", &wiw, sizeof(wiw)) == sizeof(wiw); if (loaded) { #ifdef UI_APP_DEBUG rt_println("wiw.placement: %d,%d %dx%d", wiw.placement.x, wiw.placement.y, wiw.placement.w, wiw.placement.h); rt_println("wiw.mrc: %d,%d %dx%d", wiw.mrc.x, wiw.mrc.y, wiw.mrc.w, wiw.mrc.h); rt_println("wiw.work_area: %d,%d %dx%d", wiw.work_area.x, wiw.work_area.y, wiw.work_area.w, wiw.work_area.h); rt_println("wiw.min_position: %d,%d", wiw.min_position.x, wiw.min_position.y); rt_println("wiw.max_position: %d,%d", wiw.max_position.x, wiw.max_position.y); rt_println("wiw.max_track: %d,%d", wiw.max_track.x, wiw.max_track.y); rt_println("wiw.dpi: %d", wiw.dpi); rt_println("wiw.flags: %d", wiw.flags); rt_println("wiw.show: %d", wiw.show); #endif ui_app_update_mi(&wiw.placement, MONITOR_DEFAULTTONEAREST); bool same_monitor = memcmp(&wiw.mrc, &ui_app.mrc, sizeof(wiw.mrc)) == 0; // rt_println("%d,%d %dx%d", p->x, p->y, p->w, p->h); if (same_monitor) { *rect = wiw.placement; } else { // moving to another monitor rect->x = (wiw.placement.x - wiw.mrc.x) * ui_app.mrc.w / wiw.mrc.w; rect->y = (wiw.placement.y - wiw.mrc.y) * ui_app.mrc.h / wiw.mrc.h; // adjust according to monitors DPI difference: // (w, h) theoretically could be as large as 0xFFFF const int64_t w = (int64_t)wiw.placement.w * ui_app.dpi.monitor_max; const int64_t h = (int64_t)wiw.placement.h * ui_app.dpi.monitor_max; rect->w = (int32_t)(w / wiw.dpi); rect->h = (int32_t)(h / wiw.dpi); } *visibility = wiw.show; } // rt_println("%d,%d %dx%d show=%d", rect->x, rect->y, rect->w, rect->h, *visibility); ui_app_bring_window_inside_monitor(&ui_app.mrc, rect); // rt_println("%d,%d %dx%d show=%d", rect->x, rect->y, rect->w, rect->h, *visibility); return loaded; } static bool ui_app_load_console_pos(ui_rect_t* rect, int32_t *visibility) { ui_app_wiw_t wiw = {0}; // where is window *visibility = 0; // boolean bool loaded = rt_config.load(ui_app.class_name, "wic", &wiw, sizeof(wiw)) == sizeof(wiw); if (loaded) { ui_app_update_mi(&wiw.placement, MONITOR_DEFAULTTONEAREST); bool same_monitor = memcmp(&wiw.mrc, &ui_app.mrc, sizeof(wiw.mrc)) == 0; // rt_println("%d,%d %dx%d", p->x, p->y, p->w, p->h); if (same_monitor) { *rect = wiw.placement; } else { // moving to another monitor rect->x = (wiw.placement.x - wiw.mrc.x) * ui_app.mrc.w / wiw.mrc.w; rect->y = (wiw.placement.y - wiw.mrc.y) * ui_app.mrc.h / wiw.mrc.h; // adjust according to monitors DPI difference: // (w, h) theoretically could be as large as 0xFFFF const int64_t w = (int64_t)wiw.placement.w * ui_app.dpi.monitor_max; const int64_t h = (int64_t)wiw.placement.h * ui_app.dpi.monitor_max; rect->w = (int32_t)(w / wiw.dpi); rect->h = (int32_t)(h / wiw.dpi); } *visibility = wiw.show != 0; ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); } return loaded; } static void ui_app_timer_kill(ui_timer_t timer) { rt_fatal_win32err(KillTimer(ui_app_window(), timer)); } static ui_timer_t ui_app_timer_set(uintptr_t id, int32_t ms) { rt_not_null(ui_app_window()); rt_assert(10 <= ms && ms < 0x7FFFFFFF); ui_timer_t tid = (ui_timer_t)SetTimer(ui_app_window(), id, (uint32_t)ms, null); rt_fatal_if(tid == 0); rt_assert(tid == id); return tid; } static void ui_app_timer(ui_view_t* view, ui_timer_t id) { ui_view.timer(view, id); if (id == ui_app_timer_1s_id) { ui_view.every_sec(view); } if (id == ui_app_timer_100ms_id) { ui_view.every_100ms(view); } } static void ui_app_animate_timer(void) { ui_app_post_message(ui.message.animate, (int64_t)ui_app_animate.step + 1, (int64_t)(uintptr_t)ui_app_animate.f); } static void ui_app_wm_timer(ui_timer_t id) { if (ui_app.animating.time != 0 && ui_app.now > ui_app.animating.time) { ui_app.show_toast(null, 0); } if (ui_app_animate.timer == id) { ui_app_animate_timer(); } ui_app_timer(ui_app.root, id); } static void ui_app_window_dpi(void) { int32_t dpi = (int32_t)GetDpiForWindow(ui_app_window()); if (dpi == 0) { dpi = (int32_t)GetDpiForWindow(GetParent(ui_app_window())); } if (dpi == 0) { dpi = (int32_t)GetDpiForWindow(GetDesktopWindow()); } if (dpi == 0) { dpi = (int32_t)GetSystemDpiForProcess(GetCurrentProcess()); } if (dpi == 0) { dpi = (int32_t)GetDpiForSystem(); } ui_app.dpi.window = dpi; } static void ui_app_window_opening(void) { ui_app_window_dpi(); ui_app_init_fonts(ui_app.dpi.window); ui_app_init_cursors(); ui_app_timer_1s_id = ui_app.set_timer((uintptr_t)&ui_app_timer_1s_id, 1000); ui_app_timer_100ms_id = ui_app.set_timer((uintptr_t)&ui_app_timer_100ms_id, 100); rt_assert(ui_app.cursors.arrow != null); ui_app.set_cursor(ui_app.cursors.arrow); ui_app.canvas = (ui_canvas_t)GetDC(ui_app_window()); rt_not_null(ui_app.canvas); if (ui_app.opened != null) { ui_app.opened(); } ui_view.set_text(ui_app.root, "ui_app.root"); // debugging ui_app_wm_timer(ui_app_timer_100ms_id); ui_app_wm_timer(ui_app_timer_1s_id); rt_fatal_if(ReleaseDC(ui_app_window(), ui_app_canvas()) == 0); ui_app.canvas = null; ui_app.request_layout(); // request layout if (ui_app.last_visibility == ui.visibility.maximize) { ShowWindow(ui_app_window(), ui.visibility.maximize); } // ui_app_dump_dpi(); // if (forced_locale != 0) { // SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (uintptr_t)"intl", 0, 1000, null); // } } static void ui_app_window_closing(void) { if (ui_app.can_close == null || ui_app.can_close()) { if (ui_app.is_full_screen) { ui_app.full_screen(false); } ui_app.kill_timer(ui_app_timer_1s_id); ui_app.kill_timer(ui_app_timer_100ms_id); ui_app_timer_1s_id = 0; ui_app_timer_100ms_id = 0; if (ui_app.closed != null) { ui_app.closed(); } ui_app_save_window_pos(ui_app.window, "wiw", false); ui_app_save_console_pos(); DestroyWindow(ui_app_window()); ui_app.window = null; } } static void ui_app_get_min_max_info(MINMAXINFO* mmi) { const ui_window_sizing_t* ws = &ui_app.window_sizing; const ui_rect_t* wa = &ui_app.work_area; const int32_t min_w = ws->min_w > 0 ? ui_app.in2px(ws->min_w) : ui_app.in2px(1.0); const int32_t min_h = ws->min_h > 0 ? ui_app.in2px(ws->min_h) : ui_app.in2px(0.5); mmi->ptMinTrackSize.x = min_w; mmi->ptMinTrackSize.y = min_h; const int32_t max_w = ws->max_w > 0 ? ui_app.in2px(ws->max_w) : wa->w; const int32_t max_h = ws->max_h > 0 ? ui_app.in2px(ws->max_h) : wa->h; if (ui_app.no_clip) { mmi->ptMaxTrackSize.x = max_w; mmi->ptMaxTrackSize.y = max_h; } else { // clip max_w and max_h to monitor work area mmi->ptMaxTrackSize.x = rt_min(max_w, wa->w); mmi->ptMaxTrackSize.y = rt_min(max_h, wa->h); } mmi->ptMaxSize.x = mmi->ptMaxTrackSize.x; mmi->ptMaxSize.y = mmi->ptMaxTrackSize.y; } static void ui_app_paint(ui_view_t* view) { rt_assert(ui_app_window() != null); // crc = {0,0} on minimized windows but paint is still called if (ui_app.crc.w > 0 && ui_app.crc.h > 0) { ui_view.paint(view); } } static void ui_app_measure_and_layout(ui_view_t* view) { // restore from minimized calls ui_app.crc.w,h == 0 if (ui_app.crc.w > 0 && ui_app.crc.h > 0 && ui_app_window() != null) { ui_view.measure(view); ui_view.layout(view); ui_app_layout_dirty = false; } } static void ui_app_toast_character(const char* utf8); static bool ui_app_toast_key_pressed(int64_t key); static bool ui_app_toast_tap(ui_view_t* v, int32_t ix, bool pressed); static void ui_app_dispatch_wm_char(ui_view_t* view, const uint16_t* utf16) { char utf8[32 + 1]; int32_t utf8bytes = rt_str.utf8_bytes(utf16, -1); rt_swear(utf8bytes < rt_countof(utf8) - 1); // 32 bytes + 0x00 rt_str.utf16to8(utf8, rt_countof(utf8), utf16, -1); utf8[utf8bytes] = 0x00; if (ui_app.animating.view != null) { ui_app_toast_character(utf8); } else { ui_view.character(view, utf8); } ui_app_high_surrogate = 0x0000; } static void ui_app_wm_char(ui_view_t* view, const uint16_t* utf16) { int32_t utf16chars = rt_str.len16(utf16); rt_swear(0 < utf16chars && utf16chars < 4); // wParam is 64bits const uint16_t utf16char = utf16[0]; if (utf16chars == 1 && rt_str.utf16_is_high_surrogate(utf16char)) { ui_app_high_surrogate = utf16char; } else if (utf16chars == 1 && rt_str.utf16_is_low_surrogate(utf16char)) { if (ui_app_high_surrogate != 0) { uint16_t utf16_surrogate_pair[3] = { ui_app_high_surrogate, utf16char, 0x0000 }; ui_app_dispatch_wm_char(view, utf16_surrogate_pair); } } else { ui_app_dispatch_wm_char(view, utf16); } } static bool ui_app_wm_key_pressed(ui_view_t* v, int64_t key) { if (ui_app.animating.view != null) { return ui_app_toast_key_pressed(key); } else { return ui_view.key_pressed(v, key); } } static bool ui_app_mouse(ui_view_t* v, int32_t m, int64_t f) { bool swallow = false; // override ui_app_update_mouse_buttons_state() (sic): // because mouse message can be from the past ui_app.mouse_left = f & (ui_app.mouse_swapped ? MK_RBUTTON : MK_LBUTTON); ui_app.mouse_middle = f & MK_MBUTTON; ui_app.mouse_right = f & (ui_app.mouse_swapped ? MK_LBUTTON : MK_RBUTTON); ui_view_t* av = ui_app.animating.view; if (m == WM_MOUSEHOVER) { ui_view.mouse_hover(av != null && av->mouse_hover != null ? av : v); } else if (m == WM_MOUSEMOVE) { ui_view.mouse_move(av != null && av->mouse_move != null ? av : v); } else if (m == WM_LBUTTONDOWN || m == WM_LBUTTONUP || m == WM_MBUTTONDOWN || m == WM_MBUTTONUP || m == WM_RBUTTONDOWN || m == WM_RBUTTONUP) { const int i = (m == WM_LBUTTONDOWN || m == WM_LBUTTONUP) ? 0 : ((m == WM_MBUTTONDOWN || m == WM_MBUTTONUP) ? 1 : ((m == WM_RBUTTONDOWN || m == WM_RBUTTONUP) ? 2 : -1)); rt_swear(i >= 0); const int32_t ix = ui_app.mouse_swapped ? 2 - i : i; const bool pressed = m == WM_LBUTTONDOWN || m == WM_MBUTTONDOWN || m == WM_RBUTTONDOWN; if (av != null) { // because of "micro" close button: swallow = ui_app_toast_tap(ui_app.animating.view, ix, pressed); } else { if (av != null && av->tap != null) { swallow = ui_view.tap(av, ix, pressed); } else { // tap detector will handle the tap() calling } } } else if (m == WM_LBUTTONDBLCLK || m == WM_MBUTTONDBLCLK || m == WM_RBUTTONDBLCLK) { const int i = (m == WM_LBUTTONDBLCLK) ? 0 : ((m == WM_MBUTTONDBLCLK) ? 1 : ((m == WM_RBUTTONDBLCLK) ? 2 : -1)); rt_swear(i >= 0); if (av != null && av->double_tap != null) { const int32_t ix = ui_app.mouse_swapped ? 2 - i : i; swallow = ui_view.double_tap(av, ix); } // otherwise tap detector will do the double_tap() call } else { rt_assert(false, "m: 0x%04X", m); } return swallow; } static void ui_app_show_sys_menu(int32_t x, int32_t y) { HMENU sys_menu = GetSystemMenu(ui_app_window(), false); if (sys_menu != null) { // TPM_RIGHTBUTTON means both left and right click to select menu item const DWORD flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_VERPOSANIMATION; int32_t sys_cmd = TrackPopupMenu(sys_menu, flags, x, y, 0, ui_app_window(), null); if (sys_cmd != 0) { ui_app_post_message(WM_SYSCOMMAND, sys_cmd, 0); } } } static int32_t ui_app_nc_mouse_message(int32_t m) { switch (m) { case WM_NCMOUSEMOVE : return WM_MOUSEMOVE; case WM_NCLBUTTONDOWN : return WM_LBUTTONDOWN; case WM_NCLBUTTONUP : return WM_LBUTTONUP; case WM_NCLBUTTONDBLCLK : return WM_LBUTTONDBLCLK; case WM_NCMBUTTONDOWN : return WM_MBUTTONDOWN; case WM_NCMBUTTONUP : return WM_MBUTTONUP; case WM_NCMBUTTONDBLCLK : return WM_MBUTTONDBLCLK; case WM_NCRBUTTONDOWN : return WM_RBUTTONDOWN; case WM_NCRBUTTONUP : return WM_RBUTTONUP; case WM_NCRBUTTONDBLCLK : return WM_RBUTTONDBLCLK; default: rt_swear(false, "fix me m: %d", m); } return -1; } static bool ui_app_nc_mouse_buttons(int32_t m, int64_t wp, int64_t lp) { bool swallow = false; POINT screen = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; POINT client = screen; ScreenToClient(ui_app_window(), &client); ui_app.mouse = ui_app_point2ui(&client); const bool inside = ui_view.inside(ui_app.caption, &ui_app.mouse); if (!ui_view.is_hidden(ui_app.caption) && inside) { uint16_t lr = ui_app.mouse_swapped ? WM_NCLBUTTONDOWN : WM_NCRBUTTONDOWN; if (m == lr) { // rt_println("WM_NC*BUTTONDOWN %d %d", ui_app.mouse.x, ui_app.mouse.y); swallow = true; ui_app_show_sys_menu(screen.x, screen.y); } } else { swallow = ui_app_mouse(ui_app.root, ui_app_nc_mouse_message(m), wp); } return swallow; } enum { ui_app_animation_steps = 63 }; static void ui_app_toast_paint(void) { static ui_bitmap_t image_dark; if (image_dark.texture == null) { uint8_t pixels[4] = { 0x3F, 0x3F, 0x3F }; ui_gdi.bitmap_init(&image_dark, 1, 1, 3, pixels); } static ui_bitmap_t image_light; if (image_dark.texture == null) { uint8_t pixels[4] = { 0xC0, 0xC0, 0xC0 }; ui_gdi.bitmap_init(&image_light, 1, 1, 3, pixels); } ui_view_t* av = ui_app.animating.view; if (av != null) { ui_view.measure(av); bool hint = ui_app.animating.x >= 0 && ui_app.animating.y >= 0; const int32_t em_w = av->fm->em.w; const int32_t em_h = av->fm->em.h; if (!hint) { rt_assert(0 <= ui_app.animating.step && ui_app.animating.step < ui_app_animation_steps); int32_t step = ui_app.animating.step - (ui_app_animation_steps - 1); av->y = av->h * step / (ui_app_animation_steps - 1); // rt_println("step=%d of %d y=%d", ui_app.animating.step, // ui_app_toast_steps, av->y); ui_app_measure_and_layout(av); // dim main window (as `disabled`): fp64_t alpha = rt_min(0.40, 0.40 * ui_app.animating.step / (fp64_t)ui_app_animation_steps); ui_gdi.alpha(0, 0, ui_app.crc.w, ui_app.crc.h, 0, 0, image_dark.w, image_dark.h, &image_dark, alpha); av->x = (ui_app.root->w - av->w) / 2; // rt_println("ui_app.animating.y: %d av->y: %d", // ui_app.animating.y, av->y); } else { av->x = ui_app.animating.x; av->y = ui_app.animating.y; ui_app_measure_and_layout(av); int32_t mx = ui_app.root->w - av->w - em_w; int32_t cx = ui_app.animating.x - av->w / 2; av->x = rt_min(mx, rt_max(0, cx)); av->y = rt_min( ui_app.root->h - em_h, rt_max(0, ui_app.animating.y)); // rt_println("ui_app.animating.y: %d av->y: %d", // ui_app.animating.y, av->y); } int32_t x = av->x - em_w / 4; int32_t y = av->y - em_h / 8; int32_t w = av->w + em_w / 2; int32_t h = av->h + em_h / 4; int32_t radius = em_w / 2; if (radius % 2 == 0) { radius++; } ui_color_t color = ui_theme.is_app_dark() ? ui_color_rgb(45, 45, 48) : // TODO: hard coded ui_colors.get_color(ui_color_id_button_face); ui_color_t tint = ui_colors.interpolate(color, ui_colors.yellow, 0.5f); ui_gdi.rounded(x, y, w, h, radius, tint, tint); if (!hint) { av->y += em_h / 4; } ui_app_paint(av); if (!hint) { if (av->y == em_h / 4) { // micro "close" toast button: int32_t r = av->x + av->w; const int32_t tx = r - em_w / 2; const int32_t ty = 0; const ui_gdi_ta_t ta = { .fm = &ui_app.fm.prop.normal, .color = ui_color_undefined, .color_id = ui_color_id_window_text }; ui_gdi.text(&ta, tx, ty, "%s", rt_glyph_multiplication_sign); } } } } static void ui_app_toast_cancel(void) { if (ui_app.animating.view != null) { if (ui_app.animating.view->type == ui_view_mbx) { ui_mbx_t* mx = (ui_mbx_t*)ui_app.animating.view; if (mx->option < 0 && mx->callback != null) { mx->callback(&mx->view); } } ui_app.animating.view->parent = null; ui_app.animating.step = 0; ui_app.animating.view = null; ui_app.animating.time = 0; ui_app.animating.x = -1; ui_app.animating.y = -1; if (ui_app.animating.focused != null) { ui_view.set_focus(ui_app.animating.focused->focusable && !ui_view.is_hidden(ui_app.animating.focused) && !ui_view.is_disabled(ui_app.animating.focused) ? ui_app.animating.focused : null); ui_app.animating.focused = null; } else { ui_view.set_focus(null); } ui_app.request_redraw(); } } static bool ui_app_toast_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; rt_swear(v == ui_app.animating.view); if (pressed) { const ui_fm_t* fm = v->fm; const int32_t right = v->x + v->w; const int32_t x = right - fm->em.w / 2; const int32_t mx = ui_app.mouse.x; const int32_t my = ui_app.mouse.y; // micro close button which is not a button if (x <= mx && mx <= x + fm->em.w && 0 <= my && my <= fm->em.h) { ui_app_toast_cancel(); } } if (ui_app.animating.view != null) { // could have been canceled above swallow = ui_view.tap(v, ix, pressed); // TODO: do we need it? } return swallow; } static void ui_app_toast_character(const char* utf8) { char ch = utf8[0]; if (ui_app.animating.view != null && ch == 033) { // ESC traditionally in octal ui_app_toast_cancel(); ui_app.show_toast(null, 0); } else { ui_view.character(ui_app.animating.view, utf8); } } static bool ui_app_toast_key_pressed(int64_t key) { if (ui_app.animating.view != null && key == 033) { // ESC traditionally in octal ui_app_toast_cancel(); ui_app.show_toast(null, 0); return true; } else { return ui_view.key_pressed(ui_app.animating.view, key); } } static void ui_app_toast_dim(int32_t step) { ui_app.animating.step = step; ui_app.request_redraw(); UpdateWindow(ui_app_window()); } static void ui_app_animate_step(ui_app_animate_function_t f, int32_t step, int32_t steps) { // calls function(0..step-1) exactly step times bool cancel = false; if (f != null && f != ui_app_animate.f && step == 0 && steps > 0) { // start animated_groot ui_app_animate.count = steps; ui_app_animate.f = f; f(step); ui_app_animate.timer = ui_app.set_timer((uintptr_t)&ui_app_animate.timer, 10); } else if (f != null && ui_app_animate.f == f && step > 0) { cancel = step >= ui_app_animate.count; if (!cancel) { ui_app_animate.step = step; f(step); } } else if (f == null) { cancel = true; } if (cancel) { if (ui_app_animate.timer != 0) { ui_app.kill_timer(ui_app_animate.timer); } ui_app_animate.step = 0; ui_app_animate.timer = 0; ui_app_animate.f = null; ui_app_animate.count = 0; } } static void ui_app_animate_start(ui_app_animate_function_t f, int32_t steps) { // calls f(0..step-1) exactly steps times, unless cancelled with call // animate(null, 0) or animate(other_function, n > 0) ui_app_animate_step(f, 0, steps); } static void ui_app_view_paint(ui_view_t* v) { v->color = ui_colors.get_color(v->color_id); if (v->background_id > 0) { v->background = ui_colors.get_color(v->background_id); } if (!ui_color_is_undefined(v->background) && !ui_color_is_transparent(v->background)) { ui_gdi.fill(v->x, v->y, v->w, v->h, v->background); } } static void ui_app_view_layout(void) { rt_not_null(ui_app.window); rt_not_null(ui_app.canvas); if (ui_app.no_decor) { ui_app.root->x = ui_app.border.w; ui_app.root->y = ui_app.border.h; ui_app.root->w = ui_app.crc.w - ui_app.border.w * 2; ui_app.root->h = ui_app.crc.h - ui_app.border.h * 2; } else { ui_app.root->x = 0; ui_app.root->y = 0; ui_app.root->w = ui_app.crc.w; ui_app.root->h = ui_app.crc.h; } ui_app_measure_and_layout(ui_app.root); } static void ui_app_view_active_frame_paint(void) { ui_color_t c = ui_app.is_active() ? ui_colors.get_color(ui_color_id_highlight) : // ui_colors.btn_hover_highlight ui_colors.get_color(ui_color_id_inactive_title); rt_assert(ui_app.border.w == ui_app.border.h); const int32_t w = ui_app.wrc.w; const int32_t h = ui_app.wrc.h; for (int32_t i = 0; i < ui_app.border.w; i++) { ui_gdi.frame(i, i, w - i * 2, h - i * 2, c); } } static void ui_app_paint_stats(void) { if (ui_app.paint_count % 128 == 0) { ui_app.paint_max = 0; } ui_app.paint_time = rt_clock.seconds() - ui_app.now; ui_app.paint_max = rt_max(ui_app.paint_time, ui_app.paint_max); if (ui_app.paint_avg == 0) { ui_app.paint_avg = ui_app.paint_time; } else { // EMA over 32 paint() calls ui_app.paint_avg = ui_app.paint_avg * (1.0 - 1.0 / 32.0) + ui_app.paint_time / 32.0; } static fp64_t first_paint; if (first_paint == 0) { first_paint = ui_app.now; } fp64_t since_first_paint = ui_app.now - first_paint; if (since_first_paint > 0) { double fps = (double)ui_app.paint_count / since_first_paint; if (ui_app.paint_fps == 0) { ui_app.paint_fps = fps; } else { ui_app.paint_fps = ui_app.paint_fps * (1.0 - 1.0 / 32.0) + fps / 32.0; } } if (ui_app.paint_last == 0) { ui_app.paint_dt_min = 1.0 / 60.0; // 60Hz monitor } else { fp64_t since_last = ui_app.now - ui_app.paint_last; if (since_last > 1.0 / 120.0) { // 240Hz monitor ui_app.paint_dt_min = rt_min(ui_app.paint_dt_min, since_last); } // rt_println("paint_dt_min: %.6f since_last: %.6f", // ui_app.paint_dt_min, since_last); } ui_app.paint_last = ui_app.now; } static void ui_app_paint_on_canvas(HDC hdc) { ui_canvas_t canvas = ui_app.canvas; ui_app.canvas = (ui_canvas_t)hdc; ui_app_update_crc(); if (ui_app_layout_dirty) { ui_app_view_layout(); } ui_gdi.begin(null); ui_app_paint(ui_app.root); if (ui_app.animating.view != null) { ui_app_toast_paint(); } // active frame on top of everything: if (ui_app.no_decor && !ui_app.is_full_screen && !ui_app.is_maximized()) { ui_app_view_active_frame_paint(); } ui_gdi.end(); ui_app.paint_count++; ui_app.canvas = canvas; ui_app_paint_stats(); } static void ui_app_wm_paint(void) { // it is possible to receive WM_PAINT when window is not closed if (ui_app.window != null) { PAINTSTRUCT ps = {0}; BeginPaint(ui_app_window(), &ps); ui_app.prc = ui_app_rect2ui(&ps.rcPaint); // rt_println("%d,%d %dx%d", ui_app.prc.x, ui_app.prc.y, ui_app.prc.w, ui_app.prc.h); ui_app_paint_on_canvas(ps.hdc); EndPaint(ui_app_window(), &ps); } } // about (x,y) being (-32000,-32000) see: // https://chromium.googlesource.com/chromium/src.git/+/62.0.3178.1/ui/views/win/hwnd_message_handler.cc#1847 static void ui_app_window_position_changed(const WINDOWPOS* wp) { ui_app.root->state.hidden = !IsWindowVisible(ui_app_window()); const bool moved = (wp->flags & SWP_NOMOVE) == 0; const bool sized = (wp->flags & SWP_NOSIZE) == 0; const bool hiding = (wp->flags & SWP_HIDEWINDOW) != 0 || (wp->x == -32000 && wp->y == -32000); HMONITOR monitor = MonitorFromWindow(ui_app_window(), MONITOR_DEFAULTTONULL); if (!ui_app.root->state.hidden && (moved || sized) && !hiding && monitor != null) { RECT wrc = ui_app_ui2rect(&ui_app.wrc); rt_fatal_win32err(GetWindowRect(ui_app_window(), &wrc)); ui_app.wrc = ui_app_rect2ui(&wrc); ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); ui_app_update_crc(); if (ui_app_timer_1s_id != 0) { ui_app.request_layout(); } } } static void ui_app_setting_change(uintptr_t wp, uintptr_t lp) { // wp: SPI_SETWORKAREA ... SPI_SETDOCKMOVING // SPI_GETACTIVEWINDOWTRACKING ... SPI_SETGESTUREVISUALIZATION if (wp == SPI_SETLOGICALDPIOVERRIDE) { ui_app_init_fonts(ui_app.dpi.window); // font scale changed ui_app.request_layout(); } else if (lp != 0 && (strcmp((const char*)lp, "ImmersiveColorSet") == 0 || wcscmp((const uint16_t*)lp, L"ImmersiveColorSet") == 0)) { // expected: // SPI_SETICONTITLELOGFONT 0x22 ? // SPI_SETNONCLIENTMETRICS 0x2A ? // rt_println("wp: 0x%08X", wp); // actual wp == 0x0000 ui_theme.refresh(); } else if (wp == 0 && lp != 0 && strcmp((const char*)lp, "intl") == 0) { rt_println("wp: 0x%04X", wp); // SPI_SETLOCALEINFO 0x24 ? uint16_t ln[LOCALE_NAME_MAX_LENGTH + 1]; int32_t n = GetUserDefaultLocaleName(ln, rt_countof(ln)); rt_fatal_if(n <= 0); uint16_t rln[LOCALE_NAME_MAX_LENGTH + 1]; n = ResolveLocaleName(ln, rln, rt_countof(rln)); rt_fatal_if(n <= 0); LCID lc_id = LocaleNameToLCID(rln, LOCALE_ALLOW_NEUTRAL_NAMES); rt_fatal_win32err(SetThreadLocale(lc_id)); } } static void ui_app_show_task_bar(bool show) { HWND taskbar = FindWindowA("Shell_TrayWnd", null); if (taskbar != null) { ShowWindow(taskbar, show ? SW_SHOW : SW_HIDE); UpdateWindow(taskbar); } } static bool ui_app_click_detector(uint32_t msg, WPARAM wp, LPARAM lp) { bool swallow = false; enum { tap = 1, long_press = 2, double_tap = 3 }; // TODO: click detector does not handle WM_NCLBUTTONDOWN, ... // it can be modified to do so if needed #pragma push_macro("ui_set_timer") #pragma push_macro("ui_kill_timer") #pragma push_macro("ui_timers_done") #define ui_set_timer(t, ms) do { \ rt_assert(t == 0); \ t = ui_app_timer_set((uintptr_t)&t, ms); \ } while (0) #define ui_kill_timer(t) do { \ if (t != 0) { ui_app_timer_kill(t); t = 0; } \ } while (0) #define ui_timers_done(ix) do { \ clicked[ix] = 0; \ pressed[ix] = false; \ click_at[ix] = (ui_point_t){0, 0}; \ ui_kill_timer(timer_p[ix]); \ ui_kill_timer(timer_d[ix]); \ } while (0) // This function should work regardless to CS_BLKCLK being present // 0: Left, 1: Middle, 2: Right static ui_point_t click_at[3]; static fp64_t clicked[3]; // click time static bool pressed[3]; static ui_timer_t timer_d[3]; // double tap static ui_timer_t timer_p[3]; // long press bool up = false; int32_t ix = -1; int32_t m = 0; switch (msg) { case WM_LBUTTONDOWN : ix = 0; m = tap; break; case WM_MBUTTONDOWN : ix = 1; m = tap; break; case WM_RBUTTONDOWN : ix = 2; m = tap; break; case WM_LBUTTONDBLCLK: ix = 0; m = double_tap; break; case WM_MBUTTONDBLCLK: ix = 1; m = double_tap; break; case WM_RBUTTONDBLCLK: ix = 2; m = double_tap; break; case WM_LBUTTONUP : ix = 0; m = tap; up = true; break; case WM_MBUTTONUP : ix = 1; m = tap; up = true; break; case WM_RBUTTONUP : ix = 2; m = tap; up = true; break; } if (msg == WM_TIMER) { // long press && double tap for (int i = 0; i < 3; i++) { if (wp == timer_p[i]) { ui_app.mouse = (ui_point_t){ click_at[i].x, click_at[i].y }; ui_view.long_press(ui_app.root, i); // rt_println("timer_p[%d] _d && _p timers done", i); ui_timers_done(i); } if (wp == timer_d[i]) { // rt_println("timer_p[%d] _d && _p timers done", i); ui_timers_done(i); } } } if (ix != -1) { ui_app.show_hint(null, -1, -1, 0); // dismiss hint on any click const int32_t double_click_msec = (int32_t)GetDoubleClickTime(); const fp64_t double_click_dt = double_click_msec / 1000.0; // seconds // rt_println("double_click_msec: %d double_click_dt: %.3fs", // double_click_msec, double_click_dt); const int double_click_x = GetSystemMetrics(SM_CXDOUBLECLK) / 2; const int double_click_y = GetSystemMetrics(SM_CYDOUBLECLK) / 2; ui_point_t pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) }; if (m == tap && !up) { swallow = ui_view.tap(ui_app.root, ix, !up); if (ui_app.now - clicked[ix] <= double_click_dt && abs(pt.x - click_at[ix].x) <= double_click_x && abs(pt.y - click_at[ix].y) <= double_click_y) { ui_app.mouse = (ui_point_t){ click_at[ix].x, click_at[ix].y }; ui_view.double_tap(ui_app.root, ix); // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); } else { // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); // clear timers clicked[ix] = ui_app.now; click_at[ix] = pt; pressed[ix] = true; // rt_println("clicked[%d] := %.1f %d,%d pressed[%d] := true", // ix, clicked[ix], pt.x, pt.y, ix); if ((ui_app_wc.style & CS_DBLCLKS) == 0) { // only if Windows are not detecting DLBCLKs // rt_println("ui_set_timer(timer_d[%d])", ix); ui_set_timer(timer_d[ix], double_click_msec); // 0.5s } ui_set_timer(timer_p[ix], double_click_msec * 3 / 4); // 0.375s } } else if (up) { fp64_t since_clicked = ui_app.now - clicked[ix]; // rt_println("pressed[%d]: %d %.3f", ix, pressed[ix], since_clicked); // only if Windows are not detecting DLBCLKs if ((ui_app_wc.style & CS_DBLCLKS) == 0 && pressed[ix] && since_clicked > double_click_dt) { ui_view.double_tap(ui_app.root, ix); // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); } swallow = ui_view.tap(ui_app.root, ix, !up); ui_kill_timer(timer_p[ix]); // long press is not the case } else if (m == double_tap) { rt_assert((ui_app_wc.style & CS_DBLCLKS) != 0); swallow = ui_view.double_tap(ui_app.root, ix); ui_timers_done(ix); // rt_println("timer_p[%d] _d && _p timers done", ix); } } #pragma pop_macro("ui_timers_done") #pragma pop_macro("ui_kill_timer") #pragma pop_macro("ui_set_timer") return swallow; } static int64_t ui_app_root_hit_test(const ui_view_t* v, ui_point_t pt) { rt_swear(v == ui_app.root); if (ui_app.no_decor) { rt_assert(ui_app.border.w == ui_app.border.h); // on 96dpi monitors ui_app.border is 1x1 // make it easier for the user to resize window int32_t border = rt_max(4, ui_app.border.w * 2); if (ui_app.animating.view != null) { return ui.hit_test.client; // message box or toast is up } else if (!ui_view.is_hidden(&ui_caption.view) && ui_view.inside(&ui_caption.view, &pt)) { return ui_caption.view.hit_test(&ui_caption.view, pt); } else if (ui_app.is_maximized()) { int64_t ht = ui_view.hit_test(ui_app.content, pt); return ht == ui.hit_test.nowhere ? ui.hit_test.client : ht; } else if (ui_app.is_full_screen) { return ui.hit_test.client; } else if (pt.x < border && pt.y < border) { return ui.hit_test.top_left; } else if (pt.x > ui_app.crc.w - border && pt.y < border) { return ui.hit_test.top_right; } else if (pt.y < border) { return ui.hit_test.top; } else if (pt.x > ui_app.crc.w - border && pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom_right; } else if (pt.x < border && pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom_left; } else if (pt.x < border) { return ui.hit_test.left; } else if (pt.x > ui_app.crc.w - border) { return ui.hit_test.right; } else if (pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom; } else { // drop down to content hit test } } return ui.hit_test.nowhere; } static void ui_app_wm_activate(int64_t wp) { bool activate = LOWORD(wp) != WA_INACTIVE; if (!IsWindowVisible(ui_app_window()) && activate) { ui_app.show_window(ui.visibility.restore); SwitchToThisWindow(ui_app_window(), true); } ui_app.request_redraw(); // needed for windows changing active frame color } static void ui_app_update_mouse_buttons_state(void) { ui_app.mouse_swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0; ui_app.mouse_left = (GetAsyncKeyState(ui_app.mouse_swapped ? VK_RBUTTON : VK_LBUTTON) & 0x8000) != 0; ui_app.mouse_right = (GetAsyncKeyState(ui_app.mouse_swapped ? VK_LBUTTON : VK_RBUTTON) & 0x8000) != 0; } static int64_t ui_app_wm_nc_hit_test(int64_t wp, int64_t lp) { ui_point_t pt = { GET_X_LPARAM(lp) - ui_app.wrc.x, GET_Y_LPARAM(lp) - ui_app.wrc.y }; int64_t ht = ui_view.hit_test(ui_app.root, pt); if (ht != ui.hit_test.nowhere) { return ht; } else { return DefWindowProcW(ui_app_window(), WM_NCHITTEST, wp, lp); } } static int64_t ui_app_wm_sys_key_down(int64_t wp, int64_t lp) { ui_app_alt_ctrl_shift(true, wp); if (ui_app_wm_key_pressed(ui_app.root, wp) || wp == VK_MENU) { return 0; // no DefWindowProcW() } else { return DefWindowProcW(ui_app_window(), WM_SYSKEYDOWN, wp, lp); } } static void ui_app_wm_set_focus(void) { if (!ui_app.root->state.hidden) { rt_assert(GetActiveWindow() == ui_app_window()); if (ui_app.focus != null && ui_app.focus->focus_lost != null) { ui_app.focus->focus_gained(ui_app.focus); } } } static void ui_app_wm_kill_focus(void) { if (!ui_app.root->state.hidden && ui_app.focus != null && ui_app.focus->focus_lost != null) { ui_app.focus->focus_lost(ui_app.focus); } } static int64_t ui_app_wm_nc_calculate_size(int64_t wp, int64_t lp) { // NCCALCSIZE_PARAMS* szp = (NCCALCSIZE_PARAMS*)lp; // rt_println("WM_NCCALCSIZE wp: %lld is_max: %d (%d %d %d %d) (%d %d %d %d) (%d %d %d %d)", // wp, ui_app.is_maximized(), // szp->rgrc[0].left, szp->rgrc[0].top, szp->rgrc[0].right, szp->rgrc[0].bottom, // szp->rgrc[1].left, szp->rgrc[1].top, szp->rgrc[1].right, szp->rgrc[1].bottom, // szp->rgrc[2].left, szp->rgrc[2].top, szp->rgrc[2].right, szp->rgrc[2].bottom); // adjust window client area frame for no_decor windows if (wp == true && ui_app.no_decor && !ui_app.is_maximized()) { return 0; } else { return DefWindowProcW(ui_app_window(), WM_NCCALCSIZE, wp, lp); } } static int64_t ui_app_wm_get_dpi_scaled_size(int64_t wp) { // sent before WM_DPICHANGED #ifdef UI_APP_DEBUG int32_t dpi = wp; SIZE* sz = (SIZE*)lp; // in/out ui_point_t cell = { sz->cx, sz->cy }; rt_println("WM_GETDPISCALEDSIZE dpi %d := %d " "size %d,%d *may/must* be adjusted", ui_app.dpi.window, dpi, cell.x, cell.y); #else (void)wp; // unused #endif if (ui_app_timer_1s_id != 0 && !ui_app.root->state.hidden) { ui_app.request_layout(); } // IMPORTANT: return true because: // "Returning TRUE indicates that a new size has been computed. // Returning FALSE indicates that the message will not be handled, // and the default linear DPI scaling will apply to the window." // https://learn.microsoft.com/en-us/windows/win32/hidpi/wm-getdpiscaledsize return true; } static void ui_app_wm_dpi_changed(void) { ui_app_window_dpi(); ui_app_init_fonts(ui_app.dpi.window); if (ui_app_timer_1s_id != 0 && !ui_app.root->state.hidden) { ui_app.request_layout(); } else { ui_app_layout_dirty = true; } } static bool ui_app_wm_sys_command(int64_t wp, int64_t lp) { uint16_t sys_cmd = (uint16_t)(wp & 0xFF0); // rt_println("WM_SYSCOMMAND wp: 0x%08llX lp: 0x%016llX %lld sys: 0x%04X", // wp, lp, lp, sys_cmd); if (sys_cmd == SC_MINIMIZE && ui_app.hide_on_minimize) { ui_app.show_window(ui.visibility.min_na); ui_app.show_window(ui.visibility.hide); } else if (sys_cmd == SC_MINIMIZE && ui_app.no_decor) { ui_app.show_window(ui.visibility.min_na); } // if (sys_cmd == SC_KEYMENU) { rt_println("SC_KEYMENU lp: %lld", lp); } // If the selection is in menu handle the key event if (sys_cmd == SC_KEYMENU && lp != 0x20) { return true; // handled: This prevents the error/beep sound } if (sys_cmd == SC_MAXIMIZE && ui_app.no_decor) { return true; // handled: prevent maximizing no decorations window } // if (sys_cmd == SC_MOUSEMENU) { // rt_println("SC_KEYMENU.SC_MOUSEMENU 0x%00llX %lld", wp, lp); // } return false; // drop down to to DefWindowProc } static void ui_app_wm_window_position_changing(int64_t wp, int64_t lp) { #ifdef UI_APP_DEBUG // TODO: ui_app.debug.trace.window_position? WINDOWPOS* pos = (WINDOWPOS*)lp; rt_println("WM_WINDOWPOSCHANGING flags: 0x%08X", pos->flags); if (pos->flags & SWP_SHOWWINDOW) { rt_println("SWP_SHOWWINDOW"); } else if (pos->flags & SWP_HIDEWINDOW) { rt_println("SWP_HIDEWINDOW"); } #else (void)wp; // unused (void)lp; // unused #endif } static bool ui_app_wm_mouse(int32_t m, int64_t wp, int64_t lp) { // note: x, y is already in client coordinates ui_app.mouse.x = GET_X_LPARAM(lp); ui_app.mouse.y = GET_Y_LPARAM(lp); return ui_app_mouse(ui_app.root, m, wp); } static void ui_app_wm_mouse_wheel(bool vertical, int64_t wp) { if (vertical) { ui_point_t dx_dy = { 0, GET_WHEEL_DELTA_WPARAM(wp) }; ui_view.mouse_scroll(ui_app.root, dx_dy); } else { ui_point_t dx_dy = { GET_WHEEL_DELTA_WPARAM(wp), 0 }; ui_view.mouse_scroll(ui_app.root, dx_dy); } } static void ui_app_wm_input_language_change(uint64_t wp) { #ifdef UI_APP_TRACE_WM_INPUT_LANGUAGE_CHANGE static struct { uint8_t charset; const char* name; } cs[] = { { ANSI_CHARSET , "ANSI_CHARSET " }, { DEFAULT_CHARSET , "DEFAULT_CHARSET " }, { SYMBOL_CHARSET , "SYMBOL_CHARSET " }, { MAC_CHARSET , "MAC_CHARSET " }, { SHIFTJIS_CHARSET , "SHIFTJIS_CHARSET " }, { HANGEUL_CHARSET , "HANGEUL_CHARSET " }, { HANGUL_CHARSET , "HANGUL_CHARSET " }, { GB2312_CHARSET , "GB2312_CHARSET " }, { CHINESEBIG5_CHARSET, "CHINESEBIG5_CHARSET" }, { OEM_CHARSET , "OEM_CHARSET " }, { JOHAB_CHARSET , "JOHAB_CHARSET " }, { HEBREW_CHARSET , "HEBREW_CHARSET " }, { ARABIC_CHARSET , "ARABIC_CHARSET " }, { GREEK_CHARSET , "GREEK_CHARSET " }, { TURKISH_CHARSET , "TURKISH_CHARSET " }, { VIETNAMESE_CHARSET , "VIETNAMESE_CHARSET " }, { THAI_CHARSET , "THAI_CHARSET " }, { EASTEUROPE_CHARSET , "EASTEUROPE_CHARSET " }, { RUSSIAN_CHARSET , "RUSSIAN_CHARSET " }, { BALTIC_CHARSET , "BALTIC_CHARSET " } }; for (int32_t i = 0; i < rt_countof(cs); i++) { if (cs[i].charset == wp) { rt_println("WM_INPUTLANGCHANGE: 0x%08X %s", wp, cs[i].name); break; } } #else (void)wp; // unused #endif } static void ui_app_decode_keyboard(int32_t m, int64_t wp, int64_t lp) { // https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags rt_swear(m == WM_KEYDOWN || m == WM_SYSKEYDOWN || m == WM_KEYUP || m == WM_SYSKEYUP); uint16_t vk_code = LOWORD(wp); uint16_t key_flags = HIWORD(lp); uint16_t scan_code = LOBYTE(key_flags); if ((key_flags & KF_EXTENDED) == KF_EXTENDED) { scan_code = MAKEWORD(scan_code, 0xE0); } // previous key-state flag, 1 on autorepeat bool was_key_down = (key_flags & KF_REPEAT) == KF_REPEAT; // repeat count, > 0 if several key down messages was combined into one uint16_t repeat_count = LOWORD(lp); // transition-state flag, 1 on key up bool is_key_released = (key_flags & KF_UP) == KF_UP; // if we want to distinguish these keys: switch (vk_code) { case VK_SHIFT: // converts to VK_LSHIFT or VK_RSHIFT case VK_CONTROL: // converts to VK_LCONTROL or VK_RCONTROL case VK_MENU: // converts to VK_LMENU or VK_RMENU vk_code = LOWORD(MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX)); break; default: break; } static BYTE keyboard_state[256]; uint16_t utf16[3] = {0}; rt_fatal_win32err(GetKeyboardState(keyboard_state)); // HKL low word Language Identifier // high word device handle to the physical layout of the keyboard const HKL kl = GetKeyboardLayout(0); // Map virtual key to scan code UINT vk = MapVirtualKeyEx(scan_code, MAPVK_VSC_TO_VK_EX, kl); // rt_println("virtual_key: %02X keyboard layout: %08X", // virtual_key, kl); memset(ui_app_decoded_released, 0x00, sizeof(ui_app_decoded_released)); memset(ui_app_decoded_pressed, 0x00, sizeof(ui_app_decoded_pressed)); // Translate scan code to character int32_t r = ToUnicodeEx(vk, scan_code, keyboard_state, utf16, rt_countof(utf16), 0, kl); if (r > 0) { rt_static_assertion(rt_countof(ui_app_decoded_pressed) == rt_countof(ui_app_decoded_released)); enum { capacity = (int32_t)rt_countof(ui_app_decoded_released) }; char* utf8 = is_key_released ? ui_app_decoded_released : ui_app_decoded_pressed; rt_str.utf16to8(utf8, capacity, utf16, -1); if (ui_app_trace_utf16_keyboard_input) { rt_println("0x%04X%04X released: %d down: %d repeat: %d \"%s\"", utf16[0], utf16[1], is_key_released, was_key_down, repeat_count, utf8); } } else if (r == 0) { // The specified virtual key has no translation for the // current state of the keyboard. (E.g. arrows, enter etc) } else { rt_assert(r < 0); // The specified virtual key is a dead key character (accent or diacritic). if (ui_app_trace_utf16_keyboard_input) { rt_println("dead key"); } } } static void ui_app_ime_composition(int64_t lp) { if (lp & GCS_RESULTSTR) { HIMC imc = ImmGetContext(ui_app_window()); if (imc != null) { char utf8[16]; uint16_t utf16[4] = {0}; uint32_t bytes = ImmGetCompositionStringW(imc, GCS_RESULTSTR, null, 0); uint32_t count = bytes / sizeof(uint16_t); if (0 < count && count < rt_countof(utf16) - 1) { ImmGetCompositionStringW(imc, GCS_RESULTSTR, utf16, bytes); utf16[count] = 0x00; rt_str.utf16to8(utf8, rt_countof(utf8), utf16, -1); rt_println("bytes: %d 0x%04X 0x%04X %s", bytes, utf16[0], utf16[1], utf8); } rt_fatal_win32err(ImmReleaseContext(ui_app_window(), imc)); } } } static LRESULT CALLBACK ui_app_window_proc(HWND window, UINT message, WPARAM w_param, LPARAM l_param) { ui_app.now = rt_clock.seconds(); if (ui_app.window == null) { ui_app.window = (ui_window_t)window; } else { rt_assert(ui_app_window() == window); } rt_work_queue.dispatch(&ui_app_queue); ui_app_update_wt_timeout(); // because head might have changed const int32_t m = (int32_t)message; const int64_t wp = (int64_t)w_param; const int64_t lp = (int64_t)l_param; int64_t ret = 0; ui_app_update_mouse_buttons_state(); ui_view.lose_hidden_focus(ui_app.root); if (ui_app_click_detector((uint32_t)m, (WPARAM)wp, (LPARAM)lp)) { return 0; } if (ui_view.message(ui_app.root, m, wp, lp, &ret)) { return (LRESULT)ret; } if (m == ui.message.opening) { ui_app_window_opening(); return 0; } if (m == ui.message.closing) { ui_app_window_closing(); return 0; } if (m == ui.message.animate) { ui_app_animate_step((ui_app_animate_function_t)lp, (int32_t)wp, -1); return 0; } ui_app_message_handler_t* handler = ui_app.handlers; while (handler != null) { if (handler->callback(handler, m, wp, lp, &ret)) { return ret; } handler = handler->next; } switch (m) { case WM_GETMINMAXINFO: ui_app_get_min_max_info((MINMAXINFO*)lp); break; case WM_CLOSE : ui_view.set_focus(null); // before WM_CLOSING ui_app_post_message(ui.message.closing, 0, 0); return 0; case WM_DESTROY : PostQuitMessage(ui_app.exit_code); break; case WM_ACTIVATE : ui_app_wm_activate(wp); break; case WM_SYSCOMMAND : if (ui_app_wm_sys_command(wp, lp)) { return 0; } break; case WM_WINDOWPOSCHANGING: ui_app_wm_window_position_changing(wp, lp); break; case WM_WINDOWPOSCHANGED: ui_app_window_position_changed((WINDOWPOS*)lp); break; case WM_NCHITTEST : return ui_app_wm_nc_hit_test(wp, lp); case WM_SYSKEYDOWN : return ui_app_wm_sys_key_down(wp, lp); case WM_SYSCHAR : if (wp == VK_MENU) { return 0; } // swallow - no DefWindowProc() break; case WM_KEYDOWN : ui_app_alt_ctrl_shift(true, wp); if (ui_app_wm_key_pressed(ui_app.root, wp)) { return 0; } // swallow break; case WM_SYSKEYUP: case WM_KEYUP : ui_app_alt_ctrl_shift(false, wp); ui_view.key_released(ui_app.root, wp); break; case WM_TIMER : ui_app_wm_timer((ui_timer_t)wp); break; case WM_ERASEBKGND : return true; // no DefWindowProc() case WM_INPUTLANGCHANGE: ui_app_wm_input_language_change(wp); break; case WM_CHAR : ui_app_wm_char(ui_app.root, (const uint16_t*)&wp); break; case WM_PRINTCLIENT : ui_app_paint_on_canvas((HDC)wp); break; case WM_SETFOCUS : ui_app_wm_set_focus(); break; case WM_KILLFOCUS : ui_app_wm_kill_focus(); break; case WM_NCCALCSIZE: return ui_app_wm_nc_calculate_size(wp, lp); case WM_PAINT : ui_app_wm_paint(); break; case WM_CONTEXTMENU : (void)ui_view.context_menu(ui_app.root); break; case WM_THEMECHANGED : ui_theme.refresh(); break; case WM_SETTINGCHANGE: ui_app_setting_change((uintptr_t)wp, (uintptr_t)lp); break; case WM_GETDPISCALEDSIZE: // sent before WM_DPICHANGED return ui_app_wm_get_dpi_scaled_size(wp); case WM_DPICHANGED : ui_app_wm_dpi_changed(); break; case WM_NCLBUTTONDOWN : case WM_NCRBUTTONDOWN : case WM_NCMBUTTONDOWN : case WM_NCLBUTTONUP : case WM_NCRBUTTONUP : case WM_NCMBUTTONUP : case WM_NCLBUTTONDBLCLK : case WM_NCRBUTTONDBLCLK: case WM_NCMBUTTONDBLCLK: case WM_NCMOUSEMOVE : ui_app_nc_mouse_buttons(m, wp, lp); break; case WM_LBUTTONDOWN : case WM_RBUTTONDOWN : case WM_MBUTTONDOWN : case WM_LBUTTONUP : case WM_RBUTTONUP : case WM_MBUTTONUP : case WM_LBUTTONDBLCLK : case WM_RBUTTONDBLCLK: case WM_MBUTTONDBLCLK: // if (m == WM_LBUTTONDOWN) { rt_println("WM_LBUTTONDOWN"); } // if (m == WM_LBUTTONUP) { rt_println("WM_LBUTTONUP"); } // if (m == WM_LBUTTONDBLCLK) { rt_println("WM_LBUTTONDBLCLK"); } if (ui_app_wm_mouse(m, wp, lp)) { return 0; } break; case WM_MOUSEHOVER : case WM_MOUSEMOVE : if (ui_app_wm_mouse(m, wp, lp)) { return 0; } break; case WM_MOUSEWHEEL : ui_app_wm_mouse_wheel(true, wp); break; case WM_MOUSEHWHEEL : ui_app_wm_mouse_wheel(false, wp); break; // debugging: #ifdef UI_APP_DEBUGING_ALT_KEYBOARD_SHORTCUTS case WM_PARENTNOTIFY : rt_println("WM_PARENTNOTIFY"); break; case WM_ENTERMENULOOP : rt_println("WM_ENTERMENULOOP"); return 0; case WM_EXITMENULOOP : rt_println("WM_EXITMENULOOP"); return 0; case WM_INITMENU : rt_println("WM_INITMENU"); return 0; case WM_MENUCHAR : rt_println("WM_MENUCHAR"); return MNC_CLOSE << 16; case WM_CAPTURECHANGED: rt_println("WM_CAPTURECHANGED"); break; case WM_MENUSELECT : rt_println("WM_MENUSELECT"); return 0; #else // ***Important***: prevents annoying beeps on Alt+Shortcut case WM_MENUCHAR : return MNC_CLOSE << 16; // TODO: may be beeps are good if no UI controls reacted #endif // TODO: investigate WM_SETCURSOR in regards to wait cursor case WM_SETCURSOR : if (LOWORD(lp) == HTCLIENT) { // see WM_NCHITTEST SetCursor((HCURSOR)ui_app.cursor); return true; // must NOT call DefWindowProc() } break; #ifdef UI_APP_USE_WM_IME case WM_IME_CHAR: rt_println("WM_IME_CHAR: 0x%04X", wp); break; case WM_IME_NOTIFY: rt_println("WM_IME_NOTIFY"); break; case WM_IME_REQUEST: rt_println("WM_IME_REQUEST"); break; case WM_IME_STARTCOMPOSITION: rt_println("WM_IME_STARTCOMPOSITION"); break; case WM_IME_ENDCOMPOSITION: rt_println("WM_IME_ENDCOMPOSITION"); break; case WM_IME_COMPOSITION: rt_println("WM_IME_COMPOSITION"); ui_app_ime_composition(lp); break; #endif // UI_APP_USE_WM_IME // TODO: case WM_UNICHAR : // only UTF-32 via PostMessage? rt_println("???"); // see: https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-tounicode break; default: break; } return DefWindowProcW(ui_app_window(), (UINT)m, (WPARAM)wp, lp); } static long ui_app_get_window_long(int32_t index) { rt_core.set_err(0); long v = GetWindowLongA(ui_app_window(), index); rt_fatal_if_error(rt_core.err()); return v; } static long ui_app_set_window_long(int32_t index, long value) { rt_core.set_err(0); long r = SetWindowLongA(ui_app_window(), index, value); // r previous value rt_fatal_if_error(rt_core.err()); return r; } static void ui_app_modify_window_style(uint32_t include, uint32_t exclude) { long s = ui_app_get_window_long(GWL_STYLE); s &= ~exclude; s |= include; ui_app_set_window_long(GWL_STYLE, s); } static DWORD ui_app_window_style(void) { return ui_app.no_decor ? WS_POPUPWINDOW| WS_THICKFRAME| WS_MINIMIZEBOX : WS_OVERLAPPEDWINDOW; } static errno_t ui_app_set_layered_window(ui_color_t color, fp32_t alpha) { uint8_t a = 0; // alpha 0..255 uint32_t c = 0; // R8G8B8 DWORD mask = 0; if (0 <= alpha && alpha <= 1.0f) { mask |= LWA_ALPHA; a = (uint8_t)(alpha * 255 + 0.5f); } if (color != ui_color_undefined) { mask |= LWA_COLORKEY; rt_assert(ui_color_is_8bit(color)); c = ui_gdi.color_rgb(color); } return rt_b2e(SetLayeredWindowAttributes(ui_app_window(), c, a, mask)); } static void ui_app_set_dwm_attribute(uint32_t mode, void* a, DWORD bytes) { rt_fatal_if_error(DwmSetWindowAttribute(ui_app_window(), mode, a, bytes)); } static void ui_app_init_dwm(void) { if (IsWindowsVersionOrGreater(10, 0, 22000)) { // do not call on Win10 - will fail DWM_WINDOW_CORNER_PREFERENCE c = DWMWCP_ROUND; ui_app_set_dwm_attribute(DWMWA_WINDOW_CORNER_PREFERENCE, &c, sizeof(c)); COLORREF cc = (COLORREF)ui_gdi.color_rgb(ui_color_rgb(45, 45, 48)); ui_app_set_dwm_attribute(DWMWA_CAPTION_COLOR, &cc, sizeof(cc)); } BOOL e = true; // must be 32-bit BOOL because of sizeof() ui_app_set_dwm_attribute(DWMWA_USE_IMMERSIVE_DARK_MODE, &e, sizeof(e)); // kudos for double negatives - so easy to make mistakes: ui_app_set_dwm_attribute(DWMWA_TRANSITIONS_FORCEDISABLED, &e, sizeof(e)); enum DWMNCRENDERINGPOLICY rp = DWMNCRP_USEWINDOWSTYLE; ui_app_set_dwm_attribute(DWMWA_NCRENDERING_POLICY, &rp, sizeof(rp)); if (ui_app.no_decor) { ui_app_set_dwm_attribute(DWMWA_ALLOW_NCPAINT, &e, sizeof(e)); MARGINS margins = { 0, 0, 0, 0 }; rt_fatal_if_error( DwmExtendFrameIntoClientArea(ui_app_window(), &margins) ); } } static void ui_app_swp(HWND top, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t f) { rt_fatal_win32err(SetWindowPos(ui_app_window(), top, x, y, w, h, f)); } static void ui_app_swp_flags(uint32_t f) { rt_fatal_win32err(SetWindowPos(ui_app_window(), null, 0, 0, 0, 0, f)); } static void ui_app_disable_sys_menu_item(HMENU sys_menu, uint32_t item) { const uint32_t f = MF_BYCOMMAND | MF_DISABLED; rt_fatal_win32err(EnableMenuItem(sys_menu, item, f)); } static void ui_app_init_sys_menu(void) { // tried to remove unused items from system menu which leads to // AllowDarkModeForWindow() failed 0x000005B0(1456) "A menu item was not found." // SetPreferredAppMode() failed 0x000005B0(1456) "A menu item was not found." // this is why they just disabled instead. HMENU sys_menu = GetSystemMenu(ui_app_window(), false); rt_not_null(sys_menu); if (ui_app.no_min || ui_app.no_max) { int32_t exclude = WS_SIZEBOX; if (ui_app.no_min) { exclude = WS_MINIMIZEBOX; } if (ui_app.no_max) { exclude = WS_MAXIMIZEBOX; } ui_app_modify_window_style(0, exclude); if (ui_app.no_min) { ui_app_disable_sys_menu_item(sys_menu, SC_MINIMIZE); } if (ui_app.no_max) { ui_app_disable_sys_menu_item(sys_menu, SC_MAXIMIZE); } } if (ui_app.no_size) { ui_app_disable_sys_menu_item(sys_menu, SC_SIZE); ui_app_modify_window_style(0, WS_SIZEBOX); const uint32_t f = SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE; ui_app_swp_flags(f); } } static void ui_app_create_window(const ui_rect_t r) { uint16_t class_name[256]; rt_str.utf8to16(class_name, rt_countof(class_name), ui_app.class_name, -1); WNDCLASSW* wc = &ui_app_wc; // CS_DBLCLKS no longer needed. Because code detects long-press // it does double click too. Editor uses both for word and paragraph select. wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_SAVEBITS; wc->lpfnWndProc = ui_app_window_proc; wc->cbClsExtra = 0; wc->cbWndExtra = 256 * 1024; wc->hInstance = GetModuleHandleA(null); wc->hIcon = (HICON)ui_app.icon; wc->hCursor = (HCURSOR)ui_app.cursor; wc->hbrBackground = null; wc->lpszMenuName = null; wc->lpszClassName = class_name; ATOM atom = RegisterClassW(wc); rt_fatal_if(atom == 0); uint16_t title[256]; rt_str.utf8to16(title, rt_countof(title), ui_app.title, -1); HWND window = CreateWindowExW(WS_EX_COMPOSITED | WS_EX_LAYERED, class_name, title, ui_app_window_style(), r.x, r.y, r.w, r.h, null, null, wc->hInstance, null); rt_not_null(ui_app.window); rt_swear(window == ui_app_window()); ui_app.show_window(ui.visibility.hide); ui_view.set_text(&ui_caption.title, "%s", ui_app.title); ui_app.dpi.window = (int32_t)GetDpiForWindow(ui_app_window()); RECT wrc = ui_app_ui2rect(&r); rt_fatal_win32err(GetWindowRect(ui_app_window(), &wrc)); ui_app.wrc = ui_app_rect2ui(&wrc); ui_app_init_dwm(); ui_app_init_sys_menu(); ui_theme.refresh(); if (ui_app.visibility != ui.visibility.hide) { AnimateWindow(ui_app_window(), 250, AW_ACTIVATE); ui_app.show_window(ui_app.visibility); ui_app_update_crc(); } // even if it is hidden: ui_app_post_message(ui.message.opening, 0, 0); // SetWindowTheme(ui_app_window(), L"DarkMode_Explorer", null); ??? } static void ui_app_full_screen(bool on) { static long style; static WINDOWPLACEMENT wp; if (on != ui_app.is_full_screen) { ui_app_show_task_bar(!on); if (on) { ui_app_modify_window_style(0, WS_OVERLAPPEDWINDOW|WS_POPUPWINDOW); ui_app_modify_window_style(WS_POPUP | WS_VISIBLE, 0); wp.length = sizeof(wp); rt_fatal_win32err(GetWindowPlacement(ui_app_window(), &wp)); WINDOWPLACEMENT nwp = wp; nwp.showCmd = SW_SHOWNORMAL; nwp.rcNormalPosition = (RECT){ui_app.mrc.x, ui_app.mrc.y, ui_app.mrc.x + ui_app.mrc.w, ui_app.mrc.y + ui_app.mrc.h}; rt_fatal_win32err(SetWindowPlacement(ui_app_window(), &nwp)); } else { rt_fatal_win32err(SetWindowPlacement(ui_app_window(), &wp)); ui_app_set_window_long(GWL_STYLE, ui_app_window_style()); enum { flags = SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER }; ui_app_swp_flags(flags); } ui_app.is_full_screen = on; } } static bool ui_app_set_focus(ui_view_t* rt_unused(v)) { return false; } static void ui_app_request_redraw(void) { // < 2us SetEvent(ui_app_event_invalidate); } static void ui_app_draw(void) { rt_println("avoid at all cost. bad performance, bad UX"); UpdateWindow(ui_app_window()); } static void ui_app_invalidate_rect(const ui_rect_t* r) { RECT rc = ui_app_ui2rect(r); InvalidateRect(ui_app_window(), &rc, false); // rt_backtrace_here(); } static int32_t ui_app_message_loop(void) { MSG msg = {0}; while (GetMessageW(&msg, null, 0, 0)) { if (msg.message == WM_KEYDOWN || msg.message == WM_KEYUP || msg.message == WM_SYSKEYDOWN || msg.message == WM_SYSKEYUP) { // before TranslateMessage(): ui_app_decode_keyboard(msg.message, msg.wParam, msg.lParam); } TranslateMessage(&msg); DispatchMessageW(&msg); } rt_work_queue.flush(&ui_app_queue); rt_assert(msg.message == WM_QUIT); return (int32_t)msg.wParam; } static void ui_app_dispose(void) { ui_app_dispose_fonts(); rt_event.dispose(ui_app_event_invalidate); ui_app_event_invalidate = null; } static void ui_app_cursor_set(ui_cursor_t c) { // https://docs.microsoft.com/en-us/windows/win32/menurc/using-cursors ui_app.cursor = c; SetClassLongPtr(ui_app_window(), GCLP_HCURSOR, (LONG_PTR)c); POINT pt = {0}; if (GetCursorPos(&pt)) { SetCursorPos(pt.x + 1, pt.y); SetCursorPos(pt.x, pt.y); } } static void ui_app_close_window(void) { // TODO: fix me. Band aid - start up with maximized no_decor window is broken if (ui_app.is_maximized()) { ui_app.show_window(ui.visibility.restore); } ui_app_post_message(WM_CLOSE, 0, 0); } static void ui_app_quit(int32_t exit_code) { ui_app.exit_code = exit_code; if (ui_app.can_close != null) { (void)ui_app.can_close(); // and deliberately ignore result } ui_app.can_close = null; // will not be called again ui_app.close(); // close and destroy app only window } static void ui_app_show_hint_or_toast(ui_view_t* v, int32_t x, int32_t y, fp64_t timeout) { if (v != null) { ui_app.animating.x = x; ui_app.animating.y = y; ui_app.animating.focused = ui_app.focus; if (v->type == ui_view_mbx) { ((ui_mbx_t*)v)->option = -1; if (v->focusable) { ui_view.set_focus(v); } } // allow unparented ui for toast and hint ui_view_call_init(v); const int32_t steps = x < 0 && y < 0 ? ui_app_animation_steps : 1; ui_app_animate_start(ui_app_toast_dim, steps); ui_app.animating.view = v; v->parent = ui_app.root; if (v->focusable) { ui_view.set_focus(v); } ui_app.animating.time = timeout > 0 ? ui_app.now + timeout : 0; } else { ui_app_toast_cancel(); } } static void ui_app_show_toast(ui_view_t* view, fp64_t timeout) { ui_app_show_hint_or_toast(view, -1, -1, timeout); } static void ui_app_show_hint(ui_view_t* view, int32_t x, int32_t y, fp64_t timeout) { if (view != null) { ui_app_show_hint_or_toast(view, x, y, timeout); } else if (ui_app.animating.view != null && ui_app.animating.x >= 0 && ui_app.animating.y >= 0) { ui_app_toast_cancel(); // only cancel hints not toasts } } static void ui_app_formatted_toast_va(fp64_t timeout, const char* format, va_list va) { ui_app_show_toast(null, 0); static ui_label_t label = ui_label(0.0, ""); ui_label_init_va(&label, 0.0, format, va); ui_app_show_toast(&label, timeout); } static void ui_app_formatted_toast(fp64_t timeout, const char* format, ...) { va_list va; va_start(va, format); ui_app_formatted_toast_va(timeout, format, va); va_end(va); } static int32_t ui_app_caret_w; static int32_t ui_app_caret_h; static int32_t ui_app_caret_x = -1; static int32_t ui_app_caret_y = -1; static bool ui_app_caret_shown; static void ui_app_create_caret(int32_t w, int32_t h) { ui_app_caret_w = w; ui_app_caret_h = h; rt_fatal_win32err(CreateCaret(ui_app_window(), null, w, h)); rt_assert(GetSystemMetrics(SM_CARETBLINKINGENABLED)); } static void ui_app_invalidate_caret(void) { if (ui_app_caret_w > 0 && ui_app_caret_h > 0 && ui_app_caret_x >= 0 && ui_app_caret_y >= 0 && ui_app_caret_shown) { RECT rc = { ui_app_caret_x, ui_app_caret_y, ui_app_caret_x + ui_app_caret_w, ui_app_caret_y + ui_app_caret_h }; rt_fatal_win32err(InvalidateRect(ui_app_window(), &rc, false)); } } static void ui_app_show_caret(void) { rt_assert(!ui_app_caret_shown); rt_fatal_win32err(ShowCaret(ui_app_window())); ui_app_caret_shown = true; ui_app_invalidate_caret(); } static void ui_app_move_caret(int32_t x, int32_t y) { ui_app_invalidate_caret(); // where is was ui_app_caret_x = x; ui_app_caret_y = y; rt_fatal_win32err(SetCaretPos(x, y)); ui_app_invalidate_caret(); // where it is now } static void ui_app_hide_caret(void) { rt_assert(ui_app_caret_shown); rt_fatal_win32err(HideCaret(ui_app_window())); ui_app_invalidate_caret(); ui_app_caret_shown = false; } static void ui_app_destroy_caret(void) { ui_app_caret_w = 0; ui_app_caret_h = 0; rt_fatal_win32err(DestroyCaret()); } static void ui_app_beep(int32_t kind) { static int32_t beep_id[] = { MB_OK, MB_ICONINFORMATION, MB_ICONQUESTION, MB_ICONWARNING, MB_ICONERROR}; rt_swear(0 <= kind && kind < rt_countof(beep_id)); rt_fatal_win32err(MessageBeep(beep_id[kind])); } static void ui_app_enable_sys_command_close(void) { EnableMenuItem(GetSystemMenu(GetConsoleWindow(), false), SC_CLOSE, MF_BYCOMMAND | MF_ENABLED); } static void ui_app_console_disable_close(void) { EnableMenuItem(GetSystemMenu(GetConsoleWindow(), false), SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); (void)freopen("CONOUT$", "w", stdout); (void)freopen("CONOUT$", "w", stderr); atexit(ui_app_enable_sys_command_close); } static int ui_app_console_attach(void) { int r = AttachConsole(ATTACH_PARENT_PROCESS) ? 0 : rt_core.err(); if (r == 0) { ui_app_console_disable_close(); rt_thread.sleep_for(0.1); // give cmd.exe a chance to print prompt again printf("\n"); } return r; } static bool ui_app_is_stdout_redirected(void) { // https://stackoverflow.com/questions/30126490/how-to-check-if-stdout-is-redirected-to-a-file-or-to-a-console HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE); DWORD type = out == null ? FILE_TYPE_UNKNOWN : GetFileType(out); type &= ~(DWORD)FILE_TYPE_REMOTE; // FILE_TYPE_DISK or FILE_TYPE_CHAR or FILE_TYPE_PIPE return type != FILE_TYPE_UNKNOWN; } static bool ui_app_is_console_visible(void) { HWND cw = GetConsoleWindow(); return cw != null && IsWindowVisible(cw); } static int ui_app_set_console_size(int16_t w, int16_t h) { // width/height in characters HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); if (r != 0) { rt_println("GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); } else { // tricky because correct order of the calls // SetConsoleWindowInfo() SetConsoleScreenBufferSize() depends on // current Window Size (in pixels) ConsoleWindowSize(in characters) // and SetConsoleScreenBufferSize(). // After a lot of experimentation and reading docs most sensible option // is to try both calls in two different orders. COORD c = {w, h}; SMALL_RECT const min_win = { 0, 0, c.X - 1, c.Y - 1 }; c.Y = 9001; // maximum buffer number of rows at the moment of implementation int r0 = SetConsoleWindowInfo(console, true, &min_win) ? 0 : rt_core.err(); // if (r0 != 0) { rt_println("SetConsoleWindowInfo() %s", rt_strerr(r0)); } int r1 = SetConsoleScreenBufferSize(console, c) ? 0 : rt_core.err(); // if (r1 != 0) { rt_println("SetConsoleScreenBufferSize() %s", rt_strerr(r1)); } if (r0 != 0 || r1 != 0) { // try in reverse order (which expected to work): r0 = SetConsoleScreenBufferSize(console, c) ? 0 : rt_core.err(); if (r0 != 0) { rt_println("SetConsoleScreenBufferSize() %s", rt_strerr(r0)); } r1 = SetConsoleWindowInfo(console, true, &min_win) ? 0 : rt_core.err(); if (r1 != 0) { rt_println("SetConsoleWindowInfo() %s", rt_strerr(r1)); } } r = r0 == 0 ? r1 : r0; // first of two errors } return r; } static void ui_app_console_largest(void) { HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); // User have to manual uncheck "[x] Let system position window" in console // Properties -> Layout -> Window Position because I did not find the way // to programmatically unchecked it. // commented code below does not work. // see: https://www.os2museum.com/wp/disabling-quick-edit-mode/ // and: https://learn.microsoft.com/en-us/windows/console/setconsolemode /* DOES NOT WORK: DWORD mode = 0; r = GetConsoleMode(console, &mode) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleMode() %s", rt_strerr(r)); mode &= ~ENABLE_AUTO_POSITION; r = SetConsoleMode(console, &mode) ? 0 : rt_core.err(); rt_fatal_if_error(r, "SetConsoleMode() %s", rt_strerr(r)); */ CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); COORD c = GetLargestConsoleWindowSize(console); if (c.X > 80) { c.X &= ~0x7; } if (c.Y > 24) { c.Y &= ~0x3; } if (c.X > 80) { c.X -= 8; } if (c.Y > 24) { c.Y -= 4; } ui_app_set_console_size(c.X, c.Y); r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); info.dwSize.Y = 9999; // maximum value at the moment of implementation r = SetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "SetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); ui_app_save_console_pos(); } static void ui_app_make_topmost(void) { // Places the window above all non-topmost windows. // The window maintains its topmost position even when it is deactivated. enum { swp = SWP_SHOWWINDOW | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOSIZE }; ui_app_swp(HWND_TOPMOST, 0, 0, 0, 0, swp); } static void ui_app_activate(void) { rt_core.set_err(0); HWND previous = SetActiveWindow(ui_app_window()); if (previous == null) { rt_fatal_if_error(rt_core.err()); } } static void ui_app_bring_to_foreground(void) { // SetForegroundWindow() does not activate window: rt_fatal_win32err(SetForegroundWindow(ui_app_window())); } static void ui_app_bring_to_front(void) { ui_app.bring_to_foreground(); ui_app.make_topmost(); ui_app.bring_to_foreground(); // because bring_to_foreground() does not activate ui_app.activate(); ui_app.request_focus(); } static void ui_app_set_title(const char* title) { ui_view.set_text(&ui_caption.title, "%s", title); rt_fatal_win32err(SetWindowTextA(ui_app_window(), rt_nls.str(title))); } static void ui_app_capture_mouse(bool on) { static int32_t mouse_capture; if (on) { rt_swear(mouse_capture == 0); mouse_capture++; SetCapture(ui_app_window()); } else { rt_swear(mouse_capture == 1); mouse_capture--; ReleaseCapture(); } } static void ui_app_move_and_resize(const ui_rect_t* rc) { enum { swp = SWP_NOZORDER | SWP_NOACTIVATE }; ui_app_swp(null, rc->x, rc->y, rc->w, rc->h, swp); } static void ui_app_set_console_title(HWND cw) { rt_swear(rt_thread.id() == ui_app.tid); static char text[256]; text[0] = 0; GetWindowTextA((HWND)ui_app.window, text, rt_countof(text)); text[rt_countof(text) - 1] = 0; char title[256]; rt_str_printf(title, "%s - Console", text); rt_fatal_win32err(SetWindowTextA(cw, title)); } static void ui_app_restore_console(int32_t *visibility) { HWND cw = GetConsoleWindow(); if (cw != null) { RECT wr = {0}; GetWindowRect(cw, &wr); ui_rect_t rc = ui_app_rect2ui(&wr); ui_app_load_console_pos(&rc, visibility); if (rc.w > 0 && rc.h > 0) { // rt_println("%d,%d %dx%d px", rc.x, rc.y, rc.w, rc.h); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int32_t r = rt_config.load(ui_app.class_name, "console_screen_buffer_infoex", &info, (int32_t)sizeof(info)); if (r == sizeof(info)) { // 24x80 SMALL_RECT sr = info.srWindow; int16_t w = (int16_t)rt_max(sr.Right - sr.Left + 1, 80); int16_t h = (int16_t)rt_max(sr.Bottom - sr.Top + 1, 24); // rt_println("info: %dx%d", info.dwSize.X, info.dwSize.Y); // rt_println("%d,%d %dx%d", sr.Left, sr.Top, w, h); if (w > 0 && h > 0) { ui_app_set_console_size(w, h); } } // do not resize console window just restore it's position enum { flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE }; rt_fatal_win32err(SetWindowPos(cw, null, rc.x, rc.y, rc.w, rc.h, flags)); } else { ui_app_console_largest(); } } } static void ui_app_console_show(bool b) { HWND cw = GetConsoleWindow(); if (cw != null && b != ui_app.is_console_visible()) { if (ui_app.is_console_visible()) { ui_app_save_console_pos(); } if (b) { int32_t ignored_visibility = 0; ui_app_restore_console(&ignored_visibility); ui_app_set_console_title(cw); } // If the window was previously visible, the return value is nonzero. // If the window was previously hidden, the return value is zero. bool unused_was_visible = ShowWindow(cw, b ? SW_SHOWNOACTIVATE : SW_HIDE); (void)unused_was_visible; if (b) { InvalidateRect(cw, null, true); SetActiveWindow(cw); } ui_app_save_console_pos(); // again after visibility changed } } static int ui_app_console_create(void) { int r = AllocConsole() ? 0 : rt_core.err(); if (r == 0) { ui_app_console_disable_close(); int32_t visibility = 0; ui_app_restore_console(&visibility); ui_app.console_show(visibility != 0); } return r; } static fp32_t ui_app_px2in(int32_t pixels) { rt_assert(ui_app.dpi.monitor_max > 0); // rt_println("ui_app.dpi.monitor_raw: %d", ui_app.dpi.monitor_max); return ui_app.dpi.monitor_max > 0 ? (fp32_t)pixels / (fp32_t)ui_app.dpi.monitor_max : 0; } static int32_t ui_app_in2px(fp32_t inches) { rt_assert(ui_app.dpi.monitor_max > 0); // rt_println("ui_app.dpi.monitor_raw: %d", ui_app.dpi.monitor_max); return (int32_t)(inches * (fp64_t)ui_app.dpi.monitor_max + 0.5); } static void ui_app_request_layout(void) { ui_app_layout_dirty = true; ui_app.request_redraw(); } static void ui_app_show_window(int32_t show) { rt_assert(ui.visibility.hide <= show && show <= ui.visibility.force_min); // ShowWindow() does not have documented error reporting bool was_visible = ShowWindow(ui_app_window(), show); (void)was_visible; const bool hiding = show == ui.visibility.hide || show == ui.visibility.minimize || show == ui.visibility.show_na || show == ui.visibility.min_na; if (!hiding) { ui_app.bring_to_foreground(); // this does not make it ActiveWindow enum { flags = SWP_SHOWWINDOW | SWP_NOZORDER | SWP_NOSIZE | SWP_NOREPOSITION | SWP_NOMOVE }; ui_app_swp_flags(flags); ui_app.request_focus(); } else if (show == ui.visibility.hide || show == ui.visibility.minimize || show == ui.visibility.min_na) { ui_app_toast_cancel(); } } static const char* ui_app_open_file(const char* folder, const char* pairs[], int32_t n) { rt_swear(rt_thread.id() == ui_app.tid); rt_assert(pairs == null && n == 0 || n >= 2 && n % 2 == 0); static uint16_t memory[4 * 1024]; uint16_t* filter = memory; if (pairs == null || n == 0) { filter = L"All Files\0*\0\0"; } else { int32_t left = rt_countof(memory) - 2; uint16_t* s = memory; for (int32_t i = 0; i < n; i+= 2) { uint16_t* s0 = s; rt_str.utf8to16(s0, left, pairs[i + 0], -1); int32_t n0 = (int32_t)rt_str.len16(s0); rt_assert(n0 > 0); s += n0 + 1; left -= n0 + 1; uint16_t* s1 = s; rt_str.utf8to16(s1, left, pairs[i + 1], -1); int32_t n1 = (int32_t)rt_str.len16(s1); rt_assert(n1 > 0); s[n1] = 0; s += n1 + 1; left -= n1 + 1; } *s++ = 0; } static uint16_t dir[rt_files_max_path]; dir[0] = 0; rt_str.utf8to16(dir, rt_countof(dir), folder, -1); static uint16_t path[rt_files_max_path]; path[0] = 0; OPENFILENAMEW ofn = { sizeof(ofn) }; ofn.hwndOwner = (HWND)ui_app.window; ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; ofn.lpstrFilter = filter; ofn.lpstrInitialDir = dir; ofn.lpstrFile = path; ofn.nMaxFile = sizeof(path); static rt_file_name_t fn; fn.s[0] = 0; if (GetOpenFileNameW(&ofn) && path[0] != 0) { rt_str.utf16to8(fn.s, rt_countof(fn.s), path, -1); } else { fn.s[0] = 0; } return fn.s; } // TODO: use clipboard instead? static errno_t ui_app_clipboard_put_image(ui_bitmap_t* im) { HDC canvas = GetDC(null); rt_not_null(canvas); HDC src = CreateCompatibleDC(canvas); rt_not_null(src); HDC dst = CreateCompatibleDC(canvas); rt_not_null(dst); // CreateCompatibleBitmap(dst) will create monochrome bitmap! // CreateCompatibleBitmap(canvas) will create display compatible HBITMAP texture = CreateCompatibleBitmap(canvas, im->w, im->h); rt_not_null(texture); HBITMAP s = SelectBitmap(src, im->texture); rt_not_null(s); HBITMAP d = SelectBitmap(dst, texture); rt_not_null(d); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(dst, 0, 0, &pt)); rt_fatal_win32err(StretchBlt(dst, 0, 0, im->w, im->h, src, 0, 0, im->w, im->h, SRCCOPY)); errno_t r = rt_b2e(OpenClipboard(GetDesktopWindow())); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { r = rt_b2e(EmptyClipboard()); if (r != 0) { rt_println("EmptyClipboard() failed %s", rt_strerr(r)); } } if (r == 0) { r = rt_b2e(SetClipboardData(CF_BITMAP, texture)); if (r != 0) { rt_println("SetClipboardData() failed %s", rt_strerr(r)); } } if (r == 0) { r = rt_b2e(CloseClipboard()); if (r != 0) { rt_println("CloseClipboard() failed %s", rt_strerr(r)); } } rt_not_null(SelectBitmap(dst, d)); rt_not_null(SelectBitmap(src, s)); rt_fatal_win32err(DeleteBitmap(texture)); rt_fatal_win32err(DeleteDC(dst)); rt_fatal_win32err(DeleteDC(src)); rt_fatal_win32err(ReleaseDC(null, canvas)); return r; } static ui_view_t ui_app_view = ui_view(list); static ui_view_t ui_app_content = ui_view(stack); static bool ui_app_is_active(void) { return GetActiveWindow() == ui_app_window(); } static bool ui_app_is_minimized(void) { return IsIconic(ui_app_window()); } static bool ui_app_is_maximized(void) { return IsZoomed(ui_app_window()); } static bool ui_app_focused(void) { return GetFocus() == ui_app_window(); } static void window_request_focus(void* w) { // https://stackoverflow.com/questions/62649124/pywin32-setfocus-resulting-in-access-is-denied-error // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput rt_assert(rt_thread.id() == ui_app.tid, "cannot be called from background thread"); rt_core.set_err(0); HWND previous = SetFocus((HWND)w); // previously focused window if (previous == null) { rt_fatal_if_error(rt_core.err()); } } static void ui_app_request_focus(void) { window_request_focus(ui_app.window); } static void ui_app_init(void) { ui_app_event_quit = rt_event.create_manual(); ui_app_event_invalidate = rt_event.create(); ui_app.request_redraw = ui_app_request_redraw; ui_app.post = ui_app_post; ui_app.draw = ui_app_draw; ui_app.px2in = ui_app_px2in; ui_app.in2px = ui_app_in2px; ui_app.set_layered_window = ui_app_set_layered_window; ui_app.is_active = ui_app_is_active; ui_app.is_minimized = ui_app_is_minimized; ui_app.is_maximized = ui_app_is_maximized; ui_app.focused = ui_app_focused; ui_app.request_focus = ui_app_request_focus; ui_app.activate = ui_app_activate; ui_app.set_title = ui_app_set_title; ui_app.capture_mouse = ui_app_capture_mouse; ui_app.move_and_resize = ui_app_move_and_resize; ui_app.bring_to_foreground = ui_app_bring_to_foreground; ui_app.make_topmost = ui_app_make_topmost; ui_app.bring_to_front = ui_app_bring_to_front; ui_app.request_layout = ui_app_request_layout; ui_app.invalidate = ui_app_invalidate_rect; ui_app.full_screen = ui_app_full_screen; ui_app.set_cursor = ui_app_cursor_set; ui_app.close = ui_app_close_window; ui_app.quit = ui_app_quit; ui_app.set_timer = ui_app_timer_set; ui_app.kill_timer = ui_app_timer_kill; ui_app.show_window = ui_app_show_window; ui_app.show_toast = ui_app_show_toast; ui_app.show_hint = ui_app_show_hint; ui_app.toast_va = ui_app_formatted_toast_va; ui_app.toast = ui_app_formatted_toast; ui_app.create_caret = ui_app_create_caret; ui_app.show_caret = ui_app_show_caret; ui_app.move_caret = ui_app_move_caret; ui_app.hide_caret = ui_app_hide_caret; ui_app.destroy_caret = ui_app_destroy_caret; ui_app.beep = ui_app_beep; ui_app.data_save = ui_app_data_save; ui_app.data_size = ui_app_data_size; ui_app.data_load = ui_app_data_load; ui_app.open_file = ui_app_open_file; ui_app.is_stdout_redirected = ui_app_is_stdout_redirected; ui_app.is_console_visible = ui_app_is_console_visible; ui_app.console_attach = ui_app_console_attach; ui_app.console_create = ui_app_console_create; ui_app.console_show = ui_app_console_show; ui_app.root = &ui_app_view; ui_app.content = &ui_app_content; ui_app.caption = &ui_caption.view; ui_app.root->hit_test = ui_app_root_hit_test; ui_view.add(ui_app.root, ui_app.caption, ui_app.content, null); ui_view_call_init(ui_app.root); // to get done with container_init() rt_assert(ui_app.content->type == ui_view_stack); rt_assert(ui_app.content->background == ui_colors.transparent); ui_app.root->color_id = ui_color_id_window_text; ui_app.root->background_id = ui_color_id_window; ui_app.root->insets = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.root->padding = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.root->paint = ui_app_view_paint; ui_app.root->max_w = ui.infinity; ui_app.root->max_h = ui.infinity; ui_app.content->insets = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.content->padding = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.content->max_w = ui.infinity; ui_app.content->max_h = ui.infinity; ui_app.caption->state.hidden = !ui_app.no_decor; // for ui_view_debug_paint: ui_view.set_text(ui_app.root, "ui_app.root"); ui_view.set_text(ui_app.content, "ui_app.content"); if (ui_app.init != null) { ui_app.init(); } } static void ui_app_set_dpi_awareness(void) { // Mutually exclusive: // BOOL SetProcessDpiAwarenessContext() // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext // and // HRESULT SetProcessDpiAwareness() // https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-setprocessdpiawareness // Plus DPI awareness can be set by APP .exe shell properties, registry // or Windows policy. See: // https://blogs.windows.com/windowsdeveloper/2017/05/19/improving-high-dpi-experience-gdi-based-desktop-apps/ DPI_AWARENESS_CONTEXT dpi_awareness_context_1 = GetThreadDpiAwarenessContext(); // https://blogs.windows.com/windowsdeveloper/2017/05/19/improving-high-dpi-experience-gdi-based-desktop-apps/ errno_t error = rt_b2e(SetProcessDpiAwarenessContext( DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)); if (error == ERROR_ACCESS_DENIED) { rt_println("Warning: SetProcessDpiAwarenessContext(): ERROR_ACCESS_DENIED"); // dpi awareness already set, manifest, registry, windows policy // Try via Shell: HRESULT hr = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); if (hr == E_ACCESSDENIED) { rt_println("Warning: SetProcessDpiAwareness(): E_ACCESSDENIED"); } } DPI_AWARENESS_CONTEXT dpi_awareness_context_2 = GetThreadDpiAwarenessContext(); rt_swear(dpi_awareness_context_1 != dpi_awareness_context_2); } static void ui_app_init_windows(void) { ui_app_set_dpi_awareness(); InitCommonControls(); // otherwise GetOpenFileName does not work ui_app.dpi.process = (int32_t)GetSystemDpiForProcess(GetCurrentProcess()); ui_app.dpi.system = (int32_t)GetDpiForSystem(); // default was 96DPI // monitor dpi will be reinitialized in load_window_pos ui_app.dpi.monitor_effective = ui_app.dpi.system; ui_app.dpi.monitor_angular = ui_app.dpi.system; ui_app.dpi.monitor_raw = ui_app.dpi.system; ui_app.dpi.monitor_max = ui_app.dpi.system; // rt_println("ui_app.dpi.monitor_max := %d", ui_app.dpi.system); static const RECT nowhere = {0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF}; ui_rect_t r = ui_app_rect2ui(&nowhere); ui_app_update_mi(&r, MONITOR_DEFAULTTOPRIMARY); ui_app.dpi.window = ui_app.dpi.monitor_effective; } static ui_rect_t ui_app_window_initial_rectangle(void) { const ui_window_sizing_t* ws = &ui_app.window_sizing; // it is not practical and thus not implemented handling // == (0, 0) and != (0, 0) for sizing half dimension (only w or only h) rt_swear((ws->min_w != 0) == (ws->min_h != 0) && ws->min_w >= 0 && ws->min_h >= 0, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f", ws->min_w, ws->min_h); rt_swear((ws->ini_w != 0) == (ws->ini_h != 0) && ws->ini_w >= 0 && ws->ini_h >= 0, "ui_app.window_sizing .ini_w=%.1f .ini_h=%.1f", ws->ini_w, ws->ini_h); rt_swear((ws->max_w != 0) == (ws->max_h != 0) && ws->max_w >= 0 && ws->max_h >= 0, "ui_app.window_sizing .max_w=%.1f .max_h=%.1f", ws->max_w, ws->max_h); // if max is set then min and ini must be less than max if (ws->max_w != 0 || ws->max_h != 0) { rt_swear(ws->min_w <= ws->max_w && ws->min_h <= ws->max_h, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f .max_w=%1.f .max_h=%.1f", ws->min_w, ws->min_h, ws->max_w, ws->max_h); rt_swear(ws->ini_w <= ws->max_w && ws->ini_h <= ws->max_h, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f .max_w=%1.f .max_h=%.1f", ws->ini_w, ws->ini_h, ws->max_w, ws->max_h); } const int32_t ini_w = ui_app.in2px(ws->ini_w); const int32_t ini_h = ui_app.in2px(ws->ini_h); int32_t min_w = ws->min_w > 0 ? ui_app.in2px(ws->min_w) : ui_app.work_area.w / 4; int32_t min_h = ws->min_h > 0 ? ui_app.in2px(ws->min_h) : ui_app.work_area.h / 4; // (x, y) (-1, -1) means "let Windows manager position the window" ui_rect_t r = {-1, -1, ini_w > 0 ? ini_w : min_w, ini_h > 0 ? ini_h : min_h}; return r; } static FILE* ui_app_crash_log; static bool ui_app_write_backtrace(const char* s, int32_t n) { if (n > 0 && s[n - 1] == 0) { n--; } if (n > 0 && ui_app_crash_log != null) { fwrite(s, n, 1, ui_app_crash_log); } return false; } static LONG ui_app_exception_filter(EXCEPTION_POINTERS* ep) { char fn[1024]; DWORD ex = ep->ExceptionRecord->ExceptionCode; // exception code // T-connector for intercepting rt_debug.output: bool (*tee)(const char* s, int32_t n) = rt_debug.tee; rt_debug.tee = ui_app_write_backtrace; const char* home = rt_files.known_folder(rt_files.folder.home); if (home != null) { const char* name = ui_app.class_name != null ? ui_app.class_name : "ui_app"; rt_str_printf(fn, "%s\\%s_crash_log.txt", home, name); ui_app_crash_log = fopen(fn, "w"); } rt_debug.println(null, 0, null, "To file and issue report copy this log and"); rt_debug.println(null, 0, null, "paste it here: https://github.com/leok7v/ui/discussions/4"); rt_debug.println(null, 0, null, "%s exception: %s", rt_args.basename(), rt_str.error(ex)); rt_backtrace_t bt = {{0}}; rt_backtrace.context(rt_thread.self(), ep->ContextRecord, &bt); rt_backtrace.trace(&bt, "*"); rt_backtrace.trace_all_but_self(); rt_debug.tee = tee; if (ui_app_crash_log != null) { fclose(ui_app_crash_log); char cmd[1024]; rt_str_printf(cmd, "cmd.exe /c start notepad \"%s\"", fn); system(cmd); } return EXCEPTION_CONTINUE_SEARCH; } #undef UI_APP_TEST_POST #ifdef UI_APP_TEST_POST // The dispatch_until() is just for testing purposes. // Usually rt_work_queue.dispatch(q) will be called inside each // iteration of message loop of a dispatch [UI] thread. static void ui_app_test_dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n) { while (q->head != null && *i < n) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(q); } rt_work_queue.flush(q); } // simple way of passing a single pointer to call_later static void ui_app_test_every_100ms(rt_work_t* w) { int32_t* i = (int32_t*)w->data; rt_println("i: %d", *i); (*i)++; w->when = rt_clock.seconds() + 0.100; rt_work_queue.post(w); } static void ui_app_test_work_queue_1(void) { rt_work_queue_t queue = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.100, .work = ui_app_test_every_100ms, .data = &i }; rt_work_queue.post(&work); ui_app_test_dispatch_until(&queue, &i, 4); } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void ui_app_test_every_200ms(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; rt_println("ex { .i: %d, .s.a: %d .s.b: %d}", ex->i, ex->s.a, ex->s.b); ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; w->when = rt_clock.seconds() + 0.200; rt_work_queue.post(w); } static void ui_app_test_work_queue_2(void) { rt_work_queue_t queue = {0}; rt_work_ex_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.200, .work = ui_app_test_every_200ms, .data = null, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&work.base); ui_app_test_dispatch_until(&queue, &work.i, 4); } static fp64_t ui_app_test_timestamp_0; static fp64_t ui_app_test_timestamp_2; static fp64_t ui_app_test_timestamp_3; static fp64_t ui_app_test_timestamp_4; static void ui_app_test_in_1_second(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_3 = rt_clock.seconds(); rt_println("ETA 3 seconds"); } static void ui_app_test_in_2_seconds(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_2 = rt_clock.seconds(); rt_println("ETA 2 seconds"); static rt_work_t invoke_in_1_seconds; invoke_in_1_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 1.0, // seconds .work = ui_app_test_in_1_second }; ui_app.post(&invoke_in_1_seconds); } static void ui_app_test_in_4_seconds(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_4 = rt_clock.seconds(); rt_println("ETA 4 seconds"); // expected sequence of callbacks: // 2:732 ui_app_test_in_2_seconds ETA 2 seconds // 3:724 ui_app_test_in_1_second ETA 3 seconds // 4:735 ui_app_test_in_4_seconds ETA 4 seconds fp64_t dt2 = ui_app_test_timestamp_2 - ui_app_test_timestamp_0; fp64_t dt3 = ui_app_test_timestamp_3 - ui_app_test_timestamp_0; fp64_t dt4 = ui_app_test_timestamp_4 - ui_app_test_timestamp_0; // Assuming there were no huge startup delays: swear(1.75 < dt2 < 2.25); swear(2.75 < dt3 < 3.25); swear(3.75 < dt4 < 4.25); } static void ui_app_test_post(void) { ui_app_test_work_queue_1(); ui_app_test_work_queue_2(); rt_println("see Output/Timestamps"); static rt_work_t invoke_in_2_seconds; static rt_work_t invoke_in_4_seconds; ui_app_test_timestamp_0 = rt_clock.seconds(); invoke_in_2_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 2.0, // seconds .work = ui_app_test_in_2_seconds }; invoke_in_4_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 4.0, // seconds .work = ui_app_test_in_4_seconds }; ui_app.post(&invoke_in_4_seconds); ui_app.post(&invoke_in_2_seconds); } #endif static int ui_app_win_main(HINSTANCE instance) { // IDI_ICON 101: ui_app.icon = (ui_icon_t)LoadIconW(instance, MAKEINTRESOURCE(101)); ui_app_init_windows(); ui_gdi.init(); rt_clipboard.put_image = ui_app_clipboard_put_image; ui_app.last_visibility = ui.visibility.defau1t; ui_app_init(); int r = 0; // ui_app_dump_dpi(); // It is possible (but not trivial) to ask DWM to create taller tittle bar: // https://learn.microsoft.com/en-us/windows/win32/dwm/customframe // TODO: if any app need to make to app store they will probably ask for it // "wr" Window Rect in pixels: default is -1,-1, ini_w, ini_h ui_rect_t wr = ui_app_window_initial_rectangle(); ui_app.caption_height = (int32_t)GetSystemMetricsForDpi(SM_CYCAPTION, (uint32_t)ui_app.dpi.process); ui_app.border.w = (int32_t)GetSystemMetricsForDpi(SM_CXSIZEFRAME, (uint32_t)ui_app.dpi.process); ui_app.border.h = (int32_t)GetSystemMetricsForDpi(SM_CYSIZEFRAME, (uint32_t)ui_app.dpi.process); if (ui_app.no_decor) { // border is too think (5 pixels) narrow down to 3x3 const int32_t max_border = ui_app.dpi.window <= 100 ? 1 : (ui_app.dpi.window >= 192 ? 3 : 2); ui_app.border.w = rt_min(max_border, ui_app.border.w); ui_app.border.h = rt_min(max_border, ui_app.border.h); } // rt_println("frame: %d,%d caption_height: %d", ui_app.border.w, ui_app.border.h, ui_app.caption_height); // TODO: use AdjustWindowRectEx instead // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-adjustwindowrectex wr.x -= ui_app.border.w; wr.w += ui_app.border.w * 2; wr.y -= ui_app.border.h + ui_app.caption_height; wr.h += ui_app.border.h * 2 + ui_app.caption_height; if (!ui_app_load_window_pos(&wr, &ui_app.last_visibility)) { // first time - center window wr.x = ui_app.work_area.x + (ui_app.work_area.w - wr.w) / 2; wr.y = ui_app.work_area.y + (ui_app.work_area.h - wr.h) / 2; ui_app_bring_window_inside_monitor(&ui_app.mrc, &wr); } ui_app.root->state.hidden = true; // start with ui hidden ui_app.root->fm = &ui_app.fm.prop.normal; ui_app.root->w = wr.w - ui_app.border.w * 2; ui_app.root->h = wr.h - ui_app.border.h * 2 - ui_app.caption_height; ui_app_layout_dirty = true; // layout will be done before first paint rt_not_null(ui_app.class_name); ui_app_wt = (rt_event_t)CreateWaitableTimerA(null, false, null); rt_thread_t alarm = rt_thread.start(ui_app_alarm_thread, null); if (!ui_app.no_ui) { ui_app_create_window(wr); ui_app_init_fonts(ui_app.dpi.window); rt_thread_t redraw = rt_thread.start(ui_app_redraw_thread, null); #ifdef UI_APP_TEST_POST ui_app_test_post(); #endif r = ui_app_message_loop(); // ui_app.fini() must be called before ui_app_dispose() if (ui_app.fini != null) { ui_app.fini(); } rt_event.set(ui_app_event_quit); rt_thread.join(redraw, -1); ui_app_dispose(); if (r == 0 && ui_app.exit_code != 0) { r = ui_app.exit_code; } } else { r = ui_app.main(); if (ui_app.fini != null) { ui_app.fini(); } } rt_event.set(ui_app_event_quit); rt_thread.join(alarm, -1); rt_event.dispose(ui_app_event_quit); ui_app_event_quit = null; rt_event.dispose(ui_app_wt); ui_app_wt = null; ui_gdi.fini(); return r; } #pragma warning(disable: 28251) // inconsistent annotations int WINAPI WinMain(HINSTANCE instance, HINSTANCE rt_unused(previous), char* rt_unused(command), int show) { SetUnhandledExceptionFilter(ui_app_exception_filter); const COINIT co_init = COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY; rt_fatal_if_error(CoInitializeEx(0, co_init)); SetConsoleCP(CP_UTF8); // Expected manifest.xml containing UTF-8 code page // for TranslateMessage and WM_CHAR to deliver UTF-8 characters // see: // https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page // .rc file must have: // 1 RT_MANIFEST "manifest.xml" if (GetACP() != 65001) { rt_println("codepage: %d UTF-8 will not be supported", GetACP()); } // at the moment of writing there is no API call to inform Windows about process // preferred codepage except manifest.xml file in resource #1. // Absence of manifest.xml will result to ancient and useless ANSI 1252 codepage // TODO: may need to change CreateWindowA() to CreateWindowW() and // translate UTF16 to UTF8 ui_app.tid = rt_thread.id(); rt_nls.init(); ui_app.visibility = show; rt_args.WinMain(); int32_t r = ui_app_win_main(instance); rt_args.fini(); return r; } int main(int argc, const char* argv[], const char** envp) { SetUnhandledExceptionFilter(ui_app_exception_filter); rt_fatal_if_error(CoInitializeEx(0, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY)); rt_args.main(argc, argv, envp); rt_nls.init(); ui_app.tid = rt_thread.id(); int r = ui_app.main(); rt_args.fini(); return r; } #pragma pop_macro("ui_app_canvas") #pragma pop_macro("ui_app_window") #pragma comment(lib, "comctl32") #pragma comment(lib, "comdlg32") #pragma comment(lib, "dwmapi") #pragma comment(lib, "gdi32") #pragma comment(lib, "msimg32") #pragma comment(lib, "shcore") #pragma comment(lib, "uxtheme") // _______________________________ ui_button.c ________________________________ #include "rt/rt.h" static void ui_button_every_100ms(ui_view_t* v) { // every 100ms if (!v->state.hidden) { v->p.armed_until = 0; v->state.armed = false; } else if (v->p.armed_until != 0 && ui_app.now > v->p.armed_until) { v->p.armed_until = 0; v->state.armed = false; ui_view.invalidate(v, null); } if (v->p.armed_until != 0) { ui_app.show_hint(null, -1, -1, 0); } } static void ui_button_paint(ui_view_t* v) { bool pressed = (v->state.armed ^ v->state.pressed) == 0; if (v->p.armed_until != 0) { pressed = true; } const int32_t w = v->w; const int32_t h = v->h; const int32_t x = v->x; const int32_t y = v->y; const int32_t r = (0x1 | rt_max(3, v->fm->em.h / 4)); // odd radius const fp32_t d = ui_theme.is_app_dark() ? 0.50f : 0.25f; ui_color_t d0 = ui_colors.darken(v->background, d); const fp32_t d2 = d / 2; if (v->flat) { if (v->state.hover) { ui_color_t d1 = ui_theme.is_app_dark() ? ui_colors.lighten(v->background, d2) : ui_colors.darken(v->background, d2); if (!pressed) { ui_gdi.gradient(x, y, w, h, d0, d1, true); } else { ui_gdi.gradient(x, y, w, h, d1, d0, true); } } } else { // `bc` border color ui_color_t bc = ui_colors.get_color(ui_color_id_gray_text); if (v->state.armed) { bc = ui_colors.lighten(bc, 0.125f); } if (ui_view.is_disabled(v)) { bc = ui_color_rgb(30, 30, 30); } // TODO: hardcoded if (v->state.hover && !v->state.armed) { bc = ui_colors.get_color(ui_color_id_hot_tracking); } ui_color_t d1 = ui_colors.darken(v->background, d2); ui_color_t fc = ui_colors.interpolate(d0, d1, 0.5f); // fill color if (v->state.armed) { fc = ui_colors.lighten(fc, 0.250f); } else if (v->state.hover) { fc = ui_colors.darken(fc, 0.250f); } ui_gdi.rounded(v->x, v->y, v->w, v->h, r, bc, fc); } const int32_t tx = v->x + v->text.xy.x; const int32_t ty = v->y + v->text.xy.y; if (v->icon == null) { ui_color_t c = v->color; if (v->state.hover && !v->state.armed) { c = ui_theme.is_app_dark() ? ui_color_rgb(0xFF, 0xE0, 0xE0) : ui_color_rgb(0x00, 0x40, 0xFF); } if (ui_view.is_disabled(v)) { c = ui_colors.get_color(ui_color_id_gray_text); } if (v->debug.paint.fm) { ui_view.debug_paint_fm(v); } const ui_gdi_ta_t ta = { .fm = v->fm, .color = c }; ui_gdi.text(&ta, tx, ty, "%s", ui_view.string(v)); } else { const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_wh_t i_wh = { .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom }; // TODO: icon text alignment ui_gdi.icon(tx, ty + v->text.xy.y, i_wh.w, i_wh.h, v->icon); } } static void ui_button_callback(ui_button_t* b) { // for flip buttons the state of the button flips // *before* callback. if (b->flip) { b->state.pressed = !b->state.pressed; } const bool pressed = b->state.pressed; if (b->callback != null) { b->callback(b); } if (pressed != b->state.pressed) { if (b->flip) { // warn the client of strange logic: rt_println("strange flip the button with button.flip: true"); // if client wants to flip pressed state manually it // should do it for the button.flip = false } // rt_println("disarmed immediately"); b->p.armed_until = 0; b->state.armed = false; } else { if (b->flip) { // rt_println("disarmed immediately"); b->p.armed_until = 0; b->state.armed = false; } else { // rt_println("will disarm in 1/4 seconds"); b->p.armed_until = ui_app.now + 0.250; } } } static void ui_button_trigger(ui_view_t* v) { ui_button_t* b = (ui_button_t*)v; v->state.armed = true; ui_view.invalidate(v, null); ui_button_callback(b); } static void ui_button_character(ui_view_t* v, const char* utf8) { char ch = utf8[0]; // TODO: multibyte utf8 shortcuts? if (ui_view.is_shortcut_key(v, ch)) { ui_button_trigger(v); } } static bool ui_button_key_pressed(ui_view_t* v, int64_t key) { rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); const bool trigger = ui_app.alt && ui_view.is_shortcut_key(v, key); if (trigger) { ui_button_trigger(v); } return trigger; // swallow if true } static bool ui_button_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { // 'ix' ignored - button index acts on any mouse button const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { ui_view.invalidate(v, null); // always on any press/release inside ui_button_t* b = (ui_button_t*)v; if (pressed && b->flip) { if (b->flip) { ui_button_callback(b); } } else if (pressed) { v->state.armed = true; } else { // released if (!b->flip) { ui_button_callback(b); } } } return pressed && inside; // swallow clicks inside } void ui_view_init_button(ui_view_t* v) { rt_assert(v->type == ui_view_button); v->tap = ui_button_tap; v->paint = ui_button_paint; v->character = ui_button_character; v->every_100ms = ui_button_every_100ms; v->key_pressed = ui_button_key_pressed; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; if (v->debug.id == null) { v->debug.id = "#button"; } } void ui_button_init(ui_button_t* b, const char* label, fp32_t ems, void (*callback)(ui_button_t* b)) { b->type = ui_view_button; ui_view.set_text(b, "%s", label); b->callback = callback; b->min_w_em = ems; ui_view_init_button(b); } // _______________________________ ui_caption.c _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #pragma push_macro("ui_caption_glyph_rest") #pragma push_macro("ui_caption_glyph_menu") #pragma push_macro("ui_caption_glyph_dark") #pragma push_macro("ui_caption_glyph_light") #pragma push_macro("ui_caption_glyph_mini") #pragma push_macro("ui_caption_glyph_maxi") #pragma push_macro("ui_caption_glyph_full") #pragma push_macro("ui_caption_glyph_quit") #define ui_caption_glyph_rest rt_glyph_white_square_with_upper_right_quadrant // instead of rt_glyph_desktop_window #define ui_caption_glyph_menu rt_glyph_trigram_for_heaven #define ui_caption_glyph_dark rt_glyph_crescent_moon #define ui_caption_glyph_light rt_glyph_white_sun_with_rays #define ui_caption_glyph_mini rt_glyph_minimize #define ui_caption_glyph_maxi rt_glyph_white_square_with_lower_left_quadrant // instead of rt_glyph_maximize #define ui_caption_glyph_full rt_glyph_square_four_corners #define ui_caption_glyph_quit rt_glyph_cancellation_x static void ui_caption_toggle_full(void) { ui_app.full_screen(!ui_app.is_full_screen); ui_caption.view.state.hidden = ui_app.is_full_screen; ui_app.request_layout(); } static void ui_caption_esc_full_screen(ui_view_t* v, const char utf8[]) { rt_swear(v == ui_caption.view.parent); // TODO: inside ui_app.c instead of here? if (utf8[0] == 033 && ui_app.is_full_screen) { ui_caption_toggle_full(); } } static void ui_caption_quit(ui_button_t* rt_unused(b)) { ui_app.close(); } static void ui_caption_mini(ui_button_t* rt_unused(b)) { ui_app.show_window(ui.visibility.minimize); } static void ui_caption_mode_appearance(void) { if (ui_theme.is_app_dark()) { ui_view.set_text(&ui_caption.mode, "%s", ui_caption_glyph_light); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Light Mode")); } else { ui_view.set_text(&ui_caption.mode, "%s", ui_caption_glyph_dark); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Dark Mode")); } } static void ui_caption_mode(ui_button_t* rt_unused(b)) { bool was_dark = ui_theme.is_app_dark(); ui_app.light_mode = was_dark; ui_app.dark_mode = !was_dark; ui_theme.refresh(); ui_caption_mode_appearance(); } static void ui_caption_maximize_or_restore(void) { ui_view.set_text(&ui_caption.maxi, "%s", ui_app.is_maximized() ? ui_caption_glyph_rest : ui_caption_glyph_maxi); rt_str_printf(ui_caption.maxi.hint, "%s", ui_app.is_maximized() ? rt_nls.str("Restore") : rt_nls.str("Maximize")); // non-decorated windows on Win32 are "popup" style // that cannot be maximized. Full screen will serve // the purpose of maximization. ui_caption.maxi.state.hidden = ui_app.no_decor; } static void ui_caption_maxi(ui_button_t* rt_unused(b)) { if (!ui_app.is_maximized()) { ui_app.show_window(ui.visibility.maximize); } else if (ui_app.is_maximized() || ui_app.is_minimized()) { ui_app.show_window(ui.visibility.restore); } ui_caption_maximize_or_restore(); } static void ui_caption_full(ui_button_t* rt_unused(b)) { ui_caption_toggle_full(); } static int64_t ui_caption_hit_test(const ui_view_t* v, ui_point_t pt) { rt_swear(v == &ui_caption.view); rt_assert(ui_view.inside(v, &pt)); // rt_println("%d,%d ui_caption.icon: %d,%d %dx%d inside: %d", // x, y, // ui_caption.icon.x, ui_caption.icon.y, // ui_caption.icon.w, ui_caption.icon.h, // ui_view.inside(&ui_caption.icon, &pt)); if (ui_app.is_full_screen) { return ui.hit_test.client; } else if (!ui_caption.icon.state.hidden && ui_view.inside(&ui_caption.icon, &pt)) { return ui.hit_test.system_menu; } else { ui_view_for_each(&ui_caption.view, c, { bool ignore = c->type == ui_view_stack || c->type == ui_view_spacer || c->type == ui_view_label; if (!ignore && ui_view.inside(c, &pt)) { return ui.hit_test.client; } }); return ui.hit_test.caption; } } static ui_color_t ui_caption_color(void) { ui_color_t c = ui_app.is_active() ? ui_colors.get_color(ui_color_id_active_title) : ui_colors.get_color(ui_color_id_inactive_title); return c; } static const ui_margins_t ui_caption_button_button_padding = { .left = 0.25, .top = 0.0, .right = 0.25, .bottom = 0.0}; static void ui_caption_button_measure(ui_view_t* v) { rt_assert(v->type == ui_view_button); ui_view.measure_control(v); const int32_t dx = ui_app.caption_height - v->w; const int32_t dy = ui_app.caption_height - v->h; v->w += dx; v->h += dy; v->text.xy.x += dx / 2; v->text.xy.y += dy / 2; v->padding = ui_caption_button_button_padding; } static void ui_caption_button_icon_paint(ui_view_t* v) { int32_t w = v->w; int32_t h = v->h; while (h > 16 && (h & (h - 1)) != 0) { h--; } w = h; int32_t dx = (v->w - w) / 2; int32_t dy = (v->h - h) / 2; ui_gdi.icon(v->x + dx, v->y + dy, w, h, v->icon); } static void ui_caption_prepare(ui_view_t* rt_unused(v)) { ui_caption.title.state.hidden = false; } static void ui_caption_measured(ui_view_t* v) { // remeasure all child buttons with hard override: int32_t w = 0; ui_view_for_each(v, it, { if (it->type == ui_view_button) { it->fm = &ui_app.fm.mono.normal; it->flat = true; ui_caption_button_measure(it); } if (!it->state.hidden) { const ui_ltrb_t p = ui_view.margins(it, &it->padding); w += it->w + p.left + p.right; } }); const ui_ltrb_t p = ui_view.margins(v, &v->padding); w += p.left + p.right; // do not show title if there is not enough space ui_caption.title.state.hidden = w > ui_app.root->w; v->w = ui_app.root->w; const ui_ltrb_t insets = ui_view.margins(v, &v->insets); v->h = insets.top + ui_app.caption_height + insets.bottom; } static void ui_caption_composed(ui_view_t* v) { v->x = ui_app.root->x; v->y = ui_app.root->y; } static void ui_caption_paint(ui_view_t* v) { ui_color_t background = ui_caption_color(); ui_gdi.fill(v->x, v->y, v->w, v->h, background); } static void ui_caption_init(ui_view_t* v) { rt_swear(v == &ui_caption.view, "caption is a singleton"); ui_view_init_span(v); ui_caption.view.insets = (ui_margins_t){ 0.125, 0.0, 0.125, 0.0 }; ui_caption.view.state.hidden = false; v->parent->character = ui_caption_esc_full_screen; // ESC for full screen ui_view.add(&ui_caption.view, &ui_caption.icon, &ui_caption.menu, &ui_caption.title, &ui_caption.spacer, &ui_caption.mode, &ui_caption.mini, &ui_caption.maxi, &ui_caption.full, &ui_caption.quit, null); ui_caption.view.color_id = ui_color_id_window_text; static const ui_margins_t p0 = { .left = 0.0, .top = 0.0, .right = 0.0, .bottom = 0.0}; static const ui_margins_t pd = { .left = 0.25, .top = 0.0, .right = 0.25, .bottom = 0.0}; static const ui_margins_t in = { .left = 0.0, .top = 0.0, .right = 0.0, .bottom = 0.0}; ui_view_for_each(&ui_caption.view, c, { c->fm = &ui_app.fm.prop.normal; c->color_id = ui_caption.view.color_id; if (c->type != ui_view_button) { c->padding = pd; } c->insets = in; c->h = ui_app.caption_height; c->min_w_em = 0.5f; c->min_h_em = 0.5f; }); rt_str_printf(ui_caption.menu.hint, "%s", rt_nls.str("Menu")); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Light Mode")); rt_str_printf(ui_caption.mini.hint, "%s", rt_nls.str("Minimize")); rt_str_printf(ui_caption.maxi.hint, "%s", rt_nls.str("Maximize")); rt_str_printf(ui_caption.full.hint, "%s", rt_nls.str("Full Screen (ESC to restore)")); rt_str_printf(ui_caption.quit.hint, "%s", rt_nls.str("Close")); ui_caption.icon.icon = ui_app.icon; ui_caption.icon.padding = p0; ui_caption.icon.paint = ui_caption_button_icon_paint; ui_caption.view.align = ui.align.left; ui_caption.view.prepare = ui_caption_prepare; ui_caption.view.measured = ui_caption_measured; ui_caption.view.composed = ui_caption_composed; ui_view.set_text(&ui_caption.view, "#ui_caption"); // for debugging ui_caption_maximize_or_restore(); ui_caption.view.paint = ui_caption_paint; ui_caption_mode_appearance(); ui_caption.icon.debug.id = "#caption.icon"; ui_caption.menu.debug.id = "#caption.menu"; ui_caption.mode.debug.id = "#caption.mode"; ui_caption.mini.debug.id = "#caption.mini"; ui_caption.maxi.debug.id = "#caption.maxi"; ui_caption.full.debug.id = "#caption.full"; ui_caption.quit.debug.id = "#caption.quit"; ui_caption.title.debug.id = "#caption.title"; ui_caption.spacer.debug.id = "#caption.spacer"; } ui_caption_t ui_caption = { .view = { .type = ui_view_span, .fm = &ui_app.fm.prop.normal, .init = ui_caption_init, .hit_test = ui_caption_hit_test, .state.hidden = true }, .icon = ui_button(rt_glyph_nbsp, 0.0, null), .title = ui_label(0, ""), .spacer = ui_view(spacer), .menu = ui_button(ui_caption_glyph_menu, 0.0, null), .mode = ui_button(ui_caption_glyph_mini, 0.0, ui_caption_mode), .mini = ui_button(ui_caption_glyph_mini, 0.0, ui_caption_mini), .maxi = ui_button(ui_caption_glyph_maxi, 0.0, ui_caption_maxi), .full = ui_button(ui_caption_glyph_full, 0.0, ui_caption_full), .quit = ui_button(ui_caption_glyph_quit, 0.0, ui_caption_quit), }; #pragma pop_macro("ui_caption_glyph_rest") #pragma pop_macro("ui_caption_glyph_menu") #pragma pop_macro("ui_caption_glyph_dark") #pragma pop_macro("ui_caption_glyph_light") #pragma pop_macro("ui_caption_glyph_mini") #pragma pop_macro("ui_caption_glyph_maxi") #pragma pop_macro("ui_caption_glyph_full") #pragma pop_macro("ui_caption_glyph_quit") // _______________________________ ui_colors.c ________________________________ #include "rt/rt.h" static inline uint8_t ui_color_clamp_uint8(fp64_t value) { return value < 0 ? 0 : (value > 255 ? 255 : (uint8_t)value); } static inline fp64_t ui_color_fp64_min(fp64_t x, fp64_t y) { return x < y ? x : y; } static inline fp64_t ui_color_fp64_max(fp64_t x, fp64_t y) { return x > y ? x : y; } static void ui_color_rgb_to_hsi(fp64_t r, fp64_t g, fp64_t b, fp64_t *h, fp64_t *s, fp64_t *i) { r /= 255.0; g /= 255.0; b /= 255.0; fp64_t min_val = ui_color_fp64_min(r, ui_color_fp64_min(g, b)); *i = (r + g + b) / 3; fp64_t chroma = ui_color_fp64_max(r, ui_color_fp64_max(g, b)) - min_val; if (chroma == 0) { *h = 0; *s = 0; } else { *s = 1 - min_val / *i; if (*i > 0) { *s = chroma / (*i * 3); } if (r == ui_color_fp64_max(r, ui_color_fp64_max(g, b))) { *h = (g - b) / chroma + (g < b ? 6 : 0); } else if (g == ui_color_fp64_max(r, ui_color_fp64_max(g, b))) { *h = (b - r) / chroma + 2; } else { *h = (r - g) / chroma + 4; } *h *= 60; } } static ui_color_t ui_color_hsi_to_rgb(fp64_t h, fp64_t s, fp64_t i, uint8_t a) { h /= 60.0; fp64_t f = h - (int32_t)h; fp64_t p = i * (1 - s); fp64_t q = i * (1 - s * f); fp64_t t = i * (1 - s * (1 - f)); fp64_t r = 0, g = 0, b = 0; switch ((int32_t)h) { case 0: case 6: r = i * 255; g = t * 255; b = p * 255; break; case 1: r = q * 255; g = i * 255; b = p * 255; break; case 2: r = p * 255; g = i * 255; b = t * 255; break; case 3: r = p * 255; g = q * 255; b = i * 255; break; case 4: r = t * 255; g = p * 255; b = i * 255; break; case 5: r = i * 255; g = p * 255; b = q * 255; break; default: rt_swear(false); break; } rt_assert(0 <= r && r <= 255); rt_assert(0 <= g && g <= 255); rt_assert(0 <= b && b <= 255); return ui_color_rgba((uint8_t)r, (uint8_t)g, (uint8_t)b, a); } static ui_color_t ui_color_brightness(ui_color_t c, fp32_t multiplier) { fp64_t h, s, i; ui_color_rgb_to_hsi(ui_color_r(c), ui_color_g(c), ui_color_b(c), &h, &s, &i); i = ui_color_fp64_max(0, ui_color_fp64_min(1, i * (fp64_t)multiplier)); return ui_color_hsi_to_rgb(h, s, i, ui_color_a(c)); } static ui_color_t ui_color_saturation(ui_color_t c, fp32_t multiplier) { fp64_t h, s, i; ui_color_rgb_to_hsi(ui_color_r(c), ui_color_g(c), ui_color_b(c), &h, &s, &i); s = ui_color_fp64_max(0, ui_color_fp64_min(1, s * (fp64_t)multiplier)); return ui_color_hsi_to_rgb(h, s, i, ui_color_a(c)); } // Using the ui_color_interpolate function to blend colors toward // black or white can effectively adjust brightness and saturation, // offering more flexibility and potentially better results in // terms of visual transitions between colors. static ui_color_t ui_color_interpolate(ui_color_t c0, ui_color_t c1, fp32_t multiplier) { rt_assert(0.0f < multiplier && multiplier < 1.0f); fp64_t h0, s0, i0, h1, s1, i1; ui_color_rgb_to_hsi(ui_color_r(c0), ui_color_g(c0), ui_color_b(c0), &h0, &s0, &i0); ui_color_rgb_to_hsi(ui_color_r(c1), ui_color_g(c1), ui_color_b(c1), &h1, &s1, &i1); fp64_t h = h0 + (h1 - h0) * (fp64_t)multiplier; fp64_t s = s0 + (s1 - s0) * (fp64_t)multiplier; fp64_t i = i0 + (i1 - i0) * (fp64_t)multiplier; // Interpolate alphas only if differ uint8_t a0 = ui_color_a(c0); uint8_t a1 = ui_color_a(c1); uint8_t a = a0 == a1 ? a0 : ui_color_clamp_uint8(a0 + (a1 - a0) * (fp64_t)multiplier); return ui_color_hsi_to_rgb(h, s, i, a); } // Helper to get a neutral gray with the same intensity static ui_color_t ui_color_gray_with_same_intensity(ui_color_t c) { uint8_t intensity = (ui_color_r(c) + ui_color_g(c) + ui_color_b(c)) / 3; return ui_color_rgba(intensity, intensity, intensity, ui_color_a(c)); } // Adjust brightness by interpolating towards black or white // using interpolation: // // To darken the color: Interpolate between // the color and black (rgba(0,0,0,255)). // // To lighten the color: Interpolate between // the color and white (rgba(255,255,255,255)). // // This approach allows you to manipulate the // brightness by specifying how close the color // should be to either black or white, // providing a smooth transition. static ui_color_t ui_color_adjust_brightness(ui_color_t c, fp32_t multiplier, bool lighten) { ui_color_t target = lighten ? ui_color_rgba(255, 255, 255, ui_color_a(c)) : ui_color_rgba( 0, 0, 0, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } static ui_color_t ui_color_lighten(ui_color_t c, fp32_t multiplier) { const ui_color_t target = ui_color_rgba(255, 255, 255, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } static ui_color_t ui_color_darken(ui_color_t c, fp32_t multiplier) { const ui_color_t target = ui_color_rgba(0, 0, 0, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } // Adjust saturation by interpolating towards a gray of the same intensity // // To adjust saturation, the approach is similar but slightly // more nuanced because saturation involves both the color's // purity and its brightness: static ui_color_t ui_color_adjust_saturation(ui_color_t c, fp32_t multiplier) { ui_color_t gray = ui_color_gray_with_same_intensity(c); return ui_color_interpolate(c, gray, 1 - multiplier); } static struct { const char* name; ui_color_t dark; ui_color_t light; } ui_theme_colors[] = { // empirical { .name = "Undefiled" ,.dark = ui_color_undefined, .light = ui_color_undefined }, { .name = "ActiveTitle" ,.dark = 0x001F1F1F, .light = 0x00D1B499 }, { .name = "ButtonFace" ,.dark = 0x00333333, .light = 0x00F0F0F0 }, { .name = "ButtonText" ,.dark = 0x00C8C8C8, .light = 0x00161616 }, // { .name = "ButtonText" ,.dark = 0x00F6F3EE, .light = 0x00000000 }, { .name = "GrayText" ,.dark = 0x00666666, .light = 0x006D6D6D }, { .name = "Hilight" ,.dark = 0x00626262, .light = 0x00D77800 }, { .name = "HilightText" ,.dark = 0x00000000, .light = 0x00FFFFFF }, { .name = "HotTrackingColor" ,.dark = 0x00B16300, .light = 0x00FF0000 }, // automatic Win11 "accent" ABRG: 0xFFB16300 // { .name = "HotTrackingColor" ,.dark = 0x00B77878, .light = 0x00CC6600 }, { .name = "InactiveTitle" ,.dark = 0x002B2B2B, .light = 0x00DBCDBF }, { .name = "InactiveTitleText",.dark = 0x00969696, .light = 0x00000000 }, { .name = "MenuHilight" ,.dark = 0x00002642, .light = 0x00FF9933 }, { .name = "TitleText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }, // { .name = "Window" ,.dark = 0x00000000, .light = 0x00FFFFFF }, // too contrast // { .name = "Window" ,.dark = 0x00121212, .light = 0x00E0E0E0 }, { .name = "Window" ,.dark = 0x002E2E2E, .light = 0x00E0E0E0 }, { .name = "WindowText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }, }; // TODO: add // Accent Color BGR: B16300 RGB: 0063B1 light blue // [HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM] // "AccentColor"=dword:ffb16300 // Windows used as accent almost on everything // see here: https://github.com/leok7v/ui/discussions/5 static ui_color_t ui_colors_get_color(int32_t color_id) { // SysGetColor() does not work on Win10 rt_swear(0 < color_id && color_id < rt_countof(ui_theme_colors)); return ui_theme.is_app_dark() ? ui_theme_colors[color_id].dark : ui_theme_colors[color_id].light; } ui_colors_if ui_colors = { .get_color = ui_colors_get_color, .rgb_to_hsi = ui_color_rgb_to_hsi, .hsi_to_rgb = ui_color_hsi_to_rgb, .interpolate = ui_color_interpolate, .gray_with_same_intensity = ui_color_gray_with_same_intensity, .lighten = ui_color_lighten, .darken = ui_color_darken, .adjust_saturation = ui_color_adjust_saturation, .multiply_brightness = ui_color_brightness, .multiply_saturation = ui_color_saturation, .transparent = ui_color_transparent, .none = (ui_color_t)0xFFFFFFFFU, // aka CLR_INVALID in wingdi .text = ui_color_rgb(240, 231, 220), .white = ui_color_rgb(255, 255, 255), .black = ui_color_rgb(0, 0, 0), .red = ui_color_rgb(255, 0, 0), .green = ui_color_rgb(0, 255, 0), .blue = ui_color_rgb(0, 0, 255), .yellow = ui_color_rgb(255, 255, 0), .cyan = ui_color_rgb(0, 255, 255), .magenta = ui_color_rgb(255, 0, 255), .gray = ui_color_rgb(128, 128, 128), // tone down RGB colors: .tone_white = ui_color_rgb(164, 164, 164), .tone_red = ui_color_rgb(192, 64, 64), .tone_green = ui_color_rgb(64, 192, 64), .tone_blue = ui_color_rgb(64, 64, 192), .tone_yellow = ui_color_rgb(192, 192, 64), .tone_cyan = ui_color_rgb(64, 192, 192), .tone_magenta = ui_color_rgb(192, 64, 192), // miscellaneous: .orange = ui_color_rgb(255, 165, 0), // 0xFFA500 .dark_green = ui_color_rgb( 1, 50, 32), // 0x013220 .pink = ui_color_rgb(255, 192, 203), // 0xFFC0CB .ochre = ui_color_rgb(204, 119, 34), // 0xCC7722 .gold = ui_color_rgb(255, 215, 0), // 0xFFD700 .teal = ui_color_rgb( 0, 128, 128), // 0x008080 .wheat = ui_color_rgb(245, 222, 179), // 0xF5DEB3 .tan = ui_color_rgb(210, 180, 140), // 0xD2B48C .brown = ui_color_rgb(165, 42, 42), // 0xA52A2A .maroon = ui_color_rgb(128, 0, 0), // 0x800000 .barbie_pink = ui_color_rgb(224, 33, 138), // 0xE0218A .steel_pink = ui_color_rgb(204, 51, 204), // 0xCC33CC .salmon_pink = ui_color_rgb(255, 145, 164), // 0xFF91A4 .gainsboro = ui_color_rgb(220, 220, 220), // 0xDCDCDC .light_gray = ui_color_rgb(211, 211, 211), // 0xD3D3D3 .silver = ui_color_rgb(192, 192, 192), // 0xC0C0C0 .dark_gray = ui_color_rgb(169, 169, 169), // 0xA9A9A9 .dim_gray = ui_color_rgb(105, 105, 105), // 0x696969 .light_slate_gray = ui_color_rgb(119, 136, 153), // 0x778899 .slate_gray = ui_color_rgb(112, 128, 144), // 0x708090 /* Main Panel Backgrounds */ .ennui_black = ui_color_rgb( 18, 18, 18), // 0x1212121 .charcoal = ui_color_rgb( 54, 69, 79), // 0x36454F .onyx = ui_color_rgb( 53, 56, 57), // 0x353839 .gunmetal = ui_color_rgb( 42, 52, 57), // 0x2A3439 .jet_black = ui_color_rgb( 52, 52, 52), // 0x343434 .outer_space = ui_color_rgb( 65, 74, 76), // 0x414A4C .eerie_black = ui_color_rgb( 27, 27, 27), // 0x1B1B1B .oil = ui_color_rgb( 59, 60, 54), // 0x3B3C36 .black_coral = ui_color_rgb( 84, 98, 111), // 0x54626F .obsidian = ui_color_rgb( 58, 50, 45), // 0x3A322D /* Secondary Panels or Sidebars */ .raisin_black = ui_color_rgb( 39, 38, 53), // 0x272635 .dark_charcoal = ui_color_rgb( 48, 48, 48), // 0x303030 .dark_jungle_green = ui_color_rgb( 26, 36, 33), // 0x1A2421 .pine_tree = ui_color_rgb( 42, 47, 35), // 0x2A2F23 .rich_black = ui_color_rgb( 0, 64, 64), // 0x004040 .eclipse = ui_color_rgb( 63, 57, 57), // 0x3F3939 .cafe_noir = ui_color_rgb( 75, 54, 33), // 0x4B3621 /* Flat Buttons */ .prussian_blue = ui_color_rgb( 0, 49, 83), // 0x003153 .midnight_green = ui_color_rgb( 0, 73, 83), // 0x004953 .charleston_green = ui_color_rgb( 35, 43, 43), // 0x232B2B .rich_black_fogra = ui_color_rgb( 10, 15, 13), // 0x0A0F0D .dark_liver = ui_color_rgb( 83, 75, 79), // 0x534B4F .dark_slate_gray = ui_color_rgb( 47, 79, 79), // 0x2F4F4F .black_olive = ui_color_rgb( 59, 60, 54), // 0x3B3C36 .cadet = ui_color_rgb( 83, 104, 114), // 0x536872 /* Button highlights (hover) */ .dark_sienna = ui_color_rgb( 60, 20, 20), // 0x3C1414 .bistre_brown = ui_color_rgb(150, 113, 23), // 0x967117 .dark_puce = ui_color_rgb( 79, 58, 60), // 0x4F3A3C .wenge = ui_color_rgb(100, 84, 82), // 0x645452 /* Raised button effects */ .dark_scarlet = ui_color_rgb( 86, 3, 25), // 0x560319 .burnt_umber = ui_color_rgb(138, 51, 36), // 0x8A3324 .caput_mortuum = ui_color_rgb( 89, 39, 32), // 0x592720 .barn_red = ui_color_rgb(124, 10, 2), // 0x7C0A02 /* Text and Icons */ .platinum = ui_color_rgb(229, 228, 226), // 0xE5E4E2 .anti_flash_white = ui_color_rgb(242, 243, 244), // 0xF2F3F4 .silver_sand = ui_color_rgb(191, 193, 194), // 0xBFC1C2 .quick_silver = ui_color_rgb(166, 166, 166), // 0xA6A6A6 /* Links and Selections */ .dark_powder_blue = ui_color_rgb( 0, 51, 153), // 0x003399 .sapphire_blue = ui_color_rgb( 15, 82, 186), // 0x0F52BA .international_klein_blue = ui_color_rgb( 0, 47, 167), // 0x002FA7 .zaffre = ui_color_rgb( 0, 20, 168), // 0x0014A8 /* Additional Colors */ .fish_belly = ui_color_rgb(232, 241, 212), // 0xE8F1D4 .rusty_red = ui_color_rgb(218, 44, 67), // 0xDA2C43 .falu_red = ui_color_rgb(128, 24, 24), // 0x801818 .cordovan = ui_color_rgb(137, 63, 69), // 0x893F45 .dark_raspberry = ui_color_rgb(135, 38, 87), // 0x872657 .deep_magenta = ui_color_rgb(204, 0, 204), // 0xCC00CC .byzantium = ui_color_rgb(112, 41, 99), // 0x702963 .amethyst = ui_color_rgb(153, 102, 204), // 0x9966CC .wisteria = ui_color_rgb(201, 160, 220), // 0xC9A0DC .lavender_purple = ui_color_rgb(150, 123, 182), // 0x967BB6 .opera_mauve = ui_color_rgb(183, 132, 167), // 0xB784A7 .mauve_taupe = ui_color_rgb(145, 95, 109), // 0x915F6D .rich_lavender = ui_color_rgb(167, 107, 207), // 0xA76BCF .pansy_purple = ui_color_rgb(120, 24, 74), // 0x78184A .violet_eggplant = ui_color_rgb(153, 17, 153), // 0x991199 .jazzberry_jam = ui_color_rgb(165, 11, 94), // 0xA50B5E .dark_orchid = ui_color_rgb(153, 50, 204), // 0x9932CC .electric_purple = ui_color_rgb(191, 0, 255), // 0xBF00FF .sky_magenta = ui_color_rgb(207, 113, 175), // 0xCF71AF .brilliant_rose = ui_color_rgb(230, 103, 206), // 0xE667CE .fuchsia_purple = ui_color_rgb(204, 57, 123), // 0xCC397B .french_raspberry = ui_color_rgb(199, 44, 72), // 0xC72C48 .wild_watermelon = ui_color_rgb(252, 108, 133), // 0xFC6C85 .neon_carrot = ui_color_rgb(255, 163, 67), // 0xFFA343 .burnt_orange = ui_color_rgb(204, 85, 0), // 0xCC5500 .carrot_orange = ui_color_rgb(237, 145, 33), // 0xED9121 .tiger_orange = ui_color_rgb(253, 106, 2), // 0xFD6A02 .giant_onion = ui_color_rgb(176, 181, 137), // 0xB0B589 .rust = ui_color_rgb(183, 65, 14), // 0xB7410E .copper_red = ui_color_rgb(203, 109, 81), // 0xCB6D51 .dark_tangerine = ui_color_rgb(255, 168, 18), // 0xFFA812 .bright_marigold = ui_color_rgb(252, 192, 6), // 0xFCC006 .bone = ui_color_rgb(227, 218, 201), // 0xE3DAC9 /* Earthy Tones */ .sienna = ui_color_rgb(160, 82, 45), // 0xA0522D .sandy_brown = ui_color_rgb(244, 164, 96), // 0xF4A460 .golden_brown = ui_color_rgb(153, 101, 21), // 0x996515 .camel = ui_color_rgb(193, 154, 107), // 0xC19A6B .burnt_sienna = ui_color_rgb(238, 124, 88), // 0xEE7C58 .khaki = ui_color_rgb(195, 176, 145), // 0xC3B091 .dark_khaki = ui_color_rgb(189, 183, 107), // 0xBDB76B /* Greens */ .fern_green = ui_color_rgb( 79, 121, 66), // 0x4F7942 .moss_green = ui_color_rgb(138, 154, 91), // 0x8A9A5B .myrtle_green = ui_color_rgb( 49, 120, 115), // 0x317873 .pine_green = ui_color_rgb( 1, 121, 111), // 0x01796F .jungle_green = ui_color_rgb( 41, 171, 135), // 0x29AB87 .sacramento_green = ui_color_rgb( 4, 57, 39), // 0x043927 /* Blues */ .yale_blue = ui_color_rgb( 15, 77, 146), // 0x0F4D92 .cobalt_blue = ui_color_rgb( 0, 71, 171), // 0x0047AB .persian_blue = ui_color_rgb( 28, 57, 187), // 0x1C39BB .royal_blue = ui_color_rgb( 65, 105, 225), // 0x4169E1 .iceberg = ui_color_rgb(113, 166, 210), // 0x71A6D2 .blue_yonder = ui_color_rgb( 80, 114, 167), // 0x5072A7 /* Miscellaneous */ .cocoa_brown = ui_color_rgb(210, 105, 30), // 0xD2691E .cinnamon_satin = ui_color_rgb(205, 96, 126), // 0xCD607E .fallow = ui_color_rgb(193, 154, 107), // 0xC19A6B .cafe_au_lait = ui_color_rgb(166, 123, 91), // 0xA67B5B .liver = ui_color_rgb(103, 76, 71), // 0x674C47 .shadow = ui_color_rgb(138, 121, 93), // 0x8A795D .cool_grey = ui_color_rgb(140, 146, 172), // 0x8C92AC .payne_grey = ui_color_rgb( 83, 104, 120), // 0x536878 /* Lighter Tones for Contrast */ .timberwolf = ui_color_rgb(219, 215, 210), // 0xDBD7D2 .silver_chalice = ui_color_rgb(172, 172, 172), // 0xACACAC .roman_silver = ui_color_rgb(131, 137, 150), // 0x838996 /* Dark Mode Specific Highlights */ .electric_lavender = ui_color_rgb(244, 191, 255), // 0xF4BFFF .magenta_haze = ui_color_rgb(159, 69, 118), // 0x9F4576 .cyber_grape = ui_color_rgb( 88, 66, 124), // 0x58427C .purple_navy = ui_color_rgb( 78, 81, 128), // 0x4E5180 .liberty = ui_color_rgb( 84, 90, 167), // 0x545AA7 .purple_mountain_majesty = ui_color_rgb(150, 120, 182), // 0x9678B6 .ceil = ui_color_rgb(146, 161, 207), // 0x92A1CF .moonstone_blue = ui_color_rgb(115, 169, 194), // 0x73A9C2 .independence = ui_color_rgb( 76, 81, 109) // 0x4C516D }; // _____________________________ ui_containers.c ______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" static bool ui_containers_debug; #pragma push_macro("debugln") #pragma push_macro("ui_layout_dump") #pragma push_macro("ui_layout_enter") #pragma push_macro("ui_layout_exit") // Usage of: ui_view_for_each_begin(p, c) { ... } ui_view_for_each_end(p, c) // makes code inside iterator debugger friendly and ensures correct __LINE__ #define debugln(...) do { \ if (ui_containers_debug) { rt_println(__VA_ARGS__); } \ } while (0) static int32_t ui_layout_nesting; #define ui_layout_enter(v) do { \ ui_ltrb_t i_ = ui_view.margins(v, &v->insets); \ ui_ltrb_t p_ = ui_view.margins(v, &v->padding); \ debugln("%*c> %4d,%-4d %4dx%-4d p: %d %d %d %d i: %d %d %d %d %s", \ ui_layout_nesting, 0x20, \ v->x, v->y, v->w, v->h, \ p_.left, p_.top, p_.right, p_.bottom, \ i_.left, i_.top, i_.right, i_.bottom, \ ui_view_debug_id(v)); \ ui_layout_nesting += 4; \ } while (0) #define ui_layout_exit(v) do { \ ui_layout_nesting -= 4; \ debugln("%*c< %4d,%-4d %4dx%-4d %s", \ ui_layout_nesting, 0x20, \ v->x, v->y, v->w, v->h, ui_view_debug_id(v)); \ } while (0) #define ui_layout_clild(v) do { \ debugln("%*c %4d,%-4d %4dx%-4d %s", ui_layout_nesting, 0x20, \ c->x, c->y, c->w, c->h, ui_view_debug_id(v)); \ } while (0) static const char* ui_stack_finite_int(int32_t v, char* text, int32_t count) { rt_swear(v >= 0); if (v == ui.infinity) { rt_str.format(text, count, "%s", rt_glyph_infinity); } else { rt_str.format(text, count, "%d", v); } return text; } #define ui_layout_dump(v) do { \ char maxw[32]; \ char maxh[32]; \ debugln("%s[%4.4s] %4d,%-4d %4dx%-4d, max[%sx%s] " \ "padding { %.3f %.3f %.3f %.3f } " \ "insets { %.3f %.3f %.3f %.3f } align: 0x%02X", \ ui_view_debug_id(v), \ &v->type, v->x, v->y, v->w, v->h, \ ui_stack_finite_int(v->max_w, maxw, rt_countof(maxw)), \ ui_stack_finite_int(v->max_h, maxh, rt_countof(maxh)), \ v->padding.left, v->padding.top, v->padding.right, v->padding.bottom, \ v->insets.left, v->insets.top, v->insets.right, v->insets.bottom, \ v->align); \ } while (0) static void ui_span_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_span, "type %4.4s 0x%08X", &p->type, p->type); ui_ltrb_t insets; ui_view.inbox(p, null, &insets); int32_t w = insets.left; int32_t h = 0; int32_t max_w = w; ui_view_for_each_begin(p, c) { rt_swear(c->max_w == 0 || c->max_w >= c->w, "max_w: %d w: %d", c->max_w, c->w); if (ui_view.is_hidden(c)) { // nothing } else if (c->type == ui_view_spacer) { c->padding = (ui_margins_t){ 0, 0, 0, 0 }; c->w = 0; // layout will distribute excess here c->h = 0; // starts with zero max_w = ui.infinity; // spacer make width greedy } else { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); h = rt_max(h, cbx.h); if (c->max_w == ui.infinity) { max_w = ui.infinity; } else if (max_w < ui.infinity && c->max_w != 0) { rt_swear(c->max_w >= c->w, "c->max_w %d < c->w %d ", c->max_w, c->w); max_w += c->max_w; } else if (max_w < ui.infinity) { rt_swear(0 <= max_w + cbx.w && (int64_t)max_w + (int64_t)cbx.w < (int64_t)ui.infinity, "max_w:%d + cbx.w:%d = %d", max_w, cbx.w, max_w + cbx.w); max_w += cbx.w; } w += cbx.w; } ui_layout_clild(c); } ui_view_for_each_end(p, c); if (0 < max_w && max_w < ui.infinity) { rt_swear(0 <= max_w + insets.right && (int64_t)max_w + (int64_t)insets.right < (int64_t)ui.infinity, "max_w:%d + right:%d = %d", max_w, insets.right, max_w + insets.right); max_w += insets.right; } rt_swear(max_w == 0 || max_w >= w, "max_w: %d w: %d", max_w, w); if (ui_view.is_hidden(p)) { p->w = 0; p->h = 0; } else { p->w = w + insets.right; p->h = insets.top + h + insets.bottom; rt_swear(p->max_w == 0 || p->max_w >= p->w, "max_w: %d is less than actual width: %d", p->max_w, p->w); } ui_layout_exit(p); } // after measure of the subtree is concluded the parent ui_span // may adjust span_w wider number depending on it's own width // and ui_span.max_w agreement static int32_t ui_span_place_child(ui_view_t* c, ui_rect_t pbx, int32_t x) { ui_ltrb_t padding = ui_view.margins(c, &c->padding); // setting child`s max_h to infinity means that child`s height is // *always* fill vertical view size of the parent // childs.h can exceed parent.h (vertical overflow) - is not // encouraged but allowed if (c->max_h == ui.infinity) { // important c->h changed, cbx.h is no longer valid c->h = rt_max(c->h, pbx.h - padding.top - padding.bottom); } int32_t min_y = pbx.y + padding.top; if ((c->align & ui.align.top) != 0) { rt_assert(c->align == ui.align.top); c->y = min_y; } else if ((c->align & ui.align.bottom) != 0) { rt_assert(c->align == ui.align.bottom); c->y = rt_max(min_y, pbx.y + pbx.h - c->h - padding.bottom); } else { // effective height (c->h might have been changed) rt_assert(c->align == ui.align.center, "only top, center, bottom alignment for span"); const int32_t ch = padding.top + c->h + padding.bottom; c->y = rt_max(min_y, pbx.y + (pbx.h - ch) / 2 + padding.top); } c->x = x + padding.left; return c->x + c->w + padding.right; } static void ui_span_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_span, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t spacers = 0; // Number of spacers int32_t max_w_count = 0; int32_t x = p->x + insets.left; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { if (c->type == ui_view_spacer) { c->x = x; c->y = pbx.y; c->h = pbx.h; c->w = 0; spacers++; } else { x = ui_span_place_child(c, pbx, x); rt_swear(c->max_w == 0 || c->max_w >= c->w, "max_w:%d < w:%d", c->max_w, c->w); if (c->max_w > 0) { max_w_count++; } } ui_layout_clild(c); } } ui_view_for_each_end(p, c); int32_t xw = rt_max(0, pbx.x + pbx.w - x); // excess width int32_t max_w_sum = 0; if (xw > 0 && max_w_count > 0) { ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c) && c->type != ui_view_spacer && c->max_w > 0) { max_w_sum += rt_min(c->max_w, xw); ui_layout_clild(c); } } ui_view_for_each_end(p, c); } if (xw > 0 && max_w_count > 0) { debugln("%*c pass 2: fill parent", ui_layout_nesting, 0x20); x = p->x + insets.left; int32_t k = 0; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { rt_swear(padding.left == 0 && padding.right == 0); } else if (c->max_w > 0) { const int32_t max_w = rt_min(c->max_w, xw); int64_t proportional = (xw * (int64_t)max_w) / max_w_sum; rt_assert(proportional <= (int64_t)INT32_MAX); int32_t cw = (int32_t)proportional; c->w = rt_min(c->max_w, c->w + cw); k++; } // TODO: take into account .align of a child and adjust x // depending on ui.align.left/right/center // distributing excess width on the left and right of a child c->x = padding.left + x; x = c->x + padding.left + c->w + padding.right; ui_layout_clild(c); } } ui_view_for_each_end(p, c); rt_swear(k == max_w_count); } // excess width after max_w of non-spacers taken into account xw = rt_max(0, pbx.x + pbx.w - x); if (xw > 0 && spacers > 0) { // evenly distribute excess among spacers debugln("%*c pass 3: expand spacers", ui_layout_nesting, 0x20); int32_t partial = xw / spacers; x = p->x + insets.left; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { c->y = pbx.y; c->w = partial; c->h = pbx.h; spacers--; } c->x = x + padding.left; x = c->x + c->w + padding.right; ui_layout_clild(c); } } ui_view_for_each_end(p, c); } ui_layout_exit(p); } static void ui_list_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_list, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t max_h = insets.top; int32_t h = insets.top; int32_t w = 0; ui_view_for_each_begin(p, c) { rt_swear(c->max_h == 0 || c->max_h >= c->h, "max_h: %d h: %d", c->max_h, c->h); if (!ui_view.is_hidden(c)) { if (c->type == ui_view_spacer) { c->padding = (ui_margins_t){ 0, 0, 0, 0 }; c->h = 0; // layout will distribute excess here max_h = ui.infinity; // spacer make height greedy } else { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); w = rt_max(w, cbx.w); if (c->max_h == ui.infinity) { max_h = ui.infinity; } else if (max_h < ui.infinity && c->max_h != 0) { rt_swear(c->max_h >= c->h, "c->max_h:%d < c->h: %d", c->max_h, c->h); max_h += c->max_h; } else if (max_h < ui.infinity) { rt_swear(0 <= max_h + cbx.h && (int64_t)max_h + (int64_t)cbx.h < (int64_t)ui.infinity, "max_h:%d + ch:%d = %d", max_h, cbx.h, max_h + cbx.h); max_h += cbx.h; } h += cbx.h; } ui_layout_clild(c); } } ui_view_for_each_end(p, c); if (max_h < ui.infinity) { rt_swear(0 <= max_h + insets.bottom && (int64_t)max_h + (int64_t)insets.bottom < (int64_t)ui.infinity, "max_h:%d + bottom:%d = %d", max_h, insets.bottom, max_h + insets.bottom); max_h += insets.bottom; } if (ui_view.is_hidden(p)) { p->w = 0; p->h = 0; } else if (p == ui_app.root) { // ui_app.root is special occupying whole window client rectangle // sans borders and caption thus it should not be re-measured } else { p->h = h + insets.bottom; p->w = insets.left + w + insets.right; } ui_layout_exit(p); } static int32_t ui_list_place_child(ui_view_t* c, ui_rect_t pbx, int32_t y) { ui_ltrb_t padding = ui_view.margins(c, &c->padding); // setting child`s max_w to infinity means that child`s height is // *always* fill vertical view size of the parent // childs.w can exceed parent.w (horizontal overflow) - not encouraged but allowed if (c->max_w == ui.infinity) { c->w = rt_max(c->w, pbx.w - padding.left - padding.right); } int32_t min_x = pbx.x + padding.left; if ((c->align & ui.align.left) != 0) { rt_assert(c->align == ui.align.left); c->x = min_x; } else if ((c->align & ui.align.right) != 0) { rt_assert(c->align == ui.align.right); c->x = rt_max(min_x, pbx.x + pbx.w - c->w - padding.right); } else { rt_assert(c->align == ui.align.center, "only left, center, right, alignment for list"); const int32_t cw = padding.left + c->w + padding.right; c->x = rt_max(min_x, pbx.x + (pbx.w - cw) / 2 + padding.left); } c->y = y + padding.top; return c->y + c->h + padding.bottom; } static void ui_list_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_list, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t spacers = 0; // Number of spacers int32_t max_h_sum = 0; int32_t max_h_count = 0; int32_t y = pbx.y; ui_view_for_each_begin(p, c) { if (ui_view.is_hidden(c)) { // nothing } else if (c->type == ui_view_spacer) { c->x = pbx.x; c->y = y; c->w = pbx.w; c->h = 0; spacers++; } else { y = ui_list_place_child(c, pbx, y); rt_swear(c->max_h == 0 || c->max_h >= c->h, "max_h:%d < h:%d", c->max_h, c->h); if (c->max_h > 0) { // clamp max_h to the effective parent height max_h_count++; } } } ui_view_for_each_end(p, c); int32_t xh = rt_max(0, pbx.y + pbx.h - y); // excess height if (xh > 0 && max_h_count > 0) { ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c) && c->type != ui_view_spacer && c->max_h > 0) { max_h_sum += rt_min(c->max_h, xh); } } ui_view_for_each_end(p, c); } if (xh > 0 && max_h_count > 0) { debugln("%*c pass 2: fill parent", ui_layout_nesting, 0x20); y = pbx.y; int32_t k = 0; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type != ui_view_spacer && c->max_h > 0) { const int32_t max_h = rt_min(c->max_h, xh); int64_t proportional = (xh * (int64_t)max_h) / max_h_sum; rt_assert(proportional <= (int64_t)INT32_MAX); int32_t ch = (int32_t)proportional; c->h = rt_min(c->max_h, c->h + ch); k++; } int32_t ch = padding.top + c->h + padding.bottom; c->y = y + padding.top; y += ch; ui_layout_clild(c); } } ui_view_for_each_end(p, c); rt_swear(k == max_h_count); } // excess height after max_h of non-spacers taken into account xh = rt_max(0, pbx.y + pbx.h - y); // excess height if (xh > 0 && spacers > 0) { // evenly distribute excess among spacers debugln("%*c pass 3: expand spacers", ui_layout_nesting, 0x20); int32_t partial = xh / spacers; y = pbx.y; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { c->x = pbx.x; c->w = pbx.x + pbx.w - pbx.x; c->h = partial; // TODO: last? spacers--; } int32_t ch = padding.top + c->h + padding.bottom; c->y = y + padding.top; y += ch; ui_layout_clild(c); } } ui_view_for_each_end(p, c); } ui_layout_exit(p); } static void ui_stack_child_3x3(ui_view_t* c, int32_t *row, int32_t *col) { *row = 0; *col = 0; // makes code analysis happier if (c->align == (ui.align.left|ui.align.top)) { *row = 0; *col = 0; } else if (c->align == ui.align.top) { *row = 0; *col = 1; } else if (c->align == (ui.align.right|ui.align.top)) { *row = 0; *col = 2; } else if (c->align == ui.align.left) { *row = 1; *col = 0; } else if (c->align == ui.align.center) { *row = 1; *col = 1; } else if (c->align == ui.align.right) { *row = 1; *col = 2; } else if (c->align == (ui.align.left|ui.align.bottom)) { *row = 2; *col = 0; } else if (c->align == ui.align.bottom) { *row = 2; *col = 1; } else if (c->align == (ui.align.right|ui.align.bottom)) { *row = 2; *col = 2; } else { rt_swear(false, "invalid child align: 0x%02X", c->align); } } static void ui_stack_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_stack, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); ui_wh_t sides[3][3] = { {0, 0} }; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); int32_t row = 0; int32_t col = 0; ui_stack_child_3x3(c, &row, &col); sides[row][col].w = rt_max(sides[row][col].w, cbx.w); sides[row][col].h = rt_max(sides[row][col].h, cbx.h); ui_layout_clild(c); } } ui_view_for_each_end(p, c); if (ui_containers_debug) { for (int32_t r = 0; r < rt_countof(sides); r++) { char text[1024]; text[0] = 0; for (int32_t c = 0; c < rt_countof(sides[r]); c++) { char line[128]; rt_str_printf(line, " %4dx%-4d", sides[r][c].w, sides[r][c].h); strcat(text, line); } debugln("%*c sides[%d] %s", ui_layout_nesting, 0x20, r, text); } } ui_wh_t wh = {0, 0}; for (int32_t r = 0; r < 3; r++) { int32_t sum_w = 0; for (int32_t c = 0; c < 3; c++) { sum_w += sides[r][c].w; } wh.w = rt_max(wh.w, sum_w); } for (int32_t c = 0; c < 3; c++) { int32_t sum_h = 0; for (int32_t r = 0; r < 3; r++) { sum_h += sides[r][c].h; } wh.h = rt_max(wh.h, sum_h); } debugln("%*c wh %4dx%-4d", ui_layout_nesting, 0x20, wh.w, wh.h); p->w = insets.left + wh.w + insets.right; p->h = insets.top + wh.h + insets.bottom; ui_layout_exit(p); } static void ui_stack_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_stack, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); ui_view_for_each_begin(p, c) { if (c->type != ui_view_spacer && !ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); const int32_t pw = p->w - insets.left - insets.right - padding.left - padding.right; const int32_t ph = p->h - insets.top - insets.bottom - padding.top - padding.bottom; int32_t cw = c->max_w == ui.infinity ? pw : c->max_w; if (cw > 0) { c->w = rt_min(cw, pw); } int32_t ch = c->max_h == ui.infinity ? ph : c->max_h; if (ch > 0) { c->h = rt_min(ch, ph); } rt_swear((c->align & (ui.align.left|ui.align.right)) != (ui.align.left|ui.align.right), "align: left|right 0x%02X", c->align); rt_swear((c->align & (ui.align.top|ui.align.bottom)) != (ui.align.top|ui.align.bottom), "align: top|bottom 0x%02X", c->align); int32_t min_x = pbx.x + padding.left; if ((c->align & ui.align.left) != 0) { c->x = min_x; } else if ((c->align & ui.align.right) != 0) { c->x = rt_max(min_x, pbx.x + pbx.w - c->w - padding.right); } else { c->x = rt_max(min_x, min_x + (pbx.w - (padding.left + c->w + padding.right)) / 2); } int32_t min_y = pbx.y + padding.top; if ((c->align & ui.align.top) != 0) { c->y = min_y; } else if ((c->align & ui.align.bottom) != 0) { c->y = rt_max(min_y, pbx.y + pbx.h - c->h - padding.bottom); } else { c->y = rt_max(min_y, min_y + (pbx.h - (padding.top + c->h + padding.bottom)) / 2); } ui_layout_clild(c); } } ui_view_for_each_end(p, c); ui_layout_exit(p); } static void ui_container_paint(ui_view_t* v) { if (!ui_color_is_undefined(v->background) && !ui_color_is_transparent(v->background)) { ui_gdi.fill(v->x, v->y, v->w, v->h, v->background); } else { // rt_println("%s undefined", ui_view_debug_id(v)); } } static void ui_view_container_init(ui_view_t* v) { v->background = ui_colors.transparent; v->insets = (ui_margins_t){ .left = 0.25, .top = 0.125, .right = 0.25, .bottom = 0.125 // .left = 0.25, .top = 0.0625, // TODO: why? // .right = 0.25, .bottom = 0.1875 }; } void ui_view_init_span(ui_view_t* v) { rt_swear(v->type == ui_view_span, "type %4.4s 0x%08X", &v->type, v->type); ui_view_container_init(v); if (v->measure == null) { v->measure = ui_span_measure; } if (v->layout == null) { v->layout = ui_span_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_span"); } if (v->debug.id == null) { v->debug.id = "#ui_span"; } } void ui_view_init_list(ui_view_t* v) { rt_swear(v->type == ui_view_list, "type %4.4s 0x%08X", &v->type, v->type); ui_view_container_init(v); if (v->measure == null) { v->measure = ui_list_measure; } if (v->layout == null) { v->layout = ui_list_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_list"); } if (v->debug.id == null) { v->debug.id = "#ui_list"; } } void ui_view_init_spacer(ui_view_t* v) { rt_swear(v->type == ui_view_spacer, "type %4.4s 0x%08X", &v->type, v->type); v->w = 0; v->h = 0; v->max_w = ui.infinity; v->max_h = ui.infinity; if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_spacer"); } if (v->debug.id == null) { v->debug.id = "#ui_spacer"; } } void ui_view_init_stack(ui_view_t* v) { ui_view_container_init(v); if (v->measure == null) { v->measure = ui_stack_measure; } if (v->layout == null) { v->layout = ui_stack_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_stack"); } if (v->debug.id == null) { v->debug.id = "#ui_stack"; } } #pragma pop_macro("ui_layout_exit") #pragma pop_macro("ui_layout_enter") #pragma pop_macro("ui_layout_dump") #pragma pop_macro("debugln") // ________________________________ ui_core.c _________________________________ #include "rt/rt.h" #include "rt/rt_win32.h" #define UI_WM_ANIMATE (WM_APP + 0x7FFF) #define UI_WM_OPENING (WM_APP + 0x7FFE) #define UI_WM_CLOSING (WM_APP + 0x7FFD) #define UI_WM_TAP (WM_APP + 0x7FFC) #define UI_WM_DTAP (WM_APP + 0x7FFB) // double tap (aka click) #define UI_WM_PRESS (WM_APP + 0x7FFA) static bool ui_point_in_rect(const ui_point_t* p, const ui_rect_t* r) { return r->x <= p->x && p->x < r->x + r->w && r->y <= p->y && p->y < r->y + r->h; } static bool ui_intersect_rect(ui_rect_t* i, const ui_rect_t* r0, const ui_rect_t* r1) { ui_rect_t r = {0}; r.x = rt_max(r0->x, r1->x); // Maximum of left edges r.y = rt_max(r0->y, r1->y); // Maximum of top edges r.w = rt_min(r0->x + r0->w, r1->x + r1->w) - r.x; // Width of overlap r.h = rt_min(r0->y + r0->h, r1->y + r1->h) - r.y; // Height of overlap bool b = r.w > 0 && r.h > 0; if (!b) { r.w = 0; r.h = 0; } if (i != null) { *i = r; } return b; } static ui_rect_t ui_combine_rect(const ui_rect_t* r0, const ui_rect_t* r1) { return (ui_rect_t) { .x = rt_min(r0->x, r1->x), .y = rt_min(r0->y, r1->y), .w = rt_max(r0->x + r0->w, r1->x + r1->w) - rt_min(r0->x, r1->x), .h = rt_max(r0->y + r0->h, r1->y + r1->h) - rt_min(r0->y, r1->y) }; } ui_if ui = { .point_in_rect = ui_point_in_rect, .intersect_rect = ui_intersect_rect, .combine_rect = ui_combine_rect, .infinity = INT32_MAX, .align = { .center = 0, .left = 0x01, .top = 0x02, .right = 0x10, .bottom = 0x20 }, .visibility = { // window visibility see ShowWindow link below .hide = SW_HIDE, .normal = SW_SHOWNORMAL, .minimize = SW_SHOWMINIMIZED, .maximize = SW_SHOWMAXIMIZED, .normal_na = SW_SHOWNOACTIVATE, .show = SW_SHOW, .min_next = SW_MINIMIZE, .min_na = SW_SHOWMINNOACTIVE, .show_na = SW_SHOWNA, .restore = SW_RESTORE, .defau1t = SW_SHOWDEFAULT, .force_min = SW_FORCEMINIMIZE }, .message = { .animate = UI_WM_ANIMATE, .opening = UI_WM_OPENING, .closing = UI_WM_CLOSING }, .mouse = { .button = { .left = MK_LBUTTON, .right = MK_RBUTTON } }, .hit_test = { .error = HTERROR, .transparent = HTTRANSPARENT, .nowhere = HTNOWHERE, .client = HTCLIENT, .caption = HTCAPTION, .system_menu = HTSYSMENU, .grow_box = HTGROWBOX, .menu = HTMENU, .horizontal_scroll = HTHSCROLL, .vertical_scroll = HTVSCROLL, .min_button = HTMINBUTTON, .max_button = HTMAXBUTTON, .left = HTLEFT, .right = HTRIGHT, .top = HTTOP, .top_left = HTTOPLEFT, .top_right = HTTOPRIGHT, .bottom = HTBOTTOM, .bottom_left = HTBOTTOMLEFT, .bottom_right = HTBOTTOMRIGHT, .border = HTBORDER, .object = HTOBJECT, .close = HTCLOSE, .help = HTHELP }, .key = { .up = VK_UP, .down = VK_DOWN, .left = VK_LEFT, .right = VK_RIGHT, .home = VK_HOME, .end = VK_END, .page_up = VK_PRIOR, .page_down = VK_NEXT, .insert = VK_INSERT, .del = VK_DELETE, .back = VK_BACK, .escape = VK_ESCAPE, .enter = VK_RETURN, .minus = VK_OEM_MINUS, .plus = VK_OEM_PLUS, .f1 = VK_F1, .f2 = VK_F2, .f3 = VK_F3, .f4 = VK_F4, .f5 = VK_F5, .f6 = VK_F6, .f7 = VK_F7, .f8 = VK_F8, .f9 = VK_F9, .f10 = VK_F10, .f11 = VK_F11, .f12 = VK_F12, .f13 = VK_F13, .f14 = VK_F14, .f15 = VK_F15, .f16 = VK_F16, .f17 = VK_F17, .f18 = VK_F18, .f19 = VK_F19, .f20 = VK_F20, .f21 = VK_F21, .f22 = VK_F22, .f23 = VK_F23, .f24 = VK_F24, }, .beep = { .ok = 0, .info = 1, .question = 2, .warning = 3, .error = 4 } }; // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow // ______________________________ ui_edit_doc.c _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #undef UI_EDIT_STR_TEST #undef UI_EDIT_DOC_TEST #undef UI_STR_TEST_REPLACE_ALL_PERMUTATIONS #undef UI_EDIT_DOC_TEST_PARAGRAPHS #if 0 // flip to 1 to run tests #define UI_EDIT_STR_TEST #define UI_EDIT_DOC_TEST #if 0 // flip to 1 to run exhausting lengthy tests #define UI_STR_TEST_REPLACE_ALL_PERMUTATIONS #define UI_EDIT_DOC_TEST_PARAGRAPHS #endif #endif #pragma push_macro("ui_edit_check_zeros") #pragma push_macro("ui_edit_check_pg_inside_text") #pragma push_macro("ui_edit_check_range_inside_text") #pragma push_macro("ui_edit_pg_dump") #pragma push_macro("ui_edit_range_dump") #pragma push_macro("ui_edit_text_dump") #pragma push_macro("ui_edit_doc_dump") #define ui_edit_pg_dump(pg) \ rt_debug.println(__FILE__, __LINE__, __func__, \ "pn:%d gp:%d", (pg)->pn, (pg)->gp) #define ui_edit_range_dump(r) \ rt_debug.println(__FILE__, __LINE__, __func__, \ "from {pn:%d gp:%d} to {pn:%d gp:%d}", \ (r)->from.pn, (r)->from.gp, (r)->to.pn, (r)->to.gp); #define ui_edit_text_dump(t) do { \ for (int32_t i_ = 0; i_ < (t)->np; i_++) { \ const ui_edit_str_t* p_ = &t->ps[i_]; \ rt_debug.println(__FILE__, __LINE__, __func__, \ "ps[%d].%d: %.*s", i_, p_->b, p_->b, p_->u); \ } \ } while (0) // TODO: undo/redo stacks and listeners #define ui_edit_doc_dump(d) do { \ for (int32_t i_ = 0; i_ < (d)->text.np; i_++) { \ const ui_edit_str_t* p_ = &(d)->text.ps[i_]; \ rt_debug.println(__FILE__, __LINE__, __func__, \ "ps[%d].b:%d.c:%d: %p %.*s", i_, p_->b, p_->c, \ p_, p_->b, p_->u); \ } \ } while (0) #ifdef DEBUG // ui_edit_check_zeros only works for packed structs: #define ui_edit_check_zeros(a_, b_) do { \ for (int32_t i_ = 0; i_ < (int32_t)(b_); i_++) { \ rt_assert(((const uint8_t*)(a_))[i_] == 0x00); \ } \ } while (0) #define ui_edit_check_pg_inside_text(t_, pg_) \ rt_assert(0 <= (pg_)->pn && (pg_)->pn < (t_)->np && \ 0 <= (pg_)->gp && (pg_)->gp <= (t_)->ps[(pg_)->pn].g) #define ui_edit_check_range_inside_text(t_, r_) do { \ rt_assert((r_)->from.pn <= (r_)->to.pn); \ rt_assert((r_)->from.pn < (r_)->to.pn || (r_)->from.gp <= (r_)->to.gp); \ ui_edit_check_pg_inside_text(t_, (&(r_)->from)); \ ui_edit_check_pg_inside_text(t_, (&(r_)->to)); \ } while (0) #else #define ui_edit_check_zeros(a, b) do { } while (0) #define ui_edit_check_pg_inside_text(t, pg) do { } while (0) #define ui_edit_check_range_inside_text(t, r) do { } while (0) #endif static ui_edit_range_t ui_edit_text_all_on_null(const ui_edit_text_t* t, const ui_edit_range_t* range) { ui_edit_range_t r; if (range != null) { r = *range; } else { rt_assert(t->np >= 1); r.from.pn = 0; r.from.gp = 0; r.to.pn = t->np - 1; r.to.gp = t->ps[r.to.pn].g; } return r; } static int ui_edit_range_compare(const ui_edit_pg_t pg1, const ui_edit_pg_t pg2) { int64_t d = (((int64_t)pg1.pn << 32) | pg1.gp) - (((int64_t)pg2.pn << 32) | pg2.gp); return d < 0 ? -1 : d > 0 ? 1 : 0; } static ui_edit_range_t ui_edit_range_order(const ui_edit_range_t range) { ui_edit_range_t r = range; uint64_t f = ((uint64_t)r.from.pn << 32) | r.from.gp; uint64_t t = ((uint64_t)r.to.pn << 32) | r.to.gp; if (ui_edit_range.compare(r.from, r.to) > 0) { uint64_t swap = t; t = f; f = swap; r.from.pn = (int32_t)(f >> 32); r.from.gp = (int32_t)(f); r.to.pn = (int32_t)(t >> 32); r.to.gp = (int32_t)(t); } return r; } static ui_edit_range_t ui_edit_text_ordered(const ui_edit_text_t* t, const ui_edit_range_t* r) { return ui_edit_range.order(ui_edit_text.all_on_null(t, r)); } static bool ui_edit_range_is_valid(const ui_edit_range_t r) { if (0 <= r.from.pn && 0 <= r.to.pn && 0 <= r.from.gp && 0 <= r.to.gp) { ui_edit_range_t o = ui_edit_range.order(r); return ui_edit_range.compare(o.from, o.to) <= 0; } else { return false; } } static bool ui_edit_range_is_empty(const ui_edit_range_t r) { return r.from.pn == r.to.pn && r.from.gp == r.to.gp; } static ui_edit_pg_t ui_edit_text_end(const ui_edit_text_t* t) { return (ui_edit_pg_t){ .pn = t->np - 1, .gp = t->ps[t->np - 1].g }; } static ui_edit_range_t ui_edit_text_end_range(const ui_edit_text_t* t) { ui_edit_pg_t e = (ui_edit_pg_t){ .pn = t->np - 1, .gp = t->ps[t->np - 1].g }; return (ui_edit_range_t){ .from = e, .to = e }; } static uint64_t ui_edit_range_uint64(const ui_edit_pg_t pg) { rt_assert(pg.pn >= 0 && pg.gp >= 0); return ((uint64_t)pg.pn << 32) | (uint64_t)pg.gp; } static ui_edit_pg_t ui_edit_range_pg(uint64_t uint64) { rt_assert((int32_t)(uint64 >> 32) >= 0 && (int32_t)uint64 >= 0); return (ui_edit_pg_t){ .pn = (int32_t)(uint64 >> 32), .gp = (int32_t)uint64 }; } static bool ui_edit_range_inside_text(const ui_edit_text_t* t, const ui_edit_range_t r) { return ui_edit_range.is_valid(r) && 0 <= r.from.pn && r.from.pn <= r.to.pn && r.to.pn < t->np && 0 <= r.from.gp && r.from.gp <= r.to.gp && r.to.gp <= t->ps[r.to.pn - 1].g; } static ui_edit_range_t ui_edit_range_intersect(const ui_edit_range_t r1, const ui_edit_range_t r2) { if (ui_edit_range.is_valid(r1) && ui_edit_range.is_valid(r2)) { ui_edit_range_t o1 = ui_edit_range.order(r1); ui_edit_range_t o2 = ui_edit_range.order(r1); uint64_t f1 = ((uint64_t)o1.from.pn << 32) | o1.from.gp; uint64_t t1 = ((uint64_t)o1.to.pn << 32) | o1.to.gp; uint64_t f2 = ((uint64_t)o2.from.pn << 32) | o2.from.gp; uint64_t t2 = ((uint64_t)o2.to.pn << 32) | o2.to.gp; if (f1 <= f2 && f2 <= t1) { // f2 is inside r1 if (t2 <= t1) { // r2 is fully inside r1 return r2; } else { // r2 is partially inside r1 ui_edit_range_t r = {0}; r.from.pn = (int32_t)(f2 >> 32); r.from.gp = (int32_t)(f2); r.to.pn = (int32_t)(t1 >> 32); r.to.gp = (int32_t)(t1); return r; } } else if (f2 <= f1 && f1 <= t2) { // f1 is inside r2 if (t1 <= t2) { // r1 is fully inside r2 return r1; } else { // r1 is partially inside r2 ui_edit_range_t r = {0}; r.from.pn = (int32_t)(f1 >> 32); r.from.gp = (int32_t)(f1); r.to.pn = (int32_t)(t2 >> 32); r.to.gp = (int32_t)(t2); return r; } } else { return *ui_edit_range.invalid_range; } } else { return *ui_edit_range.invalid_range; } } static bool ui_edit_doc_realloc_ps_no_init(ui_edit_str_t* *ps, int32_t old_np, int32_t new_np) { // reallocate paragraphs for (int32_t i = new_np; i < old_np; i++) { ui_edit_str.free(&(*ps)[i]); } bool ok = true; if (new_np == 0) { rt_heap.free(*ps); *ps = null; } else { ok = rt_heap.realloc_zero((void**)ps, new_np * sizeof(ui_edit_str_t)) == 0; } return ok; } static bool ui_edit_doc_realloc_ps(ui_edit_str_t* *ps, int32_t old_np, int32_t new_np) { // reallocate paragraphs bool ok = ui_edit_doc_realloc_ps_no_init(ps, old_np, new_np); if (ok) { for (int32_t i = old_np; i < new_np; i++) { ok = ui_edit_str.init(&(*ps)[i], null, 0, false); rt_swear(ok, "because .init(\"\", 0) does NOT allocate memory"); } } return ok; } static bool ui_edit_text_init(ui_edit_text_t* t, const char* s, int32_t b, bool heap) { // When text comes from the source that lifetime is shorter // than text itself (e.g. paste from clipboard) the parameter // heap: true allows to make a copy of data on the heap ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); if (b < 0) { b = (int32_t)strlen(s); } // if caller is concerned with best performance - it should pass b >= 0 int32_t np = 0; // number of paragraphs int32_t n = rt_max(b / 64, 2); // initial number of allocated paragraphs ui_edit_str_t* ps = null; // ps[n] bool ok = ui_edit_doc_realloc_ps(&ps, 0, n); if (ok) { bool lf = false; int32_t i = 0; while (ok && i < b) { int32_t k = i; while (k < b && s[k] != '\n') { k++; } lf = k < b && s[k] == '\n'; if (np >= n) { int32_t n1_5 = n * 3 / 2; // n * 1.5 rt_assert(n1_5 > n); ok = ui_edit_doc_realloc_ps(&ps, n, n1_5); if (ok) { n = n1_5; } } if (ok) { // insider knowledge about ui_edit_str allocation behaviour: rt_assert(ps[np].c == 0 && ps[np].b == 0 && ps[np].g2b[0] == 0); ui_edit_str.free(&ps[np]); // process "\r\n" strings const int32_t e = k > i && s[k - 1] == '\r' ? k - 1 : k; const int32_t bytes = e - i; rt_assert(bytes >= 0); const char* u = bytes == 0 ? null : s + i; // str.init may allocate str.g2b[] on the heap and may fail ok = ui_edit_str.init(&ps[np], u, bytes, heap && bytes > 0); if (ok) { np++; } } i = k + lf; } if (ok && lf) { // last paragraph ended with line feed if (np + 1 >= n) { ok = ui_edit_doc_realloc_ps(&ps, n, n + 1); if (ok) { n = n + 1; } } if (ok) { np++; } } } if (ok && np == 0) { // special case empty string to a single paragraph rt_assert(b <= 0 && (b == 0 || s[0] == 0x00)); np = 1; // ps[0] is already initialized as empty str ok = ui_edit_doc_realloc_ps(&ps, n, 1); rt_swear(ok, "shrinking ps[] above"); } if (ok) { rt_assert(np > 0); t->np = np; t->ps = ps; } else if (ps != null) { bool shrink = ui_edit_doc_realloc_ps(&ps, n, 0); // free() rt_swear(shrink); rt_heap.free(ps); t->np = 0; t->ps = null; } return ok; } static void ui_edit_text_dispose(ui_edit_text_t* t) { if (t->np != 0) { ui_edit_doc_realloc_ps(&t->ps, t->np, 0); rt_assert(t->ps == null); t->np = 0; } else { rt_assert(t->np == 0 && t->ps == null); } } static void ui_edit_doc_dispose_to_do(ui_edit_to_do_t* to_do) { if (to_do->text.np > 0) { ui_edit_text_dispose(&to_do->text); } memset(&to_do->range, 0x00, sizeof(to_do->range)); ui_edit_check_zeros(to_do, sizeof(*to_do)); } static int32_t ui_edit_text_bytes(const ui_edit_text_t* t, const ui_edit_range_t* range) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); int32_t bytes = 0; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; if (pn == r.from.pn && pn == r.to.pn) { bytes += p->g2b[r.to.gp] - p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes += p->b - p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes += p->g2b[r.to.gp]; } else { bytes += p->b; } } return bytes; } static int32_t ui_edit_doc_bytes(const ui_edit_doc_t* d, const ui_edit_range_t* r) { return ui_edit_text.bytes(&d->text, r); } static int32_t ui_edit_doc_utf8bytes(const ui_edit_doc_t* d, const ui_edit_range_t* range) { const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); int32_t bytes = ui_edit_text.bytes(&d->text, &r); // "\n" after each paragraph and 0x00 return bytes + r.to.pn - r.from.pn + 1; } static void ui_edit_notify_before(ui_edit_doc_t* d, const ui_edit_notify_info_t* ni) { ui_edit_listener_t* o = d->listeners; while (o != null) { if (o->notify != null && o->notify->before != null) { o->notify->before(o->notify, ni); } o = o->next; } } static void ui_edit_notify_after(ui_edit_doc_t* d, const ui_edit_notify_info_t* ni) { ui_edit_listener_t* o = d->listeners; while (o != null) { if (o->notify != null && o->notify->after != null) { o->notify->after(o->notify, ni); } o = o->next; } } static bool ui_edit_doc_subscribe(ui_edit_doc_t* t, ui_edit_notify_t* notify) { // TODO: not sure about double linked list. // heap allocated resizable array may serve better and may be easier to maintain bool ok = true; ui_edit_listener_t* o = t->listeners; if (o == null) { ok = rt_heap.alloc_zero((void**)&t->listeners, sizeof(*o)) == 0; if (ok) { o = t->listeners; } } else { while (o->next != null) { rt_swear(o->notify != notify); o = o->next; } ok = rt_heap.alloc_zero((void**)&o->next, sizeof(*o)) == 0; if (ok) { o->next->prev = o; o = o->next; } } if (ok) { o->notify = notify; } return ok; } static void ui_edit_doc_unsubscribe(ui_edit_doc_t* t, ui_edit_notify_t* notify) { ui_edit_listener_t* o = t->listeners; bool removed = false; while (o != null) { ui_edit_listener_t* n = o->next; if (o->notify == notify) { rt_assert(!removed); if (o->prev != null) { o->prev->next = n; } if (o->next != null) { o->next->prev = o->prev; } if (o == t->listeners) { t->listeners = n; } rt_heap.free(o); removed = true; } o = n; } rt_swear(removed); } static bool ui_edit_doc_copy_text(const ui_edit_doc_t* d, const ui_edit_range_t* range, ui_edit_text_t* t) { ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); ui_edit_check_range_inside_text(&d->text, &r); int32_t np = r.to.pn - r.from.pn + 1; bool ok = ui_edit_doc_realloc_ps(&t->ps, 0, np); if (ok) { t->np = np; } for (int32_t pn = r.from.pn; ok && pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &d->text.ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } rt_assert(t->ps[pn - r.from.pn].g == 0); const char* u_or_null = bytes == 0 ? null : u; ui_edit_str.replace(&t->ps[pn - r.from.pn], 0, 0, u_or_null, bytes); } if (!ok) { ui_edit_text.dispose(t); ui_edit_check_zeros(t, sizeof(*t)); } return ok; } static void ui_edit_doc_copy(const ui_edit_doc_t* d, const ui_edit_range_t* range, char* text, int32_t b) { const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); ui_edit_check_range_inside_text(&d->text, &r); char* to = text; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &d->text.ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } const int32_t c = (int32_t)(uintptr_t)(to - text); if (bytes > 0) { rt_swear(c + bytes < b, "c: %d bytes: %d b: %d", c, bytes, b); memmove(to, u, (size_t)bytes); to += bytes; } if (pn < r.to.pn) { rt_swear(c + bytes < b, "c: %d bytes: %d b: %d", c, bytes, b); *to++ = '\n'; } } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + 1 == b, "c: %d b: %d", c, b); *to++ = 0x00; } static bool ui_edit_text_insert_2_or_more(ui_edit_text_t* t, int32_t pn, const ui_edit_str_t* s, const ui_edit_text_t* insert, const ui_edit_str_t* e) { // insert 2 or more paragraphs rt_assert(0 <= pn && pn < t->np); const int32_t np = t->np + insert->np - 1; rt_assert(np > 0); ui_edit_str_t* ps = null; // ps[np] bool ok = ui_edit_doc_realloc_ps_no_init(&ps, 0, np); if (ok) { memmove(ps, t->ps, (size_t)pn * sizeof(ui_edit_str_t)); // `s` first line of `insert` ok = ui_edit_str.init(&ps[pn], s->u, s->b, true); // lines of `insert` between `s` and `e` for (int32_t i = 1; ok && i < insert->np - 1; i++) { ok = ui_edit_str.init(&ps[pn + i], insert->ps[i].u, insert->ps[i].b, true); } // `e` last line of `insert` if (ok) { const int32_t ix = pn + insert->np - 1; // last `insert` index ok = ui_edit_str.init(&ps[ix], e->u, e->b, true); } rt_assert(t->np - pn - 1 >= 0); memmove(ps + pn + insert->np, t->ps + pn + 1, (size_t)(t->np - pn - 1) * sizeof(ui_edit_str_t)); if (ok) { // this two regions where moved to `ps` memset(t->ps, 0x00, pn * sizeof(ui_edit_str_t)); memset(t->ps + pn + 1, 0x00, (size_t)(t->np - pn - 1) * sizeof(ui_edit_str_t)); // deallocate what was copied from `insert` ui_edit_doc_realloc_ps_no_init(&t->ps, t->np, 0); t->np = np; t->ps = ps; } else { // free allocated memory: ui_edit_doc_realloc_ps_no_init(&ps, np, 0); } } return ok; } static bool ui_edit_text_insert_1(ui_edit_text_t* t, const ui_edit_pg_t ip, // insertion point const ui_edit_text_t* insert) { rt_assert(0 <= ip.pn && ip.pn < t->np); ui_edit_str_t* str = &t->ps[ip.pn]; // string in document text rt_assert(insert->np == 1); ui_edit_str_t* ins = &insert->ps[0]; // string to insert rt_assert(0 <= ip.gp && ip.gp <= str->g); // ui_edit_str.replace() is all or nothing: return ui_edit_str.replace(str, ip.gp, ip.gp, ins->u, ins->b); } static bool ui_edit_substr_append(ui_edit_str_t* d, const ui_edit_str_t* s1, int32_t gp1, const ui_edit_str_t* s2) { // s1[0:gp1] + s2 rt_assert(d != s1 && d != s2); const int32_t b = s1->g2b[gp1]; bool ok = ui_edit_str.init(d, b == 0 ? null : s1->u, b, true); if (ok) { ok = ui_edit_str.replace(d, d->g, d->g, s2->u, s2->b); } else { *d = *ui_edit_str.empty; } return ok; } static bool ui_edit_append_substr(ui_edit_str_t* d, const ui_edit_str_t* s1, const ui_edit_str_t* s2, int32_t gp2) { // s1 + s2[gp1:*] rt_assert(d != s1 && d != s2); bool ok = ui_edit_str.init(d, s1->b == 0 ? null : s1->u, s1->b, true); if (ok) { const int32_t o = s2->g2b[gp2]; // offset (bytes) const int32_t b = s2->b - o; ok = ui_edit_str.replace(d, d->g, d->g, b == 0 ? null : s2->u + o, b); } else { *d = *ui_edit_str.empty; } return ok; } static bool ui_edit_text_insert(ui_edit_text_t* t, const ui_edit_pg_t ip, const ui_edit_text_t* i) { bool ok = true; if (ok) { if (i->np == 1) { ok = ui_edit_text_insert_1(t, ip, i); } else { ui_edit_str_t* str = &t->ps[ip.pn]; ui_edit_str_t s = {0}; // start line of insert text `i` ui_edit_str_t e = {0}; // end line if (ui_edit_substr_append(&s, str, ip.gp, &i->ps[0])) { if (ui_edit_append_substr(&e, &i->ps[i->np - 1], str, ip.gp)) { ok = ui_edit_text_insert_2_or_more(t, ip.pn, &s, i, &e); ui_edit_str.free(&e); } ui_edit_str.free(&s); } } } return ok; } static bool ui_edit_text_remove_lines(ui_edit_text_t* t, ui_edit_str_t* merge, int32_t from, int32_t to) { bool ok = true; for (int32_t pn = from + 1; pn <= to; pn++) { ui_edit_str.free(&t->ps[pn]); } if (t->np - to - 1 > 0) { memmove(&t->ps[from + 1], &t->ps[to + 1], (size_t)(t->np - to - 1) * sizeof(ui_edit_str_t)); } t->np -= to - from; if (ok) { ui_edit_str.swap(&t->ps[from], merge); } return ok; } static bool ui_edit_text_insert_remove(ui_edit_text_t* t, const ui_edit_range_t r, const ui_edit_text_t* i) { bool ok = true; ui_edit_str_t merge = {0}; const ui_edit_str_t* s = &t->ps[r.from.pn]; const ui_edit_str_t* e = &t->ps[r.to.pn]; const int32_t o = e->g2b[r.to.gp]; const int32_t b = e->b - o; const char* u = b == 0 ? null : e->u + o; ok = ui_edit_substr_append(&merge, s, r.from.gp, &i->ps[i->np - 1]) && ui_edit_str.replace(&merge, merge.g, merge.g, u, b); if (ok) { const bool empty_text = i->np == 1 && i->ps[0].g == 0; if (!empty_text) { ok = ui_edit_text_insert(t, r.to, i); } if (ok) { ok = ui_edit_text_remove_lines(t, &merge, r.from.pn, r.to.pn); } } if (merge.c > 0 || merge.g > 0) { ui_edit_str.free(&merge); } return ok; } static bool ui_edit_text_copy_text(const ui_edit_text_t* t, const ui_edit_range_t* range, ui_edit_text_t* to) { ui_edit_check_zeros(to, sizeof(*to)); memset(to, 0x00, sizeof(*to)); const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); int32_t np = r.to.pn - r.from.pn + 1; bool ok = ui_edit_doc_realloc_ps(&to->ps, 0, np); if (ok) { to->np = np; } for (int32_t pn = r.from.pn; ok && pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } rt_assert(to->ps[pn - r.from.pn].g == 0); const char* u_or_null = bytes == 0 ? null : u; ui_edit_str.replace(&to->ps[pn - r.from.pn], 0, 0, u_or_null, bytes); } if (!ok) { ui_edit_text.dispose(to); ui_edit_check_zeros(to, sizeof(*to)); } return ok; } static void ui_edit_text_copy(const ui_edit_text_t* t, const ui_edit_range_t* range, char* text, int32_t b) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); char* to = text; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + bytes < b, "d: %d bytes:%d b: %d", c, bytes, b); if (bytes > 0) { memmove(to, u, (size_t)bytes); to += bytes; } if (pn < r.to.pn) { rt_swear(c + bytes + 1 < b, "d: %d bytes:%d b: %d", c, bytes, b); *to++ = '\n'; } } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + 1 == b, "d: %d b: %d", c, b); *to++ = 0x00; } static bool ui_edit_text_replace(ui_edit_text_t* t, const ui_edit_range_t* range, const ui_edit_text_t* i, ui_edit_to_do_t* undo) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); bool ok = undo == null ? true : ui_edit_text.copy_text(t, &r, &undo->text); ui_edit_range_t x = r; if (ok) { if (ui_edit_range.is_empty(r)) { x.to.pn = r.from.pn + i->np - 1; x.to.gp = i->np == 1 ? r.from.gp + i->ps[0].g : i->ps[i->np - 1].g; ok = ui_edit_text_insert(t, r.from, i); } else if (i->np == 1 && r.from.pn == r.to.pn) { x.to.pn = r.from.pn + i->np - 1; x.to.gp = r.from.gp + i->ps[0].g; ok = ui_edit_str.replace(&t->ps[r.from.pn], r.from.gp, r.to.gp, i->ps[0].u, i->ps[0].b); } else { x.to.pn = r.from.pn + i->np - 1; x.to.gp = i->np == 1 ? r.from.gp + i->ps[0].g : i->ps[0].g; ok = ui_edit_text_insert_remove(t, r, i); } } if (undo != null) { undo->range = x; } return ok; } static bool ui_edit_text_replace_utf8(ui_edit_text_t* t, const ui_edit_range_t* range, const char* utf8, int32_t b, ui_edit_to_do_t* undo) { if (b < 0) { b = (int32_t)strlen(utf8); } ui_edit_text_t i = {0}; bool ok = ui_edit_text.init(&i, utf8, b, false); if (ok) { ok = ui_edit_text.replace(t, range, &i, undo); ui_edit_text.dispose(&i); } return ok; } static bool ui_edit_text_dup(ui_edit_text_t* t, const ui_edit_text_t* s) { ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); bool ok = ui_edit_doc_realloc_ps(&t->ps, 0, s->np); if (ok) { t->np = s->np; for (int32_t i = 0; ok && i < s->np; i++) { const ui_edit_str_t* p = &s->ps[i]; ok = ui_edit_str.replace(&t->ps[i], 0, 0, p->u, p->b); } } if (!ok) { ui_edit_text.dispose(t); } return ok; } static bool ui_edit_text_equal(const ui_edit_text_t* t1, const ui_edit_text_t* t2) { bool equal = t1->np != t2->np; for (int32_t i = 0; equal && i < t1->np; i++) { const ui_edit_str_t* p1 = &t1->ps[i]; const ui_edit_str_t* p2 = &t2->ps[i]; equal = p1->b == p2->b && memcmp(p1->u, p2->u, p1->b) == 0; } return equal; } static void ui_edit_doc_before_replace_text(ui_edit_doc_t* d, const ui_edit_range_t r, const ui_edit_text_t* t) { ui_edit_check_range_inside_text(&d->text, &r); ui_edit_range_t x = r; x.to.pn = r.from.pn + t->np - 1; if (r.from.pn == r.to.pn && t->np == 1) { x.to.gp = r.from.gp + t->ps[0].g; } else { x.to.gp = t->ps[t->np - 1].g; } const ui_edit_notify_info_t ni_before = { .ok = true, .d = d, .r = &r, .x = &x, .t = t, .pnf = r.from.pn, .pnt = r.to.pn, .deleted = 0, .inserted = 0 }; ui_edit_notify_before(d, &ni_before); } static void ui_edit_doc_after_replace_text(ui_edit_doc_t* d, bool ok, const ui_edit_range_t r, const ui_edit_range_t x, const ui_edit_text_t* t) { const ui_edit_notify_info_t ni_after = { .ok = ok, .d = d, .r = &r, .x = &x, .t = t, .pnf = r.from.pn, .pnt = x.to.pn, .deleted = r.to.pn - r.from.pn, .inserted = t->np - 1 }; ui_edit_notify_after(d, &ni_after); } static bool ui_edit_doc_replace_text(ui_edit_doc_t* d, const ui_edit_range_t* range, const ui_edit_text_t* i, ui_edit_to_do_t* undo) { ui_edit_text_t* t = &d->text; const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_doc_before_replace_text(d, r, i); bool ok = ui_edit_text.replace(t, &r, i, undo); ui_edit_doc_after_replace_text(d, ok, r, undo->range, i); return ok; } static bool ui_edit_doc_replace_undoable(ui_edit_doc_t* d, const ui_edit_range_t* r, const ui_edit_text_t* t, ui_edit_to_do_t* undo) { bool ok = ui_edit_doc_replace_text(d, r, t, undo); if (ok && undo != null) { undo->next = d->undo; d->undo = undo; // redo stack is not valid after new replace, empty it: while (d->redo != null) { ui_edit_to_do_t* next = d->redo->next; d->redo->next = null; ui_edit_doc.dispose_to_do(d->redo); rt_heap.free(d->redo); d->redo = next; } } return ok; } static bool ui_edit_utf8_to_heap_text(const char* u, int32_t b, ui_edit_text_t* it) { rt_assert((b == 0) == (u == null || u[0] == 0x00)); return ui_edit_text.init(it, b != 0 ? u : null, b, true); } static bool ui_edit_doc_coalesce_undo(ui_edit_doc_t* d, ui_edit_text_t* i) { ui_edit_to_do_t* undo = d->undo; ui_edit_to_do_t* next = undo->next; // rt_println("i: %.*s", i->ps[0].b, i->ps[0].u); // if (i->np == 1 && i->ps[0].g == 1) { // rt_println("an: %d", ui_edit_str.is_letter(rt_str.utf32(i->ps[0].u, i->ps[0].b))); // } bool coalesced = false; const bool alpha_numeric = i->np == 1 && i->ps[0].g == 1 && ui_edit_str.is_letter(rt_str.utf32(i->ps[0].u, i->ps[0].b)); if (alpha_numeric && next != null) { const ui_edit_range_t ur = undo->range; const ui_edit_text_t* ut = &undo->text; const ui_edit_range_t nr = next->range; const ui_edit_text_t* nt = &next->text; // rt_println("next: \"%.*s\" %d:%d..%d:%d undo: \"%.*s\" %d:%d..%d:%d", // nt->ps[0].b, nt->ps[0].u, nr.from.pn, nr.from.gp, nr.to.pn, nr.to.gp, // ut->ps[0].b, ut->ps[0].u, ur.from.pn, ur.from.gp, ur.to.pn, ur.to.gp); const bool c = nr.from.pn == nr.to.pn && ur.from.pn == ur.to.pn && nr.from.pn == ur.from.pn && ut->np == 1 && ut->ps[0].g == 0 && nt->np == 1 && nt->ps[0].g == 0 && nr.to.gp == ur.from.gp && nr.to.gp > 0; if (c) { const ui_edit_str_t* str = &d->text.ps[nr.from.pn]; const int32_t* g2b = str->g2b; const char* utf8 = str->u + g2b[nr.to.gp - 1]; uint32_t utf32 = rt_str.utf32(utf8, g2b[nr.to.gp] - g2b[nr.to.gp - 1]); coalesced = ui_edit_str.is_letter(utf32); } if (coalesced) { // rt_println("coalesced"); next->range.to.gp++; d->undo = next; undo->next = null; coalesced = true; } } return coalesced; } static bool ui_edit_doc_replace(ui_edit_doc_t* d, const ui_edit_range_t* range, const char* u, int32_t b) { ui_edit_text_t* t = &d->text; const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_to_do_t* undo = null; bool ok = rt_heap.alloc_zero((void**)&undo, sizeof(ui_edit_to_do_t)) == 0; if (ok) { ui_edit_text_t i = {0}; ok = ui_edit_utf8_to_heap_text(u, b, &i); if (ok) { ok = ui_edit_doc_replace_undoable(d, &r, &i, undo); if (ok) { if (ui_edit_doc_coalesce_undo(d, &i)) { ui_edit_doc.dispose_to_do(undo); rt_heap.free(undo); undo = null; } } ui_edit_text.dispose(&i); } if (!ok) { ui_edit_doc.dispose_to_do(undo); rt_heap.free(undo); undo = null; } } return ok; } static bool ui_edit_doc_do(ui_edit_doc_t* d, ui_edit_to_do_t* to_do, ui_edit_to_do_t* *stack) { const ui_edit_range_t* r = &to_do->range; ui_edit_to_do_t* redo = null; bool ok = rt_heap.alloc_zero((void**)&redo, sizeof(ui_edit_to_do_t)) == 0; if (ok) { ok = ui_edit_doc_replace_text(d, r, &to_do->text, redo); if (ok) { ui_edit_doc.dispose_to_do(to_do); rt_heap.free(to_do); } if (ok) { redo->next = *stack; *stack = redo; } else { if (redo != null) { ui_edit_doc.dispose_to_do(redo); rt_heap.free(redo); } } } return ok; } static bool ui_edit_doc_redo(ui_edit_doc_t* d) { ui_edit_to_do_t* to_do = d->redo; if (to_do == null) { return false; } else { d->redo = d->redo->next; to_do->next = null; return ui_edit_doc_do(d, to_do, &d->undo); } } static bool ui_edit_doc_undo(ui_edit_doc_t* d) { ui_edit_to_do_t* to_do = d->undo; if (to_do == null) { return false; } else { d->undo = d->undo->next; to_do->next = null; return ui_edit_doc_do(d, to_do, &d->redo); } } static bool ui_edit_doc_init(ui_edit_doc_t* d, const char* utf8, int32_t bytes, bool heap) { bool ok = true; ui_edit_check_zeros(d, sizeof(*d)); memset(d, 0x00, sizeof(d)); if (bytes < 0) { size_t n = strlen(utf8); rt_swear(n < INT32_MAX); bytes = (int32_t)n; } rt_assert((utf8 == null) == (bytes == 0)); if (ok) { if (bytes == 0) { // empty string ok = rt_heap.alloc_zero((void**)&d->text.ps, sizeof(ui_edit_str_t)) == 0; if (ok) { d->text.np = 1; ok = ui_edit_str.init(&d->text.ps[0], null, 0, false); } } else { ok = ui_edit_text.init(&d->text, utf8, bytes, heap); } } return ok; } static void ui_edit_doc_dispose(ui_edit_doc_t* d) { for (int32_t i = 0; i < d->text.np; i++) { ui_edit_str.free(&d->text.ps[i]); } if (d->text.ps != null) { rt_heap.free(d->text.ps); d->text.ps = null; } d->text.np = 0; while (d->undo != null) { ui_edit_to_do_t* next = d->undo->next; d->undo->next = null; ui_edit_doc.dispose_to_do(d->undo); rt_heap.free(d->undo); d->undo = next; } while (d->redo != null) { ui_edit_to_do_t* next = d->redo->next; d->redo->next = null; ui_edit_doc.dispose_to_do(d->redo); rt_heap.free(d->redo); d->redo = next; } rt_assert(d->listeners == null, "unsubscribe listeners?"); while (d->listeners != null) { ui_edit_listener_t* next = d->listeners->next; d->listeners->next = null; rt_heap.free(d->listeners->next); d->listeners = next; } ui_edit_check_zeros(d, sizeof(*d)); } // ui_edit_str static int32_t ui_edit_str_g2b_ascii[1024]; // ui_edit_str_g2b_ascii[i] == i for all "i" static char ui_edit_str_empty_utf8[1] = {0x00}; static const ui_edit_str_t ui_edit_str_empty = { .u = ui_edit_str_empty_utf8, .g2b = ui_edit_str_g2b_ascii, .c = 0, .b = 0, .g = 0 }; static bool ui_edit_str_init(ui_edit_str_t* s, const char* u, int32_t b, bool heap); static void ui_edit_str_swap(ui_edit_str_t* s1, ui_edit_str_t* s2); static int32_t ui_edit_str_gp_to_bp(const char* s, int32_t bytes, int32_t gp); static int32_t ui_edit_str_bytes(ui_edit_str_t* s, int32_t f, int32_t t); static bool ui_edit_str_expand(ui_edit_str_t* s, int32_t c); static void ui_edit_str_shrink(ui_edit_str_t* s); static bool ui_edit_str_replace(ui_edit_str_t* s, int32_t f, int32_t t, const char* u, int32_t b); // bool (*is_zwj)(uint32_t utf32); // zero width joiner // bool (*is_letter)(uint32_t utf32); // in European Alphabets // bool (*is_digit)(uint32_t utf32); // bool (*is_symbol)(uint32_t utf32); // bool (*is_alphanumeric)(uint32_t utf32); // bool (*is_blank)(uint32_t utf32); // white space // bool (*is_punctuation)(uint32_t utf32); // bool (*is_combining)(uint32_t utf32); // bool (*is_spacing)(uint32_t utf32); // spacing modifiers // bool (*is_cjk_or_emoji)(uint32_t utf32); static bool ui_edit_str_is_zwj(uint32_t utf32); static bool ui_edit_str_is_letter(uint32_t utf32); static bool ui_edit_str_is_digit(uint32_t utf32); static bool ui_edit_str_is_symbol(uint32_t utf32); static bool ui_edit_str_is_alphanumeric(uint32_t utf32); static bool ui_edit_str_is_blank(uint32_t utf32); static bool ui_edit_str_is_punctuation(uint32_t utf32); static bool ui_edit_str_is_combining(uint32_t utf32); static bool ui_edit_str_is_spacing(uint32_t utf32); static bool ui_edit_str_is_blank(uint32_t utf32); static bool ui_edit_str_is_cjk_or_emoji(uint32_t utf32); static bool ui_edit_str_can_break(uint32_t cp1, uint32_t cp2); static void ui_edit_str_test(void); static void ui_edit_str_free(ui_edit_str_t* s); ui_edit_str_if ui_edit_str = { .init = ui_edit_str_init, .swap = ui_edit_str_swap, .gp_to_bp = ui_edit_str_gp_to_bp, .bytes = ui_edit_str_bytes, .expand = ui_edit_str_expand, .shrink = ui_edit_str_shrink, .replace = ui_edit_str_replace, .is_zwj = ui_edit_str_is_zwj, .is_letter = ui_edit_str_is_letter, .is_digit = ui_edit_str_is_digit, .is_symbol = ui_edit_str_is_symbol, .is_alphanumeric = ui_edit_str_is_alphanumeric, .is_blank = ui_edit_str_is_blank, .is_punctuation = ui_edit_str_is_punctuation, .is_combining = ui_edit_str_is_combining, .is_spacing = ui_edit_str_is_spacing, .is_punctuation = ui_edit_str_is_punctuation, .is_cjk_or_emoji = ui_edit_str_is_cjk_or_emoji, .can_break = ui_edit_str_can_break, .test = ui_edit_str_test, .free = ui_edit_str_free, .empty = &ui_edit_str_empty }; #pragma push_macro("ui_edit_str_check") #pragma push_macro("ui_edit_str_check_from_to") #pragma push_macro("ui_edit_check_zeros") #pragma push_macro("ui_edit_str_check_empty") #pragma push_macro("ui_edit_str_parameters") #ifdef DEBUG #define ui_edit_str_check(s) do { \ /* check the s struct constrains */ \ rt_assert(s->b >= 0); \ rt_assert(s->c == 0 || s->c >= s->b); \ rt_assert(s->g >= 0); \ /* s->g2b[] may be null (not heap allocated) when .b == 0 */ \ if (s->g == 0) { rt_assert(s->b == 0); } \ if (s->g > 0) { \ rt_assert(s->g2b[0] == 0 && s->g2b[s->g] == s->b); \ } \ for (int32_t i = 1; i < s->g; i++) { \ rt_assert(0 < s->g2b[i] - s->g2b[i - 1] && \ s->g2b[i] - s->g2b[i - 1] <= 4); \ rt_assert(s->g2b[i] - s->g2b[i - 1] == \ rt_str.utf8bytes( \ s->u + s->g2b[i - 1], s->g2b[i] - s->g2b[i - 1])); \ } \ } while (0) #define ui_edit_str_check_from_to(s, f, t) do { \ rt_assert(0 <= f && f <= s->g); \ rt_assert(0 <= t && t <= s->g); \ rt_assert(f <= t); \ } while (0) #define ui_edit_str_check_empty(u, b) do { \ if (b == 0) { rt_assert(u != null && u[0] == 0x00); } \ if (u == null || u[0] == 0x00) { rt_assert(b == 0); } \ } while (0) #else #define ui_edit_str_check(s) do { } while (0) #define ui_edit_str_check_from_to(s, f, t) do { } while (0) #define ui_edit_str_check_empty(u, b) do { } while (0) #endif // ui_edit_str_foo(*, "...", -1) treat as 0x00 terminated // ui_edit_str_foo(*, null, 0) treat as ("", 0) #define ui_edit_str_parameters(u, b) do { \ if (u == null) { u = ui_edit_str_empty_utf8; } \ if (b < 0) { \ rt_assert(strlen(u) < INT32_MAX); \ b = (int32_t)strlen(u); \ } \ ui_edit_str_check_empty(u, b); \ } while (0) static int32_t ui_edit_str_gp_to_bp(const char* utf8, int32_t bytes, int32_t gp) { rt_swear(bytes >= 0); bool ok = true; int32_t c = 0; int32_t i = 0; if (bytes > 0) { while (c < gp && ok) { rt_assert(i < bytes); const int32_t b = rt_str.utf8bytes(utf8 + i, bytes - i); ok = 0 < b && i + b <= bytes; if (ok) { i += b; c++; } } } rt_assert(i <= bytes); return ok ? i : -1; } static void ui_edit_str_free(ui_edit_str_t* s) { if (s->g2b != null && s->g2b != ui_edit_str_g2b_ascii) { rt_heap.free(s->g2b); } else { #ifdef UI_EDIT_STR_TEST // check ui_edit_str_g2b_ascii integrity for (int32_t i = 0; i < rt_countof(ui_edit_str_g2b_ascii); i++) { rt_assert(ui_edit_str_g2b_ascii[i] == i); } #endif } s->g2b = null; s->g = 0; if (s->c > 0) { rt_heap.free(s->u); s->u = null; s->c = 0; s->b = 0; } else { s->u = null; s->b = 0; } ui_edit_check_zeros(s, sizeof(*s)); } static bool ui_edit_str_init_g2b(ui_edit_str_t* s) { const int64_t _4_bytes = (int64_t)sizeof(int32_t); // start with number of glyphs == number of bytes (ASCII text): bool ok = rt_heap.alloc(&s->g2b, (size_t)(s->b + 1) * _4_bytes) == 0; int32_t i = 0; // index in u[] string int32_t k = 1; // glyph number // g2b[k] start postion in uint8_t offset from utf8 text of glyph[k] while (i < s->b && ok) { const int32_t b = rt_str.utf8bytes(s->u + i, s->b - i); ok = b > 0 && i + b <= s->b; if (ok) { i += b; s->g2b[k] = i; k++; } } if (ok) { rt_assert(0 < k && k <= s->b + 1); s->g2b[0] = 0; rt_assert(s->g2b[k - 1] == s->b); s->g = k - 1; if (k < s->b + 1) { ok = rt_heap.realloc(&s->g2b, k * _4_bytes) == 0; rt_assert(ok, "shrinking - should always be ok"); } } return ok; } static bool ui_edit_str_init(ui_edit_str_t* s, const char* u, int32_t b, bool heap) { enum { n = rt_countof(ui_edit_str_g2b_ascii) }; if (ui_edit_str_g2b_ascii[n - 1] != n - 1) { for (int32_t i = 0; i < n; i++) { ui_edit_str_g2b_ascii[i] = i; } } bool ok = true; ui_edit_check_zeros(s, sizeof(*s)); // caller must zero out memset(s, 0x00, sizeof(*s)); ui_edit_str_parameters(u, b); if (b == 0) { // cast below intentionally removes "const" qualifier s->g2b = (int32_t*)ui_edit_str_g2b_ascii; s->u = (char*)u; rt_assert(s->c == 0 && u[0] == 0x00); } else { if (heap) { ok = rt_heap.alloc((void**)&s->u, b) == 0; if (ok) { s->c = b; memmove(s->u, u, (size_t)b); } } else { s->u = (char*)u; } if (ok) { s->b = b; if (b == 1 && u[0] <= 0x7F) { s->g2b = (int32_t*)ui_edit_str_g2b_ascii; s->g = 1; } else { ok = ui_edit_str_init_g2b(s); } } } if (ok) { ui_edit_str.shrink(s); } else { ui_edit_str.free(s); } return ok; } static void ui_edit_str_swap(ui_edit_str_t* s1, ui_edit_str_t* s2) { ui_edit_str_t s = *s1; *s1 = *s2; *s2 = s; } static int32_t ui_edit_str_bytes(ui_edit_str_t* s, int32_t f, int32_t t) { // glyph positions ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); return s->g2b[t] - s->g2b[f]; } static bool ui_edit_str_move_g2b_to_heap(ui_edit_str_t* s) { bool ok = true; if (s->g2b == ui_edit_str_g2b_ascii) { // even for s->g == 0 if (s->b == s->g && s->g < rt_countof(ui_edit_str_g2b_ascii) - 1) { // rt_println("forcefully moving to heap"); // this is usually done in the process of concatenation // of 2 ascii strings when result is known to be longer // than rt_countof(ui_edit_str_g2b_ascii) - 1 but the // first string in concatenation is short. It's OK. } const int32_t bytes = (s->g + 1) * (int32_t)sizeof(int32_t); ok = rt_heap.alloc(&s->g2b, bytes) == 0; if (ok) { memmove(s->g2b, ui_edit_str_g2b_ascii, (size_t)bytes); } } return ok; } static bool ui_edit_str_move_to_heap(ui_edit_str_t* s, int32_t c) { bool ok = true; rt_assert(c >= s->b, "can expand cannot shrink"); if (s->c == 0) { // s->u points outside of the heap const char* o = s->u; ok = rt_heap.alloc((void**)&s->u, c) == 0; if (ok) { memmove(s->u, o, (size_t)s->b); } } else if (s->c < c) { ok = rt_heap.realloc((void**)&s->u, c) == 0; } if (ok) { s->c = c; } return ok; } static bool ui_edit_str_expand(ui_edit_str_t* s, int32_t c) { rt_swear(c > 0); bool ok = ui_edit_str_move_to_heap(s, c); if (ok && c > s->c) { if (rt_heap.realloc((void**)&s->u, c) == 0) { s->c = c; } else { ok = false; } } return ok; } static void ui_edit_str_shrink(ui_edit_str_t* s) { if (s->c > s->b) { // s->c == 0 for empty and single byte ASCII strings rt_assert(s->u != ui_edit_str_empty_utf8); if (s->b == 0) { rt_heap.free(s->u); s->u = ui_edit_str_empty_utf8; } else { bool ok = rt_heap.realloc((void**)&s->u, s->b) == 0; rt_swear(ok, "smaller size is always expected to be ok"); } s->c = s->b; } // Optimize memory for short ASCII only strings: if (s->g2b != ui_edit_str_g2b_ascii) { if (s->g == s->b && s->g < rt_countof(ui_edit_str_g2b_ascii) - 1) { // If this is an ascii only utf8 string shorter than // ui_edit_str_g2b_ascii it does not need .g2b[] allocated: if (s->g2b != ui_edit_str_g2b_ascii) { rt_heap.free(s->g2b); s->g2b = ui_edit_str_g2b_ascii; } } else { // const int32_t b64 = rt_min(s->b, 64); // rt_println("none ASCII: .b:%d .g:%d %*.*s", s->b, s->g, b64, b64, s->u); } } } static bool ui_edit_str_remove(ui_edit_str_t* s, int32_t f, int32_t t) { bool ok = true; // optimistic approach ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); const int32_t bytes_to_remove = s->g2b[t] - s->g2b[f]; rt_assert(bytes_to_remove >= 0); if (bytes_to_remove > 0) { ok = ui_edit_str_move_to_heap(s, s->b); if (ok) { const int32_t bytes_to_shift = s->b - s->g2b[t]; rt_assert(0 <= bytes_to_shift && bytes_to_shift <= s->b); memmove(s->u + s->g2b[f], s->u + s->g2b[t], (size_t)bytes_to_shift); if (s->g2b != ui_edit_str_g2b_ascii) { memmove(s->g2b + f, s->g2b + t, (size_t)(s->g - t + 1) * sizeof(int32_t)); for (int32_t i = f; i <= s->g; i++) { s->g2b[i] -= bytes_to_remove; } } else { // no need to shrink g2b[] for ASCII only strings: for (int32_t i = 0; i <= s->g; i++) { rt_assert(s->g2b[i] == i); } } s->b -= bytes_to_remove; s->g -= t - f; } } ui_edit_str_check(s); return ok; } static bool ui_edit_str_replace(ui_edit_str_t* s, int32_t f, int32_t t, const char* u, int32_t b) { const int64_t _4_bytes = (int64_t)sizeof(int32_t); bool ok = true; // optimistic approach ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); ui_edit_str_parameters(u, b); // we are inserting "b" bytes and removing "t - f" glyphs const int32_t bytes_to_remove = s->g2b[t] - s->g2b[f]; const int32_t bytes_to_insert = b; // only for readability if (b == 0) { // just remove glyphs ok = ui_edit_str_remove(s, f, t); } else { // remove and insert ui_edit_str_t ins = {0}; // ui_edit_str_init_ro() verifies utf-8 and calculates g2b[]: ok = ui_edit_str_init(&ins, u, b, false); const int32_t glyphs_to_insert = ins.g; // only for readability const int32_t glyphs_to_remove = t - f; // only for readability if (ok) { const int32_t bytes = s->b + bytes_to_insert - bytes_to_remove; rt_assert(ins.g2b != null); // pacify code analysis rt_assert(bytes > 0); const int32_t c = rt_max(s->b, bytes); // keep g2b == ui_edit_str_g2b_ascii as much as possible const bool all_ascii = s->g2b == ui_edit_str_g2b_ascii && ins.g2b == ui_edit_str_g2b_ascii && bytes < rt_countof(ui_edit_str_g2b_ascii) - 1; ok = ui_edit_str_move_to_heap(s, c); if (ok) { if (!all_ascii) { ui_edit_str_move_g2b_to_heap(s); } // insert ui_edit_str_t "ins" at glyph position "f" // reusing ins.u[0..ins.b-1] and ins.g2b[0..ins.g] // moving memory using memmove() left to right: if (bytes_to_insert <= bytes_to_remove) { memmove(s->u + s->g2b[f] + bytes_to_insert, s->u + s->g2b[f] + bytes_to_remove, (size_t)(s->b - s->g2b[f] - bytes_to_remove)); if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); memmove(s->g2b + f + glyphs_to_insert, s->g2b + f + glyphs_to_remove, (size_t)(s->g - t + 1) * _4_bytes); } memmove(s->u + s->g2b[f], ins.u, (size_t)ins.b); } else { if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); const int32_t g = s->g + glyphs_to_insert - glyphs_to_remove; rt_assert(g > s->g); ok = rt_heap.realloc(&s->g2b, (size_t)(g + 1) * _4_bytes) == 0; } // need to shift bytes staring with s.g2b[t] toward the end if (ok) { memmove(s->u + s->g2b[f] + bytes_to_insert, s->u + s->g2b[f] + bytes_to_remove, (size_t)(s->b - s->g2b[f] - bytes_to_remove)); if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); memmove(s->g2b + f + glyphs_to_insert, s->g2b + f + glyphs_to_remove, (size_t)(s->g - t + 1) * _4_bytes); } memmove(s->u + s->g2b[f], ins.u, (size_t)ins.b); } } if (ok) { if (!all_ascii) { rt_assert(s->g2b != null && s->g2b != ui_edit_str_g2b_ascii); for (int32_t i = f; i <= f + glyphs_to_insert; i++) { s->g2b[i] = ins.g2b[i - f] + s->g2b[f]; } } else { rt_assert(s->g2b == ui_edit_str_g2b_ascii); for (int32_t i = f; i <= f + glyphs_to_insert; i++) { rt_assert(ui_edit_str_g2b_ascii[i] == i); rt_assert(ins.g2b[i - f] + s->g2b[f] == i); } } s->b += bytes_to_insert - bytes_to_remove; s->g += glyphs_to_insert - glyphs_to_remove; rt_assert(s->b == bytes); if (!all_ascii) { rt_assert(s->g2b != ui_edit_str_g2b_ascii); for (int32_t i = f + glyphs_to_insert + 1; i <= s->g; i++) { s->g2b[i] += bytes_to_insert - bytes_to_remove; } s->g2b[s->g] = s->b; } else { rt_assert(s->g2b == ui_edit_str_g2b_ascii); for (int32_t i = f + glyphs_to_insert + 1; i <= s->g; i++) { rt_assert(s->g2b[i] == i); rt_assert(ui_edit_str_g2b_ascii[i] == i); } rt_assert(s->g2b[s->g] == s->b); } } } ui_edit_str_free(&ins); } } ui_edit_str_shrink(s); ui_edit_str_check(s); return ok; } static bool ui_edit_str_is_zwj(uint32_t utf32) { return utf32 == 0x200D; } static bool ui_edit_str_is_punctuation(uint32_t utf32) { return (utf32 >= 0x0021 && utf32 <= 0x0023) || // !"# (utf32 >= 0x0025 && utf32 <= 0x002A) || // %&'()*+ (utf32 >= 0x002C && utf32 <= 0x002F) || // ,-./ (utf32 >= 0x003A && utf32 <= 0x003B) || //:; (utf32 >= 0x003F && utf32 <= 0x0040) || // ?@ (utf32 >= 0x005B && utf32 <= 0x005D) || // [\] (utf32 == 0x005F) || // _ (utf32 == 0x007B) || // { (utf32 == 0x007D) || // } (utf32 == 0x007E) || // ~ (utf32 >= 0x2000 && utf32 <= 0x206F) || // General Punctuation (utf32 >= 0x3000 && utf32 <= 0x303F) || // CJK Symbols and Punctuation (utf32 >= 0xFE30 && utf32 <= 0xFE4F) || // CJK Compatibility Forms (utf32 >= 0xFE50 && utf32 <= 0xFE6F) || // Small Form Variants (utf32 >= 0xFF01 && utf32 <= 0xFF0F) || // Fullwidth ASCII variants (utf32 >= 0xFF1A && utf32 <= 0xFF1F) || // Fullwidth ASCII variants (utf32 >= 0xFF3B && utf32 <= 0xFF3D) || // Fullwidth ASCII variants (utf32 == 0xFF3F) || // Fullwidth _ (utf32 >= 0xFF5B && utf32 <= 0xFF65); // Fullwidth ASCII variants and halfwidth forms } static bool ui_edit_str_is_letter(uint32_t utf32) { return (utf32 >= 0x0041 && utf32 <= 0x005A) || // Latin uppercase (utf32 >= 0x0061 && utf32 <= 0x007A) || // Latin lowercase (utf32 >= 0x00C0 && utf32 <= 0x00D6) || // Latin-1 uppercase (utf32 >= 0x00D8 && utf32 <= 0x00F6) || // Latin-1 lowercase (utf32 >= 0x00F8 && utf32 <= 0x00FF) || // Latin-1 lowercase (utf32 >= 0x0100 && utf32 <= 0x017F) || // Latin Extended-A (utf32 >= 0x0180 && utf32 <= 0x024F) || // Latin Extended-B (utf32 >= 0x0250 && utf32 <= 0x02AF) || // IPA Extensions (utf32 >= 0x0370 && utf32 <= 0x03FF) || // Greek and Coptic (utf32 >= 0x0400 && utf32 <= 0x04FF) || // Cyrillic (utf32 >= 0x0500 && utf32 <= 0x052F) || // Cyrillic Supplement (utf32 >= 0x0530 && utf32 <= 0x058F) || // Armenian (utf32 >= 0x10A0 && utf32 <= 0x10FF) || // Georgian (utf32 >= 0x0600 && utf32 <= 0x06FF) || // Arabic (covers Arabic, Kurdish, and Pashto) (utf32 >= 0x0900 && utf32 <= 0x097F) || // Devanagari (covers Hindi) (utf32 >= 0x0980 && utf32 <= 0x09FF) || // Bengali (utf32 >= 0x0A00 && utf32 <= 0x0A7F) || // Gurmukhi (common in Northern India, related to Punjabi) (utf32 >= 0x0B80 && utf32 <= 0x0BFF) || // Tamil (utf32 >= 0x0C00 && utf32 <= 0x0C7F) || // Telugu (utf32 >= 0x0C80 && utf32 <= 0x0CFF) || // Kannada (utf32 >= 0x0D00 && utf32 <= 0x0D7F) || // Malayalam (utf32 >= 0x0D80 && utf32 <= 0x0DFF) || // Sinhala (utf32 >= 0x3040 && utf32 <= 0x309F) || // Hiragana (because it is syllabic) (utf32 >= 0x30A0 && utf32 <= 0x30FF) || // Katakana (utf32 >= 0x1E00 && utf32 <= 0x1EFF); // Latin Extended Additional } static bool ui_edit_str_is_spacing(uint32_t utf32) { return (utf32 >= 0x02B0 && utf32 <= 0x02FF) || // Spacing Modifier Letters (utf32 >= 0xA700 && utf32 <= 0xA71F); // Modifier Tone Letters } static bool ui_edit_str_is_combining(uint32_t utf32) { return (utf32 >= 0x0300 && utf32 <= 0x036F) || // Combining Diacritical Marks (utf32 >= 0x1AB0 && utf32 <= 0x1AFF) || // Combining Diacritical Marks Extended (utf32 >= 0x1DC0 && utf32 <= 0x1DFF) || // Combining Diacritical Marks Supplement (utf32 >= 0x20D0 && utf32 <= 0x20FF) || // Combining Diacritical Marks for Symbols (utf32 >= 0xFE20 && utf32 <= 0xFE2F); // Combining Half Marks } static bool ui_edit_str_is_blank(uint32_t utf32) { return (utf32 == 0x0009) || // Horizontal Tab (utf32 == 0x000A) || // Line Feed (utf32 == 0x000B) || // Vertical Tab (utf32 == 0x000C) || // Form Feed (utf32 == 0x000D) || // Carriage Return (utf32 == 0x0020) || // Space (utf32 == 0x0085) || // Next Line (utf32 == 0x00A0) || // Non-breaking Space (utf32 == 0x1680) || // Ogham Space Mark (utf32 >= 0x2000 && utf32 <= 0x200A) || // En Quad to Hair Space (utf32 == 0x2028) || // Line Separator (utf32 == 0x2029) || // Paragraph Separator (utf32 == 0x202F) || // Narrow No-Break Space (utf32 == 0x205F) || // Medium Mathematical Space (utf32 == 0x3000); // Ideographic Space } static bool ui_edit_str_is_symbol(uint32_t utf32) { return (utf32 >= 0x0024 && utf32 <= 0x0024) || // Dollar sign (utf32 >= 0x00A2 && utf32 <= 0x00A5) || // Cent sign to Yen sign (utf32 >= 0x20A0 && utf32 <= 0x20CF) || // Currency Symbols (utf32 >= 0x2100 && utf32 <= 0x214F) || // Letter like Symbols (utf32 >= 0x2190 && utf32 <= 0x21FF) || // Arrows (utf32 >= 0x2200 && utf32 <= 0x22FF) || // Mathematical Operators (utf32 >= 0x2300 && utf32 <= 0x23FF) || // Miscellaneous Technical (utf32 >= 0x2400 && utf32 <= 0x243F) || // Control Pictures (utf32 >= 0x2440 && utf32 <= 0x245F) || // Optical Character Recognition (utf32 >= 0x2460 && utf32 <= 0x24FF) || // Enclosed Alphanumeric (utf32 >= 0x2500 && utf32 <= 0x257F) || // Box Drawing (utf32 >= 0x2580 && utf32 <= 0x259F) || // Block Elements (utf32 >= 0x25A0 && utf32 <= 0x25FF) || // Geometric Shapes (utf32 >= 0x2600 && utf32 <= 0x26FF) || // Miscellaneous Symbols (utf32 >= 0x2700 && utf32 <= 0x27BF) || // Dingbats (utf32 >= 0x2900 && utf32 <= 0x297F) || // Supplemental Arrows-B (utf32 >= 0x2B00 && utf32 <= 0x2BFF) || // Miscellaneous Symbols and Arrows (utf32 >= 0xFB00 && utf32 <= 0xFB4F) || // Alphabetic Presentation Forms (utf32 >= 0xFE50 && utf32 <= 0xFE6F) || // Small Form Variants (utf32 >= 0xFF01 && utf32 <= 0xFF20) || // Fullwidth ASCII variants (utf32 >= 0xFF3B && utf32 <= 0xFF40) || // Fullwidth ASCII variants (utf32 >= 0xFF5B && utf32 <= 0xFF65); // Fullwidth ASCII variants } static bool ui_edit_str_is_digit(uint32_t utf32) { return (utf32 >= 0x0030 && utf32 <= 0x0039) || // ASCII digits 0-9 (utf32 >= 0x0660 && utf32 <= 0x0669) || // Arabic-Indic digits (utf32 >= 0x06F0 && utf32 <= 0x06F9) || // Extended Arabic-Indic digits (utf32 >= 0x07C0 && utf32 <= 0x07C9) || // N'Ko digits (utf32 >= 0x0966 && utf32 <= 0x096F) || // Devanagari digits (utf32 >= 0x09E6 && utf32 <= 0x09EF) || // Bengali digits (utf32 >= 0x0A66 && utf32 <= 0x0A6F) || // Gurmukhi digits (utf32 >= 0x0AE6 && utf32 <= 0x0AEF) || // Gujarati digits (utf32 >= 0x0B66 && utf32 <= 0x0B6F) || // Oriya digits (utf32 >= 0x0BE6 && utf32 <= 0x0BEF) || // Tamil digits (utf32 >= 0x0C66 && utf32 <= 0x0C6F) || // Telugu digits (utf32 >= 0x0CE6 && utf32 <= 0x0CEF) || // Kannada digits (utf32 >= 0x0D66 && utf32 <= 0x0D6F) || // Malayalam digits (utf32 >= 0x0E50 && utf32 <= 0x0E59) || // Thai digits (utf32 >= 0x0ED0 && utf32 <= 0x0ED9) || // Lao digits (utf32 >= 0x0F20 && utf32 <= 0x0F29) || // Tibetan digits (utf32 >= 0x1040 && utf32 <= 0x1049) || // Myanmar digits (utf32 >= 0x17E0 && utf32 <= 0x17E9) || // Khmer digits (utf32 >= 0x1810 && utf32 <= 0x1819) || // Mongolian digits (utf32 >= 0xFF10 && utf32 <= 0xFF19); // Fullwidth digits } static bool ui_edit_str_is_alphanumeric(uint32_t utf32) { return ui_edit_str.is_letter(utf32) || ui_edit_str.is_digit(utf32); } static bool ui_edit_str_is_cjk_or_emoji(uint32_t utf32) { return !ui_edit_str_is_letter(utf32) && ((utf32 >= 0x4E00 && utf32 <= 0x9FFF) || // CJK Unified Ideographs (utf32 >= 0x3400 && utf32 <= 0x4DBF) || // CJK Unified Ideographs Extension A (utf32 >= 0x20000 && utf32 <= 0x2A6DF) || // CJK Unified Ideographs Extension B (utf32 >= 0x2A700 && utf32 <= 0x2B73F) || // CJK Unified Ideographs Extension C (utf32 >= 0x2B740 && utf32 <= 0x2B81F) || // CJK Unified Ideographs Extension D (utf32 >= 0x2B820 && utf32 <= 0x2CEAF) || // CJK Unified Ideographs Extension E (utf32 >= 0x2CEB0 && utf32 <= 0x2EBEF) || // CJK Unified Ideographs Extension F (utf32 >= 0xF900 && utf32 <= 0xFAFF) || // CJK Compatibility Ideographs (utf32 >= 0x2F800 && utf32 <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement (utf32 >= 0x1F600 && utf32 <= 0x1F64F) || // Emoticons (utf32 >= 0x1F300 && utf32 <= 0x1F5FF) || // Misc Symbols and Pictographs (utf32 >= 0x1F680 && utf32 <= 0x1F6FF) || // Transport and Map (utf32 >= 0x1F700 && utf32 <= 0x1F77F) || // Alchemical Symbols (utf32 >= 0x1F780 && utf32 <= 0x1F7FF) || // Geometric Shapes Extended (utf32 >= 0x1F800 && utf32 <= 0x1F8FF) || // Supplemental Arrows-C (utf32 >= 0x1F900 && utf32 <= 0x1F9FF) || // Supplemental Symbols and Pictographs (utf32 >= 0x1FA00 && utf32 <= 0x1FA6F) || // Chess Symbols (utf32 >= 0x1FA70 && utf32 <= 0x1FAFF) || // Symbols and Pictographs Extended-A (utf32 >= 0x1FB00 && utf32 <= 0x1FBFF)); // Symbols for Legacy Computing } static bool ui_edit_str_can_break(uint32_t cp1, uint32_t cp2) { return !ui_edit_str.is_zwj(cp2) && (ui_edit_str.is_cjk_or_emoji(cp1) || ui_edit_str.is_cjk_or_emoji(cp2) || ui_edit_str.is_punctuation(cp1) || ui_edit_str.is_punctuation(cp2) || ui_edit_str.is_blank(cp1) || ui_edit_str.is_blank(cp2) || ui_edit_str.is_combining(cp1) || ui_edit_str.is_combining(cp2) || ui_edit_str.is_spacing(cp1) || ui_edit_str.is_spacing(cp2)); } #pragma push_macro("ui_edit_usd") #pragma push_macro("ui_edit_gbp") #pragma push_macro("ui_edit_euro") #pragma push_macro("ui_edit_money_bag") #pragma push_macro("ui_edit_pot_of_honey") #pragma push_macro("ui_edit_gothic_hwair") #define ui_edit_usd "\x24" #define ui_edit_gbp "\xC2\xA3" #define ui_edit_euro "\xE2\x82\xAC" // https://www.compart.com/en/unicode/U+1F4B0 #define ui_edit_money_bag "\xF0\x9F\x92\xB0" // https://www.compart.com/en/unicode/U+1F36F #define ui_edit_pot_of_honey "\xF0\x9F\x8D\xAF" // https://www.compart.com/en/unicode/U+10348 #define ui_edit_gothic_hwair "\xF0\x90\x8D\x88" // Gothic Letter Hwair static void ui_edit_str_test_replace(void) { // exhaustive permutations // Exhaustive 9,765,625 replace permutations may take // up to 5 minutes of CPU time in release. // Recommended to be invoked at least once after making any // changes to ui_edit_str.replace and around. // Menu: Debug / Windows / Show Diagnostic Tools allows to watch // memory pressure for whole 3 minutes making sure code is // not leaking memory profusely. const char* gs[] = { // glyphs "", ui_edit_usd, ui_edit_gbp, ui_edit_euro, ui_edit_money_bag }; const int32_t gb[] = {0, 1, 2, 3, 4}; // number of bytes per codepoint enum { n = rt_countof(gs) }; int32_t npn = 1; // n to the power of n for (int32_t i = 0; i < n; i++) { npn *= n; } int32_t gix_src[n] = {0}; // 5^5 = 3,125 3,125 * 3,125 = 9,765,625 for (int32_t i = 0; i < npn; i++) { int32_t vi = i; for (int32_t j = 0; j < n; j++) { gix_src[j] = vi % n; vi /= n; } int32_t g2p[n + 1] = {0}; int32_t ngx = 1; // next glyph index char src[128] = {0}; for (int32_t j = 0; j < n; j++) { if (gix_src[j] > 0) { strcat(src, gs[gix_src[j]]); rt_assert(1 <= ngx && ngx <= n); g2p[ngx] = g2p[ngx - 1] + gb[gix_src[j]]; ngx++; } } if (i % 100 == 99) { rt_println("%2d%% [%d][%d][%d][%d][%d] " "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\": \"%s\"", (i * 100) / npn, gix_src[0], gix_src[1], gix_src[2], gix_src[3], gix_src[4], gs[gix_src[0]], gs[gix_src[1]], gs[gix_src[2]], gs[gix_src[3]], gs[gix_src[4]], src); } ui_edit_str_t s = {0}; // reference constructor does not copy to heap: bool ok = ui_edit_str_init(&s, src, -1, false); rt_swear(ok); for (int32_t f = 0; f <= s.g; f++) { // from for (int32_t t = f; t <= s.g; t++) { // to int32_t gix_rep[n] = {0}; // replace range [f, t] with all possible glyphs sequences: for (int32_t k = 0; k < npn; k++) { int32_t vk = i; for (int32_t j = 0; j < n; j++) { gix_rep[j] = vk % n; vk /= n; } char rep[128] = {0}; for (int32_t j = 0; j < n; j++) { strcat(rep, gs[gix_rep[j]]); } char e1[128] = {0}; // expected based on s.g2b[] snprintf(e1, rt_countof(e1), "%.*s%s%.*s", s.g2b[f], src, rep, s.b - s.g2b[t], src + s.g2b[t] ); char e2[128] = {0}; // expected based on gs[] snprintf(e2, rt_countof(e1), "%.*s%s%.*s", g2p[f], src, rep, (int32_t)strlen(src) - g2p[t], src + g2p[t] ); rt_swear(strcmp(e1, e2) == 0, "s.u[%d:%d]: \"%.*s\" g:%d [%d:%d] rep=\"%s\" " "e1: \"%s\" e2: \"%s\"", s.b, s.c, s.b, s.u, s.g, f, t, rep, e1, e2); ui_edit_str_t c = {0}; // copy ok = ui_edit_str_init(&c, src, -1, true); rt_swear(ok); ok = ui_edit_str_replace(&c, f, t, rep, -1); rt_swear(ok); rt_swear(memcmp(c.u, e1, c.b) == 0, "s.u[%d:%d]: \"%.*s\" g:%d [%d:%d] rep=\"%s\" " "expected: \"%s\"", s.b, s.c, s.b, s.u, s.g, f, t, rep, e1); ui_edit_str_free(&c); } } } ui_edit_str_free(&s); } } static void ui_edit_str_test_glyph_bytes(void) { #pragma push_macro("glyph_bytes_test") #define glyph_bytes_test(s, b, expectancy) \ rt_swear(rt_str.utf8bytes(s, b) == expectancy) // Valid Sequences glyph_bytes_test("a", 1, 1); glyph_bytes_test(ui_edit_gbp, 2, 2); glyph_bytes_test(ui_edit_euro, 3, 3); glyph_bytes_test(ui_edit_gothic_hwair, 4, 4); // Invalid Continuation Bytes glyph_bytes_test("\xC2\x00", 2, 0); glyph_bytes_test("\xE0\x80\x00", 3, 0); glyph_bytes_test("\xF0\x80\x80\x00", 4, 0); // Overlong Encodings glyph_bytes_test("\xC0\xAF", 2, 0); // '!' glyph_bytes_test("\xE0\x9F\xBF", 3, 0); // upside down '?' glyph_bytes_test("\xF0\x80\x80\xBF", 4, 0); // '~' // UTF-16 Surrogates glyph_bytes_test("\xED\xA0\x80", 3, 0); // High surrogate glyph_bytes_test("\xED\xBF\xBF", 3, 0); // Low surrogate // Code Points Outside Valid Range glyph_bytes_test("\xF4\x90\x80\x80", 4, 0); // U+110000 // Invalid Initial Bytes glyph_bytes_test("\xC0", 1, 0); glyph_bytes_test("\xC1", 1, 0); glyph_bytes_test("\xF5", 1, 0); glyph_bytes_test("\xFF", 1, 0); // 5-byte sequence (always invalid) glyph_bytes_test("\xF8\x88\x80\x80\x80", 5, 0); #pragma pop_macro("glyph_bytes_test") } static void ui_edit_str_test(void) { ui_edit_str_test_glyph_bytes(); { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "hello", -1, false); rt_swear(ok); rt_swear(s.b == 5 && s.c == 0 && memcmp(s.u, "hello", 5) == 0); rt_swear(s.g == 5 && s.g2b != null); for (int32_t i = 0; i <= s.g; i++) { rt_swear(s.g2b[i] == i); } ui_edit_str_free(&s); } const char* currencies = ui_edit_usd ui_edit_gbp ui_edit_euro ui_edit_money_bag; const char* money = currencies; { ui_edit_str_t s = {0}; const int32_t n = (int32_t)strlen(currencies); bool ok = ui_edit_str_init(&s, money, n, true); rt_swear(ok); rt_swear(s.b == n && s.c == s.b && memcmp(s.u, money, s.b) == 0); rt_swear(s.g == 4 && s.g2b != null); const int32_t g2b[] = {0, 1, 3, 6, 10}; for (int32_t i = 0; i <= s.g; i++) { rt_swear(s.g2b[i] == g2b[i]); } ui_edit_str_free(&s); } { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "hello", -1, false); rt_swear(ok); ok = ui_edit_str_replace(&s, 1, 4, null, 0); rt_swear(ok); rt_swear(s.b == 2 && memcmp(s.u, "ho", 2) == 0); rt_swear(s.g == 2 && s.g2b[0] == 0 && s.g2b[1] == 1 && s.g2b[2] == 2); ui_edit_str_free(&s); } { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "Hello world", -1, false); rt_swear(ok); ok = ui_edit_str_replace(&s, 5, 6, " cruel ", -1); rt_swear(ok); ok = ui_edit_str_replace(&s, 0, 5, "Goodbye", -1); rt_swear(ok); ok = ui_edit_str_replace(&s, s.g - 5, s.g, "Universe", -1); rt_swear(ok); rt_swear(s.g == 22 && s.g2b[0] == 0 && s.g2b[s.g] == s.b); for (int32_t i = 1; i < s.g; i++) { rt_swear(s.g2b[i] == i); // because every glyph is ASCII } rt_swear(memcmp(s.u, "Goodbye cruel Universe", 22) == 0); ui_edit_str_free(&s); } #ifdef UI_STR_TEST_REPLACE_ALL_PERMUTATIONS ui_edit_str_test_replace(); #else (void)(void*)ui_edit_str_test_replace; // mitigate unused warning #endif } #pragma push_macro("ui_edit_gothic_hwair") #pragma push_macro("ui_edit_pot_of_honey") #pragma push_macro("ui_edit_money_bag") #pragma push_macro("ui_edit_euro") #pragma push_macro("ui_edit_gbp") #pragma push_macro("ui_edit_usd") #pragma pop_macro("ui_edit_str_parameters") #pragma pop_macro("ui_edit_str_check_empty") #pragma pop_macro("ui_edit_check_zeros") #pragma pop_macro("ui_edit_str_check_from_to") #pragma pop_macro("ui_edit_str_check") #ifdef UI_EDIT_STR_TEST rt_static_init(ui_edit_str) { ui_edit_str.test(); } #endif // tests: static void ui_edit_doc_test_big_text(void) { enum { MB10 = 10 * 1000 * 1000 }; char* text = null; rt_heap.alloc(&text, MB10); memset(text, 'a', (size_t)MB10 - 1); char* p = text; uint32_t seed = 0x1; for (;;) { int32_t n = rt_num.random32(&seed) % 40 + 40; if (p + n >= text + MB10) { break; } p += n; *p = '\n'; } text[MB10 - 1] = 0x00; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, text, MB10, false); rt_swear(ok); ui_edit_text.dispose(&t); rt_heap.free(text); } static void ui_edit_doc_test_paragraphs(void) { // ui_edit_doc_to_paragraphs() is about 1 microsecond for (int i = 0; i < 100; i++) { { // empty string to paragraphs: ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, null, 0, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 1); rt_swear(t.ps[0].u[0] == 0 && t.ps[0].c == 0); rt_swear(t.ps[0].b == 0 && t.ps[0].g == 0); ui_edit_text.dispose(&t); } { // string without "\n" const char* hello = "hello"; const int32_t n = (int32_t)strlen(hello); ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, n, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 1); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == n); rt_swear(t.ps[0].g == n); ui_edit_text.dispose(&t); } { // string with "\n" at the end const char* hello = "hello\n"; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, -1, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 2); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(t.ps[1].u[0] == 0x00); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[1].b == 0); rt_swear(t.ps[1].g == 0); ui_edit_text.dispose(&t); } { // two string separated by "\n" const char* hello = "hello\nworld"; const char* world = hello + 6; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, -1, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 2); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(t.ps[1].u == world); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[1].b == 5); rt_swear(t.ps[1].g == 5); ui_edit_text.dispose(&t); } } for (int i = 0; i < 10; i++) { ui_edit_doc_test_big_text(); } } typedef struct ui_edit_doc_test_notify_s { ui_edit_notify_t notify; int32_t count_before; int32_t count_after; } ui_edit_doc_test_notify_t; static void ui_edit_doc_test_before(ui_edit_notify_t* n, const ui_edit_notify_info_t* rt_unused(ni)) { ui_edit_doc_test_notify_t* notify = (ui_edit_doc_test_notify_t*)n; notify->count_before++; } static void ui_edit_doc_test_after(ui_edit_notify_t* n, const ui_edit_notify_info_t* rt_unused(ni)) { ui_edit_doc_test_notify_t* notify = (ui_edit_doc_test_notify_t*)n; notify->count_after++; } static struct { ui_edit_notify_t notify; } ui_edit_doc_test_notify; static void ui_edit_doc_test_0(void) { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_text_t ins_text = {0}; rt_swear(ui_edit_text.init(&ins_text, "a", 1, false)); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, null, &ins_text, &undo)); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } static void ui_edit_doc_test_1(void) { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_text_t ins_text = {0}; rt_swear(ui_edit_text.init(&ins_text, "a", 1, false)); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, null, &ins_text, &undo)); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } static void ui_edit_doc_test_2(void) { { // two string separated by "\n" ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_notify_t notify1 = {0}; ui_edit_notify_t notify2 = {0}; ui_edit_doc_test_notify_t before_and_after = {0}; before_and_after.notify.before = ui_edit_doc_test_before; before_and_after.notify.after = ui_edit_doc_test_after; ui_edit_doc.subscribe(d, ¬ify1); ui_edit_doc.subscribe(d, &before_and_after.notify); ui_edit_doc.subscribe(d, ¬ify2); rt_swear(ui_edit_doc.bytes(d, null) == 0, "expected empty"); const char* hello = "hello\nworld"; rt_swear(ui_edit_doc.replace(d, null, hello, -1)); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); rt_swear(t.np == 2); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(memcmp(t.ps[0].u, "hello", 5) == 0); rt_swear(t.ps[1].b == 5); rt_swear(t.ps[1].g == 5); rt_swear(memcmp(t.ps[1].u, "world", 5) == 0); ui_edit_text.dispose(&t); ui_edit_doc.unsubscribe(d, ¬ify1); ui_edit_doc.unsubscribe(d, &before_and_after.notify); ui_edit_doc.unsubscribe(d, ¬ify2); ui_edit_doc.dispose(d); } // TODO: "GoodbyeCruelUniverse" insert 2x"\n" splitting in 3 paragraphs { // three string separated by "\n" ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "Goodbye" "\n" "Cruel" "\n" "Universe"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); ui_edit_text.dispose(&t); ui_edit_range_t r = { .from = {.pn = 0, .gp = 4}, .to = {.pn = 2, .gp = 3} }; rt_swear(ui_edit_doc.replace(d, &r, null, 0)); rt_swear(d->text.np == 1); rt_swear(d->text.ps[0].b == 9); rt_swear(d->text.ps[0].g == 9); rt_swear(memcmp(d->text.ps[0].u, "Goodverse", 9) == 0); rt_swear(ui_edit_doc.replace(d, null, null, 0)); // remove all rt_swear(d->text.np == 1); rt_swear(d->text.ps[0].b == 0); rt_swear(d->text.ps[0].g == 0); ui_edit_doc.dispose(d); } // TODO: "GoodbyeCruelUniverse" insert 2x"\n" splitting in 3 paragraphs { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; const char* ins[] = { "X\nY", "X\n", "\nY", "\n", "X\nY\nZ" }; for (int32_t i = 0; i < rt_countof(ins); i++) { rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "GoodbyeCruelUniverse"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); ui_edit_range_t r = { .from = {.pn = 0, .gp = 7}, .to = {.pn = 0, .gp = 12} }; ui_edit_text_t ins_text = {0}; ui_edit_text.init(&ins_text, ins[i], -1, false); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, &r, &ins_text, &undo)); ui_edit_to_do_t redo = {0}; rt_swear(ui_edit_text.replace(&d->text, &undo.range, &undo.text, &redo)); ui_edit_doc.dispose_to_do(&undo); undo.range = (ui_edit_range_t){0}; rt_swear(ui_edit_text.replace(&d->text, &redo.range, &redo.text, &undo)); ui_edit_doc.dispose_to_do(&redo); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } } } static void ui_edit_doc_test_3(void) { { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; ui_edit_doc_test_notify_t before_and_after = {0}; before_and_after.notify.before = ui_edit_doc_test_before; before_and_after.notify.after = ui_edit_doc_test_after; rt_swear(ui_edit_doc.init(d, null, 0, false)); rt_swear(ui_edit_doc.subscribe(d, &before_and_after.notify)); const char* s = "Goodbye Cruel Universe"; const int32_t before = before_and_after.count_before; const int32_t after = before_and_after.count_after; rt_swear(ui_edit_doc.replace(d, null, s, -1)); const int32_t bytes = (int32_t)strlen(s); rt_swear(before + 1 == before_and_after.count_before); rt_swear(after + 1 == before_and_after.count_after); rt_swear(d->text.np == 1); rt_swear(ui_edit_doc.bytes(d, null) == bytes); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); rt_swear(t.np == 1); rt_swear(t.ps[0].b == bytes); rt_swear(t.ps[0].g == bytes); rt_swear(memcmp(t.ps[0].u, s, t.ps[0].b) == 0); // with "\n" and 0x00 at the end: int32_t utf8bytes = ui_edit_doc.utf8bytes(d, null); char* p = null; rt_swear(rt_heap.alloc((void**)&p, utf8bytes) == 0); p[utf8bytes - 1] = 0xFF; ui_edit_doc.copy(d, null, p, utf8bytes); rt_swear(p[utf8bytes - 1] == 0x00); rt_swear(memcmp(p, s, bytes) == 0); rt_heap.free(p); ui_edit_text.dispose(&t); ui_edit_doc.unsubscribe(d, &before_and_after.notify); ui_edit_doc.dispose(d); } { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "Hello World" "\n" "Goodbye Cruel Universe"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); rt_swear(ui_edit_doc.undo(d)); rt_swear(ui_edit_doc.bytes(d, null) == 0); rt_swear(ui_edit_doc.utf8bytes(d, null) == 1); rt_swear(ui_edit_doc.redo(d)); { int32_t utf8bytes = ui_edit_doc.utf8bytes(d, null); char* p = null; rt_swear(rt_heap.alloc((void**)&p, utf8bytes) == 0); p[utf8bytes - 1] = 0xFF; ui_edit_doc.copy(d, null, p, utf8bytes); rt_swear(p[utf8bytes - 1] == 0x00); rt_swear(memcmp(p, s, utf8bytes) == 0); rt_heap.free(p); } ui_edit_doc.dispose(d); } } static void ui_edit_doc_test_4(void) { { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_range_t r = {0}; r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "a", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "b", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "c", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); ui_edit_doc.dispose(d); } } static void ui_edit_doc_test(void) { { ui_edit_range_t r = { .from = {0,0}, .to = {0,0} }; rt_static_assertion(sizeof(r.from) + sizeof(r.from) == sizeof(r.a)); rt_swear(&r.from == &r.a[0] && &r.to == &r.a[1]); } #ifdef UI_EDIT_DOC_TEST_PARAGRAPHS ui_edit_doc_test_paragraphs(); #else (void)(void*)ui_edit_doc_test_paragraphs; // unused #endif // use n = 10,000,000 and Diagnostic Tools to watch for memory leaks enum { n = 1000 }; // enum { n = 10 * 1000 * 1000 }; for (int32_t i = 0; i < n; i++) { ui_edit_doc_test_0(); ui_edit_doc_test_1(); ui_edit_doc_test_2(); ui_edit_doc_test_3(); ui_edit_doc_test_4(); } } static const ui_edit_range_t ui_edit_invalid_range = { .from = { .pn = -1, .gp = -1}, .to = { .pn = -1, .gp = -1} }; ui_edit_range_if ui_edit_range = { .compare = ui_edit_range_compare, .order = ui_edit_range_order, .is_valid = ui_edit_range_is_valid, .is_empty = ui_edit_range_is_empty, .uint64 = ui_edit_range_uint64, .pg = ui_edit_range_pg, .inside = ui_edit_range_inside_text, .intersect = ui_edit_range_intersect, .invalid_range = &ui_edit_invalid_range }; ui_edit_text_if ui_edit_text = { .init = ui_edit_text_init, .bytes = ui_edit_text_bytes, .all_on_null = ui_edit_text_all_on_null, .ordered = ui_edit_text_ordered, .end = ui_edit_text_end, .end_range = ui_edit_text_end_range, .dup = ui_edit_text_dup, .equal = ui_edit_text_equal, .copy_text = ui_edit_text_copy_text, .copy = ui_edit_text_copy, .replace = ui_edit_text_replace, .replace_utf8 = ui_edit_text_replace_utf8, .dispose = ui_edit_text_dispose }; ui_edit_doc_if ui_edit_doc = { .init = ui_edit_doc_init, .replace = ui_edit_doc_replace, .bytes = ui_edit_doc_bytes, .copy_text = ui_edit_doc_copy_text, .utf8bytes = ui_edit_doc_utf8bytes, .copy = ui_edit_doc_copy, .redo = ui_edit_doc_redo, .undo = ui_edit_doc_undo, .subscribe = ui_edit_doc_subscribe, .unsubscribe = ui_edit_doc_unsubscribe, .dispose_to_do = ui_edit_doc_dispose_to_do, .dispose = ui_edit_doc_dispose, .test = ui_edit_doc_test }; #pragma push_macro("ui_edit_doc_dump") #pragma push_macro("ui_edit_text_dump") #pragma push_macro("ui_edit_range_dump") #pragma push_macro("ui_edit_pg_dump") #pragma push_macro("ui_edit_check_range_inside_text") #pragma push_macro("ui_edit_check_pg_inside_text") #pragma push_macro("ui_edit_check_zeros") #ifdef UI_EDIT_DOC_TEST rt_static_init(ui_edit_doc) { ui_edit_doc.test(); } #endif // ______________________________ ui_edit_view.c ______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" // TODO: find all "== dt->np" it is wrong pn < dt->np fix them all // TODO: undo/redo coalescing // TODO: back/forward navigation // TODO: exit (Ctrl+W?)/save(Ctrl+S, Ctrl+Shift+S) keyboard shortcuts? // TODO: ctrl left, ctrl right jump word ctrl+shift left/right select word? // TODO: iBeam cursor (definitely yes - see how MSVC does it) // TODO: vertical scrollbar ui // TODO: horizontal scroll: trivial to implement: // add horizontal_scroll to e->w and paint // paragraphs in a horizontally shifted clip // http://worrydream.com/refs/Tesler%20-%20A%20Personal%20History%20of%20Modeless%20Text%20Editing%20and%20Cut-Copy-Paste.pdf // https://web.archive.org/web/20221216044359/http://worrydream.com/refs/Tesler%20-%20A%20Personal%20History%20of%20Modeless%20Text%20Editing%20and%20Cut-Copy-Paste.pdf // Rich text options that are not addressed yet: // * Color of ranges (useful for code editing) // * Soft line breaks inside the paragraph (useful for e.g. bullet lists of options) // * Bold/Italic/Underline (along with color ranges) // * Multiple fonts (as long as run vertical size is the maximum of font) // * Kerning (?! like in overhung "Fl") // When implementation and header are amalgamated // into a single file header library name_space is // used to separate different modules namespaces. typedef struct ui_edit_glyph_s { const char* s; int32_t bytes; } ui_edit_glyph_t; static void ui_edit_layout(ui_view_t* v); static ui_point_t ui_edit_pg_to_xy(ui_edit_view_t* e, const ui_edit_pg_t pg); // Glyphs in monospaced Windows fonts may have different width for non-ASCII // characters. Thus even if edit is monospaced glyph measurements are used // in text layout. static void ui_edit_invalidate_parent(const ui_edit_view_t* e, const ui_rect_t* rc) { // For transparent background of edit_view parent must draw background. // In the current implementation invalidate() causes whole stack redraw // in rectangle thus it does not matter much. But if it is ever optimized // it will matter. ui_color_t b = e->background; if (ui_color_is_undefined(b) || ui_color_is_transparent(b)) { ui_view.invalidate(e->parent, rc); } } static void ui_edit_invalidate_rect(const ui_edit_view_t* e, const ui_rect_t rc) { rt_assert(rc.w >= 0 && rc.h > 0); // w may be zero for empty selection if (rc.w > 0 && rc.h > 0) { ui_view.invalidate(&e->view, &rc); ui_edit_invalidate_parent(e, &rc); } } static void ui_edit_invalidate_view(const ui_edit_view_t* e) { ui_view.invalidate(&e->view, null); ui_edit_invalidate_parent(e, null); } static int32_t ui_edit_line_height(ui_edit_view_t* e) { // at 96dpi: // "Segoe UI" height + line_gap: 16 // ui_app.fm.prop h: 15 pt: 11.250 a: 3 c: 9 d: 3 bl: 12 il: 3 lg: 2 // "Cascadia Mono" height + line_gap: 17 // ui_app.fm.mono h: 16 pt: 12.000 a: 2 c: 11 d: 3 bl: 13 il: 4 lg: 0 return e->fm->height + e->fm->line_gap; } static ui_rect_t ui_edit_selection_rect(ui_edit_view_t* e) { const ui_edit_range_t r = ui_edit_range.order(e->selection); const ui_ltrb_t i = ui_view.margins(&e->view, &e->insets); const ui_point_t p0 = ui_edit_pg_to_xy(e, r.from); const ui_point_t p1 = ui_edit_pg_to_xy(e, r.to); if (p0.x < 0 || p1.x < 0) { // selection outside of visible area return (ui_rect_t) { .x = 0, .y = 0, .w = e->w, .h = e->h }; } else if (p0.y == p1.y) { const int32_t max_w = rt_max(e->fm->max_char_width, e->fm->em.w); int32_t w = p1.x - p0.x != 0 ? p1.x - p0.x + max_w : e->caret_width; return (ui_rect_t) { .x = p0.x, .y = i.top + p0.y, .w = w, .h = ui_edit_line_height(e) }; } else { const int32_t h = p1.y - p0.y + ui_edit_line_height(e); return (ui_rect_t) { .x = 0, .y = i.top + p0.y, .w = e->w, .h = h }; } } #if 0 static void ui_edit_text_width_gp(ui_edit_view_t* e, const char* utf8, int32_t bytes) { const int32_t glyphs = rt_str.glyphs(utf8, bytes); rt_println("\"%.*s\" bytes:%d glyphs:%d", bytes, utf8, bytes, glyphs); int32_t* x = (int32_t*)rt_stackalloc((glyphs + 1) * sizeof(int32_t)); const ui_gdi_ta_t ta = { .fm = e->fm }; ui_wh_t wh = ui_gdi.glyphs_placement(&ta, utf8, bytes, x, glyphs); // rt_println("wh: %dx%d", wh.w, wh.h); } #endif static int32_t ui_edit_text_width(ui_edit_view_t* e, const char* s, int32_t n) { // fp64_t time = rt_clock.seconds(); // average GDI measure_text() performance per character: // "ui_app.fm.mono" ~500us (microseconds) // "ui_app.fm.prop.normal" ~250us (microseconds) DirectWrite ~100us const ui_gdi_ta_t ta = { .fm = e->fm, .color = e->color, .measure = true }; int32_t x = n == 0 ? 0 : ui_gdi.text(&ta, 0, 0, "%.*s", n, s).w; // time = (rt_clock.seconds() - time) * 1000.0; // static fp64_t time_sum; // static fp64_t length_sum; // time_sum += time; // length_sum += n; // rt_println("avg=%.6fms per char total %.3fms", time_sum / length_sum, time_sum); return x; } static int32_t ui_edit_word_break_at(ui_edit_view_t* e, int32_t pn, int32_t rn, const int32_t width, bool allow_zero) { // TODO: in sqlite.c 257,674 lines it takes 11 seconds to get all runs() // on average ui_edit_word_break_at() takes 4 x ui_edit_text_width() // measurements and they are slow. If we can reduce this amount // (not clear how) at least 2 times it will be a win. // Another way is background thread runs() processing but this is // involving a lot of complexity. // MSVC devenv.exe edits sqlite3.c w/o any visible delays int32_t count = 0; // stats logging int32_t chars = 0; ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); ui_edit_paragraph_t* p = &e->para[pn]; const ui_edit_str_t* str = &dt->ps[pn]; int32_t k = 1; // at least 1 glyph // offsets inside a run in glyphs and bytes from start of the paragraph: int32_t gp = p->run[rn].gp; int32_t bp = p->run[rn].bp; if (gp < str->g - 1) { const char* text = str->u + bp; const int32_t glyphs_in_this_run = str->g - gp; int32_t* g2b = &str->g2b[gp]; // 4 is maximum number of bytes in a UTF-8 sequence int32_t gc = rt_min(4, glyphs_in_this_run); int32_t w = ui_edit_text_width(e, text, g2b[gc] - bp); count++; chars += g2b[gc] - bp; while (gc < glyphs_in_this_run && w < width) { gc = rt_min(gc * 4, glyphs_in_this_run); w = ui_edit_text_width(e, text, g2b[gc] - bp); count++; chars += g2b[gc] - bp; } if (w < width) { k = gc; rt_assert(1 <= k && k <= str->g - gp); } else { int32_t i = 0; int32_t j = gc; k = (i + j) / 2; while (i < j) { rt_assert(allow_zero || 1 <= k && k < gc + 1); const int32_t n = g2b[k + 1] - bp; int32_t px = ui_edit_text_width(e, text, n); count++; chars += n; if (px == width) { break; } if (px < width) { i = k + 1; } else { j = k; } if (!allow_zero && (i + j) / 2 == 0) { break; } k = (i + j) / 2; rt_assert(allow_zero || 1 <= k && k <= str->g - gp); } } } rt_assert(allow_zero || 1 <= k && k <= str->g - gp); return k; } static int32_t ui_edit_word_break(ui_edit_view_t* e, int32_t pn, int32_t rn) { return ui_edit_word_break_at(e, pn, rn, e->edit.w, false); } static int32_t ui_edit_glyph_at_x(ui_edit_view_t* e, int32_t pn, int32_t rn, int32_t x) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); if (x == 0 || dt->ps[pn].b == 0) { return 0; } else { return ui_edit_word_break_at(e, pn, rn, x + 1, true); } } static ui_edit_glyph_t ui_edit_glyph_at(ui_edit_view_t* e, ui_edit_pg_t p) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_glyph_t g = { .s = "", .bytes = 0 }; rt_assert(0 <= p.pn && p.pn < dt->np); const ui_edit_str_t* str = &dt->ps[p.pn]; const int32_t bytes = str->b; const char* s = str->u; const int32_t bp = str->g2b[p.gp]; if (bp < bytes) { g.s = s + bp; g.bytes = rt_str.utf8bytes(g.s, bytes - bp); rt_swear(g.bytes > 0); } return g; } // paragraph_runs() breaks paragraph into `runs` according to `width` static const ui_edit_run_t* ui_edit_paragraph_runs(ui_edit_view_t* e, int32_t pn, int32_t* runs) { // fp64_t time = rt_clock.seconds(); rt_assert(e->w > 0); ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); const ui_edit_run_t* r = null; if (e->para[pn].run != null) { *runs = e->para[pn].runs; r = e->para[pn].run; } else { rt_assert(0 <= pn && pn < dt->np); ui_edit_paragraph_t* p = &e->para[pn]; const ui_edit_str_t* str = &dt->ps[pn]; if (p->run == null) { rt_assert(p->runs == 0 && p->run == null); const int32_t max_runs = str->b + 1; bool ok = rt_heap.alloc((void**)&p->run, max_runs * sizeof(ui_edit_run_t)) == 0; rt_swear(ok); ui_edit_run_t* run = p->run; run[0].bp = 0; run[0].gp = 0; int32_t gc = str->b == 0 ? 0 : ui_edit_word_break(e, pn, 0); if (gc == str->g) { // whole paragraph fits into width p->runs = 1; run[0].bytes = str->b; run[0].glyphs = str->g; int32_t pixels = ui_edit_text_width(e, str->u, str->g2b[gc]); run[0].pixels = pixels; } else { rt_assert(gc < str->g); int32_t rc = 0; // runs count int32_t ix = 0; // glyph index from to start of paragraph const char* text = str->u; int32_t bytes = str->b; while (bytes > 0) { rt_assert(rc < max_runs); run[rc].bp = (int32_t)(text - str->u); run[rc].gp = ix; int32_t glyphs = ui_edit_word_break(e, pn, rc); int32_t utf8bytes = str->g2b[ix + glyphs] - run[rc].bp; int32_t pixels = ui_edit_text_width(e, text, utf8bytes); if (glyphs > 1 && utf8bytes < bytes && text[utf8bytes - 1] != 0x20) { // try to find word break SPACE character. utf8 space is 0x20 int32_t i = utf8bytes; while (i > 0 && text[i - 1] != 0x20) { i--; } if (i > 0 && i != utf8bytes) { utf8bytes = i; glyphs = rt_str.glyphs(text, utf8bytes); rt_assert(glyphs >= 0); pixels = ui_edit_text_width(e, text, utf8bytes); } } run[rc].bytes = utf8bytes; run[rc].glyphs = glyphs; run[rc].pixels = pixels; rc++; text += utf8bytes; rt_assert(0 <= utf8bytes && utf8bytes <= bytes); bytes -= utf8bytes; ix += glyphs; } rt_assert(rc > 0); p->runs = rc; // truncate heap capacity array: ok = rt_heap.realloc((void**)&p->run, rc * sizeof(ui_edit_run_t)) == 0; rt_swear(ok); } } *runs = p->runs; r = p->run; } rt_assert(r != null && *runs >= 1); return r; } static int32_t ui_edit_paragraph_run_count(ui_edit_view_t* e, int32_t pn) { rt_swear(e->w > 0); ui_edit_text_t* dt = &e->doc->text; // document text int32_t runs = 0; if (e->w > 0 && 0 <= pn && pn < dt->np) { (void)ui_edit_paragraph_runs(e, pn, &runs); } return runs; } static int32_t ui_edit_glyphs_in_paragraph(ui_edit_view_t* e, int32_t pn) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); (void)ui_edit_paragraph_run_count(e, pn); // word break into runs return dt->ps[pn].g; } static void ui_edit_create_caret(ui_edit_view_t* e) { rt_fatal_if(e->focused); rt_assert(ui_app.is_active()); rt_assert(ui_app.focused()); fp64_t px = ui_app.dpi.monitor_raw / 100.0 + 0.5; e->caret_width = rt_min(3, rt_max(1, (int32_t)px)); ui_app.create_caret(e->caret_width, e->fm->height); // w/o line_gap e->focused = true; // means caret was created // rt_println("e->focused := true %s", ui_view_debug_id(&e->view)); } static void ui_edit_destroy_caret(ui_edit_view_t* e) { rt_fatal_if(!e->focused); ui_app.destroy_caret(); e->focused = false; // means caret was destroyed // rt_println("e->focused := false %s", ui_view_debug_id(&e->view)); } static void ui_edit_show_caret(ui_edit_view_t* e) { if (e->focused) { rt_assert(ui_app.is_active()); rt_assert(ui_app.focused()); rt_assert((e->caret.x < 0) == (e->caret.y < 0)); const ui_ltrb_t insets = ui_view.margins(&e->view, &e->insets); int32_t x = e->caret.x < 0 ? insets.left : e->caret.x; int32_t y = e->caret.y < 0 ? insets.top : e->caret.y; ui_app.move_caret(e->x + x, e->y + y); // TODO: it is possible to support unblinking caret if desired // do not set blink time - use global default // fatal_if_false(SetCaretBlinkTime(500)); ui_app.show_caret(); e->shown++; rt_assert(e->shown == 1); } } static void ui_edit_hide_caret(ui_edit_view_t* e) { if (e->focused) { ui_app.hide_caret(); e->shown--; rt_assert(e->shown == 0); } } static void ui_edit_allocate_runs(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(e->para == null); rt_assert(dt->np > 0); rt_assert(e->para == null); bool done = rt_heap.alloc_zero((void**)&e->para, dt->np * sizeof(e->para[0])) == 0; rt_swear(done, "out of memory - cannot continue"); } static void ui_edit_invalidate_run(ui_edit_view_t* e, int32_t i) { if (e->para[i].run != null) { rt_assert(e->para[i].runs > 0); rt_heap.free(e->para[i].run); e->para[i].run = null; e->para[i].runs = 0; } else { rt_assert(e->para[i].runs == 0); } } static void ui_edit_invalidate_runs(ui_edit_view_t* e, int32_t f, int32_t t, int32_t np) { // [from..to] inclusive inside [0..np - 1] rt_swear(e->para != null && f <= t && 0 <= f && t < np); for (int32_t i = f; i <= t; i++) { ui_edit_invalidate_run(e, i); } } static void ui_edit_invalidate_all_runs(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_invalidate_runs(e, 0, dt->np - 1, dt->np); } static void ui_edit_dispose_runs(ui_edit_view_t* e, int32_t np) { rt_assert(e->para != null); ui_edit_invalidate_runs(e, 0, np - 1, np); rt_heap.free(e->para); e->para = null; } static void ui_edit_dispose_all_runs(ui_edit_view_t* e) { ui_edit_dispose_runs(e, e->doc->text.np); } static void ui_edit_layout_now(ui_edit_view_t* e) { if (e->measure != null && e->layout != null && e->w > 0) { e->layout(&e->view); ui_edit_invalidate_view(e); } } static void ui_edit_if_sle_layout(ui_edit_view_t* e) { // only for single line edit controls that were already initialized // and measured horizontally at least once. if (e->sle && e->layout != null && e->w > 0) { ui_edit_layout_now(e); } } static void ui_edit_view_set_font(ui_edit_view_t* e, ui_fm_t* f) { ui_edit_invalidate_all_runs(e); e->scroll.rn = 0; e->fm = f; ui_edit_layout_now(e); ui_app.request_layout(); } // Paragraph number, glyph number -> run number static ui_edit_pr_t ui_edit_pg_to_pr(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); const ui_edit_str_t* str = &dt->ps[pg.pn]; ui_edit_pr_t pr = { .pn = pg.pn, .rn = -1 }; if (str->b == 0) { // empty rt_assert(pg.gp == 0); pr.rn = 0; } else { rt_assert(0 <= pg.pn && pg.pn < dt->np); int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pg.pn, &runs); if (pg.gp == str->g + 1) { pr.rn = runs - 1; // TODO: past last glyph ??? is this correct? } else { rt_assert(0 <= pg.gp && pg.gp <= str->g); for (int32_t j = 0; j < runs && pr.rn < 0; j++) { const int32_t last_run = j == runs - 1; const int32_t start = run[j].gp; const int32_t end = run[j].gp + run[j].glyphs + last_run; if (start <= pg.gp && pg.gp < end) { pr.rn = j; } } rt_assert(pr.rn >= 0); } } return pr; } static int32_t ui_edit_runs_between(ui_edit_view_t* e, const ui_edit_pg_t pg0, const ui_edit_pg_t pg1) { rt_assert(ui_edit_range.uint64(pg0) <= ui_edit_range.uint64(pg1)); int32_t rn0 = ui_edit_pg_to_pr(e, pg0).rn; int32_t rn1 = ui_edit_pg_to_pr(e, pg1).rn; int32_t rc = 0; if (pg0.pn == pg1.pn) { rt_assert(rn0 <= rn1); rc = rn1 - rn0; } else { rt_assert(pg0.pn < pg1.pn); for (int32_t i = pg0.pn; i < pg1.pn; i++) { const int32_t runs = ui_edit_paragraph_run_count(e, i); if (i == pg0.pn) { rc += runs - rn0; } else { // i < pg1.pn rc += runs; } } rc += rn1; } return rc; } static ui_edit_pg_t ui_edit_scroll_pg(ui_edit_view_t* e) { int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, e->scroll.pn, &runs); // layout may decrease number of runs when view is growing: if (e->scroll.rn >= runs) { e->scroll.rn = runs - 1; } rt_assert(0 <= e->scroll.rn && e->scroll.rn < runs, "e->scroll.rn: %d runs: %d", e->scroll.rn, runs); return (ui_edit_pg_t) { .pn = e->scroll.pn, .gp = run[e->scroll.rn].gp }; } static int32_t ui_edit_first_visible_run(ui_edit_view_t* e, int32_t pn) { return pn == e->scroll.pn ? e->scroll.rn : 0; } // ui_edit::pg_to_xy() paragraph # glyph # -> (x,y) in [0,0 width x height] static ui_point_t ui_edit_pg_to_xy(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); ui_point_t pt = { .x = -1, .y = 0 }; const int32_t spn = e->scroll.pn + 1; const int32_t pn = rt_min(rt_max(spn, pg.pn + 1), dt->np - 1); for (int32_t i = e->scroll.pn; i <= pn && pt.x < 0; i++) { rt_assert(0 <= i && i < dt->np); const ui_edit_str_t* str = &dt->ps[i]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, i, &runs); for (int32_t j = ui_edit_first_visible_run(e, i); j < runs; j++) { const int32_t last_run = j == runs - 1; const int32_t gc = run[j].glyphs; // glyphs count if (i == pg.pn) { // in the last `run` of a paragraph x after last glyph is OK if (run[j].gp <= pg.gp && pg.gp < run[j].gp + gc + last_run) { const char* s = str->u + run[j].bp; const uint32_t bp2e = str->b - run[j].bp; // to end of str int32_t ofs = ui_edit_str.gp_to_bp(s, bp2e, pg.gp - run[j].gp); rt_swear(ofs >= 0); pt.x = ui_edit_text_width(e, s, ofs); break; } } pt.y += ui_edit_line_height(e); } } if (0 <= pt.x && pt.x < e->edit.w && 0 <= pt.y && pt.y < e->edit.h) { // all good, inside visible rectangle or right after it } else { rt_println("%d:%d (%d,%d) outside of %dx%d", pg.pn, pg.gp, pt.x, pt.y, e->edit.w, e->edit.h); pt = (ui_point_t){-1, -1}; } return pt; } static int32_t ui_edit_glyph_width_px(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); const ui_edit_str_t* str = &dt->ps[pg.pn]; const char* text = str->u; int32_t gc = str->g; if (pg.gp == 0 && gc == 0) { return 0; // empty paragraph } else if (pg.gp < gc) { const int32_t bp = ui_edit_str.gp_to_bp(text, str->b, pg.gp); rt_swear(bp >= 0); const char* s = text + bp; int32_t bytes_in_glyph = rt_str.utf8bytes(s, str->b - bp); rt_swear(bytes_in_glyph > 0); int32_t x = ui_edit_text_width(e, s, bytes_in_glyph); return x; } else { rt_assert(pg.gp == gc, "only next position past last glyph is allowed"); return 0; } } // xy_to_pg() (x,y) (0,0, width x height) -> paragraph # glyph # static ui_edit_pg_t ui_edit_xy_to_pg(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = {-1, -1}; int32_t py = 0; // paragraph `y' coordinate for (int32_t i = e->scroll.pn; i < dt->np && pg.pn < 0; i++) { rt_assert(0 <= i && i < dt->np); const ui_edit_str_t* str = &dt->ps[i]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, i, &runs); for (int32_t j = ui_edit_first_visible_run(e, i); j < runs && pg.pn < 0; j++) { const ui_edit_run_t* r = &run[j]; const char* s = str->u + run[j].bp; if (py <= y && y < py + ui_edit_line_height(e)) { int32_t w = ui_edit_text_width(e, s, r->bytes); pg.pn = i; if (x >= w) { pg.gp = r->gp + r->glyphs; } else { pg.gp = r->gp + ui_edit_glyph_at_x(e, i, j, x); if (pg.gp < r->glyphs - 1) { ui_edit_pg_t right = {pg.pn, pg.gp + 1}; int32_t x0 = ui_edit_pg_to_xy(e, pg).x; int32_t x1 = ui_edit_pg_to_xy(e, right).x; if (x1 - x < x - x0) { pg.gp++; // snap to closest glyph's 'x' } } } } else { py += ui_edit_line_height(e); } } if (py > e->h) { break; } } return pg; } static void ui_edit_set_caret(ui_edit_view_t* e, int32_t x, int32_t y) { if (e->caret.x != x || e->caret.y != y) { if (e->focused && ui_app.focused()) { ui_app.move_caret(e->x + x, e->y + y); } const ui_ltrb_t i = ui_view.margins(&e->view, &e->insets); // caret in i.left .. e->view.w - i.right // i.top .. e->view.h - i.bottom // coordinate space rt_swear(i.left <= x && x < e->w && i.top <= y && y < e->h); e->caret.x = x; e->caret.y = y; } } static ui_edit_pg_t ui_edit_view_end_of_text(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text return (ui_edit_pg_t){ .pn = dt->np - 1, .gp = dt->ps[dt->np - 1].g }; } static ui_edit_pg_t ui_edit_view_last_fully_visible(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = ui_edit_scroll_pg(e); int32_t visible_runs = e->visible_runs; while (visible_runs > 0) { int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pg.pn, &runs); int32_t i = 0; pg.gp = 0; while (visible_runs > 0 && i < runs) { pg.gp += run[i].glyphs; visible_runs--; i++; } if (visible_runs > 0) { if (pg.pn < dt->np - 1) { pg.pn++; pg.gp = 0; } else { visible_runs = 0; // reached end of text } } } return pg; } // scroll_up() text moves up (north) in the visible view, // scroll position increments moves down (south) static void ui_edit_scroll_up(ui_edit_view_t* e, int32_t run_count) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 < run_count, "does it make sense to have 0 scroll?"); ui_edit_pg_t eot = ui_edit_view_end_of_text(e); while (run_count > 0) { ui_edit_pg_t lfv = ui_edit_view_last_fully_visible(e); rt_println("eot: %d:%d lfv: %d:%d", eot.pn, eot.gp, lfv.pn, lfv.gp); if (ui_edit_range.compare(lfv, eot) == 0) { run_count = 0; } else { const int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); if (e->scroll.rn < runs - 1) { e->scroll.rn++; run_count--; } else if (e->scroll.pn < dt->np - 1) { e->scroll.pn++; e->scroll.rn = 0; run_count--; } else { rt_println("???"); run_count = 0; // enough } rt_assert(e->scroll.pn >= 0 && e->scroll.rn >= 0); } } ui_edit_if_sle_layout(e); ui_edit_invalidate_view(e); } // scroll_dw() text moves down (south) in the visible view, // scroll position decrements moves up (north) static void ui_edit_scroll_down(ui_edit_view_t* e, int32_t run_count) { rt_assert(0 < run_count, "does it make sense to have 0 scroll?"); while (run_count > 0 && (e->scroll.pn > 0 || e->scroll.rn > 0)) { int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); e->scroll.rn = rt_min(e->scroll.rn, runs - 1); if (e->scroll.rn == 0 && e->scroll.pn > 0) { e->scroll.pn--; e->scroll.rn = ui_edit_paragraph_run_count(e, e->scroll.pn) - 1; } else if (e->scroll.rn > 0) { e->scroll.rn--; } rt_assert(e->scroll.pn >= 0 && e->scroll.rn >= 0); rt_assert(0 <= e->scroll.rn && e->scroll.rn < ui_edit_paragraph_run_count(e, e->scroll.pn)); run_count--; } ui_edit_if_sle_layout(e); ui_edit_invalidate_view(e); } static void ui_edit_scroll_into_view(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np && dt->np > 0); if (e->inside.bottom > 0) { if (e->sle) { rt_assert(pg.pn == 0); } const int32_t rn = ui_edit_pg_to_pr(e, pg).rn; const uint64_t scroll = (uint64_t)e->scroll.pn << 32 | e->scroll.rn; const uint64_t caret = (uint64_t)pg.pn << 32 | rn; uint64_t last = 0; int32_t py = 0; const int32_t pn = e->scroll.pn; const int32_t bottom = e->inside.bottom; for (int32_t i = pn; i < dt->np && py < bottom; i++) { int32_t runs = ui_edit_paragraph_run_count(e, i); const int32_t fvr = ui_edit_first_visible_run(e, i); for (int32_t j = fvr; j < runs && py < bottom; j++) { last = (uint64_t)i << 32 | j; py += ui_edit_line_height(e); } } int32_t sle_runs = e->sle && e->w > 0 ? ui_edit_paragraph_run_count(e, 0) : 0; ui_edit_pg_t end = ui_edit_text.end(dt); ui_edit_pr_t lp = ui_edit_pg_to_pr(e, end); uint64_t eof = (uint64_t)(dt->np - 1) << 32 | lp.rn; if (last == eof && py <= bottom - ui_edit_line_height(e)) { // vertical white space for EOF on the screen last = (uint64_t)dt->np << 32 | 0; } if (scroll <= caret && caret < last) { // no scroll } else if (caret < scroll) { ui_edit_invalidate_view(e); e->scroll.pn = pg.pn; e->scroll.rn = rn; } else if (e->sle && sle_runs * ui_edit_line_height(e) <= e->h) { // single line edit control fits vertically - no scroll } else { ui_edit_invalidate_view(e); rt_assert(caret >= last); e->scroll.pn = pg.pn; e->scroll.rn = rn; while (e->scroll.pn > 0 || e->scroll.rn > 0) { ui_point_t pt = ui_edit_pg_to_xy(e, pg); if (pt.y + ui_edit_line_height(e) > bottom - ui_edit_line_height(e)) { break; } if (e->scroll.rn > 0) { e->scroll.rn--; } else { e->scroll.pn--; e->scroll.rn = ui_edit_paragraph_run_count(e, e->scroll.pn) - 1; } } } } } static void ui_edit_caret_to(ui_edit_view_t* e, const ui_edit_pg_t to) { ui_edit_scroll_into_view(e, to); ui_point_t pt = ui_edit_pg_to_xy(e, to); if (pt.x >= 0 && pt.y >= 0) { ui_edit_set_caret(e, pt.x + e->inside.left, pt.y + e->inside.top); } } static void ui_edit_move_caret(ui_edit_view_t* e, const ui_edit_pg_t pg) { if (e->w > 0) { // width == 0 means no measure/layout yet ui_rect_t before = ui_edit_selection_rect(e); ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); // single line edit control cannot move caret past fist paragraph if (!e->sle || pg.pn < dt->np) { e->selection.a[1] = pg; ui_edit_caret_to(e, pg); if (!ui_app.shift && e->edit.buttons == 0) { e->selection.a[0] = e->selection.a[1]; } } ui_rect_t after = ui_edit_selection_rect(e); ui_edit_invalidate_rect(e, ui.combine_rect(&before, &after)); } } static ui_edit_pg_t ui_edit_insert_inline(ui_edit_view_t* e, ui_edit_pg_t pg, const char* text, int32_t bytes) { // insert_inline() inserts text (not containing '\n' in it) rt_assert(bytes > 0); for (int32_t i = 0; i < bytes; i++) { rt_assert(text[i] != '\n'); } ui_edit_range_t r = { .from = pg, .to = pg }; int32_t g = 0; if (ui_edit_doc.replace(e->doc, &r, text, bytes)) { ui_edit_text_t t = {0}; if (ui_edit_text.init(&t, text, bytes, false)) { rt_assert(t.ps != null && t.np == 1); g = t.np == 1 && t.ps != null ? t.ps[0].g : 0; ui_edit_text.dispose(&t); } } r.from.gp += g; r.to.gp += g; e->selection = r; ui_edit_move_caret(e, e->selection.from); return r.to; } static ui_edit_pg_t ui_edit_insert_paragraph_break(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_range_t r = { .from = pg, .to = pg }; bool ok = ui_edit_doc.replace(e->doc, &r, "\n", 1); ui_edit_pg_t next = {.pn = pg.pn + 1, .gp = 0}; return ok ? next : pg; } static bool ui_edit_is_blank(ui_edit_glyph_t g) { return g.bytes == 0 || ui_edit_str.is_blank(rt_str.utf32(g.s, g.bytes)); } static bool ui_edit_is_punctuation(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && ui_edit_str.is_punctuation(utf32); } static bool ui_edit_is_alphanumeric(ui_edit_glyph_t g) { return g.bytes > 0 && ui_edit_str.is_alphanumeric(rt_str.utf32(g.s, g.bytes)); } static bool ui_edit_is_cjk_or_emoji_or_symbol(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && (ui_edit_str.is_cjk_or_emoji(utf32) || ui_edit_str.is_symbol(utf32)); } static bool ui_edit_is_break(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && (ui_edit_str.is_blank(utf32) || ui_edit_str.is_punctuation(utf32) || ui_edit_str.is_symbol(utf32) || ui_edit_str.is_cjk_or_emoji(utf32)); } static ui_edit_glyph_t ui_edit_left_of(ui_edit_view_t* e, ui_edit_pg_t pg) { if (pg.gp > 0) { pg.gp--; return ui_edit_glyph_at(e, pg); } else { return (ui_edit_glyph_t){ null, 0 }; } } static ui_edit_glyph_t ui_edit_right_of(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text if (pg.gp < dt->ps[pg.pn].g - 1) { pg.gp++; return ui_edit_glyph_at(e, pg); } else { return (ui_edit_glyph_t){ null, 0 }; } } static ui_edit_pg_t ui_edit_skip_left_blanks(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_swear(pg.pn <= dt->np - 1); while (pg.gp > 0) { pg.gp--; ui_edit_glyph_t glyph = ui_edit_glyph_at(e, pg); if (glyph.bytes > 0 && !ui_edit_is_blank(glyph)) { pg.gp++; break; } } return pg; } static ui_edit_pg_t ui_edit_skip_right_blanks(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_swear(pg.pn <= dt->np - 1); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, pg.pn); ui_edit_glyph_t glyph = ui_edit_glyph_at(e, pg); while (pg.gp < glyphs && glyph.bytes > 0 && ui_edit_is_blank(glyph)) { pg.gp++; glyph = ui_edit_glyph_at(e, pg); } return pg; } static ui_edit_range_t ui_edit_word_range(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_range_t r = { .from = pg, .to = pg }; ui_edit_text_t* dt = &e->doc->text; // document text if (0 <= pg.pn && 0 <= pg.gp) { rt_swear(pg.pn <= dt->np - 1); // number of glyphs in paragraph: int32_t ng = ui_edit_glyphs_in_paragraph(e, pg.pn); if (pg.gp > ng) { pg.gp = rt_max(0, ng); } ui_edit_glyph_t g = ui_edit_glyph_at(e, pg); if (ng <= 1) { r.to.gp = ng; } else if (ui_edit_is_cjk_or_emoji_or_symbol(g)) { // r == {pg,pg} } else { ui_edit_pg_t from = pg; ui_edit_pg_t to = pg; if (pg.gp > 0 && ui_edit_is_punctuation(g)) { from.gp--; g = ui_edit_glyph_at(e, from); } else if (pg.gp > 0 && ui_edit_is_blank(g)) { from.gp--; to.gp--; g = ui_edit_glyph_at(e, from); } if (ui_edit_is_blank(g)) { while (from.gp > 0 && ui_edit_is_blank(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_blank(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } else if (ui_edit_is_alphanumeric(g)) { while (from.gp > 0 && ui_edit_is_alphanumeric(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_alphanumeric(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } else { while (from.gp > 0 && ui_edit_is_break(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_break(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } } } return r; } static void ui_edit_ctrl_left(ui_edit_view_t* e) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); const ui_edit_range_t s = e->selection; ui_edit_pg_t to = e->selection.to; if (to.gp == 0) { if (to.pn > 0) { to.pn--; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, to.pn, &runs); to.gp = run[runs - 1].gp + run[runs - 1].glyphs; } } else { to.gp--; } const ui_edit_pg_t lf = ui_edit_skip_left_blanks(e, to); const ui_edit_range_t w = ui_edit_word_range(e, lf); e->selection.to = w.from; if (ui_app.shift) { e->selection.from = s.from; } else { e->selection.from = w.from; } ui_edit_move_caret(e, e->selection.to); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } static void ui_edit_view_key_left(ui_edit_view_t* e) { ui_edit_pg_t to = e->selection.a[1]; if (to.pn > 0 || to.gp > 0) { if (ui_app.ctrl) { ui_edit_ctrl_left(e); } else { ui_point_t pt = ui_edit_pg_to_xy(e, to); if (pt.x == 0 && pt.y == 0) { ui_edit_scroll_down(e, 1); } if (to.gp > 0) { to.gp--; } else if (to.pn > 0) { to.pn--; to.gp = ui_edit_glyphs_in_paragraph(e, to.pn); } ui_edit_move_caret(e, to); e->last_x = -1; } } } static void ui_edit_ctrl_right(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_range_t s = e->selection; ui_edit_pg_t to = e->selection.to; int32_t glyphs = ui_edit_glyphs_in_paragraph(e, to.pn); if (to.pn < dt->np - 1 || to.gp < glyphs) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); if (to.gp == glyphs) { to.pn++; to.gp = 0; } else { to.gp++; } ui_edit_pg_t rt = ui_edit_skip_right_blanks(e, to); ui_edit_range_t w = ui_edit_word_range(e, rt); e->selection.to = w.to; if (ui_app.shift) { e->selection.from = s.from; } else { e->selection.from = w.to; } ui_edit_move_caret(e, e->selection.to); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } } static void ui_edit_view_key_right(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t to = e->selection.a[1]; if (to.pn < dt->np) { if (ui_app.ctrl) { ui_edit_ctrl_right(e); } else { int32_t glyphs = ui_edit_glyphs_in_paragraph(e, to.pn); if (to.gp < glyphs) { to.gp++; ui_edit_scroll_into_view(e, to); } else if (!e->sle && to.pn < dt->np - 1) { to.pn++; to.gp = 0; ui_edit_scroll_into_view(e, to); } ui_edit_move_caret(e, to); // TODO: last_x does not work! e->last_x = -1; } } } static void ui_edit_reuse_last_x(ui_edit_view_t* e, ui_point_t* pt) { // Vertical caret movement visually tend to move caret horizontally // in proportional font text. Remembering starting `x' value for vertical // movements alleviates this unpleasant UX experience to some degree. if (pt->x > 0) { if (e->last_x > 0) { int32_t prev = e->last_x - e->fm->em.w; int32_t next = e->last_x + e->fm->em.w; if (prev <= pt->x && pt->x <= next) { pt->x = e->last_x; } } e->last_x = pt->x; } } static void ui_edit_view_key_up(ui_edit_view_t* e) { const ui_edit_pg_t pg = e->selection.a[1]; ui_edit_pg_t to = pg; if (to.pn > 0 || ui_edit_pg_to_pr(e, to).rn > 0) { // top of the text ui_point_t pt = ui_edit_pg_to_xy(e, to); rt_assert(pt.x >= 0 && pt.y >= 0); if (pt.y == 0) { ui_edit_scroll_down(e, 1); } else { pt.y -= 1; } ui_edit_reuse_last_x(e, &pt); rt_assert(pt.y >= 0); to = ui_edit_xy_to_pg(e, pt.x, pt.y); if (to.pn >= 0 && to.gp >= 0) { int32_t rn0 = ui_edit_pg_to_pr(e, pg).rn; int32_t rn1 = ui_edit_pg_to_pr(e, to).rn; if (rn1 > 0 && rn0 == rn1) { // same run rt_assert(to.gp > 0, "word break must not break on zero gp"); int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, to.pn, &runs); to.gp = run[rn1].gp; } } } if (to.pn >= 0 && to.gp >= 0) { ui_edit_move_caret(e, to); } } static void ui_edit_view_key_down(ui_edit_view_t* e) { const ui_edit_pg_t pg = e->selection.a[1]; ui_point_t pt = ui_edit_pg_to_xy(e, pg); ui_edit_reuse_last_x(e, &pt); // TODO: does not work! (used to work broken now) // scroll runs guaranteed to be already laid out for current state of view: ui_edit_pg_t scroll = ui_edit_scroll_pg(e); const int32_t run_count = ui_edit_runs_between(e, scroll, pg); if (!e->sle && run_count > e->visible_runs - 1) { ui_edit_scroll_up(e, 1); } else { pt.y += ui_edit_line_height(e); } ui_edit_pg_t to = ui_edit_xy_to_pg(e, pt.x, pt.y); if (to.pn >= 0 && to.gp >= 0) { ui_edit_move_caret(e, to); } } static void ui_edit_view_key_home(ui_edit_view_t* e) { if (ui_app.ctrl) { e->scroll.pn = 0; e->scroll.rn = 0; e->selection.a[1].pn = 0; e->selection.a[1].gp = 0; ui_edit_invalidate_view(e); } const int32_t pn = e->selection.a[1].pn; int32_t runs = ui_edit_paragraph_run_count(e, pn); const ui_edit_paragraph_t* para = &e->para[pn]; if (runs <= 1) { e->selection.a[1].gp = 0; } else { int32_t rn = ui_edit_pg_to_pr(e, e->selection.a[1]).rn; rt_assert(0 <= rn && rn < runs); const int32_t gp = para->run[rn].gp; if (e->selection.a[1].gp != gp) { // first Home keystroke moves caret to start of run e->selection.a[1].gp = gp; } else { // second Home keystroke moves caret start of paragraph e->selection.a[1].gp = 0; if (e->scroll.pn >= e->selection.a[1].pn) { // scroll in e->scroll.pn = e->selection.a[1].pn; e->scroll.rn = 0; ui_edit_invalidate_view(e); } } } if (!ui_app.shift) { e->selection.a[0] = e->selection.a[1]; } ui_edit_move_caret(e, e->selection.a[1]); } static void ui_edit_view_key_eol(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text int32_t pn = e->selection.a[1].pn; int32_t gp = e->selection.a[1].gp; rt_assert(0 <= pn && pn < dt->np); const ui_edit_str_t* str = &dt->ps[pn]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pn, &runs); int32_t rn = ui_edit_pg_to_pr(e, e->selection.a[1]).rn; rt_assert(0 <= rn && rn < runs); if (rn == runs - 1) { e->selection.a[1].gp = str->g; } else if (e->selection.a[1].gp == str->g) { // at the end of paragraph do nothing (or move caret to EOF?) } else if (str->g > 0 && gp != run[rn].glyphs - 1) { e->selection.a[1].gp = run[rn].gp + run[rn].glyphs - 1; } else { e->selection.a[1].gp = str->g; } } static void ui_edit_view_key_end(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text if (ui_app.ctrl) { int32_t py = e->inside.bottom; for (int32_t i = dt->np - 1; i >= 0 && py >= ui_edit_line_height(e); i--) { int32_t runs = ui_edit_paragraph_run_count(e, i); for (int32_t j = runs - 1; j >= 0 && py >= ui_edit_line_height(e); j--) { py -= ui_edit_line_height(e); if (py < ui_edit_line_height(e)) { e->scroll.pn = i; e->scroll.rn = j; } } } e->selection.a[1] = ui_edit_text.end(dt); ui_edit_invalidate_view(e); } else { ui_edit_view_key_eol(e); } if (!ui_app.shift) { e->selection.a[0] = e->selection.a[1]; } ui_edit_move_caret(e, e->selection.a[1]); } static void ui_edit_view_key_page_up(ui_edit_view_t* e) { int32_t n = rt_max(1, e->visible_runs - 1); ui_edit_pg_t scr = ui_edit_scroll_pg(e); const ui_edit_pg_t prev = (ui_edit_pg_t){ .pn = rt_max(scr.pn - e->visible_runs - 1, 0), .gp = 0 }; const int32_t m = ui_edit_runs_between(e, prev, scr); if (m > n) { ui_point_t pt = ui_edit_pg_to_xy(e, e->selection.a[1]); ui_edit_pr_t scroll = e->scroll; ui_edit_scroll_down(e, n); if (scroll.pn != e->scroll.pn || scroll.rn != e->scroll.rn) { ui_edit_pg_t pg = ui_edit_xy_to_pg(e, pt.x, pt.y); ui_edit_move_caret(e, pg); } } else { const ui_edit_pg_t bof = {.pn = 0, .gp = 0}; ui_edit_move_caret(e, bof); } } static void ui_edit_view_key_page_down(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text const int32_t n = rt_max(1, e->visible_runs - 1); const ui_edit_pg_t scr = ui_edit_scroll_pg(e); const ui_edit_pg_t next = (ui_edit_pg_t){ .pn = rt_min(scr.pn + 1, dt->np - 1), .gp = scr.pn + 1 == dt->np - 1 ? dt->ps[dt->np - 1].g : 0 }; const int32_t m = ui_edit_runs_between(e, scr, next); if (m > n) { const ui_point_t pt = ui_edit_pg_to_xy(e, e->selection.a[1]); const ui_edit_pr_t scroll = e->scroll; ui_edit_scroll_up(e, n); if (scroll.pn != e->scroll.pn || scroll.rn != e->scroll.rn) { ui_edit_pg_t pg = ui_edit_xy_to_pg(e, pt.x, pt.y); ui_edit_move_caret(e, pg); } } else { const ui_edit_pg_t end = ui_edit_text.end(dt); ui_edit_move_caret(e, end); } } static void ui_edit_view_key_delete(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text uint64_t f = ui_edit_range.uint64(e->selection.a[0]); uint64_t t = ui_edit_range.uint64(e->selection.a[1]); uint64_t end = ui_edit_range.uint64(ui_edit_text.end(dt)); if (f == t && t != end) { ui_edit_pg_t s1 = e->selection.a[1]; ui_edit_view.key_right(e); e->selection.a[1] = s1; } ui_edit_view.erase(e); } static void ui_edit_view_key_backspace(ui_edit_view_t* e) { uint64_t f = ui_edit_range.uint64(e->selection.a[0]); uint64_t t = ui_edit_range.uint64(e->selection.a[1]); if (t != 0 && f == t) { ui_edit_pg_t s1 = e->selection.a[1]; ui_edit_view.key_left(e); e->selection.a[1] = s1; } ui_edit_view.erase(e); } static void ui_edit_view_key_enter(ui_edit_view_t* e) { rt_assert(!e->ro); if (!e->sle) { ui_edit_view.erase(e); e->selection.a[1] = ui_edit_insert_paragraph_break(e, e->selection.a[1]); e->selection.a[0] = e->selection.a[1]; ui_edit_move_caret(e, e->selection.a[1]); } else { // single line edit callback if (ui_edit_view.enter != null) { ui_edit_view.enter(e); } } } static bool ui_edit_view_key_pressed(ui_view_t* v, int64_t key) { bool swallow = false; rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_text_t* dt = &e->doc->text; // document text if (e->focused) { swallow = true; if (key == ui.key.down && e->selection.a[1].pn < dt->np) { ui_edit_view.key_down(e); } else if (key == ui.key.up && dt->np > 1) { ui_edit_view.key_up(e); } else if (key == ui.key.left) { ui_edit_view.key_left(e); } else if (key == ui.key.right) { ui_edit_view.key_right(e); } else if (key == ui.key.page_up) { ui_edit_view.key_page_up(e); } else if (key == ui.key.page_down) { ui_edit_view.key_page_down(e); } else if (key == ui.key.home) { ui_edit_view.key_home(e); } else if (key == ui.key.end) { ui_edit_view.key_end(e); } else if (key == ui.key.del && !e->ro) { ui_edit_view.key_delete(e); } else if (key == ui.key.back && !e->ro) { ui_edit_view.key_backspace(e); } else if (key == ui.key.enter && !e->ro) { ui_edit_view.key_enter(e); } else { swallow = false; // ignore other keys } } return swallow; } static void ui_edit_undo(ui_edit_view_t* e) { if (e->doc->undo != null) { ui_edit_doc.undo(e->doc); } else { ui_app.beep(ui.beep.error); } } static void ui_edit_redo(ui_edit_view_t* e) { if (e->doc->redo != null) { ui_edit_doc.redo(e->doc); } else { ui_app.beep(ui.beep.error); } } static void ui_edit_character(ui_view_t* v, const char* utf8) { rt_assert(v->type == ui_view_text); rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); #pragma push_macro("ui_edit_ctrl") #define ui_edit_ctrl(c) ((char)((c) - 'a' + 1)) ui_edit_view_t* e = (ui_edit_view_t*)v; if (e->focused) { char ch = utf8[0]; if (ui_app.ctrl) { if (ch == ui_edit_ctrl('a')) { ui_edit_view.select_all(e); } if (ch == ui_edit_ctrl('c')) { ui_edit_view.copy(e); } if (!e->ro) { if (ch == ui_edit_ctrl('x')) { ui_edit_view.cut(e); } if (ch == ui_edit_ctrl('v')) { ui_edit_view.paste(e); } if (ch == ui_edit_ctrl('y')) { ui_edit_redo(e); } if (ch == ui_edit_ctrl('z') || ch == ui_edit_ctrl('Z')) { if (ui_app.shift) { // Ctrl+Shift+Z ui_edit_redo(e); } else { // Ctrl+Z ui_edit_undo(e); } } } } if (0x20u <= (uint8_t)ch && !e->ro) { // 0x20 space int32_t len = (int32_t)strlen(utf8); int32_t bytes = rt_str.utf8bytes(utf8, len); if (bytes > 0) { ui_edit_view.erase(e); // remove selected text to be replaced by glyph e->selection.a[1] = ui_edit_insert_inline(e, e->selection.a[1], utf8, bytes); e->selection.a[0] = e->selection.a[1]; ui_edit_move_caret(e, e->selection.a[1]); } else { rt_println("invalid UTF8: 0x%02X%02X%02X%02X", utf8[0], utf8[1], utf8[2], utf8[3]); } } } #pragma pop_macro("ui_edit_ctrl") } static void ui_edit_select_word(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (0 <= pg.pn && 0 <= pg.gp) { ui_edit_range_t r = ui_edit_word_range(e, pg); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, r.to.pn); if (r.to.pn == r.from.pn && r.to.gp == r.from.gp && r.to.gp < glyphs) { r.to.gp++; // at least one glyph to the right } if (ui_edit_range.compare(r.from, pg) != 0 || ui_edit_range.compare(r.to, pg) != 0) { e->selection = r; ui_edit_caret_to(e, r.to); // rt_println("e->selection.a[1] = %d.%d", to.pn, to.gp); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); e->edit.buttons = 0; } } } static void ui_edit_select_paragraph(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t p = ui_edit_xy_to_pg(e, x, y); if (0 <= p.pn && 0 <= p.gp) { ui_edit_range_t r = ui_edit_text.ordered(dt, &e->selection); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, p.pn); if (p.gp > glyphs) { p.gp = rt_max(0, glyphs); } if (p.pn == r.a[0].pn && r.a[0].pn == r.a[1].pn && r.a[0].gp <= p.gp && p.gp <= r.a[1].gp) { r.a[0].gp = 0; if (p.pn < dt->np - 1) { r.a[1].pn = p.pn + 1; r.a[1].gp = 0; } else { r.a[1].gp = dt->ps[p.pn].g; } e->selection = r; ui_edit_caret_to(e, r.to); } ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); e->edit.buttons = 0; } } static void ui_edit_click(ui_edit_view_t* e, int32_t x, int32_t y) { // x, y in 0..e->w, 0->e.h coordinate space rt_assert(0 <= x && x < e->w && 0 <= y && y < e->h); ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (0 <= pg.pn && 0 <= pg.gp && ui_view.has_focus(&e->view)) { rt_swear(dt->np > 0 && pg.pn < dt->np); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, pg.pn); if (pg.gp > glyphs) { pg.gp = rt_max(0, glyphs); } ui_edit_move_caret(e, pg); } } static void ui_edit_mouse_button_down(ui_edit_view_t* e, int32_t ix) { e->edit.buttons |= (1 << ix); } static void ui_edit_mouse_button_up(ui_edit_view_t* e, int32_t ix) { e->edit.buttons &= ~(1 << ix); } static bool ui_edit_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { // `ix` ignored for now till context menu (copy/paste/select...) ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); // not just inside view but inside insets: bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside) { if (pressed) { e->edit.buttons = 0; ui_edit_click(e, x, y); ui_edit_mouse_button_down(e, ix); } else if (!pressed) { ui_edit_mouse_button_up(e, ix); } } if (!pressed) { ui_edit_mouse_button_up(e, ix); } return true; } static bool ui_edit_long_press(ui_view_t* v, int32_t rt_unused(ix)) { ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside && ui_edit_range.is_empty(e->selection)) { ui_edit_select_paragraph(e, x, y); } return true; } static bool ui_edit_double_tap(ui_view_t* v, int32_t rt_unused(ix)) { ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside && e->selection.a[0].pn == e->selection.a[1].pn) { ui_edit_select_word(e, x, y); } return false; } static void ui_edit_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { if (v->w > 0 && v->h > 0) { const int32_t dy = dx_dy.y; // TODO: maybe make a use of dx in single line no-word-break edit control? if (ui_app.focus == v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; int32_t lines = (abs(dy) + ui_edit_line_height(e) - 1) / ui_edit_line_height(e); if (dy > 0) { ui_edit_scroll_down(e, lines); } else if (dy < 0) { ui_edit_scroll_up(e, lines); } // TODO: Ctrl UP/DW and caret of out of visible area scrolls are not // implemented. Not sure they are very good UX experience. // MacOS users may be used to scroll with touchpad, take a visual // peek, do NOT click and continue editing at last cursor position. // To me back forward stack navigation is much more intuitive and // much mode "modeless" in spirit of cut/copy/paste. But opinions // and editing habits vary. Easy to implement. const int32_t x = e->caret.x - e->inside.left; const int32_t y = e->caret.y - e->inside.top; ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (pg.pn >= 0 && pg.gp >= 0) { rt_assert(pg.gp <= e->doc->text.ps[pg.pn].g); ui_edit_move_caret(e, pg); } else { ui_edit_click(e, x, y); } } } } static bool ui_edit_focus_gained(ui_view_t* v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; rt_assert(v->focusable); if (ui_app.focused() && !e->focused) { ui_edit_create_caret(e); ui_edit_show_caret(e); ui_edit_if_sle_layout(e); } e->edit.buttons = 0; ui_app.request_redraw(); return true; } static void ui_edit_focus_lost(ui_view_t* v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; if (e->focused) { ui_edit_hide_caret(e); ui_edit_destroy_caret(e); ui_edit_if_sle_layout(e); } e->edit.buttons = 0; ui_app.request_redraw(); } static void ui_edit_view_erase(ui_edit_view_t* e) { if (e->selection.from.pn != e->selection.to.pn) { ui_edit_invalidate_view(e); } else { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } ui_edit_range_t r = ui_edit_range.order(e->selection); if (!ui_edit_range.is_empty(r) && ui_edit_doc.replace(e->doc, &r, null, 0)) { e->selection = r; e->selection.to = e->selection.from; ui_edit_move_caret(e, e->selection.from); } } static void ui_edit_select_all(ui_edit_view_t* e) { e->selection = ui_edit_text.all_on_null(&e->doc->text, null); ui_edit_invalidate_view(e); } static int32_t ui_edit_view_save(ui_edit_view_t* e, char* text, int32_t* bytes) { rt_not_null(bytes); enum { error_insufficient_buffer = 122, // ERROR_INSUFFICIENT_BUFFER error_more_data = 234 // ERROR_MORE_DATA }; int32_t r = 0; const int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, null); if (text == null) { *bytes = utf8bytes; r = rt_core.error.more_data; } else if (*bytes < utf8bytes) { r = rt_core.error.insufficient_buffer; } else { ui_edit_doc.copy(e->doc, null, text, utf8bytes); rt_assert(text[utf8bytes - 1] == 0x00); } return r; } static void ui_edit_view_copy(ui_edit_view_t* e) { int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, &e->selection); if (utf8bytes > 0) { char* text = null; bool ok = rt_heap.alloc((void**)&text, utf8bytes) == 0; rt_swear(ok); ui_edit_doc.copy(e->doc, &e->selection, text, utf8bytes); rt_assert(text[utf8bytes - 1] == 0x00); // verify zero termination rt_clipboard.put_text(text); rt_heap.free(text); static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); int32_t x = e->x + e->caret.x; int32_t y = e->y + e->caret.y - ui_edit_line_height(e); if (y < ui_app.content->y) { y += ui_edit_line_height(e) * 2; } if (y > ui_app.content->y + ui_app.content->h - ui_edit_line_height(e)) { y = e->caret.y; } ui_app.show_hint(&hint, x, y, 0.5); } } static void ui_edit_view_cut(ui_edit_view_t* e) { int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, &e->selection); if (utf8bytes > 0) { ui_edit_view_copy(e); } if (!e->ro) { ui_edit_view.erase(e); } } static ui_edit_pg_t ui_edit_paste_text(ui_edit_view_t* e, const char* text, int32_t bytes) { rt_assert(!e->ro); ui_edit_text_t t = {0}; ui_edit_text.init(&t, text, bytes, false); ui_edit_range_t r = ui_edit_text.all_on_null(&t, null); ui_edit_doc.replace(e->doc, &e->selection, text, bytes); ui_edit_pg_t pg = e->selection.from; pg.pn += r.to.pn; if (e->selection.from.pn == e->selection.to.pn && r.to.pn == 0) { pg.gp = e->selection.from.gp + r.to.gp; } else { pg.gp = r.to.gp; } ui_edit_text.dispose(&t); return pg; } static void ui_edit_view_replace(ui_edit_view_t* e, const char* s, int32_t n) { if (!e->ro) { if (n < 0) { n = (int32_t)strlen(s); } ui_edit_view.erase(e); e->selection.a[1] = ui_edit_paste_text(e, s, n); e->selection.a[0] = e->selection.a[1]; if (e->w > 0) { ui_edit_move_caret(e, e->selection.a[1]); } } } static void ui_edit_view_paste(ui_edit_view_t* e) { if (!e->ro) { ui_edit_pg_t pg = e->selection.a[1]; int32_t bytes = 0; rt_clipboard.get_text(null, &bytes); if (bytes > 0) { char* text = null; bool ok = rt_heap.alloc((void**)&text, bytes) == 0; rt_swear(ok); int32_t r = rt_clipboard.get_text(text, &bytes); rt_fatal_if_error(r); if (bytes > 0 && text[bytes - 1] == 0) { bytes--; // clipboard includes zero terminator } if (bytes > 0) { ui_edit_view.erase(e); pg = ui_edit_paste_text(e, text, bytes); ui_edit_move_caret(e, pg); } rt_heap.free(text); } } } static void ui_edit_prepare_sle(ui_edit_view_t* e) { ui_view_t* v = &e->view; rt_swear(e->sle && v->w > 0); // shingle line edit is capable of resizing itself to two // lines of text (and shrinking back) to avoid horizontal scroll int32_t runs = rt_max(1, rt_min(2, ui_edit_paragraph_run_count(e, 0))); const ui_ltrb_t insets = ui_view.margins(v, &v->insets); int32_t h = insets.top + ui_edit_line_height(e) * runs + insets.bottom; fp32_t min_h_em = (fp32_t)h / v->fm->em.h; if (v->min_h_em != min_h_em) { v->min_h_em = min_h_em; } } static void ui_edit_insets(ui_edit_view_t* e) { ui_view_t* v = &e->view; const ui_ltrb_t insets = ui_view.margins(v, &v->insets); e->inside = (ui_ltrb_t){ .left = insets.left, .top = insets.top, .right = v->w - insets.right, .bottom = v->h - insets.bottom }; const int32_t width = e->edit.w; // previous width e->edit.w = e->inside.right - e->inside.left; e->edit.h = e->inside.bottom - e->inside.top; if (e->edit.w != width) { ui_edit_invalidate_all_runs(e); } } static void ui_edit_measure(ui_view_t* v) { // bottom up rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; if (v->w > 0 && e->sle) { ui_edit_prepare_sle(e); } v->w = (int32_t)((fp64_t)v->fm->em.w * (fp64_t)v->min_w_em + 0.5); v->h = (int32_t)((fp64_t)v->fm->em.h * (fp64_t)v->min_h_em + 0.5); const ui_ltrb_t i = ui_view.margins(v, &v->insets); // enforce minimum size - it makes it checking corner cases much simpler // and it's hard to edit anything in a smaller area - will result in bad UX if (v->w < v->fm->em.w * 4) { v->w = i.left + v->fm->em.w * 4 + i.right; } if (v->h < ui_edit_line_height(e)) { v->h = i.top + ui_edit_line_height(e) + i.bottom; } } static void ui_edit_layout(ui_view_t* v) { // top down rt_assert(v->type == ui_view_text); rt_assert(v->w > 0 && v->h > 0); // could be `if' ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_insets(e); // fully visible runs e->visible_runs = e->h / ui_edit_line_height(e); ui_edit_invalidate_run(e, e->scroll.pn); // number of runs in e->scroll.pn may have changed with e->w change int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); // glyph position in scroll_pn paragraph: const ui_edit_pg_t scroll = v->w == 0 ? (ui_edit_pg_t){0, 0} : ui_edit_scroll_pg(e); e->scroll.rn = ui_edit_pg_to_pr(e, scroll).rn; rt_assert(0 <= e->scroll.rn && e->scroll.rn < runs); (void)runs; if (e->sle) { // single line edit (if changed on the fly): e->selection.a[0].pn = 0; // only has single paragraph e->selection.a[1].pn = 0; // scroll line on top of current cursor position into view const ui_edit_run_t* run = ui_edit_paragraph_runs(e, 0, &runs); if (runs <= 2 && e->scroll.rn == 1) { ui_edit_pg_t top = scroll; top.gp = rt_max(0, top.gp - run[e->scroll.rn].glyphs - 1); ui_edit_scroll_into_view(e, top); } } ui_edit_scroll_into_view(e, e->selection.a[1]); ui_edit_caret_to(e, e->selection.a[1]); if (e->focused) { // recreate caret because fm->height may have changed ui_edit_hide_caret(e); ui_edit_destroy_caret(e); ui_edit_create_caret(e); ui_edit_show_caret(e); rt_assert(e->focused); } } static void ui_edit_paint_selection(ui_edit_view_t* e, int32_t y, const ui_edit_run_t* r, const char* text, int32_t pn, int32_t c0, int32_t c1) { uint64_t s0 = ui_edit_range.uint64(e->selection.a[0]); uint64_t e0 = ui_edit_range.uint64(e->selection.a[1]); if (s0 > e0) { uint64_t swap = e0; e0 = s0; s0 = swap; } const ui_edit_pg_t pnc0 = {.pn = pn, .gp = c0}; const ui_edit_pg_t pnc1 = {.pn = pn, .gp = c1}; uint64_t s1 = ui_edit_range.uint64(pnc0); uint64_t e1 = ui_edit_range.uint64(pnc1); if (s0 <= e1 && s1 <= e0) { uint64_t start = rt_max(s0, s1) - (uint64_t)c0; uint64_t end = rt_min(e0, e1) - (uint64_t)c0; if (start < end) { int32_t fro = (int32_t)start; int32_t to = (int32_t)end; int32_t ofs0 = ui_edit_str.gp_to_bp(text, r->bytes, fro); int32_t ofs1 = ui_edit_str.gp_to_bp(text, r->bytes, to); rt_swear(ofs0 >= 0 && ofs1 >= 0); int32_t x0 = ui_edit_text_width(e, text, ofs0); int32_t x1 = ui_edit_text_width(e, text, ofs1); // selection color is MSVC dark mode selection color // TODO: need light mode selection color tpp ui_color_t sc = ui_color_rgb(0x26, 0x4F, 0x78); // selection color if (!e->focused || !ui_app.focused()) { sc = ui_colors.darken(sc, 0.1f); } const ui_ltrb_t insets = ui_view.margins(&e->view, &e->insets); int32_t x = e->x + insets.left; // event if background is transparent ui_gdi.fill(x + x0, y, x1 - x0, ui_edit_line_height(e), sc); } } } static int32_t ui_edit_paint_paragraph(ui_edit_view_t* e, const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t pn, ui_rect_t rc) { static const char* ww = rt_glyph_south_west_arrow_with_hook; ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); const ui_edit_str_t* str = &dt->ps[pn]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pn, &runs); for (int32_t j = ui_edit_first_visible_run(e, pn); j < runs && y < e->y + e->inside.bottom; j++) { // rt_println("[%d.%d] @%d,%d bytes: %d", pn, j, x, y, run[j].bytes); if (rc.y - ui_edit_line_height(e) <= y && y < rc.y + rc.h) { const char* text = str->u + run[j].bp; ui_edit_paint_selection(e, y, &run[j], text, pn, run[j].gp, run[j].gp + run[j].glyphs); ui_gdi.text(ta, x, y, "%.*s", run[j].bytes, text); if (j < runs - 1 && !e->hide_word_wrap) { ui_gdi.text(ta, x + e->edit.w, y, "%s", ww); } } y += ui_edit_line_height(e); } return y; } static void ui_edit_paint(ui_view_t* v) { rt_assert(v->type == ui_view_text); rt_assert(!ui_view.is_hidden(v)); ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_text_t* dt = &e->doc->text; // document text // drawing text is really expensive, only paint what's needed: ui_rect_t vrc = (ui_rect_t){v->x, v->y, v->w, v->h}; ui_rect_t rc; if (ui.intersect_rect(&rc, &vrc, &ui_app.prc)) { // because last line of the view may extend over the bottom ui_gdi.set_clip(v->x, v->y, v->w, v->h); ui_color_t b = v->background; if (!ui_color_is_undefined(b) && !ui_color_is_transparent(b)) { ui_gdi.fill(rc.x, rc.y, rc.w, rc.h, b); } const ui_ltrb_t insets = ui_view.margins(v, &v->insets); int32_t x = v->x + insets.left; int32_t y = v->y + insets.top; const ui_gdi_ta_t ta = { .fm = v->fm, .color = v->color }; const int32_t pn = e->scroll.pn; const int32_t bottom = v->y + e->inside.bottom; rt_assert(pn < dt->np); for (int32_t i = pn; i < dt->np && y < bottom; i++) { y = ui_edit_paint_paragraph(e, &ta, x, y, i, rc); } ui_gdi.set_clip(0, 0, 0, 0); } } static void ui_edit_view_move(ui_edit_view_t* e, ui_edit_pg_t pg) { if (e->w > 0) { ui_edit_move_caret(e, pg); // may select text on move } else { e->selection.a[1] = pg; } e->selection.a[0] = e->selection.a[1]; } static bool ui_edit_reallocate_runs(ui_edit_view_t* e, int32_t p, int32_t np) { // This function is called in after() callback when // d->text.np already changed to `new_np`. // It has to manipulate e->para[] array w/o calling // ui_edit_invalidate_runs() ui_edit_dispose_all_runs() // because they assume that e->para[] array is in sync // d->text.np. ui_edit_text_t* dt = &e->doc->text; // document text bool ok = true; int32_t old_np = np; // old (before) number of paragraphs int32_t new_np = dt->np; // new (after) number of paragraphs rt_assert(old_np > 0 && new_np > 0 && e->para != null); rt_assert(0 <= p && p < old_np); if (old_np == new_np) { ui_edit_invalidate_run(e, p); } else if (new_np < old_np) { // shrinking - delete runs const int32_t d = old_np - new_np; // `d` delta > 0 if (p + d < old_np - 1) { const int32_t n = rt_max(0, old_np - p - d - 1); memcpy(e->para + p + 1, e->para + p + 1 + d, n * sizeof(e->para[0])); } if (p < new_np) { ui_edit_invalidate_run(e, p); } ok = rt_heap.realloc((void**)&e->para, new_np * sizeof(e->para[0])) == 0; rt_swear(ok, "shrinking"); } else { // growing - insert runs ui_edit_invalidate_run(e, p); int32_t d = new_np - old_np; // `d` delta > 0 ok = rt_heap.realloc_zero((void**)&e->para, new_np * sizeof(e->para[0])) == 0; if (ok) { const int32_t n = rt_max(0, new_np - p - d - 1); memmove(e->para + p + 1 + d, e->para + p + 1, (size_t)n * sizeof(e->para[0])); const int32_t m = rt_min(new_np, p + 1 + d); for (int32_t i = p + 1; i < m; i++) { e->para[i].run = null; e->para[i].runs = 0; } } } return ok; } static void ui_edit_before(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni) { ui_edit_notify_view_t* n = (ui_edit_notify_view_t*)notify; ui_edit_view_t* e = (ui_edit_view_t*)n->that; rt_swear(e->doc == ni->d); if (e->w > 0 && e->h > 0) { const ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(dt->np > 0); // `n->data` is number of paragraphs before replace(): n->data = (uintptr_t)dt->np; if (e->selection.from.pn != e->selection.to.pn) { ui_edit_invalidate_view(e); } else { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } } } static void ui_edit_after(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni) { ui_edit_notify_view_t* n = (ui_edit_notify_view_t*)notify; ui_edit_view_t* e = (ui_edit_view_t*)n->that; const ui_edit_text_t* dt = &ni->d->text; // document text rt_assert(ni->d == e->doc && dt->np > 0); if (e->w > 0 && e->h > 0) { // number of paragraphs before replace(): const int32_t np = (int32_t)n->data; rt_swear(dt->np == np - ni->deleted + ni->inserted); ui_edit_reallocate_runs(e, ni->r->from.pn, np); e->selection = *ni->x; // this is needed by undo/redo: trim selection ui_edit_pg_t* pg = e->selection.a; for (int32_t i = 0; i < rt_countof(e->selection.a); i++) { pg[i].pn = rt_max(0, rt_min(dt->np - 1, pg[i].pn)); pg[i].gp = rt_max(0, rt_min(dt->ps[pg[i].pn].g, pg[i].gp)); } if (ni->r->from.pn != ni->r->to.pn && ni->x->from.pn != ni->x->to.pn && ni->r->from.pn == ni->x->from.pn) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } else { ui_edit_invalidate_view(e); } ui_edit_scroll_into_view(e, e->selection.to); } } static void ui_edit_view_init(ui_edit_view_t* e, ui_edit_doc_t* d) { memset(e, 0, sizeof(*e)); rt_assert(d != null && d->text.np > 0); e->doc = d; rt_assert(d->text.np > 0); e->listener.that = (void*)e; e->listener.data = 0; e->listener.notify.before = ui_edit_before; e->listener.notify.after = ui_edit_after; rt_static_assertion(offsetof(ui_edit_notify_view_t, notify) == 0); ui_edit_doc.subscribe(d, &e->listener.notify); e->color_id = ui_color_id_window_text; e->background_id = ui_color_id_window; e->fm = &ui_app.fm.prop.normal; e->insets = (ui_margins_t){ 0.25, 0.25, 0.50, 0.25 }; e->padding = (ui_margins_t){ 0.25, 0.25, 0.25, 0.25 }; e->min_w_em = 1.0; e->min_h_em = 1.0; e->type = ui_view_text; e->focusable = true; e->last_x = -1; e->focused = false; e->sle = false; e->ro = false; e->caret = (ui_point_t){-1, -1}; e->paint = ui_edit_paint; e->measure = ui_edit_measure; e->layout = ui_edit_layout; e->tap = ui_edit_tap; e->long_press = ui_edit_long_press; e->double_tap = ui_edit_double_tap; e->character = ui_edit_character; e->focus_gained = ui_edit_focus_gained; e->focus_lost = ui_edit_focus_lost; e->key_pressed = ui_edit_view_key_pressed; e->mouse_scroll = ui_edit_mouse_scroll; ui_edit_allocate_runs(e); if (e->debug.id == null) { e->debug.id = "#edit"; } } static void ui_edit_view_dispose(ui_edit_view_t* e) { ui_edit_doc.unsubscribe(e->doc, &e->listener.notify); ui_edit_dispose_all_runs(e); memset(e, 0, sizeof(*e)); } ui_edit_view_if ui_edit_view = { .init = ui_edit_view_init, .set_font = ui_edit_view_set_font, .move = ui_edit_view_move, .replace = ui_edit_view_replace, .save = ui_edit_view_save, .erase = ui_edit_view_erase, .cut = ui_edit_view_cut, .copy = ui_edit_view_copy, .paste = ui_edit_view_paste, .select_all = ui_edit_select_all, .key_down = ui_edit_view_key_down, .key_up = ui_edit_view_key_up, .key_left = ui_edit_view_key_left, .key_right = ui_edit_view_key_right, .key_page_up = ui_edit_view_key_page_up, .key_page_down = ui_edit_view_key_page_down, .key_home = ui_edit_view_key_home, .key_end = ui_edit_view_key_end, .key_delete = ui_edit_view_key_delete, .key_backspace = ui_edit_view_key_backspace, .key_enter = ui_edit_view_key_enter, .fuzz = null, .dispose = ui_edit_view_dispose }; // _______________________________ ui_fuzzing.c _______________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" // TODO: Ctrl+A Ctrl+V Ctrl+C Ctrl+X Ctrl+Z Ctrl+Y static bool ui_fuzzing_debug = true; static uint32_t ui_fuzzing_seed; static bool ui_fuzzing_running; static bool ui_fuzzing_inside; static ui_fuzzing_t ui_fuzzing_work; static const char* lorem_ipsum_words[] = { "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "quisque", "faucibus", "ex", "sapien", "vitae", "pellentesque", "sem", "placerat", "in", "id", "cursus", "mi", "pretium", "tellus", "duis", "convallis", "tempus", "leo", "eu", "aenean", "sed", "diam", "urna", "tempor", "pulvinar", "vivamus", "fringilla", "lacus", "nec", "metus", "bibendum", "egestas", "iaculis", "massa", "nisl", "malesuada", "lacinia", "integer", "nunc", "posuere", "ut", "hendrerit", "semper", "vel", "class", "aptent", "taciti", "sociosqu", "ad", "litora", "torquent", "per", "conubia", "nostra", "inceptos", "himenaeos", "orci", "varius", "natoque", "penatibus", "et", "magnis", "dis", "parturient", "montes", "nascetur", "ridiculus", "mus", "donec", "rhoncus", "eros", "lobortis", "nulla", "molestie", "mattis", "scelerisque", "maximus", "eget", "fermentum", "odio", "phasellus", "non", "purus", "est", "efficitur", "laoreet", "mauris", "pharetra", "vestibulum", "fusce", "dictum", "risus", "blandit", "quis", "suspendisse", "aliquet", "nisi", "sodales", "consequat", "magna", "ante", "condimentum", "neque", "at", "luctus", "nibh", "finibus", "facilisis", "dapibus", "etiam", "interdum", "tortor", "ligula", "congue", "sollicitudin", "erat", "viverra", "ac", "tincidunt", "nam", "porta", "elementum", "a", "enim", "euismod", "quam", "justo", "lectus", "commodo", "augue", "arcu", "dignissim", "velit", "aliquam", "imperdiet", "mollis", "nullam", "volutpat", "porttitor", "ullamcorper", "rutrum", "gravida", "cras", "eleifend", "turpis", "fames", "primis", "vulputate", "ornare", "sagittis", "vehicula", "praesent", "dui", "felis", "venenatis", "ultrices", "proin", "libero", "feugiat", "tristique", "accumsan", "maecenas", "potenti", "ultricies", "habitant", "morbi", "senectus", "netus", "suscipit", "auctor", "curabitur", "facilisi", "cubilia", "curae", "hac", "habitasse", "platea", "dictumst" }; #define ui_fuzzing_lorem_ipsum_canonique \ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " \ "eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad " \ "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " \ "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " \ "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur " \ "sint occaecat cupidatat non proident, sunt in culpa qui officia " \ "deserunt mollit anim id est laborum." #define ui_fuzzing_lorem_ipsum_chinese \ "\xE6\x88\x91\xE6\x98\xAF\xE6\x94\xBE\xE7\xBD\xAE\xE6\x96\x87\xE6\x9C\xAC\xE7\x9A\x84\xE4" \ "\xBD\x8D\xE7\xBD\xAE\xE3\x80\x82\xE8\xBF\x99\xE9\x87\x8C\xE6\x94\xBE\xE7\xBD\xAE\xE4\xBA" \ "\x86\xE5\x81\x87\xE6\x96\x87\xE5\x81\x87\xE5\xAD\x97\xE3\x80\x82\xE5\xB8\x8C\xE6\x9C\x9B" \ "\xE8\xBF\x99\xE4\xBA\x9B\xE6\x96\x87\xE5\xAD\x97\xE5\x8F\xAF\xE4\xBB\xA5\xE5\xA1\xAB\xE5" \ "\x85\x85\xE7\xA9\xBA\xE7\x99\xBD\xE3\x80\x82"; #define ui_fuzzing_lorem_ipsum_japanese \ "\xE3\x81\x93\xE3\x82\x8C\xE3\x81\xAF\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3" \ "\x82\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x81\xA7\xE3\x81\x99\xE3\x80\x82\xE3\x81\x93\xE3\x81" \ "\x93\xE3\x81\xAB\xE6\x96\x87\xE7\xAB\xA0\xE3\x81\x8C\xE5\x85\xA5\xE3\x82\x8A\xE3\x81\xBE" \ "\xE3\x81\x99\xE3\x80\x82\xE8\xAA\xAD\xE3\x81\xBF\xE3\x82\x84\xE3\x81\x99\xE3\x81\x84\xE3" \ "\x82\x88\xE3\x81\x86\xE3\x81\xAB\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3\x82" \ "\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x82\x92\xE4\xBD\xBF\xE7\x94\xA8\xE3\x81\x97\xE3\x81\xA6" \ "\xE3\x81\x84\xE3\x81\xBE\xE3\x81\x99\xE3\x80\x82"; #define ui_fuzzing_lorem_ipsum_korean \ "\xEC\x9D\xB4\xEA\xB2\x83\xEC\x9D\x80\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED\x85\x8D\xEC\x8A" \ "\xA4\xED\x8A\xB8\xEC\x9E\x85\xEB\x8B\x88\xEB\x8B\xA4\x2E\x20\xEC\x97\xAC\xEA\xB8\xB0\xEC" \ "\x97\x90\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEB\x93\x9C\xEC\x96\xB4\xEA\xB0\x80" \ "\xEB\x8A\x94\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEC\x9E\x88\xEB\x8B\xA4\x2E\x20" \ "\xEC\x9D\xBD\xEA\xB8\xB0\x20\xEC\x89\xBD\xEA\xB2\x8C\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED" \ "\x85\x8D\xEC\x8A\xA4\xED\x8A\xB8\xEB\xA5\xBC\x20\xEC\x82\xAC\xEC\x9A\xA9\xED\x95\xA9\xEB" \ "\x8B\x88\xEB\x8B\xA4\x2E"; #define ui_fuzzing_lorem_ipsum_emoji \ "\xF0\x9F\x8D\x95\xF0\x9F\x9A\x80\xF0\x9F\xA6\x84\xF0\x9F\x92\xBB\xF0\x9F\x8E\x89\xF0\x9F" \ "\x8C\x88\xF0\x9F\x90\xB1\xF0\x9F\x93\x9A\xF0\x9F\x8E\xA8\xF0\x9F\x8D\x94\xF0\x9F\x8D\xA6" \ "\xF0\x9F\x8E\xB8\xF0\x9F\xA7\xA9\xF0\x9F\x8D\xBF\xF0\x9F\x93\xB7\xF0\x9F\x8E\xA4\xF0\x9F" \ "\x91\xBE\xF0\x9F\x8C\xAE\xF0\x9F\x8E\x88\xF0\x9F\x9A\xB2\xF0\x9F\x8D\xA9\xF0\x9F\x8E\xAE" \ "\xF0\x9F\x8D\x89\xF0\x9F\x8E\xAC\xF0\x9F\x90\xB6\xF0\x9F\x93\xB1\xF0\x9F\x8E\xB9\xF0\x9F" \ "\xA6\x96\xF0\x9F\x8C\x9F\xF0\x9F\x8D\xAD\xF0\x9F\x8E\xA4\xF0\x9F\x8F\x96\xF0\x9F\xA6\x8B" \ "\xF0\x9F\x8E\xB2\xF0\x9F\x8E\xAF\xF0\x9F\x8D\xA3\xF0\x9F\x9A\x81\xF0\x9F\x8E\xAD\xF0\x9F" \ "\x91\x9F\xF0\x9F\x9A\x82\xF0\x9F\x8D\xAA\xF0\x9F\x8E\xBB\xF0\x9F\x9B\xB8\xF0\x9F\x8C\xBD" \ "\xF0\x9F\x93\x80\xF0\x9F\x9A\x80\xF0\x9F\xA7\x81\xF0\x9F\x93\xAF\xF0\x9F\x8C\xAF\xF0\x9F" \ "\x90\xA5\xF0\x9F\xA7\x83\xF0\x9F\x8D\xBB\xF0\x9F\x8E\xAE"; typedef struct { char* text; int32_t count; // at least 1KB uint32_t seed; // seed for random generator int32_t min_paragraphs; // at least 1 int32_t max_paragraphs; int32_t min_sentences; // at least 1 int32_t max_sentences; int32_t min_words; // at least 2 int32_t max_words; const char* append; // append after each paragraph (e.g. extra "\n") } ui_fuzzing_generator_params_t; static uint32_t ui_fuzzing_random(void) { return rt_num.random32(&ui_fuzzing_seed); } static fp64_t ui_fuzzing_random_fp64(void) { uint32_t r = ui_fuzzing_random(); return (fp64_t)r / (fp64_t)UINT32_MAX; } static void ui_fuzzing_generator(ui_fuzzing_generator_params_t p) { rt_fatal_if(p.count < 1024); // at least 1KB expected rt_fatal_if_not(0 < p.min_paragraphs && p.min_paragraphs <= p.max_paragraphs); rt_fatal_if_not(0 < p.min_sentences && p.min_sentences <= p.max_sentences); rt_fatal_if_not(2 < p.min_words && p.min_words <= p.max_words); char* s = p.text; // assume longest word is less than 128 char* end = p.text + p.count - 128; uint32_t paragraphs = p.min_paragraphs + (p.min_paragraphs == p.max_paragraphs ? 0 : rt_num.random32(&p.seed) % (p.max_paragraphs - p.min_paragraphs + 1)); while (paragraphs > 0 && s < end) { uint32_t sentences_in_paragraph = p.min_sentences + (p.min_sentences == p.max_sentences ? 0 : rt_num.random32(&p.seed) % (p.max_sentences - p.min_sentences + 1)); while (sentences_in_paragraph > 0 && s < end) { const uint32_t words_in_sentence = p.min_words + (p.min_words == p.max_words ? 0 : rt_num.random32(&p.seed) % (p.max_words - p.min_words + 1)); for (uint32_t i = 0; i < words_in_sentence && s < end; i++) { const int32_t ix = rt_num.random32(&p.seed) % rt_countof(lorem_ipsum_words); const char* word = lorem_ipsum_words[ix]; memcpy(s, word, strlen(word)); if (i == 0) { *s = (char)toupper(*s); } s += strlen(word); if (i < words_in_sentence - 1 && s < end) { const char* delimiter = "\x20"; int32_t punctuation = rt_num.random32(&p.seed) % 128; switch (punctuation) { case 0: case 1: case 2: delimiter = ", "; break; case 3: case 4: delimiter = "; "; break; case 6: delimiter = ": "; break; case 7: delimiter = " - "; break; default: break; } memcpy(s, delimiter, strlen(delimiter)); s += strlen(delimiter); } } if (sentences_in_paragraph > 1 && s < end) { memcpy(s, ".\x20", 2); s += 2; } else { *s++ = '.'; } sentences_in_paragraph--; } if (paragraphs > 1 && s < end) { *s++ = '\n'; } if (p.append != null && p.append[0] != 0) { memcpy(s, p.append, strlen(p.append)); s += strlen(p.append); } paragraphs--; } *s = 0; // rt_println("%s\n", p.text); } static void ui_fuzzing_next_gibberish(int32_t number_of_characters, char text[]) { static fp64_t freq[96] = { 0.1716, 0.0023, 0.0027, 0.0002, 0.0001, 0.0005, 0.0013, 0.0012, 0.0015, 0.0014, 0.0017, 0.0002, 0.0084, 0.0020, 0.0075, 0.0040, 0.0135, 0.0045, 0.0053, 0.0053, 0.0047, 0.0047, 0.0043, 0.0047, 0.0057, 0.0044, 0.0037, 0.0004, 0.0016, 0.0004, 0.0017, 0.0017, 0.0020, 0.0045, 0.0026, 0.0020, 0.0027, 0.0021, 0.0025, 0.0026, 0.0030, 0.0025, 0.0021, 0.0018, 0.0028, 0.0026, 0.0024, 0.0020, 0.0025, 0.0026, 0.0030, 0.0022, 0.0027, 0.0022, 0.0020, 0.0023, 0.0015, 0.0016, 0.0009, 0.0005, 0.0005, 0.0001, 0.0003, 0.0003, 0.0078, 0.0013, 0.0012, 0.0008, 0.0012, 0.0007, 0.0006, 0.0011, 0.0016, 0.0012, 0.0011, 0.0004, 0.0004, 0.0016, 0.0013, 0.0009, 0.0009, 0.0008, 0.0013, 0.0011, 0.0013, 0.0012, 0.0006, 0.0007, 0.0011, 0.0005, 0.0007, 0.0003, 0.0002, 0.0006, 0.0002, 0.0005 }; static fp64_t cumulative_freq[96]; static bool initialized = 0; if (!initialized) { cumulative_freq[0] = freq[0]; for (int i = 1; i < rt_countof(freq); i++) { cumulative_freq[i] = cumulative_freq[i - 1] + freq[i]; } initialized = 1; } int32_t i = 0; while (i < number_of_characters) { text[i] = 0x00; fp64_t r = ui_fuzzing_random_fp64(); for (int j = 0; j < 96 && text[i] == 0; j++) { if (r < cumulative_freq[j]) { text[i] = (char)(0x20 + j); } } if (text[i] != 0) { i++; } } text[number_of_characters] = 0x00; } static void ui_fuzzing_dispatch(ui_fuzzing_t* work) { rt_swear(work == &ui_fuzzing_work); ui_app.alt = work->alt; ui_app.ctrl = work->ctrl; ui_app.shift = work->shift; if (work->utf8 != null && work->utf8[0] != 0) { ui_view.character(ui_app.content, work->utf8); work->utf8 = work->utf8[1] == 0 ? null : work->utf8++; } else if (work->key != 0) { ui_view.key_pressed(ui_app.content, work->key); ui_view.key_released(ui_app.content, work->key); work->key = 0; } else if (work->pt != null) { const int32_t x = work->pt->x; const int32_t y = work->pt->y; ui_app.mouse.x = x; ui_app.mouse.y = y; // https://stackoverflow.com/questions/22259936/ // https://stackoverflow.com/questions/65691101/ // rt_println("%d,%d", x + ui_app.wrc.x, y + ui_app.wrc.y); // // next line works only when running as administrator: // rt_fatal_win32err(SetCursorPos(x + ui_app.wrc.x, y + ui_app.wrc.y)); const bool l_button = ui_app.mouse_left != work->left; const bool r_button = ui_app.mouse_right != work->right; ui_app.mouse_left = work->left; ui_app.mouse_right = work->right; ui_view.mouse_move(ui_app.content); if (l_button) { ui_view.tap(ui_app.content, 0, work->left); } if (r_button) { ui_view.tap(ui_app.content, 2, work->right); } work->pt = null; } else { rt_assert(false, "TODO: ?"); } if (ui_fuzzing_running) { if (ui_fuzzing.next == null) { ui_fuzzing.next_random(work); } else { ui_fuzzing.next(work); } } } static void ui_fuzzing_do_work(rt_work_t* p) { if (ui_fuzzing_running) { ui_fuzzing_inside = true; if (ui_fuzzing.custom != null) { ui_fuzzing.custom((ui_fuzzing_t*)p); } else { ui_fuzzing.dispatch((ui_fuzzing_t*)p); } ui_fuzzing_inside = false; } else { // fuzzing has been .stop()-ed drop it } } static void ui_fuzzing_post(void) { ui_app.post(&ui_fuzzing_work.base); } static void ui_fuzzing_alt_ctrl_shift(void) { ui_fuzzing_t* w = &ui_fuzzing_work; switch (ui_fuzzing_random() % 8) { case 0: w->alt = 0; w->ctrl = 0; w->shift = 0; break; case 1: w->alt = 1; w->ctrl = 0; w->shift = 0; break; case 2: w->alt = 0; w->ctrl = 1; w->shift = 0; break; case 3: w->alt = 1; w->ctrl = 1; w->shift = 0; break; case 4: w->alt = 0; w->ctrl = 0; w->shift = 1; break; case 5: w->alt = 1; w->ctrl = 0; w->shift = 1; break; case 6: w->alt = 0; w->ctrl = 1; w->shift = 1; break; case 7: w->alt = 1; w->ctrl = 1; w->shift = 1; break; default: rt_assert(false); } } static void ui_fuzzing_character(void) { static char utf8[4 * 1024]; if (ui_fuzzing_work.utf8 == null) { fp64_t r = ui_fuzzing_random_fp64(); if (r < 0.125) { uint32_t rnd = ui_fuzzing_random(); int32_t n = (int32_t)rt_max(1, rnd % 32); ui_fuzzing_next_gibberish(n, utf8); ui_fuzzing_work.utf8 = utf8; if (ui_fuzzing_debug) { // rt_println("%s", utf8); } } else if (r < 0.25) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_chinese; } else if (r < 0.375) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_japanese; } else if (r < 0.5) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_korean; } else if (r < 0.5 + 0.125) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_emoji; } else { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_canonique; } } ui_fuzzing_post(); } static void ui_fuzzing_key(void) { struct { int32_t key; const char* name; } keys[] = { { ui.key.up, "up", }, { ui.key.down, "down", }, { ui.key.left, "left", }, { ui.key.right, "right", }, { ui.key.home, "home", }, { ui.key.end, "end", }, { ui.key.page_up, "pgup", }, { ui.key.page_down, "pgdw", }, { ui.key.insert, "insert" }, { ui.key.enter, "enter" }, { ui.key.del, "delete" }, { ui.key.back, "back" }, }; ui_fuzzing_alt_ctrl_shift(); uint32_t ix = ui_fuzzing_random() % rt_countof(keys); if (ui_fuzzing_debug) { // rt_println("key(%s)", keys[ix].name); } ui_fuzzing_work.key = keys[ix].key; ui_fuzzing_post(); } static void ui_fuzzing_mouse(void) { // mouse events only inside edit control otherwise // they will start clicking buttons around ui_view_t* v = ui_app.content; ui_fuzzing_t* w = &ui_fuzzing_work; int32_t x = ui_fuzzing_random() % v->w; int32_t y = ui_fuzzing_random() % v->h; static ui_point_t pt; pt = (ui_point_t){ x + v->x, y + v->y }; if (ui_fuzzing_random() % 2) { w->left = !w->left; } if (ui_fuzzing_random() % 2) { w->right = !w->right; } if (ui_fuzzing_debug) { // rt_println("mouse(%d,%d) %s%s", pt.x, pt.y, // w->left ? "L" : "_", w->right ? "R" : "_"); } w->pt = &pt; ui_fuzzing_post(); } static void ui_fuzzing_start(uint32_t seed) { ui_fuzzing_seed = seed | 0x1; ui_fuzzing_running = true; if (ui_fuzzing.next == null) { ui_fuzzing.next_random(&ui_fuzzing_work); } else { ui_fuzzing.next(&ui_fuzzing_work); } } static bool ui_fuzzing_is_running(void) { return ui_fuzzing_running; } static bool ui_fuzzing_from_inside(void) { return ui_fuzzing_inside; } static void ui_fuzzing_stop(void) { ui_fuzzing_running = false; } static void ui_fuzzing_next_random(ui_fuzzing_t* f) { rt_swear(f == &ui_fuzzing_work); ui_fuzzing_work = (ui_fuzzing_t){ .base = { .when = rt_clock.seconds() + 0.001, // 1ms .work = ui_fuzzing_do_work }, }; uint32_t rnd = ui_fuzzing_random() % 100; if (rnd < 80) { ui_fuzzing_character(); } else if (rnd < 90) { ui_fuzzing_key(); } else { ui_fuzzing_mouse(); } } ui_fuzzing_if ui_fuzzing = { .start = ui_fuzzing_start, .is_running = ui_fuzzing_is_running, .from_inside = ui_fuzzing_from_inside, .next_random = ui_fuzzing_next_random, .dispatch = ui_fuzzing_dispatch, .next = null, .custom = null, .stop = ui_fuzzing_stop }; // _________________________________ ui_gdi.c _________________________________ #include "rt/rt.h" #include "rt/rt_win32.h" #pragma push_macro("ui_gdi_with_hdc") #pragma push_macro("ui_gdi_hdc_with_font") static ui_brush_t ui_gdi_brush_hollow; static ui_brush_t ui_gdi_brush_color; static ui_pen_t ui_gdi_pen_hollow; static ui_region_t ui_gdi_clip; typedef struct ui_gdi_context_s { HDC hdc; // window canvas() or memory DC int32_t background_mode; int32_t stretch_mode; ui_pen_t pen; ui_font_t font; ui_color_t text_color; POINT brush_origin; ui_brush_t brush; HBITMAP texture; } ui_gdi_context_t; static ui_gdi_context_t ui_gdi_context; #define ui_gdi_hdc() (ui_gdi_context.hdc) static void ui_gdi_init(void) { ui_gdi_brush_hollow = (ui_brush_t)GetStockBrush(HOLLOW_BRUSH); ui_gdi_brush_color = (ui_brush_t)GetStockBrush(DC_BRUSH); ui_gdi_pen_hollow = (ui_pen_t)GetStockPen(NULL_PEN); } static void ui_gdi_fini(void) { if (ui_gdi_clip != null) { rt_fatal_win32err(DeleteRgn(ui_gdi_clip)); } ui_gdi_clip = null; } static ui_pen_t ui_gdi_set_pen(ui_pen_t p) { rt_not_null(p); return (ui_pen_t)SelectPen(ui_gdi_hdc(), (HPEN)p); } static ui_brush_t ui_gdi_set_brush(ui_brush_t b) { rt_not_null(b); return (ui_brush_t)SelectBrush(ui_gdi_hdc(), b); } static uint32_t ui_gdi_color_rgb(ui_color_t c) { rt_assert(ui_color_is_8bit(c)); return (COLORREF)(c & 0xFFFFFFFF); } static COLORREF ui_gdi_color_ref(ui_color_t c) { return ui_gdi.color_rgb(c); } static ui_color_t ui_gdi_set_text_color(ui_color_t c) { return SetTextColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); } static ui_font_t ui_gdi_set_font(ui_font_t f) { rt_not_null(f); return (ui_font_t)SelectFont(ui_gdi_hdc(), (HFONT)f); } static void ui_gdi_begin(ui_bitmap_t* image) { rt_swear(ui_gdi_context.hdc == null, "no nested begin()/end()"); if (image != null) { rt_swear(image->texture != null); ui_gdi_context.hdc = CreateCompatibleDC((HDC)ui_app.canvas); ui_gdi_context.texture = SelectBitmap(ui_gdi_hdc(), (HBITMAP)image->texture); } else { ui_gdi_context.hdc = (HDC)ui_app.canvas; rt_swear(ui_gdi_context.texture == null); } ui_gdi_context.font = ui_gdi_set_font(ui_app.fm.prop.normal.font); ui_gdi_context.pen = ui_gdi_set_pen(ui_gdi_pen_hollow); ui_gdi_context.brush = ui_gdi_set_brush(ui_gdi_brush_hollow); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &ui_gdi_context.brush_origin)); ui_color_t tc = ui_colors.get_color(ui_color_id_window_text); ui_gdi_context.text_color = ui_gdi_set_text_color(tc); ui_gdi_context.background_mode = SetBkMode(ui_gdi_hdc(), TRANSPARENT); ui_gdi_context.stretch_mode = SetStretchBltMode(ui_gdi_hdc(), HALFTONE); } static void ui_gdi_end(void) { rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), ui_gdi_context.brush_origin.x, ui_gdi_context.brush_origin.y, null)); ui_gdi_set_brush(ui_gdi_context.brush); ui_gdi_set_pen(ui_gdi_context.pen); ui_gdi_set_text_color(ui_gdi_context.text_color); SetBkMode(ui_gdi_hdc(), ui_gdi_context.background_mode); SetStretchBltMode(ui_gdi_hdc(), ui_gdi_context.stretch_mode); if (ui_gdi_context.hdc != (HDC)ui_app.canvas) { rt_swear(ui_gdi_context.texture != null); // 1x1 bitmap SelectBitmap(ui_gdi_context.hdc, (HBITMAP)ui_gdi_context.texture); rt_fatal_win32err(DeleteDC(ui_gdi_context.hdc)); } memset(&ui_gdi_context, 0x00, sizeof(ui_gdi_context)); } static ui_pen_t ui_gdi_set_colored_pen(ui_color_t c) { ui_pen_t p = (ui_pen_t)SelectPen(ui_gdi_hdc(), GetStockPen(DC_PEN)); SetDCPenColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); return p; } static ui_pen_t ui_gdi_create_pen(ui_color_t c, int32_t width) { rt_assert(width >= 1); ui_pen_t pen = (ui_pen_t)CreatePen(PS_SOLID, width, ui_gdi_color_ref(c)); rt_not_null(pen); return pen; } static void ui_gdi_delete_pen(ui_pen_t p) { rt_fatal_win32err(DeletePen(p)); } static ui_brush_t ui_gdi_create_brush(ui_color_t c) { return (ui_brush_t)CreateSolidBrush(ui_gdi_color_ref(c)); } static void ui_gdi_delete_brush(ui_brush_t b) { DeleteBrush((HBRUSH)b); } static ui_color_t ui_gdi_set_brush_color(ui_color_t c) { return SetDCBrushColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); } static void ui_gdi_set_clip(int32_t x, int32_t y, int32_t w, int32_t h) { if (ui_gdi_clip != null) { DeleteRgn(ui_gdi_clip); ui_gdi_clip = null; } if (w > 0 && h > 0) { ui_gdi_clip = (ui_region_t)CreateRectRgn(x, y, x + w, y + h); rt_not_null(ui_gdi_clip); } rt_fatal_if(SelectClipRgn(ui_gdi_hdc(), (HRGN)ui_gdi_clip) == ERROR); } static void ui_gdi_pixel(int32_t x, int32_t y, ui_color_t c) { rt_not_null(ui_app.canvas); rt_fatal_win32err(SetPixel(ui_gdi_hdc(), x, y, ui_gdi_color_ref(c))); } static void ui_gdi_rectangle(int32_t x, int32_t y, int32_t w, int32_t h) { rt_fatal_win32err(Rectangle(ui_gdi_hdc(), x, y, x + w, y + h)); } static void ui_gdi_line(int32_t x0, int32_t y0, int32_t x1, int32_t y1, ui_color_t c) { POINT pt; rt_fatal_win32err(MoveToEx(ui_gdi_hdc(), x0, y0, &pt)); ui_pen_t p = ui_gdi_set_colored_pen(c); rt_fatal_win32err(LineTo(ui_gdi_hdc(), x1, y1)); ui_gdi_set_pen(p); rt_fatal_win32err(MoveToEx(ui_gdi_hdc(), pt.x, pt.y, null)); } static void ui_gdi_frame(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c) { ui_brush_t b = ui_gdi_set_brush(ui_gdi_brush_hollow); ui_pen_t p = ui_gdi_set_colored_pen(c); ui_gdi_rectangle(x, y, w, h); ui_gdi_set_pen(p); ui_gdi_set_brush(b); } static void ui_gdi_rect(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t border, ui_color_t fill) { const bool tf = ui_color_is_transparent(fill); // transparent fill const bool tb = ui_color_is_transparent(border); // transparent border ui_brush_t b = tf ? ui_gdi_brush_hollow : ui_gdi_brush_color; b = ui_gdi_set_brush(b); ui_color_t c = tf ? ui_colors.transparent : ui_gdi_set_brush_color(fill); ui_pen_t p = tb ? ui_gdi_set_pen(ui_gdi_pen_hollow) : ui_gdi_set_colored_pen(border); ui_gdi_rectangle(x, y, w, h); if (!tf) { ui_gdi_set_brush_color(c); } ui_gdi_set_pen(p); ui_gdi_set_brush(b); } static void ui_gdi_fill(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c) { // rt_println("%d,%d %dx%d 0x%08X", x, y, w, h, (uint32_t)c); ui_brush_t b = ui_gdi_set_brush(ui_gdi_brush_color); c = ui_gdi_set_brush_color(c); RECT rc = { x, y, x + w, y + h }; HBRUSH brush = (HBRUSH)GetCurrentObject(ui_gdi_hdc(), OBJ_BRUSH); rt_fatal_win32err(FillRect(ui_gdi_hdc(), &rc, brush)); ui_gdi_set_brush_color(c); ui_gdi_set_brush(b); } static void ui_gdi_poly(ui_point_t* points, int32_t count, ui_color_t c) { // make sure ui_point_t and POINT have the same memory layout: static_assert(sizeof(points->x) == sizeof(((POINT*)0)->x), "ui_point_t"); static_assert(sizeof(points->y) == sizeof(((POINT*)0)->y), "ui_point_t"); static_assert(sizeof(points[0]) == sizeof(*((POINT*)0)), "ui_point_t"); rt_assert(ui_gdi_hdc() != null && count > 1); ui_pen_t pen = ui_gdi_set_colored_pen(c); rt_fatal_win32err(Polyline(ui_gdi_hdc(), (POINT*)points, count)); ui_gdi_set_pen(pen); } static void ui_gdi_circle(int32_t x, int32_t y, int32_t radius, ui_color_t border, ui_color_t fill) { rt_swear(!ui_color_is_transparent(border) || ui_color_is_transparent(fill)); // Win32 GDI even radius drawing looks ugly squarish and asymmetrical. rt_swear(radius % 2 == 1, "radius: %d must be odd"); if (ui_color_is_transparent(border)) { rt_assert(!ui_color_is_transparent(fill)); border = fill; } rt_assert(!ui_color_is_transparent(border)); const bool tf = ui_color_is_transparent(fill); // transparent fill ui_brush_t brush = tf ? ui_gdi_set_brush(ui_gdi_brush_hollow) : ui_gdi_set_brush(ui_gdi_brush_color); ui_color_t c = tf ? ui_colors.transparent : ui_gdi_set_brush_color(fill); ui_pen_t p = ui_gdi_set_colored_pen(border); HDC hdc = ui_gdi_context.hdc; int32_t l = x - radius; int32_t t = y - radius; int32_t r = x + radius + 1; int32_t b = y + radius + 1; Ellipse(hdc, l, t, r, b); // SetPixel(hdc, x, y, RGB(255, 255, 255)); ui_gdi_set_pen(p); if (!tf) { ui_gdi_set_brush_color(c); } ui_gdi_set_brush(brush); } static void ui_gdi_fill_rounded(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t fill) { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi_circle(x + radius, y + radius, radius, fill, fill); ui_gdi_circle(r - radius, y + radius, radius, fill, fill); ui_gdi_circle(x + radius, b - radius, radius, fill, fill); ui_gdi_circle(r - radius, b - radius, radius, fill, fill); // rectangles ui_gdi.fill(x + radius, y, w - radius * 2, h, fill); r = x + w - radius; ui_gdi.fill(x, y + radius, radius, h - radius * 2, fill); ui_gdi.fill(r, y + radius, radius, h - radius * 2, fill); } static void ui_gdi_rounded_border(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t border) { { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi.set_clip(x, y, radius + 1, radius + 1); ui_gdi_circle(x + radius, y + radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(r - radius, y, radius + 1, radius + 1); ui_gdi_circle(r - radius, y + radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(x, b - radius, radius + 1, radius + 1); ui_gdi_circle(x + radius, b - radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(r - radius, b - radius, radius + 1, radius + 1); ui_gdi_circle(r - radius, b - radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(0, 0, 0, 0); } { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi.line(x + radius, y, r - radius + 1, y, border); ui_gdi.line(x + radius, b, r - radius + 1, b, border); ui_gdi.line(x - 1, y + radius, x - 1, b - radius + 1, border); ui_gdi.line(r + 1, y + radius, r + 1, b - radius + 1, border); } } static void ui_gdi_rounded(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t border, ui_color_t fill) { rt_swear(!ui_color_is_transparent(border) || !ui_color_is_transparent(fill)); if (!ui_color_is_transparent(fill)) { ui_gdi_fill_rounded(x, y, w, h, radius, fill); } if (!ui_color_is_transparent(border)) { ui_gdi_rounded_border(x, y, w, h, radius, border); } } static void ui_gdi_gradient(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t rgba_from, ui_color_t rgba_to, bool vertical) { TRIVERTEX vertex[2] = {0}; vertex[0].x = x; vertex[0].y = y; // TODO: colors: vertex[0].Red = (COLOR16)(((rgba_from >> 0) & 0xFF) << 8); vertex[0].Green = (COLOR16)(((rgba_from >> 8) & 0xFF) << 8); vertex[0].Blue = (COLOR16)(((rgba_from >> 16) & 0xFF) << 8); vertex[0].Alpha = (COLOR16)(((rgba_from >> 24) & 0xFF) << 8); vertex[1].x = x + w; vertex[1].y = y + h; vertex[1].Red = (COLOR16)(((rgba_to >> 0) & 0xFF) << 8); vertex[1].Green = (COLOR16)(((rgba_to >> 8) & 0xFF) << 8); vertex[1].Blue = (COLOR16)(((rgba_to >> 16) & 0xFF) << 8); vertex[1].Alpha = (COLOR16)(((rgba_to >> 24) & 0xFF) << 8); GRADIENT_RECT gRect = {0, 1}; const uint32_t mode = vertical ? GRADIENT_FILL_RECT_V : GRADIENT_FILL_RECT_H; GradientFill(ui_gdi_hdc(), vertex, 2, &gRect, 1, mode); } static BITMAPINFO* ui_gdi_greyscale_bitmap_info(void) { typedef struct bitmap_rgb_s { BITMAPINFO bi; RGBQUAD rgb[256]; } bitmap_rgb_t; static bitmap_rgb_t storage; // for gs palette static BITMAPINFO* bi = &storage.bi; BITMAPINFOHEADER* bih = &bi->bmiHeader; if (bih->biSize == 0) { // once bih->biSize = sizeof(BITMAPINFOHEADER); for (int32_t i = 0; i < 256; i++) { RGBQUAD* q = &bi->bmiColors[i]; q->rgbReserved = 0; q->rgbBlue = q->rgbGreen = q->rgbRed = (uint8_t)i; } bih->biPlanes = 1; bih->biBitCount = 8; bih->biCompression = BI_RGB; bih->biClrUsed = 256; bih->biClrImportant = 256; } return bi; } static void ui_gdi_pixels(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, int32_t bpp, const uint8_t* pixels) { if (bpp == 1) { ui_gdi.greyscale(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else if (bpp == 3) { ui_gdi.bgr(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else if (bpp == 4) { ui_gdi.bgrx(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else { rt_fatal("bpp: %d not {1, 3, 4}", bpp); } } static void ui_gdi_greyscale(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFO *bi = ui_gdi_greyscale_bitmap_info(); // global! not thread safe BITMAPINFOHEADER* bih = &bi->bmiHeader; bih->biWidth = width; bih->biHeight = -height; // top down image bih->biSizeImage = (DWORD)(iw * abs(ih)); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static BITMAPINFOHEADER ui_gdi_bgrx_init_bi(int32_t w, int32_t h, int32_t bpp) { rt_assert(w > 0 && h >= 0); // h cannot be negative? BITMAPINFOHEADER bi = { .biSize = sizeof(BITMAPINFOHEADER), .biPlanes = 1, .biBitCount = (uint16_t)(bpp * 8), .biCompression = BI_RGB, .biWidth = w, .biHeight = -h, // top down image .biSizeImage = (DWORD)(w * abs(h) * bpp), .biClrUsed = 0, .biClrImportant = 0 }; return bi; } // bgr(width) assumes strides are padded and rounded up to 4 bytes // if this is not the case use ui_gdi.bitmap_init() that will unpack // and align scanlines prior to draw static void ui_gdi_bgr(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width * 3 + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFOHEADER bi = ui_gdi_bgrx_init_bi(width, height, 3); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, (BITMAPINFO*)&bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static void ui_gdi_bgrx(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width * 4 + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFOHEADER bi = ui_gdi_bgrx_init_bi(width, height, 4); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, (BITMAPINFO*)&bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static BITMAPINFO* ui_gdi_init_bitmap_info(int32_t w, int32_t h, int32_t bpp, BITMAPINFO* bi) { rt_assert(w > 0 && h >= 0); // h cannot be negative? bi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bi->bmiHeader.biWidth = w; bi->bmiHeader.biHeight = -h; // top down image bi->bmiHeader.biPlanes = 1; bi->bmiHeader.biBitCount = (uint16_t)(bpp * 8); bi->bmiHeader.biCompression = BI_RGB; bi->bmiHeader.biSizeImage = (DWORD)(w * abs(h) * bpp); return bi; } static void ui_gdi_create_dib_section(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp) { rt_fatal_if(image->texture != null, "bitmap_dispose() not called?"); // not using GetWindowDC(ui_app.window) will allow to initialize images // before window is created HDC c = CreateCompatibleDC(null); // GetWindowDC(ui_app.window); BITMAPINFO local = { {sizeof(BITMAPINFOHEADER)} }; BITMAPINFO* bi = bpp == 1 ? ui_gdi_greyscale_bitmap_info() : &local; image->texture = (ui_texture_t)CreateDIBSection(c, ui_gdi_init_bitmap_info(w, h, bpp, bi), DIB_RGB_COLORS, &image->pixels, null, 0x0 ); rt_fatal_if(image->texture == null || image->pixels == null); rt_fatal_win32err(DeleteDC(c)); } static void ui_gdi_bitmap_init_rgbx(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels) { bool swapped = bpp < 0; bpp = abs(bpp); rt_fatal_if(bpp != 4, "bpp: %d", bpp); ui_gdi_create_dib_section(image, w, h, bpp); const int32_t stride = (w * bpp + 3) & ~0x3; uint8_t* scanline = image->pixels; const uint8_t* rgbx = pixels; if (!swapped) { for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { bgra[0] = rgbx[2]; bgra[1] = rgbx[1]; bgra[2] = rgbx[0]; bgra[3] = 0xFF; bgra += 4; rgbx += 4; } pixels += w * 4; scanline += stride; } } else { for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { bgra[0] = rgbx[0]; bgra[1] = rgbx[1]; bgra[2] = rgbx[2]; bgra[3] = 0xFF; bgra += 4; rgbx += 4; } pixels += w * 4; scanline += stride; } } image->w = w; image->h = h; image->bpp = bpp; image->stride = stride; } static void ui_gdi_bitmap_init(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels) { bool swapped = bpp < 0; bpp = abs(bpp); rt_fatal_if(bpp < 0 || bpp == 2 || bpp > 4, "bpp=%d not {1, 3, 4}", bpp); ui_gdi_create_dib_section(image, w, h, bpp); // Win32 bitmaps stride is rounded up to 4 bytes const int32_t stride = (w * bpp + 3) & ~0x3; uint8_t* scanline = image->pixels; if (bpp == 1) { for (int32_t y = 0; y < h; y++) { memcpy(scanline, pixels, (size_t)w); pixels += w; scanline += stride; } } else if (bpp == 3 && !swapped) { const uint8_t* rgb = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgr = scanline; for (int32_t x = 0; x < w; x++) { bgr[0] = rgb[2]; bgr[1] = rgb[1]; bgr[2] = rgb[0]; bgr += 3; rgb += 3; } pixels += w * bpp; scanline += stride; } } else if (bpp == 3 && swapped) { const uint8_t* rgb = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgr = scanline; for (int32_t x = 0; x < w; x++) { bgr[0] = rgb[0]; bgr[1] = rgb[1]; bgr[2] = rgb[2]; bgr += 3; rgb += 3; } pixels += w * bpp; scanline += stride; } } else if (bpp == 4 && !swapped) { // premultiply alpha, see: // https://stackoverflow.com/questions/24595717/alphablend-generating-incorrect-colors const uint8_t* rgba = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { int32_t alpha = rgba[3]; bgra[0] = (uint8_t)(rgba[2] * alpha / 255); bgra[1] = (uint8_t)(rgba[1] * alpha / 255); bgra[2] = (uint8_t)(rgba[0] * alpha / 255); bgra[3] = rgba[3]; bgra += 4; rgba += 4; } pixels += w * 4; scanline += stride; } } else if (bpp == 4 && swapped) { // premultiply alpha, see: // https://stackoverflow.com/questions/24595717/alphablend-generating-incorrect-colors const uint8_t* rgba = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { int32_t alpha = rgba[3]; bgra[0] = (uint8_t)(rgba[0] * alpha / 255); bgra[1] = (uint8_t)(rgba[1] * alpha / 255); bgra[2] = (uint8_t)(rgba[2] * alpha / 255); bgra[3] = rgba[3]; bgra += 4; rgba += 4; } pixels += w * 4; scanline += stride; } } image->w = w; image->h = h; image->bpp = bpp; image->stride = stride; } static void ui_gdi_alpha(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* image, fp64_t alpha) { rt_assert(image->bpp > 0); rt_assert(0 <= alpha && alpha <= 1); rt_not_null(ui_gdi_hdc()); HDC c = CreateCompatibleDC(ui_gdi_hdc()); rt_not_null(c); HBITMAP zero1x1 = SelectBitmap((HDC)c, (HBITMAP)image->texture); BLENDFUNCTION bf = { 0 }; bf.SourceConstantAlpha = (uint8_t)(0xFF * alpha + 0.49); if (image->bpp == 4) { bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = AC_SRC_ALPHA; } else { bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = 0; } rt_assert(0 <= ix && ix < image->w && 0 <= iy && iy < image->h); rt_assert(ix + iw <= image->w && iy + ih <= image->h); rt_fatal_win32err(AlphaBlend(ui_gdi_hdc(), dx, dy, dw, dh, c, ix, iy, iw, ih, bf)); SelectBitmap((HDC)c, zero1x1); rt_fatal_win32err(DeleteDC(c)); } static void ui_gdi_bitmap(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* image) { rt_assert(image->bpp == 1 || image->bpp == 3 || image->bpp == 4); rt_assert(0 <= ix && ix < image->w && 0 <= iy && iy < image->h); rt_assert(ix + iw <= image->w && iy + ih <= image->h); rt_not_null(ui_gdi_hdc()); if (image->bpp == 1) { // StretchBlt() is bad for greyscale BITMAPINFO* bi = ui_gdi_greyscale_bitmap_info(); BITMAPINFO* info = ui_gdi_init_bitmap_info(image->w, image->h, 1, bi); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, image->pixels, info, DIB_RGB_COLORS, SRCCOPY) == 0); } else { HDC c = CreateCompatibleDC(ui_gdi_hdc()); rt_not_null(c); HBITMAP zero1x1 = SelectBitmap(c, image->texture); rt_fatal_win32err(StretchBlt(ui_gdi_hdc(), dx, dy, dw, dh, c, ix, iy, iw, ih, SRCCOPY)); SelectBitmap(c, zero1x1); rt_fatal_win32err(DeleteDC(c)); } } static void ui_gdi_icon(int32_t x, int32_t y, int32_t w, int32_t h, ui_icon_t icon) { DrawIconEx(ui_gdi_hdc(), x, y, (HICON)icon, w, h, 0, NULL, DI_NORMAL | DI_COMPAT); } static void ui_gdi_cleartype(bool on) { enum { spif = SPIF_UPDATEINIFILE | SPIF_SENDCHANGE }; rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHING, true, 0, spif)); uintptr_t s = on ? FE_FONTSMOOTHINGCLEARTYPE : FE_FONTSMOOTHINGSTANDARD; rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHINGTYPE, 0, (void*)s, spif)); } static void ui_gdi_font_smoothing_contrast(int32_t c) { rt_fatal_if(!(c == -1 || 1000 <= c && c <= 2200), "contrast: %d", c); if (c == -1) { c = 1400; } rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHINGCONTRAST, 0, (void*)(uintptr_t)c, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE)); } rt_static_assertion(ui_gdi_font_quality_default == DEFAULT_QUALITY); rt_static_assertion(ui_gdi_font_quality_draft == DRAFT_QUALITY); rt_static_assertion(ui_gdi_font_quality_proof == PROOF_QUALITY); rt_static_assertion(ui_gdi_font_quality_nonantialiased == NONANTIALIASED_QUALITY); rt_static_assertion(ui_gdi_font_quality_antialiased == ANTIALIASED_QUALITY); rt_static_assertion(ui_gdi_font_quality_cleartype == CLEARTYPE_QUALITY); rt_static_assertion(ui_gdi_font_quality_cleartype_natural == CLEARTYPE_NATURAL_QUALITY); static ui_font_t ui_gdi_create_font(const char* family, int32_t h, int32_t q) { rt_assert(h > 0); LOGFONTA lf = {0}; int32_t n = GetObjectA(ui_app.fm.prop.normal.font, sizeof(lf), &lf); rt_fatal_if(n != (int32_t)sizeof(lf)); lf.lfHeight = -h; rt_str_printf(lf.lfFaceName, "%s", family); if (ui_gdi_font_quality_default <= q && q <= ui_gdi_font_quality_cleartype_natural) { lf.lfQuality = (uint8_t)q; } else { rt_fatal_if(q != -1, "use -1 for do not care quality"); } return (ui_font_t)CreateFontIndirectA(&lf); } static ui_font_t ui_gdi_font(ui_font_t f, int32_t h, int32_t q) { rt_assert(f != null && h > 0); LOGFONTA lf = {0}; int32_t n = GetObjectA(f, sizeof(lf), &lf); rt_fatal_if(n != (int32_t)sizeof(lf)); lf.lfHeight = -h; if (ui_gdi_font_quality_default <= q && q <= ui_gdi_font_quality_cleartype_natural) { lf.lfQuality = (uint8_t)q; } else { rt_fatal_if(q != -1, "use -1 for do not care quality"); } return (ui_font_t)CreateFontIndirectA(&lf); } static void ui_gdi_delete_font(ui_font_t f) { rt_fatal_win32err(DeleteFont(f)); } // guaranteed to return dc != null even if not painting static HDC ui_gdi_get_dc(void) { rt_not_null(ui_app.window); HDC hdc = ui_gdi_hdc() != null ? ui_gdi_hdc() : GetDC((HWND)ui_app.window); rt_not_null(hdc); return hdc; } static void ui_gdi_release_dc(HDC hdc) { if (ui_gdi_hdc() == null) { ReleaseDC((HWND)ui_app.window, hdc); } } #define ui_gdi_with_hdc(code) do { \ HDC hdc = ui_gdi_get_dc(); \ code \ ui_gdi_release_dc(hdc); \ } while (0) #define ui_gdi_hdc_with_font(f, ...) do { \ rt_not_null(f); \ HDC hdc = ui_gdi_get_dc(); \ HFONT font_ = SelectFont(hdc, (HFONT)f); \ { __VA_ARGS__ } \ SelectFont(hdc, font_); \ ui_gdi_release_dc(hdc); \ } while (0) static void ui_gdi_dump_hdc_fm(HDC hdc) { // https://en.wikipedia.org/wiki/Quad_(typography) // https://learn.microsoft.com/en-us/windows/win32/gdi/string-widths-and-heights // https://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font // Amazingly same since Windows 3.1 1992 // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-textmetrica // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-outlinetextmetrica TEXTMETRICA tm = {0}; rt_fatal_win32err(GetTextMetricsA(hdc, &tm)); char pitch[64] = { 0 }; if (tm.tmPitchAndFamily & TMPF_FIXED_PITCH) { strcat(pitch, "FIXED_PITCH "); } if (tm.tmPitchAndFamily & TMPF_VECTOR) { strcat(pitch, "VECTOR "); } if (tm.tmPitchAndFamily & TMPF_DEVICE) { strcat(pitch, "DEVICE "); } if (tm.tmPitchAndFamily & TMPF_TRUETYPE) { strcat(pitch, "TRUETYPE "); } rt_println("tm: .pitch_and_family: %s", pitch); rt_println(".height : %2d .ascent (baseline) : %2d .descent: %2d", tm.tmHeight, tm.tmAscent, tm.tmDescent); rt_println(".internal_leading : %2d .external_leading : %2d .ave_char_width: %2d", tm.tmInternalLeading, tm.tmExternalLeading, tm.tmAveCharWidth); rt_println(".max_char_width : %2d .weight : %2d .overhang: %2d", tm.tmMaxCharWidth, tm.tmWeight, tm.tmOverhang); rt_println(".digitized_aspect_x: %2d .digitized_aspect_y: %2d", tm.tmDigitizedAspectX, tm.tmDigitizedAspectY); rt_swear(tm.tmPitchAndFamily & TMPF_TRUETYPE); OUTLINETEXTMETRICA otm = { .otmSize = sizeof(OUTLINETEXTMETRICA) }; uint32_t bytes = GetOutlineTextMetricsA(hdc, otm.otmSize, &otm); rt_swear(bytes == sizeof(OUTLINETEXTMETRICA)); // unsupported XHeight CapEmHeight // ignored: MacDescent, MacLineGap, EMSquare, ItalicAngle // CharSlopeRise, CharSlopeRun, ItalicAngle rt_println("otm: .Ascent : %2d .Descent : %2d", otm.otmAscent, otm.otmDescent); rt_println(".otmLineGap : %2u", otm.otmLineGap); rt_println(".FontBox.ltrb : %d,%d %2d,%2d", otm.otmrcFontBox.left, otm.otmrcFontBox.top, otm.otmrcFontBox.right, otm.otmrcFontBox.bottom); rt_println(".MinimumPPEM : %2u (minimum height in pixels)", otm.otmusMinimumPPEM); rt_println(".SubscriptOffset : %d,%d .SubscriptSize.x : %dx%d", otm.otmptSubscriptOffset.x, otm.otmptSubscriptOffset.y, otm.otmptSubscriptSize.x, otm.otmptSubscriptSize.y); rt_println(".SuperscriptOffset : %d,%d .SuperscriptSize.x : %dx%d", otm.otmptSuperscriptOffset.x, otm.otmptSuperscriptOffset.y, otm.otmptSuperscriptSize.x, otm.otmptSuperscriptSize.y); rt_println(".UnderscoreSize : %2d .UnderscorePosition: %2d", otm.otmsUnderscoreSize, otm.otmsUnderscorePosition); rt_println(".StrikeoutSize : %2u .StrikeoutPosition : %2d ", otm.otmsStrikeoutSize, otm.otmsStrikeoutPosition); int32_t h = otm.otmAscent + abs(tm.tmDescent); // without diacritical space above fp32_t pts = (h * 72.0f) / GetDeviceCaps(hdc, LOGPIXELSY); rt_println("height: %.1fpt", pts); } static void ui_gdi_dump_fm(ui_font_t f) { rt_not_null(f); ui_gdi_hdc_with_font(f, { ui_gdi_dump_hdc_fm(hdc); }); } static void ui_gdi_get_fm(HDC hdc, ui_fm_t* fm) { TEXTMETRICA tm = {0}; rt_fatal_win32err(GetTextMetricsA(hdc, &tm)); rt_swear(tm.tmPitchAndFamily & TMPF_TRUETYPE); OUTLINETEXTMETRICA otm = { .otmSize = sizeof(OUTLINETEXTMETRICA) }; uint32_t bytes = GetOutlineTextMetricsA(hdc, otm.otmSize, &otm); rt_swear(bytes == sizeof(OUTLINETEXTMETRICA)); // "tm.tmAscent" The ascent (units above the base line) of characters // and actually is "baseline" in other terminology // "otm.otmAscent" The maximum distance characters in this font extend // above the base line. This is the typographic ascent for the font. // otm.otmEMSquare usually is 2048 which is size of rasterizer fm->height = tm.tmHeight; fm->baseline = tm.tmAscent; fm->ascent = otm.otmAscent; fm->descent = tm.tmDescent; fm->baseline = tm.tmAscent; fm->x_height = otm.otmsXHeight; fm->cap_em_height = otm.otmsCapEmHeight; fm->internal_leading = tm.tmInternalLeading; fm->external_leading = tm.tmExternalLeading; fm->average_char_width = tm.tmAveCharWidth; fm->max_char_width = tm.tmMaxCharWidth; fm->line_gap = otm.otmLineGap; fm->subscript.w = otm.otmptSubscriptSize.x; fm->subscript.h = otm.otmptSubscriptSize.y; fm->subscript_offset.x = otm.otmptSubscriptOffset.x; fm->subscript_offset.y = otm.otmptSubscriptOffset.y; fm->superscript.w = otm.otmptSuperscriptSize.x; fm->superscript.h = otm.otmptSuperscriptSize.y; fm->superscript_offset.x = otm.otmptSuperscriptOffset.x; fm->superscript_offset.y = otm.otmptSuperscriptOffset.y; fm->underscore = otm.otmsUnderscoreSize; fm->underscore_position = otm.otmsUnderscorePosition; fm->strike_through = otm.otmsStrikeoutSize; fm->strike_through_position = otm.otmsStrikeoutPosition; fm->design_units_per_em = (int)otm.otmEMSquare; fm->box = (ui_rect_t){ otm.otmrcFontBox.left, otm.otmrcFontBox.top, otm.otmrcFontBox.right - otm.otmrcFontBox.left, otm.otmrcFontBox.top - otm.otmrcFontBox.bottom // inverted }; // otm.Descent: The maximum distance characters in this font extend below // the base line. This is the typographic descent for the font. // Negative from the bottom (font.height) // tm.Descent: The descent (units below the base line) of characters. // Positive from the baseline down rt_assert(tm.tmDescent >= 0 && otm.otmDescent <= 0 && -otm.otmDescent <= tm.tmDescent, "tm.tmDescent: %d otm.otmDescent: %d", tm.tmDescent, otm.otmDescent); // "Mac" typography is ignored because it's usefulness is unclear. // Italic angle/slant/run is ignored because at the moment edit // view implementation does not support italics and thus does not // need it. Easy to add if necessary. } static void ui_gdi_update_fm(ui_fm_t* fm, ui_font_t f) { rt_not_null(f); SIZE em = {0, 0}; // "m" *fm = (ui_fm_t){ .font = f }; // ui_gdi.dump_fm(f); ui_gdi_hdc_with_font(f, { ui_gdi_get_fm(hdc, fm); // rt_glyph_nbsp and "M" have the same result rt_fatal_win32err(GetTextExtentPoint32A(hdc, "m", 1, &em)); SIZE vl = {0}; // "|" Vertical Line https://www.compart.com/en/unicode/U+007C rt_fatal_win32err(GetTextExtentPoint32A(hdc, "|", 1, &vl)); SIZE e3 = {0}; // Three-Em Dash rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_three_em_dash, 1, &e3)); fm->mono = em.cx == vl.cx && vl.cx == e3.cx; // rt_println("vl: %d %d", vl.cx, vl.cy); // rt_println("e3: %d %d", e3.cx, e3.cy); // rt_println("fm->mono: %d height: %d baseline: %d ascent: %d descent: %d", // fm->mono, fm->height, fm->baseline, fm->ascent, fm->descent); }); rt_assert(fm->baseline <= fm->height); fm->em = (ui_wh_t){ .w = fm->height, .h = fm->height }; // rt_println("fm.em: %dx%d", fm->em.w, fm->em.h); } static int32_t ui_gdi_draw_utf16(ui_font_t font, const char* s, int32_t n, RECT* r, uint32_t format) { // ~70 microsecond Core i-7 3667U 2.0 GHz (2012) // if font == null, draws on HDC with selected font if (0) { HDC hdc = ui_gdi_hdc(); if (hdc != null) { SIZE em = {0, 0}; // "M" rt_fatal_win32err(GetTextExtentPoint32A(hdc, "M", 1, &em)); rt_println("em: %d %d", em.cx, em.cy); rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_em_quad, 1, &em)); rt_println("em: %d %d", em.cx, em.cy); SIZE vl = {0}; // "|" Vertical Line https://www.compart.com/en/unicode/U+007C SIZE e3 = {0}; // Three-Em Dash rt_fatal_win32err(GetTextExtentPoint32A(hdc, "|", 1, &vl)); rt_println("vl: %d %d", vl.cx, vl.cy); rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_three_em_dash, 1, &e3)); rt_println("e3: %d %d", e3.cx, e3.cy); } } int32_t count = rt_str.utf16_chars(s, -1); rt_assert(0 < count && count < 4096, "be reasonable count: %d?", count); uint16_t ws[4096]; rt_swear(count <= rt_countof(ws), "find another way to draw!"); rt_str.utf8to16(ws, count, s, -1); int32_t h = 0; // return value is the height of the text if (font != null) { ui_gdi_hdc_with_font(font, { h = DrawTextW(hdc, ws, n, r, format); }); } else { // with already selected font ui_gdi_with_hdc({ h = DrawTextW(hdc, ws, n, r, format); }); } return h; } typedef struct { // draw text parameters const ui_fm_t* fm; const char* format; // format string va_list va; RECT rc; uint32_t flags; // flags: // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtextw // DT_CALCRECT DT_NOCLIP useful for measure // DT_END_ELLIPSIS useful for clipping // DT_LEFT, DT_RIGHT, DT_CENTER useful for paragraphs // DT_WORDBREAK is not good (GDI does not break nicely) // DT_BOTTOM, DT_VCENTER limited usability in weird cases (layout is better) // DT_NOPREFIX not to draw underline at "&Keyboard shortcuts // DT_SINGLELINE versus multiline } ui_gdi_dtp_t; static void ui_gdi_text_draw(ui_gdi_dtp_t* p) { rt_not_null(p); char text[4096]; // expected to be enough for single text draw text[0] = 0; rt_str.format_va(text, rt_countof(text), p->format, p->va); text[rt_countof(text) - 1] = 0; int32_t k = (int32_t)rt_str.len(text); if (k > 0) { rt_swear(k > 0 && k < rt_countof(text), "k=%d n=%d fmt=%s", k, p->format); // rectangle is always calculated - it makes draw text // much slower but UI layer is mostly uses bitmap caching: if ((p->flags & DT_CALCRECT) == 0) { // no actual drawing just calculate rectangle bool b = ui_gdi_draw_utf16(p->fm->font, text, -1, &p->rc, p->flags | DT_CALCRECT); rt_assert(b, "text_utf16(%s) failed", text); (void)b; } bool b = ui_gdi_draw_utf16(p->fm->font, text, -1, &p->rc, p->flags); rt_assert(b, "text_utf16(%s) failed", text); (void)b; } else { p->rc.right = p->rc.left; p->rc.bottom = p->rc.top + p->fm->height; } } enum { sl_draw = DT_LEFT|DT_NOCLIP|DT_SINGLELINE|DT_NOCLIP, sl_measure = sl_draw|DT_CALCRECT, ml_draw_break = DT_LEFT|DT_NOPREFIX|DT_NOCLIP|DT_NOFULLWIDTHCHARBREAK| DT_WORDBREAK, ml_measure_break = ml_draw_break|DT_CALCRECT, ml_draw = DT_LEFT|DT_NOPREFIX|DT_NOCLIP|DT_NOFULLWIDTHCHARBREAK, ml_measure = ml_draw|DT_CALCRECT }; static ui_wh_t ui_gdi_text_with_flags(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va, uint32_t flags) { const int32_t right = w == 0 ? 0 : x + w; ui_gdi_dtp_t p = { .fm = ta->fm, .format = format, .va = va, .rc = {.left = x, .top = y, .right = right, .bottom = 0 }, .flags = flags }; ui_color_t c = ta->color; if (!ta->measure) { if (ui_color_is_undefined(c)) { rt_swear(ta->color_id > 0); c = ui_colors.get_color(ta->color_id); } else { rt_swear(ta->color_id == 0); } c = ui_gdi_set_text_color(c); } ui_gdi_text_draw(&p); if (!ta->measure) { ui_gdi_set_text_color(c); } // restore color return (ui_wh_t){ p.rc.right - p.rc.left, p.rc.bottom - p.rc.top }; } static ui_wh_t ui_gdi_text_va(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, va_list va) { const uint32_t flags = sl_draw | (ta->measure ? sl_measure : 0); return ui_gdi_text_with_flags(ta, x, y, 0, format, va, flags); } static ui_wh_t ui_gdi_text(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, ...) { const uint32_t flags = sl_draw | (ta->measure ? sl_measure : 0); va_list va; va_start(va, format); ui_wh_t wh = ui_gdi_text_with_flags(ta, x, y, 0, format, va, flags); va_end(va); return wh; } static ui_wh_t ui_gdi_multiline_va(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va) { const uint32_t flags = ta->measure ? (w <= 0 ? ml_measure : ml_measure_break) : (w <= 0 ? ml_draw : ml_draw_break); return ui_gdi_text_with_flags(ta, x, y, w, format, va, flags); } static ui_wh_t ui_gdi_multiline(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, ...) { va_list va; va_start(va, format); ui_wh_t wh = ui_gdi_multiline_va(ta, x, y, w, format, va); va_end(va); return wh; } static ui_wh_t ui_gdi_glyphs_placement(const ui_gdi_ta_t* ta, const char* utf8, int32_t bytes, int32_t x[], int32_t glyphs) { rt_swear(bytes >= 0 && glyphs >= 0 && glyphs <= bytes); rt_assert(false, "Does not work for Tamil simplest utf8: \xe0\xae\x9a utf16: 0x0B9A"); x[0] = 0; ui_wh_t wh = { .w = 0, .h = 0 }; if (bytes > 0) { const int32_t chars = rt_str.utf16_chars(utf8, bytes); uint16_t* utf16 = rt_stackalloc((chars + 1) * sizeof(uint16_t)); uint16_t* output = rt_stackalloc((chars + 1) * sizeof(uint16_t)); const errno_t r = rt_str.utf8to16(utf16, chars, utf8, bytes); rt_swear(r == 0); // TODO: remove #if 1 char str[16 * 1024] = {0}; char hex[16 * 1024] = {0}; for (int i = 0; i < chars; i++) { rt_str_printf(hex, "%04X ", utf16[i]); strcat(str, hex); } rt_println("%.*s %s %p bytes:%d glyphs:%d font:%p hdc:%p", bytes, utf8, str, utf8, bytes, glyphs, ta->fm->font, ui_gdi_context.hdc); #endif GCP_RESULTSW gcp = { .lStructSize = sizeof(GCP_RESULTSW), .lpOutString = output, .nGlyphs = glyphs }; gcp.lpDx = (int*)rt_stackalloc((chars + 1) * sizeof(int)); DWORD n = 0; const int mx = INT32_MAX; // max extent const DWORD f = GCP_MAXEXTENT; // |GCP_GLYPHSHAPE|GCP_DIACRITIC|GCP_LIGATE if (ta->fm->font != null) { ui_gdi_hdc_with_font(ta->fm->font, { n = GetCharacterPlacementW(hdc, utf16, chars, mx, &gcp, f); }); } else { // with already selected font ui_gdi_with_hdc({ n = GetCharacterPlacementW(hdc, utf16, chars, mx, &gcp, f); }); } wh = (ui_wh_t){ .w = LOWORD(n), .h = HIWORD(n) }; if (n != 0) { // IS_HIGH_SURROGATE(wch) // IS_LOW_SURROGATE(wch) // IS_SURROGATE_PAIR(hs, ls) int32_t i = 0; int32_t k = 1; while (i < chars) { x[k] = x[k - 1] + gcp.lpDx[i]; // rt_println("%d", x[i]); k++; if (i < chars - 1 && rt_str.utf16_is_high_surrogate(utf16[i]) && rt_str.utf16_is_low_surrogate(utf16[i + 1])) { i += 2; } else { i++; } } rt_assert(k == glyphs + 1); } else { // rt_assert(false, "GetCharacterPlacementW() failed"); rt_println("GetCharacterPlacementW() failed"); } } return wh; } // to enable load_bitmap() function // 1. Add // curl.exe https://raw.githubusercontent.com/nothings/stb/master/stb_bitmap.h stb_bitmap.h // to the project precompile build step // 2. After // #define ui_implementation // include "ui/ui.h" // add // #define STBI_ASSERT(x) assert(x) // #define STB_bitmap_IMPLEMENTATION // #include "stb_bitmap.h" static uint8_t* ui_gdi_load_bitmap(const void* data, int32_t bytes, int* w, int* h, int* bytes_per_pixel, int32_t preferred_bytes_per_pixel) { #ifdef STBI_VERSION return stbi_load_from_memory((uint8_t const*)data, bytes, w, h, bytes_per_pixel, preferred_bytes_per_pixel); #else // see instructions above (void)data; (void)bytes; (void)data; (void)w; (void)h; (void)bytes_per_pixel; (void)preferred_bytes_per_pixel; rt_fatal_if(true, "curl.exe --silent --fail --create-dirs " "https://raw.githubusercontent.com/nothings/stb/master/stb_bitmap.h " "--output ext/stb_bitmap.h"); return null; #endif } static void ui_gdi_bitmap_dispose(ui_bitmap_t* image) { rt_fatal_win32err(DeleteBitmap(image->texture)); memset(image, 0, sizeof(ui_bitmap_t)); } ui_gdi_if ui_gdi = { .ta = { .prop = { .normal = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.normal, .measure = false }, .title = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.title, .measure = false }, .rubric = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.rubric, .measure = false }, .H1 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H1, .measure = false }, .H2 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H2, .measure = false }, .H3 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H3, .measure = false } }, .mono = { .normal = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.normal, .measure = false }, .title = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.title, .measure = false }, .rubric = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.rubric, .measure = false }, .H1 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H1, .measure = false }, .H2 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H2, .measure = false }, .H3 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H3, .measure = false } }, }, .init = ui_gdi_init, .begin = ui_gdi_begin, .end = ui_gdi_end, .color_rgb = ui_gdi_color_rgb, .bitmap_init = ui_gdi_bitmap_init, .bitmap_init_rgbx = ui_gdi_bitmap_init_rgbx, .bitmap_dispose = ui_gdi_bitmap_dispose, .alpha = ui_gdi_alpha, .bitmap = ui_gdi_bitmap, .icon = ui_gdi_icon, .set_clip = ui_gdi_set_clip, .pixel = ui_gdi_pixel, .line = ui_gdi_line, .frame = ui_gdi_frame, .rect = ui_gdi_rect, .fill = ui_gdi_fill, .poly = ui_gdi_poly, .circle = ui_gdi_circle, .rounded = ui_gdi_rounded, .gradient = ui_gdi_gradient, .pixels = ui_gdi_pixels, .greyscale = ui_gdi_greyscale, .bgr = ui_gdi_bgr, .bgrx = ui_gdi_bgrx, .cleartype = ui_gdi_cleartype, .font_smoothing_contrast = ui_gdi_font_smoothing_contrast, .create_font = ui_gdi_create_font, .font = ui_gdi_font, .delete_font = ui_gdi_delete_font, .dump_fm = ui_gdi_dump_fm, .update_fm = ui_gdi_update_fm, .text_va = ui_gdi_text_va, .text = ui_gdi_text, .multiline_va = ui_gdi_multiline_va, .multiline = ui_gdi_multiline, .glyphs_placement = ui_gdi_glyphs_placement, .fini = ui_gdi_fini }; #pragma pop_macro("ui_gdi_hdc_with_font") #pragma pop_macro("ui_gdi_with_hdc") // ________________________________ ui_image.c ________________________________ #include "rt/rt.h" static fp64_t ui_image_scale_of(int32_t nominator, int32_t denominator) { const int32_t zn = 1 << (nominator - 1); const int32_t zd = 1 << (denominator - 1); return (fp64_t)zn / (fp64_t)zd; } static fp64_t ui_image_scale(ui_image_t* iv) { if (iv->fit && iv->w > 0 && iv->h > 0) { return min((fp64_t)iv->w / iv->image.w, (fp64_t)iv->h / iv->image.h); } else if (iv->fill && iv->w > 0 && iv->h > 0) { return max((fp64_t)iv->w / iv->image.w, (fp64_t)iv->h / iv->image.h); } else { return ui_image_scale_of(iv->zn, iv->zd); } } static ui_rect_t ui_image_position(ui_image_t* iv) { ui_rect_t rc = { 0, 0, 0, 0 }; if (iv->image.pixels != null) { int32_t iw = iv->image.w; int32_t ih = iv->image.h; // zoomed image width and height rc.w = (int32_t)((fp64_t)iw * ui_image.scale(iv)); rc.h = (int32_t)((fp64_t)ih * ui_image.scale(iv)); int32_t shift_x = (int32_t)((rc.w - iv->w) * iv->sx); int32_t shift_y = (int32_t)((rc.h - iv->h) * iv->sy); // shift_x and shift_y are in zoomed image coordinates rc.x = iv->x - shift_x; // screen x rc.y = iv->y - shift_y; // screen y } return rc; } static void ui_image_paint(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; // ui_gdi.fill(v->x, v->y, v->w, v->h, ui_colors.black); if (iv->image.pixels != null) { ui_gdi.set_clip(v->x, v->y, v->w, v->h); rt_swear(!iv->fit || !iv->fill, "make up your mind"); rt_swear(0 < iv->zn && iv->zn <= 16); rt_swear(0 < iv->zd && iv->zd <= 16); // only 1:2 and 2:1 etc are supported: if (iv->zn != 1) { rt_swear(iv->zd == 1); } if (iv->zd != 1) { rt_swear(iv->zn == 1); } const int32_t iw = iv->image.w; const int32_t ih = iv->image.h; ui_rect_t rc = ui_image_position(iv); if (iv->image.bpp == 1) { ui_gdi.greyscale(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else if (iv->image.bpp == 3) { ui_gdi.bgr(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else if (iv->image.bpp == 4) { if (iv->image.texture == null) { ui_gdi.bgrx(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else { ui_gdi.alpha(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, &iv->image, iv->alpha); } } else { rt_swear(false, "unsupported .c: %d", iv->image.bpp); } if (ui_view.has_focus(v)) { ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); ui_gdi.frame(v->x, v->y, v->w, v->h, highlight); } ui_gdi.set_clip(0, 0, 0, 0); } } static void ui_image_tools_background(ui_view_t* v) { ui_color_t face = ui_colors.get_color(ui_color_id_button_face); ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); ui_gdi.fill(v->x, v->y, v->w, v->h, face); ui_gdi.frame(v->x, v->y, v->w, v->h, highlight); } static void ui_image_show_tools(ui_image_t* iv, bool show) { if (iv->focusable) { if (iv->tool.bar.state.hidden != !show) { iv->tool.bar.state.hidden = !show; iv->tool.bar.state.disabled = !show; iv->tool.ratio.state.hidden = !show; ui_app.request_layout(); } if (show) { // hide in 3.3 seconds: iv->when = rt_clock.seconds() + 3.3; } else { iv->when = 0; } } } static void ui_image_fit_fill_scale(ui_image_t* iv) { fp64_t s = ui_image.scale(iv); rt_assert(s != 0); if (s > 1) { ui_view.set_text(&iv->tool.ratio, "1:%.3f", s); } else if (s != 0 && s <= 1) { ui_view.set_text(&iv->tool.ratio, "%.3f:1", 1.0 / s); } else { // s should not be zero ever } } static void ui_image_measure(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (!v->focusable) { v->w = (int32_t)(iv->image.w * ui_image.scale(iv)); v->h = (int32_t)(iv->image.h * ui_image.scale(iv)); if (iv->fit || iv->fill) { ui_image_fit_fill_scale(iv); } } else { v->w = 0; v->h = 0; } } static void ui_image_layout(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (iv->fit || iv->fill) { ui_image_fit_fill_scale(iv); ui_view.measure_control(&iv->tool.ratio); } iv->tool.bar.x = v->x + v->w - iv->tool.bar.w; iv->tool.bar.y = v->y; iv->tool.ratio.x = v->x + v->w - iv->tool.ratio.w; iv->tool.ratio.y = v->y + v->h - iv->tool.ratio.h; } static void ui_image_every_100ms(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (iv->when != 0 && rt_clock.seconds() > iv->when) { ui_image_show_tools(iv, false); } } static void ui_image_focus_lost(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; ui_image_show_tools(iv, ui_view.has_focus(v)); } static void ui_image_focus_gained(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; ui_image_show_tools(iv, ui_view.has_focus(v)); } static void ui_image_zoomed(ui_image_t* iv) { iv->fill = false; iv->fit = false; // 0=16:1 1=8:1 2=4:1 3=2:1 4=1:1 5=1:2 6=1:4 7=1:8 8=1:16 int32_t n = iv->zoom - 4; int32_t zn = iv->zn; int32_t zd = iv->zd; fp64_t scale_before = ui_image.scale(iv); if (n > 0) { zn = n + 1; zd = 1; } else if (n < 0) { zn = 1; zd = -n + 1; } else if (n == 0) { zn = 1; zd = 1; } fp64_t scale_after = ui_image_scale_of(zn, zd); if (scale_after != scale_before) { iv->zn = zn; iv->zd = zd; const int32_t nm = 1 << (iv->zn - 1); const int32_t dm = 1 << (iv->zd - 1); ui_view.set_text(&iv->tool.ratio, "%d:%d", nm, dm); } if (iv->zn == 1) { iv->zoom = 4 - (iv->zd - 1); } else if (iv->zd == 1) { iv->zoom = 4 + (iv->zn - 1); } else { rt_swear(false); } // is whole image visible? fp64_t s = ui_image.scale(iv); bool whole = (int32_t)(iv->image.w * s) <= iv->w && (int32_t)(iv->image.h * s) <= iv->h; if (whole) { iv->sx = 0.5; iv->sy = 0.5; } ui_view.invalidate(&iv->view, null); ui_image_show_tools(iv, true); } static void ui_image_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { fp64_t dx = (fp64_t)dx_dy.x; fp64_t dy = (fp64_t)dx_dy.y; ui_image_t* iv = (ui_image_t*)v; if (ui_view.has_focus(v)) { fp64_t s = ui_image.scale(iv); if (iv->image.w * s > iv->w || iv->image.h * s > iv->h) { iv->sx = max(0.0, min(iv->sx + dx / iv->image.w, 1.0)); } else { iv->sx = 0.5; } if (iv->image.h * s > iv->h) { iv->sy = max(0.0, min(iv->sy + dy / iv->image.h, 1.0)); } else { iv->sy = 0.5; } ui_view.invalidate(&iv->view, null); } } static bool ui_image_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; if (v->focusable) { ui_image_t* iv = (ui_image_t*)v; const int32_t x = ui_app.mouse.x - iv->x; const int32_t y = ui_app.mouse.y - iv->y; bool tools = !iv->tool.bar.state.hidden && ui_view.inside(&iv->tool.bar, &ui_app.mouse); bool inside = ui_view.inside(&iv->view, &ui_app.mouse) && !tools; bool left = ix == 0; bool drag_started = iv->drag_start.x >= 0 && iv->drag_start.y >= 0; if (left && inside && !drag_started) { iv->drag_start = (ui_point_t){x, y}; } if (!pressed) { iv->drag_start = (ui_point_t){-1, -1}; } swallow = inside || tools; } // rt_println("inside %s", inside ? "true" : "false"); return swallow; } static bool ui_image_mouse_move(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; bool drag_started = iv->drag_start.x >= 0 && iv->drag_start.y >= 0; bool tools = !iv->tool.bar.state.hidden && ui_view.inside(&iv->tool.bar, &ui_app.mouse); bool inside = ui_view.inside(&iv->view, &ui_app.mouse) && !tools; if (drag_started && inside) { ui_image_show_tools(iv, false); const int32_t x = ui_app.mouse.x - iv->x; const int32_t y = ui_app.mouse.y - iv->y; ui_point_t dx_dy = {iv->drag_start.x - x, iv->drag_start.y - y}; ui_image_mouse_scroll(v, dx_dy); iv->drag_start = (ui_point_t){x, y}; } else if (inside) { ui_image_show_tools(iv, true); } else if (!inside && !tools) { ui_image_show_tools(iv, false); } // rt_println("inside %s", inside ? "true" : "false"); return inside; } static bool ui_image_key_pressed(ui_view_t* v, int64_t vk) { ui_image_t* iv = (ui_image_t*)v; bool swallowed = false; if (ui_view.has_focus(v)) { swallowed = true; if (vk == ui.key.up) { ui_image_mouse_scroll(v, (ui_point_t){0, -iv->h / 8}); } else if (vk == ui.key.down) { ui_image_mouse_scroll(v, (ui_point_t){0, +iv->h / 8}); } else if (vk == ui.key.left) { ui_image_mouse_scroll(v, (ui_point_t){-iv->w / 8, 0}); } else if (vk == ui.key.right) { ui_image_mouse_scroll(v, (ui_point_t){+iv->w / 8, 0}); } else if (vk == ui.key.plus) { if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } else if (vk == ui.key.minus) { if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } else { swallowed = false; } } return swallowed; } static void ui_image_zoom_in(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } static void ui_image_zoom_out(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } static void ui_image_fit(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->fit = true; iv->fill = false; ui_image_fit_fill_scale(iv); ui_view.invalidate(&iv->view, null); } static void ui_image_fill(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->fill = true; iv->fit = false; ui_image_fit_fill_scale(iv); ui_view.invalidate(&iv->view, null); } static void ui_image_zoom_1t1(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->zoom = 4; ui_image_zoomed(iv); } static ui_label_t ui_image_about = ui_label(0, "Keyboard shortcuts:\n\n" "Ctrl+C copies image to the clipboard.\n\n" rt_glyph_heavy_plus_sign " zoom in; " rt_glyph_heavy_minus_sign " zoom out;\n" rt_glyph_open_circle_arrows_one_overlay " 1:1.\n\n" rt_glyph_up_down_arrow " Fit;\n" rt_glyph_left_right_arrow " Fill.\n\n" "Left/Right Arrows " rt_glyph_leftward_arrow rt_glyph_rightwards_arrow "Up/Down Arrows " rt_glyph_upwards_arrow rt_glyph_downwards_arrow "\npans the image inside view.\n\n" "Mouse wheel or mouse / touchpad hold and drag to pan.\n" ); static void ui_image_help(ui_button_t* rt_unused(b)) { ui_app.show_toast(&ui_image_about, 7.0); } static void ui_image_copy_to_clipboard(ui_image_t* iv) { ui_bitmap_t image = {0}; if (iv->image.texture != null) { rt_clipboard.put_image(&iv->image); } else { ui_gdi.bitmap_init(&image, iv->image.w, iv->image.h, iv->image.bpp, iv->image.pixels); rt_clipboard.put_image(&image); ui_gdi.bitmap_dispose(&image); } static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); ui_app.show_hint(&hint, ui_app.mouse.x, ui_app.mouse.y + iv->fm->height, 1.5); } static void ui_image_copy(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; ui_image_copy_to_clipboard(iv); } static void ui_image_character(ui_view_t* v, const char* utf8) { ui_image_t* iv = (ui_image_t*)v; if (ui_view.has_focus(v)) { // && ui_app.ctrl ? char ch = utf8[0]; if (ch == '+' || ch == '=') { if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } else if (ch == '-' || ch == '_') { if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } else if (ch == '<' || ch == ',') { ui_image_mouse_scroll(v, (ui_point_t){-iv->w / 8, 0}); } else if (ch == '>' || ch == '.') { ui_image_mouse_scroll(v, (ui_point_t){+iv->w / 8, 0}); } else if (ch == '0') { iv->zoom = 4; ui_image_zoomed(iv); } else if (ch == 3 && iv->image.pixels != null) { // Ctrl+C ui_image_copy_to_clipboard(iv); } } } static void ui_image_add_button(ui_image_t* iv, ui_button_t* b, const char* label, void (*cb)(ui_button_t* b), const char* hint) { *b = (ui_button_t)ui_button("", 0.0f, cb); ui_view.set_text(b, label); b->that = iv; b->insets.top = 0; b->insets.bottom = 0; b->padding.top = 0; b->padding.bottom = 0; b->insets = (ui_margins_t){0}; b->padding = (ui_margins_t){0}; b->flat = true; b->fm = &ui_app.fm.mono.normal; b->min_w_em = 1.5f; rt_str_printf(b->hint, "%s", hint); ui_view.add_last(&iv->tool.bar, b); } void ui_image_init(ui_image_t* iv) { memset(iv, 0x00, sizeof(*iv)); iv->type = ui_view_image; iv->paint = ui_image_paint; iv->tap = ui_image_tap; iv->mouse_move = ui_image_mouse_move; iv->measure = ui_image_measure; iv->layout = ui_image_layout; iv->every_100ms = ui_image_every_100ms; iv->focus_lost = ui_image_focus_lost; iv->focus_gained = ui_image_focus_gained; iv->mouse_scroll = ui_image_mouse_scroll; iv->character = ui_image_character; iv->key_pressed = ui_image_key_pressed; iv->fm = &ui_app.fm.prop.normal; iv->tool.bar = (ui_view_t)ui_view(span); // buttons: ui_image_add_button(iv, &iv->tool.copy, "\xF0\x9F\x93\x8B", ui_image_copy, "Copy to Clipboard Ctrl+C"); ui_image_add_button(iv, &iv->tool.zoom_out, rt_glyph_heavy_minus_sign, ui_image_zoom_out, "Zoom Out"); ui_image_add_button(iv, &iv->tool.zoom_1t1, rt_glyph_open_circle_arrows_one_overlay, ui_image_zoom_1t1, "Reset to 1:1"); ui_image_add_button(iv, &iv->tool.zoom_in, rt_glyph_heavy_plus_sign, ui_image_zoom_in, "Zoom In"); ui_image_add_button(iv, &iv->tool.fit, rt_glyph_up_down_arrow, ui_image_fit, "Fit"); ui_image_add_button(iv, &iv->tool.fill, rt_glyph_left_right_arrow, ui_image_fill, "Fill"); ui_image_add_button(iv, &iv->tool.help, "?", ui_image_help, "Help"); iv->tool.zoom_1t1.min_w_em = 1.25f; iv->tool.ratio = (ui_label_t)ui_label(0, "1:1"); iv->tool.ratio.color = ui_colors.get_color(ui_color_id_highlight); iv->tool.ratio.color_id = ui_color_id_highlight; ui_view.add_last(&iv->view, &iv->tool.bar); ui_view.add_last(&iv->view, &iv->tool.ratio); iv->tool.bar.state.hidden = true; iv->tool.ratio.state.hidden = true; iv->tool.bar.erase = ui_image_tools_background; iv->tool.ratio.erase = ui_image_tools_background; iv->zoom = 4; iv->zn = 1; iv->zd = 1; iv->sx = 0.5; iv->sy = 0.5; iv->drag_start = (ui_point_t){-1, -1}; iv->debug.id = "#image"; } void ui_image_init_with(ui_image_t* iv, const uint8_t* pixels, int32_t w, int32_t h, int32_t c, int32_t s) { ui_image_init(iv); iv->image.pixels = (uint8_t*)pixels; iv->image.w = w; iv->image.h = h; iv->image.bpp = c; iv->image.stride = s; } static void ui_image_ratio(ui_image_t* iv, int32_t zn, int32_t zd) { rt_swear(0 < zn && zn <= 16); rt_swear(0 < zd && zd <= 16); // only 1:2 and 2:1 etc are supported: if (zn != 1) { rt_swear(zd == 1); } if (zd != 1) { rt_swear(zn == 1); } iv->zn = zn; iv->zd = zd; iv->fit = false; iv->fill = false; } ui_image_if ui_image = { .init = ui_image_init, .init_with = ui_image_init_with, .ratio = ui_image_ratio, .scale = ui_image_scale, .position = ui_image_position }; // ________________________________ ui_label.c ________________________________ #include "rt/rt.h" static void ui_label_paint(ui_view_t* v) { rt_assert(v->type == ui_view_label); rt_assert(!ui_view.is_hidden(v)); const char* s = ui_view.string(v); ui_color_t c = v->state.hover && v->highlightable ? ui_colors.interpolate(v->color, ui_colors.blue, 1.0f / 8.0f) : v->color; const int32_t tx = v->x + v->text.xy.x; const int32_t ty = v->y + v->text.xy.y; const ui_gdi_ta_t ta = { .fm = v->fm, .color = c }; const bool multiline = strchr(s, '\n') != null; if (multiline) { int32_t w = (int32_t)((fp64_t)v->min_w_em * (fp64_t)v->fm->em.w + 0.5); ui_gdi.multiline(&ta, tx, ty, w, "%s", ui_view.string(v)); } else { ui_gdi.text(&ta, tx, ty, "%s", ui_view.string(v)); } if (v->state.hover && !v->flat && v->highlightable) { ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); int32_t radius = (v->fm->em.h / 4) | 0x1; // corner radius int32_t h = multiline ? v->h : v->fm->baseline + v->fm->descent; ui_gdi.rounded(v->x - radius, v->y, v->w + 2 * radius, h, radius, highlight, ui_colors.transparent); } } static bool ui_label_context_menu(ui_view_t* v) { rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { rt_clipboard.put_text(ui_view.string(v)); static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); int32_t x = v->x + v->w / 2; int32_t y = v->y + v->h; ui_app.show_hint(&hint, x, y, 0.75); } return inside; } static void ui_label_character(ui_view_t* v, const char* utf8) { rt_assert(v->type == ui_view_label); if (v->state.hover && !ui_view.is_hidden(v)) { char ch = utf8[0]; // Copy to clipboard works for hover over text if ((ch == 3 || ch == 'c' || ch == 'C') && ui_app.ctrl) { rt_clipboard.put_text(ui_view.string(v)); // 3 is ASCII for Ctrl+C } } } void ui_view_init_label(ui_view_t* v) { rt_assert(v->type == ui_view_label); v->paint = ui_label_paint; v->character = ui_label_character; v->context_menu = ui_label_context_menu; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; v->text_align = ui.align.left; } void ui_label_init_va(ui_label_t* v, fp32_t min_w_em, const char* format, va_list va) { ui_view.set_text(v, format, va); v->min_w_em = min_w_em; v->type = ui_view_label; ui_view_init_label(v); } void ui_label_init(ui_label_t* v, fp32_t min_w_em, const char* format, ...) { va_list va; va_start(va, format); ui_label_init_va(v, min_w_em, format, va); va_end(va); } // _________________________________ ui_mbx.c _________________________________ #include "rt/rt.h" static void ui_mbx_button(ui_button_t* b) { ui_mbx_t* m = (ui_mbx_t*)b->parent; rt_assert(m->type == ui_view_mbx); m->option = -1; for (int32_t i = 0; i < rt_countof(m->button) && m->option < 0; i++) { if (b == &m->button[i]) { m->option = i; if (m->callback != null) { m->callback(&m->view); // need to disarm button because message box about to close b->state.pressed = false; b->state.armed = false; } } } ui_app.show_toast(null, 0); } static void ui_mbx_measured(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; int32_t n = 0; ui_view_for_each(v, c, { n++; }); n--; // number of buttons const int32_t em_x = m->label.fm->em.w; const int32_t em_y = m->label.fm->em.h; const int32_t tw = m->label.w; const int32_t th = m->label.h; if (n > 0) { int32_t bw = 0; for (int32_t i = 0; i < n; i++) { bw += m->button[i].w; } v->w = rt_max(tw, bw + em_x * 2); v->h = th + m->button[0].h + em_y + em_y / 2; } else { v->h = th + em_y / 2; v->w = tw; } } static void ui_mbx_layout(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; int32_t n = 0; ui_view_for_each(v, c, { n++; }); n--; // number of buttons const int32_t em_y = m->label.fm->em.h; m->label.x = v->x; m->label.y = v->y + em_y * 2 / 3; const int32_t tw = m->label.w; const int32_t th = m->label.h; if (n > 0) { int32_t bw = 0; for (int32_t i = 0; i < n; i++) { bw += m->button[i].w; } // center text: m->label.x = v->x + (v->w - tw) / 2; // spacing between buttons: int32_t sp = (v->w - bw) / (n + 1); int32_t x = sp; for (int32_t i = 0; i < n; i++) { m->button[i].x = v->x + x; m->button[i].y = v->y + th + em_y * 3 / 2; x += m->button[i].w + sp; } } } void ui_view_init_mbx(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; v->measured = ui_mbx_measured; v->layout = ui_mbx_layout; m->fm = &ui_app.fm.prop.normal; int32_t n = 0; while (m->options[n] != null && n < rt_countof(m->button) - 1) { m->button[n] = (ui_button_t)ui_button("", 6.0, ui_mbx_button); ui_view.set_text(&m->button[n], "%s", m->options[n]); n++; } rt_swear(n <= rt_countof(m->button), "inhumane: %d buttons is too many", n); if (n > rt_countof(m->button)) { n = rt_countof(m->button); } m->label = (ui_label_t)ui_label(0, ""); ui_view.set_text(&m->label, "%s", ui_view.string(&m->view)); ui_view.add_last(&m->view, &m->label); for (int32_t i = 0; i < n; i++) { ui_view.add_last(&m->view, &m->button[i]); m->button[i].fm = m->fm; } m->label.fm = m->fm; ui_view.set_text(&m->view, ""); m->option = -1; if (m->debug.id == null) { m->debug.id = "#mbx"; } } void ui_mbx_init(ui_mbx_t* m, const char* options[], const char* format, ...) { m->type = ui_view_mbx; m->measured = ui_mbx_measured; m->layout = ui_mbx_layout; m->color_id = ui_color_id_window; m->options = options; m->focusable = true; va_list va; va_start(va, format); ui_view.set_text_va(&m->view, format, va); ui_label_init(&m->label, 0.0, ui_view.string(&m->view)); va_end(va); ui_view_init_mbx(&m->view); } // ________________________________ ui_midi.c _________________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "rt/rt_win32.h" #include #pragma comment(lib, "winmm") typedef struct ui_midi_s_ { MCI_OPEN_PARMSA mop; // opaque ui_app_message_handler_t handler; char alias[32]; int64_t device_id; uintptr_t window; bool playing; } ui_midi_t_; rt_static_assertion(sizeof(ui_midi_t) >= sizeof(ui_midi_t_) + sizeof(void*)); rt_static_assertion(MMSYSERR_NOERROR == 0); static void ui_midi_error(errno_t r, char* text, int32_t count) { rt_fatal_win32err(mciGetErrorStringA(r, text, (UINT)count)); } static void ui_midi_warn_if_error_(int r, const char* call, const char* func, int line) { if (r != 0) { static char error[256]; ui_midi_error(r, error, rt_countof(error)); rt_println("%s:%d %s", func, line, call); rt_println("%d - MCIERR_BASE: %d %s", r, r - MCIERR_BASE, error); } } #define ui_midi_warn_if_error(r) do { \ ui_midi_warn_if_error_(r, #r, __func__, __LINE__); \ } while (0) #define ui_midi_fatal_if_error(call) do { \ int _r_ = call; ui_midi_warn_if_error_(r, #call, __func__, __LINE__); \ rt_fatal_if_error(r); \ } while (0) static bool ui_midi_message_callback(ui_app_message_handler_t* h, int32_t m, int64_t wp, int64_t lp, int64_t* rt) { if (m == MM_MCINOTIFY) { #ifdef UI_MIDI_DEBUG rt_println("device_id: %lld", lp); if (wp & MCI_NOTIFY_SUCCESSFUL) { rt_println("SUCCESSFUL"); } if (wp & MCI_NOTIFY_SUPERSEDED) { rt_println("SUPERSEDED"); } if (wp & MCI_NOTIFY_ABORTED) { rt_println("ABORTED"); } if (wp & MCI_NOTIFY_FAILURE) { rt_println("FAILURE"); } #endif ui_midi_t* midi = (ui_midi_t*)h->that; ui_midi_t_* mi = (ui_midi_t_*)midi; if (mi->device_id == lp) { if (midi->notify != null) { *rt = midi->notify(midi, wp); } else { *rt = 0; } return true; } } return false; } static void ui_midi_remove_handler(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; ui_app_message_handler_t* h = ui_app.handlers; if (h == &mi->handler) { ui_app.handlers = h->next; } else { while (h->next != null && h->next != &mi->handler) { h = h->next; } rt_swear(h->next == &mi->handler); if (h->next == &mi->handler) { h->next = h->next->next; } } mi->handler.callback = null; mi->handler.that = null; mi->handler.next = null; } static errno_t ui_midi_open(ui_midi_t* m, const char* filename) { rt_swear(rt_thread.id() == ui_app.tid); ui_midi_t_* mi = (ui_midi_t_*)m; mi->handler.that = mi; mi->handler.next = ui_app.handlers; ui_app.handlers = &mi->handler; mi->window = (uintptr_t)ui_app.window; mi->playing = false; mi->mop.dwCallback = mi->window; mi->mop.wDeviceID = (WORD)-1; mi->mop.lpstrDeviceType = (const char*)MCI_DEVTYPE_SEQUENCER; mi->mop.lpstrElementName = filename; mi->mop.lpstrAlias = mi->alias; rt_str_printf(mi->alias, "%p", m); const DWORD_PTR flags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT | MCI_OPEN_ALIAS; errno_t r = mciSendCommandA(0, MCI_OPEN, flags, (uintptr_t)&mi->mop); ui_midi_warn_if_error(r); rt_assert(mi->mop.wDeviceID != -1); mi->handler.callback = ui_midi_message_callback, mi->device_id = mi->mop.wDeviceID; if (r != 0) { ui_midi_remove_handler(m); memset(&mi->mop, 0x00, sizeof(mi->mop)); mi->window = 0; } return r; } static errno_t ui_midi_play(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); ui_midi_t_* mi = (ui_midi_t_*)m; rt_swear(ui_midi.is_open(m)); MCI_PLAY_PARMS pp = { .dwCallback = (uintptr_t)mi->window }; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_PLAY, MCI_NOTIFY, (uintptr_t)&pp); ui_midi_warn_if_error(r); if (r == 0) { mi->playing = true; } return r; } static errno_t ui_midi_rewind(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m)); ui_midi_t_* mi = (ui_midi_t_*)m; MCI_SEEK_PARMS p = { .dwCallback = (uintptr_t)mi->window, .dwTo = 0 }; const DWORD f = MCI_WAIT|MCI_SEEK_TO_START; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_SEEK, f, (DWORD_PTR)&p); ui_midi_warn_if_error(r); return r; } static errno_t ui_midi_get_volume(ui_midi_t* m, fp64_t* volume) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); DWORD v = 0; errno_t r = midiOutGetVolume((HMIDIOUT)0, &v); ui_midi_warn_if_error(r); *volume = (fp64_t)v / (fp64_t)0xFFFFFFFFU; return 0; } static errno_t ui_midi_set_volume(ui_midi_t* m, fp64_t volume) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); DWORD v = (DWORD)(volume * (fp64_t)0xFFFFFFFFU); const UINT n = midiOutGetNumDevs(); // Handle to a MIDI Output Device HMIDIOUT h = (HMIDIOUT)(uintptr_t)(n - 1); errno_t r = n == 0 ? MCIERR_DEVICE_NOT_INSTALLED : midiOutSetVolume(h, v); ui_midi_warn_if_error(r); rt_fatal_if_error(r); return r; } static errno_t ui_midi_stop(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); ui_midi_t_* mi = (ui_midi_t_*)m; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_STOP, 0, 0); ui_midi_warn_if_error(r); if (r == 0) { mi->playing = false; } return r; } static void ui_midi_close(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && !ui_midi.is_playing(m)); ui_midi_t_* mi = (ui_midi_t_*)m; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_CLOSE, MCI_WAIT, 0); ui_midi_warn_if_error(r); r = mciSendCommandA(MCI_ALL_DEVICE_ID, MCI_CLOSE, MCI_WAIT, 0); ui_midi_warn_if_error(r); rt_fatal_if_error(r, "sound card is unplugged on the fly?"); memset(&mi->mop, 0x00, sizeof(mi->mop)); mi->window = 0; ui_midi_remove_handler(m); } static bool ui_midi_is_open(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; return mi->window != 0; } static bool ui_midi_is_playing(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; return mi->playing; } ui_midi_if ui_midi = { .success = MCI_NOTIFY_SUCCESSFUL, .failure = MCI_NOTIFY_FAILURE, .aborted = MCI_NOTIFY_ABORTED, .superseded = MCI_NOTIFY_SUPERSEDED, .error = ui_midi_error, .open = ui_midi_open, .play = ui_midi_play, .rewind = ui_midi_rewind, .get_volume = ui_midi_get_volume, .set_volume = ui_midi_set_volume, .stop = ui_midi_stop, .is_open = ui_midi_is_open, .is_playing = ui_midi_is_playing, .close = ui_midi_close }; // _______________________________ ui_slider.c ________________________________ #include "rt/rt.h" static void ui_slider_invalidate(const ui_slider_t* s) { const ui_view_t* v = &s->view; ui_view.invalidate(v, null); if (!s->dec.state.hidden) { ui_view.invalidate(&s->dec, null); } if (!s->inc.state.hidden) { ui_view.invalidate(&s->dec, null); } } static int32_t ui_slider_width(const ui_slider_t* s) { const ui_ltrb_t i = ui_view.margins(&s->view, &s->insets); int32_t w = s->w - i.left - i.right; if (!s->dec.state.hidden) { const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const ui_ltrb_t inc_p = ui_view.margins(&s->inc, &s->inc.padding); w -= s->dec.w + s->inc.w + dec_p.right + inc_p.left; } return w; } static ui_wh_t measure_text(const ui_fm_t* fm, const char* format, ...) { va_list va; va_start(va, format); const ui_gdi_ta_t ta = { .fm = fm, .color = ui_colors.white, .measure = true }; ui_wh_t wh = ui_gdi.text_va(&ta, 0, 0, format, va); va_end(va); return wh; } static ui_wh_t ui_slider_measure_text(ui_slider_t* s) { char formatted[rt_countof(s->p.text)]; const ui_fm_t* fm = s->fm; const char* text = ui_view.string(&s->view); const ui_ltrb_t i = ui_view.margins(&s->view, &s->insets); ui_wh_t wh = s->fm->em; if (s->debug.trace.mt) { const ui_ltrb_t p = ui_view.margins(&s->view, &s->padding); rt_println(">%dx%d em: %dx%d min: %.1fx%.1f " "i: %d %d %d %d p: %d %d %d %d \"%.*s\"", s->w, s->h, fm->em.w, fm->em.h, s->min_w_em, s->min_h_em, i.left, i.top, i.right, i.bottom, p.left, p.top, p.right, p.bottom, rt_min(64, strlen(text)), text); const ui_margins_t in = s->insets; const ui_margins_t pd = s->padding; rt_println(" i: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f" " p: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f", in.left, in.top, in.right, in.bottom, in.left + in.right, in.top + in.bottom, pd.left, pd.top, pd.right, pd.bottom, pd.left + pd.right, pd.top + pd.bottom); } if (s->format != null) { s->format(&s->view); rt_str_printf(formatted, "%s", text); wh = measure_text(s->fm, "%s", formatted); // TODO: format string 0x08X? } else if (text != null && (strstr(text, "%d") != null || strstr(text, "%u") != null)) { ui_wh_t mt_min = measure_text(s->fm, text, s->value_min); ui_wh_t mt_max = measure_text(s->fm, text, s->value_max); ui_wh_t mt_val = measure_text(s->fm, text, s->value); wh.h = rt_max(mt_val.h, rt_max(mt_min.h, mt_max.h)); wh.w = rt_max(mt_val.w, rt_max(mt_min.w, mt_max.w)); } else if (text != null && text[0] != 0) { wh = measure_text(s->fm, "%s", text); } if (s->debug.trace.mt) { rt_println(" mt: %dx%d", wh.w, wh.h); } return wh; } static void ui_slider_measure(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); // slider cannot be smaller than 2*em const fp32_t min_w_em = rt_max(2.0f, v->min_w_em); v->w = (int32_t)((fp64_t)fm->em.w * (fp64_t) min_w_em + 0.5); v->h = (int32_t)((fp64_t)fm->em.h * (fp64_t)v->min_h_em + 0.5); // dec and inc have same font metrics as a slider: s->dec.fm = fm; s->inc.fm = fm; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "not the same"); ui_view.measure_control(v); // s->text.mt = ui_slider_measure_text(s); if (s->dec.state.hidden) { v->w = rt_max(v->w, i.left + s->wh.w + i.right); } else { ui_view.measure(&s->dec); // remeasure with inherited metrics ui_view.measure(&s->inc); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const ui_ltrb_t inc_p = ui_view.margins(&s->inc, &s->inc.padding); v->w = rt_max(v->w, s->dec.w + dec_p.right + s->wh.w + inc_p.left + s->inc.w); } v->h = rt_max(v->h, i.top + fm->em.h + i.bottom); if (s->debug.trace.mt) { rt_println("<%dx%d", s->w, s->h); } } static void ui_slider_layout(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; // disregard inc/dec .state.hidden bit for layout: const ui_ltrb_t i = ui_view.margins(v, &v->insets); s->dec.x = v->x + i.left; s->dec.y = v->y; s->inc.x = v->x + v->w - i.right - s->inc.w; s->inc.y = v->y; } static void ui_slider_paint(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); // dec button is sticking to the left into slider padding const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "hidden or not together"); const int32_t dx = s->dec.state.hidden ? 0 : dec_w; const int32_t x = v->x + dx + i.left; const int32_t w = ui_slider_width(s); // draw background: fp32_t d = ui_theme.is_app_dark() ? 0.50f : 0.25f; ui_color_t d0 = ui_colors.darken(v->background, d); d /= 4; ui_color_t d1 = ui_colors.darken(v->background, d); ui_gdi.gradient(x, v->y, w, v->h, d1, d0, true); // draw value: ui_color_t c = ui_theme.is_app_dark() ? ui_colors.darken(ui_colors.green, 1.0f / 128.0f) : ui_colors.jungle_green; d1 = c; d0 = ui_colors.darken(c, 1.0f / 64.0f); const fp64_t range = (fp64_t)s->value_max - (fp64_t)s->value_min; rt_assert(range > 0, "range: %.6f", range); const fp64_t vw = (fp64_t)w * (s->value - s->value_min) / range; const int32_t wi = (int32_t)(vw + 0.5); ui_gdi.gradient(x, v->y, wi, v->h, d1, d0, true); if (!v->flat) { ui_color_t color = v->state.hover ? ui_colors.get_color(ui_color_id_hot_tracking) : ui_colors.get_color(ui_color_id_gray_text); if (ui_view.is_disabled(v)) { color = ui_color_rgb(30, 30, 30); } // TODO: hardcoded ui_gdi.frame(x, v->y, w, v->h, color); } // text: const char* text = ui_view.string(v); char formatted[rt_countof(v->p.text)]; if (s->format != null) { s->format(v); s->p.strid = 0; // nls again text = ui_view.string(v); } else if (text != null && (strstr(text, "%d") != null || strstr(text, "%u") != null)) { rt_str.format(formatted, rt_countof(formatted), text, s->value); s->p.strid = 0; // nls again text = rt_nls.str(formatted); } // because current value was formatted into `text` need to // remeasure and align text again: ui_view.text_measure(v, text, &v->text); ui_view.text_align(v, &v->text); const ui_color_t text_color = !v->state.hover ? v->color : (ui_theme.is_app_dark() ? ui_colors.white : ui_colors.black); const ui_gdi_ta_t ta = { .fm = fm, .color = text_color }; ui_gdi.text(&ta, v->x + v->text.xy.x, v->y + v->text.xy.y, "%s", text); } static bool ui_slider_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { if (pressed) { ui_slider_t* s = (ui_slider_t*)v; const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "hidden or not together"); const int32_t sw = ui_slider_width(s); // slider width const int32_t dx = s->dec.state.hidden ? 0 : dec_w + dec_p.right; const int32_t vx = v->x + i.left + dx; const int32_t x = ui_app.mouse.x - vx; const int32_t y = ui_app.mouse.y - (v->y + i.top); if (0 <= x && x < sw && 0 <= y && y < v->h) { const fp64_t range = (fp64_t)s->value_max - (fp64_t)s->value_min; fp64_t val = (fp64_t)x * range / (fp64_t)(sw - 1); int32_t vw = (int32_t)(val + s->value_min + 0.5); s->value = rt_min(rt_max(vw, s->value_min), s->value_max); if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } return pressed && inside; // swallow inside clicks } static void ui_slider_mouse_move(ui_view_t* v) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { const ui_ltrb_t i = ui_view.margins(v, &v->insets); ui_slider_t* s = (ui_slider_t*)v; bool drag = ui_app.mouse_left || ui_app.mouse_right; if (drag) { const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, ".dec .inc must be .hidden in sync"); const int32_t sw = ui_slider_width(s); // slider width const int32_t dx = s->dec.state.hidden ? 0 : dec_w + dec_p.right; const int32_t vx = v->x + i.left + dx; const int32_t x = ui_app.mouse.x - vx; const int32_t y = ui_app.mouse.y - (v->y + i.top); if (0 <= x && x < sw && 0 <= y && y < v->h) { const fp64_t fmax = (fp64_t)s->value_max; const fp64_t fmin = (fp64_t)s->value_min; const fp64_t range = fmax - fmin; fp64_t val = (fp64_t)x * range / (fp64_t)(sw - 1); int32_t vw = (int32_t)(val + s->value_min + 0.5); s->value = rt_min(rt_max(vw, s->value_min), s->value_max); if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } } static void ui_slider_inc_dec_value(ui_slider_t* s, int32_t sign, int32_t mul) { if (!ui_view.is_hidden(&s->view) && !ui_view.is_disabled(&s->view)) { // full 0x80000000..0x7FFFFFFF (-2147483648..2147483647) range int32_t v = s->value; if (v > s->value_min && sign < 0) { mul = rt_min(v - s->value_min, mul); v += mul * sign; } else if (v < s->value_max && sign > 0) { mul = rt_min(s->value_max - v, mul); v += mul * sign; } if (s->value != v) { s->value = v; if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } static void ui_slider_inc_dec(ui_button_t* b) { ui_slider_t* s = (ui_slider_t*)b->parent; if (!ui_view.is_hidden(&s->view) && !ui_view.is_disabled(&s->view)) { int32_t sign = b == &s->inc ? +1 : -1; int32_t mul = ui_app.shift && ui_app.ctrl ? 1000 : ui_app.shift ? 100 : ui_app.ctrl ? 10 : 1; ui_slider_inc_dec_value(s, sign, mul); } } static void ui_slider_every_100ms(ui_view_t* v) { // 100ms rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; if (ui_view.is_hidden(v) || ui_view.is_disabled(v)) { s->time = 0; } else if (!s->dec.state.armed && !s->inc.state.armed) { s->time = 0; } else { if (s->time == 0) { s->time = ui_app.now; } else if (ui_app.now - s->time > 1.0) { const int32_t sign = s->dec.state.armed ? -1 : +1; const int32_t sec = (int32_t)(ui_app.now - s->time + 0.5); int32_t initial = ui_app.shift && ui_app.ctrl ? 1000 : ui_app.shift ? 100 : ui_app.ctrl ? 10 : 1; int32_t mul = sec >= 1 ? initial << (sec - 1) : initial; const int64_t range = (int64_t)s->value_max - (int64_t)s->value_min; if (mul > range / 8) { mul = (int32_t)(range / 8); } ui_slider_inc_dec_value(s, sign, rt_max(mul, 1)); } } } void ui_view_init_slider(ui_view_t* v) { rt_assert(v->type == ui_view_slider); v->measure = ui_slider_measure; v->layout = ui_slider_layout; v->paint = ui_slider_paint; v->tap = ui_slider_tap; v->mouse_move = ui_slider_mouse_move; v->every_100ms = ui_slider_every_100ms; v->color_id = ui_color_id_window_text; v->background_id = ui_color_id_button_face; ui_slider_t* s = (ui_slider_t*)v; static const char* accel = " Hold key while clicking\n" " Ctrl: x 10 Shift: x 100 \n" " Ctrl+Shift: x 1000 \n for step multiplier."; s->dec = (ui_button_t)ui_button(rt_glyph_fullwidth_hyphen_minus, 0, // rt_glyph_heavy_minus_sign ui_slider_inc_dec); s->dec.fm = v->fm; rt_str_printf(s->dec.hint, "%s", accel); s->inc = (ui_button_t)ui_button(rt_glyph_fullwidth_plus_sign, 0, // rt_glyph_heavy_plus_sign ui_slider_inc_dec); s->inc.fm = v->fm; ui_view.add(&s->view, &s->dec, &s->inc, null); // single glyph buttons less insets look better: ui_view_for_each(&s->view, it, { it->insets.left = 0.125f; it->insets.right = 0.125f; }); // inherit initial padding and insets from buttons. // caller may change those later and it should be accounted to // in measure() and layout() v->insets = s->dec.insets; v->padding = s->dec.padding; s->dec.padding.right = 0; s->dec.padding.left = 0; s->inc.padding.left = 0; s->inc.padding.right = 0; s->dec.flat = true; s->inc.flat = true; s->dec.min_h_em = 1.0f + ui_view_i_tb * 2; s->dec.min_w_em = 1.0f + ui_view_i_tb * 2; s->inc.min_h_em = 1.0f + ui_view_i_tb * 2; s->inc.min_w_em = 1.0f + ui_view_i_tb * 2; rt_str_printf(s->inc.hint, "%s", accel); v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; if (v->debug.id == null) { v->debug.id = "#slider"; } } void ui_slider_init(ui_slider_t* s, const char* label, fp32_t min_w_em, int32_t value_min, int32_t value_max, void (*callback)(ui_view_t* r)) { static_assert(offsetof(ui_slider_t, view) == 0, "offsetof(.view)"); if (min_w_em < 6.0) { rt_println("6.0 em minimum"); } s->type = ui_view_slider; ui_view.set_text(&s->view, "%s", label); s->callback = callback; s->min_w_em = rt_max(6.0f, min_w_em); s->value_min = value_min; s->value_max = value_max; s->value = value_min; ui_view_init_slider(&s->view); } // ________________________________ ui_theme.c ________________________________ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" // ________________________________ rt_win32.h ________________________________ #ifdef WIN32 #pragma warning(push) #pragma warning(disable: 4255) // no function prototype: '()' to '(void)' #pragma warning(disable: 4459) // declaration of '...' hides global declaration #pragma push_macro("UNICODE") #define UNICODE // always because otherwise IME does not work // ut: #include // used by: #include // both rt_loader.c and rt_processes.c #include // rt_processes.c #include // rt_processes.c #include // for knownfolders #include // rt_files.c #include // rt_files.c #include // rt_files.c #include // rt_files.c // ui: #include #include #include #include #include #include #include #include #include #pragma pop_macro("UNICODE") #pragma warning(pop) #include #define rt_export __declspec(dllexport) // Win32 API BOOL -> errno_t translation #define rt_b2e(call) ((errno_t)(call ? 0 : GetLastError())) void rt_win32_close_handle(void* h); /* translate ix to error */ errno_t rt_wait_ix2e(uint32_t r); #endif // WIN32 static int32_t ui_theme_dark = -1; // -1 unknown static errno_t ui_theme_reg_get_uint32(HKEY root, const char* path, const char* key, DWORD *v) { *v = 0; DWORD type = REG_DWORD; DWORD light_theme = 0; DWORD bytes = sizeof(light_theme); errno_t r = RegGetValueA(root, path, key, RRF_RT_DWORD, &type, v, &bytes); if (r != 0) { rt_println("RegGetValueA(%s\\%s) failed %s", path, key, rt_strerr(r)); } return r; } #pragma push_macro("ux_theme_reg_cv") #pragma push_macro("ux_theme_reg_default_colors") #define ux_theme_reg_cv "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\" #define ux_theme_reg_default_colors ux_theme_reg_cv "Themes\\DefaultColors\\" static bool ui_theme_use_light_theme(const char* key) { if ((!ui_app.dark_mode && !ui_app.light_mode) || ( ui_app.dark_mode && ui_app.light_mode)) { const char* personalize = ux_theme_reg_cv "Themes\\Personalize"; DWORD light_theme = 0; ui_theme_reg_get_uint32(HKEY_CURRENT_USER, personalize, key, &light_theme); return light_theme != 0; } else if (ui_app.light_mode) { return true; } else { rt_assert(ui_app.dark_mode); return false; } } #pragma pop_macro("ux_theme_reg_cv") #pragma pop_macro("ux_theme_reg_default_colors") static HMODULE ui_theme_uxtheme(void) { static HMODULE uxtheme; if (uxtheme == null) { uxtheme = GetModuleHandleA("uxtheme.dll"); if (uxtheme == null) { uxtheme = LoadLibraryA("uxtheme.dll"); } } rt_not_null(uxtheme); return uxtheme; } static void* ui_theme_uxtheme_func(uint16_t ordinal) { HMODULE uxtheme = ui_theme_uxtheme(); void* proc = (void*)GetProcAddress(uxtheme, MAKEINTRESOURCEA(ordinal)); rt_not_null(proc); return proc; } static void ui_theme_set_preferred_app_mode(int32_t mode) { typedef BOOL (__stdcall *SetPreferredAppMode_t)(int32_t mode); SetPreferredAppMode_t SetPreferredAppMode = (SetPreferredAppMode_t) (SetPreferredAppMode_t)ui_theme_uxtheme_func(135); errno_t r = rt_b2e(SetPreferredAppMode(mode)); // On Win11: 10.0.22631 // SetPreferredAppMode(true) failed 0x0000047E(1150) ERROR_OLD_WIN_VERSION // "The specified program requires a newer version of Windows." if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("SetPreferredAppMode(AllowDark) failed %s", rt_strerr(r)); } } // https://stackoverflow.com/questions/75835069/dark-system-contextmenu-in-window static void ui_theme_flush_menu_themes(void) { typedef BOOL (__stdcall *FlushMenuThemes_t)(void); FlushMenuThemes_t FlushMenuThemes = (FlushMenuThemes_t) (FlushMenuThemes_t)ui_theme_uxtheme_func(136); errno_t r = rt_b2e(FlushMenuThemes()); // FlushMenuThemes() works but returns ERROR_OLD_WIN_VERSION // on newest Windows 11 but it is not documented thus no complains. if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("FlushMenuThemes(AllowDark) failed %s", rt_strerr(r)); } } static void ui_theme_allow_dark_mode_for_app(bool allow) { // https://github.com/rizonesoft/Notepad3/tree/96a48bd829a3f3192bbc93cd6944cafb3228b96d/src/DarkMode typedef BOOL (__stdcall *AllowDarkModeForApp_t)(bool allow); AllowDarkModeForApp_t AllowDarkModeForApp = (AllowDarkModeForApp_t)ui_theme_uxtheme_func(132); if (AllowDarkModeForApp != null) { errno_t r = rt_b2e(AllowDarkModeForApp(allow)); if (r != 0 && r != ERROR_PROC_NOT_FOUND) { rt_println("AllowDarkModeForApp(true) failed %s", rt_strerr(r)); } } } static void ui_theme_allow_dark_mode_for_window(bool allow) { typedef BOOL (__stdcall *AllowDarkModeForWindow_t)(HWND hWnd, bool allow); AllowDarkModeForWindow_t AllowDarkModeForWindow = (AllowDarkModeForWindow_t)ui_theme_uxtheme_func(133); if (AllowDarkModeForWindow != null) { int r = rt_b2e(AllowDarkModeForWindow((HWND)ui_app.window, allow)); // On Win11: 10.0.22631 // AllowDarkModeForWindow(true) failed 0x0000047E(1150) ERROR_OLD_WIN_VERSION // "The specified program requires a newer version of Windows." if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("AllowDarkModeForWindow(true) failed %s", rt_strerr(r)); } } } static bool ui_theme_are_apps_dark(void) { return !ui_theme_use_light_theme("AppsUseLightTheme"); } static bool ui_theme_is_system_dark(void) { return !ui_theme_use_light_theme("SystemUsesLightTheme"); } static bool ui_theme_is_app_dark(void) { if (ui_theme_dark < 0) { ui_theme_dark = ui_theme.are_apps_dark(); } return ui_theme_dark; } static void ui_theme_refresh(void) { rt_swear(ui_app.window != null); ui_theme_dark = -1; BOOL dark_mode = ui_theme_is_app_dark(); // must be 32-bit "BOOL" static const DWORD DWMWA_USE_IMMERSIVE_DARK_MODE = 20; /* 20 == DWMWA_USE_IMMERSIVE_DARK_MODE in Windows 11 SDK. This value was undocumented for Windows 10 versions 2004 and later, supported for Windows 11 Build 22000 and later. */ errno_t r = DwmSetWindowAttribute((HWND)ui_app.window, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark_mode, sizeof(dark_mode)); if (r != 0) { rt_println("DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) " "failed %s", rt_strerr(r)); } ui_theme.allow_dark_mode_for_app(dark_mode); ui_theme.allow_dark_mode_for_window(dark_mode); ui_theme.set_preferred_app_mode(dark_mode ? ui_theme_app_mode_force_dark : ui_theme_app_mode_force_light); ui_theme.flush_menu_themes(); ui_app.request_layout(); } ui_theme_if ui_theme = { .is_app_dark = ui_theme_is_app_dark, .is_system_dark = ui_theme_is_system_dark, .are_apps_dark = ui_theme_are_apps_dark, .set_preferred_app_mode = ui_theme_set_preferred_app_mode, .flush_menu_themes = ui_theme_flush_menu_themes, .allow_dark_mode_for_app = ui_theme_allow_dark_mode_for_app, .allow_dark_mode_for_window = ui_theme_allow_dark_mode_for_window, .refresh = ui_theme_refresh, }; // _______________________________ ui_toggle.c ________________________________ #include "rt/rt.h" static void ui_toggle_paint_on_off(ui_view_t* v) { const ui_ltrb_t i = ui_view.margins(v, &v->insets); int32_t x = v->x; int32_t y = v->y + i.top; ui_color_t c = ui_colors.darken(v->background, !ui_theme.is_app_dark() ? 0.125f : 0.5f); ui_color_t b = v->state.pressed ? ui_colors.tone_green : c; const int32_t a = v->fm->ascent; const int32_t d = v->fm->descent; const int32_t w = v->fm->em.w; int32_t r = ((a + d + 1) / 2) | 0x1; // radius must be odd int32_t h = r * 2 + 1; y += (v->h - i.top - i.bottom - h + 1) / 2; y += r + 1; // because radius is odd x += r; ui_color_t border = ui_theme.is_app_dark() ? ui_colors.darken(v->color, 0.5) : ui_colors.lighten(v->color, 0.5); if (v->state.hover) { border = ui_colors.get_color(ui_color_id_hot_tracking); } ui_gdi.circle(x, y, r, border, b); ui_gdi.circle(x + w - r, y, r, border, b); ui_gdi.fill(x, y - r, w - r + 1, h, b); ui_gdi.line(x, y - r, x + w - r + 1, y - r, border); ui_gdi.line(x, y + r, x + w - r + 1, y + r, border); int32_t x1 = v->state.pressed ? x + w - r : x; // circle is too bold in control color - water it down ui_color_t fill = ui_theme.is_app_dark() ? ui_colors.darken(v->color, 0.5f) : ui_colors.lighten(v->color, 0.5f); border = ui_theme.is_app_dark() ? ui_colors.darken(fill, 0.0625f) : ui_colors.lighten(fill, 0.0625f); ui_gdi.circle(x1, y, r - 2, border, fill); } static const char* ui_toggle_on_off_label(ui_view_t* v, char* label, int32_t count) { rt_str.format(label, count, "%s", ui_view.string(v)); char* s = strstr(label, "___"); if (s != null) { memcpy(s, v->state.pressed ? "On " : "Off", 3); } return rt_nls.str(label); } static void ui_toggle_measure(ui_view_t* v) { if (v->min_w_em < 3.0f) { rt_println("3.0f em minimum width"); v->min_w_em = 4.0f; } ui_view.measure_control(v); rt_assert(v->type == ui_view_toggle); } static void ui_toggle_paint(ui_view_t* v) { rt_assert(v->type == ui_view_toggle); char txt[rt_countof(v->p.text)]; const char* label = ui_toggle_on_off_label(v, txt, rt_countof(txt)); const char* text = rt_nls.str(label); ui_view.text_measure(v, text, &v->text); ui_view.text_align(v, &v->text); ui_toggle_paint_on_off(v); const ui_color_t text_color = !v->state.hover ? v->color : (ui_theme.is_app_dark() ? ui_colors.white : ui_colors.black); const ui_gdi_ta_t ta = { .fm = v->fm, .color = text_color }; ui_gdi.text(&ta, v->x + v->text.xy.x, v->y + v->text.xy.y, "%s", text); } static void ui_toggle_flip(ui_toggle_t* t) { ui_view.invalidate((ui_view_t*)t, null); t->state.pressed = !t->state.pressed; if (t->callback != null) { t->callback(t); } } static void ui_toggle_character(ui_view_t* v, const char* utf8) { char ch = utf8[0]; if (ui_view.is_shortcut_key(v, ch)) { ui_toggle_flip((ui_toggle_t*)v); } } static bool ui_toggle_key_pressed(ui_view_t* v, int64_t key) { const bool trigger = ui_app.alt && ui_view.is_shortcut_key(v, key); if (trigger) { ui_toggle_flip((ui_toggle_t*)v); } return trigger; // swallow if true } static bool ui_toggle_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (pressed && inside) { ui_toggle_flip((ui_toggle_t*)v); } return pressed && inside; } void ui_view_init_toggle(ui_view_t* v) { rt_assert(v->type == ui_view_toggle); v->tap = ui_toggle_tap; v->paint = ui_toggle_paint; v->measure = ui_toggle_measure; v->character = ui_toggle_character; v->key_pressed = ui_toggle_key_pressed; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; v->text_align = ui.align.left; if (v->debug.id == null) { v->debug.id = "#toggle"; } } void ui_toggle_init(ui_toggle_t* t, const char* label, fp32_t ems, void (*callback)(ui_toggle_t* b)) { ui_view.set_text(t, "%s", label); t->min_w_em = ems; t->callback = callback; t->type = ui_view_toggle; ui_view_init_toggle(t); } // ________________________________ ui_view.c _________________________________ #include "rt/rt.h" static const fp64_t ui_view_hover_delay = 1.5; // seconds #pragma push_macro("ui_view_for_each") static void ui_view_update_shortcut(ui_view_t* v); // adding and removing views is not expected to be frequent // actions by application code (human factor - UI design) // thus extra checks and verifications are there even in // release code because C is not type safety champion language. static inline void ui_view_check_type(ui_view_t* v) { // little endian: rt_static_assertion(('vwXX' & 0xFFFF0000U) == ('vwZZ' & 0xFFFF0000U)); rt_static_assertion((ui_view_stack & 0xFFFF0000U) == ('vwXX' & 0xFFFF0000U)); rt_swear(((uint32_t)v->type & 0xFFFF0000U) == ('vwXX' & 0xFFFF0000U), "not a view: %4.4s 0x%08X (forgotten &static_view?)", &v->type, v->type); } static void ui_view_verify(ui_view_t* p) { ui_view_check_type(p); ui_view_for_each(p, c, { ui_view_check_type(c); ui_view_update_shortcut(c); rt_swear(c->parent == p); rt_swear(c == c->next->prev); rt_swear(c == c->prev->next); }); } static ui_view_t* ui_view_add(ui_view_t* p, ...) { va_list va; va_start(va, p); ui_view_t* c = va_arg(va, ui_view_t*); while (c != null) { rt_swear(c->parent == null && c->prev == null && c->next == null); ui_view.add_last(p, c); c = va_arg(va, ui_view_t*); } va_end(va); ui_view_call_init(p); ui_app.request_layout(); return p; } static void ui_view_add_first(ui_view_t* p, ui_view_t* c) { rt_swear(c->parent == null && c->prev == null && c->next == null); c->parent = p; if (p->child == null) { c->prev = c; c->next = c; } else { c->prev = p->child->prev; c->next = p->child; c->prev->next = c; c->next->prev = c; } p->child = c; ui_view_call_init(c); ui_app.request_layout(); } static void ui_view_add_last(ui_view_t* p, ui_view_t* c) { rt_swear(c->parent == null && c->prev == null && c->next == null); c->parent = p; if (p->child == null) { c->prev = c; c->next = c; p->child = c; } else { c->prev = p->child->prev; c->next = p->child; c->prev->next = c; c->next->prev = c; } ui_view_call_init(c); ui_view_verify(p); ui_app.request_layout(); } static void ui_view_add_after(ui_view_t* c, ui_view_t* a) { rt_swear(c->parent == null && c->prev == null && c->next == null); rt_not_null(a->parent); c->parent = a->parent; c->next = a->next; c->prev = a; a->next = c; c->prev->next = c; c->next->prev = c; ui_view_call_init(c); ui_view_verify(c->parent); ui_app.request_layout(); } static void ui_view_add_before(ui_view_t* c, ui_view_t* b) { rt_swear(c->parent == null && c->prev == null && c->next == null); rt_not_null(b->parent); c->parent = b->parent; c->prev = b->prev; c->next = b; b->prev = c; c->prev->next = c; c->next->prev = c; ui_view_call_init(c); ui_view_verify(c->parent); ui_app.request_layout(); } static void ui_view_remove(ui_view_t* c) { rt_not_null(c->parent); rt_not_null(c->parent->child); // if a view that has focus is removed from parent: if (c == ui_app.focus) { ui_view.set_focus(null); } if (c->prev == c) { rt_swear(c->next == c); c->parent->child = null; } else { c->prev->next = c->next; c->next->prev = c->prev; if (c->parent->child == c) { c->parent->child = c->next; } } c->prev = null; c->next = null; ui_view_verify(c->parent); c->parent = null; ui_app.request_layout(); } static void ui_view_remove_all(ui_view_t* p) { while (p->child != null) { ui_view.remove(p->child); } ui_app.request_layout(); } static void ui_view_disband(ui_view_t* p) { // do not disband composite controls if (p->type != ui_view_mbx && p->type != ui_view_slider) { while (p->child != null) { ui_view_disband(p->child); ui_view.remove(p->child); } } ui_app.request_layout(); } static void ui_view_invalidate(const ui_view_t* v, const ui_rect_t* r) { if (ui_view.is_hidden(v)) { rt_println("hidden: %s", ui_view_debug_id(v)); } else { ui_rect_t rc = {0}; if (r != null) { rc = (ui_rect_t){ .x = v->x + r->x, .y = v->y + r->y, .w = r->w, .h = r->h }; } else { rc = (ui_rect_t){ v->x, v->y, v->w, v->h}; // expand view rectangle by padding const ui_ltrb_t p = ui_view.margins(v, &v->padding); rc.x -= p.left; rc.y -= p.top; rc.w += p.left + p.right; rc.h += p.top + p.bottom; } if (v->debug.trace.prc) { rt_println("%d,%d %dx%d", rc.x, rc.y, rc.w, rc.h); } ui_app.invalidate(&rc); } } static const char* ui_view_string(ui_view_t* v) { if (v->p.strid == 0) { int32_t id = rt_nls.strid(v->p.text); v->p.strid = id > 0 ? id : -1; } return v->p.strid < 0 ? v->p.text : // not localized rt_nls.string(v->p.strid, v->p.text); } static ui_wh_t ui_view_text_metrics_va(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, va_list va) { const ui_gdi_ta_t ta = { .fm = fm, .color = ui_colors.transparent, .measure = true }; return multiline ? ui_gdi.multiline_va(&ta, x, y, w, format, va) : ui_gdi.text_va(&ta, x, y, format, va); } static ui_wh_t ui_view_text_metrics(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, ...) { va_list va; va_start(va, format); ui_wh_t wh = ui_view_text_metrics_va(x, y, multiline, w, fm, format, va); va_end(va); return wh; } static void ui_view_text_measure(ui_view_t* v, const char* s, ui_view_text_metrics_t* tm) { const ui_fm_t* fm = v->fm; tm->wh = (ui_wh_t){ .w = 0, .h = fm->height }; if (s[0] == 0) { tm->multiline = false; } else { tm->multiline = strchr(s, '\n') != null; if (v->type == ui_view_label && tm->multiline) { int32_t w = (int32_t)((fp64_t)v->min_w_em * (fp64_t)fm->em.w + 0.5); tm->wh = ui_view.text_metrics(v->x, v->y, true, w, fm, "%s", s); } else { tm->wh = ui_view.text_metrics(v->x, v->y, false, 0, fm, "%s", s); } } } static void ui_view_text_align(ui_view_t* v, ui_view_text_metrics_t* tm) { tm->xy = (ui_point_t){ .x = -1, .y = -1 }; const ui_ltrb_t i = ui_view.margins(v, &v->insets); // i_wh the inside insets w x h: const ui_wh_t i_wh = { .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom }; const int32_t h_align = v->text_align & ~(ui.align.top|ui.align.bottom); const int32_t v_align = v->text_align & ~(ui.align.left|ui.align.right); tm->xy.x = i.left + (i_wh.w - tm->wh.w + 1) / 2; if (h_align & ui.align.left) { tm->xy.x = i.left; } else if (h_align & ui.align.right) { tm->xy.x = i_wh.w - tm->wh.w - i.right; } // vertical centering is trickier. // mt.h is height of all measured lines of text tm->xy.y = i.top + (i_wh.h - tm->wh.h + 1) / 2; if (v_align & ui.align.top) { tm->xy.y = i.top; } else if (v_align & ui.align.bottom) { tm->xy.y = i_wh.h - tm->wh.h - i.bottom; } else if (!tm->multiline) { #if 0 // TODO: doesn't look good or right: // UI controls should have x-height line in the dead center // of the control to be visually balanced. // y offset of "x-line" of the glyph: const ui_fm_t* fm = v->fm; const int32_t y_of_x_line = fm->baseline - fm->x_height; // `dy` offset of the center to x-line (middle of glyph cell) const int32_t dy = tm->wh.h / 2 - y_of_x_line; tm->xy.y += dy / 2; if (v->debug.trace.mt) { rt_println(" x-line: %d mt.h: %d mt.h / 2 - x_line: %d", y_of_x_line, tm->wh.h, dy); } #endif } } static void ui_view_measure_control(ui_view_t* v) { v->p.strid = 0; const char* s = ui_view.string(v); const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); v->w = (int32_t)((fp64_t)fm->em.w * (fp64_t)v->min_w_em + 0.5); v->h = (int32_t)((fp64_t)fm->em.h * (fp64_t)v->min_h_em + 0.5); if (v->debug.trace.mt) { const ui_ltrb_t p = ui_view.margins(v, &v->padding); rt_println(">%dx%d em: %dx%d min: %.3fx%.3f " "i: %d %d %d %d p: %d %d %d %d %s \"%.*s\"", v->w, v->h, fm->em.w, fm->em.h, v->min_w_em, v->min_h_em, i.left, i.top, i.right, i.bottom, p.left, p.top, p.right, p.bottom, ui_view_debug_id(v), rt_min(64, strlen(s)), s); const ui_margins_t in = v->insets; const ui_margins_t pd = v->padding; rt_println(" i: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f" " p: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f", in.left, in.top, in.right, in.bottom, in.left + in.right, in.top + in.bottom, pd.left, pd.top, pd.right, pd.bottom, pd.left + pd.right, pd.top + pd.bottom); } ui_view_text_measure(v, s, &v->text); if (v->debug.trace.mt) { rt_println(" mt: %d %d", v->text.wh.w, v->text.wh.h); } v->w = rt_max(v->w, i.left + v->text.wh.w + i.right); v->h = rt_max(v->h, i.top + v->text.wh.h + i.bottom); ui_view_text_align(v, &v->text); if (v->debug.trace.mt) { rt_println("<%dx%d text_align x,y: %d,%d %s", v->w, v->h, v->text.xy.x, v->text.xy.y, ui_view_debug_id(v)); } } static void ui_view_measure_children(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_for_each(v, c, { ui_view.measure(c); }); } } static void ui_view_measure(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_measure_children(v); if (v->prepare != null) { v->prepare(v); } if (v->measure != null && v->measure != ui_view_measure) { v->measure(v); } else { ui_view.measure_control(v); } if (v->measured != null) { v->measured(v); } } } static void ui_layout_view(ui_view_t* rt_unused(v)) { // ui_ltrb_t i = ui_view.margins(v, &v->insets); // ui_ltrb_t p = ui_view.margins(v, &v->padding); // rt_println(">%s %d,%d %dx%d p: %d %d %d %d i: %d %d %d %d", // v->p.text, v->x, v->y, v->w, v->h, // p.left, p.top, p.right, p.bottom, // i.left, i.top, i.right, i.bottom); // rt_println("<%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); } static void ui_view_layout_children(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_for_each(v, c, { ui_view.layout(c); }); } } static void ui_view_layout(ui_view_t* v) { // rt_println(">%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); if (!ui_view.is_hidden(v)) { if (v->layout != null && v->layout != ui_view_layout) { v->layout(v); } else { ui_layout_view(v); } if (v->composed != null) { v->composed(v); } ui_view_layout_children(v); } // rt_println("<%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); } static bool ui_view_inside(const ui_view_t* v, const ui_point_t* pt) { const int32_t x = pt->x - v->x; const int32_t y = pt->y - v->y; return 0 <= x && x < v->w && 0 <= y && y < v->h; } static bool ui_view_is_parent_of(const ui_view_t* parent, const ui_view_t* child) { rt_swear(parent != null && child != null); const ui_view_t* p = child->parent; while (p != null) { if (parent == p) { return true; } p = p->parent; } return false; } static ui_ltrb_t ui_view_margins(const ui_view_t* v, const ui_margins_t* m) { const fp64_t gw = (fp64_t)m->left + (fp64_t)m->right; const fp64_t gh = (fp64_t)m->top + (fp64_t)m->bottom; const ui_wh_t* em = &v->fm->em; const int32_t em_w = (int32_t)(em->w * gw + 0.5); const int32_t em_h = (int32_t)(em->h * gh + 0.5); const int32_t left = (int32_t)((fp64_t)em->w * (fp64_t)m->left + 0.5); const int32_t top = (int32_t)((fp64_t)em->h * (fp64_t)m->top + 0.5); return (ui_ltrb_t) { .left = left, .top = top, .right = em_w - left, .bottom = em_h - top }; } static void ui_view_inbox(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* insets) { rt_swear(r != null || insets != null); rt_swear(v->max_w >= 0 && v->max_h >= 0); const ui_ltrb_t i = ui_view_margins(v, &v->insets); if (insets != null) { *insets = i; } if (r != null) { *r = (ui_rect_t) { .x = v->x + i.left, .y = v->y + i.top, .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom, }; } } static void ui_view_outbox(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* padding) { rt_swear(r != null || padding != null); rt_swear(v->max_w >= 0 && v->max_h >= 0); const ui_ltrb_t p = ui_view_margins(v, &v->padding); if (padding != null) { *padding = p; } if (r != null) { // rt_println("%s %d,%d %dx%d %.1f %.1f %.1f %.1f", v->p.text, // v->x, v->y, v->w, v->h, // v->padding.left, v->padding.top, v->padding.right, v->padding.bottom); *r = (ui_rect_t) { .x = v->x - p.left, .y = v->y - p.top, .w = v->w + p.left + p.right, .h = v->h + p.top + p.bottom, }; // rt_println("%s %d,%d %dx%d", v->p.text, // r->x, r->y, r->w, r->h); } } static void ui_view_update_shortcut(ui_view_t* v) { if (ui_view.is_control(v) && v->type != ui_view_text && v->shortcut == 0x00) { const char* s = ui_view.string(v); const char* a = strchr(s, '&'); if (a != null && a[1] != 0 && a[1] != '&') { // TODO: utf-8 shortcuts? possible v->shortcut = a[1]; } } } static void ui_view_set_text_va(ui_view_t* v, const char* format, va_list va) { char t[rt_countof(v->p.text)]; rt_str.format_va(t, rt_countof(t), format, va); char* s = v->p.text; if (strcmp(s, t) != 0) { int32_t n = (int32_t)strlen(t); memcpy(s, t, (size_t)n + 1); v->p.strid = 0; // next call to nls() will localize it ui_view_update_shortcut(v); ui_app.request_layout(); } } static void ui_view_set_text(ui_view_t* v, const char* format, ...) { va_list va; va_start(va, format); ui_view.set_text_va(v, format, va); va_end(va); } static void ui_view_show_hint(ui_view_t* v, ui_view_t* hint) { ui_view_call_init(hint); ui_view.set_text(hint, v->hint); ui_view.measure(hint); int32_t x = v->x + v->w / 2 - hint->w / 2 + hint->fm->em.w / 4; int32_t y = v->y + v->h + hint->fm->em.h / 4; if (x + hint->w > ui_app.crc.w) { x = ui_app.crc.w - hint->w - hint->fm->em.w / 2; } if (x < 0) { x = hint->fm->em.w / 2; } if (y + hint->h > ui_app.crc.h) { y = ui_app.crc.h - hint->h - hint->fm->em.h / 2; } if (y < 0) { y = hint->fm->em.h / 2; } // show_tooltip will center horizontally ui_app.show_hint(hint, x + hint->w / 2, y, 0); } static void ui_view_hovering(ui_view_t* v, bool start) { static ui_label_t hint = ui_label(0.0, ""); if (start && ui_app.animating.view == null && v->hint[0] != 0 && !ui_view.is_hidden(v)) { hint.padding = (ui_margins_t){0, 0, 0, 0}; hint.parent = ui_app.content; hint.state.hidden = false; ui_view_show_hint(v, &hint); } else if (!start && ui_app.animating.view == &hint) { ui_app.show_hint(null, -1, -1, 0); } } static bool ui_view_is_shortcut_key(ui_view_t* v, int64_t key) { // Supported keyboard shortcuts are ASCII characters only for now // If there is not focused UI control in Alt+key [Alt] is optional. // If there is focused control only Alt+Key is accepted as shortcut char ch = 0x20 <= key && key <= 0x7F ? (char)toupper((char)key) : 0x00; bool needs_alt = ui_app.focus != null && ui_app.focus != v && !ui_view.is_parent_of(ui_app.focus, v); bool keyboard_shortcut = ch != 0x00 && v->shortcut != 0x00 && (ui_app.alt || ui_app.ctrl || !needs_alt) && toupper(v->shortcut) == ch; return keyboard_shortcut; } static bool ui_view_is_orphan(const ui_view_t* v) { while (v != ui_app.root && v != null) { v = v->parent; } return v == null; } static bool ui_view_is_hidden(const ui_view_t* v) { bool hidden = v->state.hidden || ui_view.is_orphan(v); while (!hidden && v->parent != null) { v = v->parent; hidden = v->state.hidden; } return hidden; } static bool ui_view_is_disabled(const ui_view_t* v) { bool disabled = v->state.disabled; while (!disabled && v->parent != null) { v = v->parent; disabled = v->state.disabled; } return disabled; } static void ui_view_timer(ui_view_t* v, ui_timer_t id) { if (v->timer != null) { v->timer(v, id); } // timers are delivered even to hidden and disabled views: ui_view_for_each(v, c, { ui_view_timer(c, id); }); } static void ui_view_every_sec(ui_view_t* v) { if (v->every_sec != null) { v->every_sec(v); } ui_view_for_each(v, c, { ui_view_every_sec(c); }); } static void ui_view_every_100ms(ui_view_t* v) { if (v->every_100ms != null) { v->every_100ms(v); } ui_view_for_each(v, c, { ui_view_every_100ms(c); }); } static bool ui_view_key_pressed(ui_view_t* v, int64_t k) { bool done = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->key_pressed != null) { ui_view_update_shortcut(v); done = v->key_pressed(v, k); } if (!done) { ui_view_for_each(v, c, { done = ui_view_key_pressed(c, k); if (done) { break; } }); } } return done; } static bool ui_view_key_released(ui_view_t* v, int64_t k) { bool done = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->key_released != null) { done = v->key_released(v, k); } if (!done) { ui_view_for_each(v, c, { done = ui_view_key_released(c, k); if (done) { break; } }); } } return done; } static void ui_view_character(ui_view_t* v, const char* utf8) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->character != null) { ui_view_update_shortcut(v); v->character(v, utf8); } ui_view_for_each(v, c, { ui_view_character(c, utf8); }); } } static void ui_view_resolve_color_ids(ui_view_t* v) { if (v->color_id > 0) { v->color = ui_colors.get_color(v->color_id); } if (v->background_id > 0) { v->background = ui_colors.get_color(v->background_id); } } static void ui_view_paint(ui_view_t* v) { rt_assert(ui_app.crc.w > 0 && ui_app.crc.h > 0); ui_view_resolve_color_ids(v); if (v->debug.trace.prc) { const char* s = ui_view.string(v); rt_println("%d,%d %dx%d prc: %d,%d %dx%d \"%.*s\"", v->x, v->y, v->w, v->h, ui_app.prc.x, ui_app.prc.y, ui_app.prc.w, ui_app.prc.h, rt_min(64, strlen(s)), s); } if (!v->state.hidden && ui_app.crc.w > 0 && ui_app.crc.h > 0) { if (v->erase != null) { v->erase(v); } if (v->paint != null) { v->paint(v); } if (v->painted != null) { v->painted(v); } if (v->debug.paint.margins) { ui_view.debug_paint_margins(v); } if (v->debug.paint.fm) { ui_view.debug_paint_fm(v); } if (v->debug.paint.call && v->debug_paint != null) { v->debug_paint(v); } ui_view_for_each(v, c, { ui_view_paint(c); }); } } static bool ui_view_has_focus(const ui_view_t* v) { return ui_app.focused() && ui_app.focus == v; } static void ui_view_set_focus(ui_view_t* v) { if (ui_app.focus != v) { ui_view_t* loosing = ui_app.focus; ui_view_t* gaining = v; if (gaining != null) { rt_swear(gaining->focusable && !ui_view.is_hidden(gaining) && !ui_view.is_disabled(gaining)); } if (loosing != null) { rt_swear(loosing->focusable); } ui_app.focus = v; if (loosing != null && loosing->focus_lost != null) { loosing->focus_lost(loosing); } if (gaining != null && gaining->focus_gained != null) { gaining->focus_gained(gaining); } } } static int64_t ui_view_hit_test(const ui_view_t* v, ui_point_t pt) { int64_t ht = ui.hit_test.nowhere; if (!ui_view.is_hidden(v) && v->hit_test != null) { ht = v->hit_test(v, pt); } if (ht == ui.hit_test.nowhere) { ui_view_for_each(v, c, { if (!c->state.hidden && ui_view.inside(c, &pt)) { ht = ui_view_hit_test(c, pt); if (ht != ui.hit_test.nowhere) { break; } } }); } return ht; } static void ui_view_update_hover(ui_view_t* v, bool hidden) { const bool hover = v->state.hover; const bool inside = ui_view.inside(v, &ui_app.mouse); v->state.hover = !ui_view.is_hidden(v) && inside; if (hover != v->state.hover) { // rt_println("hover := %d %p %s", v->state.hover, v, ui_view_debug_id(v)); ui_view.hover_changed(v); // even for hidden if (!hidden) { ui_view.invalidate(v, null); } } } static void ui_view_mouse_hover(ui_view_t* v) { // rt_println("%d,%d %s", ui_app.mouse.x, ui_app.mouse.y, // ui_app.mouse_left ? "L" : "_", // ui_app.mouse_right ? "R" : "_"); // mouse hover over is dispatched even to disabled views const bool hidden = ui_view.is_hidden(v); ui_view_update_hover(v, hidden); if (!hidden && v->mouse_hover != null) { v->mouse_hover(v); } ui_view_for_each(v, c, { ui_view_mouse_hover(c); }); } static void ui_view_mouse_move(ui_view_t* v) { // rt_println("%d,%d %s", ui_app.mouse.x, ui_app.mouse.y, // ui_app.mouse_left ? "L" : "_", // ui_app.mouse_right ? "R" : "_"); // mouse move is dispatched even to disabled views const bool hidden = ui_view.is_hidden(v); ui_view_update_hover(v, hidden); if (!hidden && v->mouse_move != null) { v->mouse_move(v); } ui_view_for_each(v, c, { ui_view_mouse_move(c); }); } static void ui_view_double_click(ui_view_t* v, int32_t ix) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { if (v->focusable) { ui_view.set_focus(v); } if (v->double_click != null) { v->double_click(v, ix); } } ui_view_for_each(v, c, { ui_view_double_click(c, ix); }); } } static void ui_view_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->mouse_scroll != null) { v->mouse_scroll(v, dx_dy); } ui_view_for_each(v, c, { ui_view_mouse_scroll(c, dx_dy); }); } } static void ui_view_hover_changed(ui_view_t* v) { if (!v->state.hidden) { if (!v->state.hover) { v->p.hover_when = 0; ui_view.hovering(v, false); // cancel hover } else { rt_swear(ui_view_hover_delay >= 0); if (v->p.hover_when >= 0) { v->p.hover_when = ui_app.now + ui_view_hover_delay; } } } } static void ui_view_lose_hidden_focus(ui_view_t* v) { // removes focus from hidden or disabled ui controls if (ui_app.focus != null) { if (ui_app.focus == v && (v->state.disabled || v->state.hidden)) { ui_view.set_focus(null); } else { ui_view_for_each(v, c, { if (ui_app.focus != null) { ui_view_lose_hidden_focus(c); } }); } } } static bool ui_view_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_tap(c, ix, pressed); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && pressed && inside) { if (v->focusable) { ui_view.set_focus(v); } if (v->tap != null) { swallow = v->tap(v, ix, pressed); } } if (!swallow && !pressed) { // mouse click release is never swallowed because a lot // of controls want to hear it: if (v->tap != null) { (void)v->tap(v, ix, pressed); } } } return swallow; } static bool ui_view_long_press(ui_view_t* v, int32_t ix) { bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_long_press(c, ix); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->long_press != null) { swallow = v->long_press(v, ix); } } return swallow; } static bool ui_view_double_tap(ui_view_t* v, int32_t ix) { // 0: left 1: middle 2: right bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_double_tap(c, ix); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->double_tap != null) { swallow = v->double_tap(v, ix); } } return swallow; } static bool ui_view_context_menu(ui_view_t* v) { bool swallow = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_context_menu(c); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->context_menu != null) { swallow = v->context_menu(v); } } return swallow; } static bool ui_view_message(ui_view_t* view, int32_t m, int64_t wp, int64_t lp, int64_t* ret) { if (!view->state.hidden) { if (view->p.hover_when > 0 && ui_app.now > view->p.hover_when) { view->p.hover_when = -1; // "already called" ui_view.hovering(view, true); } } // message() callback is called even for hidden and disabled views // could be useful for enabling conditions of post() messages from // background rt_thread. if (view->message != null) { if (view->message(view, m, wp, lp, ret)) { return true; } } ui_view_for_each(view, c, { if (ui_view_message(c, m, wp, lp, ret)) { return true; } }); return false; } static bool ui_view_is_container(const ui_view_t* v) { return v->type == ui_view_stack || v->type == ui_view_span || v->type == ui_view_list; } static bool ui_view_is_spacer(const ui_view_t* v) { return v->type == ui_view_spacer; } static bool ui_view_is_control(const ui_view_t* v) { return v->type == ui_view_text || v->type == ui_view_label || v->type == ui_view_toggle || v->type == ui_view_button || v->type == ui_view_slider || v->type == ui_view_mbx; } static void ui_view_debug_paint_margins(ui_view_t* v) { if (v->debug.paint.margins) { if (v->type == ui_view_spacer) { ui_gdi.fill(v->x, v->y, v->w, v->h, ui_color_rgb(128, 128, 128)); } const ui_ltrb_t p = ui_view.margins(v, &v->padding); const ui_ltrb_t i = ui_view.margins(v, &v->insets); ui_color_t c = ui_colors.green; const int32_t pl = p.left; const int32_t pr = p.right; const int32_t pt = p.top; const int32_t pb = p.bottom; if (pl > 0) { ui_gdi.frame(v->x - pl, v->y, pl, v->h, c); } if (pr > 0) { ui_gdi.frame(v->x + v->w, v->y, pr, v->h, c); } if (pt > 0) { ui_gdi.frame(v->x, v->y - pt, v->w, pt, c); } if (p.bottom > 0) { ui_gdi.frame(v->x, v->y + v->h, v->w, pb, c); } c = ui_colors.orange; const int32_t il = i.left; const int32_t ir = i.right; const int32_t it = i.top; const int32_t ib = i.bottom; if (il > 0) { ui_gdi.frame(v->x, v->y, il, v->h, c); } if (ir > 0) { ui_gdi.frame(v->x + v->w - ir, v->y, ir, v->h, c); } if (it > 0) { ui_gdi.frame(v->x, v->y, v->w, it, c); } if (ib > 0) { ui_gdi.frame(v->x, v->y + v->h - ib, v->w, ib, c); } if ((ui_view.is_container(v) || ui_view.is_spacer(v)) && v->w > 0 && v->h > 0) { ui_wh_t wh = ui_view_text_metrics(v->x, v->y, false, 0, v->fm, "%s", ui_view.string(v)); const int32_t tx = v->x; const int32_t ty = v->y + v->h - wh.h; const ui_gdi_ta_t ta = { .fm = v->fm, .color = ui_colors.red }; ui_gdi.text(&ta, tx, ty, "%s %d,%d %dx%d", ui_view_debug_id(v), v->x, v->y, v->w, v->h); } } } static void ui_view_debug_paint_fm(ui_view_t* v) { if (v->debug.paint.fm && v->p.text[0] != 0 && !ui_view_is_container(v) && !ui_view_is_spacer(v)) { const ui_point_t t = v->text.xy; const int32_t x = v->x; const int32_t y = v->y; const int32_t w = v->w; const int32_t y_0 = y + t.y; const int32_t y_b = y_0 + v->fm->baseline; const int32_t y_a = y_b - v->fm->ascent; const int32_t y_h = y_0 + v->fm->height; const int32_t y_x = y_b - v->fm->x_height; const int32_t y_d = y_b + v->fm->descent; // fm.height y == 0 line is painted one pixel higher: ui_gdi.line(x, y_0 - 1, x + w, y_0 - 1, ui_colors.red); ui_gdi.line(x, y_a, x + w, y_a, ui_colors.green); ui_gdi.line(x, y_x, x + w, y_x, ui_colors.orange); ui_gdi.line(x, y_b, x + w, y_b, ui_colors.red); ui_gdi.line(x, y_d, x + w, y_d, ui_colors.green); if (y_h != y_d) { ui_gdi.line(x, y_d, x + w, y_d, ui_colors.green); ui_gdi.line(x, y_h, x + w, y_h, ui_colors.red); } else { ui_gdi.line(x, y_h, x + w, y_h, ui_colors.orange); } // fm.height line painted _under_ the actual height } } #pragma push_macro("ui_view_no_siblings") #define ui_view_no_siblings(v) do { \ rt_swear((v)->parent == null && (v)->child == null && \ (v)->prev == null && (v)->next == null); \ } while (0) static void ui_view_test(void) { ui_view_t p0 = ui_view(stack); ui_view_t c1 = ui_view(stack); ui_view_t c2 = ui_view(stack); ui_view_t c3 = ui_view(stack); ui_view_t c4 = ui_view(stack); ui_view_t g1 = ui_view(stack); ui_view_t g2 = ui_view(stack); ui_view_t g3 = ui_view(stack); ui_view_t g4 = ui_view(stack); // add grand children to children: ui_view.add(&c2, &g1, &g2, null); ui_view_verify(&c2); ui_view.add(&c3, &g3, &g4, null); ui_view_verify(&c3); // single child ui_view.add(&p0, &c1, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); // two children ui_view.add(&p0, &c1, &c2, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); // three children ui_view.add(&p0, &c1, &c2, &c3, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); ui_view.remove(&c3); ui_view_verify(&p0); // add_first, add_last, add_before, add_after ui_view.add_first(&p0, &c1); ui_view_verify(&p0); rt_swear(p0.child == &c1); ui_view.add_last(&p0, &c4); ui_view_verify(&p0); rt_swear(p0.child == &c1 && p0.child->prev == &c4); ui_view.add_after(&c2, &c1); ui_view_verify(&p0); rt_swear(p0.child == &c1); rt_swear(c1.next == &c2); ui_view.add_before(&c3, &c4); ui_view_verify(&p0); rt_swear(p0.child == &c1); rt_swear(c4.prev == &c3); // removing all ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); ui_view.remove(&c3); ui_view_verify(&p0); ui_view.remove(&c4); ui_view_verify(&p0); ui_view_no_siblings(&p0); ui_view_no_siblings(&c1); ui_view_no_siblings(&c4); ui_view.remove(&g1); ui_view_verify(&c2); ui_view.remove(&g2); ui_view_verify(&c2); ui_view.remove(&g3); ui_view_verify(&c3); ui_view.remove(&g4); ui_view_verify(&c3); ui_view_no_siblings(&c2); ui_view_no_siblings(&c3); ui_view_no_siblings(&g1); ui_view_no_siblings(&g2); ui_view_no_siblings(&g3); ui_view_no_siblings(&g4); // a bit more intuitive (for a human) nested way to initialize tree: ui_view.add(&p0, &c1, ui_view.add(&c2, &g1, &g2, null), ui_view.add(&c3, &g3, &g4, null), &c4); ui_view_verify(&p0); ui_view_disband(&p0); ui_view_no_siblings(&p0); ui_view_no_siblings(&c1); ui_view_no_siblings(&c2); ui_view_no_siblings(&c3); ui_view_no_siblings(&c4); ui_view_no_siblings(&g1); ui_view_no_siblings(&g2); ui_view_no_siblings(&g3); ui_view_no_siblings(&g4); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("ui_view_no_siblings") ui_view_if ui_view = { .add = ui_view_add, .add_first = ui_view_add_first, .add_last = ui_view_add_last, .add_after = ui_view_add_after, .add_before = ui_view_add_before, .remove = ui_view_remove, .remove_all = ui_view_remove_all, .disband = ui_view_disband, .inside = ui_view_inside, .is_parent_of = ui_view_is_parent_of, .margins = ui_view_margins, .inbox = ui_view_inbox, .outbox = ui_view_outbox, .set_text = ui_view_set_text, .set_text_va = ui_view_set_text_va, .invalidate = ui_view_invalidate, .text_metrics_va = ui_view_text_metrics_va, .text_metrics = ui_view_text_metrics, .text_measure = ui_view_text_measure, .text_align = ui_view_text_align, .measure_control = ui_view_measure_control, .measure_children = ui_view_measure_children, .layout_children = ui_view_layout_children, .measure = ui_view_measure, .layout = ui_view_layout, .string = ui_view_string, .is_orphan = ui_view_is_orphan, .is_hidden = ui_view_is_hidden, .is_disabled = ui_view_is_disabled, .is_control = ui_view_is_control, .is_container = ui_view_is_container, .is_spacer = ui_view_is_spacer, .timer = ui_view_timer, .every_sec = ui_view_every_sec, .every_100ms = ui_view_every_100ms, .hit_test = ui_view_hit_test, .key_pressed = ui_view_key_pressed, .key_released = ui_view_key_released, .character = ui_view_character, .paint = ui_view_paint, .has_focus = ui_view_has_focus, .set_focus = ui_view_set_focus, .lose_hidden_focus = ui_view_lose_hidden_focus, .mouse_hover = ui_view_mouse_hover, .mouse_move = ui_view_mouse_move, .mouse_scroll = ui_view_mouse_scroll, .hovering = ui_view_hovering, .hover_changed = ui_view_hover_changed, .is_shortcut_key = ui_view_is_shortcut_key, .context_menu = ui_view_context_menu, .tap = ui_view_tap, .long_press = ui_view_long_press, .double_tap = ui_view_double_tap, .message = ui_view_message, .debug_paint_margins = ui_view_debug_paint_margins, .debug_paint_fm = ui_view_debug_paint_fm, .test = ui_view_test }; #ifdef UI_VIEW_TEST rt_static_init(ui_view) { ui_view.test(); } #endif #pragma pop_macro("ui_view_for_each") #endif // ui_implementation ================================================ FILE: src/rt/rt.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // #include "ut/macos.h" // TODO // #include "ut/linux.h" // TODO ================================================ FILE: src/rt/rt_args.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static void* rt_args_memory; static void rt_args_main(int32_t argc, const char* argv[], const char** env) { rt_swear(rt_args.c == 0 && rt_args.v == null && rt_args.env == null); rt_swear(rt_args_memory == null); rt_args.c = argc; rt_args.v = argv; rt_args.env = env; } static int32_t rt_args_option_index(const char* option) { for (int32_t i = 1; i < rt_args.c; i++) { if (strcmp(rt_args.v[i], "--") == 0) { break; } // no options after '--' if (strcmp(rt_args.v[i], option) == 0) { return i; } } return -1; } static void rt_args_remove_at(int32_t ix) { // returns new argc rt_assert(0 < rt_args.c); rt_assert(0 < ix && ix < rt_args.c); // cannot remove rt_args.v[0] for (int32_t i = ix; i < rt_args.c; i++) { rt_args.v[i] = rt_args.v[i + 1]; } rt_args.v[rt_args.c - 1] = ""; rt_args.c--; } static bool rt_args_option_bool(const char* option) { int32_t ix = rt_args_option_index(option); if (ix > 0) { rt_args_remove_at(ix); } return ix > 0; } static bool rt_args_option_int(const char* option, int64_t *value) { int32_t ix = rt_args_option_index(option); if (ix > 0 && ix < rt_args.c - 1) { const char* s = rt_args.v[ix + 1]; int32_t base = (strstr(s, "0x") == s || strstr(s, "0X") == s) ? 16 : 10; const char* b = s + (base == 10 ? 0 : 2); char* e = null; errno = 0; int64_t v = strtoll(b, &e, base); if (errno == 0 && e > b && *e == 0) { *value = v; } else { ix = -1; } } else { ix = -1; } if (ix > 0) { rt_args_remove_at(ix); // remove option rt_args_remove_at(ix); // remove following number } return ix > 0; } static const char* rt_args_option_str(const char* option) { int32_t ix = rt_args_option_index(option); const char* s = null; if (ix > 0 && ix < rt_args.c - 1) { s = rt_args.v[ix + 1]; } else { ix = -1; } if (ix > 0) { rt_args_remove_at(ix); // remove option rt_args_remove_at(ix); // remove following string } return ix > 0 ? s : null; } // Terminology: "quote" in the code and comments below // actually refers to "fp64_t quote mark" and used for brevity. // TODO: posix like systems // Looks like all shells support quote marks but // AFAIK MacOS bash and zsh also allow (and prefer) backslash escaped // space character. Unclear what other escaping shell and posix compliant // parser should support. // Lengthy discussion here: // https://stackoverflow.com/questions/1706551/parse-string-into-argv-argc // Microsoft specific argument parsing: // https://web.archive.org/web/20231115181633/http://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=msvc-170 // Alternative: just use CommandLineToArgvW() typedef struct { const char* s; char* d; const char* e; } rt_args_pair_t; static rt_args_pair_t rt_args_parse_backslashes(rt_args_pair_t p) { enum { quote = '"', backslash = '\\' }; const char* s = p.s; char* d = p.d; rt_swear(*s == backslash); int32_t bsc = 0; // number of backslashes while (*s == backslash) { s++; bsc++; } if (*s == quote) { while (bsc > 1 && d < p.e) { *d++ = backslash; bsc -= 2; } if (bsc == 1 && d < p.e) { *d++ = *s++; } } else { // Backslashes are interpreted literally, // unless they immediately precede a quote: while (bsc > 0 && d < p.e) { *d++ = backslash; bsc--; } } return (rt_args_pair_t){ .s = s, .d = d, .e = p.e }; } static rt_args_pair_t rt_args_parse_quoted(rt_args_pair_t p) { enum { quote = '"', backslash = '\\' }; const char* s = p.s; char* d = p.d; rt_swear(*s == quote); s++; // opening quote (skip) while (*s != 0x00) { if (*s == backslash) { p = rt_args_parse_backslashes((rt_args_pair_t){ .s = s, .d = d, .e = p.e }); s = p.s; d = p.d; } else if (*s == quote && s[1] == quote) { // Within a quoted string, a pair of quote is // interpreted as a single escaped quote. if (d < p.e) { *d++ = *s++; } s++; // 1 for 2 quotes } else if (*s == quote) { s++; // closing quote (skip) break; } else if (d < p.e) { *d++ = *s++; } } return (rt_args_pair_t){ .s = s, .d = d, .e = p.e }; } static void rt_args_parse(const char* s) { rt_swear(s[0] != 0, "cannot parse empty string"); rt_swear(rt_args.c == 0); rt_swear(rt_args.v == null); rt_swear(rt_args_memory == null); enum { quote = '"', backslash = '\\', tab = '\t', space = 0x20 }; const int32_t len = (int32_t)strlen(s); // Worst-case scenario (possible to optimize with dry run of parse) // at least 2 characters per token in "a b c d e" plush null at the end: const int32_t k = ((len + 2) / 2 + 1) * (int32_t)sizeof(void*) + (int32_t)sizeof(void*); const int32_t n = k + (len + 2) * (int32_t)sizeof(char); rt_fatal_if_error(rt_heap.allocate(null, &rt_args_memory, n, true)); rt_args.c = 0; rt_args.v = (const char**)rt_args_memory; char* d = (char*)(((char*)rt_args.v) + k); char* e = d + n; // end of memory // special rules for 1st argument: if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } if (*s == quote) { s++; while (*s != 0x00 && *s != quote && d < e) { *d++ = *s++; } if (*s == quote) { // // closing quote s++; // skip closing quote *d++ = 0x00; } else { while (*s != 0x00) { s++; } } } else { while (*s != 0x00 && *s != space && *s != tab && d < e) { *d++ = *s++; } } if (d < e) { *d++ = 0; } while (d < e) { while (*s == space || *s == tab) { s++; } if (*s == 0) { break; } if (*s == quote && s[1] == 0 && d < e) { // unbalanced single quote if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } // spec does not say what to do *d++ = *s++; } else if (*s == quote) { // quoted arg if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } rt_args_pair_t p = rt_args_parse_quoted( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else { // non-quoted arg (that can have quoted strings inside) if (rt_args.c < n) { rt_args.v[rt_args.c++] = d; } while (*s != 0) { if (*s == backslash) { rt_args_pair_t p = rt_args_parse_backslashes( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else if (*s == quote) { rt_args_pair_t p = rt_args_parse_quoted( (rt_args_pair_t){ .s = s, .d = d, .e = e }); s = p.s; d = p.d; } else if (*s == tab || *s == space) { break; } else if (d < e) { *d++ = *s++; } } } if (d < e) { *d++ = 0; } } if (rt_args.c < n) { rt_args.v[rt_args.c] = null; } rt_swear(rt_args.c < n, "not enough memory - adjust guestimates"); rt_swear(d <= e, "not enough memory - adjust guestimates"); } static const char* rt_args_basename(void) { static char basename[260]; rt_swear(rt_args.c > 0); if (basename[0] == 0) { const char* s = rt_args.v[0]; const char* b = s; while (*s != 0) { if (*s == '\\' || *s == '/') { b = s + 1; } s++; } int32_t n = rt_str.len(b); rt_swear(n < rt_countof(basename)); strncpy(basename, b, rt_countof(basename) - 1); char* d = basename + n - 1; while (d > basename && *d != '.') { d--; } if (*d == '.') { *d = 0x00; } } return basename; } static void rt_args_fini(void) { rt_heap.deallocate(null, rt_args_memory); // can be null is parse() was not called rt_args_memory = null; rt_args.c = 0; rt_args.v = null; } static void rt_args_WinMain(void) { rt_swear(rt_args.c == 0 && rt_args.v == null && rt_args.env == null); rt_swear(rt_args_memory == null); const uint16_t* wcl = GetCommandLineW(); int32_t n = (int32_t)rt_str.len16(wcl); char* cl = null; rt_fatal_if_error(rt_heap.allocate(null, (void**)&cl, n * 2 + 1, false)); rt_str.utf16to8(cl, n * 2 + 1, wcl, -1); rt_args_parse(cl); rt_heap.deallocate(null, cl); rt_args.env = (const char**)(void*)_environ; } #ifdef RT_TESTS // https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments // Command-line input argv[1] argv[2] argv[3] // "a b c" d e a b c d e // "ab\"c" "\\" d ab"c \ d // a\\\b d"e f"g h a\\\b de fg h // a\\\"b c d a\"b c d // a\\\\"b c" d e a\\b c d e // a"b"" c d ab" c d #ifndef __INTELLISENSE__ // confused data analysis static void rt_args_test_verify(const char* cl, int32_t expected, ...) { if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { rt_println("cl: `%s`", cl); } int32_t argc = rt_args.c; const char** argv = rt_args.v; void* memory = rt_args_memory; rt_args.c = 0; rt_args.v = null; rt_args_memory = null; rt_args_parse(cl); va_list va; va_start(va, expected); for (int32_t i = 0; i < expected; i++) { const char* s = va_arg(va, const char*); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("rt_args.v[%d]: `%s` expected: `%s`", i, rt_args.v[i], s); // } // Warning 6385: reading data outside of array const char* ai = _Pragma("warning(suppress: 6385)")rt_args.v[i]; rt_swear(strcmp(ai, s) == 0, "rt_args.v[%d]: `%s` expected: `%s`", i, ai, s); } va_end(va); rt_args.fini(); // restore command line arguments: rt_args.c = argc; rt_args.v = argv; rt_args_memory = memory; } #endif // __INTELLISENSE__ static void rt_args_test(void) { // The first argument (rt_args.v[0]) is treated specially. // It represents the program name. Because it must be a valid pathname, // parts surrounded by quote (") are allowed. The quote aren't included // in the rt_args.v[0] output. The parts surrounded by quote prevent // interpretation of a space or tab character as the end of the argument. // The escaping rules don't apply. rt_args_test_verify("\"c:\\foo\\bar\\snafu.exe\"", 1, "c:\\foo\\bar\\snafu.exe"); rt_args_test_verify("c:\\foo\\bar\\snafu.exe", 1, "c:\\foo\\bar\\snafu.exe"); rt_args_test_verify("foo.exe \"a b c\" d e", 4, "foo.exe", "a b c", "d", "e"); rt_args_test_verify("foo.exe \"ab\\\"c\" \"\\\\\" d", 4, "foo.exe", "ab\"c", "\\", "d"); rt_args_test_verify("foo.exe a\\\\\\b d\"e f\"g h", 4, "foo.exe", "a\\\\\\b", "de fg", "h"); rt_args_test_verify("foo.exe a\\\\\\b d\"e f\"g h", 4, "foo.exe", "a\\\\\\b", "de fg", "h"); rt_args_test_verify("foo.exe a\"b\"\" c d", 2, // unmatched quote "foo.exe", "ab\" c d"); // unbalanced quote and backslash: rt_args_test_verify("foo.exe \"", 2, "foo.exe", "\""); rt_args_test_verify("foo.exe \\", 2, "foo.exe", "\\"); rt_args_test_verify("foo.exe \\\\", 2, "foo.exe", "\\\\"); rt_args_test_verify("foo.exe \\\\\\", 2, "foo.exe", "\\\\\\"); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_args_test(void) {} #endif rt_args_if rt_args = { .main = rt_args_main, .WinMain = rt_args_WinMain, .option_index = rt_args_option_index, .remove_at = rt_args_remove_at, .option_bool = rt_args_option_bool, .option_int = rt_args_option_int, .option_str = rt_args_option_str, .basename = rt_args_basename, .fini = rt_args_fini, .test = rt_args_test }; ================================================ FILE: src/rt/rt_atomics.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" #include // needs cl.exe /experimental:c11atomics command line // see: https://developercommunity.visualstudio.com/t/C11--C17-include-stdatomich-issue/10620622 #pragma warning(push) #pragma warning(disable: 4746) // volatile access of 'int32_var' is subject to /volatile: setting; consider using __iso_volatile_load/store intrinsic functions #ifndef UT_ATOMICS_HAS_STDATOMIC_H static int32_t rt_atomics_increment_int32(volatile int32_t* a) { return InterlockedIncrement((volatile LONG*)a); } static int32_t rt_atomics_decrement_int32(volatile int32_t* a) { return InterlockedDecrement((volatile LONG*)a); } static int64_t rt_atomics_increment_int64(volatile int64_t* a) { return InterlockedIncrement64((__int64 volatile *)a); } static int64_t rt_atomics_decrement_int64(volatile int64_t* a) { return InterlockedDecrement64((__int64 volatile *)a); } static int32_t rt_atomics_add_int32(volatile int32_t* a, int32_t v) { return InterlockedAdd((LONG volatile *)a, v); } static int64_t rt_atomics_add_int64(volatile int64_t* a, int64_t v) { return InterlockedAdd64((__int64 volatile *)a, v); } static int64_t rt_atomics_exchange_int64(volatile int64_t* a, int64_t v) { return (int64_t)InterlockedExchange64((LONGLONG*)a, (LONGLONG)v); } static int32_t rt_atomics_exchange_int32(volatile int32_t* a, int32_t v) { rt_assert(sizeof(int32_t) == sizeof(unsigned long)); return (int32_t)InterlockedExchange((volatile LONG*)a, (unsigned long)v); } static bool rt_atomics_compare_exchange_int64(volatile int64_t* a, int64_t comparand, int64_t v) { return (int64_t)InterlockedCompareExchange64((LONGLONG*)a, (LONGLONG)v, (LONGLONG)comparand) == comparand; } static bool rt_atomics_compare_exchange_int32(volatile int32_t* a, int32_t comparand, int32_t v) { return (int64_t)InterlockedCompareExchange((LONG*)a, (LONG)v, (LONG)comparand) == comparand; } static void memory_fence(void) { #ifdef _M_ARM64 atomic_thread_fence(memory_order_seq_cst); #else _mm_mfence(); #endif } #else // stdatomic.h version: #ifndef __INTELLISENSE__ // IntelliSense chokes on _Atomic(_Type) // __INTELLISENSE__ Defined as 1 during an IntelliSense compiler pass // in the Visual Studio IDE. Otherwise, undefined. You can use this macro // to guard code the IntelliSense compiler doesn't understand, // or use it to toggle between the build and IntelliSense compiler. // _strong() operations are the same as _explicit(..., memory_order_seq_cst) // memory_order_seq_cst stands for Sequentially Consistent Ordering // // This is the strongest memory order, providing the guarantee that // all sequentially consistent operations appear to be executed in // the same order on all threads (cores) // // int_fast32_t: Fastest integer type with at least 32 bits. // int_least32_t: Smallest integer type with at least 32 bits. rt_static_assertion(sizeof(int32_t) == sizeof(int_fast32_t)); rt_static_assertion(sizeof(int32_t) == sizeof(int_least32_t)); static int32_t rt_atomics_increment_int32(volatile int32_t* a) { return atomic_fetch_add((volatile atomic_int_fast32_t*)a, 1) + 1; } static int32_t rt_atomics_decrement_int32(volatile int32_t* a) { return atomic_fetch_sub((volatile atomic_int_fast32_t*)a, 1) - 1; } static int64_t rt_atomics_increment_int64(volatile int64_t* a) { return atomic_fetch_add((volatile atomic_int_fast64_t*)a, 1) + 1; } static int64_t rt_atomics_decrement_int64(volatile int64_t* a) { return atomic_fetch_sub((volatile atomic_int_fast64_t*)a, 1) - 1; } static int32_t rt_atomics_add_int32(volatile int32_t* a, int32_t v) { return atomic_fetch_add((volatile atomic_int_fast32_t*)a, v) + v; } static int64_t rt_atomics_add_int64(volatile int64_t* a, int64_t v) { return atomic_fetch_add((volatile atomic_int_fast64_t*)a, v) + v; } static int64_t rt_atomics_exchange_int64(volatile int64_t* a, int64_t v) { return atomic_exchange((volatile atomic_int_fast64_t*)a, v); } static int32_t rt_atomics_exchange_int32(volatile int32_t* a, int32_t v) { return atomic_exchange((volatile atomic_int_fast32_t*)a, v); } static bool rt_atomics_compare_exchange_int64(volatile int64_t* a, int64_t comparand, int64_t v) { return atomic_compare_exchange_strong((volatile atomic_int_fast64_t*)a, &comparand, v); } // Code here is not "seen" by IntelliSense but is compiled normally. static bool rt_atomics_compare_exchange_int32(volatile int32_t* a, int32_t comparand, int32_t v) { return atomic_compare_exchange_strong((volatile atomic_int_fast32_t*)a, &comparand, v); } static void memory_fence(void) { atomic_thread_fence(memory_order_seq_cst); } #endif // __INTELLISENSE__ #endif // UT_ATOMICS_HAS_STDATOMIC_H static int32_t rt_atomics_load_int32(volatile int32_t* a) { return rt_atomics.add_int32(a, 0); } static int64_t rt_atomics_load_int64(volatile int64_t* a) { return rt_atomics.add_int64(a, 0); } static void* rt_atomics_exchange_ptr(volatile void* *a, void* v) { rt_static_assertion(sizeof(void*) == sizeof(uint64_t)); return (void*)(intptr_t)rt_atomics.exchange_int64((int64_t*)a, (int64_t)v); } static bool rt_atomics_compare_exchange_ptr(volatile void* *a, void* comparand, void* v) { rt_static_assertion(sizeof(void*) == sizeof(int64_t)); return rt_atomics.compare_exchange_int64((int64_t*)a, (int64_t)comparand, (int64_t)v); } #pragma push_macro("rt_sync_bool_compare_and_swap") #pragma push_macro("rt_builtin_cpu_pause") // https://en.wikipedia.org/wiki/Spinlock #define rt_sync_bool_compare_and_swap(p, old_val, new_val) \ (_InterlockedCompareExchange64(p, new_val, old_val) == old_val) // https://stackoverflow.com/questions/37063700/mm-pause-usage-in-gcc-on-intel #define rt_builtin_cpu_pause() do { YieldProcessor(); } while (0) static void spinlock_acquire(volatile int64_t* spinlock) { // Very basic implementation of a spinlock. This is currently // only used to guarantee thread-safety during context initialization // and shutdown (which are both executed very infrequently and // have minimal thread contention). // Not a performance champion (because of mem_fence()) but serves // the purpose. mem_fence() can be reduced to mem_sfence()... sigh while (!rt_sync_bool_compare_and_swap(spinlock, 0, 1)) { while (*spinlock) { rt_builtin_cpu_pause(); } } rt_atomics.memory_fence(); // not strictly necessary on strong mem model Intel/AMD but // see: https://cfsamsonbooks.gitbook.io/explaining-atomics-in-rust/ // Fig 2 Inconsistent C11 execution of SB and 2+2W rt_assert(*spinlock == 1); } #pragma pop_macro("rt_builtin_cpu_pause") #pragma pop_macro("rt_sync_bool_compare_and_swap") static void spinlock_release(volatile int64_t* spinlock) { rt_assert(*spinlock == 1); *spinlock = 0; // tribute to lengthy Linus discussion going since 2006: rt_atomics.memory_fence(); } static void rt_atomics_test(void) { #ifdef RT_TESTS volatile int32_t int32_var = 0; volatile int64_t int64_var = 0; volatile void* ptr_var = null; int64_t spinlock = 0; void* old_ptr = rt_atomics.exchange_ptr(&ptr_var, (void*)123); rt_swear(old_ptr == null); rt_swear(ptr_var == (void*)123); int32_t incremented_int32 = rt_atomics.increment_int32(&int32_var); rt_swear(incremented_int32 == 1); rt_swear(int32_var == 1); int32_t decremented_int32 = rt_atomics.decrement_int32(&int32_var); rt_swear(decremented_int32 == 0); rt_swear(int32_var == 0); int64_t incremented_int64 = rt_atomics.increment_int64(&int64_var); rt_swear(incremented_int64 == 1); rt_swear(int64_var == 1); int64_t decremented_int64 = rt_atomics.decrement_int64(&int64_var); rt_swear(decremented_int64 == 0); rt_swear(int64_var == 0); int32_t added_int32 = rt_atomics.add_int32(&int32_var, 5); rt_swear(added_int32 == 5); rt_swear(int32_var == 5); int64_t added_int64 = rt_atomics.add_int64(&int64_var, 10); rt_swear(added_int64 == 10); rt_swear(int64_var == 10); int32_t old_int32 = rt_atomics.exchange_int32(&int32_var, 3); rt_swear(old_int32 == 5); rt_swear(int32_var == 3); int64_t old_int64 = rt_atomics.exchange_int64(&int64_var, 6); rt_swear(old_int64 == 10); rt_swear(int64_var == 6); bool int32_exchanged = rt_atomics.compare_exchange_int32(&int32_var, 3, 4); rt_swear(int32_exchanged); rt_swear(int32_var == 4); bool int64_exchanged = rt_atomics.compare_exchange_int64(&int64_var, 6, 7); rt_swear(int64_exchanged); rt_swear(int64_var == 7); ptr_var = (void*)0x123; bool ptr_exchanged = rt_atomics.compare_exchange_ptr(&ptr_var, (void*)0x123, (void*)0x456); rt_swear(ptr_exchanged); rt_swear(ptr_var == (void*)0x456); rt_atomics.spinlock_acquire(&spinlock); rt_swear(spinlock == 1); rt_atomics.spinlock_release(&spinlock); rt_swear(spinlock == 0); int32_t loaded_int32 = rt_atomics.load32(&int32_var); rt_swear(loaded_int32 == int32_var); int64_t loaded_int64 = rt_atomics.load64(&int64_var); rt_swear(loaded_int64 == int64_var); rt_atomics.memory_fence(); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } #ifndef __INTELLISENSE__ // IntelliSense chokes on _Atomic(_Type) rt_static_assertion(sizeof(void*) == sizeof(int64_t)); rt_static_assertion(sizeof(void*) == sizeof(uintptr_t)); rt_atomics_if rt_atomics = { .exchange_ptr = rt_atomics_exchange_ptr, .increment_int32 = rt_atomics_increment_int32, .decrement_int32 = rt_atomics_decrement_int32, .increment_int64 = rt_atomics_increment_int64, .decrement_int64 = rt_atomics_decrement_int64, .add_int32 = rt_atomics_add_int32, .add_int64 = rt_atomics_add_int64, .exchange_int32 = rt_atomics_exchange_int32, .exchange_int64 = rt_atomics_exchange_int64, .compare_exchange_int64 = rt_atomics_compare_exchange_int64, .compare_exchange_int32 = rt_atomics_compare_exchange_int32, .compare_exchange_ptr = rt_atomics_compare_exchange_ptr, .load32 = rt_atomics_load_int32, .load64 = rt_atomics_load_int64, .spinlock_acquire = spinlock_acquire, .spinlock_release = spinlock_release, .memory_fence = memory_fence, .test = rt_atomics_test }; #endif // __INTELLISENSE__ // 2024-03-20 latest windows runtime and toolchain cl.exe // ... VC\Tools\MSVC\14.39.33519\include // see: // vcruntime_c11_atomic_support.h // vcruntime_c11_stdatomic.h // stdatomic.h // https://developercommunity.visualstudio.com/t/C11--C17-include--issue/10620622 // cl.exe /std:c11 /experimental:c11atomics // command line option are required // even in C17 mode in spring of 2024 #pragma warning(pop) ================================================ FILE: src/rt/rt_backtrace.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static void* rt_backtrace_process; static DWORD rt_backtrace_pid; typedef rt_begin_packed struct symbol_info_s { SYMBOL_INFO info; char name[rt_backtrace_max_symbol]; } rt_end_packed symbol_info_t; #pragma push_macro("rt_backtrace_load_dll") #define rt_backtrace_load_dll(fn) do { \ if (GetModuleHandleA(fn) == null) { \ rt_fatal_win32err(LoadLibraryA(fn)); \ } \ } while (0) static void rt_backtrace_init(void) { if (rt_backtrace_process == null) { rt_backtrace_load_dll("dbghelp.dll"); rt_backtrace_load_dll("imagehlp.dll"); DWORD options = SymGetOptions(); // options |= SYMOPT_DEBUG; options |= SYMOPT_NO_PROMPTS; options |= SYMOPT_LOAD_LINES; options |= SYMOPT_UNDNAME; options |= SYMOPT_LOAD_ANYTHING; rt_swear(SymSetOptions(options)); rt_backtrace_pid = GetProcessId(GetCurrentProcess()); rt_swear(rt_backtrace_pid != 0); rt_backtrace_process = OpenProcess(PROCESS_ALL_ACCESS, false, rt_backtrace_pid); rt_swear(rt_backtrace_process != null); rt_swear(SymInitialize(rt_backtrace_process, null, true), "%s", rt_str.error(rt_core.err())); } } #pragma pop_macro("rt_backtrace_load_dll") static void rt_backtrace_capture(rt_backtrace_t* bt, int32_t skip) { rt_backtrace_init(); SetLastError(0); bt->frames = CaptureStackBackTrace(1 + skip, rt_countof(bt->stack), bt->stack, (DWORD*)&bt->hash); bt->error = rt_core.err(); } static bool rt_backtrace_function(DWORD64 pc, SYMBOL_INFO* si) { // find DLL exported function bool found = false; const DWORD64 module_base = SymGetModuleBase64(rt_backtrace_process, pc); if (module_base != 0) { const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS; HMODULE module_handle = null; if (GetModuleHandleExA(flags, (const char*)pc, &module_handle)) { DWORD bytes = 0; IMAGE_EXPORT_DIRECTORY* dir = (IMAGE_EXPORT_DIRECTORY*) ImageDirectoryEntryToDataEx(module_handle, true, IMAGE_DIRECTORY_ENTRY_EXPORT, &bytes, null); if (dir) { uint8_t* m = (uint8_t*)module_handle; DWORD* functions = (DWORD*)(m + dir->AddressOfFunctions); DWORD* names = (DWORD*)(m + dir->AddressOfNames); WORD* ordinals = (WORD*)(m + dir->AddressOfNameOrdinals); DWORD64 address = 0; // closest address DWORD64 min_distance = (DWORD64)-1; const char* function = NULL; // closest function name for (DWORD i = 0; i < dir->NumberOfNames; i++) { // function address DWORD64 fa = (DWORD64)(m + functions[ordinals[i]]); if (fa <= pc) { DWORD64 distance = pc - fa; if (distance < min_distance) { min_distance = distance; address = fa; function = (const char*)(m + names[i]); } } } if (function != null) { si->ModBase = (uint64_t)m; snprintf(si->Name, si->MaxNameLen - 1, "%s", function); si->Name[si->MaxNameLen - 1] = 0x00; si->NameLen = (DWORD)strlen(si->Name); si->Address = address; found = true; } } } } return found; } // SimpleStackWalker::showVariablesAt() can be implemented if needed like this: // https://accu.org/journals/overload/29/165/orr/ // https://github.com/rogerorr/articles/tree/main/Debugging_Optimised_Code // https://github.com/rogerorr/articles/blob/main/Debugging_Optimised_Code/SimpleStackWalker.cpp#L301 static const void rt_backtrace_symbolize_inline_frame(rt_backtrace_t* bt, int32_t i, DWORD64 pc, DWORD inline_context, symbol_info_t* si) { si->info.Name[0] = 0; si->info.NameLen = 0; bt->file[i][0] = 0; bt->line[i] = 0; bt->symbol[i][0] = 0; DWORD64 displacement = 0; if (SymFromInlineContext(rt_backtrace_process, pc, inline_context, &displacement, &si->info)) { rt_str_printf(bt->symbol[i], "%s", si->info.Name); } else { bt->error = rt_core.err(); } IMAGEHLP_LINE64 li = { .SizeOfStruct = sizeof(IMAGEHLP_LINE64) }; DWORD offset = 0; if (SymGetLineFromInlineContext(rt_backtrace_process, pc, inline_context, 0, &offset, &li)) { rt_str_printf(bt->file[i], "%s", li.FileName); bt->line[i] = li.LineNumber; } } // Too see kernel addresses in Stack Back Traces: // // Windows Registry Editor Version 5.00 // [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management] // "DisablePagingExecutive"=dword:00000001 // // https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc757875(v=ws.10) static int32_t rt_backtrace_symbolize_frame(rt_backtrace_t* bt, int32_t i) { const DWORD64 pc = (DWORD64)bt->stack[i]; symbol_info_t si = { .info = { .SizeOfStruct = sizeof(SYMBOL_INFO), .MaxNameLen = rt_countof(si.name) } }; bt->file[i][0] = 0; bt->line[i] = 0; bt->symbol[i][0] = 0; DWORD64 offsetFromSymbol = 0; const DWORD inline_count = SymAddrIncludeInlineTrace(rt_backtrace_process, pc); if (inline_count > 0) { DWORD ic = 0; // inline context DWORD fi = 0; // frame index if (SymQueryInlineTrace(rt_backtrace_process, pc, 0, pc, pc, &ic, &fi)) { for (DWORD k = 0; k < inline_count; k++, ic++) { rt_backtrace_symbolize_inline_frame(bt, i, pc, ic, &si); i++; } } } else { if (SymFromAddr(rt_backtrace_process, pc, &offsetFromSymbol, &si.info)) { rt_str_printf(bt->symbol[i], "%s", si.info.Name); DWORD d = 0; // displacement IMAGEHLP_LINE64 ln = { .SizeOfStruct = sizeof(IMAGEHLP_LINE64) }; if (SymGetLineFromAddr64(rt_backtrace_process, pc, &d, &ln)) { bt->line[i] = ln.LineNumber; rt_str_printf(bt->file[i], "%s", ln.FileName); } else { bt->error = rt_core.err(); if (rt_backtrace_function(pc, &si.info)) { GetModuleFileNameA((HANDLE)si.info.ModBase, bt->file[i], rt_countof(bt->file[i]) - 1); bt->file[i][rt_countof(bt->file[i]) - 1] = 0; bt->line[i] = 0; } else { bt->file[i][0] = 0x00; bt->line[i] = 0; } } i++; } else { bt->error = rt_core.err(); if (rt_backtrace_function(pc, &si.info)) { rt_str_printf(bt->symbol[i], "%s", si.info.Name); GetModuleFileNameA((HANDLE)si.info.ModBase, bt->file[i], rt_countof(bt->file[i]) - 1); bt->file[i][rt_countof(bt->file[i]) - 1] = 0; bt->error = 0; i++; } else { // will not do i++ } } } return i; } static void rt_backtrace_symbolize_backtrace(rt_backtrace_t* bt) { rt_assert(!bt->symbolized); bt->error = 0; rt_backtrace_init(); // rt_backtrace_symbolize_frame() may produce zero, one or many frames int32_t n = bt->frames; void* stack[rt_countof(bt->stack)]; memcpy(stack, bt->stack, n * sizeof(stack[0])); bt->frames = 0; for (int32_t i = 0; i < n && bt->frames < rt_countof(bt->stack); i++) { bt->stack[bt->frames] = stack[i]; bt->frames = rt_backtrace_symbolize_frame(bt, i); } bt->symbolized = true; } static void rt_backtrace_symbolize(rt_backtrace_t* bt) { if (!bt->symbolized) { rt_backtrace_symbolize_backtrace(bt); } } static const char* rt_backtrace_stops[] = { "main", "WinMain", "BaseThreadInitThunk", "RtlUserThreadStart", "mainCRTStartup", "WinMainCRTStartup", "invoke_main", "NdrInterfacePointerMemorySize", null }; static void rt_backtrace_trace(const rt_backtrace_t* bt, const char* stop) { #pragma push_macro("rt_backtrace_glyph_called_from") #define rt_backtrace_glyph_called_from rt_glyph_north_west_arrow_with_hook rt_assert(bt->symbolized, "need rt_backtrace.symbolize(bt)"); const char** alt = stop != null && strcmp(stop, "*") == 0 ? rt_backtrace_stops : null; for (int32_t i = 0; i < bt->frames; i++) { rt_debug.println(bt->file[i], bt->line[i], bt->symbol[i], rt_backtrace_glyph_called_from "%s", i == i < bt->frames - 1 ? "\n" : ""); // extra \n for last line if (stop != null && strcmp(bt->symbol[i], stop) == 0) { break; } const char** s = alt; while (s != null && *s != null && strcmp(bt->symbol[i], *s) != 0) { s++; } if (s != null && *s != null) { break; } } #pragma pop_macro("rt_backtrace_glyph_called_from") } static const char* rt_backtrace_string(const rt_backtrace_t* bt, char* text, int32_t count) { rt_assert(bt->symbolized, "need rt_backtrace.symbolize(bt)"); char s[1024]; char* p = text; int32_t n = count; for (int32_t i = 0; i < bt->frames && n > 128; i++) { int32_t line = bt->line[i]; const char* file = bt->file[i]; const char* name = bt->symbol[i]; if (file[0] != 0 && name[0] != 0) { rt_str_printf(s, "%s(%d): %s\n", file, line, name); } else if (file[0] == 0 && name[0] != 0) { rt_str_printf(s, "%s\n", name); } s[rt_countof(s) - 1] = 0; int32_t k = (int32_t)strlen(s); if (k < n) { memcpy(p, s, (size_t)k + 1); p += k; n -= k; } } return text; } typedef struct { char name[32]; } rt_backtrace_thread_name_t; static rt_backtrace_thread_name_t rt_backtrace_thread_name(HANDLE thread) { rt_backtrace_thread_name_t tn; tn.name[0] = 0; wchar_t* thread_name = null; if (SUCCEEDED(GetThreadDescription(thread, &thread_name))) { rt_str.utf16to8(tn.name, rt_countof(tn.name), thread_name, -1); LocalFree(thread_name); } return tn; } static void rt_backtrace_context(rt_thread_t thread, const void* ctx, rt_backtrace_t* bt) { CONTEXT* context = (CONTEXT*)ctx; STACKFRAME64 stack_frame = { 0 }; int machine_type = IMAGE_FILE_MACHINE_UNKNOWN; #if defined(_M_IX86) #error "Unsupported platform" #elif defined(_M_ARM64) machine_type = IMAGE_FILE_MACHINE_ARM64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Pc, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Fp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Sp, .Mode = AddrModeFlat} }; #elif defined(_M_X64) machine_type = IMAGE_FILE_MACHINE_AMD64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Rip, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Rbp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Rsp, .Mode = AddrModeFlat} }; #elif defined(_M_IA64) int machine_type = IMAGE_FILE_MACHINE_IA64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->StIIP, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->IntSp, .Mode = AddrModeFlat}, .AddrBStore = {.Offset = context->RsBSP, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->IntSp, .Mode = AddrModeFlat} } #elif defined(_M_ARM64) machine_type = IMAGE_FILE_MACHINE_ARM64; stack_frame = (STACKFRAME64){ .AddrPC = {.Offset = context->Pc, .Mode = AddrModeFlat}, .AddrFrame = {.Offset = context->Fp, .Mode = AddrModeFlat}, .AddrStack = {.Offset = context->Sp, .Mode = AddrModeFlat} }; #else #error "Unsupported platform" #endif rt_backtrace_init(); while (StackWalk64(machine_type, rt_backtrace_process, (HANDLE)thread, &stack_frame, context, null, SymFunctionTableAccess64, SymGetModuleBase64, null)) { DWORD64 pc = stack_frame.AddrPC.Offset; if (pc == 0) { break; } if (bt->frames < rt_countof(bt->stack)) { bt->stack[bt->frames] = (void*)pc; bt->frames = rt_backtrace_symbolize_frame(bt, bt->frames); } } bt->symbolized = true; } static void rt_backtrace_thread(HANDLE thread, rt_backtrace_t* bt) { bt->frames = 0; // cannot suspend callers thread rt_swear(rt_thread.id_of(thread) != rt_thread.id()); if (SuspendThread(thread) != (DWORD)-1) { CONTEXT context = { .ContextFlags = CONTEXT_FULL }; GetThreadContext(thread, &context); rt_backtrace.context(thread, &context, bt); if (ResumeThread(thread) == (DWORD)-1) { rt_println("ResumeThread() failed %s", rt_str.error(rt_core.err())); ExitProcess(0xBD); } } } static void rt_backtrace_trace_self(const char* stop) { rt_backtrace_t bt = {{0}}; rt_backtrace.capture(&bt, 2); rt_backtrace.symbolize(&bt); rt_backtrace.trace(&bt, stop); } static void rt_backtrace_trace_all_but_self(void) { rt_backtrace_init(); rt_assert(rt_backtrace_process != null && rt_backtrace_pid != 0); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (snapshot == INVALID_HANDLE_VALUE) { rt_println("CreateToolhelp32Snapshot failed %s", rt_str.error(rt_core.err())); } else { THREADENTRY32 te = { .dwSize = sizeof(THREADENTRY32) }; if (!Thread32First(snapshot, &te)) { rt_println("Thread32First failed %s", rt_str.error(rt_core.err())); } else { do { if (te.th32OwnerProcessID == rt_backtrace_pid) { static const DWORD flags = THREAD_ALL_ACCESS | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT; uint32_t tid = te.th32ThreadID; if (tid != (uint32_t)rt_thread.id()) { HANDLE thread = OpenThread(flags, false, tid); if (thread != null) { rt_backtrace_t bt = {0}; rt_backtrace_thread(thread, &bt); rt_backtrace_thread_name_t tn = rt_backtrace_thread_name(thread); rt_debug.println(">Thread", tid, tn.name, "id 0x%08X (%d)", tid, tid); if (bt.frames > 0) { rt_backtrace.trace(&bt, "*"); } rt_debug.println(" 0 && s[count - 1] == 0) { // zero terminated int32_t k = (int32_t)(uintptr_t)( rt_backtrace_test_output_p - rt_backtrace_test_output); int32_t space = rt_countof(rt_backtrace_test_output) - k; if (count < space) { memcpy(rt_backtrace_test_output_p, s, count); rt_backtrace_test_output_p += count - 1; // w/o 0x00 } } else { rt_debug.breakpoint(); // incorrect output() cannot append } return true; // intercepted, do not do OutputDebugString() } static void rt_backtrace_test_thread(void* e) { rt_event.wait(*(rt_event_t*)e); } static void rt_backtrace_test(void) { rt_backtrace_debug_tee = rt_debug.tee; rt_backtrace_test_output_p = rt_backtrace_test_output; rt_backtrace_test_output[0] = 0x00; rt_debug.tee = rt_backtrace_tee; rt_backtrace_t bt = {{0}}; rt_backtrace.capture(&bt, 0); // rt_backtrace_test <- rt_core_test <- run <- main rt_swear(bt.frames >= 3); rt_backtrace.symbolize(&bt); rt_backtrace.trace(&bt, null); rt_backtrace.trace(&bt, "main"); rt_backtrace.trace(&bt, null); rt_backtrace.trace(&bt, "main"); rt_event_t e = rt_event.create(); rt_thread_t thread = rt_thread.start(rt_backtrace_test_thread, &e); rt_backtrace.trace_all_but_self(); rt_event.set(e); rt_thread.join(thread, -1.0); rt_event.dispose(e); rt_debug.tee = rt_backtrace_debug_tee; if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { rt_debug.output(rt_backtrace_test_output, (int32_t)strlen(rt_backtrace_test_output) + 1); } rt_swear(strstr(rt_backtrace_test_output, "rt_backtrace_test") != null, "%s", rt_backtrace_test_output); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_backtrace_test(void) { } #endif rt_backtrace_if rt_backtrace = { .capture = rt_backtrace_capture, .context = rt_backtrace_context, .symbolize = rt_backtrace_symbolize, .trace = rt_backtrace_trace, .trace_self = rt_backtrace_trace_self, .trace_all_but_self = rt_backtrace_trace_all_but_self, .string = rt_backtrace_string, .test = rt_backtrace_test }; ================================================ FILE: src/rt/rt_clipboard.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static errno_t rt_clipboard_put_text(const char* utf8) { int32_t chars = rt_str.utf16_chars(utf8, -1); int32_t bytes = (chars + 1) * 2; uint16_t* utf16 = null; errno_t r = rt_heap.alloc((void**)&utf16, (size_t)bytes); if (utf16 != null) { rt_str.utf8to16(utf16, bytes, utf8, -1); rt_assert(utf16[chars - 1] == 0); const int32_t n = (int32_t)rt_str.len16(utf16) + 1; r = OpenClipboard(GetDesktopWindow()) ? 0 : rt_core.err(); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { r = EmptyClipboard() ? 0 : rt_core.err(); if (r != 0) { rt_println("EmptyClipboard() failed %s", rt_strerr(r)); } } void* global = null; if (r == 0) { global = GlobalAlloc(GMEM_MOVEABLE, (size_t)n * 2); r = global != null ? 0 : rt_core.err(); if (r != 0) { rt_println("GlobalAlloc() failed %s", rt_strerr(r)); } } if (r == 0) { char* d = (char*)GlobalLock(global); rt_not_null(d); memcpy(d, utf16, (size_t)n * 2); r = rt_b2e(SetClipboardData(CF_UNICODETEXT, global)); GlobalUnlock(global); if (r != 0) { rt_println("SetClipboardData() failed %s", rt_strerr(r)); GlobalFree(global); } else { // do not free global memory. It's owned by system clipboard now } } if (r == 0) { r = rt_b2e(CloseClipboard()); if (r != 0) { rt_println("CloseClipboard() failed %s", rt_strerr(r)); } } rt_heap.free(utf16); } return r; } static errno_t rt_clipboard_get_text(char* utf8, int32_t* bytes) { rt_not_null(bytes); errno_t r = rt_b2e(OpenClipboard(GetDesktopWindow())); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { HANDLE global = GetClipboardData(CF_UNICODETEXT); if (global == null) { r = rt_core.err(); } else { uint16_t* utf16 = (uint16_t*)GlobalLock(global); if (utf16 != null) { int32_t utf8_bytes = rt_str.utf8_bytes(utf16, -1); if (utf8 != null) { char* decoded = (char*)malloc((size_t)utf8_bytes); if (decoded == null) { r = ERROR_OUTOFMEMORY; } else { rt_str.utf16to8(decoded, utf8_bytes, utf16, -1); int32_t n = rt_min(*bytes, utf8_bytes); memcpy(utf8, decoded, (size_t)n); free(decoded); if (n < utf8_bytes) { r = ERROR_INSUFFICIENT_BUFFER; } } } *bytes = utf8_bytes; GlobalUnlock(global); } } r = rt_b2e(CloseClipboard()); } return r; } #ifdef RT_TESTS static void rt_clipboard_test(void) { rt_fatal_if_error(rt_clipboard.put_text("Hello Clipboard")); char text[256]; int32_t bytes = rt_countof(text); rt_fatal_if_error(rt_clipboard.get_text(text, &bytes)); rt_swear(strcmp(text, "Hello Clipboard") == 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_clipboard_test(void) { } #endif rt_clipboard_if rt_clipboard = { .put_text = rt_clipboard_put_text, .get_text = rt_clipboard_get_text, .put_image = null, // implemented in ui.app .test = rt_clipboard_test }; ================================================ FILE: src/rt/rt_clock.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" enum { rt_clock_nsec_in_usec = 1000, // nano in micro rt_clock_nsec_in_msec = rt_clock_nsec_in_usec * 1000, // nano in milli rt_clock_nsec_in_sec = rt_clock_nsec_in_msec * 1000, rt_clock_usec_in_msec = 1000, // micro in mill rt_clock_msec_in_sec = 1000, // milli in sec rt_clock_usec_in_sec = rt_clock_usec_in_msec * rt_clock_msec_in_sec // micro in sec }; static uint64_t rt_clock_microseconds_since_epoch(void) { // NOT monotonic FILETIME ft; // time in 100ns interval (tenth of microsecond) // since 12:00 A.M. January 1, 1601 Coordinated Universal Time (UTC) GetSystemTimePreciseAsFileTime(&ft); uint64_t microseconds = (((uint64_t)ft.dwHighDateTime) << 32 | ft.dwLowDateTime) / 10; rt_assert(microseconds > 0); return microseconds; } static uint64_t rt_clock_localtime(void) { TIME_ZONE_INFORMATION tzi; // UTC = local time + bias GetTimeZoneInformation(&tzi); uint64_t bias = (uint64_t)tzi.Bias * 60LL * 1000 * 1000; // in microseconds return rt_clock_microseconds_since_epoch() - bias; } static void rt_clock_utc(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc) { uint64_t time_in_100ns = microseconds * 10; FILETIME mst = { (DWORD)(time_in_100ns & 0xFFFFFFFF), (DWORD)(time_in_100ns >> 32) }; SYSTEMTIME utc; FileTimeToSystemTime(&mst, &utc); *year = utc.wYear; *month = utc.wMonth; *day = utc.wDay; *hh = utc.wHour; *mm = utc.wMinute; *ss = utc.wSecond; *ms = utc.wMilliseconds; *mc = microseconds % 1000; } static void rt_clock_local(uint64_t microseconds, int32_t* year, int32_t* month, int32_t* day, int32_t* hh, int32_t* mm, int32_t* ss, int32_t* ms, int32_t* mc) { uint64_t time_in_100ns = microseconds * 10; FILETIME mst = { (DWORD)(time_in_100ns & 0xFFFFFFFF), (DWORD)(time_in_100ns >> 32) }; SYSTEMTIME utc; FileTimeToSystemTime(&mst, &utc); DYNAMIC_TIME_ZONE_INFORMATION tzi; GetDynamicTimeZoneInformation(&tzi); SYSTEMTIME lt = {0}; SystemTimeToTzSpecificLocalTimeEx(&tzi, &utc, <); *year = lt.wYear; *month = lt.wMonth; *day = lt.wDay; *hh = lt.wHour; *mm = lt.wMinute; *ss = lt.wSecond; *ms = lt.wMilliseconds; *mc = microseconds % 1000; } static fp64_t rt_clock_seconds(void) { // since_boot LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); static fp64_t one_over_freq; if (one_over_freq == 0) { LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); one_over_freq = 1.0 / (fp64_t)frequency.QuadPart; } return (fp64_t)qpc.QuadPart * one_over_freq; } // Max duration in nanoseconds=2^64 - 1 nanoseconds // 2^64 - 1 ns 1 sec 1 min // Max Duration in Hours = ----------- x ------------ x ------------- // 10^9 ns / s 60 sec / min 60 min / hour // // 1 hour // Max Duration in Days = --------------- // 24 hours / day // // it would take approximately 213,503 days (or about 584.5 years) // for rt_clock.nanoseconds() to overflow // // for divider = rt_num.gcd32(nsec_in_sec, freq) below and 10MHz timer // the actual duration is shorter because of (mul == 100) // (uint64_t)qpc.QuadPart * mul // 64 bit overflow and is about 5.8 years. // // In a long running code like services is advisable to use // rt_clock.nanoseconds() to measure only deltas and pay close attention // to the wrap around despite of 5 years monotony static uint64_t rt_clock_nanoseconds(void) { LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); static uint32_t freq; static uint32_t mul = rt_clock_nsec_in_sec; if (freq == 0) { LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); rt_assert(frequency.HighPart == 0); // even 1GHz frequency should fit into 32 bit unsigned rt_assert(frequency.HighPart == 0, "%08lX%%08lX", frequency.HighPart, frequency.LowPart); // known values: 10,000,000 and 3,000,000 10MHz, 3MHz rt_assert(frequency.LowPart % (1000 * 1000) == 0); // if we start getting weird frequencies not // multiples of MHz rt_num.gcd() approach may need // to be revised in favor of rt_num.muldiv64x64() freq = frequency.LowPart; rt_assert(freq != 0 && freq < (uint32_t)rt_clock.nsec_in_sec); // to avoid rt_num.muldiv128: uint32_t divider = rt_num.gcd32((uint32_t)rt_clock.nsec_in_sec, freq); freq /= divider; mul /= divider; } uint64_t ns_mul_freq = (uint64_t)qpc.QuadPart * mul; return freq == 1 ? ns_mul_freq : ns_mul_freq / freq; } // Difference between 1601 and 1970 in microseconds: static const uint64_t rt_clock_epoch_diff_usec = 11644473600000000ULL; static uint64_t rt_clock_unix_microseconds(void) { return rt_clock.microseconds() - rt_clock_epoch_diff_usec; } static uint64_t rt_clock_unix_seconds(void) { return rt_clock.unix_microseconds() / (uint64_t)rt_clock.usec_in_sec; } static void rt_clock_test(void) { #ifdef RT_TESTS // TODO: implement more tests uint64_t t0 = rt_clock.nanoseconds(); uint64_t t1 = rt_clock.nanoseconds(); int32_t count = 0; while (t0 == t1 && count < 1024) { t1 = rt_clock.nanoseconds(); count++; } rt_swear(t0 != t1, "count: %d t0: %lld t1: %lld", count, t0, t1); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_clock_if rt_clock = { .nsec_in_usec = rt_clock_nsec_in_usec, .nsec_in_msec = rt_clock_nsec_in_msec, .nsec_in_sec = rt_clock_nsec_in_sec, .usec_in_msec = rt_clock_usec_in_msec, .msec_in_sec = rt_clock_msec_in_sec, .usec_in_sec = rt_clock_usec_in_sec, .seconds = rt_clock_seconds, .nanoseconds = rt_clock_nanoseconds, .unix_microseconds = rt_clock_unix_microseconds, .unix_seconds = rt_clock_unix_seconds, .microseconds = rt_clock_microseconds_since_epoch, .localtime = rt_clock_localtime, .utc = rt_clock_utc, .local = rt_clock_local, .test = rt_clock_test }; ================================================ FILE: src/rt/rt_config.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // On Unix the implementation should keep KV pairs in // key-named files inside .name/ folder static const char* rt_config_apps = "Software\\ui\\apps"; static const DWORD rt_config_access = KEY_READ|KEY_WRITE|KEY_SET_VALUE|KEY_QUERY_VALUE| KEY_ENUMERATE_SUB_KEYS|DELETE; static errno_t rt_config_get_reg_key(const char* name, HKEY *key) { char path[256] = {0}; rt_str_printf(path, "%s\\%s", rt_config_apps, name); errno_t r = RegOpenKeyExA(HKEY_CURRENT_USER, path, 0, rt_config_access, key); if (r != 0) { const DWORD option = REG_OPTION_NON_VOLATILE; r = RegCreateKeyExA(HKEY_CURRENT_USER, path, 0, null, option, rt_config_access, null, key, null); } return r; } static errno_t rt_config_save(const char* name, const char* key, const void* data, int32_t bytes) { errno_t r = 0; HKEY k = null; r = rt_config_get_reg_key(name, &k); if (k != null) { r = RegSetValueExA(k, key, 0, REG_BINARY, (const uint8_t*)data, (DWORD)bytes); rt_fatal_if_error(RegCloseKey(k)); } return r; } static errno_t rt_config_remove(const char* name, const char* key) { errno_t r = 0; HKEY k = null; r = rt_config_get_reg_key(name, &k); if (k != null) { r = RegDeleteValueA(k, key); rt_fatal_if_error(RegCloseKey(k)); } return r; } static errno_t rt_config_clean(const char* name) { errno_t r = 0; HKEY k = null; if (RegOpenKeyExA(HKEY_CURRENT_USER, rt_config_apps, 0, rt_config_access, &k) == 0) { r = RegDeleteTreeA(k, name); rt_fatal_if_error(RegCloseKey(k)); } return r; } static int32_t rt_config_size(const char* name, const char* key) { int32_t bytes = -1; HKEY k = null; errno_t r = rt_config_get_reg_key(name, &k); if (k != null) { DWORD type = REG_BINARY; DWORD cb = 0; r = RegQueryValueExA(k, key, null, &type, null, &cb); if (r == ERROR_FILE_NOT_FOUND) { bytes = 0; // do not report data_size() often used this way } else if (r != 0) { rt_println("%s.RegQueryValueExA(\"%s\") failed %s", name, key, rt_strerr(r)); bytes = 0; // on any error behave as empty data } else { bytes = (int32_t)cb; } rt_fatal_if_error(RegCloseKey(k)); } return bytes; } static int32_t rt_config_load(const char* name, const char* key, void* data, int32_t bytes) { int32_t read = -1; HKEY k = null; errno_t r = rt_config_get_reg_key(name, &k); if (k != null) { DWORD type = REG_BINARY; DWORD cb = (DWORD)bytes; r = RegQueryValueExA(k, key, null, &type, (uint8_t*)data, &cb); if (r == ERROR_MORE_DATA) { // returns -1 ui_app.data_size() should be used } else if (r != 0) { if (r != ERROR_FILE_NOT_FOUND) { rt_println("%s.RegQueryValueExA(\"%s\") failed %s", name, key, rt_strerr(r)); } read = 0; // on any error behave as empty data } else { read = (int32_t)cb; } rt_fatal_if_error(RegCloseKey(k)); } return read; } #ifdef RT_TESTS static void rt_config_test(void) { const char* name = strrchr(rt_args.v[0], '\\'); if (name == null) { name = strrchr(rt_args.v[0], '/'); } name = name != null ? name + 1 : rt_args.v[0]; rt_swear(name != null); const char* key = "test"; const char data[] = "data"; int32_t bytes = sizeof(data); rt_swear(rt_config.save(name, key, data, bytes) == 0); char read[256]; rt_swear(rt_config.load(name, key, read, bytes) == bytes); int32_t size = rt_config.size(name, key); rt_swear(size == bytes); rt_swear(rt_config.remove(name, key) == 0); rt_swear(rt_config.clean(name) == 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_config_test(void) { } #endif rt_config_if rt_config = { .save = rt_config_save, .size = rt_config_size, .load = rt_config_load, .remove = rt_config_remove, .clean = rt_config_clean, .test = rt_config_test }; ================================================ FILE: src/rt/rt_core.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // abort does NOT call atexit() functions and // does NOT flush rt_streams. Also Win32 runtime // abort() attempt to show Abort/Retry/Ignore // MessageBox - thus ExitProcess() static void rt_core_abort(void) { ExitProcess(ERROR_FATAL_APP_EXIT); } static void rt_core_exit(int32_t exit_code) { exit(exit_code); } // TODO: consider r = HRESULT_FROM_WIN32() and r = HRESULT_CODE(hr); // this separates posix error codes from win32 error codes static errno_t rt_core_err(void) { return (errno_t)GetLastError(); } static void rt_core_seterr(errno_t err) { SetLastError((DWORD)err); } rt_static_init(runtime) { SetErrorMode( // The system does not display the critical-error-handler message box. // Instead, the system sends the error to the calling process: SEM_FAILCRITICALERRORS| // The system automatically fixes memory alignment faults and // makes them invisible to the application. SEM_NOALIGNMENTFAULTEXCEPT| // The system does not display the Windows Error Reporting dialog. SEM_NOGPFAULTERRORBOX| // The OpenFile function does not display a message box when it fails // to find a file. Instead, the error is returned to the caller. // This error mode overrides the OF_PROMPT flag. SEM_NOOPENFILEERRORBOX); } #ifdef RT_TESTS static void rt_core_test(void) { // in alphabetical order rt_args.test(); rt_atomics.test(); rt_backtrace.test(); rt_clipboard.test(); rt_clock.test(); rt_config.test(); rt_debug.test(); rt_event.test(); rt_files.test(); rt_generics.test(); rt_heap.test(); rt_loader.test(); rt_mem.test(); rt_mutex.test(); rt_num.test(); rt_processes.test(); rt_static_init_test(); rt_str.test(); rt_streams.test(); rt_thread.test(); rt_vigil.test(); rt_worker.test(); } #else static void rt_core_test(void) { } #endif rt_core_if rt_core = { .err = rt_core_err, .set_err = rt_core_seterr, .abort = rt_core_abort, .exit = rt_core_exit, .test = rt_core_test, .error = { // posix .access_denied = ERROR_ACCESS_DENIED, // EACCES .bad_file = ERROR_BAD_FILE_TYPE, // EBADF .broken_pipe = ERROR_BROKEN_PIPE, // EPIPE .device_not_ready = ERROR_NOT_READY, // ENXIO .directory_not_empty = ERROR_DIR_NOT_EMPTY, // ENOTEMPTY .disk_full = ERROR_DISK_FULL, // ENOSPC .file_exists = ERROR_FILE_EXISTS, // EEXIST .file_not_found = ERROR_FILE_NOT_FOUND, // ENOENT .insufficient_buffer = ERROR_INSUFFICIENT_BUFFER, // E2BIG .interrupted = ERROR_OPERATION_ABORTED, // EINTR .invalid_data = ERROR_INVALID_DATA, // EINVAL .invalid_handle = ERROR_INVALID_HANDLE, // EBADF .invalid_parameter = ERROR_INVALID_PARAMETER, // EINVAL .io_error = ERROR_IO_DEVICE, // EIO .more_data = ERROR_MORE_DATA, // ENOBUFS .name_too_long = ERROR_FILENAME_EXCED_RANGE, // ENAMETOOLONG .no_child_process = ERROR_NO_PROC_SLOTS, // ECHILD .not_a_directory = ERROR_DIRECTORY, // ENOTDIR .not_empty = ERROR_DIR_NOT_EMPTY, // ENOTEMPTY .out_of_memory = ERROR_OUTOFMEMORY, // ENOMEM .path_not_found = ERROR_PATH_NOT_FOUND, // ENOENT .pipe_not_connected = ERROR_PIPE_NOT_CONNECTED, // EPIPE .read_only_file = ERROR_WRITE_PROTECT, // EROFS .resource_deadlock = ERROR_LOCK_VIOLATION, // EDEADLK .too_many_open_files = ERROR_TOO_MANY_OPEN_FILES, // EMFILE } }; #pragma comment(lib, "advapi32") #pragma comment(lib, "ntdll") #pragma comment(lib, "psapi") #pragma comment(lib, "shell32") #pragma comment(lib, "shlwapi") #pragma comment(lib, "kernel32") #pragma comment(lib, "user32") // clipboard #pragma comment(lib, "imm32") // Internationalization input method #pragma comment(lib, "ole32") // rt_files.known_folder CoMemFree #pragma comment(lib, "dbghelp") #pragma comment(lib, "imagehlp") ================================================ FILE: src/rt/rt_debug.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static const char* rt_debug_abbreviate(const char* file) { const char* fn = strrchr(file, '\\'); if (fn == null) { fn = strrchr(file, '/'); } return fn != null ? fn + 1 : file; } #ifdef WINDOWS static int32_t rt_debug_max_file_line; static int32_t rt_debug_max_function; static void rt_debug_output(const char* s, int32_t count) { bool intercepted = false; if (rt_debug.tee != null) { intercepted = rt_debug.tee(s, count); } if (!intercepted) { // For link.exe /Subsystem:Windows code stdout/stderr are often closed if (stderr != null && fileno(stderr) >= 0) { fprintf(stderr, "%s", s); } // SetConsoleCP(CP_UTF8) is not guaranteed to be called uint16_t* wide = rt_stackalloc((count + 1) * sizeof(uint16_t)); rt_str.utf8to16(wide, count, s, -1); OutputDebugStringW(wide); } } static void rt_debug_println_va(const char* file, int32_t line, const char* func, const char* format, va_list va) { if (func == null) { func = ""; } char file_line[1024]; if (line == 0 && file == null || file[0] == 0x00) { file_line[0] = 0x00; } else { if (file == null) { file = ""; } // backtrace can have null files // full path is useful in MSVC debugger output pane (clickable) // for all other scenarios short filename without path is preferable: const char* name = IsDebuggerPresent() ? file : rt_files.basename(file); snprintf(file_line, rt_countof(file_line) - 1, "%s(%d):", name, line); } file_line[rt_countof(file_line) - 1] = 0x00; // always zero terminated' rt_debug_max_file_line = rt_max(rt_debug_max_file_line, (int32_t)strlen(file_line)); rt_debug_max_function = rt_max(rt_debug_max_function, (int32_t)strlen(func)); char prefix[2 * 1024]; // snprintf() does not guarantee zero termination on truncation snprintf(prefix, rt_countof(prefix) - 1, "%-*s %-*s", rt_debug_max_file_line, file_line, rt_debug_max_function, func); prefix[rt_countof(prefix) - 1] = 0; // zero terminated char text[2 * 1024]; if (format != null && format[0] != 0) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif vsnprintf(text, rt_countof(text) - 1, format, va); text[rt_countof(text) - 1] = 0; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } else { text[0] = 0; } char output[4 * 1024]; snprintf(output, rt_countof(output) - 1, "%s %s", prefix, text); output[rt_countof(output) - 2] = 0; // strip trailing \n which can be remnant of fprintf("...\n") int32_t n = (int32_t)strlen(output); while (n > 0 && (output[n - 1] == '\n' || output[n - 1] == '\r')) { output[n - 1] = 0; n--; } rt_assert(n + 1 < rt_countof(output)); // Win32 OutputDebugString() needs \n output[n + 0] = '\n'; output[n + 1] = 0; rt_debug.output(output, n + 2); // including 0x00 } #else // posix version: static void rt_debug_vprintf(const char* file, int32_t line, const char* func, const char* format, va_list va) { fprintf(stderr, "%s(%d): %s ", file, line, func); vfprintf(stderr, format, va); fprintf(stderr, "\n"); } #endif static void rt_debug_perrno(const char* file, int32_t line, const char* func, int32_t err_no, const char* format, ...) { if (err_no != 0) { if (format != null && format[0] != 0) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } rt_debug.println(file, line, func, "errno: %d %s", err_no, strerror(err_no)); } } static void rt_debug_perror(const char* file, int32_t line, const char* func, int32_t error, const char* format, ...) { if (error != 0) { if (format != null && format[0] != 0) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } rt_debug.println(file, line, func, "error: %s", rt_strerr(error)); } } static void rt_debug_println(const char* file, int32_t line, const char* func, const char* format, ...) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); } static bool rt_debug_is_debugger_present(void) { return IsDebuggerPresent(); } static void rt_debug_breakpoint(void) { if (rt_debug.is_debugger_present()) { DebugBreak(); } } static errno_t rt_debug_raise(uint32_t exception) { rt_core.set_err(0); RaiseException(exception, EXCEPTION_NONCONTINUABLE, 0, null); return rt_core.err(); } static int32_t rt_debug_verbosity_from_string(const char* s) { char* n = null; long v = strtol(s, &n, 10); if (stricmp(s, "quiet") == 0) { return rt_debug.verbosity.quiet; } else if (stricmp(s, "info") == 0) { return rt_debug.verbosity.info; } else if (stricmp(s, "verbose") == 0) { return rt_debug.verbosity.verbose; } else if (stricmp(s, "debug") == 0) { return rt_debug.verbosity.debug; } else if (stricmp(s, "trace") == 0) { return rt_debug.verbosity.trace; } else if (n > s && rt_debug.verbosity.quiet <= v && v <= rt_debug.verbosity.trace) { return v; } else { rt_fatal("invalid verbosity: %s", s); return rt_debug.verbosity.quiet; } } static void rt_debug_test(void) { #ifdef RT_TESTS // not clear what can be tested here if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } #ifndef STATUS_POSSIBLE_DEADLOCK #define STATUS_POSSIBLE_DEADLOCK 0xC0000194uL #endif rt_debug_if rt_debug = { .verbosity = { .level = 0, .quiet = 0, .info = 1, .verbose = 2, .debug = 3, .trace = 4, }, .verbosity_from_string = rt_debug_verbosity_from_string, .tee = null, .output = rt_debug_output, .println = rt_debug_println, .println_va = rt_debug_println_va, .perrno = rt_debug_perrno, .perror = rt_debug_perror, .is_debugger_present = rt_debug_is_debugger_present, .breakpoint = rt_debug_breakpoint, .raise = rt_debug_raise, .exception = { .access_violation = EXCEPTION_ACCESS_VIOLATION, .datatype_misalignment = EXCEPTION_DATATYPE_MISALIGNMENT, .breakpoint = EXCEPTION_BREAKPOINT, .single_step = EXCEPTION_SINGLE_STEP, .array_bounds = EXCEPTION_ARRAY_BOUNDS_EXCEEDED, .float_denormal_operand = EXCEPTION_FLT_DENORMAL_OPERAND, .float_divide_by_zero = EXCEPTION_FLT_DIVIDE_BY_ZERO, .float_inexact_result = EXCEPTION_FLT_INEXACT_RESULT, .float_invalid_operation = EXCEPTION_FLT_INVALID_OPERATION, .float_overflow = EXCEPTION_FLT_OVERFLOW, .float_stack_check = EXCEPTION_FLT_STACK_CHECK, .float_underflow = EXCEPTION_FLT_UNDERFLOW, .int_divide_by_zero = EXCEPTION_INT_DIVIDE_BY_ZERO, .int_overflow = EXCEPTION_INT_OVERFLOW, .priv_instruction = EXCEPTION_PRIV_INSTRUCTION, .in_page_error = EXCEPTION_IN_PAGE_ERROR, .illegal_instruction = EXCEPTION_ILLEGAL_INSTRUCTION, .noncontinuable = EXCEPTION_NONCONTINUABLE_EXCEPTION, .stack_overflow = EXCEPTION_STACK_OVERFLOW, .invalid_disposition = EXCEPTION_INVALID_DISPOSITION, .guard_page = EXCEPTION_GUARD_PAGE, .invalid_handle = EXCEPTION_INVALID_HANDLE, .possible_deadlock = EXCEPTION_POSSIBLE_DEADLOCK }, .test = rt_debug_test }; ================================================ FILE: src/rt/rt_files.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // TODO: test FILE_APPEND_DATA // https://learn.microsoft.com/en-us/windows/win32/fileio/appending-one-file-to-another-file?redirectedfrom=MSDN // are posix and Win32 seek in agreement? rt_static_assertion(SEEK_SET == FILE_BEGIN); rt_static_assertion(SEEK_CUR == FILE_CURRENT); rt_static_assertion(SEEK_END == FILE_END); #ifndef O_SYNC #define O_SYNC (0x10000) #endif static errno_t rt_files_open(rt_file_t* *file, const char* fn, int32_t f) { DWORD access = (f & rt_files.o_wr) ? GENERIC_WRITE : (f & rt_files.o_rw) ? GENERIC_READ | GENERIC_WRITE : GENERIC_READ; access |= (f & rt_files.o_append) ? FILE_APPEND_DATA : 0; DWORD disposition = (f & rt_files.o_create) ? ((f & rt_files.o_excl) ? CREATE_NEW : (f & rt_files.o_trunc) ? CREATE_ALWAYS : OPEN_ALWAYS) : (f & rt_files.o_trunc) ? TRUNCATE_EXISTING : OPEN_EXISTING; const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; DWORD attr = FILE_ATTRIBUTE_NORMAL; attr |= (f & O_SYNC) ? FILE_FLAG_WRITE_THROUGH : 0; *file = CreateFileA(fn, access, share, null, disposition, attr, null); return *file != INVALID_HANDLE_VALUE ? 0 : rt_core.err(); } static bool rt_files_is_valid(rt_file_t* file) { // both null and rt_files.invalid return file != rt_files.invalid && file != null; } static errno_t rt_files_seek(rt_file_t* file, int64_t *position, int32_t method) { LARGE_INTEGER distance_to_move = { .QuadPart = *position }; LARGE_INTEGER p = { 0 }; // pointer errno_t r = rt_b2e(SetFilePointerEx(file, distance_to_move, &p, (DWORD)method)); if (r == 0) { *position = p.QuadPart; } return r; } static inline uint64_t rt_files_ft_to_us(FILETIME ft) { // us (microseconds) return (ft.dwLowDateTime | (((uint64_t)ft.dwHighDateTime) << 32)) / 10; } static int64_t rt_files_a2t(DWORD a) { int64_t type = 0; if (a & FILE_ATTRIBUTE_REPARSE_POINT) { type |= rt_files.type_symlink; } if (a & FILE_ATTRIBUTE_DIRECTORY) { type |= rt_files.type_folder; } if (a & FILE_ATTRIBUTE_DEVICE) { type |= rt_files.type_device; } return type; } #ifdef FILES_LINUX_PATH_BY_FD static int get_final_path_name_by_fd(int fd, char *buffer, int32_t bytes) { swear(bytes >= 0); char fd_path[16 * 1024]; // /proc/self/fd/* is a symbolic link snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", fd); size_t len = readlink(fd_path, buffer, bytes - 1); if (len != -1) { buffer[len] = 0x00; } // Null-terminate the result return len == -1 ? errno : 0; } #endif static errno_t rt_files_stat(rt_file_t* file, rt_files_stat_t* s, bool follow_symlink) { errno_t r = 0; BY_HANDLE_FILE_INFORMATION fi; rt_fatal_win32err(GetFileInformationByHandle(file, &fi)); const bool symlink = (fi.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; if (follow_symlink && symlink) { const DWORD flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS; DWORD n = GetFinalPathNameByHandleA(file, null, 0, flags); if (n == 0) { r = rt_core.err(); } else { char* name = null; r = rt_heap.allocate(null, (void**)&name, (int64_t)n + 2, false); if (r == 0) { n = GetFinalPathNameByHandleA(file, name, n + 1, flags); if (n == 0) { r = rt_core.err(); } else { rt_file_t* f = rt_files.invalid; r = rt_files.open(&f, name, rt_files.o_rd); if (r == 0) { // keep following: r = rt_files.stat(f, s, follow_symlink); rt_files.close(f); } } rt_heap.deallocate(null, name); } } } else { s->size = (int64_t)((uint64_t)fi.nFileSizeLow | (((uint64_t)fi.nFileSizeHigh) << 32)); s->created = rt_files_ft_to_us(fi.ftCreationTime); // since epoch s->accessed = rt_files_ft_to_us(fi.ftLastAccessTime); s->updated = rt_files_ft_to_us(fi.ftLastWriteTime); s->type = rt_files_a2t(fi.dwFileAttributes); } return r; } static errno_t rt_files_read(rt_file_t* file, void* data, int64_t bytes, int64_t *transferred) { errno_t r = 0; *transferred = 0; while (bytes > 0 && r == 0) { DWORD chunk_size = (DWORD)(bytes > UINT32_MAX ? UINT32_MAX : bytes); DWORD bytes_read = 0; r = rt_b2e(ReadFile(file, data, chunk_size, &bytes_read, null)); if (r == 0) { *transferred += bytes_read; bytes -= bytes_read; data = (uint8_t*)data + bytes_read; } } return r; } static errno_t rt_files_write(rt_file_t* file, const void* data, int64_t bytes, int64_t *transferred) { errno_t r = 0; *transferred = 0; while (bytes > 0 && r == 0) { DWORD chunk_size = (DWORD)(bytes > UINT32_MAX ? UINT32_MAX : bytes); DWORD bytes_read = 0; r = rt_b2e(WriteFile(file, data, chunk_size, &bytes_read, null)); if (r == 0) { *transferred += bytes_read; bytes -= bytes_read; data = (const uint8_t*)data + bytes_read; } } return r; } static errno_t rt_files_flush(rt_file_t* file) { return rt_b2e(FlushFileBuffers(file)); } static void rt_files_close(rt_file_t* file) { rt_win32_close_handle(file); } static errno_t rt_files_write_fully(const char* filename, const void* data, int64_t bytes, int64_t *transferred) { if (transferred != null) { *transferred = 0; } errno_t r = 0; const DWORD access = GENERIC_WRITE; const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; const DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH; HANDLE file = CreateFileA(filename, access, share, null, CREATE_ALWAYS, flags, null); if (file == INVALID_HANDLE_VALUE) { r = rt_core.err(); } else { int64_t written = 0; const uint8_t* p = (const uint8_t*)data; while (r == 0 && bytes > 0) { uint64_t write = bytes >= UINT32_MAX ? (uint64_t)(UINT32_MAX) - 0xFFFFuLL : (uint64_t)bytes; rt_assert(0 < write && write < (uint64_t)UINT32_MAX); DWORD chunk = 0; r = rt_b2e(WriteFile(file, p, (DWORD)write, &chunk, null)); written += chunk; bytes -= chunk; } if (transferred != null) { *transferred = written; } errno_t rc = rt_b2e(FlushFileBuffers(file)); if (r == 0) { r = rc; } rt_win32_close_handle(file); } return r; } static errno_t rt_files_unlink(const char* pathname) { if (rt_files.is_folder(pathname)) { return rt_b2e(RemoveDirectoryA(pathname)); } else { return rt_b2e(DeleteFileA(pathname)); } } static errno_t rt_files_create_tmp(char* fn, int32_t count) { // create temporary file (not folder!) see folders_test() about racing rt_swear(fn != null && count > 0); const char* tmp = rt_files.tmp(); errno_t r = 0; if (count < (int32_t)strlen(tmp) + 8) { r = ERROR_BUFFER_OVERFLOW; } else { rt_assert(count > (int32_t)strlen(tmp) + 8); // If GetTempFileNameA() succeeds, the return value is the length, // in chars, of the string copied to lpBuffer, not including the // terminating null character.If the function fails, // the return value is zero. if (count > (int32_t)strlen(tmp) + 8) { char prefix[4] = { 0 }; r = GetTempFileNameA(tmp, prefix, 0, fn) == 0 ? rt_core.err() : 0; if (r == 0) { rt_assert(rt_files.exists(fn) && !rt_files.is_folder(fn)); } else { rt_println("GetTempFileNameA() failed %s", rt_strerr(r)); } } else { r = ERROR_BUFFER_OVERFLOW; } } return r; } #pragma push_macro("files_acl_args") #pragma push_macro("files_get_acl") #pragma push_macro("files_set_acl") #define rt_files_acl_args(acl) DACL_SECURITY_INFORMATION, null, null, acl, null #define rt_files_get_acl(obj, type, acl, sd) (errno_t)( \ (type == SE_FILE_OBJECT ? GetNamedSecurityInfoA((char*)obj, \ SE_FILE_OBJECT, rt_files_acl_args(acl), &sd) : \ (type == SE_KERNEL_OBJECT) ? GetSecurityInfo((HANDLE)obj, \ SE_KERNEL_OBJECT, rt_files_acl_args(acl), &sd) : \ ERROR_INVALID_PARAMETER)) #define rt_files_set_acl(obj, type, acl) (errno_t)( \ (type == SE_FILE_OBJECT ? SetNamedSecurityInfoA((char*)obj, \ SE_FILE_OBJECT, rt_files_acl_args(acl)) : \ (type == SE_KERNEL_OBJECT) ? SetSecurityInfo((HANDLE)obj, \ SE_KERNEL_OBJECT, rt_files_acl_args(acl)) : \ ERROR_INVALID_PARAMETER)) static errno_t rt_files_acl_add_ace(ACL* acl, SID* sid, uint32_t mask, ACL** free_me, byte flags) { ACL_SIZE_INFORMATION info = {0}; ACL* bigger = null; uint32_t bytes_needed = sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(sid) - sizeof(DWORD); errno_t r = rt_b2e(GetAclInformation(acl, &info, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation)); if (r == 0 && info.AclBytesFree < bytes_needed) { const int64_t bytes = (int64_t)(info.AclBytesInUse + bytes_needed); r = rt_heap.allocate(null, (void**)&bigger, bytes, true); if (r == 0) { r = rt_b2e(InitializeAcl((ACL*)bigger, info.AclBytesInUse + bytes_needed, ACL_REVISION)); } } if (r == 0 && bigger != null) { for (int32_t i = 0; i < (int32_t)info.AceCount; i++) { ACCESS_ALLOWED_ACE* ace = null; r = rt_b2e(GetAce(acl, (DWORD)i, (void**)&ace)); if (r != 0) { break; } r = rt_b2e(AddAce(bigger, ACL_REVISION, MAXDWORD, ace, ace->Header.AceSize)); if (r != 0) { break; } } } if (r == 0) { ACCESS_ALLOWED_ACE* ace = null; r = rt_heap.allocate(null, (void**)&ace, bytes_needed, true); if (r == 0) { ace->Header.AceFlags = flags; ace->Header.AceType = ACCESS_ALLOWED_ACE_TYPE; ace->Header.AceSize = (WORD)bytes_needed; ace->Mask = mask; ace->SidStart = sizeof(ACCESS_ALLOWED_ACE); memcpy(&ace->SidStart, sid, GetLengthSid(sid)); r = rt_b2e(AddAce(bigger != null ? bigger : acl, ACL_REVISION, MAXDWORD, ace, bytes_needed)); rt_heap.deallocate(null, ace); } } *free_me = bigger; return r; } static errno_t rt_files_lookup_sid(ACCESS_ALLOWED_ACE* ace) { // handy for debugging SID* sid = (SID*)&ace->SidStart; DWORD l1 = 128, l2 = 128; char account[128]; char group[128]; SID_NAME_USE use; errno_t r = rt_b2e(LookupAccountSidA(null, sid, account, &l1, group, &l2, &use)); if (r == 0) { rt_println("%s/%s: type: %d, mask: 0x%X, flags:%d", group, account, ace->Header.AceType, ace->Mask, ace->Header.AceFlags); } else { rt_println("LookupAccountSidA() failed %s", rt_strerr(r)); } return r; } static errno_t rt_files_add_acl_ace(void* obj, int32_t obj_type, int32_t sid_type, uint32_t mask) { uint8_t stack[SECURITY_MAX_SID_SIZE] = {0}; DWORD n = rt_countof(stack); SID* sid = (SID*)stack; errno_t r = rt_b2e(CreateWellKnownSid((WELL_KNOWN_SID_TYPE)sid_type, null, sid, &n)); if (r != 0) { return ERROR_INVALID_PARAMETER; } ACL* acl = null; void* sd = null; r = rt_files_get_acl(obj, obj_type, &acl, sd); if (r == 0) { ACCESS_ALLOWED_ACE* found = null; for (int32_t i = 0; i < acl->AceCount; i++) { ACCESS_ALLOWED_ACE* ace = null; r = rt_b2e(GetAce(acl, (DWORD)i, (void**)&ace)); if (r != 0) { break; } if (EqualSid((SID*)&ace->SidStart, sid)) { if (ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && (ace->Header.AceFlags & INHERITED_ACE) == 0) { found = ace; } else if (ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE) { rt_println("%d ACE_TYPE is not supported.", ace->Header.AceType); r = ERROR_INVALID_PARAMETER; } break; } } if (r == 0 && found) { if ((found->Mask & mask) != mask) { // rt_println("updating existing ace"); found->Mask |= mask; r = rt_files_set_acl(obj, obj_type, acl); } else { // rt_println("desired access is already allowed by ace"); } } else if (r == 0) { // rt_println("inserting new ace"); ACL* new_acl = null; byte flags = obj_type == SE_FILE_OBJECT ? CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE : 0; r = rt_files_acl_add_ace(acl, sid, mask, &new_acl, flags); if (r == 0) { r = rt_files_set_acl(obj, obj_type, (new_acl != null ? new_acl : acl)); } if (new_acl != null) { rt_heap.deallocate(null, new_acl); } } } if (sd != null) { LocalFree(sd); } return r; } #pragma pop_macro("files_set_acl") #pragma pop_macro("files_get_acl") #pragma pop_macro("files_acl_args") static errno_t rt_files_chmod777(const char* pathname) { SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY; PSID everyone = null; // Create a well-known SID for the Everyone group. rt_fatal_win32err(AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &everyone)); EXPLICIT_ACCESSA ea[1] = { { 0 } }; // Initialize an EXPLICIT_ACCESS structure for an ACE. ea[0].grfAccessPermissions = 0xFFFFFFFF; ea[0].grfAccessMode = GRANT_ACCESS; // The ACE will allow everyone all access. ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ea[0].Trustee.ptstrName = (LPSTR)everyone; // Create a new ACL that contains the new ACEs. ACL* acl = null; rt_fatal_if_error(SetEntriesInAclA(1, ea, null, &acl)); // Initialize a security descriptor. uint8_t stack[SECURITY_DESCRIPTOR_MIN_LENGTH] = {0}; SECURITY_DESCRIPTOR* sd = (SECURITY_DESCRIPTOR*)stack; rt_fatal_win32err(InitializeSecurityDescriptor(sd, SECURITY_DESCRIPTOR_REVISION)); // Add the ACL to the security descriptor. rt_fatal_win32err(SetSecurityDescriptorDacl(sd, /* present flag: */ true, acl, /* not a default DACL: */ false)); // Change the security attributes errno_t r = rt_b2e(SetFileSecurityA(pathname, DACL_SECURITY_INFORMATION, sd)); if (r != 0) { rt_println("chmod777(%s) failed %s", pathname, rt_strerr(r)); } if (everyone != null) { FreeSid(everyone); } if (acl != null) { LocalFree(acl); } return r; } // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectorya // "If lpSecurityAttributes is null, the directory gets a default security // descriptor. The ACLs in the default security descriptor for a directory // are inherited from its parent directory." static errno_t rt_files_mkdirs(const char* dir) { const int32_t n = (int32_t)strlen(dir) + 1; char* s = null; errno_t r = rt_heap.allocate(null, (void**)&s, n, true); const char* next = strchr(dir, '\\'); if (next == null) { next = strchr(dir, '/'); } while (r == 0 && next != null) { if (next > dir && *(next - 1) != ':') { memcpy(s, dir, (size_t)(next - dir)); r = rt_b2e(CreateDirectoryA(s, null)); if (r == ERROR_ALREADY_EXISTS) { r = 0; } } if (r == 0) { const char* prev = ++next; next = strchr(prev, '\\'); if (next == null) { next = strchr(prev, '/'); } } } if (r == 0) { r = rt_b2e(CreateDirectoryA(dir, null)); } rt_heap.deallocate(null, s); return r == ERROR_ALREADY_EXISTS ? 0 : r; } #pragma push_macro("rt_files_realloc_path") #pragma push_macro("rt_files_append_name") #define rt_files_realloc_path(r, pn, pnc, fn, name) do { \ const int32_t bytes = (int32_t)(strlen(fn) + strlen(name) + 3); \ if (bytes > pnc) { \ r = rt_heap.reallocate(null, (void**)&pn, bytes, false); \ if (r != 0) { \ pnc = bytes; \ } else { \ rt_heap.deallocate(null, pn); \ pn = null; \ } \ } \ } while (0) #define rt_files_append_name(pn, pnc, fn, name) do { \ if (strcmp(fn, "\\") == 0 || strcmp(fn, "/") == 0) { \ rt_str.format(pn, pnc, "\\%s", name); \ } else { \ rt_str.format(pn, pnc, "%.*s\\%s", k, fn, name); \ } \ } while (0) static errno_t rt_files_rmdirs(const char* fn) { rt_files_stat_t st; rt_folder_t folder; errno_t r = rt_files.opendir(&folder, fn); if (r == 0) { int32_t k = (int32_t)strlen(fn); // remove trailing backslash (except if it is root: "/" or "\\") if (k > 1 && (fn[k - 1] == '/' || fn[k - 1] == '\\')) { k--; } int32_t pnc = 64 * 1024; // pathname "pn" capacity in bytes char* pn = null; r = rt_heap.allocate(null, (void**)&pn, pnc, false); while (r == 0) { // recurse into sub folders and remove them first // do NOT follow symlinks - it could be disastrous const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && (st.type & rt_files.type_symlink) == 0 && (st.type & rt_files.type_folder) != 0) { rt_files_realloc_path(r, pn, pnc, fn, name); if (r == 0) { rt_files_append_name(pn, pnc, fn, name); r = rt_files.rmdirs(pn); } } } rt_files.closedir(&folder); r = rt_files.opendir(&folder, fn); while (r == 0) { const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } // symlinks are already removed as normal files if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && (st.type & rt_files.type_folder) == 0) { rt_files_realloc_path(r, pn, pnc, fn, name); if (r == 0) { rt_files_append_name(pn, pnc, fn, name); r = rt_files.unlink(pn); if (r != 0) { rt_println("remove(%s) failed %s", pn, rt_strerr(r)); } } } } rt_heap.deallocate(null, pn); rt_files.closedir(&folder); } if (r == 0) { r = rt_files.unlink(fn); } return r; } #pragma pop_macro("rt_files_append_name") #pragma pop_macro("rt_files_realloc_path") static bool rt_files_exists(const char* path) { return PathFileExistsA(path); } static bool rt_files_is_folder(const char* path) { return PathIsDirectoryA(path); } static bool rt_files_is_symlink(const char* filename) { DWORD attributes = GetFileAttributesA(filename); return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; } static const char* rt_files_basename(const char* pathname) { const char* bn = strrchr(pathname, '\\'); if (bn == null) { bn = strrchr(pathname, '/'); } return bn != null ? bn + 1 : pathname; } static errno_t rt_files_copy(const char* s, const char* d) { return rt_b2e(CopyFileA(s, d, false)); } static errno_t rt_files_move(const char* s, const char* d) { static const DWORD flags = MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH; return rt_b2e(MoveFileExA(s, d, flags)); } static errno_t rt_files_link(const char* from, const char* to) { // note reverse order of parameters: return rt_b2e(CreateHardLinkA(to, from, null)); } static errno_t rt_files_symlink(const char* from, const char* to) { // The correct order of parameters for CreateSymbolicLinkA is: // CreateSymbolicLinkA(symlink_to_create, existing_file, flags); DWORD flags = rt_files.is_folder(from) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; return rt_b2e(CreateSymbolicLinkA(to, from, flags)); } static const char* rt_files_known_folder(int32_t kf) { // known folder ids order must match enum see: static const GUID* kf_ids[] = { &FOLDERID_Profile, &FOLDERID_Desktop, &FOLDERID_Documents, &FOLDERID_Downloads, &FOLDERID_Music, &FOLDERID_Pictures, &FOLDERID_Videos, &FOLDERID_Public, &FOLDERID_ProgramFiles, &FOLDERID_ProgramData }; static rt_file_name_t known_folders[rt_countof(kf_ids)]; rt_fatal_if(!(0 <= kf && kf < rt_countof(kf_ids)), "invalid kf=%d", kf); if (known_folders[kf].s[0] == 0) { uint16_t* path = null; rt_fatal_if_error(SHGetKnownFolderPath(kf_ids[kf], 0, null, &path)); const int32_t n = rt_countof(known_folders[kf].s); rt_str.utf16to8(known_folders[kf].s, n, path, -1); CoTaskMemFree(path); } return known_folders[kf].s; } static const char* rt_files_bin(void) { return rt_files_known_folder(rt_files.folder.bin); } static const char* rt_files_data(void) { return rt_files_known_folder(rt_files.folder.data); } static const char* rt_files_tmp(void) { static char tmp[rt_files_max_path]; if (tmp[0] == 0) { // If GetTempPathA() succeeds, the return value is the length, // in chars, of the string copied to lpBuffer, not including // the terminating null character. If the function fails, the // return value is zero. errno_t r = GetTempPathA(rt_countof(tmp), tmp) == 0 ? rt_core.err() : 0; rt_fatal_if(r != 0, "GetTempPathA() failed %s", rt_strerr(r)); } return tmp; } static errno_t rt_files_cwd(char* fn, int32_t count) { rt_swear(count > 1); DWORD bytes = (DWORD)(count - 1); errno_t r = rt_b2e(GetCurrentDirectoryA(bytes, fn)); fn[count - 1] = 0; // always return r; } static errno_t rt_files_chdir(const char* fn) { return rt_b2e(SetCurrentDirectoryA(fn)); } typedef struct rt_files_dir_s { HANDLE handle; WIN32_FIND_DATAA find; // On Win64: 320 bytes } rt_files_dir_t; rt_static_assertion(sizeof(rt_files_dir_t) <= sizeof(rt_folder_t)); static errno_t rt_files_opendir(rt_folder_t* folder, const char* folder_name) { rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; int32_t n = (int32_t)strlen(folder_name); char* fn = null; // extra room for "\*" suffix errno_t r = rt_heap.allocate(null, (void**)&fn, (int64_t)n + 3, false); if (r == 0) { rt_str.format(fn, n + 3, "%s\\*", folder_name); fn[n + 2] = 0; d->handle = FindFirstFileA(fn, &d->find); if (d->handle == INVALID_HANDLE_VALUE) { r = rt_core.err(); } rt_heap.deallocate(null, fn); } return r; } static uint64_t rt_files_ft2us(FILETIME* ft) { // 100ns units to microseconds: return (((uint64_t)ft->dwHighDateTime) << 32 | ft->dwLowDateTime) / 10; } static const char* rt_files_readdir(rt_folder_t* folder, rt_files_stat_t* s) { const char* fn = null; rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; if (FindNextFileA(d->handle, &d->find)) { fn = d->find.cFileName; // Ensure zero termination d->find.cFileName[rt_countof(d->find.cFileName) - 1] = 0x00; if (s != null) { s->accessed = rt_files_ft2us(&d->find.ftLastAccessTime); s->created = rt_files_ft2us(&d->find.ftCreationTime); s->updated = rt_files_ft2us(&d->find.ftLastWriteTime); s->type = rt_files_a2t(d->find.dwFileAttributes); s->size = (int64_t)((((uint64_t)d->find.nFileSizeHigh) << 32) | (uint64_t)d->find.nFileSizeLow); } } return fn; } static void rt_files_closedir(rt_folder_t* folder) { rt_files_dir_t* d = (rt_files_dir_t*)(void*)folder; rt_fatal_win32err(FindClose(d->handle)); } #pragma push_macro("files_test_failed") #ifdef RT_TESTS // TODO: change rt_fatal_if() to swear() #define rt_files_test_failed " failed %s", rt_strerr(rt_core.err()) #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void folders_dump_time(const char* label, uint64_t us) { int32_t year = 0; int32_t month = 0; int32_t day = 0; int32_t hh = 0; int32_t mm = 0; int32_t ss = 0; int32_t ms = 0; int32_t mc = 0; rt_clock.local(us, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); rt_println("%-7s: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d", label, year, month, day, hh, mm, ss, ms, mc); } static void folders_test(void) { uint64_t now = rt_clock.microseconds(); // microseconds since epoch uint64_t before = now - 1 * (uint64_t)rt_clock.usec_in_sec; // one second earlier uint64_t after = now + 2 * (uint64_t)rt_clock.usec_in_sec; // two seconds later int32_t year = 0; int32_t month = 0; int32_t day = 0; int32_t hh = 0; int32_t mm = 0; int32_t ss = 0; int32_t ms = 0; int32_t mc = 0; rt_clock.local(now, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); verbose("now: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d", year, month, day, hh, mm, ss, ms, mc); // Test cwd, setcwd const char* tmp = rt_files.tmp(); char cwd[256] = { 0 }; rt_fatal_if(rt_files.cwd(cwd, sizeof(cwd)) != 0, "rt_files.cwd() failed"); rt_fatal_if(rt_files.chdir(tmp) != 0, "rt_files.chdir(\"%s\") failed %s", tmp, rt_strerr(rt_core.err())); // there is no racing free way to create temporary folder // without having a temporary file for the duration of folder usage: char tmp_file[rt_files_max_path]; // create_tmp() is thread safe race free: errno_t r = rt_files.create_tmp(tmp_file, rt_countof(tmp_file)); rt_fatal_if(r != 0, "rt_files.create_tmp() failed %s", rt_strerr(r)); char tmp_dir[rt_files_max_path]; rt_str_printf(tmp_dir, "%s.dir", tmp_file); r = rt_files.mkdirs(tmp_dir); rt_fatal_if(r != 0, "rt_files.mkdirs(%s) failed %s", tmp_dir, rt_strerr(r)); verbose("%s", tmp_dir); rt_folder_t folder; char pn[rt_files_max_path] = { 0 }; rt_str_printf(pn, "%s/file", tmp_dir); // cannot test symlinks because they are only // available to Administrators and in Developer mode // char sym[rt_files_max_path] = { 0 }; char hard[rt_files_max_path] = { 0 }; char sub[rt_files_max_path] = { 0 }; rt_str_printf(hard, "%s/hard", tmp_dir); rt_str_printf(sub, "%s/subd", tmp_dir); const char* content = "content"; int64_t transferred = 0; r = rt_files.write_fully(pn, content, (int64_t)strlen(content), &transferred); rt_fatal_if(r != 0, "rt_files.write_fully(\"%s\") failed %s", pn, rt_strerr(r)); rt_swear(transferred == (int64_t)strlen(content)); r = rt_files.link(pn, hard); rt_fatal_if(r != 0, "rt_files.link(\"%s\", \"%s\") failed %s", pn, hard, rt_strerr(r)); r = rt_files.mkdirs(sub); rt_fatal_if(r != 0, "rt_files.mkdirs(\"%s\") failed %s", sub, rt_strerr(r)); r = rt_files.opendir(&folder, tmp_dir); rt_fatal_if(r != 0, "rt_files.opendir(\"%s\") failed %s", tmp_dir, rt_strerr(r)); for (;;) { rt_files_stat_t st = { 0 }; const char* name = rt_files.readdir(&folder, &st); if (name == null) { break; } uint64_t at = st.accessed; uint64_t ct = st.created; uint64_t ut = st.updated; rt_swear(ct <= at && ct <= ut); rt_clock.local(ct, &year, &month, &day, &hh, &mm, &ss, &ms, &mc); bool is_folder = st.type & rt_files.type_folder; bool is_symlink = st.type & rt_files.type_symlink; int64_t bytes = st.size; verbose("%s: %04d-%02d-%02d %02d:%02d:%02d.%03d:%03d %lld bytes %s%s", name, year, month, day, hh, mm, ss, ms, mc, bytes, is_folder ? "[folder]" : "", is_symlink ? "[symlink]" : ""); if (strcmp(name, "file") == 0 || strcmp(name, "hard") == 0) { rt_swear(bytes == (int64_t)strlen(content), "size of \"%s\": %lld is incorrect expected: %d", name, bytes, transferred); } if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { rt_swear(is_folder, "\"%s\" is_folder: %d", name, is_folder); } else { rt_swear((strcmp(name, "subd") == 0) == is_folder, "\"%s\" is_folder: %d", name, is_folder); // empirically timestamps are imprecise on NTFS rt_swear(at >= before, "access: %lld >= %lld", at, before); if (ct < before || ut < before || at >= after || ct >= after || ut >= after) { rt_println("file: %s", name); folders_dump_time("before", before); folders_dump_time("create", ct); folders_dump_time("update", ut); folders_dump_time("access", at); } rt_swear(ct >= before, "create: %lld >= %lld", ct, before); rt_swear(ut >= before, "update: %lld >= %lld", ut, before); // and no later than 2 seconds since folders_test() rt_swear(at < after, "access: %lld < %lld", at, after); rt_swear(ct < after, "create: %lld < %lld", ct, after); rt_swear(at < after, "update: %lld < %lld", ut, after); } } rt_files.closedir(&folder); r = rt_files.rmdirs(tmp_dir); rt_fatal_if(r != 0, "rt_files.rmdirs(\"%s\") failed %s", tmp_dir, rt_strerr(r)); r = rt_files.unlink(tmp_file); rt_fatal_if(r != 0, "rt_files.unlink(\"%s\") failed %s", tmp_file, rt_strerr(r)); rt_fatal_if(rt_files.chdir(cwd) != 0, "rt_files.chdir(\"%s\") failed %s", cwd, rt_strerr(rt_core.err())); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("verbose") static void rt_files_test_append_thread(void* p) { rt_file_t* f = (rt_file_t*)p; uint8_t data[256] = {0}; for (int i = 0; i < 256; i++) { data[i] = (uint8_t)i; } int64_t transferred = 0; rt_fatal_if(rt_files.write(f, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write()" rt_files_test_failed); } static void rt_files_test(void) { folders_test(); uint64_t now = rt_clock.microseconds(); // epoch time char tf[256]; // temporary file rt_fatal_if(rt_files.create_tmp(tf, rt_countof(tf)) != 0, "rt_files.create_tmp()" rt_files_test_failed); uint8_t data[256] = {0}; int64_t transferred = 0; for (int i = 0; i < 256; i++) { data[i] = (uint8_t)i; } { rt_file_t* f = rt_files.invalid; rt_fatal_if(rt_files.open(&f, tf, rt_files.o_wr | rt_files.o_create | rt_files.o_trunc) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_fatal_if(rt_files.write_fully(tf, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write_fully()" rt_files_test_failed); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rd) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); for (int32_t i = 0; i < 256; i++) { for (int32_t j = 1; j < 256 - i; j++) { uint8_t test[rt_countof(data)] = { 0 }; int64_t position = i; rt_fatal_if(rt_files.seek(f, &position, rt_files.seek_set) != 0 || position != i, "rt_files.seek(position: %lld) failed %s", position, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.read(f, test, j, &transferred) != 0 || transferred != j, "rt_files.read() transferred: %lld failed %s", transferred, rt_strerr(rt_core.err())); for (int32_t k = 0; k < j; k++) { rt_swear(test[k] == data[i + k], "Data mismatch at position: %d, length %d" "test[%d]: 0x%02X != data[%d + %d]: 0x%02X ", i, j, k, test[k], i, k, data[i + k]); } } } rt_swear((rt_files.o_rd | rt_files.o_wr) != rt_files.o_rw); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rw) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); for (int32_t i = 0; i < 256; i++) { uint8_t val = ~data[i]; int64_t pos = i; rt_fatal_if(rt_files.seek(f, &pos, rt_files.seek_set) != 0 || pos != i, "rt_files.seek() failed %s", rt_core.err()); rt_fatal_if(rt_files.write(f, &val, 1, &transferred) != 0 || transferred != 1, "rt_files.write()" rt_files_test_failed); pos = i; rt_fatal_if(rt_files.seek(f, &pos, rt_files.seek_set) != 0 || pos != i, "rt_files.seek(pos: %lld i: %d) failed %s", pos, i, rt_core.err()); uint8_t read_val = 0; rt_fatal_if(rt_files.read(f, &read_val, 1, &transferred) != 0 || transferred != 1, "rt_files.read()" rt_files_test_failed); rt_swear(read_val == val, "Data mismatch at position %d", i); } rt_files_stat_t s = { 0 }; rt_files.stat(f, &s, false); uint64_t before = now - 1 * (uint64_t)rt_clock.usec_in_sec; // one second before now uint64_t after = now + 2 * (uint64_t)rt_clock.usec_in_sec; // two seconds after rt_swear(before <= s.created && s.created <= after, "before: %lld created: %lld after: %lld", before, s.created, after); rt_swear(before <= s.accessed && s.accessed <= after, "before: %lld created: %lld accessed: %lld", before, s.accessed, after); rt_swear(before <= s.updated && s.updated <= after, "before: %lld created: %lld updated: %lld", before, s.updated, after); rt_files.close(f); rt_fatal_if(rt_files.open(&f, tf, rt_files.o_wr | rt_files.o_create | rt_files.o_trunc) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_files.stat(f, &s, false); rt_swear(s.size == 0, "File is not empty after truncation. .size: %lld", s.size); rt_files.close(f); } { // Append test with threads rt_file_t* f = rt_files.invalid; rt_fatal_if(rt_files.open(&f, tf, rt_files.o_rw | rt_files.o_append) != 0 || !rt_files.is_valid(f), "rt_files.open()" rt_files_test_failed); rt_thread_t thread1 = rt_thread.start(rt_files_test_append_thread, f); rt_thread_t thread2 = rt_thread.start(rt_files_test_append_thread, f); rt_thread.join(thread1, -1); rt_thread.join(thread2, -1); rt_files.close(f); } { // write_fully, exists, is_folder, mkdirs, rmdirs, create_tmp, chmod777 rt_fatal_if(rt_files.write_fully(tf, data, rt_countof(data), &transferred) != 0 || transferred != rt_countof(data), "rt_files.write_fully() failed %s", rt_core.err()); rt_fatal_if(!rt_files.exists(tf), "file \"%s\" does not exist", tf); rt_fatal_if(rt_files.is_folder(tf), "%s is a folder", tf); rt_fatal_if(rt_files.chmod777(tf) != 0, "rt_files.chmod777(\"%s\") failed %s", tf, rt_strerr(rt_core.err())); char folder[256] = { 0 }; rt_str_printf(folder, "%s.folder\\subfolder", tf); rt_fatal_if(rt_files.mkdirs(folder) != 0, "rt_files.mkdirs(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.is_folder(folder), "\"%s\" is not a folder", folder); rt_fatal_if(rt_files.chmod777(folder) != 0, "rt_files.chmod777(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.rmdirs(folder) != 0, "rt_files.rmdirs(\"%s\") failed %s", folder, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists(folder), "folder \"%s\" still exists", folder); } { // getcwd, chdir const char* tmp = rt_files.tmp(); char cwd[256] = { 0 }; rt_fatal_if(rt_files.cwd(cwd, sizeof(cwd)) != 0, "rt_files.cwd() failed"); rt_fatal_if(rt_files.chdir(tmp) != 0, "rt_files.chdir(\"%s\") failed %s", tmp, rt_strerr(rt_core.err())); // symlink if (rt_processes.is_elevated()) { char sym_link[rt_files_max_path]; rt_str_printf(sym_link, "%s.sym_link", tf); rt_fatal_if(rt_files.symlink(tf, sym_link) != 0, "rt_files.symlink(\"%s\", \"%s\") failed %s", tf, sym_link, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.is_symlink(sym_link), "\"%s\" is not a sym_link", sym_link); rt_fatal_if(rt_files.unlink(sym_link) != 0, "rt_files.unlink(\"%s\") failed %s", sym_link, rt_strerr(rt_core.err())); } else { rt_println("Skipping rt_files.symlink test: process is not elevated"); } // hard link char hard_link[rt_files_max_path]; rt_str_printf(hard_link, "%s.hard_link", tf); rt_fatal_if(rt_files.link(tf, hard_link) != 0, "rt_files.link(\"%s\", \"%s\") failed %s", tf, hard_link, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.exists(hard_link), "\"%s\" does not exist", hard_link); rt_fatal_if(rt_files.unlink(hard_link) != 0, "rt_files.unlink(\"%s\") failed %s", hard_link, rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists(hard_link), "\"%s\" still exists", hard_link); // copy, move: rt_fatal_if(rt_files.copy(tf, "copied_file") != 0, "rt_files.copy(\"%s\", 'copied_file') failed %s", tf, rt_strerr(rt_core.err())); rt_fatal_if(!rt_files.exists("copied_file"), "'copied_file' does not exist"); rt_fatal_if(rt_files.move("copied_file", "moved_file") != 0, "rt_files.move('copied_file', 'moved_file') failed %s", rt_strerr(rt_core.err())); rt_fatal_if(rt_files.exists("copied_file"), "'copied_file' still exists"); rt_fatal_if(!rt_files.exists("moved_file"), "'moved_file' does not exist"); rt_fatal_if(rt_files.unlink("moved_file") != 0, "rt_files.unlink('moved_file') failed %s", rt_strerr(rt_core.err())); rt_fatal_if(rt_files.chdir(cwd) != 0, "rt_files.chdir(\"%s\") failed %s", cwd, rt_strerr(rt_core.err())); } rt_fatal_if(rt_files.unlink(tf) != 0); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_files_test(void) {} #endif // RT_TESTS #pragma pop_macro("files_test_failed") rt_files_if rt_files = { .invalid = (rt_file_t*)INVALID_HANDLE_VALUE, // rt_files_stat_t.type: .type_folder = 0x00000010, // FILE_ATTRIBUTE_DIRECTORY .type_symlink = 0x00000400, // FILE_ATTRIBUTE_REPARSE_POINT .type_device = 0x00000040, // FILE_ATTRIBUTE_DEVICE // seek() methods: .seek_set = SEEK_SET, .seek_cur = SEEK_CUR, .seek_end = SEEK_END, // open() flags: missing O_RSYNC, O_DSYNC, O_NONBLOCK, O_NOCTTY .o_rd = O_RDONLY, .o_wr = O_WRONLY, .o_rw = O_RDWR, .o_append = O_APPEND, .o_create = O_CREAT, .o_excl = O_EXCL, .o_trunc = O_TRUNC, .o_sync = O_SYNC, // known folders ids: .folder = { .home = 0, // c:\Users\ .desktop = 1, .documents = 2, .downloads = 3, .music = 4, .pictures = 5, .videos = 6, .shared = 7, // c:\Users\Public .bin = 8, // c:\Program Files .data = 9 // c:\ProgramData }, // methods: .open = rt_files_open, .is_valid = rt_files_is_valid, .seek = rt_files_seek, .stat = rt_files_stat, .read = rt_files_read, .write = rt_files_write, .flush = rt_files_flush, .close = rt_files_close, .write_fully = rt_files_write_fully, .exists = rt_files_exists, .is_folder = rt_files_is_folder, .is_symlink = rt_files_is_symlink, .mkdirs = rt_files_mkdirs, .rmdirs = rt_files_rmdirs, .create_tmp = rt_files_create_tmp, .chmod777 = rt_files_chmod777, .unlink = rt_files_unlink, .link = rt_files_link, .symlink = rt_files_symlink, .basename = rt_files_basename, .copy = rt_files_copy, .move = rt_files_move, .cwd = rt_files_cwd, .chdir = rt_files_chdir, .known_folder = rt_files_known_folder, .bin = rt_files_bin, .data = rt_files_data, .tmp = rt_files_tmp, .opendir = rt_files_opendir, .readdir = rt_files_readdir, .closedir = rt_files_closedir, .test = rt_files_test }; ================================================ FILE: src/rt/rt_generics.c ================================================ #include "rt/rt.h" #ifdef RT_TESTS static void rt_generics_test(void) { { int8_t a = 10, b = 20; rt_swear(rt_max(a++, b++) == 20); rt_swear(rt_min(a++, b++) == 11); } { int32_t a = 10, b = 20; rt_swear(rt_max(a++, b++) == 20); rt_swear(rt_min(a++, b++) == 11); } { fp32_t a = 1.1f, b = 2.2f; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp64_t a = 1.1, b = 2.2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp32_t a = 1.1f, b = 2.2f; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { fp64_t a = 1.1, b = 2.2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { char a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { unsigned char a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } // MS cl.exe version 19.39.33523 has issues with "long": // does not pick up int32_t/uint32_t types for "long" and "unsigned long" { long int a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { unsigned long a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } { long long a = 1, b = 2; rt_swear(rt_max(a, b) == b); rt_swear(rt_min(a, b) == a); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_generics_test(void) { } #endif rt_generics_if rt_generics = { .test = rt_generics_test }; ================================================ FILE: src/rt/rt_heap.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static errno_t rt_heap_alloc(void* *a, int64_t bytes) { return rt_heap.allocate(null, a, bytes, false); } static errno_t rt_heap_alloc_zero(void* *a, int64_t bytes) { return rt_heap.allocate(null, a, bytes, true); } static errno_t rt_heap_realloc(void* *a, int64_t bytes) { return rt_heap.reallocate(null, a, bytes, false); } static errno_t rt_heap_realloc_zero(void* *a, int64_t bytes) { return rt_heap.reallocate(null, a, bytes, true); } static void rt_heap_free(void* a) { rt_heap.deallocate(null, a); } static rt_heap_t* rt_heap_create(bool serialized) { const DWORD options = serialized ? 0 : HEAP_NO_SERIALIZE; return (rt_heap_t*)HeapCreate(options, 0, 0); } static void rt_heap_dispose(rt_heap_t* h) { rt_fatal_win32err(HeapDestroy((HANDLE)h)); } static inline HANDLE rt_heap_or_process_heap(rt_heap_t* h) { static HANDLE process_heap; if (process_heap == null) { process_heap = GetProcessHeap(); } return h != null ? (HANDLE)h : process_heap; } static errno_t rt_heap_allocate(rt_heap_t* h, void* *p, int64_t bytes, bool zero) { rt_swear(bytes > 0); #ifdef DEBUG static bool enabled; if (!enabled) { enabled = true; HeapSetInformation(null, HeapEnableTerminationOnCorruption, null, 0); } #endif const DWORD flags = zero ? HEAP_ZERO_MEMORY : 0; *p = HeapAlloc(rt_heap_or_process_heap(h), flags, (SIZE_T)bytes); return *p == null ? ERROR_OUTOFMEMORY : 0; } static errno_t rt_heap_reallocate(rt_heap_t* h, void* *p, int64_t bytes, bool zero) { rt_swear(bytes > 0); const DWORD flags = zero ? HEAP_ZERO_MEMORY : 0; void* a = *p == null ? // HeapReAlloc(..., null, bytes) may not work HeapAlloc(rt_heap_or_process_heap(h), flags, (SIZE_T)bytes) : HeapReAlloc(rt_heap_or_process_heap(h), flags, *p, (SIZE_T)bytes); if (a != null) { *p = a; } return a == null ? ERROR_OUTOFMEMORY : 0; } static void rt_heap_deallocate(rt_heap_t* h, void* a) { rt_fatal_win32err(HeapFree(rt_heap_or_process_heap(h), 0, a)); } static int64_t rt_heap_bytes(rt_heap_t* h, void* a) { SIZE_T bytes = HeapSize(rt_heap_or_process_heap(h), 0, a); rt_fatal_if(bytes == (SIZE_T)-1); return (int64_t)bytes; } #ifdef RT_TESTS static void rt_heap_test(void) { // TODO: allocate, reallocate deallocate, create, dispose void* a[1024]; // addresses int32_t b[1024]; // bytes uint32_t seed = 0x1; for (int i = 0; i < 1024; i++) { b[i] = (int32_t)(rt_num.random32(&seed) % 1024) + 1; errno_t r = rt_heap.alloc(&a[i], b[i]); rt_swear(r == 0); } for (int i = 0; i < 1024; i++) { rt_heap.free(a[i]); } HeapCompact(rt_heap_or_process_heap(null), 0); // "There is no extended error information for HeapValidate; // do not call GetLastError." rt_swear(HeapValidate(rt_heap_or_process_heap(null), 0, null)); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_heap_test(void) { } #endif rt_heap_if rt_heap = { .alloc = rt_heap_alloc, .alloc_zero = rt_heap_alloc_zero, .realloc = rt_heap_realloc, .realloc_zero = rt_heap_realloc_zero, .free = rt_heap_free, .create = rt_heap_create, .allocate = rt_heap_allocate, .reallocate = rt_heap_reallocate, .deallocate = rt_heap_deallocate, .bytes = rt_heap_bytes, .dispose = rt_heap_dispose, .test = rt_heap_test }; ================================================ FILE: src/rt/rt_loader.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // This is oversimplified Win32 version completely ignoring mode. // I bit more Posix compliant version is here: // https://github.com/dlfcn-win32/dlfcn-win32/blob/master/src/dlfcn.c // POSIX says that if the value of file is NULL, a handle on a global // symbol object must be provided. That object must be able to access // all symbols from the original program file, and any objects loaded // with the RTLD_GLOBAL flag. // The return value from GetModuleHandle( ) allows us to retrieve // symbols only from the original program file. EnumProcessModules() is // used to access symbols from other libraries. For objects loaded // with the RTLD_LOCAL flag, we create our own list later on. They are // excluded from EnumProcessModules() iteration. static void* rt_loader_all; static void* rt_loader_sym_all(const char* name) { void* sym = null; DWORD bytes = 0; rt_fatal_win32err(EnumProcessModules(GetCurrentProcess(), null, 0, &bytes)); rt_assert(bytes % sizeof(HMODULE) == 0); rt_assert(bytes / sizeof(HMODULE) < 1024); // OK to allocate 8KB on stack HMODULE* modules = null; rt_fatal_if_error(rt_heap.allocate(null, (void**)&modules, bytes, false)); rt_fatal_win32err(EnumProcessModules(GetCurrentProcess(), modules, bytes, &bytes)); const int32_t n = bytes / (int32_t)sizeof(HMODULE); for (int32_t i = 0; i < n && sym != null; i++) { sym = rt_loader.sym(modules[i], name); } if (sym == null) { sym = rt_loader.sym(GetModuleHandleA(null), name); } rt_heap.deallocate(null, modules); return sym; } static void* rt_loader_open(const char* filename, int32_t rt_unused(mode)) { return filename == null ? &rt_loader_all : (void*)LoadLibraryA(filename); } static void* rt_loader_sym(void* handle, const char* name) { return handle == &rt_loader_all ? (void*)rt_loader_sym_all(name) : (void*)GetProcAddress((HMODULE)handle, name); } static void rt_loader_close(void* handle) { if (handle != &rt_loader_all) { rt_fatal_win32err(FreeLibrary(handle)); } } #ifdef RT_TESTS // manually test exported function once and comment out because of // creating .lib out of each .exe is annoying #undef RT_LOADER_TEST_EXPORTED_FUNCTION #ifdef RT_LOADER_TEST_EXPORTED_FUNCTION static int32_t rt_loader_test_calls_count; rt_export void rt_loader_test_exported_function(void); void rt_loader_test_exported_function(void) { rt_loader_test_calls_count++; } #endif static void rt_loader_test(void) { void* global = rt_loader.open(null, rt_loader.local); rt_loader.close(global); // NtQueryTimerResolution - http://undocumented.ntinternals.net/ typedef long (__stdcall *query_timer_resolution_t)( long* minimum_resolution, long* maximum_resolution, long* current_resolution); void* nt_dll = rt_loader.open("ntdll", rt_loader.local); query_timer_resolution_t query_timer_resolution = (query_timer_resolution_t)rt_loader.sym(nt_dll, "NtQueryTimerResolution"); // in 100ns = 0.1us units long min_resolution = 0; long max_resolution = 0; // lowest possible delay between timer events long cur_resolution = 0; rt_fatal_if(query_timer_resolution( &min_resolution, &max_resolution, &cur_resolution) != 0); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f millisecond", // min_resolution / 10.0 / 1000.0, // max_resolution / 10.0 / 1000.0, // cur_resolution / 10.0 / 1000.0); // // Interesting observation cur_resolution sometimes 15.625ms or 1.0ms // } rt_loader.close(nt_dll); #ifdef RT_LOADER_TEST_EXPORTED_FUNCTION rt_loader_test_calls_count = 0; rt_loader_test_exported_function(); // to make sure it is linked in rt_swear(rt_loader_test_calls_count == 1); typedef void (*foo_t)(void); foo_t foo = (foo_t)rt_loader.sym(global, "rt_loader_test_exported_function"); foo(); rt_swear(rt_loader_test_calls_count == 2); #endif if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_loader_test(void) {} #endif enum { rt_loader_local = 0, // RTLD_LOCAL All symbols are not made available for relocation processing by other modules. rt_loader_lazy = 1, // RTLD_LAZY Relocations are performed at an implementation-dependent time. rt_loader_now = 2, // RTLD_NOW Relocations are performed when the object is loaded. rt_loader_global = 0x00100, // RTLD_GLOBAL All symbols are available for relocation processing of other modules. }; rt_loader_if rt_loader = { .local = rt_loader_local, .lazy = rt_loader_lazy, .now = rt_loader_now, .global = rt_loader_global, .open = rt_loader_open, .sym = rt_loader_sym, .close = rt_loader_close, .test = rt_loader_test }; ================================================ FILE: src/rt/rt_mem.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static errno_t rt_mem_map_view_of_file(HANDLE file, void* *data, int64_t *bytes, bool rw) { errno_t r = 0; void* address = null; HANDLE mapping = CreateFileMapping(file, null, rw ? PAGE_READWRITE : PAGE_READONLY, (uint32_t)(*bytes >> 32), (uint32_t)*bytes, null); if (mapping == null) { r = rt_core.err(); } else { DWORD access = rw ? FILE_MAP_ALL_ACCESS : FILE_MAP_READ; address = MapViewOfFile(mapping, access, 0, 0, (SIZE_T)*bytes); if (address == null) { r = rt_core.err(); } rt_win32_close_handle(mapping); } if (r == 0) { *data = address; } else { *data = null; *bytes = 0; } return r; } // see: https://learn.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c-- static errno_t rt_mem_set_token_privilege(void* token, const char* name, bool e) { TOKEN_PRIVILEGES tp = { .PrivilegeCount = 1 }; tp.Privileges[0].Attributes = e ? SE_PRIVILEGE_ENABLED : 0; rt_fatal_win32err(LookupPrivilegeValueA(null, name, &tp.Privileges[0].Luid)); return rt_b2e(AdjustTokenPrivileges(token, false, &tp, sizeof(TOKEN_PRIVILEGES), null, null)); } static errno_t rt_mem_adjust_process_privilege_manage_volume_name(void) { // see: https://devblogs.microsoft.com/oldnewthing/20160603-00/?p=93565 const uint32_t access = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; const HANDLE process = GetCurrentProcess(); HANDLE token = null; errno_t r = rt_b2e(OpenProcessToken(process, access, &token)); if (r == 0) { const char* se_manage_volume_name = "SeManageVolumePrivilege"; r = rt_mem_set_token_privilege(token, se_manage_volume_name, true); rt_win32_close_handle(token); } return r; } static errno_t rt_mem_map_file(const char* filename, void* *data, int64_t *bytes, bool rw) { if (rw) { // for SetFileValidData() call: (void)rt_mem_adjust_process_privilege_manage_volume_name(); } errno_t r = 0; const DWORD access = GENERIC_READ | (rw ? GENERIC_WRITE : 0); const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; const DWORD disposition = rw ? OPEN_ALWAYS : OPEN_EXISTING; const DWORD flags = FILE_ATTRIBUTE_NORMAL; HANDLE file = CreateFileA(filename, access, share, null, disposition, flags, null); if (file == INVALID_HANDLE_VALUE) { r = rt_core.err(); } else { LARGE_INTEGER eof = { .QuadPart = 0 }; rt_fatal_win32err(GetFileSizeEx(file, &eof)); if (rw && *bytes > eof.QuadPart) { // increase file size const LARGE_INTEGER size = { .QuadPart = *bytes }; r = r != 0 ? r : (rt_b2e(SetFilePointerEx(file, size, null, FILE_BEGIN))); r = r != 0 ? r : (rt_b2e(SetEndOfFile(file))); // the following not guaranteed to work but helps with sparse files r = r != 0 ? r : (rt_b2e(SetFileValidData(file, *bytes))); // SetFileValidData() only works for Admin (verified) or System accounts if (r == ERROR_PRIVILEGE_NOT_HELD) { r = 0; } // ignore // SetFileValidData() is also semi-security hole because it allows to read // previously not zeroed disk content of other files const LARGE_INTEGER zero = { .QuadPart = 0 }; // rewind stream: r = r != 0 ? r : (rt_b2e(SetFilePointerEx(file, zero, null, FILE_BEGIN))); } else { *bytes = eof.QuadPart; } r = r != 0 ? r : rt_mem_map_view_of_file(file, data, bytes, rw); rt_win32_close_handle(file); } return r; } static errno_t rt_mem_map_ro(const char* filename, void* *data, int64_t *bytes) { return rt_mem_map_file(filename, data, bytes, false); } static errno_t rt_mem_map_rw(const char* filename, void* *data, int64_t *bytes) { return rt_mem_map_file(filename, data, bytes, true); } static void rt_mem_unmap(void* data, int64_t bytes) { rt_assert(data != null && bytes > 0); (void)bytes; /* unused only need for posix version */ if (data != null && bytes > 0) { rt_fatal_win32err(UnmapViewOfFile(data)); } } static errno_t rt_mem_map_resource(const char* label, void* *data, int64_t *bytes) { HRSRC res = FindResourceA(null, label, (const char*)RT_RCDATA); // "LockResource does not actually lock memory; it is just used to // obtain a pointer to the memory containing the resource data. // The name of the function comes from versions prior to Windows XP, // when it was used to lock a global memory block allocated by LoadResource." if (res != null) { *bytes = SizeofResource(null, res); } HGLOBAL g = res != null ? LoadResource(null, res) : null; *data = g != null ? LockResource(g) : null; return *data != null ? 0 : rt_core.err(); } static int32_t rt_mem_page_size(void) { static SYSTEM_INFO system_info; if (system_info.dwPageSize == 0) { GetSystemInfo(&system_info); } return (int32_t)system_info.dwPageSize; } static int rt_mem_large_page_size(void) { static SIZE_T large_page_minimum = 0; if (large_page_minimum == 0) { large_page_minimum = GetLargePageMinimum(); } return (int32_t)large_page_minimum; } static void* rt_mem_allocate(int64_t bytes_multiple_of_page_size) { rt_assert(bytes_multiple_of_page_size > 0); SIZE_T bytes = (SIZE_T)bytes_multiple_of_page_size; SIZE_T page_size = (SIZE_T)rt_mem_page_size(); rt_assert(bytes % page_size == 0); errno_t r = 0; void* a = null; if (bytes_multiple_of_page_size < 0 || bytes % page_size != 0) { SetLastError(ERROR_INVALID_PARAMETER); r = EINVAL; } else { const DWORD type = MEM_COMMIT | MEM_RESERVE; const DWORD physical = type | MEM_PHYSICAL; a = VirtualAlloc(null, bytes, physical, PAGE_READWRITE); if (a == null) { a = VirtualAlloc(null, bytes, type, PAGE_READWRITE); } if (a == null) { r = rt_core.err(); if (r != 0) { rt_println("VirtualAlloc(%lld) failed %s", bytes, rt_strerr(r)); } } else { r = VirtualLock(a, bytes) ? 0 : rt_core.err(); if (r == ERROR_WORKING_SET_QUOTA) { // The default size is 345 pages (for example, // this is 1,413,120 bytes on systems with a 4K page size). SIZE_T min_mem = 0, max_mem = 0; r = rt_b2e(GetProcessWorkingSetSize(GetCurrentProcess(), &min_mem, &max_mem)); if (r != 0) { rt_println("GetProcessWorkingSetSize() failed %s", rt_strerr(r)); } else { max_mem = max_mem + bytes * 2LL; max_mem = (max_mem + page_size - 1) / page_size * page_size + page_size * 16; if (min_mem < max_mem) { min_mem = max_mem; } r = rt_b2e(SetProcessWorkingSetSize(GetCurrentProcess(), min_mem, max_mem)); if (r != 0) { rt_println("SetProcessWorkingSetSize(%lld, %lld) failed %s", (uint64_t)min_mem, (uint64_t)max_mem, rt_strerr(r)); } else { r = rt_b2e(VirtualLock(a, bytes)); } } } if (r != 0) { rt_println("VirtualLock(%lld) failed %s", bytes, rt_strerr(r)); } } } if (r != 0) { rt_println("mem_alloc_pages(%lld) failed %s", bytes, rt_strerr(r)); rt_assert(a == null); } return a; } static void rt_mem_deallocate(void* a, int64_t bytes_multiple_of_page_size) { rt_assert(bytes_multiple_of_page_size > 0); SIZE_T bytes = (SIZE_T)bytes_multiple_of_page_size; errno_t r = 0; SIZE_T page_size = (SIZE_T)rt_mem_page_size(); if (bytes_multiple_of_page_size < 0 || bytes % page_size != 0) { r = EINVAL; rt_println("failed %s", rt_strerr(r)); } else { if (a != null) { // in case it was successfully locked r = rt_b2e(VirtualUnlock(a, bytes)); if (r != 0) { rt_println("VirtualUnlock() failed %s", rt_strerr(r)); } // If the "dwFreeType" parameter is MEM_RELEASE, "dwSize" parameter // must be the base address returned by the VirtualAlloc function when // the region of pages is reserved. r = rt_b2e(VirtualFree(a, 0, MEM_RELEASE)); if (r != 0) { rt_println("VirtuaFree() failed %s", rt_strerr(r)); } } } } static void rt_mem_test(void) { #ifdef RT_TESTS rt_swear(rt_args.c > 0); void* data = null; int64_t bytes = 0; rt_swear(rt_mem.map_ro(rt_args.v[0], &data, &bytes) == 0); rt_swear(data != null && bytes != 0); rt_mem.unmap(data, bytes); // TODO: page_size large_page_size allocate deallocate // TODO: test heap functions if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_mem_if rt_mem = { .map_ro = rt_mem_map_ro, .map_rw = rt_mem_map_rw, .unmap = rt_mem_unmap, .map_resource = rt_mem_map_resource, .page_size = rt_mem_page_size, .large_page_size = rt_mem_large_page_size, .allocate = rt_mem_allocate, .deallocate = rt_mem_deallocate, .test = rt_mem_test }; ================================================ FILE: src/rt/rt_nls.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // Simplistic Win32 implementation of national language support. // Windows NLS family of functions is very complicated and has // difficult history of LANGID vs LCID etc... See: // ResolveLocaleName() // GetThreadLocale() // SetThreadLocale() // GetUserDefaultLocaleName() // WM_SETTINGCHANGE lParam="intl" // and many others... enum { rt_nls_str_count_max = 1024, rt_nls_str_mem_max = 64 * rt_nls_str_count_max }; static char rt_nls_strings_memory[rt_nls_str_mem_max]; // increase if overflows static char* rt_nls_strings_free = rt_nls_strings_memory; static int32_t rt_nls_strings_count; static const char* rt_nls_ls[rt_nls_str_count_max]; // localized strings static const char* rt_nls_ns[rt_nls_str_count_max]; // neutral language strings static uint16_t* rt_nls_load_string(int32_t strid, LANGID lang_id) { rt_assert(0 <= strid && strid < rt_countof(rt_nls_ns)); uint16_t* r = null; int32_t block = strid / 16 + 1; int32_t index = strid % 16; HRSRC res = FindResourceExW(((HMODULE)null), RT_STRING, MAKEINTRESOURCEW(block), lang_id); // rt_println("FindResourceExA(block=%d lang_id=%04X)=%p", block, lang_id, res); uint8_t* memory = res == null ? null : (uint8_t*)LoadResource(null, res); uint16_t* ws = memory == null ? null : (uint16_t*)LockResource(memory); // rt_println("LockResource(block=%d lang_id=%04X)=%p", block, lang_id, ws); if (ws != null) { for (int32_t i = 0; i < 16 && r == null; i++) { if (ws[0] != 0) { int32_t count = (int32_t)ws[0]; // String size in characters. ws++; rt_assert(ws[count - 1] == 0, "use rc.exe /n command line option"); if (i == index) { // the string has been found // rt_println("%04X found %s", lang_id, utf16to8(ws)); r = ws; } ws += count; } else { ws++; } } } return r; } static const char* rt_nls_save_string(uint16_t* utf16) { const int32_t bytes = rt_str.utf8_bytes(utf16, -1); rt_swear(bytes > 1); char* s = rt_nls_strings_free; uintptr_t left = (uintptr_t)rt_countof(rt_nls_strings_memory) - (uintptr_t)(rt_nls_strings_free - rt_nls_strings_memory); rt_fatal_if(left < (uintptr_t)bytes, "string_memory[] overflow"); rt_str.utf16to8(s, (int32_t)left, utf16, -1); rt_assert((int32_t)strlen(s) == bytes - 1, "utf16to8() does not truncate"); rt_nls_strings_free += bytes; return s; } static const char* rt_nls_localized_string(int32_t strid) { rt_swear(0 < strid && strid < rt_countof(rt_nls_ns)); const char* s = null; if (0 < strid && strid < rt_countof(rt_nls_ns)) { if (rt_nls_ls[strid] != null) { s = rt_nls_ls[strid]; } else { LCID lc_id = GetThreadLocale(); LANGID lang_id = LANGIDFROMLCID(lc_id); uint16_t* utf16 = rt_nls_load_string(strid, lang_id); if (utf16 == null) { // try default dialect: LANGID primary = PRIMARYLANGID(lang_id); lang_id = MAKELANGID(primary, SUBLANG_NEUTRAL); utf16 = rt_nls_load_string(strid, lang_id); } if (utf16 != null && utf16[0] != 0x0000) { s = rt_nls_save_string(utf16); rt_nls_ls[strid] = s; } } } return s; } static int32_t rt_nls_strid(const char* s) { int32_t strid = -1; for (int32_t i = 1; i < rt_nls_strings_count && strid == -1; i++) { if (rt_nls_ns[i] != null && strcmp(s, rt_nls_ns[i]) == 0) { strid = i; rt_nls_localized_string(strid); // to save it, ignore result } } return strid; } static const char* rt_nls_string(int32_t strid, const char* defau1t) { const char* r = rt_nls_localized_string(strid); return r == null ? defau1t : r; } static const char* rt_nls_str(const char* s) { int32_t id = rt_nls_strid(s); return id < 0 ? s : rt_nls_string(id, s); } static const char* rt_nls_locale(void) { uint16_t utf16[LOCALE_NAME_MAX_LENGTH + 1]; LCID lc_id = GetThreadLocale(); int32_t n = LCIDToLocaleName(lc_id, utf16, rt_countof(utf16), LOCALE_ALLOW_NEUTRAL_NAMES); static char ln[LOCALE_NAME_MAX_LENGTH * 4 + 1]; ln[0] = 0; if (n == 0) { errno_t r = rt_core.err(); rt_println("LCIDToLocaleName(0x%04X) failed %s", lc_id, rt_str.error(r)); } else { rt_str.utf16to8(ln, rt_countof(ln), utf16, -1); } return ln; } static errno_t rt_nls_set_locale(const char* locale) { errno_t r = 0; uint16_t utf16[LOCALE_NAME_MAX_LENGTH + 1]; rt_str.utf8to16(utf16, rt_countof(utf16), locale, -1); uint16_t rln[LOCALE_NAME_MAX_LENGTH + 1]; // resolved locale name int32_t n = (int32_t)ResolveLocaleName(utf16, rln, (DWORD)rt_countof(rln)); if (n == 0) { r = rt_core.err(); rt_println("ResolveLocaleName(\"%s\") failed %s", locale, rt_str.error(r)); } else { LCID lc_id = LocaleNameToLCID(rln, LOCALE_ALLOW_NEUTRAL_NAMES); if (lc_id == 0) { r = rt_core.err(); rt_println("LocaleNameToLCID(\"%s\") failed %s", locale, rt_str.error(r)); } else { rt_fatal_win32err(SetThreadLocale(lc_id)); memset((void*)rt_nls_ls, 0, sizeof(rt_nls_ls)); // start all over } } return r; } static void rt_nls_init(void) { static_assert(rt_countof(rt_nls_ns) % 16 == 0, "rt_countof(ns) must be multiple of 16"); LANGID lang_id = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL); for (int32_t strid = 0; strid < rt_countof(rt_nls_ns); strid += 16) { int32_t block = strid / 16 + 1; HRSRC res = FindResourceExW(((HMODULE)null), RT_STRING, MAKEINTRESOURCEW(block), lang_id); uint8_t* memory = res == null ? null : (uint8_t*)LoadResource(null, res); uint16_t* ws = memory == null ? null : (uint16_t*)LockResource(memory); if (ws == null) { break; } for (int32_t i = 0; i < 16; i++) { int32_t ix = strid + i; uint16_t count = ws[0]; if (count > 0) { ws++; rt_fatal_if(ws[count - 1] != 0, "use rc.exe /n"); rt_nls_ns[ix] = rt_nls_save_string(ws); rt_nls_strings_count = ix + 1; // rt_println("ns[%d] := %d \"%s\"", ix, strlen(rt_nls_ns[ix]), rt_nls_ns[ix]); ws += count; } else { ws++; } } } } rt_nls_if rt_nls = { .init = rt_nls_init, .strid = rt_nls_strid, .str = rt_nls_str, .string = rt_nls_string, .locale = rt_nls_locale, .set_locale = rt_nls_set_locale, }; ================================================ FILE: src/rt/rt_num.c ================================================ #include "rt/rt.h" #include //#include // _tzcnt_u32 static inline rt_num128_t rt_num_add128_inline(const rt_num128_t a, const rt_num128_t b) { rt_num128_t r = a; r.hi += b.hi; r.lo += b.lo; if (r.lo < b.lo) { r.hi++; } // carry return r; } static inline rt_num128_t rt_num_sub128_inline(const rt_num128_t a, const rt_num128_t b) { rt_num128_t r = a; r.hi -= b.hi; if (r.lo < b.lo) { r.hi--; } // borrow r.lo -= b.lo; return r; } static rt_num128_t rt_num_add128(const rt_num128_t a, const rt_num128_t b) { return rt_num_add128_inline(a, b); } static rt_num128_t rt_num_sub128(const rt_num128_t a, const rt_num128_t b) { return rt_num_sub128_inline(a, b); } static rt_num128_t rt_num_mul64x64(uint64_t a, uint64_t b) { uint64_t a_lo = (uint32_t)a; uint64_t a_hi = a >> 32; uint64_t b_lo = (uint32_t)b; uint64_t b_hi = b >> 32; uint64_t low = a_lo * b_lo; uint64_t cross1 = a_hi * b_lo; uint64_t cross2 = a_lo * b_hi; uint64_t high = a_hi * b_hi; // this cannot overflow as (2^32-1)^2 + 2^32-1 < 2^64-1 cross1 += low >> 32; // this one can overflow cross1 += cross2; // propagate the carry if any high += ((uint64_t)(cross1 < cross2 != 0)) << 32; high = high + (cross1 >> 32); low = ((cross1 & 0xFFFFFFFF) << 32) + (low & 0xFFFFFFFF); return (rt_num128_t){.lo = low, .hi = high }; } static inline void rt_num_shift128_left_inline(rt_num128_t* n) { const uint64_t top = (1ULL << 63); n->hi = (n->hi << 1) | ((n->lo & top) ? 1 : 0); n->lo = (n->lo << 1); } static inline void rt_num_shift128_right_inline(rt_num128_t* n) { const uint64_t top = (1ULL << 63); n->lo = (n->lo >> 1) | ((n->hi & 0x1) ? top : 0); n->hi = (n->hi >> 1); } static inline bool rt_num_less128_inline(const rt_num128_t a, const rt_num128_t b) { return a.hi < b.hi || (a.hi == b.hi && a.lo < b.lo); } static inline bool rt_num_uint128_high_bit(const rt_num128_t a) { return (int64_t)a.hi < 0; } static uint64_t rt_num_muldiv128(uint64_t a, uint64_t b, uint64_t divisor) { rt_swear(divisor > 0, "divisor: %lld", divisor); rt_num128_t r = rt_num.mul64x64(a, b); // reminder: a * b uint64_t q = 0; // quotient if (r.hi >= divisor) { q = UINT64_MAX; // overflow } else { int32_t shift = 0; rt_num128_t d = { .hi = 0, .lo = divisor }; while (!rt_num_uint128_high_bit(d) && rt_num_less128_inline(d, r)) { rt_num_shift128_left_inline(&d); shift++; } rt_assert(shift <= 64); while (shift >= 0 && (d.hi != 0 || d.lo != 0)) { if (!rt_num_less128_inline(r, d)) { r = rt_num_sub128_inline(r, d); rt_assert(shift < 64); q |= (1ULL << shift); } rt_num_shift128_right_inline(&d); shift--; } } return q; } static uint32_t rt_num_gcd32(uint32_t u, uint32_t v) { #pragma push_macro("rt_trailing_zeros") #ifdef _M_ARM64 #define rt_trailing_zeros(x) (_CountTrailingZeros(x)) #else #define rt_trailing_zeros(x) ((int32_t)_tzcnt_u32(x)) #endif if (u == 0) { return v; } else if (v == 0) { return u; } uint32_t i = rt_trailing_zeros(u); u >>= i; uint32_t j = rt_trailing_zeros(v); v >>= j; uint32_t k = rt_min(i, j); for (;;) { rt_assert(u % 2 == 1, "u = %d should be odd", u); rt_assert(v % 2 == 1, "v = %d should be odd", v); if (u > v) { uint32_t swap = u; u = v; v = swap; } v -= u; if (v == 0) { return u << k; } v >>= rt_trailing_zeros(v); } #pragma pop_macro("rt_trailing_zeros") } static uint32_t rt_num_random32(uint32_t* state) { // https://gist.github.com/tommyettinger/46a874533244883189143505d203312c static rt_thread_local bool started; // first seed must be odd if (!started) { started = true; *state |= 1; } uint32_t z = (*state += 0x6D2B79F5UL); z = (z ^ (z >> 15)) * (z | 1UL); z ^= z + (z ^ (z >> 7)) * (z | 61UL); return z ^ (z >> 14); } static uint64_t rt_num_random64(uint64_t *state) { // https://gist.github.com/tommyettinger/e6d3e8816da79b45bfe582384c2fe14a static rt_thread_local bool started; // first seed must be odd if (!started) { started = true; *state |= 1; } const uint64_t s = *state; const uint64_t z = (s ^ s >> 25) * (*state += 0x6A5D39EAE12657AAULL); return z ^ (z >> 22); } // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function static uint32_t rt_num_hash32(const char *data, int64_t len) { uint32_t hash = 0x811c9dc5; // FNV_offset_basis for 32-bit uint32_t prime = 0x01000193; // FNV_prime for 32-bit if (len > 0) { for (int64_t i = 1; i < len; i++) { hash ^= (uint32_t)data[i]; hash *= prime; } } else { for (int64_t i = 0; data[i] != 0; i++) { hash ^= (uint32_t)data[i]; hash *= prime; } } return hash; } static uint64_t rt_num_hash64(const char *data, int64_t len) { uint64_t hash = 0xcbf29ce484222325; // FNV_offset_basis for 64-bit uint64_t prime = 0x100000001b3; // FNV_prime for 64-bit if (len > 0) { for (int64_t i = 0; i < len; i++) { hash ^= (uint64_t)data[i]; hash *= prime; } } else { for (int64_t i = 0; data[i] != 0; i++) { hash ^= (uint64_t)data[i]; hash *= prime; } } return hash; } static uint32_t ctz_2(uint32_t x) { if (x == 0) return 32; unsigned n = 0; while ((x & 1) == 0) { x >>= 1; n++; } return n; } static void rt_num_test(void) { #ifdef RT_TESTS { rt_swear(rt_num.gcd32(1000000000, 24000000) == 8000000); // https://asecuritysite.com/encryption/nprimes?y=64 // https://www.rapidtables.com/convert/number/decimal-to-hex.html uint64_t p = 15843490434539008357u; // prime uint64_t q = 16304766625841520833u; // prime // pq: 258324414073910997987910483408576601381 // 0xC25778F20853A9A1EC0C27C467C45D25 rt_num128_t pq = {.hi = 0xC25778F20853A9A1uLL, .lo = 0xEC0C27C467C45D25uLL }; rt_num128_t p_q = rt_num.mul64x64(p, q); rt_swear(p_q.hi == pq.hi && pq.lo == pq.lo); uint64_t p1 = rt_num.muldiv128(p, q, q); uint64_t q1 = rt_num.muldiv128(p, q, p); rt_swear(p1 == p); rt_swear(q1 == q); } #ifdef DEBUG enum { n = 100 }; #else enum { n = 10000 }; #endif uint64_t seed64 = 1; for (int32_t i = 0; i < n; i++) { uint64_t p = rt_num.random64(&seed64); uint64_t q = rt_num.random64(&seed64); uint64_t p1 = rt_num.muldiv128(p, q, q); uint64_t q1 = rt_num.muldiv128(p, q, p); rt_swear(p == p1, "0%16llx (0%16llu) != 0%16llx (0%16llu)", p, p1); rt_swear(q == q1, "0%16llx (0%16llu) != 0%16llx (0%16llu)", p, p1); } uint32_t seed32 = 1; for (int32_t i = 0; i < n; i++) { uint64_t p = rt_num.random32(&seed32); uint64_t q = rt_num.random32(&seed32); uint64_t r = rt_num.muldiv128(p, q, 1); rt_swear(r == p * q); // division by the maximum uint64_t value: r = rt_num.muldiv128(p, q, UINT64_MAX); rt_swear(r == 0); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } #endif } rt_num_if rt_num = { .add128 = rt_num_add128, .sub128 = rt_num_sub128, .mul64x64 = rt_num_mul64x64, .muldiv128 = rt_num_muldiv128, .gcd32 = rt_num_gcd32, .random32 = rt_num_random32, .random64 = rt_num_random64, .hash32 = rt_num_hash32, .hash64 = rt_num_hash64, .test = rt_num_test }; ================================================ FILE: src/rt/rt_processes.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" typedef struct rt_processes_pidof_lambda_s rt_processes_pidof_lambda_t; typedef struct rt_processes_pidof_lambda_s { bool (*each)(rt_processes_pidof_lambda_t* p, uint64_t pid); // returns true to continue uint64_t* pids; size_t size; // pids[size] size_t count; // number of valid pids in the pids fp64_t timeout; errno_t error; } rt_processes_pidof_lambda_t; static int32_t rt_processes_for_each_pidof(const char* pname, rt_processes_pidof_lambda_t* la) { char stack[1024]; // avoid alloca() int32_t n = rt_str.len(pname); rt_fatal_if(n + 5 >= rt_countof(stack), "name is too long: %s", pname); const char* name = pname; // append ".exe" if not present: if (!rt_str.iends(pname, ".exe")) { int32_t k = (int32_t)strlen(pname) + 5; char* exe = stack; rt_str.format(exe, k, "%s.exe", pname); name = exe; } const char* base = strrchr(name, '\\'); if (base != null) { base++; // advance past "\\" } else { base = name; } uint16_t wn[1024]; rt_fatal_if(strlen(base) >= rt_countof(wn), "name too long: %s", base); rt_str.utf8to16(wn, rt_countof(wn), base, -1); size_t count = 0; uint64_t pid = 0; uint8_t* data = null; ULONG bytes = 0; errno_t r = NtQuerySystemInformation(SystemProcessInformation, data, 0, &bytes); #pragma push_macro("STATUS_INFO_LENGTH_MISMATCH") #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004 while (r == (errno_t)STATUS_INFO_LENGTH_MISMATCH) { // bytes == 420768 on Windows 11 which may be a bit // too much for stack alloca() // add little extra if new process is spawned in between calls. bytes += sizeof(SYSTEM_PROCESS_INFORMATION) * 32; r = rt_heap.reallocate(null, (void**)&data, bytes, false); if (r == 0) { r = NtQuerySystemInformation(SystemProcessInformation, data, bytes, &bytes); } else { rt_assert(r == (errno_t)ERROR_NOT_ENOUGH_MEMORY); } } #pragma pop_macro("STATUS_INFO_LENGTH_MISMATCH") if (r == 0 && data != null) { SYSTEM_PROCESS_INFORMATION* proc = (SYSTEM_PROCESS_INFORMATION*)data; while (proc != null) { uint16_t* img = proc->ImageName.Buffer; // last name only, not a pathname! bool match = img != null && wcsicmp(img, wn) == 0; if (match) { pid = (uint64_t)proc->UniqueProcessId; // HANDLE .UniqueProcessId if (base != name) { char path[rt_files_max_path]; match = rt_processes.nameof(pid, path, rt_countof(path)) == 0 && rt_str.iends(path, name); // rt_println("\"%s\" -> \"%s\" match: %d", name, path, match); } } if (match) { if (la != null && count < la->size && la->pids != null) { la->pids[count] = pid; } count++; if (la != null && la->each != null && !la->each(la, pid)) { break; } } proc = proc->NextEntryOffset != 0 ? (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*)proc + proc->NextEntryOffset) : null; } } if (data != null) { rt_heap.deallocate(null, data); } rt_assert(count <= (uint64_t)INT32_MAX); return (int32_t)count; } static errno_t rt_processes_nameof(uint64_t pid, char* name, int32_t count) { rt_assert(name != null && count > 0); errno_t r = 0; name[0] = 0; HANDLE p = OpenProcess(PROCESS_ALL_ACCESS, false, (DWORD)pid); if (p != null) { r = rt_b2e(GetModuleFileNameExA(p, null, name, count)); name[count - 1] = 0; // ensure zero termination rt_win32_close_handle(p); } else { r = ERROR_NOT_FOUND; } return r; } static bool rt_processes_present(uint64_t pid) { void* h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, (DWORD)pid); bool b = h != null; if (h != null) { rt_win32_close_handle(h); } return b; } static bool rt_processes_first_pid(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { lambda->pids[0] = pid; return false; } static uint64_t rt_processes_pid(const char* pname) { uint64_t first[1] = {0}; rt_processes_pidof_lambda_t lambda = { .each = rt_processes_first_pid, .pids = first, .size = 1, .count = 0, .timeout = 0, .error = 0 }; rt_processes_for_each_pidof(pname, &lambda); return first[0]; } static bool rt_processes_store_pid(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { if (lambda->pids != null && lambda->count < lambda->size) { lambda->pids[lambda->count++] = pid; } return true; // always - need to count all } static errno_t rt_processes_pids(const char* pname, uint64_t* pids/*[size]*/, int32_t size, int32_t *count) { *count = 0; rt_processes_pidof_lambda_t lambda = { .each = rt_processes_store_pid, .pids = pids, .size = (size_t)size, .count = 0, .timeout = 0, .error = 0 }; *count = rt_processes_for_each_pidof(pname, &lambda); return (int32_t)lambda.count == *count ? 0 : ERROR_MORE_DATA; } static errno_t rt_processes_kill(uint64_t pid, fp64_t timeout) { DWORD milliseconds = timeout < 0 ? INFINITE : (DWORD)(timeout * 1000); enum { access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | SYNCHRONIZE }; rt_assert((DWORD)pid == pid); // Windows... HANDLE vs DWORD in different APIs errno_t r = ERROR_NOT_FOUND; HANDLE h = OpenProcess(access, 0, (DWORD)pid); if (h != null) { char path[rt_files_max_path]; path[0] = 0; r = rt_b2e(TerminateProcess(h, ERROR_PROCESS_ABORTED)); if (r == 0) { DWORD ix = WaitForSingleObject(h, milliseconds); r = rt_wait_ix2e(ix); } else { DWORD bytes = rt_countof(path); errno_t rq = rt_b2e(QueryFullProcessImageNameA(h, 0, path, &bytes)); if (rq != 0) { rt_println("QueryFullProcessImageNameA(pid=%d, h=%p) " "failed %s", pid, h, rt_strerr(rq)); } } rt_win32_close_handle(h); if (r == ERROR_ACCESS_DENIED) { // special case rt_thread.sleep_for(0.015); // need to wait a bit HANDLE retry = OpenProcess(access, 0, (DWORD)pid); // process may have died before we have chance to terminate it: if (retry == null) { rt_println("TerminateProcess(pid=%d, h=%p, im=%s) " "failed but zombie died after: %s", pid, h, path, rt_strerr(r)); r = 0; } else { rt_win32_close_handle(retry); } } if (r != 0) { rt_println("TerminateProcess(pid=%d, h=%p, im=%s) failed %s", pid, h, path, rt_strerr(r)); } } if (r != 0) { errno = r; } return r; } static bool rt_processes_kill_one(rt_processes_pidof_lambda_t* lambda, uint64_t pid) { errno_t r = rt_processes_kill(pid, lambda->timeout); if (r != 0) { lambda->error = r; } return true; // keep going } static errno_t rt_processes_kill_all(const char* name, fp64_t timeout) { rt_processes_pidof_lambda_t lambda = { .each = rt_processes_kill_one, .pids = null, .size = 0, .count = 0, .timeout = timeout, .error = 0 }; int32_t c = rt_processes_for_each_pidof(name, &lambda); return c == 0 ? ERROR_NOT_FOUND : lambda.error; } static bool rt_processes_is_elevated(void) { // Is process running as Admin / System ? BOOL elevated = false; PSID administrators_group = null; // Allocate and initialize a SID of the administrators group. SID_IDENTIFIER_AUTHORITY administrators_group_authority = SECURITY_NT_AUTHORITY; errno_t r = rt_b2e(AllocateAndInitializeSid(&administrators_group_authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administrators_group)); if (r != 0) { rt_println("AllocateAndInitializeSid() failed %s", rt_strerr(r)); } PSID system_ops = null; SID_IDENTIFIER_AUTHORITY system_ops_authority = SECURITY_NT_AUTHORITY; r = rt_b2e(AllocateAndInitializeSid(&system_ops_authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_SYSTEM_OPS, 0, 0, 0, 0, 0, 0, &system_ops)); if (r != 0) { rt_println("AllocateAndInitializeSid() failed %s", rt_strerr(r)); } if (administrators_group != null) { r = rt_b2e(CheckTokenMembership(null, administrators_group, &elevated)); } if (system_ops != null && !elevated) { r = rt_b2e(CheckTokenMembership(null, administrators_group, &elevated)); } if (administrators_group != null) { FreeSid(administrators_group); } if (system_ops != null) { FreeSid(system_ops); } if (r != 0) { rt_println("failed %s", rt_strerr(r)); } return elevated; } static errno_t rt_processes_restart_elevated(void) { errno_t r = 0; if (!rt_processes.is_elevated()) { const char* path = rt_processes.name(); SHELLEXECUTEINFOA sei = { sizeof(sei) }; sei.lpVerb = "runas"; sei.lpFile = path; sei.hwnd = null; sei.nShow = SW_NORMAL; r = rt_b2e(ShellExecuteExA(&sei)); if (r == ERROR_CANCELLED) { rt_println("The user unable or refused to allow privileges elevation"); } else if (r == 0) { rt_core.exit(0); // second copy of the app is running now } } return r; } static void rt_processes_close_pipes(STARTUPINFOA* si, HANDLE *read_out, HANDLE *read_err, HANDLE *write_in) { if (si->hStdOutput != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdOutput); } if (si->hStdError != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdError); } if (si->hStdInput != INVALID_HANDLE_VALUE) { rt_win32_close_handle(si->hStdInput); } if (*read_out != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*read_out); } if (*read_err != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*read_err); } if (*write_in != INVALID_HANDLE_VALUE) { rt_win32_close_handle(*write_in); } } static errno_t rt_processes_child_read(rt_stream_if* out, HANDLE pipe) { char data[32 * 1024]; // Temporary buffer for reading DWORD available = 0; errno_t r = rt_b2e(PeekNamedPipe(pipe, null, sizeof(data), null, &available, null)); if (r != 0) { if (r != ERROR_BROKEN_PIPE) { // unexpected! // rt_println("PeekNamedPipe() failed %s", rt_strerr(r)); } // process has exited and closed the pipe rt_assert(r == ERROR_BROKEN_PIPE); } else if (available > 0) { DWORD bytes_read = 0; r = rt_b2e(ReadFile(pipe, data, sizeof(data), &bytes_read, null)); // rt_println("r: %d bytes_read: %d", r, bytes_read); if (out != null) { if (r == 0) { r = out->write(out, data, bytes_read, null); } } else { // no one interested - drop on the floor } } return r; } static errno_t rt_processes_child_write(rt_stream_if* in, HANDLE pipe) { errno_t r = 0; if (in != null) { uint8_t memory[32 * 1024]; // Temporary buffer for reading uint8_t* data = memory; int64_t bytes_read = 0; in->read(in, data, sizeof(data), &bytes_read); while (r == 0 && bytes_read > 0) { DWORD bytes_written = 0; r = rt_b2e(WriteFile(pipe, data, (DWORD)bytes_read, &bytes_written, null)); rt_println("r: %d bytes_written: %d", r, bytes_written); rt_assert((int32_t)bytes_written <= bytes_read); data += bytes_written; bytes_read -= bytes_written; } } return r; } static errno_t rt_processes_run(rt_processes_child_t* child) { const fp64_t deadline = rt_clock.seconds() + child->timeout; errno_t r = 0; STARTUPINFOA si = { .cb = sizeof(STARTUPINFOA), .hStdInput = INVALID_HANDLE_VALUE, .hStdOutput = INVALID_HANDLE_VALUE, .hStdError = INVALID_HANDLE_VALUE, .dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES, .wShowWindow = SW_HIDE }; SECURITY_ATTRIBUTES sa = { sizeof(sa), null, true }; // Inheritable handles PROCESS_INFORMATION pi = {0}; HANDLE read_out = INVALID_HANDLE_VALUE; HANDLE read_err = INVALID_HANDLE_VALUE; HANDLE write_in = INVALID_HANDLE_VALUE; errno_t ro = rt_b2e(CreatePipe(&read_out, &si.hStdOutput, &sa, 0)); errno_t re = rt_b2e(CreatePipe(&read_err, &si.hStdError, &sa, 0)); errno_t ri = rt_b2e(CreatePipe(&si.hStdInput, &write_in, &sa, 0)); if (ro != 0 || re != 0 || ri != 0) { rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); if (ro != 0) { rt_println("CreatePipe() failed %s", rt_strerr(ro)); r = ro; } if (re != 0) { rt_println("CreatePipe() failed %s", rt_strerr(re)); r = re; } if (ri != 0) { rt_println("CreatePipe() failed %s", rt_strerr(ri)); r = ri; } } if (r == 0) { r = rt_b2e(CreateProcessA(null, rt_str.drop_const(child->command), null, null, true, CREATE_NO_WINDOW, null, null, &si, &pi)); if (r != 0) { rt_println("CreateProcess() failed %s", rt_strerr(r)); rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); } } if (r == 0) { // not relevant: stdout can be written in other threads rt_win32_close_handle(pi.hThread); pi.hThread = null; // need to close si.hStdO* handles on caller side so, // when the process closes handles of the pipes, EOF happens // on caller side with io result ERROR_BROKEN_PIPE // indicating no more data can be read or written rt_win32_close_handle(si.hStdOutput); rt_win32_close_handle(si.hStdError); rt_win32_close_handle(si.hStdInput); si.hStdOutput = INVALID_HANDLE_VALUE; si.hStdError = INVALID_HANDLE_VALUE; si.hStdInput = INVALID_HANDLE_VALUE; bool done = false; while (!done && r == 0) { if (child->timeout > 0 && rt_clock.seconds() > deadline) { r = rt_b2e(TerminateProcess(pi.hProcess, ERROR_SEM_TIMEOUT)); if (r != 0) { rt_println("TerminateProcess() failed %s", rt_strerr(r)); } else { done = true; } } if (r == 0) { r = rt_processes_child_write(child->in, write_in); } if (r == 0) { r = rt_processes_child_read(child->out, read_out); } if (r == 0) { r = rt_processes_child_read(child->err, read_err); } if (!done) { DWORD ix = WaitForSingleObject(pi.hProcess, 0); // ix == 0 means process has exited (or terminated) // r == ERROR_BROKEN_PIPE process closed one of the handles done = ix == WAIT_OBJECT_0 || r == ERROR_BROKEN_PIPE; } // to avoid tight loop 100% cpu utilization: if (!done) { rt_thread.yield(); } } // broken pipe actually signifies EOF on the pipe if (r == ERROR_BROKEN_PIPE) { r = 0; } // not an error // if (r != 0) { rt_println("pipe loop failed %s", rt_strerr(r));} DWORD xc = 0; errno_t rx = rt_b2e(GetExitCodeProcess(pi.hProcess, &xc)); if (rx == 0) { child->exit_code = xc; } else { rt_println("GetExitCodeProcess() failed %s", rt_strerr(rx)); if (r != 0) { r = rx; } // report earliest error } rt_processes_close_pipes(&si, &read_out, &read_err, &write_in); // expected never to fail rt_win32_close_handle(pi.hProcess); } return r; } typedef struct { rt_stream_if stream; rt_stream_if* output; errno_t error; } rt_processes_io_merge_out_and_err_if; static errno_t rt_processes_merge_write(rt_stream_if* stream, const void* data, int64_t bytes, int64_t* transferred) { if (transferred != null) { *transferred = 0; } rt_processes_io_merge_out_and_err_if* s = (rt_processes_io_merge_out_and_err_if*)stream; if (s->output != null && bytes > 0) { s->error = s->output->write(s->output, data, bytes, transferred); } return s->error; } static errno_t rt_processes_open(const char* command, int32_t *exit_code, rt_stream_if* output, fp64_t timeout) { rt_not_null(output); rt_processes_io_merge_out_and_err_if merge_out_and_err = { .stream ={ .write = rt_processes_merge_write }, .output = output, .error = 0 }; rt_processes_child_t child = { .command = command, .in = null, .out = &merge_out_and_err.stream, .err = &merge_out_and_err.stream, .exit_code = 0, .timeout = timeout }; errno_t r = rt_processes.run(&child); if (exit_code != null) { *exit_code = (int32_t)child.exit_code; } uint8_t zero = 0; // zero termination merge_out_and_err.stream.write(&merge_out_and_err.stream, &zero, 1, null); if (r == 0 && merge_out_and_err.error != 0) { r = merge_out_and_err.error; // zero termination is not guaranteed } return r; } static errno_t rt_processes_spawn(const char* command) { errno_t r = 0; STARTUPINFOA si = { .cb = sizeof(STARTUPINFOA), .dwFlags = STARTF_USESHOWWINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS, .wShowWindow = SW_HIDE, .hStdInput = INVALID_HANDLE_VALUE, .hStdOutput = INVALID_HANDLE_VALUE, .hStdError = INVALID_HANDLE_VALUE }; const DWORD flags = CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS; PROCESS_INFORMATION pi = { .hProcess = null, .hThread = null }; r = rt_b2e(CreateProcessA(null, rt_str.drop_const(command), null, null, /*bInheritHandles:*/false, flags, null, null, &si, &pi)); if (r == 0) { // Close handles immediately rt_win32_close_handle(pi.hProcess); rt_win32_close_handle(pi.hThread); } else { rt_println("CreateProcess() failed %s", rt_strerr(r)); } return r; } static const char* rt_processes_name(void) { static char mn[rt_files_max_path]; if (mn[0] == 0) { rt_fatal_win32err(GetModuleFileNameA(null, mn, rt_countof(mn))); } return mn; } #ifdef RT_TESTS #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void rt_processes_test(void) { #ifdef RT_TESTS // in alphabetical order const char* names[] = { "svchost", "RuntimeBroker", "conhost" }; for (int32_t j = 0; j < rt_countof(names); j++) { int32_t size = 0; int32_t count = 0; uint64_t* pids = null; errno_t r = rt_processes.pids(names[j], null, size, &count); while (r == ERROR_MORE_DATA && count > 0) { size = count * 2; // set of processes may change rapidly r = rt_heap.reallocate(null, (void**)&pids, (int64_t)sizeof(uint64_t) * (int64_t)size, false); if (r == 0) { r = rt_processes.pids(names[j], pids, size, &count); } } if (r == 0 && count > 0) { for (int32_t i = 0; i < count; i++) { char path[256] = {0}; #pragma warning(suppress: 6011) // dereferencing null r = rt_processes.nameof(pids[i], path, rt_countof(path)); if (r != ERROR_NOT_FOUND) { rt_assert(r == 0 && path[0] != 0); verbose("%6d %s %s", pids[i], path, rt_strerr(r)); } } } rt_heap.deallocate(null, pids); } // test popen() int32_t xc = 0; char data[32 * 1024]; rt_stream_memory_if output; rt_streams.write_only(&output, data, rt_countof(data)); const char* cmd = "cmd /c dir 2>nul >nul"; errno_t r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c dir \"folder that does not exist\\\""; r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c dir"; r = rt_processes.popen(cmd, &xc, &output.stream, 99999.0); verbose("r: %d xc: %d output:\n%s", r, xc, data); rt_streams.write_only(&output, data, rt_countof(data)); cmd = "cmd /c timeout 1"; r = rt_processes.popen(cmd, &xc, &output.stream, 1.0E-9); verbose("r: %d xc: %d output:\n%s", r, xc, data); #endif } #pragma pop_macro("verbose") #else static void rt_processes_test(void) { } #endif rt_processes_if rt_processes = { .pid = rt_processes_pid, .pids = rt_processes_pids, .nameof = rt_processes_nameof, .present = rt_processes_present, .kill = rt_processes_kill, .kill_all = rt_processes_kill_all, .is_elevated = rt_processes_is_elevated, .restart_elevated = rt_processes_restart_elevated, .run = rt_processes_run, .popen = rt_processes_open, .spawn = rt_processes_spawn, .name = rt_processes_name, .test = rt_processes_test }; ================================================ FILE: src/rt/rt_static.c ================================================ #include "rt/rt.h" static void* rt_static_symbol_reference[1024]; static int32_t rt_static_symbol_reference_count; void* rt_force_symbol_reference(void* symbol) { rt_assert(rt_static_symbol_reference_count <= rt_countof(rt_static_symbol_reference), "increase size of rt_static_symbol_reference[%d] to at least %d", rt_countof(rt_static_symbol_reference), rt_static_symbol_reference); if (rt_static_symbol_reference_count < rt_countof(rt_static_symbol_reference)) { rt_static_symbol_reference[rt_static_symbol_reference_count] = symbol; // rt_println("rt_static_symbol_reference[%d] = %p", rt_static_symbol_reference_count, // rt_static_symbol_reference[symbol_reference_count]); rt_static_symbol_reference_count++; } return symbol; } // test rt_static_init() { code } that will be executed in random // order but before main() #ifdef RT_TESTS static int32_t rt_static_init_function_called; static void rt_force_inline rt_static_init_function(void) { rt_static_init_function_called = 1; } rt_static_init(static_init_test) { rt_static_init_function(); } void rt_static_init_test(void) { rt_fatal_if(rt_static_init_function_called != 1, "static_init_function() expected to be called before main()"); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else void rt_static_init_test(void) {} #endif ================================================ FILE: src/rt/rt_str.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static char* rt_str_drop_const(const char* s) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-qual" #endif return (char*)s; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } static int32_t rt_str_len(const char* s) { return (int32_t)strlen(s); } static int32_t rt_str_utf16len(const uint16_t* utf16) { return (int32_t)wcslen(utf16); } static int32_t rt_str_utf8bytes(const char* s, int32_t b) { rt_assert(b >= 1, "should not be called with bytes < 1"); const uint8_t* const u = (const uint8_t*)s; // based on: // https://stackoverflow.com/questions/66715611/check-for-valid-utf-8-encoding-in-c if (b >= 1 && (u[0] & 0x80u) == 0x00u) { return 1; } else if (b > 1) { uint32_t c = (u[0] << 8) | u[1]; // TODO: 0xC080 is a hack - consider removing if (c == 0xC080) { return 2; } // 0xC080 as not zero terminating '\0' if (0xC280 <= c && c <= 0xDFBF && (c & 0xE0C0) == 0xC080) { return 2; } if (b > 2) { c = (c << 8) | u[2]; // reject utf16 surrogates: if (0xEDA080 <= c && c <= 0xEDBFBF) { return 0; } if (0xE0A080 <= c && c <= 0xEFBFBF && (c & 0xF0C0C0) == 0xE08080) { return 3; } if (b > 3) { c = (c << 8) | u[3]; if (0xF0908080 <= c && c <= 0xF48FBFBF && (c & 0xF8C0C0C0) == 0xF0808080) { return 4; } } } } return 0; // invalid utf8 sequence } static int32_t rt_str_glyphs(const char* utf8, int32_t bytes) { rt_swear(bytes >= 0); bool ok = true; int32_t i = 0; int32_t k = 1; while (i < bytes && ok) { const int32_t b = rt_str.utf8bytes(utf8 + i, bytes - i); ok = 0 < b && i + b <= bytes; if (ok) { i += b; k++; } } return ok ? k - 1 : -1; } static void rt_str_lower(char* d, int32_t capacity, const char* s) { int32_t n = rt_str.len(s); rt_swear(capacity > n); for (int32_t i = 0; i < n; i++) { d[i] = (char)tolower(s[i]); } d[n] = 0; } static void rt_str_upper(char* d, int32_t capacity, const char* s) { int32_t n = rt_str.len(s); rt_swear(capacity > n); for (int32_t i = 0; i < n; i++) { d[i] = (char)toupper(s[i]); } d[n] = 0; } static bool rt_str_starts(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && memcmp(s1, s2, n2) == 0; } static bool rt_str_ends(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && memcmp(s1 + n1 - n2, s2, n2) == 0; } static bool rt_str_i_starts(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && strnicmp(s1, s2, n2) == 0; } static bool rt_str_i_ends(const char* s1, const char* s2) { int32_t n1 = (int32_t)strlen(s1); int32_t n2 = (int32_t)strlen(s2); return n1 >= n2 && strnicmp(s1 + n1 - n2, s2, n2) == 0; } static int32_t rt_str_utf8_bytes(const uint16_t* utf16, int32_t chars) { // If `chars` argument is -1, the function utf8_bytes includes the zero // terminating character in the conversion and the returned byte count. // Function will fail (return 0) on incomplete surrogate pairs like // 0xD83D without following 0xDC1E https://compart.com/en/unicode/U+1F41E if (chars == 0) { return 0; } if (chars < 0 && utf16[0] == 0x0000) { return 1; } const int32_t required_bytes_count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16, chars, null, 0, null, null); if (required_bytes_count == 0) { errno_t r = rt_core.err(); rt_println("WideCharToMultiByte() failed %s", rt_strerr(r)); rt_core.set_err(r); } return required_bytes_count == 0 ? -1 : required_bytes_count; } static int32_t rt_str_utf16_chars(const char* utf8, int32_t bytes) { // If `bytes` argument is -1, the function utf16_chars() includes the zero // terminating character in the conversion and the returned character count. if (bytes == 0) { return 0; } if (bytes < 0 && utf8[0] == 0x00) { return 1; } const int32_t required_wide_chars_count = MultiByteToWideChar(CP_UTF8, 0, utf8, bytes, null, 0); if (required_wide_chars_count == 0) { errno_t r = rt_core.err(); rt_println("MultiByteToWideChar() failed %s", rt_strerr(r)); rt_core.set_err(r); } return required_wide_chars_count == 0 ? -1 : required_wide_chars_count; } static errno_t rt_str_utf16to8(char* utf8, int32_t capacity, const uint16_t* utf16, int32_t chars) { if (chars == 0) { return 0; } if (chars < 0 && utf16[0] == 0x0000) { rt_swear(capacity >= 1); utf8[0] = 0x00; return 0; } const int32_t required = rt_str.utf8_bytes(utf16, chars); errno_t r = required < 0 ? rt_core.err() : 0; if (r == 0) { rt_swear(required > 0 && capacity >= required); int32_t bytes = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16, chars, utf8, capacity, null, null); rt_swear(required == bytes); } return r; } static errno_t rt_str_utf8to16(uint16_t* utf16, int32_t capacity, const char* utf8, int32_t bytes) { const int32_t required = rt_str.utf16_chars(utf8, bytes); errno_t r = required < 0 ? rt_core.err() : 0; if (r == 0) { rt_swear(required >= 0 && capacity >= required); int32_t count = MultiByteToWideChar(CP_UTF8, 0, utf8, bytes, utf16, capacity); rt_swear(required == count); #if 0 // TODO: incorrect need output != input if (count > 0 && !IsNormalizedString(NormalizationC, utf16, count)) { rt_core.set_err(0); int32_t n = NormalizeString(NormalizationC, utf16, count, utf16, count); if (n <= 0) { r = rt_core.err(); rt_println("NormalizeString() failed %s", rt_strerr(r)); } } #endif } return r; } static bool rt_str_utf16_is_low_surrogate(uint16_t utf16char) { return 0xDC00 <= utf16char && utf16char <= 0xDFFF; } static bool rt_str_utf16_is_high_surrogate(uint16_t utf16char) { return 0xD800 <= utf16char && utf16char <= 0xDBFF; } static uint32_t rt_str_utf32(const char* utf8, int32_t bytes) { uint32_t utf32 = 0; if ((utf8[0] & 0x80) == 0) { utf32 = utf8[0]; rt_swear(bytes == 1); } else if ((utf8[0] & 0xE0) == 0xC0) { utf32 = (utf8[0] & 0x1F) << 6; utf32 |= (utf8[1] & 0x3F); rt_swear(bytes == 2); } else if ((utf8[0] & 0xF0) == 0xE0) { utf32 = (utf8[0] & 0x0F) << 12; utf32 |= (utf8[1] & 0x3F) << 6; utf32 |= (utf8[2] & 0x3F); rt_swear(bytes == 3); } else if ((utf8[0] & 0xF8) == 0xF0) { utf32 = (utf8[0] & 0x07) << 18; utf32 |= (utf8[1] & 0x3F) << 12; utf32 |= (utf8[2] & 0x3F) << 6; utf32 |= (utf8[3] & 0x3F); rt_swear(bytes == 4); } else { rt_swear(false); } return utf32; } static void rt_str_format_va(char* utf8, int32_t count, const char* format, va_list va) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif vsnprintf(utf8, (size_t)count, format, va); utf8[count - 1] = 0; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif } static void rt_str_format(char* utf8, int32_t count, const char* format, ...) { va_list va; va_start(va, format); rt_str.format_va(utf8, count, format, va); va_end(va); } static rt_str1024_t rt_str_error_for_language(int32_t error, LANGID language) { DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; HMODULE module = null; HRESULT hr = 0 <= error && error <= 0xFFFF ? HRESULT_FROM_WIN32((uint32_t)error) : (HRESULT)error; if ((error & 0xC0000000U) == 0xC0000000U) { // https://stackoverflow.com/questions/25566234/how-to-convert-specific-ntstatus-value-to-the-hresult static HMODULE ntdll; // RtlNtStatusToDosError implies linking to ntdll if (ntdll == null) { ntdll = GetModuleHandleA("ntdll.dll"); } if (ntdll == null) { ntdll = LoadLibraryA("ntdll.dll"); } module = ntdll; hr = HRESULT_FROM_WIN32(RtlNtStatusToDosError((NTSTATUS)error)); flags |= FORMAT_MESSAGE_FROM_HMODULE; } rt_str1024_t text; uint16_t utf16[rt_countof(text.s)]; DWORD count = FormatMessageW(flags, module, hr, language, utf16, rt_countof(utf16) - 1, (va_list*)null); utf16[rt_countof(utf16) - 1] = 0; // always // If FormatMessageW() succeeds, the return value is the number of utf16 // characters stored in the output buffer, excluding the terminating zero. if (count > 0) { rt_swear(count < rt_countof(utf16)); utf16[count] = 0; // remove trailing '\r\n' int32_t k = count; if (k > 0 && utf16[k - 1] == '\n') { utf16[k - 1] = 0; } k = (int32_t)rt_str.len16(utf16); if (k > 0 && utf16[k - 1] == '\r') { utf16[k - 1] = 0; } char message[rt_countof(text.s)]; const int32_t bytes = rt_str.utf8_bytes(utf16, -1); if (bytes >= rt_countof(message)) { rt_str_printf(message, "error message is too long: %d bytes", bytes); } else { rt_str.utf16to8(message, rt_countof(message), utf16, -1); } // truncating printf to string: rt_str_printf(text.s, "0x%08X(%d) \"%s\"", error, error, message); } else { rt_str_printf(text.s, "0x%08X(%d)", error, error); } return text; } static rt_str1024_t rt_str_error(int32_t error) { const LANGID language = MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT); return rt_str_error_for_language(error, language); } static rt_str1024_t rt_str_error_nls(int32_t error) { const LANGID language = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); return rt_str_error_for_language(error, language); } static const char* rt_str_grouping_separator(void) { #ifdef WINDOWS // en-US Windows 10/11: // grouping_separator == "," // decimal_separator == "." static char grouping_separator[8]; if (grouping_separator[0] == 0x00) { errno_t r = rt_b2e(GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, grouping_separator, sizeof(grouping_separator))); rt_swear(r == 0 && grouping_separator[0] != 0); } return grouping_separator; #else // en-US Windows 10/11: // grouping_separator == "" // decimal_separator == "." struct lconv *locale_info = localeconv(); const char* grouping_separator = null; if (grouping_separator == null) { grouping_separator = locale_info->thousands_sep; swear(grouping_separator != null); } return grouping_separator; #endif } // Posix and Win32 C runtime: // #include // struct lconv *locale_info = localeconv(); // const char* grouping_separator = locale_info->thousands_sep; // const char* decimal_separator = locale_info->decimal_point; // en-US Windows 1x: // grouping_separator == "" // decimal_separator == "." // // Win32 API: // rt_b2e(GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, // grouping_separator, sizeof(grouping_separator))); // rt_b2e(GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, // decimal_separator, sizeof(decimal_separator))); // en-US Windows 1x: // grouping_separator == "," // decimal_separator == "." static rt_str64_t rt_str_int64_dg(int64_t v, // digit_grouped bool uint, const char* gs) { // grouping separator: gs // sprintf format %`lld may not be implemented or // does not respect locale or UI separators... // Do it hard way: const int32_t m = (int32_t)strlen(gs); rt_swear(m < 5); // utf-8 4 bytes max // 64 calls per thread 32 or less bytes each because: // "18446744073709551615" 21 characters + 6x4 groups: // "18'446'744'073'709'551'615" 27 characters rt_str64_t text; enum { max_text_bytes = rt_countof(text.s) }; int64_t abs64 = v < 0 ? -v : v; // incorrect for INT64_MIN uint64_t n = uint ? (uint64_t)v : (v != INT64_MIN ? (uint64_t)abs64 : (uint64_t)INT64_MIN); int32_t i = 0; int32_t groups[8]; // 2^63 - 1 ~= 9 x 10^19 upto 7 groups of 3 digits do { groups[i] = n % 1000; n = n / 1000; i++; } while (n > 0); const int32_t gc = i - 1; // group count char* s = text.s; if (v < 0 && !uint) { *s++ = '-'; } // sign int32_t r = max_text_bytes - 1; while (i > 0) { i--; rt_assert(r > 3 + m); if (i == gc) { rt_str.format(s, r, "%d%s", groups[i], gc > 0 ? gs : ""); } else { rt_str.format(s, r, "%03d%s", groups[i], i > 0 ? gs : ""); } int32_t k = (int32_t)strlen(s); r -= k; s += k; } *s = 0; return text; } static rt_str64_t rt_str_int64(int64_t v) { return rt_str_int64_dg(v, false, rt_glyph_hair_space); } static rt_str64_t rt_str_uint64(uint64_t v) { return rt_str_int64_dg(v, true, rt_glyph_hair_space); } static rt_str64_t rt_str_int64_lc(int64_t v) { return rt_str_int64_dg(v, false, rt_str_grouping_separator()); } static rt_str64_t rt_str_uint64_lc(uint64_t v) { return rt_str_int64_dg(v, true, rt_str_grouping_separator()); } static rt_str128_t rt_str_fp(const char* format, fp64_t v) { static char decimal_separator[8]; if (decimal_separator[0] == 0) { errno_t r = rt_b2e(GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, decimal_separator, sizeof(decimal_separator))); rt_swear(r == 0 && decimal_separator[0] != 0); } rt_swear(strlen(decimal_separator) <= 4); rt_str128_t f; // formatted float point // snprintf format does not handle thousands separators on all know runtimes // and respects setlocale() on Un*x systems but in MS runtime only when // _snprintf_l() is used. f.s[0] = 0x00; rt_str.format(f.s, rt_countof(f.s), format, v); f.s[rt_countof(f.s) - 1] = 0x00; rt_str128_t text; char* s = f.s; char* d = text.s; while (*s != 0x00) { if (*s == '.') { const char* sep = decimal_separator; while (*sep != 0x00) { *d++ = *sep++; } s++; } else { *d++ = *s++; } } *d = 0x00; // TODO: It's possible to handle mantissa grouping but... // Not clear if human expects it in 5 digits or 3 digits chunks // and unfortunately locale does not specify how return text; } #ifdef RT_TESTS static void rt_str_test(void) { rt_swear(rt_str.len("hello") == 5); rt_swear(rt_str.starts("hello world", "hello")); rt_swear(rt_str.ends("hello world", "world")); rt_swear(rt_str.istarts("hello world", "HeLlO")); rt_swear(rt_str.iends("hello world", "WoRlD")); char ls[20] = {0}; rt_str.lower(ls, rt_countof(ls), "HeLlO WoRlD"); rt_swear(strcmp(ls, "hello world") == 0); char upper[11] = {0}; rt_str.upper(upper, rt_countof(upper), "hello12345"); rt_swear(strcmp(upper, "HELLO12345") == 0); #pragma push_macro("glyph_chinese_one") #pragma push_macro("glyph_chinese_two") #pragma push_macro("glyph_teddy_bear") #pragma push_macro("glyph_ice_cube") #define glyph_chinese_one "\xE5\xA3\xB9" #define glyph_chinese_two "\xE8\xB4\xB0" #define glyph_teddy_bear "\xF0\x9F\xA7\xB8" #define glyph_ice_cube "\xF0\x9F\xA7\x8A" const char* utf8_str = glyph_teddy_bear "0" rt_glyph_chinese_jin4 rt_glyph_chinese_gong "3456789 " glyph_ice_cube; rt_swear(rt_str.utf8bytes("\x01", 1) == 1); rt_swear(rt_str.utf8bytes("\x7F", 1) == 1); rt_swear(rt_str.utf8bytes("\x80", 1) == 0); // swear(rt_str.utf8bytes(glyph_chinese_one, 0) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 1) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 2) == 0); rt_swear(rt_str.utf8bytes(glyph_chinese_one, 3) == 3); rt_swear(rt_str.utf8bytes(glyph_teddy_bear, 4) == 4); #pragma pop_macro("glyph_ice_cube") #pragma pop_macro("glyph_teddy_bear") #pragma pop_macro("glyph_chinese_two") #pragma pop_macro("glyph_chinese_one") uint16_t wide_str[100] = {0}; rt_str.utf8to16(wide_str, rt_countof(wide_str), utf8_str, -1); char utf8[100] = {0}; rt_str.utf16to8(utf8, rt_countof(utf8), wide_str, -1); uint16_t utf16[100]; rt_str.utf8to16(utf16, rt_countof(utf16), utf8, -1); char narrow_str[100] = {0}; rt_str.utf16to8(narrow_str, rt_countof(narrow_str), utf16, -1); rt_swear(strcmp(narrow_str, utf8_str) == 0); char formatted[100]; rt_str.format(formatted, rt_countof(formatted), "n: %d, s: %s", 42, "test"); rt_swear(strcmp(formatted, "n: 42, s: test") == 0); // numeric values digit grouping format: rt_swear(strcmp("0", rt_str.int64_dg(0, true, ",").s) == 0); rt_swear(strcmp("-1", rt_str.int64_dg(-1, false, ",").s) == 0); rt_swear(strcmp("999", rt_str.int64_dg(999, true, ",").s) == 0); rt_swear(strcmp("-999", rt_str.int64_dg(-999, false, ",").s) == 0); rt_swear(strcmp("1,001", rt_str.int64_dg(1001, true, ",").s) == 0); rt_swear(strcmp("-1,001", rt_str.int64_dg(-1001, false, ",").s) == 0); rt_swear(strcmp("18,446,744,073,709,551,615", rt_str.int64_dg(UINT64_MAX, true, ",").s) == 0 ); rt_swear(strcmp("9,223,372,036,854,775,807", rt_str.int64_dg(INT64_MAX, false, ",").s) == 0 ); rt_swear(strcmp("-9,223,372,036,854,775,808", rt_str.int64_dg(INT64_MIN, false, ",").s) == 0 ); // see: // https://en.wikipedia.org/wiki/Single-precision_floating-point_format uint32_t pi_fp32 = 0x40490FDBULL; // 3.14159274101257324 rt_swear(strcmp("3.141592741", rt_str.fp("%.9f", *(fp32_t*)&pi_fp32).s) == 0, "%s", rt_str.fp("%.9f", *(fp32_t*)&pi_fp32).s ); // 3.141592741 // ********^ (*** true digits ^ first rounded digit) // 123456 (%.6f) // // https://en.wikipedia.org/wiki/Double-precision_floating-point_format uint64_t pi_fp64 = 0x400921FB54442D18ULL; rt_swear(strcmp("3.141592653589793116", rt_str.fp("%.18f", *(fp64_t*)&pi_fp64).s) == 0, "%s", rt_str.fp("%.18f", *(fp64_t*)&pi_fp64).s ); // 3.141592653589793116 // *****************^ (*** true digits ^ first rounded digit) // 123456789012345 (%.15f) // https://en.wikipedia.org/wiki/Double-precision_floating-point_format // // actual "pi" first 64 digits: // 3.1415926535897932384626433832795028841971693993751058209749445923 if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_str_test(void) {} #endif rt_str_if rt_str = { .drop_const = rt_str_drop_const, .len = rt_str_len, .len16 = rt_str_utf16len, .utf8bytes = rt_str_utf8bytes, .glyphs = rt_str_glyphs, .lower = rt_str_lower, .upper = rt_str_upper, .starts = rt_str_starts, .ends = rt_str_ends, .istarts = rt_str_i_starts, .iends = rt_str_i_ends, .utf8_bytes = rt_str_utf8_bytes, .utf16_chars = rt_str_utf16_chars, .utf16to8 = rt_str_utf16to8, .utf8to16 = rt_str_utf8to16, .utf16_is_low_surrogate = rt_str_utf16_is_low_surrogate, .utf16_is_high_surrogate = rt_str_utf16_is_high_surrogate, .utf32 = rt_str_utf32, .format = rt_str_format, .format_va = rt_str_format_va, .error = rt_str_error, .error_nls = rt_str_error_nls, .grouping_separator = rt_str_grouping_separator, .int64_dg = rt_str_int64_dg, .int64 = rt_str_int64, .uint64 = rt_str_uint64, .int64_lc = rt_str_int64, .uint64_lc = rt_str_uint64, .fp = rt_str_fp, .test = rt_str_test }; ================================================ FILE: src/rt/rt_streams.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" static errno_t rt_streams_memory_read(rt_stream_if* stream, void* data, int64_t bytes, int64_t *transferred) { rt_swear(bytes > 0); rt_stream_memory_if* s = (rt_stream_memory_if*)stream; rt_swear(0 <= s->pos_read && s->pos_read <= s->bytes_read, "bytes: %lld stream .pos: %lld .bytes: %lld", bytes, s->pos_read, s->bytes_read); int64_t transfer = rt_min(bytes, s->bytes_read - s->pos_read); memcpy(data, (const uint8_t*)s->data_read + s->pos_read, (size_t)transfer); s->pos_read += transfer; if (transferred != null) { *transferred = transfer; } return 0; } static errno_t rt_streams_memory_write(rt_stream_if* stream, const void* data, int64_t bytes, int64_t *transferred) { rt_swear(bytes > 0); rt_stream_memory_if* s = (rt_stream_memory_if*)stream; rt_swear(0 <= s->pos_write && s->pos_write <= s->bytes_write, "bytes: %lld stream .pos: %lld .bytes: %lld", bytes, s->pos_write, s->bytes_write); bool overflow = s->bytes_write - s->pos_write <= 0; int64_t transfer = rt_min(bytes, s->bytes_write - s->pos_write); memcpy((uint8_t*)s->data_write + s->pos_write, data, (size_t)transfer); s->pos_write += transfer; if (transferred != null) { *transferred = transfer; } return overflow ? ERROR_INSUFFICIENT_BUFFER : 0; } static void rt_streams_read_only(rt_stream_memory_if* s, const void* data, int64_t bytes) { s->stream.read = rt_streams_memory_read; s->stream.write = null; s->data_read = data; s->bytes_read = bytes; s->pos_read = 0; s->data_write = null; s->bytes_write = 0; s->pos_write = 0; } static void rt_streams_write_only(rt_stream_memory_if* s, void* data, int64_t bytes) { s->stream.read = null; s->stream.write = rt_streams_memory_write; s->data_read = null; s->bytes_read = 0; s->pos_read = 0; s->data_write = data; s->bytes_write = bytes; s->pos_write = 0; } static void rt_streams_read_write(rt_stream_memory_if* s, const void* read, int64_t read_bytes, void* write, int64_t write_bytes) { s->stream.read = rt_streams_memory_read; s->stream.write = rt_streams_memory_write; s->data_read = read; s->bytes_read = read_bytes; s->pos_read = 0; s->pos_read = 0; s->data_write = write; s->bytes_write = write_bytes; s->pos_write = 0; } #ifdef RT_TESTS static void rt_streams_test(void) { { // read test uint8_t memory[256]; for (int32_t i = 0; i < rt_countof(memory); i++) { memory[i] = (uint8_t)i; } for (int32_t i = 1; i < rt_countof(memory) - 1; i++) { rt_stream_memory_if ms; // memory stream rt_streams.read_only(&ms, memory, sizeof(memory)); uint8_t data[256]; for (int32_t j = 0; j < rt_countof(data); j++) { data[j] = 0xFF; } int64_t transferred = 0; errno_t r = ms.stream.read(&ms.stream, data, i, &transferred); rt_swear(r == 0 && transferred == i); for (int32_t j = 0; j < i; j++) { rt_swear(data[j] == memory[j]); } for (int32_t j = i; j < rt_countof(data); j++) { rt_swear(data[j] == 0xFF); } } } { // write test // TODO: implement } { // read/write test // TODO: implement } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_streams_test(void) { } #endif rt_streams_if rt_streams = { .read_only = rt_streams_read_only, .write_only = rt_streams_write_only, .read_write = rt_streams_read_write, .test = rt_streams_test }; ================================================ FILE: src/rt/rt_threads.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" // events: static rt_event_t rt_event_create(void) { HANDLE e = CreateEvent(null, false, false, null); rt_not_null(e); return (rt_event_t)e; } static rt_event_t rt_event_create_manual(void) { HANDLE e = CreateEvent(null, true, false, null); rt_not_null(e); return (rt_event_t)e; } static void rt_event_set(rt_event_t e) { rt_fatal_win32err(SetEvent((HANDLE)e)); } static void rt_event_reset(rt_event_t e) { rt_fatal_win32err(ResetEvent((HANDLE)e)); } static int32_t rt_event_wait_or_timeout(rt_event_t e, fp64_t seconds) { uint32_t ms = seconds < 0 ? INFINITE : (uint32_t)(seconds * 1000.0 + 0.5); DWORD i = WaitForSingleObject(e, ms); rt_swear(i != WAIT_FAILED, "i: %d", i); errno_t r = rt_wait_ix2e(i); if (r != 0) { rt_swear(i == WAIT_TIMEOUT || i == WAIT_ABANDONED); } return i == WAIT_TIMEOUT ? -1 : (i == WAIT_ABANDONED ? -2 : i); } static void rt_event_wait(rt_event_t e) { rt_event_wait_or_timeout(e, -1); } static int32_t rt_event_wait_any_or_timeout(int32_t n, rt_event_t events[], fp64_t s) { rt_swear(n < 64); // Win32 API limit const uint32_t ms = s < 0 ? INFINITE : (uint32_t)(s * 1000.0 + 0.5); const HANDLE* es = (const HANDLE*)events; DWORD i = WaitForMultipleObjects((DWORD)n, es, false, ms); rt_swear(i != WAIT_FAILED, "i: %d", i); errno_t r = rt_wait_ix2e(i); if (r != 0) { rt_swear(i == WAIT_TIMEOUT || i == WAIT_ABANDONED); } return i == WAIT_TIMEOUT ? -1 : (i == WAIT_ABANDONED ? -2 : i); } static int32_t rt_event_wait_any(int32_t n, rt_event_t e[]) { return rt_event_wait_any_or_timeout(n, e, -1); } static void rt_event_dispose(rt_event_t h) { rt_win32_close_handle(h); } #ifdef RT_TESTS // test: // check if the elapsed time is within the expected range static void rt_event_test_check_time(fp64_t start, fp64_t expected) { fp64_t elapsed = rt_clock.seconds() - start; // Old Windows scheduler is prone to 2x16.6ms ~ 33ms delays (observed) rt_swear(elapsed >= expected - 0.04 && elapsed <= expected + 0.250, "expected: %f elapsed %f seconds", expected, elapsed); } static void rt_event_test(void) { rt_event_t event = rt_event.create(); fp64_t start = rt_clock.seconds(); rt_event.set(event); rt_event.wait(event); rt_event_test_check_time(start, 0); // Event should be immediate rt_event.reset(event); start = rt_clock.seconds(); const fp64_t timeout_seconds = 1.0 / 8.0; int32_t result = rt_event.wait_or_timeout(event, timeout_seconds); rt_event_test_check_time(start, timeout_seconds); rt_swear(result == -1); // Timeout expected enum { count = 5 }; rt_event_t events[count]; for (int32_t i = 0; i < rt_countof(events); i++) { events[i] = rt_event.create_manual(); } start = rt_clock.seconds(); rt_event.set(events[2]); // Set the third event int32_t index = rt_event.wait_any(rt_countof(events), events); rt_swear(index == 2); rt_event_test_check_time(start, 0); rt_swear(index == 2); // Third event should be triggered rt_event.reset(events[2]); // Reset the third event start = rt_clock.seconds(); result = rt_event.wait_any_or_timeout(rt_countof(events), events, timeout_seconds); rt_swear(result == -1); rt_event_test_check_time(start, timeout_seconds); rt_swear(result == -1); // Timeout expected // Clean up rt_event.dispose(event); for (int32_t i = 0; i < rt_countof(events); i++) { rt_event.dispose(events[i]); } if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_event_test(void) { } #endif rt_event_if rt_event = { .create = rt_event_create, .create_manual = rt_event_create_manual, .set = rt_event_set, .reset = rt_event_reset, .wait = rt_event_wait, .wait_or_timeout = rt_event_wait_or_timeout, .wait_any = rt_event_wait_any, .wait_any_or_timeout = rt_event_wait_any_or_timeout, .dispose = rt_event_dispose, .test = rt_event_test }; // mutexes: rt_static_assertion(sizeof(CRITICAL_SECTION) == sizeof(rt_mutex_t)); static void rt_mutex_init(rt_mutex_t* m) { CRITICAL_SECTION* cs = (CRITICAL_SECTION*)m; rt_fatal_win32err(InitializeCriticalSectionAndSpinCount(cs, 4096)); } static void rt_mutex_lock(rt_mutex_t* m) { EnterCriticalSection((CRITICAL_SECTION*)m); } static void rt_mutex_unlock(rt_mutex_t* m) { LeaveCriticalSection((CRITICAL_SECTION*)m); } static void rt_mutex_dispose(rt_mutex_t* m) { DeleteCriticalSection((CRITICAL_SECTION*)m); } // test: // check if the elapsed time is within the expected range static void rt_mutex_test_check_time(fp64_t start, fp64_t expected) { fp64_t elapsed = rt_clock.seconds() - start; // Old Windows scheduler is prone to 2x16.6ms ~ 33ms delays rt_swear(elapsed >= expected - 0.04 && elapsed <= expected + 0.04, "expected: %f elapsed %f seconds", expected, elapsed); } static void rt_mutex_test_lock_unlock(void* arg) { rt_mutex_t* mutex = (rt_mutex_t*)arg; rt_mutex.lock(mutex); rt_thread.sleep_for(0.01); // Hold the mutex for 10ms rt_mutex.unlock(mutex); } static void rt_mutex_test(void) { rt_mutex_t mutex; rt_mutex.init(&mutex); fp64_t start = rt_clock.seconds(); rt_mutex.lock(&mutex); rt_mutex.unlock(&mutex); // Lock and unlock should be immediate rt_mutex_test_check_time(start, 0); enum { count = 5 }; rt_thread_t ts[count]; for (int32_t i = 0; i < rt_countof(ts); i++) { ts[i] = rt_thread.start(rt_mutex_test_lock_unlock, &mutex); } // Wait for all threads to finish for (int32_t i = 0; i < rt_countof(ts); i++) { rt_thread.join(ts[i], -1); } rt_mutex.dispose(&mutex); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } rt_mutex_if rt_mutex = { .init = rt_mutex_init, .lock = rt_mutex_lock, .unlock = rt_mutex_unlock, .dispose = rt_mutex_dispose, .test = rt_mutex_test }; // threads: static void* rt_thread_ntdll(void) { static HMODULE ntdll; if (ntdll == null) { ntdll = (void*)GetModuleHandleA("ntdll.dll"); } if (ntdll == null) { ntdll = rt_loader.open("ntdll.dll", 0); } rt_not_null(ntdll); return ntdll; } static fp64_t rt_thread_ns2ms(int64_t ns) { return (fp64_t)ns / (fp64_t)rt_clock.nsec_in_msec; } static void rt_thread_set_timer_resolution(uint64_t nanoseconds) { typedef int32_t (*query_timer_resolution_t)(ULONG* minimum_resolution, ULONG* maximum_resolution, ULONG* actual_resolution); typedef int32_t (*set_timer_resolution_t)(ULONG requested_resolution, BOOLEAN set, ULONG* actual_resolution); // ntdll.dll void* nt_dll = rt_thread_ntdll(); query_timer_resolution_t query_timer_resolution = (query_timer_resolution_t) rt_loader.sym(nt_dll, "NtQueryTimerResolution"); set_timer_resolution_t set_timer_resolution = (set_timer_resolution_t) rt_loader.sym(nt_dll, "NtSetTimerResolution"); unsigned long min100ns = 16 * 10 * 1000; unsigned long max100ns = 1 * 10 * 1000; unsigned long cur100ns = 0; rt_fatal_if(query_timer_resolution(&min100ns, &max100ns, &cur100ns) != 0); uint64_t max_ns = max100ns * 100uLL; // uint64_t min_ns = min100ns * 100uLL; // uint64_t cur_ns = cur100ns * 100uLL; // max resolution is lowest possible delay between timer events // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f" // " ms (milliseconds)", // rt_thread_ns2ms(min_ns), // rt_thread_ns2ms(max_ns), // rt_thread_ns2ms(cur_ns)); // } // note that maximum resolution is actually < minimum nanoseconds = rt_max(max_ns, nanoseconds); unsigned long ns = (unsigned long)((nanoseconds + 99) / 100); rt_fatal_if(set_timer_resolution(ns, true, &cur100ns) != 0); rt_fatal_if(query_timer_resolution(&min100ns, &max100ns, &cur100ns) != 0); // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // min_ns = min100ns * 100uLL; // max_ns = max100ns * 100uLL; // the smallest interval // cur_ns = cur100ns * 100uLL; // rt_println("timer resolution min: %.3f max: %.3f cur: %.3f ms (milliseconds)", // rt_thread_ns2ms(min_ns), // rt_thread_ns2ms(max_ns), // rt_thread_ns2ms(cur_ns)); // } } static void rt_thread_power_throttling_disable_for_process(void) { static bool disabled_for_the_process; if (!disabled_for_the_process) { PROCESS_POWER_THROTTLING_STATE pt = { 0 }; pt.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; pt.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED; pt.StateMask = 0; rt_fatal_win32err(SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt))); // PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION // does not work on Win10. There is no easy way to // distinguish Windows 11 from 10 (Microsoft great engineering) pt.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; pt.StateMask = 0; // ignore error on Windows 10: (void)SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt)); disabled_for_the_process = true; } } static void rt_thread_power_throttling_disable_for_thread(HANDLE thread) { THREAD_POWER_THROTTLING_STATE pt = { 0 }; pt.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION; pt.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED; pt.StateMask = 0; rt_fatal_win32err(SetThreadInformation(thread, ThreadPowerThrottling, &pt, sizeof(pt))); } static void rt_thread_disable_power_throttling(void) { rt_thread_power_throttling_disable_for_process(); rt_thread_power_throttling_disable_for_thread(GetCurrentThread()); } static const char* rt_thread_rel2str(int32_t rel) { switch (rel) { case RelationProcessorCore : return "ProcessorCore "; case RelationNumaNode : return "NumaNode "; case RelationCache : return "Cache "; case RelationProcessorPackage: return "ProcessorPackage"; case RelationGroup : return "Group "; case RelationProcessorDie : return "ProcessorDie "; case RelationNumaNodeEx : return "NumaNodeEx "; case RelationProcessorModule : return "ProcessorModule "; default: rt_assert(false, "fix me"); return "???"; } } static uint64_t rt_thread_next_physical_processor_affinity_mask(void) { static volatile int32_t initialized; static int32_t init; static int32_t next = 1; // next physical core to use static int32_t cores = 0; // number of physical processors (cores) static uint64_t any; static uint64_t affinity[64]; // mask for each physical processor bool set_to_true = rt_atomics.compare_exchange_int32(&init, false, true); if (set_to_true) { // Concept D: 6 cores, 12 logical processors: 27 lpi entries static SYSTEM_LOGICAL_PROCESSOR_INFORMATION lpi[64]; DWORD bytes = 0; GetLogicalProcessorInformation(null, &bytes); rt_assert(bytes % sizeof(lpi[0]) == 0); // number of lpi entries == 27 on 6 core / 12 logical processors system int32_t n = bytes / sizeof(lpi[0]); rt_assert(bytes <= sizeof(lpi), "increase lpi[%d]", n); rt_fatal_win32err(GetLogicalProcessorInformation(&lpi[0], &bytes)); for (int32_t i = 0; i < n; i++) { // if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { // rt_println("[%2d] affinity mask 0x%016llX relationship=%d %s", i, // lpi[i].ProcessorMask, lpi[i].Relationship, // rt_thread_rel2str(lpi[i].Relationship)); // } if (lpi[i].Relationship == RelationProcessorCore) { rt_assert(cores < rt_countof(affinity), "increase affinity[%d]", cores); if (cores < rt_countof(affinity)) { any |= lpi[i].ProcessorMask; affinity[cores] = lpi[i].ProcessorMask; cores++; } } } initialized = true; } else { while (initialized == 0) { rt_thread.sleep_for(1 / 1024.0); } rt_assert(any != 0); // should not ever happen if (any == 0) { any = (uint64_t)(-1LL); } } uint64_t mask = next < cores ? affinity[next] : any; rt_assert(mask != 0); // assume last physical core is least popular if (next < cores) { next++; } // not circular return mask; } static void rt_thread_realtime(void) { rt_fatal_win32err(SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)); rt_fatal_win32err(SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)); rt_fatal_win32err(SetThreadPriorityBoost(GetCurrentThread(), /* bDisablePriorityBoost = */ false)); // desired: 0.5ms = 500us (microsecond) = 50,000ns rt_thread_set_timer_resolution((uint64_t)rt_clock.nsec_in_usec * 500ULL); rt_fatal_win32err(SetThreadAffinityMask(GetCurrentThread(), rt_thread_next_physical_processor_affinity_mask())); rt_thread_disable_power_throttling(); } static void rt_thread_yield(void) { SwitchToThread(); } static rt_thread_t rt_thread_start(void (*func)(void*), void* p) { rt_thread_t t = (rt_thread_t)CreateThread(null, 0, (LPTHREAD_START_ROUTINE)(void*)func, p, 0, null); rt_not_null(t); return t; } static bool is_handle_valid(void* h) { DWORD flags = 0; return GetHandleInformation(h, &flags); } static errno_t rt_thread_join(rt_thread_t t, fp64_t timeout) { rt_not_null(t); rt_fatal_if(!is_handle_valid(t)); const uint32_t ms = timeout < 0 ? INFINITE : (uint32_t)(timeout * 1000.0 + 0.5); DWORD ix = WaitForSingleObject(t, (DWORD)ms); errno_t r = rt_wait_ix2e(ix); rt_assert(r != ERROR_REQUEST_ABORTED, "AFAIK thread can`t be ABANDONED"); if (r == 0) { rt_win32_close_handle(t); } else { rt_println("failed to join thread %p %s", t, rt_strerr(r)); } return r; } static void rt_thread_detach(rt_thread_t t) { rt_not_null(t); rt_fatal_if(!is_handle_valid(t)); rt_win32_close_handle(t); } static void rt_thread_name(const char* name) { uint16_t stack[128]; rt_fatal_if(rt_str.len(name) >= rt_countof(stack), "name too long: %s", name); rt_str.utf8to16(stack, rt_countof(stack), name, -1); HRESULT r = SetThreadDescription(GetCurrentThread(), stack); // notoriously returns 0x10000000 for no good reason whatsoever rt_fatal_if(!SUCCEEDED(r)); } static void rt_thread_sleep_for(fp64_t seconds) { rt_assert(seconds >= 0); if (seconds < 0) { seconds = 0; } int64_t ns100 = (int64_t)(seconds * 1.0e+7); // in 0.1 us aka 100ns typedef int32_t (__stdcall *nt_delay_execution_t)(BOOLEAN alertable, PLARGE_INTEGER DelayInterval); static nt_delay_execution_t NtDelayExecution; // delay in 100-ns units. negative value means delay relative to current. LARGE_INTEGER delay = {0}; // delay in 100-ns units. delay.QuadPart = -ns100; // negative value means delay relative to current. if (NtDelayExecution == null) { void* ntdll = rt_thread_ntdll(); NtDelayExecution = (nt_delay_execution_t) rt_loader.sym(ntdll, "NtDelayExecution"); rt_not_null(NtDelayExecution); } // If "alertable" is set, sleep_for() can break earlier // as a result of NtAlertThread call. NtDelayExecution(false, &delay); } static uint64_t rt_thread_id_of(rt_thread_t t) { return (uint64_t)GetThreadId((HANDLE)t); } static uint64_t rt_thread_id(void) { return (uint64_t)GetThreadId(GetCurrentThread()); } static rt_thread_t rt_thread_self(void) { // GetCurrentThread() returns pseudo-handle, not a real handle // if real handle is ever needed may do // rt_thread_t t = rt_thread.open(rt_thread.id()) and // rt_thread.close(t) instead. return (rt_thread_t)GetCurrentThread(); } static errno_t rt_thread_open(rt_thread_t *t, uint64_t id) { // GetCurrentThread() returns pseudo-handle, not a real handle. // if real handle is ever needed do rt_thread_id_of() instead // but don't forget to do rt_thread.close() after that. *t = (rt_thread_t)OpenThread(THREAD_ALL_ACCESS, false, (DWORD)id); return *t == null ? rt_core.err() : 0; } static void rt_thread_close(rt_thread_t t) { rt_not_null(t); rt_win32_close_handle((HANDLE)t); } #ifdef RT_TESTS // test: https://en.wikipedia.org/wiki/Dining_philosophers_problem typedef struct rt_thread_philosophers_s rt_thread_philosophers_t; typedef struct { rt_thread_philosophers_t* ps; rt_mutex_t fork; rt_mutex_t* left_fork; rt_mutex_t* right_fork; rt_thread_t thread; uint64_t id; } rt_thread_philosopher_t; typedef struct rt_thread_philosophers_s { rt_thread_philosopher_t philosopher[3]; rt_event_t fed_up[3]; uint32_t seed; volatile bool enough; } rt_thread_philosophers_t; #pragma push_macro("verbose") // --verbosity trace #define verbose(...) do { \ if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { \ rt_println(__VA_ARGS__); \ } \ } while (0) static void rt_thread_philosopher_think(rt_thread_philosopher_t* p) { verbose("philosopher %d is thinking.", p->id); // Random think time between .1 and .3 seconds fp64_t seconds = (rt_num.random32(&p->ps->seed) % 30 + 1) / 100.0; rt_thread.sleep_for(seconds); } static void rt_thread_philosopher_eat(rt_thread_philosopher_t* p) { verbose("philosopher %d is eating.", p->id); // Random eat time between .1 and .2 seconds fp64_t seconds = (rt_num.random32(&p->ps->seed) % 20 + 1) / 100.0; rt_thread.sleep_for(seconds); } // To avoid deadlocks in the Three Philosophers problem, we can implement // the Tanenbaum's solution, which ensures that one of the philosophers // (e.g., the last one) tries to pick up the right fork first, while the // others pick up the left fork first. This breaks the circular wait // condition and prevents deadlock. // If the philosopher is the last one (p->id == n - 1) they will try to pick // up the right fork first and then the left fork. All other philosophers will // pick up the left fork first and then the right fork, as before. This change // ensures that at least one philosopher will be able to eat, breaking the // circular wait condition and preventing deadlock. static void rt_thread_philosopher_routine(void* arg) { rt_thread_philosopher_t* p = (rt_thread_philosopher_t*)arg; enum { n = rt_countof(p->ps->philosopher) }; rt_thread.name("philosopher"); rt_thread.realtime(); while (!p->ps->enough) { rt_thread_philosopher_think(p); if (p->id == n - 1) { // Last philosopher picks up the right fork first rt_mutex.lock(p->right_fork); verbose("philosopher %d picked up right fork.", p->id); rt_mutex.lock(p->left_fork); verbose("philosopher %d picked up left fork.", p->id); } else { // Other philosophers pick up the left fork first rt_mutex.lock(p->left_fork); verbose("philosopher %d picked up left fork.", p->id); rt_mutex.lock(p->right_fork); verbose("philosopher %d picked up right fork.", p->id); } rt_thread_philosopher_eat(p); rt_mutex.unlock(p->right_fork); verbose("philosopher %d put down right fork.", p->id); rt_mutex.unlock(p->left_fork); verbose("philosopher %d put down left fork.", p->id); rt_event.set(p->ps->fed_up[p->id]); } } static void rt_thread_detached_sleep(void* rt_unused(p)) { rt_thread.sleep_for(1000.0); // seconds } static void rt_thread_detached_loop(void* rt_unused(p)) { uint64_t sum = 0; for (uint64_t i = 0; i < UINT64_MAX; i++) { sum += i; } // make sure that compiler won't get rid of the loop: rt_swear(sum == 0x8000000000000001ULL, "sum: %llu 0x%16llX", sum, sum); } static void rt_thread_test(void) { rt_thread_philosophers_t ps = { .seed = 1 }; enum { n = rt_countof(ps.philosopher) }; // Initialize mutexes for forks for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; p->id = i; p->ps = &ps; rt_mutex.init(&p->fork); p->left_fork = &p->fork; ps.fed_up[i] = rt_event.create(); } // Create and start philosopher threads for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_thread_philosopher_t* r = &ps.philosopher[(i + 1) % n]; p->right_fork = r->left_fork; p->thread = rt_thread.start(rt_thread_philosopher_routine, p); } // wait for all philosophers being fed up: for (int32_t i = 0; i < n; i++) { rt_event.wait(ps.fed_up[i]); } ps.enough = true; // join all philosopher threads for (int32_t i = 0; i < n; i++) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_thread.join(p->thread, -1); } // Dispose of mutexes and events for (int32_t i = 0; i < n; ++i) { rt_thread_philosopher_t* p = &ps.philosopher[i]; rt_mutex.dispose(&p->fork); rt_event.dispose(ps.fed_up[i]); } // detached threads are hacky and not that swell of an idea // but sometimes can be useful for 1. quick hacks 2. threads // that execute blocking calls that e.g. write logs to the // internet service that hangs. // test detached threads rt_thread_t detached_sleep = rt_thread.start(rt_thread_detached_sleep, null); rt_thread.detach(detached_sleep); rt_thread_t detached_loop = rt_thread.start(rt_thread_detached_loop, null); rt_thread.detach(detached_loop); // leave detached threads sleeping and running till ExitProcess(0) // that should NOT hang. if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("verbose") #else static void rt_thread_test(void) { } #endif rt_thread_if rt_thread = { .start = rt_thread_start, .join = rt_thread_join, .detach = rt_thread_detach, .name = rt_thread_name, .realtime = rt_thread_realtime, .yield = rt_thread_yield, .sleep_for = rt_thread_sleep_for, .id_of = rt_thread_id_of, .id = rt_thread_id, .self = rt_thread_self, .open = rt_thread_open, .close = rt_thread_close, .test = rt_thread_test }; ================================================ FILE: src/rt/rt_vigil.c ================================================ #include "rt/rt.h" #include #include static void rt_vigil_breakpoint_and_abort(void) { rt_debug.breakpoint(); // only if debugger is present rt_debug.raise(rt_debug.exception.noncontinuable); rt_core.abort(); } static int32_t rt_vigil_failed_assertion(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); rt_debug.println(file, line, func, "assertion failed: %s\n", condition); // avoid warnings: conditional expression always true and unreachable code const bool always_true = rt_core.abort != null; if (always_true) { rt_vigil_breakpoint_and_abort(); } return 0; } static int32_t rt_vigil_fatal_termination_va(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, va_list va) { const int32_t er = rt_core.err(); const int32_t en = errno; rt_debug.println_va(file, line, func, format, va); if (r != er && r != 0) { rt_debug.perror(file, line, func, r, ""); } // report last errors: if (er != 0) { rt_debug.perror(file, line, func, er, ""); } if (en != 0) { rt_debug.perrno(file, line, func, en, ""); } if (condition != null && condition[0] != 0) { rt_debug.println(file, line, func, "FATAL: %s\n", condition); } else { rt_debug.println(file, line, func, "FATAL\n"); } const bool always_true = rt_core.abort != null; if (always_true) { rt_vigil_breakpoint_and_abort(); } return 0; } static int32_t rt_vigil_fatal_termination(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { va_list va; va_start(va, format); rt_vigil_fatal_termination_va(file, line, func, condition, 0, format, va); va_end(va); return 0; } static int32_t rt_vigil_fatal_if_error(const char* file, int32_t line, const char* func, const char* condition, errno_t r, const char* format, ...) { if (r != 0) { va_list va; va_start(va, format); rt_vigil_fatal_termination_va(file, line, func, condition, r, format, va); va_end(va); } return 0; } #ifdef RT_TESTS static rt_vigil_if rt_vigil_test_saved; static int32_t rt_vigil_test_failed_assertion_count; #pragma push_macro("rt_vigil") // intimate knowledge of vigil.*() functions used in macro definitions #define rt_vigil rt_vigil_test_saved static int32_t rt_vigil_test_failed_assertion(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { rt_fatal_if_not(strcmp(file, __FILE__) == 0, "file: %s", file); rt_fatal_if_not(line > __LINE__, "line: %s", line); rt_assert(strcmp(func, "rt_vigil_test") == 0, "func: %s", func); rt_fatal_if(condition == null || condition[0] == 0); rt_fatal_if(format == null || format[0] == 0); rt_vigil_test_failed_assertion_count++; if (rt_debug.verbosity.level >= rt_debug.verbosity.trace) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); rt_debug.println(file, line, func, "assertion failed: %s (expected)\n", condition); } return 0; } static int32_t rt_vigil_test_fatal_calls_count; static int32_t rt_vigil_test_fatal_termination(const char* file, int32_t line, const char* func, const char* condition, const char* format, ...) { const int32_t er = rt_core.err(); const int32_t en = errno; rt_assert(er == 2, "rt_core.err: %d expected 2", er); rt_assert(en == 2, "errno: %d expected 2", en); rt_fatal_if_not(strcmp(file, __FILE__) == 0, "file: %s", file); rt_fatal_if_not(line > __LINE__, "line: %s", line); rt_assert(strcmp(func, "rt_vigil_test") == 0, "func: %s", func); rt_assert(strcmp(condition, "") == 0); // not yet used expected to be "" rt_assert(format != null && format[0] != 0); rt_vigil_test_fatal_calls_count++; if (rt_debug.verbosity.level > rt_debug.verbosity.trace) { va_list va; va_start(va, format); rt_debug.println_va(file, line, func, format, va); va_end(va); if (er != 0) { rt_debug.perror(file, line, func, er, ""); } if (en != 0) { rt_debug.perrno(file, line, func, en, ""); } if (condition != null && condition[0] != 0) { rt_debug.println(file, line, func, "FATAL: %s (testing)\n", condition); } else { rt_debug.println(file, line, func, "FATAL (testing)\n"); } } return 0; } #pragma pop_macro("rt_vigil") static void rt_vigil_test(void) { rt_vigil_test_saved = rt_vigil; int32_t en = errno; int32_t er = rt_core.err(); errno = 2; // ENOENT rt_core.set_err(2); // ERROR_FILE_NOT_FOUND rt_vigil.failed_assertion = rt_vigil_test_failed_assertion; rt_vigil.fatal_termination = rt_vigil_test_fatal_termination; int32_t count = rt_vigil_test_fatal_calls_count; rt_fatal("testing: %s call", "fatal()"); rt_assert(rt_vigil_test_fatal_calls_count == count + 1); count = rt_vigil_test_failed_assertion_count; rt_assert(false, "testing: rt_assert(%s)", "false"); #ifdef DEBUG // verify that rt_assert() is only compiled in DEBUG: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count + 1); #else // not RELEASE buid: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count); #endif count = rt_vigil_test_failed_assertion_count; rt_swear(false, "testing: swear(%s)", "false"); // swear() is triggered in both debug and release configurations: rt_fatal_if_not(rt_vigil_test_failed_assertion_count == count + 1); errno = en; rt_core.set_err(er); rt_vigil = rt_vigil_test_saved; if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #else static void rt_vigil_test(void) { } #endif rt_vigil_if rt_vigil = { .failed_assertion = rt_vigil_failed_assertion, .fatal_termination = rt_vigil_fatal_termination, .fatal_if_error = rt_vigil_fatal_if_error, .test = rt_vigil_test }; ================================================ FILE: src/rt/rt_win32.c ================================================ #include "rt/rt.h" #include "rt/rt_win32.h" void rt_win32_close_handle(void* h) { #pragma warning(suppress: 6001) // shut up overzealous IntelliSense rt_fatal_win32err(CloseHandle((HANDLE)h)); } // WAIT_ABANDONED only reported for mutexes not events // WAIT_FAILED means event was invalid handle or was disposed // by another thread while the calling thread was waiting for it. /* translate ix to error */ errno_t rt_wait_ix2e(uint32_t r) { const int32_t ix = (int32_t)r; return (errno_t)( (int32_t)WAIT_OBJECT_0 <= ix && ix <= WAIT_OBJECT_0 + 63 ? 0 : (ix == WAIT_ABANDONED ? ERROR_REQUEST_ABORTED : (ix == WAIT_TIMEOUT ? ERROR_TIMEOUT : (ix == WAIT_FAILED) ? rt_core.err() : ERROR_INVALID_HANDLE ) ) ); } ================================================ FILE: src/rt/rt_work.c ================================================ #include "rt/rt.h" static void rt_work_queue_no_duplicates(rt_work_t* w) { rt_work_t* e = w->queue->head; bool found = false; while (e != null && !found) { found = e == w; if (!found) { e = e->next; } } rt_swear(!found); } static void rt_work_queue_post(rt_work_t* w) { rt_assert(w->queue != null && w != null && w->when >= 0.0); rt_work_queue_t* q = w->queue; rt_atomics.spinlock_acquire(&q->lock); rt_work_queue_no_duplicates(w); // under lock // Enqueue in time sorted order least ->time first to save // time searching in fetching from queue which is more frequent. rt_work_t* p = null; rt_work_t* e = q->head; while (e != null && e->when <= w->when) { p = e; e = e->next; } w->next = e; bool head = p == null; if (head) { q->head = w; } else { p->next = w; } rt_atomics.spinlock_release(&q->lock); if (head && q->changed != null) { rt_event.set(q->changed); } } static void rt_work_queue_cancel(rt_work_t* w) { rt_swear(!w->canceled && w->queue != null && w->queue->head != null); rt_work_queue_t* q = w->queue; rt_atomics.spinlock_acquire(&q->lock); rt_work_t* p = null; rt_work_t* e = q->head; bool changed = false; // head changed while (e != null && !w->canceled) { if (e == w) { changed = p == null; if (changed) { q->head = e->next; } else { p->next = e->next; } e->next = null; e->canceled = true; } else { p = e; e = e->next; } } rt_atomics.spinlock_release(&q->lock); rt_swear(w->canceled); if (w->done != null) { rt_event.set(w->done); } if (changed && q->changed != null) { rt_event.set(q->changed); } } static void rt_work_queue_flush(rt_work_queue_t* q) { while (q->head != null) { rt_work_queue.cancel(q->head); } } static bool rt_work_queue_get(rt_work_queue_t* q, rt_work_t* *r) { rt_work_t* w = null; rt_atomics.spinlock_acquire(&q->lock); bool changed = q->head != null && q->head->when <= rt_clock.seconds(); if (changed) { w = q->head; q->head = w->next; w->next = null; } rt_atomics.spinlock_release(&q->lock); *r = w; if (changed && q->changed != null) { rt_event.set(q->changed); } return w != null; } static void rt_work_queue_call(rt_work_t* w) { if (w->work != null) { w->work(w); } if (w->done != null) { rt_event.set(w->done); } } static void rt_work_queue_dispatch(rt_work_queue_t* q) { rt_work_t* w = null; while (rt_work_queue.get(q, &w)) { rt_work_queue.call(w); } } rt_work_queue_if rt_work_queue = { .post = rt_work_queue_post, .get = rt_work_queue_get, .call = rt_work_queue_call, .dispatch = rt_work_queue_dispatch, .cancel = rt_work_queue_cancel, .flush = rt_work_queue_flush }; static void rt_worker_thread(void* p) { rt_thread.name("worker"); rt_worker_t* worker = (rt_worker_t*)p; rt_work_queue_t* q = &worker->queue; while (!worker->quit) { rt_work_queue.dispatch(q); fp64_t timeout = -1.0; // forever rt_atomics.spinlock_acquire(&q->lock); if (q->head != null) { timeout = rt_max(0, q->head->when - rt_clock.seconds()); } rt_atomics.spinlock_release(&q->lock); // if another item is inserted into head after unlocking // the `wake` event guaranteed to be signalled if (!worker->quit && timeout != 0) { rt_event.wait_or_timeout(worker->wake, timeout); } } rt_work_queue.dispatch(q); } static void rt_worker_start(rt_worker_t* worker) { rt_assert(worker->wake == null && !worker->quit); worker->wake = rt_event.create(); worker->queue = (rt_work_queue_t){ .head = null, .lock = 0, .changed = worker->wake }; worker->thread = rt_thread.start(rt_worker_thread, worker); } static errno_t rt_worker_join(rt_worker_t* worker, fp64_t to) { worker->quit = true; rt_event.set(worker->wake); errno_t r = rt_thread.join(worker->thread, to); if (r == 0) { rt_event.dispose(worker->wake); worker->wake = null; worker->thread = null; worker->quit = false; rt_swear(worker->queue.head == null); } return r; } static void rt_worker_post(rt_worker_t* worker, rt_work_t* w) { rt_assert(!worker->quit && worker->wake != null && worker->thread != null); w->queue = &worker->queue; rt_work_queue.post(w); } static void rt_worker_test(void); rt_worker_if rt_worker = { .start = rt_worker_start, .post = rt_worker_post, .join = rt_worker_join, .test = rt_worker_test }; #ifdef RT_TESTS // tests: // keep in mind that rt_println() may be blocking and is a subject // of "astronomical" wait state times in order of dozens of ms. static int32_t rt_test_called; static void rt_never_called(rt_work_t* rt_unused(w)) { rt_test_called++; } static void rt_work_queue_test_1(void) { rt_test_called = 0; // testing insertion time ordering of two events into queue const fp64_t now = rt_clock.seconds(); rt_work_queue_t q = {0}; rt_work_t c1 = { .queue = &q, .work = rt_never_called, .when = now + 1.0 }; rt_work_t c2 = { .queue = &q, .work = rt_never_called, .when = now + 0.5 }; rt_work_queue.post(&c1); rt_swear(q.head == &c1 && q.head->next == null); rt_work_queue.post(&c2); rt_swear(q.head == &c2 && q.head->next == &c1); rt_work_queue.flush(&q); // test that canceled events are not dispatched rt_swear(rt_test_called == 0 && c1.canceled && c2.canceled && q.head == null); c1.canceled = false; c2.canceled = false; // test the rt_work_queue.cancel() function rt_work_queue.post(&c1); rt_work_queue.post(&c2); rt_swear(q.head == &c2 && q.head->next == &c1); rt_work_queue.cancel(&c2); rt_swear(c2.canceled && q.head == &c1 && q.head->next == null); c2.canceled = false; rt_work_queue.post(&c2); rt_work_queue.cancel(&c1); rt_swear(c1.canceled && q.head == &c2 && q.head->next == null); rt_work_queue.flush(&q); rt_swear(rt_test_called == 0 && c1.canceled && c2.canceled && q.head == null); } // simple way of passing a single pointer to call_later static fp64_t rt_test_work_start; // makes timing debug traces easier to read static void rt_every_millisecond(rt_work_t* w) { int32_t* i = (int32_t*)w->data; fp64_t now = rt_clock.seconds(); if (rt_debug.verbosity.level > rt_debug.verbosity.info) { const fp64_t since_start = now - rt_test_work_start; const fp64_t dt = w->when - rt_test_work_start; rt_println("%d now: %.6f time: %.6f", *i, since_start, dt); } (*i)++; // read rt_clock.seconds() again because rt_println() above could block w->when = rt_clock.seconds() + 0.001; rt_work_queue.post(w); } static void rt_work_queue_test_2(void) { rt_thread.realtime(); rt_test_work_start = rt_clock.seconds(); rt_work_queue_t q = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t c = { .queue = &q, .work = rt_every_millisecond, .when = rt_test_work_start + 0.001, .data = &i }; rt_work_queue.post(&c); while (q.head != null && i < 8) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(&q); } rt_work_queue.flush(&q); rt_swear(q.head == null); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("called: %d times", i); } } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { // nameless union opens up base fields into rt_work_ex_t // it is not necessary at all union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void rt_every_other_millisecond(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; fp64_t now = rt_clock.seconds(); if (rt_debug.verbosity.level > rt_debug.verbosity.info) { const fp64_t since_start = now - rt_test_work_start; const fp64_t dt = w->when - rt_test_work_start; rt_println(".i: %d .extra: {.a: %d .b: %d} now: %.6f time: %.6f", ex->i, ex->s.a, ex->s.b, since_start, dt); } ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; // read rt_clock.seconds() again because rt_println() above could block w->when = rt_clock.seconds() + 0.002; rt_work_queue.post(w); } static void rt_work_queue_test_3(void) { rt_thread.realtime(); rt_static_assertion(offsetof(rt_work_ex_t, base) == 0); const fp64_t now = rt_clock.seconds(); rt_work_queue_t q = {0}; rt_work_ex_t ex = { .queue = &q, .work = rt_every_other_millisecond, .when = now + 0.002, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&ex.base); while (q.head != null && ex.i < 8) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(&q); } rt_work_queue.flush(&q); rt_swear(q.head == null); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("called: %d times", ex.i); } } static void rt_work_queue_test(void) { rt_work_queue_test_1(); rt_work_queue_test_2(); rt_work_queue_test_3(); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } static int32_t rt_test_do_work_called; static void rt_test_do_work(rt_work_t* rt_unused(w)) { rt_test_do_work_called++; } static void rt_worker_test(void) { // uncomment one of the following lines to see the output // rt_debug.verbosity.level = rt_debug.verbosity.info; // rt_debug.verbosity.level = rt_debug.verbosity.verbose; rt_work_queue_test(); // first test rt_work_queue rt_worker_t worker = { 0 }; rt_worker.start(&worker); rt_work_t asap = { .when = 0, // A.S.A.P. .done = rt_event.create(), .work = rt_test_do_work }; rt_work_t later = { .when = rt_clock.seconds() + 0.010, // 10ms .done = rt_event.create(), .work = rt_test_do_work }; rt_worker.post(&worker, &asap); rt_worker.post(&worker, &later); // because `asap` and `later` are local variables // code needs to wait for them to be processed inside // this function before they goes out of scope rt_event.wait(asap.done); // await(asap) rt_event.dispose(asap.done); // responsibility of the caller // wait for later: rt_event.wait(later.done); // await(later) rt_event.dispose(later.done); // responsibility of the caller // quit the worker thread: rt_fatal_if_error(rt_worker.join(&worker, -1.0)); // does worker respect .when dispatch time? rt_swear(rt_clock.seconds() >= later.when); } #else static void rt_work_queue_test(void) {} static void rt_worker_test(void) {} #endif ================================================ FILE: src/samples/edit.test.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" static const char* lorem_ipsum_words[] = { "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "quisque", "faucibus", "ex", "sapien", "vitae", "pellentesque", "sem", "placerat", "in", "id", "cursus", "mi", "pretium", "tellus", "duis", "convallis", "tempus", "leo", "eu", "aenean", "sed", "diam", "urna", "tempor", "pulvinar", "vivamus", "fringilla", "lacus", "nec", "metus", "bibendum", "egestas", "iaculis", "massa", "nisl", "malesuada", "lacinia", "integer", "nunc", "posuere", "ut", "hendrerit", "semper", "vel", "class", "aptent", "taciti", "sociosqu", "ad", "litora", "torquent", "per", "conubia", "nostra", "inceptos", "himenaeos", "orci", "varius", "natoque", "penatibus", "et", "magnis", "dis", "parturient", "montes", "nascetur", "ridiculus", "mus", "donec", "rhoncus", "eros", "lobortis", "nulla", "molestie", "mattis", "scelerisque", "maximus", "eget", "fermentum", "odio", "phasellus", "non", "purus", "est", "efficitur", "laoreet", "mauris", "pharetra", "vestibulum", "fusce", "dictum", "risus", "blandit", "quis", "suspendisse", "aliquet", "nisi", "sodales", "consequat", "magna", "ante", "condimentum", "neque", "at", "luctus", "nibh", "finibus", "facilisis", "dapibus", "etiam", "interdum", "tortor", "ligula", "congue", "sollicitudin", "erat", "viverra", "ac", "tincidunt", "nam", "porta", "elementum", "a", "enim", "euismod", "quam", "justo", "lectus", "commodo", "augue", "arcu", "dignissim", "velit", "aliquam", "imperdiet", "mollis", "nullam", "volutpat", "porttitor", "ullamcorper", "rutrum", "gravida", "cras", "eleifend", "turpis", "fames", "primis", "vulputate", "ornare", "sagittis", "vehicula", "praesent", "dui", "felis", "venenatis", "ultrices", "proin", "libero", "feugiat", "tristique", "accumsan", "maecenas", "potenti", "ultricies", "habitant", "morbi", "senectus", "netus", "suscipit", "auctor", "curabitur", "facilisi", "cubilia", "curae", "hac", "habitasse", "platea", "dictumst" }; #define lorem_ipsum_canonique \ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " \ "eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad " \ "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " \ "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " \ "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur " \ "sint occaecat cupidatat non proident, sunt in culpa qui officia " \ "deserunt mollit anim id est laborum." #define lorem_ipsum_chinese \ "\xE6\x88\x91\xE6\x98\xAF\xE6\x94\xBE\xE7\xBD\xAE\xE6\x96\x87\xE6\x9C\xAC\xE7\x9A\x84\xE4" \ "\xBD\x8D\xE7\xBD\xAE\xE3\x80\x82\xE8\xBF\x99\xE9\x87\x8C\xE6\x94\xBE\xE7\xBD\xAE\xE4\xBA" \ "\x86\xE5\x81\x87\xE6\x96\x87\xE5\x81\x87\xE5\xAD\x97\xE3\x80\x82\xE5\xB8\x8C\xE6\x9C\x9B" \ "\xE8\xBF\x99\xE4\xBA\x9B\xE6\x96\x87\xE5\xAD\x97\xE5\x8F\xAF\xE4\xBB\xA5\xE5\xA1\xAB\xE5" \ "\x85\x85\xE7\xA9\xBA\xE7\x99\xBD\xE3\x80\x82"; #define lorem_ipsum_japanese \ "\xE3\x81\x93\xE3\x82\x8C\xE3\x81\xAF\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3" \ "\x82\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x81\xA7\xE3\x81\x99\xE3\x80\x82\xE3\x81\x93\xE3\x81" \ "\x93\xE3\x81\xAB\xE6\x96\x87\xE7\xAB\xA0\xE3\x81\x8C\xE5\x85\xA5\xE3\x82\x8A\xE3\x81\xBE" \ "\xE3\x81\x99\xE3\x80\x82\xE8\xAA\xAD\xE3\x81\xBF\xE3\x82\x84\xE3\x81\x99\xE3\x81\x84\xE3" \ "\x82\x88\xE3\x81\x86\xE3\x81\xAB\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3\x82" \ "\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x82\x92\xE4\xBD\xBF\xE7\x94\xA8\xE3\x81\x97\xE3\x81\xA6" \ "\xE3\x81\x84\xE3\x81\xBE\xE3\x81\x99\xE3\x80\x82"; #define lorem_ipsum_korean \ "\xEC\x9D\xB4\xEA\xB2\x83\xEC\x9D\x80\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED\x85\x8D\xEC\x8A" \ "\xA4\xED\x8A\xB8\xEC\x9E\x85\xEB\x8B\x88\xEB\x8B\xA4\x2E\x20\xEC\x97\xAC\xEA\xB8\xB0\xEC" \ "\x97\x90\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEB\x93\x9C\xEC\x96\xB4\xEA\xB0\x80" \ "\xEB\x8A\x94\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEC\x9E\x88\xEB\x8B\xA4\x2E\x20" \ "\xEC\x9D\xBD\xEA\xB8\xB0\x20\xEC\x89\xBD\xEA\xB2\x8C\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED" \ "\x85\x8D\xEC\x8A\xA4\xED\x8A\xB8\xEB\xA5\xBC\x20\xEC\x82\xAC\xEC\x9A\xA9\xED\x95\xA9\xEB" \ "\x8B\x88\xEB\x8B\xA4\x2E"; #define lorem_ipsum_emoji \ "\xF0\x9F\x8D\x95\xF0\x9F\x9A\x80\xF0\x9F\xA6\x84\xF0\x9F\x92\xBB\xF0\x9F\x8E\x89\xF0\x9F" \ "\x8C\x88\xF0\x9F\x90\xB1\xF0\x9F\x93\x9A\xF0\x9F\x8E\xA8\xF0\x9F\x8D\x94\xF0\x9F\x8D\xA6" \ "\xF0\x9F\x8E\xB8\xF0\x9F\xA7\xA9\xF0\x9F\x8D\xBF\xF0\x9F\x93\xB7\xF0\x9F\x8E\xA4\xF0\x9F" \ "\x91\xBE\xF0\x9F\x8C\xAE\xF0\x9F\x8E\x88\xF0\x9F\x9A\xB2\xF0\x9F\x8D\xA9\xF0\x9F\x8E\xAE" \ "\xF0\x9F\x8D\x89\xF0\x9F\x8E\xAC\xF0\x9F\x90\xB6\xF0\x9F\x93\xB1\xF0\x9F\x8E\xB9\xF0\x9F" \ "\xA6\x96\xF0\x9F\x8C\x9F\xF0\x9F\x8D\xAD\xF0\x9F\x8E\xA4\xF0\x9F\x8F\x96\xF0\x9F\xA6\x8B" \ "\xF0\x9F\x8E\xB2\xF0\x9F\x8E\xAF\xF0\x9F\x8D\xA3\xF0\x9F\x9A\x81\xF0\x9F\x8E\xAD\xF0\x9F" \ "\x91\x9F\xF0\x9F\x9A\x82\xF0\x9F\x8D\xAA\xF0\x9F\x8E\xBB\xF0\x9F\x9B\xB8\xF0\x9F\x8C\xBD" \ "\xF0\x9F\x93\x80\xF0\x9F\x9A\x80\xF0\x9F\xA7\x81\xF0\x9F\x93\xAF\xF0\x9F\x8C\xAF\xF0\x9F" \ "\x90\xA5\xF0\x9F\xA7\x83\xF0\x9F\x8D\xBB\xF0\x9F\x8E\xAE"; static const char* content = rt_glyph_lady_beetle "\n" #if 1 "Good bye Universe...\n" "Hello World!\n" "\n" "Ctrl+Shift and F5 starts/stops FUZZING test.\n" "\n" "FUZZING use rapid mouse clicks thus UI Fuzz button is hard to press use keyboard shortcut F5 to stop.\n" "\n" "0 10 20 30 40 50 60 70 80 90\n" "01234567890123456789012345678901234567890abcdefghi01234567890123456789012345678901234567890123456789\n" "0 10 20 30 40 50 60 70 80 90\n" "01234567890123456789012345678901234567890abcdefghi01234567890123456789012345678901234567890123456789\n" "\n" "0" rt_glyph_chinese_jin4 rt_glyph_chinese_gong "3456789\n" "\n" rt_glyph_teddy_bear "\n" rt_glyph_teddy_bear rt_glyph_ice_cube rt_glyph_teddy_bear rt_glyph_ice_cube rt_glyph_teddy_bear rt_glyph_ice_cube "\n" rt_glyph_teddy_bear rt_glyph_ice_cube rt_glyph_teddy_bear " - " rt_glyph_ice_cube rt_glyph_teddy_bear rt_glyph_ice_cube "\n" "\n" lorem_ipsum_canonique "\n" lorem_ipsum_canonique; #endif // 566K angular2.min.js // https://get.cdnpkg.com/angular.js/2.0.0-beta.17/angular2.min.js // https://web.archive.org/web/20230104221014/https://get.cdnpkg.com/angular.js/2.0.0-beta.17/angular2.min.js // https://en.wikipedia.org/wiki/Lorem_ipsum typedef struct { char* text; int32_t count; // at least 1KB uint32_t seed; // seed for random generator int32_t min_paragraphs; // at least 1 int32_t max_paragraphs; int32_t min_sentences; // at least 1 int32_t max_sentences; int32_t min_words; // at least 2 int32_t max_words; const char* append; // append after each paragraph (e.g. extra "\n") } ui_edit_lorem_ipsum_generator_params_t; static void ui_edit_lorem_ipsum_generator(ui_edit_lorem_ipsum_generator_params_t p) { rt_fatal_if(p.count < 1024); // at least 1KB expected rt_fatal_if_not(0 < p.min_paragraphs && p.min_paragraphs <= p.max_paragraphs); rt_fatal_if_not(0 < p.min_sentences && p.min_sentences <= p.max_sentences); rt_fatal_if_not(2 < p.min_words && p.min_words <= p.max_words); char* s = p.text; char* end = p.text + p.count - 64; uint32_t paragraphs = p.min_paragraphs + (p.min_paragraphs == p.max_paragraphs ? 0 : rt_num.random32(&p.seed) % (p.max_paragraphs - p.min_paragraphs + 1)); while (paragraphs > 0 && s < end) { uint32_t sentences_in_paragraph = p.min_sentences + (p.min_sentences == p.max_sentences ? 0 : rt_num.random32(&p.seed) % (p.max_sentences - p.min_sentences + 1)); while (sentences_in_paragraph > 0 && s < end) { const uint32_t words_in_sentence = p.min_words + (p.min_words == p.max_words ? 0 : rt_num.random32(&p.seed) % (p.max_words - p.min_words + 1)); for (uint32_t i = 0; i < words_in_sentence && s < end; i++) { const int32_t ix = rt_num.random32(&p.seed) % rt_countof(lorem_ipsum_words); const char* word = lorem_ipsum_words[ix]; memcpy(s, word, strlen(word)); if (i == 0) { *s = (char)toupper(*s); } s += strlen(word); if (i < words_in_sentence - 1 && s < end) { const char* delimiter = "\x20"; int32_t punctuation = rt_num.random32(&p.seed) % 128; switch (punctuation) { case 0: case 1: case 2: delimiter = ", "; break; case 3: case 4: delimiter = "; "; break; case 6: delimiter = ": "; break; case 7: delimiter = " - "; break; default: break; } memcpy(s, delimiter, strlen(delimiter)); s += strlen(delimiter); } } if (sentences_in_paragraph > 1 && s < end) { memcpy(s, ".\x20", 2); s += 2; } else { *s++ = '.'; } sentences_in_paragraph--; } if (paragraphs > 1 && s < end) { *s++ = '\n'; } if (p.append != null && p.append[0] != 0) { memcpy(s, p.append, strlen(p.append)); s += strlen(p.append); } paragraphs--; } *s = 0; // rt_println("%s\n", p.text); } void ui_edit_init_with_lorem_ipsum(ui_edit_text_t* t) { static char text[64 * 1024]; ui_edit_lorem_ipsum_generator_params_t p = { .text = text, .count = rt_countof(text), .min_paragraphs = 4, .max_paragraphs = 15, .min_sentences = 4, .max_sentences = 20, .min_words = 8, .max_words = 16, .append = "\n" }; #ifdef DEBUG p.seed = 1; // repeatable sequence of pseudo random numbers #else p.seed = (int32_t)rt_clock.nanoseconds() | 0x1; // must be odd #endif ui_edit_lorem_ipsum_generator(p); rt_swear(ui_edit_text.replace_utf8(t, null, content, -1, null)); #if 1 ui_edit_range_t end = ui_edit_text.end_range(t); rt_swear(ui_edit_text.replace_utf8(t, &end, "\n\n", -1, null)); end = ui_edit_text.end_range(t); rt_swear(ui_edit_text.replace_utf8(t, &end, p.text, -1, null)); // test bad UTF8 static const char* th_bad_utf8 = "\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB9\x80\xE0\xB8\xA7\xE0\xB8\x93\xE0\xB8\x8A\xE0\xB8\xB8\xE0\xB8\x94\xE0\xB8\xA5\xE0\xB8\xB0\xE0\xB8\xA5\xE0\xB8\xB0\xE0\xB8\xAA\xE0\xB8\xB2\xE0\xB8\x87\xE0\xE0\xB9\x80\xE0\xB8\xA3\xE0\xB8\x87\xE0\xB9\x84\xE0\xB8\xA5\xE0\xB8\xA1\xE0\xB8\x95\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\xA1\xE0\xB8\xAD\xE0\xB9\x88\xE0\xB8\xB2\xE0\xB8\x94\xE0\xB8\xAD\xE0\xB8\x94\xE0\xB8\xAA\xE0\xB9\x80\xE0\xB8\xA3\xE0\xB8\xA2\xE0\xB8\xA2\xE0\xB8\xA7\xE0\xB8\xA5\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x81\xE0\xB8\xB4\xE0\xB8\x9B\xE0\xB8\x81\xE0\xB8\xA7\xE0\xB8\xB1\xE0\xB8\x87\x2E"; end = ui_edit_text.end_range(t); // rt_println("%s", th_bad_utf8); bool expected_false = ui_edit_text.replace_utf8(t, &end, th_bad_utf8, -1, null); rt_swear(expected_false == false); static const char* en_sentence_utf8 = "\x54\x68\x65\x20\x71\x75\x69\x63\x6b\x20\x62\x72\x6f\x77\x6e\x20\x66\x6f\x78\x20\x6a\x75\x6d\x70\x73\x20\x6f\x76\x65\x72\x20\x74\x68\x65\x20\x6c\x61\x7a\x79\x20\x64\x6f\x67\x2e"; static const char* es_sentence_utf8 = "\x45\x6c\x20\x76\x65\x6c\x6f\x7a\x20\x6d\x75\x72\x63\x69\x65\xcc\x81\x6c\x61\x67\x6f\x20\x68\x69\x6e\x64\x75\xcc\x81\x20\x63\x6f\x6d\x69\xcc\x81\x61\x20\x66\x65\x6c\x69\x7a\x20\x63\x61\x72\x64\x69\x6c\x6c\x6f\x20\x79\x20\x6b\x69\x77\x69\x2e"; static const char* fr_sentence_utf8 = "\x50\x6f\x72\x74\x65\x7a\x20\x63\x65\x20\x76\x69\x65\x75\x78\x20\x77\x68\x69\x73\x6b\x79\x20\x61\x75\x20\x6a\x75\x67\x65\x20\x62\x6c\x6f\x6e\x64\x20\x71\x75\x69\x20\x66\x75\x6d\x65\x2e"; static const char* de_sentence_utf8 = "\x56\x69\x63\x74\x6f\x72\x20\x6a\x61\x67\x74\x20\x7a\x77\xc3\xb6\x6c\x66\x20\x42\x6f\x78\x6b\xc3\xa4\x6d\x70\x66\x65\x72\x20\x71\x75\x65\x72\x20\xc3\xbc\x62\x65\x72\x20\x64\x65\x6e\x20\x67\x72\x6f\xc3\x9f\x65\x6e\x20\x53\x79\x6c\x74\x65\x72\x20\x44\x65\x69\x63\x68\x2e"; static const char* pl_sentence_utf8 = "\x50\x63\x68\x6e\xc4\x85\xc4\x87\x20\x77\x20\x74\xc4\x99\x20\xc5\x82\xc3\xb3\xc4\x87\x20\x6a\x65\xc5\xbc\x61\x20\x6c\x75\x62\x20\x6f\xc5\x9b\x6d\x20\x73\x6b\x72\x7a\x79\xc5\x84\x20\x66\x69\x67\x2e"; static const char* cs_sentence_utf8 = "\x50\xc5\x99\xc3\xad\x6c\xc3\xad\xc5\xa1\x20\xc5\xbe\x6c\x75\xc5\xa5\x6f\x75\xc4\x8d\x6b\x79\x20\x6b\xc5\xaf\xc5\x88\x20\xc3\xba\x70\xc4\x9bl\x20\xc4\x8f\xc3\xa1\x62\x65\x6c\x73\x6b\xc3\xa9\x20\xc3\xb3\x64\x79\x2e"; static const char* hu_sentence_utf8 = "\xc3\x81\x72\x76\xc3\xad\x7a\x74\xc5\xb1\x72\xc5\x91\x20\x74\xc3\xbc\x6b\xc3\xb6\x72\x66\xc3\xba\x72\xc3\xb3\x67\xc3\xa9\x70\x2e"; static const char* ru_sentence_utf8 = "\xd0\xa1\xd1\x8a\xd0\xb5\xd1\x88\xd1\x8c\x20\xd0\xb6\xd0\xb5\x20\xd0\xb5\xd1\x89\xd1\x91\x20\xd1\x8d\xd1\x82\xd0\xb8\xd1\x85\x20\xd0\xbc\xd1\x8f\xd0\xb3\xd0\xba\xd0\xb8\xd1\x85\x20\xd1\x84\xd1\x80\xd0\xb0\xd0\xbd\xd1\x86\xd1\x83\xd0\xb7\xd1\x81\xd0\xba\xd0\xb8\xd1\x85\x20\xd0\xb1\xd1\x83\xd0\xbb\xd0\xbe\xd0\xba\x20\xd0\xb4\xd0\xb0\x20\xd0\xb2\xd1\x8b\xd0\xbf\xd0\xb5\xd0\xb9\x20\xd1\x87\xd0\xb0\xd1\x8e\x2e"; static const char* pt_sentence_utf8 = "\x4f\x20\x76\x65\x6c\x6f\x7a\x20\x63\x61\x63\x68\x6f\x72\x72\x6f\x20\x6d\x61\x72\x72\x6f\x6d\x20\x7a\x61\x70\x61\x20\x72\x61\x70\x69\x64\x61\x6d\x65\x6e\x74\x65\x20\x75\x6d\x20\x6b\x69\x77\x69\x2e"; static const char* it_sentence_utf8 = "\x51\x75\x65\x6c\x20\x66\x6f\x78\x20\x62\x72\x75\x6e\x6f\x20\x73\x61\x6c\x74\x61\x20\x69\x6c\x20\x63\x61\x6e\x65\x20\x70\x69\x67\x72\x6f\x20\x6c\x61\x7a\x7a\x6f\x2e"; static const char* nl_sentence_utf8 = "\x44\x65\x20\x76\x6c\x75\x67\x20\x6a\x65\x20\x7a\x65\x76\x65\x6e\x20\x71\x75\x69\x73\x74\x20\x64\x65\x20\x73\x63\x68\x61\x74\x20\x62\x72\x75\x69\x6e\x20\x6c\x61\x7a\x65\x72\x2e"; static const char* el_sentence_utf8 = "\xce\x9e\xce\xb5\xcf\x83\xce\xba\xce\xb5\xcf\x80\xce\xac\xce\xb6\xcf\x89\x20\xcf\x84\xce\xb7\xce\xbd\x20\xcf\x88\xcf\x85\xcf\x87\xce\xbf\xcf\x86\xce\xb8\xcf\x8c\xcf\x81\xce\xb1\x20\xce\xb2\xce\xb4\xce\xb5\xce\xbb\xcf\x85\xce\xb3\xce\xbc\xce\xaf\xce\xb1\x2e"; static const char* tr_sentence_utf8 = "\xc3\x87\xc4\xb1\xc4\x9f\xc4\xb1\x6c\xc3\xb6\xc4\x9f\xc4\xb1\x6e\x6d\xc3\xa7\xc3\xa7\xc3\xa7"; static const char* uk_sentence_utf8 = "\xd0\xa4\xd0\xb0\xd0\xbd\xd0\xba\xd1\x83\xd0\xb2\xd0\xb0\xd0\xbb\xd0\xb0\x20\xd0\xbd\xd0\xb0\x20\xd0\xb1\xd0\xb5\xd1\x80\xd0\xb5\xd0\xb7\xd1\x96\x20\xd0\xb2\xd0\xb5\xd0\xbb\xd0\xb8\xd0\xba\xd1\x83\xd1\x8e\x20\xd0\xbf\xd0\xbe\xd1\x80\xd1\x86\xd1\x96\xd1\x8e\x20\xd0\xbc\xd0\xb0\xd0\xbb\xd0\xb8\xd0\xbd\xd0\xbe\xd0\xb2\xd0\xbe\xd0\xb3\xd0\xbe\x20\xd0\xb2\xd0\xb0\xd1\x80\xd0\xb5\xd0\xbd\xd0\xbd\xd1\x8f\x2e"; static const char* bg_sentence_utf8 = "\xd0\x9b\xd1\x8e\xd0\xb1\xd1\x8f\x20\xd1\x81\xd0\xb2\xd0\xbe\xd0\xb9\x20\xd0\xbc\xd0\xb5\xd0\xbb\xd1\x8a\xd0\xba\x20\xd1\x86\xd0\xb2\xd1\x8f\xd1\x82\x2c\x20\xd0\xb6\xd0\xb0\xd0\xba\x20\xd0\xb4\xd1\x8a\xd0\xb2\xd1\x87\xd0\xb5\x20\xd1\x88\xd1\x83\xd0\xbc\xd0\xb5\xd0\xbd\x20\xd1\x85\xd0\xb2\xd1\x8a\xd1\x80\xd1\x87\xd0\xb0\xd1\x89\x20\xd0\xbf\xd1\x83\xd0\xb4\xd0\xb5\xd0\xbb\x2e"; static const char* sr_sentence_utf8 = "\xd0\x8f\xd0\xb5\xd0\xbf\x20\xd1\x98\xd0\xb5\x20\xd1\x88\xd1\x83\xd0\xbf\xd0\xb0\xd1\x99\x2c\x20\xd1\x84\xd0\xbe\xd0\xbb\xd0\xb8\xd1\x80\xd0\xb0\xd0\xbd\xd1\x82\xd1\x81\xd0\xba\xd0\xb0\x20\xd0\xb6\xd0\xb5\xd0\xbd\xd0\xb0\x20\xd1\x83\xd0\xb2\xd0\xb5\xd0\xba\x20\xd0\xbc\xd0\xb0\xd1\x81\xd0\xba\xd0\xb8\xd1\x80\xd0\xb0\x20\xd1\x99\xd1\x83\xd0\xbf\xd0\xba\xd0\xb5\x20\xd0\xb4\xd0\xb5\xd1\x87\xd0\xba\xd0\xb5\x2e"; static const char* sq_sentence_utf8 = "\x5A\x68\x76\x69\x6C\x6F\x6A\x61\x20\xC3\x87\x64\x6F\x6B\x6F\x72\x20\x70\xC3\xAB\x72\x62\x65\x6C\x69\x6E\xC3\xAB\x20\x6E\xC3\xAB\x6E\x20\x64\x69\x6B\x75\x72\x20\x6E\x6F\x70\x61\x6C\x20\x74\x65\x20\x66\x6A\x61\x6C\xC3\xAB\x2E"; static const char* sl_sentence_utf8 = "\x42\x6C\x61\x67\x6F\x76\x6F\x6C\x6A\x65\x6E\x20\xC5\xBE\x69\x6E\x6A\x61\x6A\x20\x70\x72\x61\x7A\x6E\x69\x20\xC5\xA1\x63\x72\x61\x6C\x20\x70\x6F\x64\x20\x76\x69\x73\x6F\x6B\x20\xC5\xBE\x65\x6C\x76\x65\x6C\x20\xC4\x8D\x75\x64\x6F\x76\x2E"; static const char* lt_sentence_utf8 = "\xC4\x84\xC5\xBE\x75\x73\x69\x6E\xC4\x97\x20\xC5\xA1\x75\x6E\x79\x73\x74\x61\x20\x70\x6F\x20\x75\xC5\xB3\x73\x69\x75\x73\x20\xC5\xBE\x61\x6C\x6D\x61\x20\xC5\xBE\x69\x65\x6D\x6F\x6E\x69\x73\x20\x6D\x61\x6C\x65\x2E"; static const char* lv_sentence_utf8 = "\xC4\x80\x72\x74\x61\x75\x72\x20\x6E\x65\x73\x65\x65\x64\x7A\xC4\xAB\x76\x73\x20\x6A\x61\x75\x20\xC5\xA1\x6B\x6F\x6C\x75\x20\xC5\xA1\x63\x65\x6E\xC5\xA1\x20\xC4\x8D\x69\x67\x61\x6E\x20\xC4\xBC\xC4\x81\x70\xC4\x81\x20\xC5\xA1\x6B\x61\x72\x62\x69\x6E\x73\x2E"; static const char* et_sentence_utf8 = "\xC3\x9C\x68\x65\x74\x6F\x72\x75\x6E\x65\x20\x74\x6F\x68\x74\x75\x6D\x20\x6B\x61\x73\x6B\x75\x73\x20\xC3\xB5\x70\x65\x64\x20\x6B\xC3\xB5\x72\x67\x75\x6D\x20\xC3\xBC\x72\x69\x74\x75\x73\x20\xC3\xB5\x75\x20\x6A\xC3\xA4\xC3\xA4\x6E\x2E"; static const char* ga_sentence_utf8 = "\x42\x68\x69\x20\x66\xC3\xA1\x6C\x74\x61\x20\x6D\x68\xC3\xA1\x69\x72\xC3\xAD\x2C\x20\x63\x68\x75\x69\x20\x66\xC3\xA9\x6E\x65\x20\x6D\x68\xC3\xA9\x69\x72\x20\xC3\xA1\x20\x63\x68\x6F\x69\x73\x20\xC3\xA9\x69\x6E\x2E"; static const char* cy_sentence_utf8 = "\x59\x20\x66\x66\x6F\x72\x6B\x20\x67\x72\x65\x6E\x66\x6F\x72\x20\x20\x62\x6C\x61\x77\x64\x64\x20\x6C\x6C\x65\x20\x77\x61\x69\x74\x68\xC3\xAF\x73\x20\xC3\xA2\x73\x2E"; static const char* is_sentence_utf8 = "\xC3\x86\x72\x69\x20\x6C\x65\x74\x6A\x61\x72\xC3\xAD\x20\x6E\xC3\xBD\x20\xC3\xB3\x76\x61\x72\x2E\x20\xC3\x9E\xC3\xA9\xC3\xB0\x20\x65\x72\x20\x61\xC3\xB0\x20\x76\x69\x6C\x6A\x61\x72\xC3\xAD\x20\x6D\xC3\xAD\x6E\x2E"; static const char* mt_sentence_utf8 = "\xC4\xA0\x75\x73\x20\x6A\x6F\x62\x62\x61\x20\x6C\x69\x20\x6C\x61\x2E"; static const char* he_sentence_utf8 = "\xD7\x90\xD7\x91\xD7\x92\xD7\x93\xD7\x94\xD7\x95\xD7\x96\xD7\x97\xD7\x98\xD7\x99\xD7\x9A\xD7\x9B\xD7\x9C\xD7\x9D\xD7\x9E\xD7\x9F\xD7\xA1\xD7\xA2\xD7\xA4\xD7\xA6\xD7\xA7\xD7\xA8\xD7\xA9\xD7\xAA"; static const char* ar_sentence_utf8 = "\xD8\xB5\xD9\x90\xD9\x81\xD9\x92\x20\xD8\xAE\xD9\x90\xD9\x84\xD9\x92\xD8\xB9\xD9\x8E\xD8\xAA\xD9\x8E\xD9\x83\xD9\x8E\x20\xD8\xA7\xD9\x84\xD9\x8A\xD9\x8E\xD9\x88\xD9\x92\xD9\x85\xD9\x8E\x20\xD8\xBA\xD9\x8E\xD8\xB7\xD9\x91\xD9\x8E\xD8\xB3\xD9\x8E\x20\xD8\xAB\xD9\x8E\xD9\x88\xD8\xA8\xD9\x8E\xD9\x83\xD9\x8E\x20\xD9\x81\xD9\x90\xD9\x8A\xD9\x92\x20\xD8\xAD\xD9\x8E\xD9\x84\xD9\x8A\xD8\xA8\xD9\x8D\x20\xD8\xAC\xD9\x8E\xD8\xA7\xD9\x85\xD9\x90\xD8\xAF\xD9\x90\x2E"; static const char* hi_sentence_utf8 = "\xE0\xA4\x97\xE0\xA4\xA3\xE0\xA5\x87\xE0\xA4\xB6\x20\xE0\xA4\xAA\xE0\xA5\x82\xE0\xA4\x9C\xE0\xA4\xA8\x20\xE0\xA4\x95\xE0\xA4\xB0\xE0\xA5\x8B\x20\xE0\xA4\x94\xE0\xA4\xB0\x20\xE0\xA4\xAA\xE0\xA5\x8D\xE0\xA4\xB0\xE0\xA4\xB8\xE0\xA4\xBE\xE0\xA4\xA6\x20\xE0\xA4\x9A\xE0\xA4\xA2\xE0\xA4\xBC\xE0\xA4\xBE\xE0\xA4\x93\xE0\xA5\xA4"; static const char* bn_sentence_utf8 = "\xE0\xA6\x8F\xE0\xA6\x95\xE0\xA6\xAF\xE0\xA7\x81\xE0\xA6\x97\xE0\xA7\x87\x20\xE0\xA6\xAD\xE0\xA6\xBE\xE0\xA6\xB0\xE0\xA6\xA4\xE0\xA6\xAC\xE0\xA6\xB0\xE0\xA7\x8D\xE0\xA6\xB7\xE0\xA7\x87\xE0\xA6\xB0\x20\xE0\xA6\xAE\xE0\xA6\xBE\xE0\xA6\xA8\xE0\xA7\x81\xE0\xA6\xB7\x20\xE0\xA6\xB8\xE0\xA6\xBE\xE0\xA6\xB0\xE0\xA6\xBE\x20\xE0\xA6\x9C\xE0\xA6\x97\xE0\xA6\xA4\xE0\xA7\x87\x20\xE0\xA6\xB6\xE0\xA7\x8D\xE0\xA6\xB0\xE0\xA7\x87\xE0\xA6\xB7\xE0\xA7\x8D\xE0\xA6\xA0\x20\xE0\xA6\xB8\xE0\xA7\x8D\xE0\xA6\xA5\xE0\xA6\xBE\xE0\xA6\xA8\x20\xE0\xA6\x85\xE0\xA6\xA7\xE0\xA6\xBF\xE0\xA6\x95\xE0\xA6\xBE\xE0\xA6\xB0\x20\xE0\xA6\x95\xE0\xA6\xB0\xE0\xA6\xBF\xE0\xA6\xAC\xE0\xA7\x87\xE0\xA5\xA4"; static const char* ta_sentence_utf8 = "\xe0\xae\x9a\xe0\xae\xbf\xe0\xae\xb5\xe0\xae\xaa\xe0\xaf\x8d\xe0\xae\xaa\xe0\xaf\x81\x20\xe0\xae\xa8\xe0\xae\xb0\xe0\xae\xbf\x20\xe0\xae\xae\xe0\xae\xa8\xe0\xaf\x8d\xe0\xae\xa4\x20\xe0\xae\xa8\xe0\xae\xbe\xe0\xae\xaf\xe0\xaf\x88\x20\xe0\xae\xa4\xe0\xae\xbe\xe0\xae\xa3\xe0\xaf\x8d\xe0\xae\x9f\xe0\xae\xbf\xe0\xae\xaf\xe0\xae\xa4\xe0\xaf\x81\x2e"; static const char* te_sentence_utf8 = "\xE0\xB0\x8E\xE0\xB0\x97\xE0\xB0\xB8\xE0\xB0\xBF\xE0\xB0\xA8\x20\xE0\xB0\x95\xE0\xB0\x82\xE0\xB0\xA6\xE0\xB0\xAE\xE0\xB1\x81\xE0\xB0\xB2\xE0\xB1\x81\x20\xE0\xB0\xAF\xE0\xB0\xB5\xE0\xB1\x8D\xE0\xB0\xB5\xE0\xB0\xA8\xE0\xB0\xBE\xE0\xB0\xA8\xE0\xB1\x8D\xE0\xB0\xA8\xE0\xB0\xBF\x20\xE0\xB0\x9A\xE0\xB1\x86\xE0\xB0\xB0\xE0\xB0\xBF\xE0\xB0\xAA\xE0\xB1\x87\x20\xE0\xB0\x9A\xE0\xB0\x82\xE0\xB0\xA6\xE0\xB0\xBE\xE0\xB0\xB2\x20\xE0\xB0\xA6\xE0\xB0\xBF\xE0\xB0\xA8\xE0\xB0\x95\xE0\xB0\xB0\xE0\xB0\x82\x2E"; static const char* kn_sentence_utf8 = "\xE0\xB2\x85\xE0\xB2\xA8\xE0\xB2\x95\xE0\xB3\x8D\xE0\xB2\x95\xE0\xB2\xA8\x20\xE0\xB2\x97\xE0\xB2\xB4\xE0\xB3\x81\xE0\xB2\xB8\xE0\xB2\xBF\x20\xE0\xB2\x8E\xE0\xB2\xA8\xE0\xB3\x8D\xE0\xB2\xA8\xE0\xB2\xA6\xE0\xB2\xBF\xE0\xB2\xA8\xE0\xB2\x95\xE0\xB2\x82\x20\xE0\xB2\xA6\xE0\xB2\xBF\xE0\xB2\x82\xE0\xB2\xA1\xE0\xB3\x81\xE0\xB2\x95\xE0\xB3\x86\xE0\xB2\xA6\x20\xE0\xB2\x9A\xE0\xB2\x82\xE0\xB2\xA6\xE0\xB2\x95\xE0\xB3\x86\xE0\xB2\x9F\xE0\xB2\xA1\xE0\xB3\x81\xE0\xB2\xB5\xE0\xB2\xBE\xE0\xB2\xB0\xE0\xB2\x93\x2E"; static const char* ml_sentence_utf8 = "\xE0\xB4\x8F\xE0\xB4\x95\xE0\xB4\xB4\xE0\xB4\x82\x20\xE0\xB4\xA4\xE0\xB4\xB0\xE0\xB4\x82\x20\xE0\xB4\xAE\xE0\xB4\xA3\xE0\xB4\xB1\xE0\xB4\x96\xE0\xB4\xB7\xE0\xB4\x82\x20\xE0\xB4\x89\xE0\xB4\xA3\xE0\xB5\x8D\xE0\xB4\xAE\xE0\xB5\x82\x20\xE0\xB4\xA8\xE0\xB4\xA4\xE0\xB4\x95\xE0\xB5\x8D\xE0\xB4\x95\xE0\xB5\x8B\x20\xE0\xB4\x87\xE0\xB4\xA4\xE0\xB4\xBF\xE0\xB4\xB0\xE0\xB5\x81\xE0\xB4\xA8\xE0\xB5\x8D\xE0\xB4\xA8\xE0\xB5\x8B\x20\xE0\xB4\xA4\xE0\xB4\xA3\xE0\xB5\x8D\xE0\xB4\xA8\xE0\xB5\x81\x2E"; static const char* si_sentence_utf8 = "\xE0\xB6\xB8\xE0\xB7\x9A\x20\xE0\xB6\xB6\xE0\xB7\x8F\xE0\xB6\xB1\xE0\xB6\xA7\xE0\xB7\x8A\xE0\xB6\xA7\x20\xE0\xB6\xB8\xE0\xB6\x9A\xE0\xB7\x8A\xE0\xB6\xA7\xE0\xB6\xA1\x20\xE0\xB6\xA2\xE0\xB6\xA7\xE0\xB6\xA2\xE0\xB7\x92\x20\xE0\xB6\xAD\xE0\xB6\xB1\xE0\xB7\x8A\x20\xE0\xB6\xBA\xE0\xB7\x8F\xE0\xB6\xA9\x20\xE0\xB6\xA7\xE0\xB6\xB1\x20\xE0\xB7\x84\xE0\xB7\x90\xE0\xB6\xB8\xE0\xB7\x9A\x2E"; static const char* th_sentence_utf8 = "\xe0\xb8\x9a\xe0\xb8\xa3\xe0\xb8\xb4\xe0\xb9\x80\xe0\xb8\xa7\xe0\xb8\x93\xe0\xb8\xaa\xe0\xb8\xb8\xe0\xb8\x94\xe0\xb8\xa5\xe0\xb8\xb0\xe0\xb8\xa5\xe0\xb8\xb0\xe0\xb8\xaa\xe0\xb8\xb2\xe0\xb8\x87\xe0\xb8\x82\xe0\xb8\xad\xe0\xb8\x87\xe0\xb9\x84\xe0\xb8\xa5\xe0\xb8\xa1\xe0\xb9\x8c\xe0\xb8\x95\xe0\xb8\xa3\xe0\xb8\xb4\xe0\xb8\xa1\xe0\xb8\xad\xe0\xb9\x88\xe0\xb8\xb2\xe0\xb8\x94\xe0\xb8\xad\xe0\xb8\x94\xe0\xb9\x80\xe0\xb8\xaa\xe0\xb8\xa3\xe0\xb8\xa2\xe0\xb8\xa7\xe0\xb8\xa5\xe0\xb8\xb1\xe0\xb8\x9a\xe0\xb8\x81\xe0\xb8\xb4\xe0\xb8\x9b\xe0\xb8\x81\xe0\xb8\xa7\xe0\xb8\xb1\xe0\xb8\x87\x2e"; static const char* lo_sentence_utf8 = "\xE0\xB8\x9A\xE0\xB8\xB3\xE0\xB8\xA7\xE0\xB8\x94\xE0\xB8\xA5\xE0\xB8\xB2\xE0\xB8\xA1\xE0\xB8\xAA\xE0\xB8\xB2\xE0\xB8\x87\xE0\xB8\x82\xE0\xB9\x80\xE0\xB8\xA7\xE0\xB8\x87\xE0\xB8\x9D\xE0\xB8\xB2\xE0\xB9\x80\xE0\xB8\xA1\xE0\xB8\xAA\xE0\xB9\x83\xE0\xB8\xB8\xE0\xB8\x88\xE0\xB8\xA7\xE0\xB8\xB2\xE0\xB8\x99\xE0\xB9\x88\xE0\xB8\xB2\xE0\xB8\x8A\xE0\xB9\x8C\xE0\xB9\x89\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB8\xA7\xE0\xB8\xA2\xE0\xB8\xAA\xE0\xB8\xA7\xE0\xB8\xAA\xE0\xB8\xA2\xE0\xB8\xB2\xE0\xB8\x88\xE0\xB8\x9D\xE0\xB8\x81\xE0\xB8\x95\xE0\xB8\xAD\xE0\xB9\x80\xE0\xB8\xB1\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x81\xE0\xB8\xA3\xE0\xB8\xB2\xE0\xB9\x81\xE0\xB8\x94\xE0\xB8\x87\xE0\xB8\xAD\xE0\xB8\x81\xE0\xB8\xB8\xE0\xB9\x80\xE0\xB8\xA1\xE0\xB9\x87\xE0\xB8\xA2\xE0\xB9\x84\xE0\xB8\xA1\xE0\xB8\x97\xE0\xB8\xAD\xE0\xB9\x88\xE0\xB8\x87\xE0\xB8\xA2\xE0\xB8\xAD\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\xAA\xE0\xB8\xA1\xE0\xB8\xAA\xE0\xB8\xA3\xE0\xB9\x80\xE0\xB8\xA2\xE0\xB9\x88\xE0\xB8\xA3\xE0\xB8\xB8\xE0\xB8\xB8\xE0\xB8\xB4\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x88\xE0\xB8\x88\xE0\xB8\x87\xE0\xB8\x9B\xE0\xB8\x9A\xE0\xB8\x82\xE0\xB8\xB9\xE0\xB8\xB9\xE0\xB8\x8D\x2E"; static const char* ka_sentence_utf8 = "\xe1\x83\x9b\xe1\x83\x90\xe1\x83\x92\xe1\x83\x90\xe1\x83\xa0\xe1\x83\x98\x20\xe1\x83\xa7\xe1\x83\x90\xe1\x83\x95\xe1\x83\x98\xe1\x83\xa1\xe1\x83\xa4\xe1\x83\x94\xe1\x83\xa0\xe1\x83\x98\x20\xe1\x83\x9b\xe1\x83\x94\xe1\x83\x9a\xe1\x83\x98\xe1\x83\x90\x20\xe1\x83\xae\xe1\x83\xa2\xe1\x83\x94\xe1\x83\x91\xe1\x83\x90\x20\xe1\x83\x96\xe1\x83\x90\xe1\x83\xa0\xe1\x83\x9b\xe1\x83\x90\xe1\x83\xaa\xe1\x83\x98\x20\xe1\x83\xab\xe1\x83\xa6\xe1\x83\x9a\xe1\x83\x98\xe1\x83\xa1\xe1\x83\x90\x20\xe1\x83\x97\xe1\x83\x90\xe1\x83\x95\xe1\x83\x96\xe1\x83\x94\x2e"; static const char* ku_sentence_utf8 = "\xd8\xb4\xdb\x8e\xd9\x88\xd9\x86\xd8\xa7\x20\xdb\x8c\xd9\x87\xda\xa9\xda\xaf\xd8\xa7\xdb\x95\xd8\xb1\xd8\xa7\xd9\x86\xdb\x8e\xd9\x85\xd8\xa7\xd9\x86\xdb\x8c\x20\xd8\xa8\xd9\x88\xd9\x88\xd9\x86\xd8\xaf\xd9\x88\xd9\x88\xd9\x85\xd8\xaf\xd8\xb1\xd8\xa7\xd9\x88\xda\x98\xd8\xa7\xd9\x86\x20\xd8\xb4\xda\xa9\xd8\xb1\xdb\x95\x20\xd8\xb4\xd8\xa7\xd9\x85\xd8\xa7\xd8\xaa\xd8\xaf\xd8\xb1\xd8\xa7\x2e"; static const char* ps_sentence_utf8 = "\xd9\x88\xd9\x8a\xd9\x88\xd9\x88\x20\xd9\x85\xd9\x84\xd8\xb3\xd9\x8a\x20\xd9\x85\xd9\x84\xd8\xb3\x20\xd9\x88\xd9\x85\xd9\x84\xd8\xb3\xd8\xaa\x20\xd9\x85\xd8\xb2\xd8\xb3\xd9\x85\xd9\x85\xd8\xaa\xd9\x8a\xd8\xaf\xd9\x8a\xd8\xac\xd8\xa8\x20\xd9\x85\xd8\xb1\xd9\x88\xd9\x8a\xd9\x88\xd8\xb3\x20\xd8\xa8\xd9\x87\xd8\xaa\xd8\xb1\xd9\x8a\xd9\x86\xd8\xaa\xd8\xb1\xd9\x8a\xd9\x86\xd9\x86\xd9\x88\xd8\xaf\xd9\x88\xd8\xa8\xd8\xa7\xd9\x84\xd9\x87\xd8\x9f"; static const char* so_sentence_utf8 = "\x43\x69\x64\x69\x20\x77\x61\x78\x61\x79\x20\x64\x61\x72\x20\x77\x61\x61\x20\x67\x61\x6c\x6d\x75\x64\x61\x20\x77\x69\x20\x71\x61\x61\x20\x6d\x69\x64\x61\x20\x63\x69\x64\x69\x2e"; static const char* uz_sentence_utf8 = "\x42\x69\x72\x20\x6B\x75\x63\x68\x20\x66\x72\x61\x7A\x61\x73\x69\x20\x61\x79\x20\x64\x75\x6E\x79\x6F\x6B\x6C\x61\x72\x69\x20\x6D\x65\x6E\x20\x62\x69\x6B\x6F\x72\x20\x74\x75\x6E\x20\x6F\x6C\x64\x69\x72\x20\x67\x69\x6D\x70\x6F\x6B\x20\x62\x69\x6C\x61\x6E\x64\x61\x20\x76\x61\x20\x6B\x6F\x6E\x75\x70\x20\x79\x69\x6C\x64\x69\x7A\x2E"; static const char* az_sentence_utf8 = "\x42\x75\x74\x75\x6E\x20\x6B\x69\x72\x70\x61\x70\x79\x61\x20\x74\xC9\x99\x6D\x69\x7A\x20\x6D\xC9\x99\x68\x73\x75\x6C\x6F\x6C\x61\x72\x20\x62\x61\x78\x74\x61\x20\x62\x61\x68\x61\x72\x61\x74\x20\x71\xC9\x99\x7A\xC9\x99\x6B\x61\x6E\x6E\xC9\x99\x20\x61\x79\xC4\xB1\x72\x20\x6F\x74\x75\x7A\x20\x6C\xC9\x99\x20\x6C\xC9\x99\x73\x74\x69\x62\x2E"; static const char* hy_sentence_utf8 = "\xD4\xB2\xD5\xA1\xD6\x80\xD5\xA6\x20\xD5\xA5\xD6\x84\x20\xD5\xAB\xD5\xA1\xD5\xBA\xD5\xB0\xD5\xB6\xD5\xB5\x20\xD5\xA2\xD5\xA1\xD5\xB2\xD5\xA8\xD5\xB5\xD5\xAF\xD5\xB8\xD5\xB2\xD5\xBA\xD5\xA1\xD5\xB0\xD5\xAB\xD5\xAC\x20\xD5\xA1\xD5\xB7\xD5\xAD\xD5\xA1\xD5\xAC\xD5\xAB\x20\xD5\xBD\xD5\xB6\xD5\xB3\x20\xD5\xB8\xD5\xA9\xD5\xAB"; static const char* ja_sentence_utf8 = "\xe3\x82\xa2\xe3\x82\xa4\xe3\x82\xa6\x20\xe3\x82\xa8\xe3\x82\xaa\x20\xe3\x82\xab\xe3\x82\xad\xe3\x82\xaf\x20\xe3\x82\xb1\xe3\x82\xb3\x20\xe3\x82\xb5\xe3\x82\xb7\xe3\x82\xb9\x20\xe3\x82\xbb\xe3\x82\xbd\x20\xe3\x82\xbf\xe3\x82\xb9\xe3\x82\xbd\x20\xe3\x82\xbb\xe3\x82\xbd\x20\xe3\x83\x88\xe3\x82\xbd\xe3\x83\x88\x20\xe3\x83\x88\xe3\x83\xaa\xe3\x83\x88\x20\xe3\x83\x88\xe3\x82\xbd\xe3\x83\x88\x20\xe3\x83\x8f\xe3\x83\xaa\xe3\x83\x88\x20\xe3\x83\x8f\xe3\x82\xbd\xe3\x83\x8f\x20\xe3\x83\x8f\xe3\x82\xbd\xe3\x82\xbd\x20\xe3\x83\x8f\xe3\x82\xbd\xe3\x83\x8f\xe3\x82\xbd\x20\xe3\x83\x8f\xe3\x82\xbd\xe3\x83\x8f\x20\xe3\x83\x8f\xe3\x82\xbd\xe3\x82\xbd"; static const char* zh_sentence_utf8 = "\xe4\xb8\xad\xe6\x96\x87\xe4\xb8\xad\xe7\x9a\x84\xe6\xaf\x8f\xe4\xb8\xaa\xe5\xad\x97\xe6\xaf\x8d\xe9\x83\xbd\xe5\xbe\x88\xe9\x87\x8d\xe8\xa6\x81\xef\xbc\x8c\xe5\xae\x83\xe4\xbb\xac\xe5\x85\xb1\xe5\x90\x8c\xe6\x9e\x84\xe6\x88\x90\xe4\xba\x86\xe5\x8f\xa5\xe5\xad\x90\xe3\x80\x82"; struct { const char* id; // language ID const char* s; // sentence } sentences[] = { {"en", en_sentence_utf8}, {"es", es_sentence_utf8}, {"fr", fr_sentence_utf8}, {"de", de_sentence_utf8}, {"ru", ru_sentence_utf8}, {"pt", pt_sentence_utf8}, {"it", it_sentence_utf8}, {"nl", nl_sentence_utf8}, {"el", el_sentence_utf8}, {"tr", tr_sentence_utf8}, {"pl", pl_sentence_utf8}, {"uk", uk_sentence_utf8}, {"cs", cs_sentence_utf8}, {"hu", hu_sentence_utf8}, {"bg", bg_sentence_utf8}, {"sr", sr_sentence_utf8}, {"sq", sq_sentence_utf8}, {"sl", sl_sentence_utf8}, {"lt", lt_sentence_utf8}, {"lv", lv_sentence_utf8}, {"et", et_sentence_utf8}, {"ga", ga_sentence_utf8}, {"cy", cy_sentence_utf8}, {"is", is_sentence_utf8}, {"mt", mt_sentence_utf8}, {"he", he_sentence_utf8}, {"ar", ar_sentence_utf8}, {"hi", hi_sentence_utf8}, {"bn", bn_sentence_utf8}, {"ta", ta_sentence_utf8}, {"te", te_sentence_utf8}, {"kn", kn_sentence_utf8}, {"ml", ml_sentence_utf8}, {"si", si_sentence_utf8}, {"th", th_sentence_utf8}, {"lo", lo_sentence_utf8}, {"ka", ka_sentence_utf8}, {"ku", ku_sentence_utf8}, {"ps", ps_sentence_utf8}, {"so", so_sentence_utf8}, {"uz", uz_sentence_utf8}, {"az", az_sentence_utf8}, {"hy", hy_sentence_utf8}, {"ja", ja_sentence_utf8}, {"zh", zh_sentence_utf8} }; for (int i = 0; i < (int)(sizeof(sentences) / sizeof(sentences[0])); i++) { end = ui_edit_text.end_range(t); // rt_println("%s %s", sentences[i].id, sentences[i].s); rt_swear(ui_edit_text.replace_utf8(t, &end, sentences[i].s, -1, null)); rt_swear(ui_edit_text.replace_utf8(t, &end, "\n\n", -1, null)); } static const char* pirate_flag_utf8 = "\xF0\x9F\x8F\xB4\xE2\x80\x8D\xE2\x98\xA0\xEF\xB8\x8F"; end = ui_edit_text.end_range(t); rt_swear(ui_edit_text.replace_utf8(t, &end, pirate_flag_utf8, -1, null)); #endif } ================================================ FILE: src/samples/groot/groot.c ================================================ #include "rt/rt.h" #include "ui/ui.h" #include "rocket.h" #include "groot.h" #include "stb_image.h" // TODO: stack(ui_text with the content "I am groot..", view_groot) // Top: Find single line edit control "groot" with Find button that selects found text // bottom: UTC time and local time status of all views?, // Right view: debug toggles (text metric and margins) + message box on close window: // "Groot: I am groot...\n" // "Rocket: He says: Where the heck are you going now?\n" // "Retry" "Abort" "Ignore" enum { width = 512, height = 512 }; static uint8_t gs[width * height]; // greyscale //static ui_bitmap_t image; // grayscale image static ui_image_t view_groot; static ui_image_t view_rocket; static ui_image_t view_gs[2]; // two views at the same image static ui_edit_view_t view_text; static ui_edit_doc_t document; static void* load_image(const uint8_t* data, int64_t bytes, int32_t* w, int32_t* h, int32_t* bpp, int32_t preferred_bytes_per_pixel) { void* pixels = stbi_load_from_memory((uint8_t const*)data, (int32_t)bytes, w, h, bpp, preferred_bytes_per_pixel); return pixels; } static void init_image(ui_bitmap_t* i, const uint8_t* data, int64_t bytes) { int32_t w = 0; int32_t h = 0; int32_t c = 0; void* pixels = load_image(data, bytes, &w, &h, &c, 0); rt_not_null(pixels); ui_gdi.bitmap_init(i, w, h, c, pixels); stbi_image_free(pixels); } static void init_gs(void) { const ui_bitmap_t* i = &view_groot.image; uint32_t* pixels = (uint32_t*)i->pixels; rt_assert(i->w == 64 && i->h == 64 && i->bpp == 4); for (int y = 0; y < height; y++) { int32_t y64 = y % 64; for (int x = 0; x < width; x++) { int32_t x64 = x % 64; uint32_t rgba = pixels[y64 * 64 + x64]; ui_color_t c = (ui_color_t)rgba; c = ui_colors.gray_with_same_intensity(c); gs[y * width + x] = ((x / 64) % 2) == ((y / 64) % 2) ? (uint8_t)(c & 0xFF) : 0xC0; } } } static void panel_erase(ui_view_t* v) { ui_gdi.frame(v->x + 1, v->y + 1, v->w - 1, v->h - 1, ui_colors.black); } static void gs_erase(ui_view_t* v) { ui_gdi.fill(v->x, v->y, v->w, v->h, ui_colors.ennui_black); } static void slider_format(ui_view_t* v) { ui_slider_t* s = (ui_slider_t*)v; ui_view.set_text(v, "%.0f%%", s->value * 100.0 / 255.0); } static void slider_callback(ui_view_t* v) { ui_slider_t* s = (ui_slider_t*)v; view_groot.alpha = (fp64_t)s->value / 256.0; // rt_println("value: %d", slider->value); } static void init_images(void) { ui_image.init(&view_groot); init_image(&view_groot.image, groot, rt_countof(groot)); // view of groot image: // ui_image.ratio(&view_groot, 4, 1); // 4:1 ui_image.ratio(&view_groot, 3, 1); // 4:1 view_groot.alpha = 0.5; view_groot.padding = (ui_margins_t){0.125f, 0.125f, 0.125f, 0.125f}; view_groot.focusable = false; // because it is stacked under text editor // view of rocket image: ui_image.init(&view_rocket); init_image(&view_rocket.image, rocket, rt_countof(rocket)); ui_image.ratio(&view_rocket, 3, 1); // 3:1 view_rocket.padding = (ui_margins_t){0.125f, 0.125f, 0.125f, 0.125f}; view_groot.focusable = false; // no zoom/pan init_gs(); } static void init_text(void) { rt_swear(ui_edit_doc.init(&document, "Star-Lord: \"What is wrong with giving tree, here?\"\n" "Rocket: \"Well, he don't know talking good like me and you, " "so his vocabulistics is limited to 'I' and 'am' and 'Groot.' " "Exclusively, in that order.\"", -1, false)); ui_edit_view.init(&view_text, &document); view_text.hide_word_wrap = true; view_text.hide_word_wrap = false; // TODO: debugging remove view_text.padding = (ui_margins_t){0}; // TODO: commented out for debugging uncomment is hiding word wrap // view_text.insets = (ui_margins_t){0}; view_text.background_id = 0; view_text.background = ui_colors.transparent; rt_str_printf(view_text.hint, "Text Edit:\n\n" "Try double clicking to select a word\n" "or long-pressing to select a paragraph.\n\n" "Ctrl+[Shift]+arrows, Ctrl+X|C|V,\n" "Undo/Redo Ctrl+Z/Y\n" ); } static ui_view_t* align(ui_view_t* v, int32_t align) { v->align = align; return v; } static ui_view_t* fill_parent(ui_view_t* v) { v->max_h = ui.infinity; v->max_w = ui.infinity; return v; } static void opened(void) { init_images(); init_text(); static ui_view_t list = ui_view(list); static ui_label_t label_left = ui_label(0, "Left"); static ui_label_t label_top = ui_label(0, "Top"); static ui_label_t label_bottom = ui_label(0, "Bottom"); // painting greyscale pixels will be handled w/o device bitmap: for (int32_t i = 0; i < rt_countof(view_gs); i++) { ui_image.init_with(&view_gs[i], gs, width, height, 1, width); view_gs[i].erase = gs_erase; view_gs[i].focusable = true; // enable zoom pan } static ui_view_t top = ui_view(stack); static ui_view_t center = ui_view(span); static ui_view_t left = ui_view(list); static ui_view_t right = ui_view(list); static ui_view_t stack = ui_view(stack); static ui_view_t bottom = ui_view(stack); static ui_view_t spacer = ui_view(spacer); static ui_slider_t slider = ui_slider("128", 16.0f, 0, 255, slider_format, slider_callback); slider.value = 128; ui_view.add(fill_parent(&left), align(&view_gs[0].view, ui.align.left), align(&view_gs[1].view, ui.align.left), null ); ui_view.add(&stack, fill_parent(&view_groot.view), fill_parent(&view_text.view), null ); ui_view.add(&right, &spacer, fill_parent(&view_rocket.view), &slider, fill_parent(&stack), null ); ui_view.add(&top, &label_top, null); ui_view.add(&bottom, &label_bottom, null); ui_view.add(¢er, align(&left, ui.align.top), align(&right, ui.align.top), null); ui_view.add(ui_app.content, ui_view.add(fill_parent(&list), align(&top, ui.align.center), align(¢er, ui.align.left), align(&bottom, ui.align.center), null ), null ); stack.debug.id = "#stack: edit+image"; list.debug.id = "#list"; right.debug.id = "#right"; // list.debug.paint.margins = true; right.debug.paint.margins = true; center.insets = (ui_margins_t){0}; center.padding = (ui_margins_t){0}; static ui_view_t* panels[] = { &top, &left, &right, &bottom }; for (int32_t i = 0; i < rt_countof(panels); i++) { panels[i]->erase = panel_erase; panels[i]->padding = (ui_margins_t){0}; panels[i]->insets = (ui_margins_t){0.125f, 0.125f, 0.125f, 0.125f}; } list.background_id = ui_color_id_window; ui_view_for_each(&list, it, { // it->debug.paint.margins = true; it->max_w = ui.infinity; }); ui_view_for_each(¢er, it, { // it->debug.paint.margins = true; it->max_h = ui.infinity; }); center.max_h = ui.infinity; for (int32_t i = 0; i < rt_countof(view_gs); i++) { fill_parent(&view_gs[i].view)->erase = panel_erase; } view_gs[0].padding.bottom = 0.25f; view_gs[1].padding.top = 0.25f; view_gs[0].fit = true; view_gs[1].fill = true; view_groot.debug.id = "#view.groot"; view_rocket.debug.id = "#view.rocket"; view_groot.fit = true; view_rocket.fit = true; // view_groot.fill = true; // view_rocket.fill = true; } static void closed(void) { ui_view.disband(ui_app.content); ui_gdi.bitmap_dispose(&view_groot.image); ui_gdi.bitmap_dispose(&view_rocket.image); } ui_app_t ui_app = { .class_name = "groot", .title = "Groot", .dark_mode = true, .opened = opened, .closed = closed, .window_sizing = { // inches .min_w = 5.0f, .min_h = 4.0f, .ini_w = 10.0f, .ini_h = 6.0f } }; ================================================ FILE: src/samples/groot/groot.h ================================================ #pragma once static unsigned char groot[] = { // groot.jpg file 64x64 rgba 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x03, 0x00, 0x00, 0x00, 0x9D, 0xB7, 0x81, 0xEC, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, 0x61, 0x05, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x2E, 0x23, 0x00, 0x00, 0x2E, 0x23, 0x01, 0x78, 0xA5, 0x3F, 0x76, 0x00, 0x00, 0x13, 0xAE, 0x69, 0x54, 0x58, 0x74, 0x58, 0x4D, 0x4C, 0x3A, 0x63, 0x6F, 0x6D, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x78, 0x6D, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6E, 0x3D, 0x22, 0xEF, 0xBB, 0xBF, 0x22, 0x20, 0x69, 0x64, 0x3D, 0x22, 0x57, 0x35, 0x4D, 0x30, 0x4D, 0x70, 0x43, 0x65, 0x68, 0x69, 0x48, 0x7A, 0x72, 0x65, 0x53, 0x7A, 0x4E, 0x54, 0x63, 0x7A, 0x6B, 0x63, 0x39, 0x64, 0x22, 0x3F, 0x3E, 0x0A, 0x3C, 0x78, 0x3A, 0x78, 0x6D, 0x70, 0x6D, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x78, 0x3D, 0x22, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x3A, 0x6E, 0x73, 0x3A, 0x6D, 0x65, 0x74, 0x61, 0x2F, 0x22, 0x20, 0x78, 0x3A, 0x78, 0x6D, 0x70, 0x74, 0x6B, 0x3D, 0x22, 0x58, 0x4D, 0x50, 0x20, 0x43, 0x6F, 0x72, 0x65, 0x20, 0x34, 0x2E, 0x34, 0x2E, 0x30, 0x2D, 0x45, 0x78, 0x69, 0x76, 0x32, 0x22, 0x3E, 0x0A, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x72, 0x64, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x77, 0x33, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x31, 0x39, 0x39, 0x39, 0x2F, 0x30, 0x32, 0x2F, 0x32, 0x32, 0x2D, 0x72, 0x64, 0x66, 0x2D, 0x73, 0x79, 0x6E, 0x74, 0x61, 0x78, 0x2D, 0x6E, 0x73, 0x23, 0x22, 0x3E, 0x0A, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x72, 0x64, 0x66, 0x3A, 0x61, 0x62, 0x6F, 0x75, 0x74, 0x3D, 0x22, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x78, 0x61, 0x70, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x6D, 0x6D, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x78, 0x61, 0x70, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x73, 0x54, 0x79, 0x70, 0x65, 0x2F, 0x52, 0x65, 0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6E, 0x74, 0x23, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x78, 0x61, 0x70, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x73, 0x54, 0x79, 0x70, 0x65, 0x2F, 0x52, 0x65, 0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x23, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x64, 0x63, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x70, 0x75, 0x72, 0x6C, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x64, 0x63, 0x2F, 0x65, 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x73, 0x2F, 0x31, 0x2E, 0x31, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x65, 0x78, 0x69, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x65, 0x78, 0x69, 0x66, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x47, 0x49, 0x4D, 0x50, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x67, 0x69, 0x6D, 0x70, 0x2E, 0x6F, 0x72, 0x67, 0x2F, 0x78, 0x6D, 0x70, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x70, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x74, 0x69, 0x66, 0x66, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x74, 0x69, 0x66, 0x66, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x6C, 0x6E, 0x73, 0x3A, 0x78, 0x6D, 0x70, 0x3D, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x6E, 0x73, 0x2E, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x78, 0x61, 0x70, 0x2F, 0x31, 0x2E, 0x30, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x44, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x49, 0x44, 0x3D, 0x22, 0x61, 0x64, 0x6F, 0x62, 0x65, 0x3A, 0x64, 0x6F, 0x63, 0x69, 0x64, 0x3A, 0x70, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x3A, 0x35, 0x31, 0x33, 0x61, 0x64, 0x63, 0x66, 0x64, 0x2D, 0x35, 0x65, 0x62, 0x30, 0x2D, 0x35, 0x35, 0x34, 0x35, 0x2D, 0x38, 0x39, 0x32, 0x37, 0x2D, 0x35, 0x36, 0x30, 0x65, 0x32, 0x30, 0x61, 0x65, 0x39, 0x32, 0x34, 0x38, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x44, 0x3D, 0x22, 0x78, 0x6D, 0x70, 0x2E, 0x69, 0x69, 0x64, 0x3A, 0x31, 0x32, 0x31, 0x62, 0x64, 0x37, 0x39, 0x63, 0x2D, 0x30, 0x63, 0x63, 0x32, 0x2D, 0x34, 0x64, 0x65, 0x61, 0x2D, 0x62, 0x31, 0x37, 0x37, 0x2D, 0x39, 0x62, 0x33, 0x61, 0x30, 0x63, 0x66, 0x66, 0x63, 0x63, 0x62, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x4F, 0x72, 0x69, 0x67, 0x69, 0x6E, 0x61, 0x6C, 0x44, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x49, 0x44, 0x3D, 0x22, 0x36, 0x38, 0x38, 0x45, 0x45, 0x41, 0x46, 0x37, 0x30, 0x30, 0x30, 0x37, 0x44, 0x31, 0x35, 0x41, 0x41, 0x38, 0x30, 0x45, 0x37, 0x45, 0x42, 0x30, 0x33, 0x39, 0x36, 0x41, 0x39, 0x41, 0x44, 0x43, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x64, 0x63, 0x3A, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x3D, 0x22, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x65, 0x78, 0x69, 0x66, 0x3A, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x3D, 0x22, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x65, 0x78, 0x69, 0x66, 0x3A, 0x45, 0x78, 0x69, 0x66, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x30, 0x32, 0x32, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x65, 0x78, 0x69, 0x66, 0x3A, 0x50, 0x69, 0x78, 0x65, 0x6C, 0x58, 0x44, 0x69, 0x6D, 0x65, 0x6E, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x39, 0x32, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x65, 0x78, 0x69, 0x66, 0x3A, 0x50, 0x69, 0x78, 0x65, 0x6C, 0x59, 0x44, 0x69, 0x6D, 0x65, 0x6E, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x38, 0x33, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x3A, 0x41, 0x50, 0x49, 0x3D, 0x22, 0x32, 0x2E, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x3A, 0x50, 0x6C, 0x61, 0x74, 0x66, 0x6F, 0x72, 0x6D, 0x3D, 0x22, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x3A, 0x54, 0x69, 0x6D, 0x65, 0x53, 0x74, 0x61, 0x6D, 0x70, 0x3D, 0x22, 0x31, 0x36, 0x35, 0x31, 0x32, 0x37, 0x31, 0x32, 0x31, 0x34, 0x38, 0x31, 0x34, 0x32, 0x38, 0x32, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x3A, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x32, 0x2E, 0x31, 0x30, 0x2E, 0x33, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x70, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x3A, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x4D, 0x6F, 0x64, 0x65, 0x3D, 0x22, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x3D, 0x22, 0x38, 0x33, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x57, 0x69, 0x64, 0x74, 0x68, 0x3D, 0x22, 0x39, 0x32, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x4F, 0x72, 0x69, 0x65, 0x6E, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x50, 0x68, 0x6F, 0x74, 0x6F, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x32, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x52, 0x65, 0x73, 0x6F, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x55, 0x6E, 0x69, 0x74, 0x3D, 0x22, 0x32, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x73, 0x50, 0x65, 0x72, 0x50, 0x69, 0x78, 0x65, 0x6C, 0x3D, 0x22, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x58, 0x52, 0x65, 0x73, 0x6F, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x33, 0x30, 0x30, 0x2F, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x59, 0x52, 0x65, 0x73, 0x6F, 0x6C, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x33, 0x30, 0x30, 0x2F, 0x31, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x3A, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x65, 0x3D, 0x22, 0x32, 0x30, 0x31, 0x39, 0x2D, 0x30, 0x38, 0x2D, 0x32, 0x34, 0x54, 0x31, 0x31, 0x3A, 0x30, 0x33, 0x3A, 0x33, 0x37, 0x2B, 0x30, 0x35, 0x3A, 0x33, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x3A, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6F, 0x72, 0x54, 0x6F, 0x6F, 0x6C, 0x3D, 0x22, 0x47, 0x49, 0x4D, 0x50, 0x20, 0x32, 0x2E, 0x31, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x3A, 0x4D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x44, 0x61, 0x74, 0x65, 0x3D, 0x22, 0x32, 0x30, 0x31, 0x39, 0x2D, 0x30, 0x38, 0x2D, 0x32, 0x34, 0x54, 0x31, 0x31, 0x3A, 0x30, 0x36, 0x3A, 0x33, 0x32, 0x2B, 0x30, 0x35, 0x3A, 0x33, 0x30, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x78, 0x6D, 0x70, 0x3A, 0x4D, 0x6F, 0x64, 0x69, 0x66, 0x79, 0x44, 0x61, 0x74, 0x65, 0x3D, 0x22, 0x32, 0x30, 0x31, 0x39, 0x2D, 0x30, 0x38, 0x2D, 0x32, 0x34, 0x54, 0x31, 0x31, 0x3A, 0x30, 0x36, 0x3A, 0x33, 0x32, 0x2B, 0x30, 0x35, 0x3A, 0x33, 0x30, 0x22, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x3C, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x48, 0x69, 0x73, 0x74, 0x6F, 0x72, 0x79, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x53, 0x65, 0x71, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x73, 0x61, 0x76, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x63, 0x68, 0x61, 0x6E, 0x67, 0x65, 0x64, 0x3D, 0x22, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x69, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x44, 0x3D, 0x22, 0x78, 0x6D, 0x70, 0x2E, 0x69, 0x69, 0x64, 0x3A, 0x36, 0x66, 0x32, 0x32, 0x36, 0x33, 0x34, 0x33, 0x2D, 0x39, 0x62, 0x35, 0x65, 0x2D, 0x37, 0x36, 0x34, 0x36, 0x2D, 0x61, 0x32, 0x30, 0x62, 0x2D, 0x35, 0x64, 0x31, 0x39, 0x62, 0x66, 0x61, 0x30, 0x30, 0x31, 0x66, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x73, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x41, 0x67, 0x65, 0x6E, 0x74, 0x3D, 0x22, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x50, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x20, 0x43, 0x43, 0x20, 0x28, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x29, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x77, 0x68, 0x65, 0x6E, 0x3D, 0x22, 0x32, 0x30, 0x31, 0x39, 0x2D, 0x30, 0x38, 0x2D, 0x32, 0x34, 0x54, 0x31, 0x31, 0x3A, 0x30, 0x36, 0x3A, 0x33, 0x32, 0x2B, 0x30, 0x35, 0x3A, 0x33, 0x30, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x63, 0x6F, 0x6E, 0x76, 0x65, 0x72, 0x74, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x3D, 0x22, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x6A, 0x70, 0x65, 0x67, 0x20, 0x74, 0x6F, 0x20, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, 0x65, 0x72, 0x73, 0x3D, 0x22, 0x63, 0x6F, 0x6E, 0x76, 0x65, 0x72, 0x74, 0x65, 0x64, 0x20, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x6A, 0x70, 0x65, 0x67, 0x20, 0x74, 0x6F, 0x20, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x73, 0x61, 0x76, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x63, 0x68, 0x61, 0x6E, 0x67, 0x65, 0x64, 0x3D, 0x22, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x69, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x44, 0x3D, 0x22, 0x78, 0x6D, 0x70, 0x2E, 0x69, 0x69, 0x64, 0x3A, 0x36, 0x66, 0x32, 0x34, 0x33, 0x65, 0x32, 0x61, 0x2D, 0x62, 0x66, 0x38, 0x31, 0x2D, 0x34, 0x63, 0x34, 0x61, 0x2D, 0x62, 0x37, 0x62, 0x32, 0x2D, 0x36, 0x36, 0x62, 0x35, 0x32, 0x38, 0x34, 0x64, 0x36, 0x32, 0x38, 0x65, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x73, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x41, 0x67, 0x65, 0x6E, 0x74, 0x3D, 0x22, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x50, 0x68, 0x6F, 0x74, 0x6F, 0x73, 0x68, 0x6F, 0x70, 0x20, 0x43, 0x43, 0x20, 0x28, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x29, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x77, 0x68, 0x65, 0x6E, 0x3D, 0x22, 0x32, 0x30, 0x31, 0x39, 0x2D, 0x30, 0x38, 0x2D, 0x32, 0x34, 0x54, 0x31, 0x31, 0x3A, 0x30, 0x36, 0x3A, 0x33, 0x32, 0x2B, 0x30, 0x35, 0x3A, 0x33, 0x30, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x61, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x73, 0x61, 0x76, 0x65, 0x64, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x63, 0x68, 0x61, 0x6E, 0x67, 0x65, 0x64, 0x3D, 0x22, 0x2F, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x69, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x44, 0x3D, 0x22, 0x78, 0x6D, 0x70, 0x2E, 0x69, 0x69, 0x64, 0x3A, 0x66, 0x61, 0x30, 0x64, 0x63, 0x37, 0x66, 0x66, 0x2D, 0x35, 0x64, 0x36, 0x66, 0x2D, 0x34, 0x64, 0x65, 0x34, 0x2D, 0x61, 0x62, 0x39, 0x31, 0x2D, 0x30, 0x66, 0x38, 0x62, 0x63, 0x61, 0x61, 0x63, 0x38, 0x63, 0x33, 0x32, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x73, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x41, 0x67, 0x65, 0x6E, 0x74, 0x3D, 0x22, 0x47, 0x69, 0x6D, 0x70, 0x20, 0x32, 0x2E, 0x31, 0x30, 0x20, 0x28, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x29, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3A, 0x77, 0x68, 0x65, 0x6E, 0x3D, 0x22, 0x32, 0x30, 0x32, 0x32, 0x2D, 0x30, 0x34, 0x2D, 0x32, 0x39, 0x54, 0x31, 0x35, 0x3A, 0x32, 0x36, 0x3A, 0x35, 0x34, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x53, 0x65, 0x71, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x48, 0x69, 0x73, 0x74, 0x6F, 0x72, 0x79, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x3C, 0x78, 0x6D, 0x70, 0x4D, 0x4D, 0x3A, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x46, 0x72, 0x6F, 0x6D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3A, 0x64, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x49, 0x44, 0x3D, 0x22, 0x36, 0x38, 0x38, 0x45, 0x45, 0x41, 0x46, 0x37, 0x30, 0x30, 0x30, 0x37, 0x44, 0x31, 0x35, 0x41, 0x41, 0x38, 0x30, 0x45, 0x37, 0x45, 0x42, 0x30, 0x33, 0x39, 0x36, 0x41, 0x39, 0x41, 0x44, 0x43, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3A, 0x69, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x44, 0x3D, 0x22, 0x78, 0x6D, 0x70, 0x2E, 0x69, 0x69, 0x64, 0x3A, 0x36, 0x66, 0x32, 0x32, 0x36, 0x33, 0x34, 0x33, 0x2D, 0x39, 0x62, 0x35, 0x65, 0x2D, 0x37, 0x36, 0x34, 0x36, 0x2D, 0x61, 0x32, 0x30, 0x62, 0x2D, 0x35, 0x64, 0x31, 0x39, 0x62, 0x66, 0x61, 0x30, 0x30, 0x31, 0x66, 0x33, 0x22, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3A, 0x6F, 0x72, 0x69, 0x67, 0x69, 0x6E, 0x61, 0x6C, 0x44, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x49, 0x44, 0x3D, 0x22, 0x36, 0x38, 0x38, 0x45, 0x45, 0x41, 0x46, 0x37, 0x30, 0x30, 0x30, 0x37, 0x44, 0x31, 0x35, 0x41, 0x41, 0x38, 0x30, 0x45, 0x37, 0x45, 0x42, 0x30, 0x33, 0x39, 0x36, 0x41, 0x39, 0x41, 0x44, 0x43, 0x22, 0x2F, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x3C, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x42, 0x69, 0x74, 0x73, 0x50, 0x65, 0x72, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x53, 0x65, 0x71, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x38, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x38, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x38, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x6C, 0x69, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x53, 0x65, 0x71, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x3C, 0x2F, 0x74, 0x69, 0x66, 0x66, 0x3A, 0x42, 0x69, 0x74, 0x73, 0x50, 0x65, 0x72, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x3E, 0x0A, 0x20, 0x20, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x3E, 0x0A, 0x20, 0x3C, 0x2F, 0x72, 0x64, 0x66, 0x3A, 0x52, 0x44, 0x46, 0x3E, 0x0A, 0x3C, 0x2F, 0x78, 0x3A, 0x78, 0x6D, 0x70, 0x6D, 0x65, 0x74, 0x61, 0x3E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0A, 0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20, 0x65, 0x6E, 0x64, 0x3D, 0x22, 0x77, 0x22, 0x3F, 0x3E, 0x4C, 0x13, 0xA6, 0xFF, 0x00, 0x00, 0x16, 0x74, 0x7A, 0x54, 0x58, 0x74, 0x52, 0x61, 0x77, 0x20, 0x70, 0x72, 0x6F, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x65, 0x78, 0x69, 0x66, 0x00, 0x00, 0x78, 0xDA, 0xA5, 0x9A, 0x69, 0x96, 0x1B, 0x39, 0x92, 0x84, 0xFF, 0xE3, 0x14, 0x73, 0x84, 0xC0, 0x0E, 0x1C, 0x07, 0xEB, 0x7B, 0x73, 0x83, 0x39, 0xFE, 0x7C, 0xE6, 0xC1, 0xCC, 0x2A, 0xA9, 0xAA, 0xBA, 0xA5, 0x6E, 0xA5, 0x92, 0x64, 0x92, 0x11, 0x58, 0x7C, 0x31, 0x33, 0x77, 0xD0, 0x9D, 0xFF, 0xFB, 0xDF, 0xEB, 0xFE, 0x87, 0x7F, 0xB9, 0xFB, 0xE6, 0x52, 0xAE, 0xAD, 0xF4, 0x52, 0x1E, 0xFE, 0xA5, 0x9E, 0x7A, 0x18, 0xBC, 0x68, 0xCF, 0xFB, 0x6F, 0xD8, 0xA3, 0x7F, 0x92, 0x3D, 0xDA, 0xBF, 0xF4, 0xF9, 0x88, 0xBF, 0x7F, 0x78, 0xDF, 0x7D, 0x7F, 0x10, 0x78, 0x2B, 0xF2, 0x1C, 0xDF, 0x3F, 0x5B, 0xF9, 0x5C, 0xFF, 0xF5, 0xBE, 0xFF, 0x1E, 0xE0, 0x7D, 0x1A, 0xBC, 0xCA, 0x7F, 0x1A, 0xA8, 0xAD, 0xCF, 0x07, 0xF3, 0xC7, 0x0F, 0xFA, 0x67, 0x86, 0xD0, 0x7E, 0x1A, 0x28, 0xBC, 0x4F, 0x51, 0x2B, 0xD2, 0xEB, 0xFD, 0x19, 0xA8, 0x7F, 0x06, 0x8A, 0xE1, 0xFD, 0xC0, 0x7F, 0x06, 0x18, 0xEF, 0xB6, 0x9E, 0xD2, 0x5B, 0xFD, 0xF3, 0x16, 0xE6, 0x79, 0x9F, 0xF7, 0xD7, 0x4E, 0xDA, 0xFB, 0xEB, 0xF4, 0x10, 0xD6, 0xE7, 0xB2, 0xCF, 0xC5, 0x3F, 0xFF, 0x9D, 0x2A, 0xD6, 0xDB, 0x99, 0x79, 0x62, 0x08, 0x27, 0xFA, 0xF8, 0xF0, 0x18, 0xE3, 0x67, 0x01, 0x51, 0xBF, 0xC1, 0xC5, 0xC1, 0x8B, 0xC4, 0x63, 0x88, 0x9D, 0x0B, 0x7D, 0xCC, 0xF6, 0xBA, 0xF0, 0x98, 0xE2, 0xD7, 0x56, 0x31, 0xC8, 0xDF, 0xD9, 0xE9, 0xFB, 0x5F, 0x67, 0x45, 0xF7, 0x7C, 0x5C, 0xF1, 0xD7, 0x8B, 0x7E, 0xF0, 0xCA, 0xF7, 0x2B, 0xFF, 0xF7, 0xEF, 0xBB, 0x9F, 0xBD, 0x95, 0xC2, 0xE7, 0x92, 0xF8, 0x93, 0x91, 0xCB, 0xF7, 0xF3, 0xDF, 0xBE, 0xEF, 0x7C, 0xFE, 0xE9, 0x83, 0xF8, 0x3D, 0x4F, 0xF8, 0x21, 0x7E, 0xDA, 0xE7, 0x55, 0xF8, 0xF1, 0xFD, 0xFD, 0xF8, 0xFC, 0xAE, 0xE8, 0x27, 0xEB, 0xEB, 0xF7, 0xDE, 0xDD, 0xAE, 0xED, 0x99, 0x5D, 0x8C, 0x54, 0x30, 0x75, 0xF9, 0x6C, 0xEA, 0x6B, 0x2B, 0xF6, 0x8A, 0xEB, 0x26, 0x53, 0x68, 0xEA, 0xE6, 0x58, 0x5A, 0x79, 0x2A, 0xBF, 0x99, 0x21, 0xAA, 0xFD, 0x74, 0x7E, 0x1A, 0x51, 0xBD, 0x08, 0x85, 0xFD, 0xAC, 0x67, 0xF2, 0xB3, 0x7C, 0xF7, 0x01, 0x77, 0x5D, 0x9F, 0xFC, 0xF6, 0xC3, 0x5F, 0x7F, 0xEC, 0x79, 0xF9, 0xC5, 0x12, 0x53, 0x38, 0x2E, 0x54, 0x5E, 0x84, 0xB0, 0x42, 0xB4, 0x37, 0x5B, 0xAC, 0xA1, 0x87, 0x15, 0xE5, 0xBF, 0xA4, 0x1F, 0x7F, 0x43, 0x8D, 0x3D, 0xEE, 0xD8, 0xF0, 0xE5, 0x32, 0xB7, 0xA7, 0x18, 0xBE, 0xD7, 0xE2, 0x6D, 0xDA, 0xFE, 0x2C, 0x67, 0xB3, 0x35, 0x66, 0xDE, 0x9E, 0x4B, 0x83, 0x67, 0x30, 0xAF, 0xB8, 0xF8, 0xDD, 0x1F, 0xF7, 0xBB, 0x37, 0xDC, 0xAB, 0x54, 0xF0, 0xFE, 0x69, 0xDF, 0xB6, 0x62, 0x5D, 0x21, 0xC8, 0xD8, 0x2C, 0x43, 0x9E, 0xD3, 0x23, 0x97, 0xE1, 0x11, 0x7F, 0x3F, 0x46, 0xCD, 0x66, 0xE0, 0xAF, 0x9F, 0x9F, 0xFF, 0xC9, 0xAF, 0x11, 0x0F, 0x66, 0x59, 0x59, 0x29, 0xD2, 0x31, 0xEC, 0x7C, 0x87, 0x98, 0xD9, 0xFF, 0x81, 0x04, 0xD1, 0x1C, 0x1D, 0xB9, 0x30, 0xF3, 0xFC, 0xE6, 0xA0, 0xAF, 0xFB, 0x33, 0x00, 0x26, 0x62, 0xEA, 0xCC, 0x62, 0x7C, 0xC4, 0x03, 0x78, 0x8D, 0xAC, 0xF0, 0xC5, 0x3F, 0x35, 0x84, 0xEA, 0x3D, 0x86, 0x6C, 0x38, 0x68, 0xB0, 0xF4, 0x10, 0x53, 0x98, 0x78, 0xC0, 0xE7, 0x1C, 0x36, 0x8B, 0x0C, 0x29, 0xC6, 0x82, 0x6F, 0x5A, 0xD0, 0xD4, 0xDC, 0x52, 0xBD, 0x5D, 0x1A, 0x72, 0xE0, 0x6D, 0xC7, 0xFB, 0x80, 0x99, 0xF2, 0x8A, 0xFC, 0xAA, 0xF8, 0xA6, 0xC7, 0x81, 0xB3, 0x52, 0xCA, 0xC4, 0x4F, 0x4D, 0x8D, 0x18, 0x1A, 0x39, 0xE6, 0x94, 0x73, 0x2E, 0xB9, 0xE6, 0x96, 0x7B, 0x1E, 0x25, 0x96, 0x54, 0x72, 0x29, 0xA5, 0x16, 0x81, 0xE2, 0xA8, 0xB1, 0x26, 0x57, 0x73, 0x2D, 0xB5, 0xD6, 0x56, 0x7B, 0x1D, 0x2D, 0xB6, 0xD4, 0x72, 0x2B, 0xAD, 0xB6, 0xD6, 0x7A, 0x1B, 0x3D, 0xF4, 0x08, 0x68, 0xE6, 0x5E, 0x7A, 0xED, 0xAD, 0xF7, 0x3E, 0x06, 0x73, 0x0E, 0x46, 0x1E, 0xDC, 0x3D, 0xB8, 0x60, 0x8C, 0x19, 0x66, 0x9C, 0x69, 0x66, 0x37, 0xCB, 0xAC, 0xB3, 0xCD, 0x3E, 0xC7, 0x22, 0x7C, 0x56, 0x5A, 0x79, 0x95, 0x55, 0x57, 0x5B, 0x7D, 0x8D, 0x1D, 0x76, 0xDC, 0xE0, 0xC7, 0x2E, 0xBB, 0xEE, 0xB6, 0xFB, 0x1E, 0xC7, 0x1F, 0x42, 0xE9, 0xA4, 0x93, 0x4F, 0x39, 0xF5, 0xB4, 0xD3, 0xCF, 0xB8, 0x84, 0xDA, 0x8D, 0xEE, 0xA6, 0x9B, 0x6F, 0xB9, 0xF5, 0xB6, 0xDB, 0xEF, 0xF8, 0xF6, 0xDA, 0xC7, 0xAD, 0x7F, 0xF9, 0xF9, 0x0D, 0xAF, 0xF9, 0x8F, 0xD7, 0x82, 0x79, 0x4A, 0x17, 0xD6, 0x6F, 0xAF, 0xF1, 0x6E, 0xAD, 0x5F, 0x43, 0x78, 0xC1, 0x49, 0x96, 0xCF, 0x70, 0x58, 0x70, 0xC9, 0xE3, 0xF1, 0x2A, 0x17, 0x10, 0xD0, 0x41, 0x3E, 0x7B, 0x9A, 0x4F, 0x29, 0xC8, 0x73, 0xF2, 0xD9, 0xD3, 0x03, 0x59, 0x91, 0x03, 0x8B, 0xCC, 0xF2, 0xD9, 0xF6, 0xF2, 0x18, 0x1E, 0x4C, 0xC7, 0x87, 0x7C, 0xFD, 0x97, 0xEF, 0x5C, 0x78, 0x3D, 0x2A, 0xCF, 0xFD, 0x57, 0x7E, 0x73, 0x35, 0xFD, 0xE0, 0xB7, 0xF0, 0x9F, 0x7A, 0xCE, 0xC9, 0x75, 0xBF, 0xE9, 0xB9, 0xBF, 0xFA, 0xED, 0xEF, 0xBC, 0xB6, 0x45, 0x43, 0xCB, 0x3C, 0xF6, 0x66, 0xA1, 0x8C, 0xFA, 0x44, 0xB2, 0x8F, 0x6B, 0x46, 0x68, 0xFC, 0x87, 0xAB, 0xFE, 0xFA, 0xEC, 0xFE, 0xE9, 0x83, 0x7F, 0x7C, 0x3E, 0x21, 0xEC, 0x79, 0x02, 0x49, 0x37, 0x62, 0xAD, 0xF8, 0x7A, 0xDC, 0x72, 0xF6, 0x72, 0xBC, 0x9C, 0xAB, 0xD4, 0x7B, 0x4B, 0x1E, 0xE9, 0xA4, 0x5E, 0x4E, 0x4C, 0x9D, 0x60, 0xAD, 0x23, 0xCC, 0x7E, 0xEF, 0xB3, 0x6A, 0xD3, 0xD3, 0x83, 0x8F, 0xE2, 0x9D, 0x99, 0x55, 0xF9, 0xBB, 0xFB, 0x5D, 0xEC, 0xF2, 0xDE, 0xB0, 0x6B, 0x3D, 0x67, 0xEF, 0x39, 0xB7, 0x6B, 0x25, 0xF2, 0xB2, 0x26, 0x9F, 0x16, 0xBF, 0x99, 0xFF, 0x62, 0xDB, 0xDF, 0x7F, 0x76, 0xBF, 0x7A, 0x61, 0xC6, 0xA7, 0xAB, 0x4C, 0x2D, 0x07, 0x53, 0xDF, 0x73, 0xE2, 0xDA, 0x97, 0xBD, 0x55, 0xD0, 0x7B, 0xAF, 0x12, 0x9D, 0x3F, 0xAB, 0xEE, 0xD2, 0x63, 0x2D, 0x0F, 0x61, 0xD7, 0xF0, 0x40, 0xB9, 0x3E, 0xDC, 0xB6, 0xEE, 0x9D, 0xA9, 0x9E, 0x35, 0xEF, 0x69, 0xFE, 0xE4, 0xDB, 0x2B, 0xC1, 0x7F, 0x70, 0x30, 0x08, 0xBE, 0xEA, 0x9D, 0xF1, 0xEC, 0x9C, 0x07, 0x9B, 0xE5, 0xB7, 0xF2, 0x29, 0x4C, 0xDB, 0x78, 0x97, 0xC8, 0xC2, 0x6D, 0x5C, 0x72, 0xE2, 0x9E, 0x7B, 0x8C, 0xB8, 0x61, 0x21, 0xBC, 0x4C, 0xCC, 0xDD, 0xBA, 0x72, 0x60, 0x90, 0x09, 0xFB, 0x14, 0x2D, 0x65, 0xD6, 0x73, 0x6F, 0x9C, 0xB7, 0x4D, 0xDF, 0x08, 0x13, 0xB9, 0x7C, 0x9C, 0x82, 0xFB, 0xC7, 0xDD, 0xFB, 0x76, 0x7E, 0xB1, 0xF0, 0x1D, 0xBB, 0xC6, 0x5B, 0xDA, 0x26, 0x44, 0xEE, 0x89, 0x8C, 0xE5, 0xCF, 0xCD, 0x93, 0x91, 0x6A, 0x3A, 0x61, 0x3E, 0xC4, 0xE7, 0x29, 0x75, 0xF8, 0x31, 0xC8, 0x97, 0xBB, 0x82, 0xEE, 0x5B, 0xB9, 0x8E, 0x52, 0x5C, 0xBB, 0x83, 0x95, 0x96, 0xC1, 0x5B, 0x77, 0xB6, 0x1B, 0x26, 0x31, 0x38, 0xEF, 0xAA, 0x73, 0xB2, 0x3E, 0x2D, 0x83, 0x89, 0xE2, 0x83, 0xE7, 0x88, 0xCE, 0xEB, 0xFB, 0xDA, 0xB5, 0xEC, 0xF3, 0x8C, 0xBA, 0x59, 0x1F, 0x86, 0xF2, 0xDC, 0x72, 0xE5, 0x55, 0x57, 0xCA, 0x60, 0xF8, 0x50, 0xD7, 0x2E, 0xA1, 0x24, 0xE2, 0xBC, 0xEF, 0x82, 0x69, 0xB1, 0x46, 0xBD, 0x27, 0x10, 0xA1, 0xE4, 0xDA, 0xC4, 0x52, 0x37, 0x05, 0x4C, 0x50, 0x71, 0x7A, 0xFC, 0xC3, 0xED, 0xED, 0x16, 0x46, 0x9A, 0x93, 0x8D, 0xB8, 0x92, 0x59, 0xE1, 0xD8, 0xC4, 0xEE, 0x11, 0x8B, 0x9C, 0x5D, 0x34, 0x7E, 0xEA, 0x32, 0xE1, 0x9E, 0x75, 0x11, 0x2D, 0xBD, 0x54, 0x8C, 0xB5, 0x76, 0x9F, 0x18, 0x81, 0x85, 0x6E, 0xAE, 0xDA, 0x61, 0x96, 0x5B, 0xBA, 0x9F, 0xBB, 0xF5, 0x93, 0x4A, 0x9D, 0x2E, 0x66, 0x73, 0x6E, 0x21, 0x53, 0xCD, 0xDB, 0xCF, 0xF3, 0x9F, 0x3D, 0xC3, 0x6B, 0xB9, 0x82, 0x44, 0x84, 0x3A, 0x88, 0x49, 0x98, 0xE0, 0xFB, 0xB9, 0x0E, 0xDB, 0xEA, 0x27, 0x6A, 0x6D, 0x13, 0xC3, 0xC2, 0xF4, 0xE9, 0x14, 0xB8, 0xFB, 0xCE, 0xC0, 0xE2, 0xF8, 0x78, 0x1D, 0x3C, 0x77, 0x4E, 0xAE, 0xFD, 0xAC, 0x4C, 0x7C, 0x01, 0x23, 0xB2, 0x30, 0xB6, 0x35, 0x83, 0x8D, 0x9D, 0x7B, 0x3A, 0x58, 0xF3, 0x99, 0x38, 0xF6, 0x8E, 0x19, 0x37, 0xC6, 0x4E, 0x9B, 0x4F, 0x19, 0x61, 0xD9, 0x35, 0xA9, 0x10, 0x86, 0x8B, 0x2D, 0xCA, 0x52, 0xE6, 0xD6, 0x7E, 0x49, 0x8E, 0xE9, 0xF2, 0x69, 0x3D, 0x9F, 0x67, 0xDF, 0x32, 0x19, 0xEE, 0xA9, 0x87, 0x10, 0x03, 0x43, 0xE7, 0x9C, 0xB5, 0x7A, 0x8C, 0x18, 0xDA, 0xDA, 0x63, 0x2F, 0xC0, 0xEF, 0xE6, 0x54, 0x27, 0xA3, 0xB6, 0x51, 0x1B, 0x71, 0x9D, 0xBB, 0x6C, 0x78, 0xCB, 0x63, 0xC3, 0x07, 0xC7, 0x82, 0x31, 0x68, 0xC4, 0xD6, 0x79, 0x9C, 0xC3, 0xAA, 0xCB, 0xEB, 0xF3, 0xD3, 0x00, 0x26, 0xCF, 0x0D, 0x4B, 0x9E, 0xC2, 0x81, 0x19, 0x53, 0x1F, 0xDD, 0xD4, 0x5F, 0x77, 0xE0, 0x7E, 0xE6, 0x1E, 0x99, 0xD9, 0xB4, 0x73, 0x07, 0xD6, 0x9D, 0x49, 0xBA, 0x96, 0xB8, 0xB5, 0x60, 0x7C, 0x31, 0x49, 0x01, 0x36, 0xB9, 0xDA, 0xC4, 0x1C, 0x57, 0x71, 0x91, 0xC9, 0xB1, 0x75, 0xBB, 0xD9, 0xEC, 0x09, 0x98, 0x0F, 0xDB, 0xCC, 0x53, 0x88, 0x67, 0x42, 0x37, 0xEC, 0xC1, 0xDF, 0x09, 0xA6, 0x25, 0x9B, 0x22, 0x3F, 0x05, 0xE3, 0x11, 0xEE, 0x63, 0x97, 0x05, 0x90, 0xB4, 0x65, 0x1B, 0xCB, 0xCF, 0xE6, 0x33, 0x62, 0x6E, 0xB1, 0x56, 0x25, 0x99, 0x5D, 0xD0, 0xD3, 0xAC, 0x7E, 0x8A, 0xCF, 0x8A, 0x22, 0x3F, 0x2D, 0x20, 0xC8, 0x4D, 0x5F, 0xF6, 0x26, 0xEC, 0x0B, 0x19, 0xAC, 0x3B, 0xF7, 0xC6, 0x96, 0x59, 0x73, 0xDF, 0xA7, 0x6D, 0x66, 0x26, 0x19, 0x49, 0x1A, 0xE2, 0x8A, 0x45, 0xDB, 0xFE, 0x17, 0xEB, 0x26, 0x8F, 0x8E, 0x42, 0x9D, 0xCD, 0xEE, 0x30, 0x66, 0x4A, 0xD3, 0x11, 0x6A, 0x95, 0x5C, 0x2D, 0x13, 0x29, 0xEE, 0x49, 0xE9, 0x3E, 0x6B, 0xD7, 0x43, 0x3C, 0x97, 0x7C, 0x88, 0x8D, 0x54, 0x99, 0xC3, 0x57, 0xD9, 0xDC, 0xEF, 0xEA, 0x4F, 0x65, 0x0C, 0xF2, 0x12, 0x9F, 0x6C, 0x98, 0x66, 0xEB, 0xF6, 0xA6, 0xDB, 0x5D, 0x38, 0x5A, 0x0A, 0x16, 0xFF, 0xAC, 0xFD, 0xE6, 0xB1, 0x59, 0x79, 0x23, 0x98, 0xC8, 0x47, 0x25, 0xD3, 0xD9, 0xA4, 0xDB, 0x10, 0xEE, 0xAF, 0xDB, 0xAA, 0x05, 0x1C, 0x6B, 0xD2, 0x22, 0x6F, 0xCA, 0x17, 0x3D, 0xD8, 0x15, 0x0B, 0x04, 0x24, 0xE1, 0x50, 0x85, 0x32, 0xE4, 0x0D, 0x66, 0x9C, 0x35, 0x2F, 0x6E, 0xA0, 0xFA, 0x2A, 0x77, 0x5B, 0xE2, 0x72, 0x3F, 0x1B, 0xDD, 0x7D, 0x4C, 0x08, 0xE8, 0xCC, 0x16, 0x81, 0xE6, 0x03, 0xB7, 0xEE, 0x9B, 0x5E, 0x30, 0x1F, 0x0D, 0x67, 0xB9, 0xFF, 0x0C, 0x5C, 0xE1, 0xC0, 0xBA, 0x4A, 0x20, 0x18, 0x8F, 0x0C, 0x8A, 0xAF, 0x9D, 0xA5, 0x41, 0x3A, 0x73, 0xB5, 0x43, 0x0A, 0xA0, 0x60, 0x15, 0xEF, 0xF9, 0xB0, 0x76, 0xDF, 0x48, 0xC6, 0x3B, 0x71, 0xCF, 0xD1, 0xDA, 0xCF, 0x8E, 0x82, 0xCF, 0x25, 0xCF, 0x6F, 0xC8, 0xA4, 0x8E, 0x0E, 0x61, 0xCF, 0x04, 0x3D, 0x27, 0xC8, 0xCE, 0xDD, 0x05, 0x1E, 0xAD, 0x2E, 0xEC, 0xD9, 0xC9, 0x87, 0x33, 0x70, 0x11, 0xEF, 0x00, 0x84, 0x90, 0x0C, 0x41, 0x37, 0xB1, 0x6D, 0x98, 0xE1, 0x10, 0x91, 0xE0, 0x05, 0x11, 0x06, 0x1E, 0x76, 0x74, 0x42, 0xEC, 0x04, 0x92, 0x5F, 0x8D, 0x50, 0x5A, 0x0B, 0xB7, 0xBB, 0xB3, 0x65, 0xD0, 0x91, 0x98, 0x39, 0x03, 0x35, 0xD8, 0xA7, 0x1B, 0x4F, 0x11, 0x0F, 0x4A, 0x10, 0xB6, 0x52, 0x78, 0x0F, 0x0C, 0x61, 0xA6, 0x2C, 0xA2, 0xEF, 0x45, 0x98, 0x54, 0xC9, 0xEB, 0xC4, 0x4C, 0x19, 0xC9, 0xB0, 0x5B, 0x41, 0x44, 0xF4, 0x85, 0x7D, 0x17, 0x00, 0x39, 0x77, 0x9D, 0x95, 0xC9, 0x51, 0xF1, 0x03, 0x0A, 0x04, 0xDD, 0x8B, 0x1C, 0x09, 0x80, 0x3F, 0xF8, 0x70, 0x10, 0xE1, 0x17, 0x00, 0xCC, 0xB8, 0xF4, 0x12, 0x1B, 0xF9, 0x66, 0x30, 0x38, 0x80, 0xD9, 0x73, 0x2B, 0x2A, 0xB7, 0x23, 0xCD, 0x08, 0x23, 0xC2, 0x78, 0x97, 0x51, 0x66, 0x82, 0xF5, 0xEF, 0x61, 0xBC, 0x46, 0x44, 0x27, 0x8B, 0x54, 0xFC, 0x4F, 0x7A, 0x96, 0xBE, 0x7C, 0xDC, 0x50, 0x7D, 0x7B, 0x10, 0x09, 0x51, 0x22, 0xAD, 0x58, 0xFA, 0x14, 0x92, 0xFE, 0xCE, 0x73, 0x1D, 0xF4, 0x89, 0x46, 0x91, 0xC9, 0xF3, 0x20, 0xC1, 0x9F, 0x0E, 0xD6, 0x5C, 0xD1, 0x51, 0x89, 0x71, 0x5B, 0x5A, 0x8D, 0xE5, 0x09, 0x05, 0x59, 0x71, 0x28, 0xCA, 0xA8, 0x36, 0x24, 0x2A, 0xF0, 0x44, 0xAC, 0x99, 0x28, 0x22, 0x23, 0x4F, 0x6C, 0xCB, 0x31, 0xFD, 0x19, 0xBD, 0x65, 0xE1, 0x67, 0x1B, 0xC8, 0x28, 0x42, 0x0D, 0x84, 0x22, 0x67, 0x48, 0xC1, 0xD4, 0x02, 0x0B, 0x07, 0x0D, 0x9E, 0xB1, 0x30, 0x2E, 0x74, 0x6F, 0x33, 0xE0, 0x4C, 0x12, 0x25, 0x2B, 0xD4, 0x59, 0xA5, 0xAF, 0x06, 0xFE, 0x46, 0x7C, 0xD4, 0x45, 0xEF, 0x47, 0xCF, 0xAE, 0x84, 0x3E, 0x46, 0xED, 0x0D, 0x34, 0x30, 0xE9, 0x30, 0xA6, 0xFF, 0x15, 0xB4, 0x75, 0x7F, 0xF7, 0x81, 0x3F, 0x50, 0x82, 0x47, 0x80, 0x41, 0xC8, 0x12, 0x71, 0x9A, 0x91, 0x7D, 0x2A, 0x91, 0x4B, 0x27, 0x91, 0x53, 0xEA, 0xA4, 0xFA, 0x52, 0x36, 0xAC, 0x80, 0x93, 0xE2, 0x09, 0x8B, 0x5C, 0x83, 0x09, 0x89, 0x46, 0xCC, 0xAD, 0x28, 0xE4, 0xE2, 0x42, 0x6A, 0x05, 0x84, 0x57, 0x27, 0x98, 0x20, 0xF6, 0xB9, 0xCF, 0x44, 0x4C, 0xD5, 0x05, 0xC8, 0x3E, 0xBB, 0xAD, 0x49, 0xE5, 0x8C, 0x89, 0x88, 0xED, 0x08, 0x50, 0x62, 0xB4, 0xB1, 0x84, 0x07, 0xCE, 0x13, 0xE9, 0xA3, 0x34, 0x45, 0xF5, 0x25, 0xEC, 0xE7, 0x1B, 0x99, 0x5C, 0x58, 0xD8, 0xE1, 0xCE, 0xBC, 0xB3, 0xAF, 0xC2, 0x80, 0x2D, 0xEE, 0x51, 0xD3, 0x94, 0x0C, 0x98, 0x37, 0xE8, 0x9D, 0xF3, 0x71, 0x51, 0x4C, 0x6B, 0xC2, 0xFD, 0x48, 0x05, 0x3E, 0x89, 0x52, 0x39, 0x24, 0x7A, 0x67, 0x8A, 0x2A, 0x50, 0x43, 0x31, 0x0A, 0x0E, 0x06, 0xE5, 0x49, 0x46, 0x8B, 0x02, 0x86, 0x75, 0x16, 0x34, 0x6B, 0x20, 0x1E, 0x3B, 0x10, 0x33, 0xFA, 0xF4, 0x89, 0x92, 0x0F, 0xC8, 0x99, 0xA0, 0x65, 0x73, 0x98, 0x14, 0x42, 0x69, 0x9B, 0x8C, 0x00, 0x58, 0x76, 0x26, 0x68, 0x4E, 0xDE, 0x33, 0xF7, 0x1D, 0x60, 0x76, 0x60, 0x5E, 0xA3, 0xED, 0x92, 0x10, 0x06, 0x31, 0x8D, 0x85, 0x54, 0x26, 0x59, 0x48, 0x4D, 0xE1, 0x64, 0xAA, 0x1A, 0xFE, 0x20, 0x6B, 0x0F, 0xF2, 0x58, 0xE4, 0x09, 0xCE, 0x66, 0x60, 0x87, 0xCC, 0xF1, 0x6C, 0x4C, 0x09, 0x80, 0x24, 0x5E, 0xC8, 0xE9, 0xD5, 0x81, 0x7E, 0xA1, 0x09, 0xD4, 0x34, 0x94, 0x92, 0xC4, 0x6F, 0x43, 0x76, 0x82, 0x6B, 0x08, 0x3C, 0x66, 0x5D, 0x86, 0xA9, 0xA4, 0x82, 0x53, 0x78, 0x98, 0x9C, 0x21, 0x3F, 0x4A, 0xCB, 0xC0, 0x0D, 0xAC, 0x8B, 0xB7, 0x06, 0x57, 0x41, 0xDA, 0x02, 0xF9, 0x7B, 0xEE, 0x24, 0xDF, 0x22, 0xF1, 0xBC, 0xA2, 0x45, 0x2A, 0xFE, 0xC1, 0x04, 0x46, 0xD1, 0x18, 0x2E, 0xFC, 0xA3, 0xFB, 0x7F, 0xF5, 0xD9, 0x83, 0x45, 0x04, 0x6F, 0xB2, 0x80, 0x9C, 0x16, 0x97, 0x28, 0xF6, 0x73, 0xD9, 0xB1, 0xE6, 0x05, 0x4F, 0xB5, 0xC4, 0x7D, 0x4D, 0xED, 0x3C, 0x47, 0x0C, 0x8E, 0x8C, 0x88, 0xE4, 0x29, 0x2A, 0x64, 0x2D, 0x3F, 0xE4, 0xE3, 0x31, 0xC9, 0x09, 0x2D, 0xBC, 0xE4, 0x4C, 0xC8, 0x45, 0x87, 0x80, 0x47, 0xA8, 0x36, 0x20, 0xC8, 0x4B, 0xE6, 0x6F, 0x89, 0xC0, 0x43, 0x54, 0x35, 0x74, 0x5A, 0x6A, 0xE2, 0x38, 0x4A, 0x0B, 0x70, 0xF6, 0x08, 0x9D, 0x1A, 0xDB, 0x46, 0xEF, 0x27, 0x54, 0x16, 0xA1, 0xB3, 0x04, 0xE9, 0x5B, 0xDC, 0x53, 0x46, 0x76, 0x0B, 0x7F, 0x80, 0x94, 0x60, 0x13, 0x71, 0x32, 0xA9, 0x6B, 0x50, 0x43, 0x08, 0xC8, 0x35, 0x05, 0x24, 0x97, 0xEC, 0x03, 0x6D, 0x8A, 0x34, 0x43, 0x57, 0xC6, 0x12, 0xAD, 0xF5, 0x41, 0x25, 0x0D, 0x11, 0xFA, 0x64, 0x4A, 0x72, 0x6D, 0x50, 0x9D, 0x24, 0x4A, 0x08, 0x89, 0xEE, 0x7D, 0x70, 0x0D, 0x9E, 0x00, 0xB6, 0xA4, 0xCC, 0xAA, 0x94, 0x59, 0x18, 0x2B, 0x37, 0xA8, 0x1A, 0x80, 0x6C, 0x14, 0xAE, 0x13, 0x4E, 0x91, 0x12, 0x62, 0x15, 0xA2, 0x0A, 0xA4, 0x28, 0x8B, 0x54, 0x9C, 0x2E, 0x49, 0xB4, 0x7D, 0x5D, 0xAD, 0x50, 0xB1, 0x65, 0x13, 0x6A, 0x0F, 0x20, 0x82, 0x27, 0x1E, 0x85, 0x26, 0x21, 0x07, 0x88, 0x3D, 0x13, 0x30, 0xD9, 0x12, 0x04, 0x08, 0x04, 0x89, 0x95, 0x2C, 0x02, 0x8A, 0xAC, 0x70, 0x21, 0xCC, 0x72, 0x22, 0xC0, 0xF6, 0x66, 0x7D, 0xA9, 0xB8, 0x88, 0x3E, 0x45, 0xC9, 0xCA, 0x17, 0x88, 0x32, 0x25, 0x16, 0x3B, 0x09, 0x02, 0x57, 0xD9, 0x9D, 0x82, 0x08, 0x77, 0x2B, 0x32, 0x26, 0x05, 0xFA, 0x16, 0x74, 0xA0, 0x82, 0x87, 0xC4, 0x1C, 0x3E, 0x80, 0x68, 0x8F, 0x84, 0x1C, 0x8A, 0xB8, 0x3A, 0xC3, 0xC7, 0x4D, 0x2D, 0x83, 0x26, 0xF6, 0x3B, 0x75, 0x22, 0x3E, 0xC4, 0x71, 0x8C, 0xB0, 0x26, 0x45, 0x45, 0xFE, 0xC5, 0x42, 0xC5, 0xFD, 0x8B, 0x0B, 0xD8, 0xDD, 0xCC, 0x29, 0x4A, 0xEA, 0x22, 0x18, 0x00, 0x4B, 0xD4, 0x0E, 0xFE, 0xBF, 0xE2, 0x7F, 0x56, 0x79, 0x8C, 0x38, 0x41, 0xB9, 0x28, 0x31, 0xE5, 0x80, 0xFB, 0xA8, 0x04, 0x1C, 0x94, 0x65, 0xEB, 0x08, 0xB3, 0xA3, 0x87, 0x65, 0x97, 0xF2, 0xFE, 0xCF, 0x78, 0x3E, 0xC1, 0x92, 0x9E, 0x93, 0xC5, 0x12, 0x63, 0x50, 0x0C, 0x10, 0x44, 0x60, 0x34, 0x12, 0x1D, 0x28, 0x5D, 0xD8, 0x08, 0x7D, 0x2B, 0xED, 0xBE, 0x24, 0xD5, 0x95, 0xAD, 0xA4, 0x26, 0x75, 0x07, 0x25, 0xD3, 0x17, 0xA6, 0x90, 0x36, 0x35, 0x3F, 0xD0, 0x34, 0xE2, 0x01, 0x20, 0x15, 0xC2, 0x31, 0x30, 0x42, 0x38, 0x41, 0x79, 0x23, 0x2B, 0xF7, 0x81, 0x20, 0xA7, 0x3B, 0x8C, 0x77, 0xE0, 0x52, 0x30, 0xAA, 0x11, 0xA5, 0x64, 0xDF, 0x7D, 0x4C, 0xF9, 0xE4, 0x84, 0xFA, 0x0F, 0x19, 0xA4, 0x9B, 0x1E, 0x4B, 0x2F, 0xEA, 0x48, 0xB1, 0x6E, 0x30, 0x9A, 0xCC, 0x1D, 0x72, 0xBA, 0xCD, 0x44, 0x7B, 0x1D, 0x0E, 0x5A, 0xF3, 0x3D, 0x5A, 0x4E, 0xCA, 0x41, 0xDA, 0x06, 0x72, 0x27, 0x82, 0x51, 0xB8, 0xB2, 0x09, 0xB5, 0xC8, 0xE2, 0xA3, 0xD4, 0x10, 0x0C, 0x78, 0x70, 0xE8, 0xA1, 0xE2, 0xDC, 0xA6, 0xBE, 0xBB, 0x59, 0x09, 0x20, 0x48, 0xB9, 0x23, 0xB4, 0x20, 0x7D, 0x82, 0xD8, 0x98, 0x5F, 0x21, 0x3E, 0x65, 0x96, 0xB9, 0x45, 0xDE, 0xD4, 0x3A, 0xB0, 0x3A, 0xD2, 0x55, 0x71, 0xBD, 0xE3, 0x14, 0x06, 0x01, 0xE1, 0x53, 0x50, 0x2A, 0x4D, 0x07, 0x5C, 0x07, 0xB6, 0x03, 0x80, 0x94, 0xE3, 0xDE, 0xB4, 0x42, 0x0B, 0x6A, 0x80, 0x03, 0x98, 0x37, 0x4A, 0x17, 0xB8, 0x74, 0x06, 0xE2, 0xB2, 0x52, 0x32, 0xE6, 0x7E, 0x3C, 0x35, 0xF7, 0x52, 0x49, 0x65, 0xD4, 0x7D, 0x94, 0xD0, 0x6C, 0xBB, 0x3E, 0xAB, 0xD5, 0x6A, 0xC0, 0x89, 0x21, 0xDD, 0xE1, 0x4E, 0x74, 0xC4, 0x37, 0xBB, 0x23, 0x80, 0xA9, 0xB9, 0x23, 0x29, 0x7E, 0xF6, 0xF0, 0xE1, 0xAD, 0x0B, 0x58, 0xF6, 0xBF, 0x2B, 0x0C, 0xDC, 0xBF, 0xC7, 0x9E, 0xDA, 0xBD, 0xDA, 0x1D, 0x1B, 0x7D, 0x43, 0xC2, 0xF0, 0x8B, 0x4E, 0x0E, 0x56, 0x3F, 0xEA, 0xE1, 0x28, 0xA1, 0x56, 0x81, 0xFB, 0x49, 0xC0, 0x09, 0x08, 0xA3, 0xDA, 0x2B, 0x64, 0x06, 0xB6, 0x44, 0x55, 0x9B, 0x70, 0xED, 0xFE, 0x22, 0xA7, 0xAE, 0x3A, 0x81, 0xC8, 0x41, 0xA8, 0xAF, 0x4E, 0x9A, 0x52, 0x89, 0x66, 0x16, 0x29, 0xBD, 0x48, 0xAD, 0x67, 0x08, 0x7B, 0x8F, 0x43, 0x04, 0xAB, 0x13, 0x81, 0xF6, 0x0A, 0xE4, 0xDC, 0x8D, 0xC4, 0x19, 0x1F, 0x7F, 0x0A, 0x44, 0x49, 0xA5, 0x1C, 0x59, 0x47, 0x45, 0xF4, 0x01, 0x59, 0x5D, 0x69, 0x6B, 0x42, 0x10, 0xEB, 0x48, 0x4E, 0x03, 0x87, 0xC8, 0x4E, 0x8F, 0xB7, 0x29, 0xFC, 0x8C, 0x9D, 0xA8, 0xAE, 0x76, 0x17, 0x8A, 0x50, 0x7B, 0x2A, 0x59, 0x25, 0x81, 0x8B, 0xC0, 0x12, 0x86, 0xC4, 0xB0, 0xBB, 0x11, 0x7A, 0x30, 0x0A, 0x77, 0x33, 0x1D, 0xEF, 0x13, 0x91, 0x6B, 0x16, 0xE0, 0x46, 0xC8, 0x6A, 0x50, 0xEB, 0x8D, 0x8A, 0x41, 0xB5, 0x08, 0x71, 0x60, 0x79, 0x14, 0x4F, 0x46, 0x75, 0x81, 0x5A, 0xD2, 0x57, 0x06, 0x7F, 0x84, 0x1A, 0x2F, 0xC4, 0x3B, 0xD2, 0x56, 0xEA, 0xB8, 0x85, 0x25, 0x68, 0xD6, 0xCA, 0x4E, 0x51, 0x3C, 0x1E, 0x84, 0xD6, 0x04, 0x16, 0xC4, 0x7E, 0x4D, 0x8C, 0xEB, 0x7B, 0x61, 0x27, 0x68, 0x60, 0x68, 0xF9, 0x7A, 0x55, 0xA3, 0x82, 0xED, 0x38, 0x66, 0x13, 0xE7, 0xAA, 0x24, 0x22, 0x48, 0x60, 0x91, 0xFB, 0x4A, 0x7A, 0x32, 0x05, 0xD9, 0x92, 0x37, 0x85, 0xC1, 0x76, 0x6F, 0x99, 0x02, 0xA7, 0xAB, 0x04, 0x27, 0x8B, 0x88, 0x7E, 0xA0, 0x8C, 0xDB, 0x40, 0xBD, 0x23, 0xE0, 0x91, 0xA3, 0x6C, 0xD5, 0x01, 0x29, 0x04, 0xC7, 0x12, 0xC7, 0x37, 0xA9, 0x4D, 0x43, 0x1E, 0x6D, 0xC4, 0x44, 0x51, 0x35, 0x57, 0xBB, 0x23, 0xE9, 0x2E, 0x55, 0x19, 0x73, 0x5F, 0x14, 0xDB, 0x67, 0x1E, 0xF0, 0xC9, 0x17, 0xE8, 0x83, 0xB2, 0xFC, 0x22, 0xDC, 0x64, 0x2F, 0xE4, 0x99, 0x4A, 0x70, 0x0F, 0x59, 0xA8, 0x11, 0x50, 0xA4, 0x00, 0x4E, 0xB7, 0xB4, 0x82, 0x9A, 0xFB, 0x75, 0xBB, 0x0A, 0xF6, 0x23, 0xA5, 0x08, 0x62, 0x0F, 0x1F, 0x17, 0x6E, 0x60, 0xAB, 0x77, 0x59, 0x75, 0xE2, 0xAD, 0x2F, 0x42, 0x11, 0x49, 0x7C, 0x32, 0x22, 0x69, 0x97, 0xC1, 0x1C, 0x40, 0xDF, 0xEA, 0x39, 0xAE, 0x50, 0x50, 0x53, 0x72, 0x16, 0x92, 0x16, 0x61, 0x81, 0x84, 0xD6, 0x00, 0x76, 0x45, 0x14, 0x5F, 0x95, 0x89, 0x0B, 0xC7, 0x20, 0x7E, 0x60, 0x2D, 0x54, 0xCA, 0x0B, 0x76, 0x68, 0xEA, 0xF8, 0x8F, 0x30, 0xE8, 0x7E, 0xB9, 0xE3, 0x73, 0x9E, 0x79, 0xD4, 0x04, 0x95, 0x9C, 0x12, 0xDA, 0x35, 0x24, 0x82, 0x7A, 0x13, 0x83, 0xE2, 0x0C, 0x79, 0xED, 0x40, 0x19, 0x54, 0x18, 0x01, 0x14, 0x16, 0x44, 0x21, 0x49, 0x8E, 0x7B, 0xE2, 0xF0, 0xE2, 0x32, 0xCB, 0x43, 0xD1, 0x9E, 0xF6, 0x42, 0xE9, 0x26, 0x9A, 0x39, 0x20, 0x5B, 0x98, 0x8F, 0x44, 0x7D, 0x0A, 0xA5, 0xB0, 0xD1, 0x66, 0x60, 0xE2, 0x1E, 0x29, 0x0B, 0xD8, 0x93, 0xBF, 0x49, 0x6A, 0xD0, 0x02, 0x70, 0x28, 0x79, 0x3D, 0xE2, 0x5B, 0xF6, 0xE6, 0x29, 0x39, 0x18, 0x70, 0xA4, 0xED, 0x7D, 0x45, 0x2C, 0x09, 0x31, 0xA4, 0x5A, 0x19, 0x97, 0xF8, 0xC0, 0x76, 0xBB, 0xE1, 0x70, 0xD6, 0xE1, 0x9A, 0x08, 0x94, 0xEA, 0x8F, 0xF4, 0xEA, 0x72, 0x84, 0x81, 0x36, 0x84, 0x19, 0x1F, 0xA1, 0x11, 0x5A, 0xD2, 0xDA, 0x56, 0xD3, 0x58, 0x89, 0xB2, 0x73, 0xE1, 0x60, 0x62, 0x0C, 0x33, 0x43, 0x50, 0x90, 0x3A, 0xE8, 0x73, 0x40, 0xF6, 0xD1, 0x5D, 0x80, 0x20, 0xA1, 0x84, 0x26, 0x25, 0x0F, 0xF4, 0x87, 0x95, 0xAE, 0x07, 0xA0, 0xCB, 0x20, 0x0B, 0x28, 0xB9, 0xC8, 0x0A, 0xB2, 0x4B, 0x25, 0x27, 0x21, 0x04, 0x8B, 0x0C, 0x04, 0xF1, 0x56, 0x4B, 0x4B, 0x94, 0x29, 0xCF, 0x61, 0xB8, 0x1E, 0xD3, 0x13, 0x1D, 0xA9, 0xBC, 0x87, 0x5C, 0xF5, 0x11, 0xED, 0x6C, 0x9C, 0x24, 0x1B, 0x24, 0x9B, 0x8E, 0x22, 0x54, 0x9A, 0x40, 0x3C, 0xE7, 0xC7, 0x60, 0x90, 0x02, 0xBD, 0x94, 0xDD, 0xBE, 0xBC, 0x15, 0x23, 0xB5, 0x26, 0x1A, 0x32, 0xDB, 0xFB, 0x9B, 0x50, 0x85, 0x19, 0x91, 0xAF, 0xCF, 0x17, 0x96, 0xF5, 0xDF, 0x6A, 0x72, 0xB8, 0x7F, 0xA5, 0xA3, 0x8E, 0xAD, 0xD2, 0x67, 0xC5, 0xD9, 0xCE, 0x6C, 0xC8, 0xBF, 0xA2, 0xF5, 0x54, 0x23, 0x1E, 0xB5, 0x34, 0x70, 0x3C, 0x2E, 0xC9, 0x25, 0x3A, 0x2B, 0x8C, 0x0F, 0x24, 0x79, 0xBB, 0xB0, 0x65, 0x9E, 0xE7, 0xA8, 0xD5, 0x93, 0xA6, 0xA5, 0x1E, 0x09, 0x43, 0x06, 0x6C, 0x93, 0x5C, 0xD6, 0x64, 0x68, 0xEA, 0x91, 0x55, 0x2B, 0xE5, 0x00, 0x3D, 0x49, 0x55, 0x52, 0x2C, 0x8D, 0xD6, 0x1D, 0x95, 0x72, 0xA0, 0xDE, 0x6B, 0x35, 0x1C, 0x7C, 0x70, 0x8D, 0x83, 0x26, 0x7E, 0x41, 0xFE, 0xC4, 0x57, 0x77, 0x1F, 0xD1, 0x47, 0x5C, 0x35, 0x60, 0xDF, 0x2B, 0xA6, 0x80, 0x31, 0x29, 0x20, 0x0D, 0xE8, 0xDF, 0x82, 0xB1, 0x50, 0x58, 0x3A, 0x8A, 0xA8, 0xF0, 0x14, 0xFE, 0xA2, 0x38, 0xCC, 0x19, 0xCB, 0xE2, 0x23, 0x9C, 0x21, 0xAE, 0x86, 0xA5, 0xCA, 0xDB, 0x34, 0x43, 0xA9, 0x51, 0xDE, 0x6C, 0x35, 0x23, 0x96, 0x7F, 0x25, 0xFD, 0xC4, 0x59, 0xB3, 0xCD, 0xA0, 0x4E, 0xAB, 0x0A, 0x4E, 0x77, 0x8D, 0x27, 0x11, 0x34, 0x8B, 0x0F, 0xA9, 0xBC, 0x89, 0xEE, 0x61, 0xC4, 0x2B, 0xC8, 0x01, 0x08, 0x1A, 0x24, 0xCE, 0x27, 0x94, 0x7A, 0x5C, 0x2D, 0xD5, 0xD8, 0x04, 0x26, 0xE4, 0x03, 0xC1, 0x52, 0x54, 0xAA, 0x8A, 0x80, 0x98, 0x11, 0xE5, 0x2F, 0xF0, 0xA6, 0x0A, 0x7B, 0xA4, 0xE1, 0x4B, 0xF7, 0x95, 0xA1, 0x58, 0x06, 0x31, 0xE2, 0xB9, 0x40, 0x05, 0x13, 0xF1, 0xD4, 0x05, 0xE2, 0xCA, 0xB0, 0xAC, 0x08, 0xC9, 0x0D, 0x13, 0xC5, 0x3A, 0xAA, 0xB4, 0x05, 0x55, 0x17, 0x37, 0x2E, 0xA7, 0x1A, 0x76, 0x9B, 0x94, 0x42, 0x95, 0x56, 0xC9, 0x0F, 0xC0, 0x38, 0x47, 0xC1, 0x16, 0xC9, 0xA8, 0x5E, 0x81, 0x99, 0xB5, 0x7F, 0x1A, 0x66, 0xF0, 0x50, 0xB3, 0xDE, 0xD1, 0xBA, 0x32, 0x39, 0x1B, 0xA8, 0xEA, 0x16, 0xA0, 0xFC, 0xA3, 0xA9, 0x6E, 0x25, 0x73, 0x18, 0x8C, 0xF2, 0x96, 0x68, 0xE3, 0xF7, 0xFA, 0xC0, 0x7F, 0x81, 0x11, 0x4C, 0x83, 0x2E, 0x3A, 0x6A, 0x5F, 0xAA, 0xD2, 0x3C, 0xC0, 0xC1, 0x69, 0xF9, 0xD3, 0x61, 0xB9, 0x2F, 0x28, 0x03, 0xE4, 0xC5, 0x62, 0x09, 0xDD, 0x0C, 0x21, 0xE0, 0x7A, 0xA8, 0x83, 0x52, 0xD7, 0x21, 0xC9, 0xD1, 0x63, 0x44, 0xD6, 0xF9, 0xA3, 0x47, 0x1C, 0xD4, 0x01, 0x31, 0x49, 0x76, 0xBE, 0x07, 0xC0, 0x70, 0xD5, 0x34, 0x0D, 0xC5, 0x1D, 0x08, 0x02, 0x4B, 0x22, 0x2F, 0x72, 0xB1, 0x14, 0x56, 0x3F, 0xD2, 0xBD, 0x68, 0xE2, 0x5F, 0x76, 0x1B, 0xC3, 0xAF, 0xED, 0x13, 0x35, 0x4A, 0xB7, 0x6E, 0xD3, 0xF5, 0xD4, 0x22, 0x72, 0x0A, 0x32, 0x39, 0x79, 0x41, 0x37, 0xC4, 0x06, 0x20, 0x78, 0x93, 0x33, 0x81, 0xE9, 0x13, 0x84, 0x04, 0x06, 0xDD, 0xE2, 0xEA, 0x2B, 0xF3, 0xEF, 0x55, 0x8D, 0xCB, 0x48, 0xC7, 0x2A, 0x8B, 0x29, 0x81, 0x2B, 0x35, 0xA4, 0x16, 0x29, 0x2A, 0x64, 0xAA, 0x8F, 0x09, 0xF7, 0x0C, 0xF5, 0xB8, 0x28, 0xBF, 0xCA, 0x02, 0xA0, 0x92, 0xBA, 0x43, 0x4B, 0x5D, 0x39, 0xCC, 0xE1, 0xE6, 0x90, 0xE9, 0xBB, 0x6A, 0xCA, 0xD5, 0x66, 0x92, 0xE2, 0x55, 0xBD, 0x4D, 0x01, 0xD1, 0x41, 0x12, 0x52, 0x69, 0x41, 0xB5, 0xA8, 0x24, 0xF8, 0x16, 0x54, 0x47, 0xD8, 0x43, 0xDD, 0x44, 0x2F, 0x24, 0x0F, 0xC7, 0x33, 0x46, 0x83, 0x37, 0xDE, 0x16, 0x2B, 0xF5, 0xD6, 0x2A, 0x6F, 0xBB, 0x94, 0xE8, 0xDF, 0x24, 0xCC, 0x95, 0x0C, 0x15, 0x62, 0x7E, 0x41, 0x8B, 0x1A, 0xBC, 0x9D, 0xBA, 0x84, 0xAA, 0x05, 0x66, 0x1F, 0xF5, 0xAD, 0x80, 0xD4, 0xF7, 0xA0, 0xE6, 0x51, 0xA3, 0x38, 0x79, 0x89, 0x51, 0xE0, 0x3C, 0xAA, 0x7F, 0x21, 0x64, 0x54, 0x1D, 0xA1, 0x83, 0x0A, 0xC4, 0xE1, 0x6E, 0x52, 0x2D, 0xD4, 0x38, 0xCA, 0xE5, 0xFA, 0x27, 0x34, 0x5A, 0x4A, 0xEF, 0xB8, 0x14, 0x61, 0x73, 0x1A, 0x8E, 0xDF, 0x98, 0xBA, 0x1B, 0x3E, 0xC6, 0x81, 0xEC, 0xE2, 0x5D, 0x3B, 0xA6, 0xB0, 0x06, 0xBE, 0xDC, 0x38, 0xC9, 0x69, 0x25, 0x09, 0xC8, 0xA1, 0x96, 0x06, 0x72, 0x40, 0x2D, 0x63, 0x65, 0xBF, 0x25, 0xD5, 0xF6, 0x4D, 0xD3, 0xCD, 0x69, 0xE0, 0xBC, 0x8F, 0x9B, 0xD4, 0x50, 0x3D, 0x48, 0x25, 0xB5, 0x00, 0x75, 0xB5, 0x07, 0x41, 0x8B, 0x1F, 0x48, 0x2D, 0xF5, 0xBF, 0x94, 0xBC, 0xAA, 0x8B, 0x48, 0x3C, 0x76, 0x83, 0x55, 0x41, 0x78, 0xA2, 0xB7, 0xD6, 0x5C, 0xE3, 0xF3, 0xD5, 0xCF, 0x7A, 0xF4, 0xE4, 0xDA, 0x78, 0x9E, 0x37, 0x9C, 0x7F, 0xE3, 0x19, 0x61, 0x52, 0x36, 0x7A, 0x5C, 0xDD, 0xD6, 0x21, 0x7D, 0xBC, 0xA8, 0x8E, 0x24, 0xB4, 0x48, 0x5C, 0x32, 0x08, 0xDC, 0xB7, 0x96, 0x25, 0x70, 0x85, 0x90, 0x92, 0x22, 0x38, 0xD4, 0x30, 0x15, 0x76, 0xB1, 0x0E, 0x36, 0xF7, 0x9A, 0x45, 0xF8, 0x18, 0x11, 0xD0, 0x1E, 0x92, 0xC0, 0x5F, 0x6B, 0x2E, 0x29, 0xDA, 0x1D, 0x98, 0x28, 0xE9, 0xA6, 0x76, 0x45, 0x06, 0x31, 0x29, 0xA3, 0x52, 0x18, 0x8F, 0xBA, 0xAD, 0xB9, 0x6A, 0xE6, 0xAB, 0x5E, 0x1A, 0x7B, 0x0F, 0x8F, 0x3A, 0x3D, 0xD5, 0x64, 0x6D, 0x55, 0x8D, 0xCD, 0x2A, 0x90, 0x88, 0xD1, 0x9A, 0xF1, 0xF7, 0x59, 0x0E, 0x19, 0xF0, 0xC0, 0x80, 0x0A, 0x16, 0xA6, 0xC3, 0xD8, 0x5D, 0x25, 0x9A, 0xCE, 0x11, 0x6C, 0xF0, 0x01, 0x5C, 0x92, 0x0C, 0x4C, 0xAC, 0xEA, 0x5F, 0x05, 0x28, 0x68, 0x21, 0xB9, 0x85, 0x02, 0xF7, 0x56, 0x00, 0xBF, 0x0A, 0x31, 0x38, 0x02, 0x9A, 0xF8, 0x42, 0xF3, 0x04, 0x91, 0x17, 0x85, 0xE3, 0x84, 0x48, 0x58, 0x19, 0x53, 0xAB, 0x71, 0x49, 0xD1, 0xA7, 0x80, 0x06, 0x1F, 0xFB, 0xDB, 0x99, 0x94, 0x42, 0x43, 0x4F, 0x7A, 0x6A, 0x01, 0xEB, 0x9F, 0x61, 0xF5, 0x0A, 0xFC, 0x77, 0x67, 0xAA, 0x91, 0x7C, 0x7F, 0x55, 0x63, 0x35, 0xD5, 0x28, 0x39, 0x45, 0x36, 0xEC, 0xA7, 0xF5, 0x50, 0xD8, 0x42, 0x99, 0xCA, 0x7F, 0x15, 0x28, 0x4A, 0x7B, 0xAA, 0xAF, 0x5D, 0xD5, 0xB7, 0x46, 0x89, 0x4F, 0x6B, 0x33, 0x01, 0x74, 0xC3, 0x95, 0x85, 0x7E, 0x56, 0xE4, 0xE9, 0x94, 0xE3, 0xED, 0xE9, 0x9E, 0xA7, 0x89, 0xA7, 0xBD, 0xFA, 0x11, 0x9B, 0xC2, 0x34, 0xED, 0x45, 0x0D, 0x40, 0xC1, 0x1A, 0x15, 0xD2, 0xD1, 0xAE, 0x1B, 0xE4, 0xE9, 0x55, 0x9B, 0x77, 0x7F, 0xA0, 0xD2, 0x51, 0x73, 0x4D, 0xE0, 0x95, 0x30, 0xED, 0x16, 0xDB, 0xD4, 0xA7, 0x4F, 0x03, 0xBF, 0x61, 0x6C, 0xF5, 0xCF, 0x40, 0x59, 0x32, 0xEA, 0x52, 0x7D, 0xB0, 0xD1, 0xA3, 0xB4, 0x66, 0xD4, 0x82, 0xEC, 0x94, 0x8D, 0x75, 0x02, 0xA2, 0x13, 0x1F, 0xE9, 0x46, 0x77, 0xD5, 0x35, 0xAD, 0xD9, 0x10, 0x5F, 0xF4, 0x5D, 0xE6, 0xD0, 0xB1, 0x9D, 0xDA, 0xA8, 0x57, 0xA7, 0xF3, 0x10, 0x46, 0xEA, 0x3E, 0x35, 0x92, 0x31, 0xEB, 0x0C, 0xC3, 0x9A, 0x99, 0x43, 0x87, 0x1B, 0xDE, 0x32, 0xB1, 0xA8, 0x78, 0x47, 0xE3, 0xBB, 0x47, 0x45, 0x6E, 0x7C, 0xD3, 0x13, 0xB0, 0xB6, 0x7A, 0x80, 0x58, 0x31, 0xBD, 0x18, 0xA1, 0xD4, 0x6D, 0xA1, 0xA1, 0xEA, 0x45, 0xE9, 0x0F, 0x98, 0x67, 0x82, 0xA7, 0x5B, 0xBA, 0xEE, 0x8B, 0xB4, 0x4A, 0x68, 0x20, 0x8D, 0xEB, 0xCA, 0x48, 0x35, 0x61, 0xCC, 0x14, 0xB7, 0x87, 0x47, 0x51, 0x58, 0x52, 0xA7, 0x6A, 0xE6, 0xAB, 0xEB, 0x61, 0x2A, 0xA1, 0xFF, 0x7C, 0x74, 0xA3, 0x45, 0x2D, 0x29, 0xCA, 0x9A, 0x8A, 0x1A, 0x80, 0x9F, 0x14, 0x29, 0xBF, 0x76, 0x2C, 0xD6, 0xF2, 0x01, 0x86, 0xBD, 0x4E, 0xF2, 0x83, 0x91, 0x79, 0x79, 0x7B, 0xFF, 0x49, 0xFD, 0x54, 0xB6, 0xB6, 0x3F, 0xC7, 0x12, 0x6A, 0x68, 0xD9, 0xB9, 0x04, 0x22, 0x82, 0xEC, 0x8F, 0xB5, 0x49, 0x76, 0x79, 0xCF, 0x22, 0x82, 0x1D, 0x75, 0x04, 0xD9, 0x82, 0x3B, 0x2C, 0x8D, 0x74, 0x4B, 0xF9, 0xDC, 0xD2, 0x81, 0x9E, 0x15, 0xDC, 0xD1, 0xF1, 0x89, 0x84, 0x02, 0x9A, 0xC2, 0xEC, 0x94, 0x14, 0x59, 0x6F, 0x0F, 0x7B, 0xCD, 0xAF, 0x6E, 0xF9, 0xF4, 0x79, 0xA1, 0xB0, 0x18, 0x55, 0x69, 0x62, 0x76, 0x84, 0xF2, 0x71, 0x03, 0xAC, 0xFF, 0x76, 0x40, 0x5C, 0xF1, 0x00, 0x01, 0xA6, 0x51, 0xA4, 0xA4, 0x3C, 0xBB, 0x52, 0xDA, 0x2A, 0x52, 0x2B, 0x35, 0xB1, 0x87, 0xBF, 0x3D, 0xBF, 0x0D, 0xB2, 0x12, 0x92, 0x75, 0xFB, 0x11, 0xD4, 0xD7, 0x8F, 0xBE, 0xD1, 0xF5, 0xB1, 0x49, 0xB8, 0x27, 0x6A, 0x74, 0x87, 0x3B, 0xC1, 0x81, 0x04, 0xF3, 0x46, 0x54, 0xE0, 0x54, 0x2B, 0xF0, 0x86, 0xF7, 0x58, 0x6D, 0xA9, 0x77, 0x8D, 0xE9, 0xDB, 0x0B, 0x65, 0xE8, 0xCA, 0xD5, 0x4C, 0xCE, 0xAC, 0xAC, 0x59, 0xA8, 0x25, 0x7A, 0x4D, 0x4B, 0xFB, 0xDB, 0x57, 0x27, 0x7E, 0xF1, 0x2D, 0x1C, 0x2A, 0xF9, 0x89, 0x33, 0xD1, 0x24, 0x4B, 0x3B, 0x43, 0xD0, 0xE0, 0x5D, 0xC0, 0x26, 0xCD, 0x93, 0xD4, 0x8C, 0xB5, 0x5E, 0x2C, 0x85, 0xEA, 0x88, 0x47, 0x54, 0x66, 0x29, 0x2D, 0xE8, 0x5D, 0x6F, 0x75, 0xE5, 0x76, 0x51, 0x6D, 0x8C, 0x5D, 0xD4, 0x34, 0xBA, 0xCA, 0x9D, 0x59, 0x90, 0x12, 0x84, 0x59, 0x9A, 0xCD, 0xEB, 0xEC, 0x8B, 0x30, 0x27, 0x12, 0x59, 0x19, 0x45, 0xCD, 0xCB, 0x5C, 0xF8, 0x6A, 0xF4, 0xA1, 0x44, 0x52, 0xD4, 0x52, 0x80, 0xB0, 0x3A, 0xD7, 0xF1, 0xDF, 0xD4, 0x91, 0x86, 0x92, 0xF6, 0x5A, 0xFA, 0x77, 0x4A, 0xCD, 0x18, 0xC2, 0xD2, 0xA1, 0x82, 0xF5, 0xA4, 0xA9, 0xE3, 0xD8, 0x8A, 0xBA, 0x3D, 0xD6, 0x38, 0xAA, 0x60, 0xCD, 0x5B, 0xF9, 0xBF, 0xC1, 0x2A, 0x50, 0x29, 0x2E, 0xA9, 0x59, 0x54, 0x63, 0x22, 0x6E, 0xA9, 0xE1, 0x51, 0xE4, 0xE9, 0x74, 0x23, 0xBD, 0xEE, 0x4D, 0x21, 0xAB, 0x44, 0xB5, 0xBA, 0x57, 0xB8, 0x9D, 0x84, 0x28, 0xE5, 0x94, 0x97, 0xA9, 0xBF, 0xBD, 0x87, 0x3F, 0x13, 0x50, 0x2B, 0x80, 0xCC, 0x43, 0x05, 0x94, 0x48, 0x20, 0xC0, 0xC2, 0x41, 0x8C, 0xA6, 0x13, 0x1F, 0x8A, 0xFE, 0x91, 0xA8, 0xDA, 0x84, 0xB7, 0x24, 0x28, 0xAB, 0x96, 0xA6, 0xD1, 0x41, 0x56, 0x56, 0xB9, 0x8B, 0x82, 0xA7, 0xC4, 0xD0, 0xA9, 0x56, 0x3E, 0xCE, 0x5A, 0x8E, 0xA4, 0xA7, 0x94, 0xA9, 0x0A, 0x2F, 0x3B, 0xD1, 0x8A, 0x56, 0x71, 0x74, 0x64, 0xC4, 0xB6, 0x23, 0xD5, 0xB8, 0x8D, 0xC2, 0xAC, 0xD5, 0xD0, 0xA1, 0xFC, 0xF4, 0xF4, 0xBE, 0x9B, 0x1A, 0x1F, 0x27, 0x1F, 0x35, 0x5F, 0x4E, 0x75, 0x51, 0x20, 0x56, 0x02, 0xDA, 0x1C, 0xF1, 0x11, 0x75, 0x65, 0xDC, 0x43, 0x25, 0x70, 0x55, 0x9D, 0x10, 0xC6, 0x5A, 0x2C, 0xC8, 0xBA, 0x6D, 0x6F, 0x23, 0xBA, 0x95, 0x57, 0x42, 0x1C, 0xF0, 0x22, 0x2F, 0x8C, 0x1E, 0x26, 0x7A, 0xDF, 0x4F, 0x57, 0xBA, 0xF8, 0x57, 0x55, 0xA6, 0x0A, 0xC0, 0x47, 0xDF, 0x4E, 0x00, 0x27, 0x4B, 0x61, 0x7D, 0x5B, 0x5A, 0xC2, 0x54, 0x71, 0x1F, 0x76, 0xCC, 0x7C, 0x1F, 0xEB, 0x11, 0x50, 0x3C, 0x53, 0x5B, 0x64, 0x21, 0xA6, 0x8E, 0x98, 0x32, 0xC8, 0x37, 0x8A, 0x03, 0x6F, 0x85, 0x21, 0xEF, 0x31, 0x91, 0x9A, 0x7C, 0x36, 0x1B, 0xC9, 0x0A, 0x83, 0x0E, 0xE4, 0xAF, 0x34, 0x8E, 0x9A, 0xDA, 0x29, 0x53, 0x36, 0xEB, 0x3B, 0x18, 0x53, 0xFD, 0x7F, 0xED, 0x14, 0x96, 0xF0, 0xB2, 0x97, 0x49, 0xD7, 0xE3, 0xBC, 0x58, 0x67, 0xFA, 0x38, 0x7B, 0xFF, 0x85, 0xEE, 0x86, 0x67, 0xC1, 0x0F, 0xD0, 0x81, 0x81, 0xBB, 0x24, 0x89, 0xDA, 0xC5, 0xA0, 0x25, 0x38, 0xE6, 0x06, 0xB0, 0xD2, 0x27, 0x25, 0x68, 0xD3, 0xA9, 0x47, 0xCD, 0xE5, 0xD1, 0xA1, 0x87, 0x4A, 0xD6, 0x3F, 0xDA, 0xCA, 0xEA, 0x47, 0xF5, 0x57, 0xA1, 0xBF, 0x4D, 0x2E, 0x28, 0x9B, 0x30, 0xD0, 0x31, 0x7B, 0x85, 0xCF, 0xED, 0xF8, 0x16, 0xEE, 0x47, 0x83, 0xAB, 0xB3, 0x8C, 0xD8, 0xCE, 0x65, 0xAA, 0x9F, 0x2D, 0xA3, 0x40, 0x47, 0x48, 0x3C, 0xEA, 0x76, 0x90, 0xB9, 0x6F, 0x84, 0x48, 0x9C, 0x6A, 0x32, 0xA2, 0x9A, 0xA6, 0x7A, 0xF1, 0xD0, 0x99, 0xF5, 0x04, 0x88, 0x86, 0x64, 0x87, 0xEA, 0xD1, 0x5D, 0x46, 0x54, 0x77, 0x54, 0x3C, 0xA7, 0x7A, 0x53, 0x67, 0x4C, 0xC2, 0x93, 0x63, 0x76, 0x8C, 0x14, 0xA3, 0x89, 0x58, 0x87, 0x8F, 0xF7, 0x9B, 0x93, 0xED, 0xED, 0x84, 0x01, 0x20, 0xEA, 0x58, 0xB2, 0xA6, 0x37, 0x09, 0xBB, 0x43, 0x25, 0xAA, 0x79, 0xE3, 0x4D, 0x9B, 0x0C, 0xE5, 0x27, 0x20, 0x37, 0x74, 0xCC, 0xD0, 0x07, 0x80, 0x90, 0x48, 0xF9, 0xBD, 0xAC, 0x59, 0x95, 0xD5, 0xEE, 0x06, 0x1D, 0x8C, 0x24, 0x33, 0x49, 0xA3, 0x2F, 0x6C, 0x29, 0x09, 0x92, 0xCA, 0x26, 0xF7, 0x7D, 0x59, 0xEE, 0x28, 0xBD, 0x25, 0x7F, 0x01, 0x12, 0x52, 0x8C, 0x6C, 0x94, 0x01, 0x75, 0x2E, 0xAB, 0x97, 0x01, 0x0C, 0x35, 0x6C, 0x84, 0x7A, 0x2B, 0x2A, 0xE2, 0x61, 0x03, 0x75, 0x49, 0xE5, 0xA5, 0xF8, 0x28, 0x8E, 0x9E, 0xED, 0xA5, 0x07, 0xD5, 0x9D, 0xE7, 0x2D, 0x54, 0x11, 0xEA, 0x59, 0x92, 0x52, 0x3A, 0x51, 0x34, 0xF9, 0x50, 0x17, 0x68, 0xA9, 0x8A, 0xD2, 0xA3, 0x02, 0xDD, 0xAB, 0x88, 0x11, 0x18, 0xCD, 0xA1, 0x03, 0xF8, 0x9D, 0xED, 0x70, 0xAB, 0x3B, 0x22, 0x0B, 0x75, 0x51, 0x94, 0x18, 0x18, 0x91, 0xA2, 0x95, 0x2A, 0x0B, 0x8A, 0x20, 0x36, 0x8F, 0x47, 0xF0, 0xC0, 0x22, 0xDE, 0x8E, 0xEE, 0x7A, 0x53, 0x4D, 0xA5, 0x33, 0xBC, 0xA1, 0xAF, 0x32, 0xCA, 0xF7, 0x4C, 0x61, 0xA8, 0x84, 0x50, 0xB1, 0x33, 0xC8, 0x92, 0xB3, 0x5A, 0x45, 0x8F, 0x5A, 0x7B, 0x3A, 0xA0, 0x1C, 0xEF, 0x99, 0xF8, 0x47, 0xEA, 0x60, 0xD2, 0xEC, 0xA7, 0xA7, 0xC8, 0x5A, 0xAD, 0x52, 0x63, 0x0F, 0x51, 0x5D, 0x7D, 0x0B, 0xBF, 0xB9, 0xA5, 0x98, 0xF4, 0xB5, 0x09, 0x8F, 0xAC, 0x89, 0xD2, 0x74, 0x1B, 0x5D, 0xE4, 0x57, 0x6D, 0x3A, 0x4F, 0x4C, 0xEA, 0x91, 0x68, 0x24, 0xD6, 0xBA, 0xDE, 0xBA, 0xB1, 0xAB, 0x82, 0x92, 0x7E, 0xD0, 0x4E, 0x18, 0x00, 0x1B, 0x13, 0x52, 0x49, 0x67, 0xB4, 0x31, 0x88, 0x0F, 0xCE, 0x75, 0xF2, 0xFD, 0x21, 0xBC, 0xAB, 0xCE, 0x99, 0x83, 0x28, 0x9A, 0x9B, 0xB9, 0xB4, 0x8B, 0x18, 0xAF, 0x76, 0xD5, 0xF4, 0xAD, 0x88, 0x0A, 0x5A, 0x79, 0x21, 0x9F, 0x0E, 0xB9, 0xB3, 0x07, 0x34, 0x19, 0x93, 0x38, 0x79, 0x8F, 0x18, 0x97, 0x6D, 0xCD, 0xBF, 0xDF, 0xE0, 0x40, 0x69, 0xDA, 0xB7, 0xC2, 0xD4, 0xB0, 0x39, 0xD6, 0x20, 0x0D, 0x40, 0xF8, 0x52, 0x05, 0x57, 0x23, 0x09, 0x1E, 0x09, 0x64, 0x4C, 0x8D, 0x4F, 0xA0, 0x8F, 0xA1, 0xAE, 0xAF, 0x11, 0x64, 0x9F, 0x76, 0x32, 0x0D, 0x8B, 0xB0, 0x91, 0xA3, 0x2A, 0xF0, 0xDD, 0xF5, 0xFA, 0x48, 0x12, 0x2A, 0x3A, 0x05, 0x31, 0xB7, 0x4A, 0x0E, 0x23, 0xB4, 0x49, 0xE0, 0x10, 0x74, 0x64, 0x43, 0xAD, 0x7A, 0xFC, 0x54, 0xF3, 0x12, 0x2B, 0xFC, 0xD1, 0xE1, 0x74, 0xE8, 0x94, 0x33, 0x06, 0x85, 0x5E, 0x61, 0x27, 0xE5, 0xB1, 0x4E, 0xDD, 0x7C, 0x1B, 0x48, 0xD8, 0xDF, 0x04, 0xEB, 0x54, 0x0B, 0x99, 0x08, 0x58, 0x6F, 0x6B, 0x60, 0x7F, 0xFA, 0x01, 0x3F, 0x7E, 0x85, 0xE6, 0xEF, 0xBF, 0x12, 0x93, 0x9F, 0x75, 0x1F, 0xF1, 0x41, 0xB5, 0x8A, 0x60, 0x8F, 0x51, 0x54, 0xD4, 0x45, 0x34, 0x64, 0x2A, 0xAA, 0x36, 0xC8, 0x05, 0x9C, 0xF0, 0xF8, 0x0F, 0x91, 0xB5, 0x74, 0x5C, 0x53, 0x65, 0x60, 0x7E, 0x7F, 0xD4, 0x7B, 0xB5, 0x2E, 0x1B, 0x37, 0x06, 0xE8, 0xF5, 0x51, 0x8F, 0xA0, 0x2E, 0x50, 0x47, 0xAD, 0x5C, 0xF9, 0x51, 0x15, 0x7F, 0x7D, 0xBD, 0x28, 0x69, 0x87, 0xBC, 0x6C, 0x9E, 0x68, 0x46, 0xC5, 0x05, 0xF7, 0x0E, 0xC0, 0x1F, 0x9B, 0x02, 0xAD, 0x68, 0x4D, 0xC5, 0xAB, 0xD5, 0x4E, 0x2A, 0x2C, 0x7D, 0x09, 0xC6, 0x2A, 0x58, 0xAF, 0x32, 0x45, 0x6C, 0x4D, 0x2D, 0xCB, 0xDE, 0x49, 0x77, 0xD1, 0x6C, 0x24, 0x7C, 0x6A, 0xEF, 0xC8, 0x9F, 0xD6, 0x65, 0x6C, 0x06, 0x81, 0x8C, 0xDE, 0x83, 0x6F, 0x85, 0x81, 0xC6, 0x7A, 0x66, 0x57, 0x4E, 0x1D, 0x2F, 0x02, 0x39, 0x06, 0xD3, 0x3D, 0xEC, 0xF7, 0x9A, 0x5D, 0xB2, 0x84, 0xA9, 0x4E, 0xA4, 0x4D, 0x74, 0xBC, 0xFA, 0xC4, 0xE9, 0xBB, 0x0E, 0x28, 0x97, 0x71, 0x73, 0xC5, 0x0C, 0x33, 0x3C, 0x08, 0xAA, 0xD6, 0x12, 0x84, 0xDF, 0xE7, 0x7B, 0xFA, 0xE8, 0x63, 0x7A, 0x63, 0xFD, 0x3D, 0x85, 0xE4, 0x5A, 0x48, 0x38, 0x5D, 0x7D, 0x2D, 0xA6, 0x57, 0xEB, 0x8A, 0x8E, 0x72, 0x8E, 0x93, 0xE9, 0xE0, 0xCC, 0x6C, 0x68, 0xB4, 0x3D, 0x75, 0xE1, 0x59, 0x00, 0x50, 0xB3, 0x3A, 0x4B, 0x27, 0x55, 0x36, 0x58, 0x7F, 0x5E, 0xF9, 0x7E, 0x93, 0xDF, 0x8F, 0x36, 0x17, 0x7E, 0x7A, 0x3F, 0x3B, 0x89, 0x19, 0xC4, 0xBF, 0x7F, 0xBF, 0x40, 0x05, 0x38, 0x28, 0x17, 0x75, 0x48, 0x90, 0x74, 0x0A, 0x00, 0xE8, 0x97, 0x4D, 0xF6, 0x7E, 0x20, 0x44, 0x6D, 0x59, 0xDD, 0xAB, 0x55, 0x10, 0xEE, 0x57, 0x56, 0x48, 0xC0, 0x3D, 0xF2, 0xC0, 0x75, 0x0A, 0x6B, 0x3B, 0x83, 0x63, 0xE9, 0x92, 0xB9, 0x5B, 0xDF, 0xE6, 0x3C, 0x0B, 0xAE, 0x7E, 0xCF, 0x48, 0x9B, 0xD5, 0xB8, 0x50, 0xFD, 0xCB, 0x2F, 0xA1, 0xD6, 0xF7, 0xDB, 0x4A, 0x87, 0x72, 0xCB, 0x0E, 0xF8, 0x54, 0x00, 0x96, 0x71, 0x5D, 0xEA, 0x7F, 0x32, 0x0D, 0x2A, 0xAF, 0xA9, 0x64, 0x8E, 0x96, 0xAB, 0xA1, 0x20, 0xA2, 0xAA, 0x02, 0x86, 0x0A, 0x4F, 0x5F, 0x5A, 0x62, 0x1A, 0x1D, 0x3F, 0xC0, 0x8D, 0xFA, 0x76, 0x1B, 0xE4, 0x31, 0xA5, 0x31, 0xF0, 0x27, 0x3E, 0x85, 0x8E, 0x0A, 0x25, 0x30, 0x6E, 0x0F, 0x4F, 0x45, 0xC7, 0xBD, 0x25, 0x59, 0x09, 0xFF, 0x5D, 0x27, 0xE2, 0xBF, 0x79, 0xFE, 0xA7, 0x81, 0xD4, 0x48, 0xEE, 0xEE, 0xFF, 0x01, 0x5C, 0x24, 0xED, 0x9E, 0x3F, 0x94, 0x9F, 0x81, 0x00, 0x00, 0x02, 0xFA, 0x50, 0x4C, 0x54, 0x45, 0x47, 0x70, 0x4C, 0xC0, 0xB9, 0xB0, 0x90, 0x88, 0x74, 0x6A, 0x5E, 0x4B, 0x78, 0x71, 0x68, 0x78, 0x6F, 0x66, 0x74, 0x69, 0x5E, 0x97, 0x92, 0x85, 0xB6, 0xAD, 0x9F, 0xC9, 0xC3, 0xB9, 0xB8, 0xB1, 0xA4, 0x82, 0x7A, 0x74, 0x8D, 0x7E, 0x72, 0xC9, 0xC1, 0xB1, 0x88, 0x7C, 0x6F, 0xAD, 0xA5, 0x96, 0x65, 0x58, 0x4D, 0xC7, 0xBA, 0xAB, 0xCE, 0xCC, 0xC8, 0xA9, 0xA2, 0x9C, 0x79, 0x6B, 0x61, 0xD4, 0xD0, 0xCA, 0x78, 0x67, 0x59, 0x94, 0x91, 0x80, 0xCB, 0xBA, 0xA7, 0x6C, 0x64, 0x52, 0x99, 0x91, 0x84, 0xB4, 0xAB, 0x9C, 0xA2, 0x9C, 0x8B, 0xB0, 0xA6, 0x94, 0xA0, 0x96, 0x82, 0x6E, 0x6D, 0x59, 0xC8, 0xC3, 0xBD, 0xA5, 0xA1, 0x84, 0x89, 0x87, 0x68, 0x83, 0x85, 0x62, 0x7B, 0x6F, 0x60, 0x62, 0x54, 0x4A, 0x6F, 0x67, 0x53, 0xBB, 0xAB, 0x97, 0xC0, 0xB0, 0x9D, 0xAC, 0x9F, 0x85, 0x85, 0x79, 0x71, 0x97, 0x93, 0x8F, 0xC7, 0xBD, 0xB6, 0x8F, 0x9D, 0x7F, 0x31, 0x2B, 0x29, 0xA8, 0x9E, 0x8B, 0xC7, 0xBF, 0xB5, 0x93, 0x8B, 0x73, 0x5E, 0x5A, 0x40, 0x81, 0x6F, 0x5E, 0xB1, 0x9E, 0x91, 0x8A, 0x7E, 0x6A, 0xA5, 0x96, 0x7F, 0x72, 0x69, 0x50, 0xAF, 0xA2, 0x8A, 0x48, 0x40, 0x3A, 0x51, 0x4E, 0x4C, 0x3C, 0x33, 0x30, 0x52, 0x46, 0x3B, 0x86, 0x77, 0x70, 0xC3, 0xBC, 0xA6, 0x51, 0x45, 0x40, 0xC7, 0xBB, 0xAA, 0x56, 0x45, 0x3D, 0xC0, 0xB8, 0x9C, 0xCB, 0xBB, 0xAF, 0xB3, 0x9B, 0x87, 0x61, 0x5E, 0x4E, 0x93, 0x88, 0x7F, 0xB3, 0xA3, 0x89, 0x6B, 0x71, 0x57, 0x6A, 0x71, 0x52, 0x60, 0x6A, 0x52, 0x7D, 0x77, 0x6F, 0xBC, 0xAB, 0x97, 0x8A, 0x7A, 0x65, 0xB9, 0xAD, 0x97, 0x2E, 0x6D, 0x6E, 0x56, 0x44, 0x36, 0x2E, 0x23, 0x19, 0x87, 0x6D, 0x5D, 0x99, 0x7F, 0x6E, 0x69, 0x59, 0x42, 0x67, 0x57, 0x3F, 0x6B, 0x57, 0x49, 0x64, 0x54, 0x3E, 0xAC, 0x93, 0x81, 0x8C, 0x75, 0x66, 0x6E, 0x63, 0x48, 0x5E, 0x4F, 0x3B, 0x5D, 0x4A, 0x3A, 0x62, 0x51, 0x41, 0x9C, 0x8F, 0x73, 0x51, 0x41, 0x2D, 0x54, 0x4A, 0x31, 0x7E, 0x66, 0x59, 0x78, 0x67, 0x51, 0x67, 0x5C, 0x45, 0x66, 0x50, 0x42, 0x7E, 0x6F, 0x52, 0x68, 0x57, 0x46, 0x97, 0x7C, 0x6A, 0xA0, 0x87, 0x78, 0xAD, 0x96, 0x86, 0x65, 0x54, 0x44, 0x71, 0x5F, 0x4A, 0x81, 0x72, 0x58, 0x95, 0x79, 0x68, 0x6B, 0x5D, 0x44, 0xB2, 0x99, 0x88, 0x8E, 0x80, 0x66, 0x9A, 0x8A, 0x73, 0x71, 0x60, 0x51, 0x62, 0x4E, 0x40, 0x5A, 0x4A, 0x36, 0x5E, 0x4E, 0x37, 0xB3, 0xA1, 0x89, 0x6A, 0x53, 0x45, 0x6C, 0x5B, 0x48, 0x7C, 0x62, 0x52, 0x94, 0x7F, 0x6E, 0x59, 0x50, 0x39, 0x6E, 0x5E, 0x49, 0x81, 0x6E, 0x60, 0x42, 0x38, 0x26, 0x87, 0x76, 0x5E, 0x83, 0x77, 0x5C, 0x58, 0x46, 0x34, 0x5F, 0x4C, 0x3E, 0x43, 0x33, 0x27, 0x5A, 0x4A, 0x3C, 0x5A, 0x45, 0x3A, 0x33, 0x2C, 0x1F, 0x77, 0x5E, 0x4E, 0x6D, 0x5C, 0x50, 0xA7, 0x91, 0x82, 0x3E, 0x32, 0x24, 0x8F, 0x79, 0x67, 0x5E, 0x4F, 0x42, 0x8A, 0x70, 0x60, 0x7A, 0x6A, 0x5B, 0x99, 0x81, 0x71, 0x9D, 0x86, 0x75, 0xA6, 0x8D, 0x7D, 0x93, 0x85, 0x68, 0xB8, 0x9F, 0x8C, 0x55, 0x41, 0x34, 0xBC, 0xA3, 0x92, 0x47, 0x37, 0x2C, 0x76, 0x61, 0x53, 0x72, 0x62, 0x4D, 0x81, 0x6A, 0x5B, 0x50, 0x43, 0x31, 0x39, 0x2F, 0x26, 0xA3, 0x8A, 0x79, 0xAD, 0x9E, 0x86, 0x7B, 0x6D, 0x55, 0x94, 0x7B, 0x6B, 0x56, 0x49, 0x36, 0x8E, 0x73, 0x60, 0x87, 0x72, 0x64, 0x84, 0x68, 0x59, 0x92, 0x77, 0x66, 0x52, 0x46, 0x35, 0x8A, 0x79, 0x60, 0x89, 0x7E, 0x61, 0xAE, 0x97, 0x80, 0xBE, 0xAC, 0x96, 0x4A, 0x3B, 0x28, 0x3D, 0x2C, 0x1C, 0x4F, 0x3C, 0x2E, 0x6D, 0x59, 0x44, 0xBE, 0xA6, 0x96, 0x26, 0x1A, 0x13, 0x90, 0x76, 0x63, 0x47, 0x40, 0x2D, 0x98, 0x87, 0x78, 0x74, 0x69, 0x4D, 0x4B, 0x3F, 0x35, 0x1F, 0x1E, 0x19, 0x63, 0x59, 0x4B, 0x77, 0x65, 0x57, 0x97, 0x87, 0x6A, 0xA4, 0x94, 0x7B, 0xA2, 0x91, 0x77, 0x7B, 0x6B, 0x4F, 0x92, 0x81, 0x67, 0x95, 0x86, 0x6E, 0xC0, 0xAA, 0x9C, 0x1E, 0x14, 0x0F, 0x59, 0x4D, 0x34, 0x9C, 0x8B, 0x79, 0x75, 0x5D, 0x50, 0x7E, 0x66, 0x54, 0xB4, 0xA7, 0x8B, 0xA2, 0x8C, 0x7B, 0x16, 0x0E, 0x0A, 0x74, 0x59, 0x4A, 0xA7, 0x99, 0x7D, 0x99, 0x84, 0x6D, 0x70, 0x59, 0x4B, 0x86, 0x73, 0x58, 0xAF, 0xA3, 0x89, 0x1D, 0x17, 0x15, 0x68, 0x5D, 0x4A, 0xA1, 0x9A, 0x7A, 0x9E, 0x93, 0x78, 0x36, 0x26, 0x1D, 0xCA, 0xB9, 0xA5, 0x82, 0x67, 0x55, 0x9E, 0x84, 0x72, 0xB5, 0x9B, 0x8A, 0xAC, 0xA6, 0x87, 0x5F, 0x58, 0x3E, 0xAF, 0xA9, 0xA3, 0xA8, 0x98, 0x88, 0xB5, 0xAA, 0x8C, 0x71, 0x6B, 0x5D, 0xAE, 0xA4, 0x9C, 0x7E, 0x78, 0x72, 0x75, 0x70, 0x6E, 0x6E, 0x65, 0x50, 0x87, 0x7C, 0x71, 0xB8, 0xAD, 0x90, 0x55, 0x47, 0x3D, 0x8A, 0x78, 0x6B, 0x69, 0x4B, 0x41, 0x7B, 0x78, 0x58, 0x71, 0x68, 0x54, 0x8F, 0x89, 0x69, 0x73, 0x77, 0x55, 0xC3, 0xB4, 0xA0, 0x40, 0x3B, 0x2D, 0x80, 0x75, 0x67, 0x5E, 0x5F, 0x41, 0x4B, 0x47, 0x46, 0xAB, 0x9C, 0x83, 0x5C, 0x54, 0x48, 0x94, 0x8E, 0x8B, 0x65, 0x6A, 0x49, 0x5E, 0x42, 0x38, 0xBE, 0xBC, 0xBD, 0x99, 0x90, 0x89, 0x99, 0x96, 0x94, 0x32, 0x17, 0x13, 0x84, 0x80, 0x7F, 0x3C, 0x36, 0x36, 0x42, 0x1E, 0x1A, 0x82, 0x87, 0x67, 0x9E, 0x9D, 0x9C, 0xB3, 0xB0, 0xB1, 0x7D, 0x58, 0x49, 0xC8, 0xD1, 0x08, 0xD9, 0x00, 0x00, 0x00, 0x4F, 0x74, 0x52, 0x4E, 0x53, 0x00, 0x04, 0xB6, 0xCA, 0x1D, 0x30, 0x8E, 0x0A, 0x15, 0x0E, 0x5D, 0x93, 0x7E, 0x62, 0x49, 0x27, 0xA5, 0x98, 0x6D, 0x68, 0xAC, 0x31, 0xC8, 0x45, 0xF8, 0xBA, 0x86, 0x84, 0x34, 0xAA, 0xE5, 0x70, 0x95, 0x95, 0xC1, 0xFD, 0xFB, 0xDF, 0xEB, 0xDC, 0x64, 0xF3, 0xBB, 0x76, 0x45, 0x3C, 0xFC, 0x59, 0x7B, 0xF3, 0xF1, 0xDF, 0x94, 0xDF, 0xBB, 0xD7, 0xEB, 0xAA, 0x40, 0xDE, 0xE0, 0x63, 0x30, 0x7F, 0xB8, 0xEF, 0x6E, 0xB2, 0xCA, 0x8F, 0x96, 0xC5, 0x9B, 0x82, 0x59, 0xE6, 0xF1, 0xFB, 0x9B, 0x80, 0x90, 0xD3, 0xAB, 0x00, 0x00, 0x05, 0xFC, 0x49, 0x44, 0x41, 0x54, 0x18, 0x19, 0xED, 0xC1, 0x65, 0x50, 0x9B, 0x07, 0x18, 0x00, 0xE0, 0x17, 0x0A, 0x04, 0xEA, 0xEE, 0xDE, 0xCE, 0xDD, 0xDD, 0x5D, 0xEF, 0x8B, 0x7B, 0x88, 0xBB, 0x12, 0x20, 0x42, 0x8C, 0x10, 0x2C, 0x48, 0x20, 0x10, 0x24, 0xB8, 0x53, 0xDC, 0xDD, 0xA1, 0x14, 0x2B, 0x50, 0xA1, 0xBA, 0x96, 0xCA, 0x2A, 0x5B, 0x3B, 0x77, 0xBB, 0xDB, 0x6E, 0x7F, 0x37, 0xD6, 0x8F, 0xED, 0xCF, 0xEE, 0xB6, 0xE7, 0x81, 0xFF, 0x8C, 0xC0, 0x55, 0xF0, 0x8F, 0xF8, 0x3C, 0x76, 0x71, 0x07, 0x06, 0xFE, 0x81, 0x5D, 0xD7, 0xC2, 0x67, 0x9E, 0x0C, 0x84, 0xBF, 0xCD, 0xFF, 0xBD, 0x19, 0xD6, 0xCC, 0x99, 0x6D, 0x00, 0x81, 0xB0, 0x78, 0x9B, 0x9E, 0xD8, 0xEB, 0xFB, 0xC4, 0xB5, 0x59, 0xCD, 0xCC, 0xCC, 0x1E, 0x58, 0x79, 0xD7, 0x1B, 0x81, 0x80, 0x16, 0x06, 0x7E, 0xB7, 0xCB, 0x7D, 0x65, 0x1F, 0x4B, 0xD8, 0x3B, 0x3B, 0x38, 0x7B, 0x62, 0x0F, 0x6C, 0xEC, 0x0C, 0xBF, 0x13, 0xD0, 0xC1, 0x2C, 0xBF, 0x6B, 0x3B, 0xFC, 0xC6, 0xEF, 0x61, 0x75, 0xBE, 0xBB, 0x43, 0x36, 0xAB, 0xE9, 0xD5, 0x8C, 0x3C, 0xEE, 0x87, 0x1B, 0x3B, 0xB3, 0x03, 0x03, 0x68, 0xF8, 0xF9, 0x0E, 0x0E, 0x3E, 0xB4, 0x29, 0x08, 0x60, 0x35, 0xAD, 0x57, 0xD9, 0x53, 0xC1, 0xD5, 0x9C, 0x66, 0xCD, 0x4E, 0xED, 0x7C, 0xA4, 0x67, 0x6A, 0x64, 0xE4, 0x4E, 0x40, 0x23, 0xC0, 0xC4, 0xBD, 0x70, 0xA1, 0x69, 0xE7, 0x03, 0xFE, 0xC2, 0xE9, 0x5E, 0xA5, 0xBA, 0xC2, 0xAA, 0x49, 0xE9, 0xCD, 0xCB, 0xEB, 0x65, 0x75, 0xE5, 0x9D, 0x38, 0xFA, 0x32, 0xA0, 0xB1, 0x3A, 0x52, 0x99, 0x7C, 0xE3, 0xF2, 0x7C, 0xFE, 0x34, 0x53, 0x49, 0x55, 0xF6, 0xD8, 0x6C, 0x5D, 0x05, 0x9D, 0x9F, 0x8E, 0x59, 0xC7, 0xA6, 0x2E, 0x9E, 0x39, 0xB0, 0x02, 0xD0, 0x58, 0x42, 0x0F, 0xBD, 0xDA, 0xE6, 0x20, 0xD2, 0x4C, 0xD2, 0x0A, 0x57, 0x16, 0xAB, 0x5C, 0xDD, 0x65, 0xEC, 0x9C, 0x62, 0xE5, 0x9F, 0x98, 0xBA, 0x38, 0xF2, 0xE9, 0x52, 0x40, 0x63, 0x89, 0x94, 0x2E, 0x65, 0x0F, 0x84, 0x91, 0xB3, 0x08, 0xD6, 0x62, 0xB9, 0xDB, 0xE6, 0x96, 0x68, 0x34, 0xA7, 0x6D, 0xEA, 0xBC, 0xB1, 0xB1, 0x03, 0x27, 0x9E, 0x07, 0x34, 0x96, 0xD0, 0x09, 0x11, 0xC1, 0x61, 0xC4, 0x90, 0xAC, 0xF6, 0xA6, 0x82, 0xF2, 0x84, 0xFC, 0x72, 0xCA, 0xC1, 0xAE, 0xF1, 0x62, 0x4D, 0x5E, 0x65, 0x5E, 0xF8, 0x47, 0x41, 0x80, 0xC6, 0x12, 0x22, 0x99, 0xCD, 0x26, 0xD0, 0xA5, 0xC9, 0x59, 0x4D, 0x97, 0xAD, 0x66, 0x75, 0xCF, 0x20, 0xA5, 0xCB, 0xA8, 0xE9, 0x0C, 0x0F, 0x1F, 0x99, 0xDB, 0x0C, 0xA8, 0x6C, 0xCC, 0x22, 0x0C, 0x44, 0xD2, 0x19, 0x32, 0x6E, 0x08, 0xD5, 0xAC, 0x2E, 0xB6, 0x1A, 0x5D, 0x92, 0xC1, 0xF1, 0x5C, 0x4A, 0x65, 0x5A, 0x65, 0xE5, 0x1D, 0x80, 0xCA, 0x16, 0x2E, 0x81, 0x8E, 0x84, 0xC6, 0xD5, 0x0E, 0x67, 0x9A, 0x13, 0x12, 0x8A, 0xC7, 0x0B, 0x8C, 0xF1, 0xAA, 0x70, 0x71, 0x6E, 0x65, 0x9A, 0xAA, 0xF2, 0x45, 0x40, 0x25, 0x20, 0x8E, 0x89, 0x20, 0x91, 0xA1, 0xB5, 0x4D, 0x66, 0x73, 0x69, 0x02, 0xB5, 0x40, 0x9C, 0x40, 0x91, 0x78, 0x28, 0x92, 0x34, 0xCF, 0x9C, 0x67, 0x0D, 0xA0, 0xB2, 0x32, 0x84, 0x81, 0xA4, 0x07, 0x87, 0xB6, 0x27, 0xA4, 0x14, 0x1F, 0x6C, 0xE2, 0xE0, 0x5D, 0x8A, 0xCA, 0xF8, 0x3A, 0x09, 0xC5, 0x93, 0x96, 0x76, 0x60, 0x05, 0xA0, 0x73, 0xBB, 0x16, 0x09, 0x0E, 0x26, 0x67, 0x95, 0xE6, 0x1A, 0xA9, 0xA7, 0xA9, 0x29, 0x29, 0xC7, 0x28, 0xF1, 0xF1, 0x62, 0xCF, 0x5C, 0x9D, 0xE7, 0xE8, 0x06, 0x40, 0x67, 0xA3, 0x9D, 0xE1, 0x4D, 0x97, 0x66, 0x1B, 0x0F, 0x9E, 0x76, 0x89, 0x0B, 0x5C, 0xA9, 0x86, 0xDC, 0x54, 0x89, 0xC4, 0xE3, 0xE9, 0xAB, 0xDB, 0xEC, 0x03, 0xE8, 0xAC, 0x97, 0x32, 0x90, 0xF4, 0x89, 0x6C, 0x0E, 0xFE, 0xA0, 0x38, 0x41, 0x2C, 0x56, 0x15, 0xC5, 0xE7, 0xA6, 0x52, 0xE6, 0xEA, 0xEA, 0xFA, 0x5E, 0x01, 0x94, 0x96, 0x93, 0x18, 0x91, 0x48, 0x26, 0x89, 0xD4, 0x5E, 0x2C, 0xA6, 0xBA, 0xA8, 0xA9, 0x0D, 0xF1, 0x12, 0x3C, 0x45, 0x95, 0xD6, 0xF7, 0xD1, 0x52, 0x40, 0xE9, 0x39, 0x9D, 0x3D, 0x5A, 0x3B, 0x44, 0x8A, 0xB6, 0x0B, 0xA9, 0xD9, 0x45, 0xD4, 0x5C, 0xBC, 0x48, 0x51, 0xEA, 0x3A, 0x4E, 0xE9, 0xEB, 0x5B, 0x05, 0x28, 0x6D, 0x9A, 0xE0, 0xDB, 0x0F, 0x91, 0xE2, 0x2E, 0x39, 0x90, 0x89, 0x6C, 0xAA, 0xD9, 0xA8, 0x28, 0xC2, 0x19, 0x4A, 0xA8, 0xF8, 0xB4, 0x3B, 0x00, 0xAD, 0x55, 0x99, 0x5A, 0xC6, 0x97, 0xA3, 0x9F, 0x9C, 0x8F, 0xAA, 0xB1, 0x67, 0x73, 0x92, 0x0A, 0x1A, 0x92, 0x26, 0xDA, 0x2F, 0x27, 0xB9, 0x54, 0x8F, 0x02, 0x5A, 0x3E, 0xC9, 0x7C, 0xFE, 0x0F, 0xA3, 0x87, 0x3F, 0xF9, 0x16, 0xD1, 0x72, 0x38, 0x45, 0x38, 0xDC, 0x97, 0xF7, 0x9F, 0xFF, 0xCE, 0x9C, 0x9B, 0x76, 0x2F, 0xA0, 0xF6, 0x08, 0x5F, 0x1B, 0xDD, 0xDA, 0x7A, 0xF8, 0x43, 0xA9, 0x8E, 0x93, 0x64, 0x2C, 0xFA, 0xE5, 0xDB, 0xC3, 0xDF, 0x7F, 0xFF, 0xA1, 0xF8, 0x49, 0x1F, 0x40, 0x6D, 0x3D, 0x7F, 0x88, 0x13, 0x17, 0x62, 0x1E, 0xD2, 0x91, 0x0C, 0x8A, 0x52, 0xBC, 0xA2, 0xA6, 0xB5, 0x35, 0xA7, 0x64, 0xEE, 0x51, 0x40, 0x6F, 0x19, 0x89, 0xD4, 0x8E, 0x4B, 0x4A, 0xA1, 0xE2, 0x38, 0x2E, 0x5C, 0x81, 0xCA, 0x83, 0x8F, 0xBB, 0x94, 0x2D, 0x89, 0x7F, 0x6C, 0x1D, 0xA0, 0xB6, 0x52, 0x28, 0xD4, 0x0D, 0xE1, 0x4A, 0x0A, 0x70, 0xB8, 0xA4, 0x22, 0x3C, 0x45, 0x35, 0x97, 0x92, 0x92, 0xAA, 0x32, 0xBB, 0xD7, 0x00, 0x6A, 0x98, 0x10, 0xAD, 0x76, 0x22, 0x36, 0x16, 0x67, 0x28, 0x51, 0x34, 0x88, 0xF0, 0x2A, 0x49, 0xAA, 0x38, 0x15, 0xDF, 0x7D, 0xE6, 0x7D, 0x40, 0x6F, 0xBD, 0xDD, 0x4E, 0x22, 0x19, 0x62, 0x4B, 0x0C, 0xA2, 0xD2, 0xE3, 0x22, 0x71, 0x2A, 0xBE, 0x54, 0xC4, 0xEF, 0xEA, 0x7C, 0x10, 0xD0, 0xF3, 0x4F, 0x74, 0x08, 0x49, 0xB1, 0x87, 0x0C, 0x1C, 0xD1, 0x30, 0x5E, 0x24, 0x3A, 0xAE, 0x28, 0x9A, 0x38, 0x34, 0x9E, 0x17, 0x08, 0x8B, 0xB0, 0x3A, 0x3A, 0x5A, 0xCA, 0xD4, 0x1D, 0x2B, 0x69, 0x38, 0x86, 0x3F, 0xAE, 0x28, 0xC5, 0xE9, 0x26, 0xE4, 0xAC, 0x9D, 0xB0, 0x18, 0x98, 0xDB, 0xD2, 0x1D, 0x4C, 0xAD, 0x6E, 0x38, 0xA9, 0x41, 0x81, 0x2B, 0x19, 0x8E, 0xE6, 0x4F, 0x96, 0xAB, 0x37, 0xC1, 0xA2, 0xF8, 0xBD, 0x84, 0x20, 0x8E, 0xB8, 0xCC, 0xEC, 0x84, 0x14, 0x7C, 0xAC, 0x37, 0xAA, 0xBB, 0x42, 0xEE, 0x0B, 0x8B, 0xF4, 0x42, 0x06, 0x82, 0x20, 0x21, 0xB8, 0xA4, 0x63, 0xB1, 0xE9, 0x19, 0xC4, 0xC9, 0x8E, 0x7D, 0xF7, 0xC1, 0xE2, 0x3C, 0xFD, 0xCC, 0x91, 0xA8, 0x9A, 0x74, 0xFE, 0x90, 0xE1, 0x50, 0x34, 0x12, 0x4C, 0x2E, 0xBF, 0x76, 0x7D, 0x3B, 0x2C, 0x86, 0xDF, 0x53, 0xF7, 0xD7, 0x63, 0x8F, 0x54, 0x67, 0xA4, 0xF3, 0x75, 0x7C, 0x69, 0x71, 0x99, 0xAC, 0x81, 0x66, 0x7B, 0x1C, 0xD0, 0xF3, 0xF3, 0x7F, 0xD6, 0x89, 0xED, 0xAF, 0x3F, 0x82, 0xC5, 0x56, 0x27, 0x46, 0x65, 0x26, 0x36, 0xB5, 0x47, 0x58, 0xBB, 0x2B, 0x1E, 0x5A, 0x05, 0xE8, 0x60, 0x5E, 0xBB, 0xCD, 0x5E, 0x56, 0x93, 0x11, 0x85, 0xED, 0xCF, 0xC1, 0x62, 0x5B, 0x32, 0x32, 0xDA, 0xA4, 0x24, 0xA4, 0x70, 0xBE, 0xF9, 0xBA, 0xDA, 0x17, 0x50, 0xF1, 0x7F, 0x98, 0xEE, 0x0D, 0xAD, 0xCA, 0xF1, 0x46, 0xD6, 0x38, 0xB1, 0x39, 0xCE, 0x28, 0x6C, 0x8B, 0xC5, 0x91, 0x38, 0x9D, 0x1C, 0x61, 0x33, 0xB9, 0x2B, 0xF6, 0x62, 0x00, 0x85, 0x37, 0xC9, 0x91, 0x76, 0xBD, 0xB7, 0x8A, 0x1D, 0x4A, 0x2F, 0xCB, 0x68, 0xFD, 0xB8, 0x1F, 0x9B, 0xD3, 0xD8, 0xB6, 0xFF, 0xEA, 0x7E, 0xCB, 0x7C, 0xB9, 0xAD, 0xBC, 0x63, 0x2F, 0x06, 0x6E, 0x6D, 0x0B, 0x33, 0x32, 0x84, 0x19, 0xC3, 0x0E, 0x0B, 0x0B, 0x9D, 0xA6, 0xC7, 0xB4, 0x8E, 0xC6, 0x7C, 0x6C, 0xF9, 0xAC, 0xED, 0xEC, 0x55, 0x6F, 0x19, 0x6D, 0x52, 0xDE, 0xD3, 0xF3, 0x16, 0xDC, 0xD2, 0xDB, 0x8D, 0xB5, 0xB5, 0x8E, 0x98, 0xA8, 0xE0, 0x01, 0x69, 0x84, 0x8E, 0xA7, 0x8F, 0xE9, 0xB7, 0xB4, 0x94, 0xD1, 0xF3, 0xCF, 0x26, 0x3B, 0x2D, 0x4C, 0x9A, 0x95, 0x3B, 0xB9, 0xEF, 0x01, 0xB8, 0x85, 0x5D, 0x37, 0x4D, 0x99, 0x99, 0xCC, 0x28, 0x67, 0x55, 0xB0, 0x37, 0x8C, 0xA8, 0x9F, 0xF7, 0x36, 0x32, 0xE9, 0x04, 0x9A, 0x72, 0xBE, 0x0C, 0xDB, 0x5F, 0x66, 0x92, 0xC9, 0x9B, 0xBF, 0xE8, 0xD8, 0x0A, 0x7F, 0xC1, 0x3F, 0x60, 0x0B, 0x8F, 0x97, 0x4F, 0x23, 0x27, 0x1E, 0xA9, 0xAE, 0x76, 0xB2, 0xAB, 0x08, 0x52, 0x13, 0xC3, 0xCB, 0xD3, 0xEB, 0x09, 0x27, 0x1B, 0xB1, 0x87, 0x47, 0xEB, 0x43, 0xA5, 0x44, 0xDA, 0x15, 0x35, 0x6B, 0x05, 0x2C, 0x68, 0x39, 0x97, 0x40, 0x94, 0x75, 0xC7, 0xCA, 0xB4, 0x8C, 0x2A, 0x4B, 0x35, 0x96, 0x1D, 0x1C, 0x46, 0xE0, 0xF2, 0x78, 0x3C, 0xA1, 0x30, 0xC2, 0x12, 0x53, 0x5F, 0x3F, 0x5A, 0xEF, 0x64, 0x48, 0xC9, 0x5F, 0xD8, 0xDC, 0xEF, 0x06, 0xC2, 0x42, 0xF6, 0xCB, 0x84, 0x0C, 0xBD, 0x52, 0x2E, 0x27, 0xEA, 0x4D, 0xCC, 0x93, 0x85, 0x6C, 0x13, 0x81, 0x5B, 0x2B, 0x97, 0x0B, 0x42, 0x78, 0x8D, 0x16, 0x27, 0x16, 0x7B, 0xAE, 0x25, 0x07, 0xD1, 0x73, 0x0A, 0xAF, 0x37, 0xFF, 0xB8, 0xC7, 0x07, 0xFE, 0xDC, 0x76, 0x1A, 0x7B, 0x40, 0x49, 0x53, 0x12, 0x68, 0x82, 0x6E, 0x81, 0x40, 0xCF, 0x60, 0x0F, 0x0C, 0x10, 0x05, 0x72, 0xB7, 0xDA, 0x78, 0xD2, 0x5B, 0x16, 0x13, 0xD3, 0xF2, 0xF3, 0x37, 0xF5, 0x8D, 0xD3, 0xC9, 0xA6, 0x0A, 0x56, 0xF8, 0x66, 0x58, 0xC0, 0x7D, 0xB7, 0xF3, 0x22, 0x0A, 0x4F, 0x36, 0x0B, 0x9A, 0x95, 0xB2, 0x58, 0x1D, 0x23, 0xB1, 0x2A, 0x8C, 0x58, 0xCB, 0xCD, 0x67, 0x69, 0xE2, 0x6D, 0x85, 0x16, 0x67, 0xCE, 0xF9, 0xAF, 0xBE, 0xFA, 0xAE, 0xA5, 0x30, 0x59, 0x78, 0x65, 0xFC, 0xE8, 0xD1, 0x40, 0x58, 0xC0, 0x72, 0xDC, 0xB0, 0x01, 0x61, 0xDE, 0xA4, 0x65, 0x11, 0x9B, 0x65, 0x61, 0x64, 0x72, 0x24, 0x41, 0x20, 0x10, 0x58, 0x3B, 0x59, 0xBC, 0xCF, 0xDA, 0xCE, 0x9E, 0xFB, 0xFA, 0x9B, 0xCF, 0x7F, 0x3A, 0x75, 0x8E, 0x99, 0x6C, 0x15, 0x1D, 0x58, 0x03, 0x0B, 0x59, 0xFA, 0xFA, 0xDD, 0xEF, 0x5C, 0x38, 0xDB, 0x16, 0x11, 0x41, 0xCC, 0x8F, 0xBB, 0xE4, 0x40, 0x1C, 0x64, 0x59, 0xF7, 0xA4, 0xBB, 0xE3, 0xC6, 0xE7, 0xA7, 0x4E, 0x9D, 0xFA, 0xE0, 0x37, 0x5F, 0x9F, 0x3B, 0x9F, 0xC8, 0x15, 0xCD, 0xBD, 0x1A, 0x04, 0x7F, 0x25, 0x28, 0x68, 0xC3, 0x86, 0x7B, 0x97, 0xAE, 0x5B, 0xB7, 0xED, 0x9E, 0xAD, 0xCB, 0x96, 0x2D, 0x0B, 0x08, 0xD8, 0x7A, 0xCF, 0xBA, 0x15, 0x6B, 0xD7, 0xAE, 0x7D, 0x70, 0xED, 0xEE, 0xDD, 0xBB, 0xD7, 0xEC, 0xF0, 0x5D, 0x7D, 0xF7, 0xB6, 0x20, 0xB8, 0x15, 0x1F, 0xF8, 0x23, 0x1F, 0xF8, 0xDF, 0xBF, 0xD8, 0xAF, 0xCE, 0xC3, 0xA2, 0x1B, 0x17, 0x18, 0xB4, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; ================================================ FILE: src/samples/groot/groot.rc ================================================ #ifdef RC_INVOKED #ifndef _INC_WINDOWS #define _INC_WINDOWS #include "winres.h" // extract from windows header #endif #endif 1 RT_MANIFEST "manifest.xml" 101 ICON "groot.ico" #define file_description "Groot" #define original_file_name "groot.exe" #define product_name "groot" #define company_name "https://leok7v.github.io/" #define copyright "Copyright (C) 2021-2024 Dmitry `Leo` Kuznetsov. All rights reserved." #include "..\..\inc\rt\version.rc.in" #include "i18n.h" LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(65001) ================================================ FILE: src/samples/groot/rocket.h ================================================ #pragma once static unsigned char rocket[] = { // rocket.jpg file 64x72 rgb 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x84, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x06, 0x05, 0x06, 0x06, 0x08, 0x07, 0x07, 0x07, 0x07, 0x08, 0x0C, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0C, 0x13, 0x0C, 0x0E, 0x0C, 0x0C, 0x0E, 0x0C, 0x13, 0x11, 0x14, 0x10, 0x0F, 0x10, 0x14, 0x11, 0x1E, 0x17, 0x15, 0x15, 0x17, 0x1E, 0x22, 0x1D, 0x1B, 0x1D, 0x22, 0x2A, 0x25, 0x25, 0x2A, 0x34, 0x32, 0x34, 0x44, 0x44, 0x5C, 0x01, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x06, 0x06, 0x05, 0x06, 0x06, 0x08, 0x07, 0x07, 0x07, 0x07, 0x08, 0x0C, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0C, 0x13, 0x0C, 0x0E, 0x0C, 0x0C, 0x0E, 0x0C, 0x13, 0x11, 0x14, 0x10, 0x0F, 0x10, 0x14, 0x11, 0x1E, 0x17, 0x15, 0x15, 0x17, 0x1E, 0x22, 0x1D, 0x1B, 0x1D, 0x22, 0x2A, 0x25, 0x25, 0x2A, 0x34, 0x32, 0x34, 0x44, 0x44, 0x5C, 0xFF, 0xC2, 0x00, 0x11, 0x08, 0x00, 0x48, 0x00, 0x40, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x06, 0x07, 0x04, 0x08, 0x01, 0x02, 0x03, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x2E, 0xDC, 0x73, 0xB0, 0xA0, 0x7E, 0x07, 0x33, 0xBE, 0xA4, 0xE0, 0xE6, 0xD4, 0x03, 0xC8, 0xD7, 0x69, 0x36, 0x02, 0x73, 0xA9, 0x5B, 0xFA, 0xE2, 0x16, 0xD3, 0x4E, 0x6B, 0x32, 0x5E, 0xB9, 0x81, 0x26, 0x3B, 0x6C, 0xF3, 0x39, 0xF0, 0x8F, 0x38, 0xA9, 0xCD, 0xFD, 0xDB, 0x69, 0xD2, 0x61, 0x8C, 0x65, 0x10, 0xAB, 0x0B, 0xD9, 0xF4, 0x98, 0x83, 0x58, 0x00, 0x7B, 0x0E, 0xAF, 0x88, 0x6C, 0x28, 0xA4, 0xBE, 0xF7, 0x54, 0x35, 0xB7, 0xF1, 0x63, 0x82, 0xFF, 0x00, 0xFF, 0xC4, 0x00, 0x19, 0x01, 0x00, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x01, 0x02, 0x05, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x10, 0x00, 0x00, 0x00, 0x99, 0x4C, 0x8C, 0x05, 0xB5, 0xC9, 0xC6, 0x04, 0x5D, 0x63, 0x06, 0xD9, 0x9A, 0x1F, 0xFF, 0xC4, 0x00, 0x19, 0x01, 0x00, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x01, 0x02, 0x05, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x10, 0x00, 0x00, 0x00, 0x90, 0xE3, 0xC9, 0x53, 0x63, 0x14, 0x23, 0x68, 0x6D, 0x2E, 0x6B, 0xEC, 0xF1, 0xFF, 0x00, 0xFF, 0xC4, 0x00, 0x25, 0x10, 0x00, 0x02, 0x03, 0x01, 0x00, 0x02, 0x03, 0x00, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x01, 0x02, 0x05, 0x06, 0x00, 0x13, 0x07, 0x11, 0x12, 0x14, 0x21, 0x10, 0x31, 0x16, 0x22, 0x41, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x12, 0x00, 0xFA, 0xFF, 0x00, 0x3F, 0xEB, 0xCC, 0x61, 0x0B, 0xA5, 0xE4, 0x16, 0x14, 0xB5, 0x7B, 0x30, 0x01, 0x56, 0x2D, 0x57, 0x71, 0xF5, 0x77, 0x55, 0x4D, 0x64, 0x12, 0xBB, 0x3A, 0x34, 0x99, 0x50, 0xC0, 0x0F, 0xC5, 0xF3, 0x98, 0xB8, 0x9A, 0xEA, 0x9C, 0x90, 0xCD, 0xAB, 0xFA, 0x85, 0x3B, 0x1B, 0x73, 0x81, 0xCD, 0x57, 0x3F, 0x28, 0x4B, 0x0C, 0xE2, 0x35, 0x6D, 0x5A, 0xF9, 0x92, 0x4E, 0x6B, 0x1B, 0xE3, 0x9C, 0x76, 0x8F, 0xCE, 0xA7, 0xA0, 0xC6, 0x81, 0x59, 0x87, 0x0C, 0x7E, 0x45, 0x3D, 0xE6, 0xA9, 0xFF, 0x00, 0x09, 0x92, 0x96, 0xE5, 0x89, 0xBF, 0xF0, 0x18, 0xF8, 0xF7, 0xA4, 0x56, 0xB3, 0x66, 0x02, 0x0A, 0x44, 0x5B, 0xF3, 0xE7, 0x3B, 0x9D, 0xD2, 0x73, 0x5A, 0x8A, 0xE9, 0x01, 0x48, 0x61, 0x78, 0xB5, 0x61, 0x8A, 0x3F, 0xAF, 0x89, 0x8B, 0xAE, 0x0D, 0x13, 0xEA, 0xC0, 0x26, 0xB3, 0x13, 0xEB, 0xF9, 0x2A, 0x9A, 0x44, 0xC3, 0xC9, 0xDD, 0x57, 0x66, 0xC7, 0xCA, 0xD0, 0x29, 0x2B, 0x00, 0x02, 0xC4, 0x75, 0x17, 0x6F, 0x02, 0xFB, 0x95, 0x66, 0x22, 0x6F, 0xE7, 0x22, 0xC9, 0x75, 0x32, 0x35, 0xB9, 0xC9, 0xBC, 0x7E, 0xA8, 0x3B, 0x36, 0xA7, 0x9C, 0x57, 0xC4, 0x3B, 0x19, 0xAB, 0x5B, 0xA0, 0xD8, 0x75, 0x64, 0x2F, 0x41, 0xFB, 0x56, 0x58, 0x39, 0x42, 0x7C, 0xB9, 0x2B, 0x51, 0x69, 0x31, 0x74, 0xEE, 0xC0, 0x83, 0x5D, 0x6C, 0x4D, 0x45, 0x7F, 0x96, 0xF2, 0x30, 0x75, 0x60, 0x70, 0x41, 0xFE, 0xD4, 0xCF, 0x67, 0x4F, 0x46, 0x8B, 0x8A, 0xB6, 0x29, 0x62, 0x67, 0xF5, 0xE7, 0x5F, 0xD0, 0x15, 0xB4, 0xF1, 0x78, 0xE0, 0x50, 0x90, 0x92, 0x20, 0x2B, 0x04, 0x37, 0x1E, 0x1B, 0x9B, 0x78, 0xD8, 0xF7, 0xFF, 0x00, 0xAD, 0x74, 0x54, 0xB0, 0xA6, 0xB1, 0xF7, 0xFD, 0x79, 0xF1, 0x87, 0x29, 0x44, 0x79, 0x47, 0x7A, 0x61, 0x88, 0x12, 0xEF, 0xE6, 0x66, 0xA6, 0x65, 0x1D, 0x6D, 0x8D, 0x77, 0x04, 0xD5, 0x98, 0x73, 0x65, 0xB3, 0xD2, 0x02, 0x6D, 0x1F, 0x8F, 0x3A, 0x88, 0x7F, 0x33, 0x1D, 0x1D, 0x01, 0x46, 0xF6, 0x72, 0xD6, 0x81, 0xAA, 0xA7, 0x7B, 0xD5, 0xF3, 0x10, 0xA5, 0x47, 0xB5, 0x25, 0xB2, 0xFE, 0xC0, 0x9D, 0x1C, 0x2C, 0xF4, 0x3A, 0xA7, 0x5F, 0xD6, 0x59, 0xEB, 0x25, 0xA1, 0x16, 0x93, 0x7F, 0x13, 0xE4, 0x44, 0xA8, 0xB9, 0x71, 0x36, 0x41, 0x48, 0x9A, 0xC1, 0x7D, 0x44, 0xF3, 0x1D, 0x37, 0x01, 0xA6, 0x86, 0x82, 0x71, 0x33, 0x60, 0x1C, 0x6D, 0x06, 0xF1, 0x31, 0xF7, 0x1F, 0x7F, 0xEB, 0xCE, 0x1B, 0xB6, 0xC3, 0x7B, 0x8A, 0x1F, 0x37, 0x4B, 0xDD, 0x77, 0xE9, 0x6F, 0xDF, 0x9F, 0x19, 0x6E, 0xF2, 0x19, 0x7D, 0x48, 0x0F, 0xA0, 0x21, 0xAD, 0xA2, 0x01, 0x16, 0xC1, 0x61, 0x55, 0xB4, 0xF0, 0xBE, 0x4C, 0xCE, 0x77, 0x50, 0xB4, 0x90, 0x2D, 0xAA, 0x7D, 0x03, 0x3B, 0xF2, 0xDE, 0xFF, 0x00, 0x0F, 0xD1, 0x75, 0x84, 0x63, 0x3F, 0x36, 0x82, 0x62, 0xC1, 0xAF, 0xF2, 0x7C, 0xCF, 0xDB, 0x06, 0x0E, 0x99, 0x0C, 0x21, 0x48, 0x43, 0x68, 0xB5, 0x64, 0x4F, 0xBA, 0x3D, 0x6E, 0x35, 0xA8, 0xB0, 0x84, 0x31, 0x10, 0x46, 0x9A, 0xDB, 0x9B, 0x2E, 0x8B, 0x43, 0x72, 0x57, 0x2C, 0x58, 0x82, 0x56, 0xF7, 0xF4, 0x78, 0x06, 0x4E, 0xA9, 0x20, 0xA0, 0x24, 0xD2, 0xD1, 0xFF, 0x00, 0xAA, 0x74, 0xD5, 0x25, 0xD4, 0xAB, 0xB1, 0x7A, 0x40, 0x6F, 0x5B, 0xC1, 0x74, 0x3A, 0x8D, 0xF2, 0xA9, 0x0E, 0xE5, 0x6E, 0x50, 0xD3, 0x33, 0xF8, 0x9B, 0xB7, 0x97, 0xAC, 0xE9, 0x9C, 0x7B, 0x40, 0xA4, 0xB9, 0xFE, 0xA4, 0x97, 0xB6, 0x49, 0x41, 0x46, 0x26, 0xAC, 0x8A, 0x84, 0xA1, 0x29, 0x23, 0x88, 0x1D, 0xEC, 0xD6, 0x6B, 0xB9, 0x22, 0x21, 0xE6, 0xA3, 0x27, 0xB6, 0x95, 0xE2, 0xD5, 0x2B, 0xC6, 0x60, 0xCB, 0x3A, 0xB0, 0xCC, 0x00, 0x56, 0x6E, 0x1C, 0xF5, 0xA5, 0xA6, 0x46, 0x1A, 0x82, 0xE6, 0xBD, 0xA6, 0x22, 0xA2, 0xAF, 0x2F, 0xB5, 0x54, 0x84, 0x4A, 0x71, 0x68, 0x15, 0x7A, 0xDB, 0xEA, 0xF2, 0xD6, 0x46, 0x26, 0x95, 0xD8, 0xAA, 0x08, 0x97, 0x39, 0xA1, 0x4C, 0xC9, 0x13, 0xE7, 0x31, 0x9F, 0x5F, 0x44, 0x00, 0xAC, 0x43, 0x0B, 0x12, 0x4B, 0x13, 0x45, 0x1E, 0xC5, 0x9A, 0x44, 0xB7, 0x9E, 0x4F, 0x55, 0xEF, 0x11, 0x0C, 0xBB, 0x94, 0x9E, 0x6E, 0x8E, 0xF0, 0xA5, 0x91, 0xDE, 0x17, 0x62, 0xC1, 0x52, 0xAF, 0xE7, 0x29, 0x91, 0x87, 0x91, 0xBD, 0x97, 0x05, 0xB9, 0x5E, 0xAF, 0xE4, 0xD1, 0x8A, 0xFC, 0xB2, 0xE5, 0x12, 0xFC, 0x0D, 0x65, 0x4B, 0x4B, 0x7D, 0x8B, 0x99, 0x29, 0x45, 0xB6, 0x85, 0x85, 0xFB, 0xFD, 0x7B, 0x6B, 0x33, 0xE6, 0x37, 0xC8, 0xDA, 0x99, 0xAB, 0x91, 0x66, 0x6D, 0x24, 0x08, 0xAF, 0x6A, 0xFA, 0xBB, 0x40, 0x81, 0xAB, 0x2F, 0xD3, 0xE1, 0x86, 0x44, 0x61, 0xCD, 0xCC, 0x4B, 0x23, 0xAE, 0x3B, 0xE4, 0x11, 0x8C, 0xC2, 0x8A, 0x08, 0x59, 0x8A, 0xDA, 0x9C, 0xF0, 0x59, 0x70, 0xA5, 0xBC, 0xAD, 0x14, 0xAC, 0xD3, 0xEA, 0xC3, 0xD4, 0xE4, 0x5D, 0xC9, 0xD6, 0x2B, 0x8C, 0x08, 0x2D, 0x4D, 0xC0, 0x56, 0xCA, 0xA0, 0xDE, 0xCF, 0x2E, 0x36, 0x87, 0x33, 0xEF, 0xFE, 0xC3, 0xF9, 0x79, 0x09, 0x40, 0xAB, 0x21, 0xA3, 0x07, 0x60, 0x77, 0x38, 0x85, 0x52, 0x16, 0xA1, 0xE2, 0x6E, 0x20, 0xEF, 0x01, 0x93, 0x44, 0xFA, 0xC3, 0x4B, 0xDE, 0x7C, 0x65, 0xD7, 0x0F, 0x9F, 0x62, 0x5B, 0x16, 0x92, 0x32, 0x82, 0x96, 0x5C, 0x6F, 0x34, 0xF0, 0x11, 0xB5, 0xAE, 0x69, 0x90, 0x5A, 0xB5, 0x9A, 0x53, 0x28, 0x4F, 0xA2, 0x40, 0x18, 0x20, 0x83, 0x0E, 0x7E, 0xCD, 0xE9, 0xC2, 0x7C, 0x17, 0x68, 0xFE, 0xC8, 0xB8, 0x6F, 0x35, 0x99, 0xAD, 0x1E, 0xE9, 0x1B, 0x2B, 0x2D, 0x92, 0x5D, 0x9B, 0x7B, 0xAF, 0x65, 0x3D, 0x1B, 0x3C, 0xD0, 0x54, 0x3D, 0x74, 0x59, 0x64, 0x45, 0xB5, 0xE2, 0x2E, 0x45, 0xE1, 0x24, 0x95, 0x5C, 0x47, 0x0D, 0x62, 0xEC, 0x47, 0xBC, 0x71, 0x3F, 0xFF, 0xC4, 0x00, 0x2B, 0x10, 0x01, 0x00, 0x02, 0x02, 0x02, 0x02, 0x01, 0x03, 0x02, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x11, 0x00, 0x03, 0x12, 0x21, 0x31, 0x41, 0x04, 0x10, 0x13, 0x22, 0x51, 0x61, 0x20, 0x32, 0x62, 0x71, 0x72, 0x91, 0x42, 0x81, 0x82, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x13, 0x3F, 0x00, 0xFE, 0x06, 0x4A, 0x21, 0x60, 0xD7, 0x8E, 0x93, 0x22, 0x9C, 0x98, 0xC2, 0xC8, 0xCA, 0x94, 0x3F, 0x1F, 0x0B, 0x9F, 0x0A, 0x51, 0x9E, 0xDA, 0xFE, 0xBD, 0xA8, 0xC6, 0x29, 0xFD, 0x24, 0xB3, 0x48, 0xCE, 0x66, 0xB6, 0x2B, 0x23, 0x66, 0xC9, 0x5A, 0xAA, 0x8D, 0x2F, 0xD3, 0x74, 0x2F, 0x6C, 0x63, 0x0D, 0x8C, 0x42, 0x12, 0x3B, 0x8D, 0x19, 0xF2, 0x67, 0x1F, 0xBD, 0x10, 0xEE, 0x4E, 0xB9, 0x74, 0x4E, 0x26, 0x3B, 0x63, 0x9A, 0x36, 0xC7, 0x69, 0x2D, 0x7E, 0xC9, 0x46, 0x2D, 0x95, 0x9A, 0x75, 0x3B, 0x77, 0x4D, 0x29, 0x8D, 0x03, 0x10, 0xEB, 0xA6, 0xD0, 0xC3, 0xE3, 0x9F, 0x1A, 0x7A, 0xA5, 0x06, 0xF8, 0xCF, 0xF3, 0x9B, 0x2F, 0x3D, 0x23, 0x58, 0x1D, 0x3D, 0x32, 0x88, 0xFE, 0xF5, 0x17, 0xE8, 0x96, 0x33, 0xE8, 0xD9, 0xAB, 0xFF, 0x00, 0x45, 0x27, 0xEE, 0x64, 0xB7, 0x87, 0xC8, 0x57, 0xA4, 0x89, 0xE4, 0x53, 0x36, 0x4C, 0x88, 0xC3, 0xE2, 0x4D, 0x36, 0xB7, 0x7D, 0x74, 0x5D, 0x99, 0xAD, 0x91, 0xAE, 0x45, 0xF6, 0x45, 0xB1, 0x5A, 0x7C, 0x65, 0xBE, 0xBB, 0x65, 0x25, 0xF0, 0x1E, 0xDC, 0xD9, 0xD1, 0xBF, 0x7E, 0xC8, 0x83, 0x38, 0x07, 0x46, 0xA8, 0x90, 0xA8, 0xE7, 0x56, 0xCA, 0x01, 0xB6, 0x31, 0x17, 0xA2, 0x4B, 0x0A, 0xFA, 0x6C, 0x63, 0x19, 0x6A, 0x2F, 0xD7, 0x29, 0x64, 0x62, 0x9C, 0x59, 0xCB, 0xCC, 0x18, 0xC8, 0x2D, 0xB0, 0x02, 0x2E, 0x47, 0xE4, 0x45, 0xDA, 0x7E, 0x0C, 0xE6, 0xC9, 0x1A, 0x24, 0xE6, 0xFD, 0x51, 0x94, 0x4B, 0x9B, 0xCA, 0x30, 0x65, 0x14, 0xA4, 0x3B, 0x72, 0x31, 0x89, 0xA7, 0x6B, 0xE6, 0x40, 0xB2, 0x10, 0xFF, 0x00, 0x6B, 0x9E, 0x2C, 0xD9, 0xF9, 0x10, 0xEB, 0xD5, 0x92, 0xB3, 0x14, 0x2F, 0x59, 0x3E, 0x3B, 0x63, 0x2F, 0x71, 0xE5, 0x77, 0x17, 0xC2, 0x7D, 0x21, 0x3F, 0xE6, 0x68, 0x08, 0xD4, 0x89, 0xE6, 0xFE, 0x2F, 0xE7, 0x33, 0x88, 0xDC, 0x00, 0x0F, 0xDE, 0x59, 0xC7, 0x59, 0xB3, 0x7C, 0x36, 0x4D, 0x99, 0x09, 0x6D, 0x11, 0x4F, 0x48, 0xB8, 0xEB, 0x74, 0x33, 0xDA, 0xF9, 0xD9, 0x73, 0xA1, 0xBF, 0xF1, 0xC9, 0x6B, 0x8A, 0xCA, 0x37, 0x55, 0xCA, 0x8B, 0x7D, 0xD2, 0x06, 0x08, 0x70, 0xDB, 0xA2, 0x46, 0xD2, 0xC7, 0xC3, 0x20, 0x42, 0xB3, 0xB5, 0xD9, 0xAA, 0xDE, 0x71, 0x85, 0x3D, 0x25, 0xA9, 0x5F, 0x41, 0xA7, 0x35, 0x35, 0x32, 0x63, 0x6C, 0xAF, 0xDA, 0xB9, 0x2D, 0x1A, 0xCD, 0x91, 0x8B, 0xEC, 0x6A, 0xF2, 0x77, 0x29, 0x4B, 0xAF, 0x37, 0x93, 0x04, 0x14, 0xA1, 0xEF, 0xA3, 0xFB, 0xFA, 0xCD, 0x6C, 0x48, 0x29, 0x1A, 0x84, 0xA7, 0xD5, 0xC9, 0xA5, 0x10, 0x4B, 0xBC, 0xD9, 0x17, 0xF3, 0xD6, 0xDC, 0x24, 0xD0, 0xF7, 0x56, 0x09, 0x90, 0xB6, 0x53, 0x92, 0xF5, 0x1E, 0xBB, 0xCE, 0x1B, 0xC5, 0x2E, 0xAE, 0x2F, 0xDC, 0xE4, 0xE6, 0xCD, 0xAE, 0xE4, 0x8D, 0x0F, 0x2D, 0x4A, 0x1C, 0x8B, 0x7B, 0x1E, 0xC3, 0x06, 0x9A, 0xE0, 0xDC, 0xA3, 0xE9, 0x94, 0x43, 0x90, 0x64, 0x1A, 0xFC, 0x2C, 0x3B, 0x8A, 0x7B, 0x32, 0x23, 0x27, 0x65, 0xCA, 0xC9, 0x74, 0x94, 0x11, 0xF3, 0x79, 0xB7, 0x6C, 0x9D, 0x51, 0xDB, 0x03, 0xBE, 0x21, 0x4D, 0xB1, 0x9F, 0xB7, 0x3E, 0x3C, 0x4D, 0x63, 0xFE, 0x6F, 0x72, 0x9D, 0x55, 0x9C, 0x96, 0x9C, 0x81, 0x72, 0xA1, 0xB6, 0x8F, 0x79, 0xF6, 0xE3, 0x18, 0x49, 0x9C, 0x9B, 0x26, 0x87, 0xFD, 0x01, 0x82, 0xA3, 0x3B, 0x55, 0xA7, 0xD5, 0x1D, 0xE2, 0x32, 0xDB, 0xA6, 0x6C, 0xBB, 0xEA, 0xED, 0x05, 0x29, 0xC7, 0x63, 0xB2, 0x36, 0x2D, 0x59, 0x08, 0xAB, 0x1F, 0xD4, 0xEB, 0x23, 0x37, 0xED, 0x9F, 0x1F, 0x61, 0xC3, 0x8C, 0x25, 0x57, 0xCC, 0x11, 0x14, 0xE9, 0xF5, 0x9B, 0x50, 0x94, 0x6A, 0xC9, 0xEA, 0x91, 0xE3, 0x9D, 0x7F, 0xBA, 0x33, 0x54, 0xB8, 0xBB, 0x0E, 0x0C, 0xB8, 0xAF, 0xA8, 0x9F, 0xF2, 0x7C, 0xD5, 0xE0, 0xD2, 0xF5, 0x55, 0x8C, 0x23, 0x1E, 0xC9, 0xC9, 0x59, 0x35, 0xD8, 0xDD, 0xC9, 0xF6, 0xD6, 0x78, 0x48, 0x27, 0x19, 0x45, 0x0E, 0x9C, 0x92, 0x44, 0x4E, 0x2C, 0x59, 0x07, 0xA6, 0x9A, 0xBC, 0x3A, 0x9C, 0x1F, 0xD1, 0xBF, 0x54, 0x7A, 0xF3, 0x86, 0x99, 0xC7, 0x94, 0x45, 0x63, 0x2E, 0x52, 0x58, 0xA0, 0x75, 0xD2, 0x2D, 0xE1, 0x16, 0xE0, 0xEB, 0x8C, 0x64, 0x46, 0xD6, 0xA4, 0x4E, 0x92, 0xCE, 0xC4, 0x71, 0x69, 0x9C, 0xF8, 0xCD, 0x8F, 0xF7, 0xEA, 0x46, 0x7F, 0xFF, 0xC4, 0x00, 0x23, 0x11, 0x00, 0x02, 0x02, 0x02, 0x00, 0x06, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x11, 0x00, 0x21, 0x04, 0x12, 0x22, 0x32, 0x41, 0x51, 0x13, 0x31, 0x61, 0x71, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x00, 0x66, 0xE4, 0xAD, 0x13, 0x66, 0xB0, 0x32, 0x9A, 0xDE, 0x38, 0x78, 0xA4, 0xB8, 0x85, 0x93, 0x65, 0x7D, 0x5F, 0x91, 0x90, 0x2C, 0xCE, 0x12, 0x5E, 0x22, 0x4E, 0xA0, 0xDD, 0xAB, 0xA0, 0x32, 0x4A, 0x0B, 0x9C, 0xA4, 0x32, 0x97, 0x92, 0xC9, 0x3A, 0xA1, 0x95, 0xAF, 0xDC, 0x8D, 0x83, 0xC3, 0x7D, 0xBF, 0x60, 0xFF, 0x00, 0x46, 0x10, 0x1C, 0x51, 0xC5, 0x22, 0x26, 0x00, 0x83, 0x47, 0x57, 0x84, 0xFA, 0xC5, 0x4A, 0x12, 0xFD, 0x6C, 0xF3, 0x51, 0xC9, 0x1A, 0x41, 0x5F, 0x18, 0x07, 0x23, 0x75, 0x92, 0x26, 0x24, 0x78, 0x37, 0xF9, 0x91, 0x71, 0x1C, 0x3A, 0x96, 0x43, 0x20, 0xBD, 0x6A, 0xEF, 0x10, 0x9A, 0x70, 0xDD, 0x44, 0x13, 0x47, 0xD8, 0xF1, 0x93, 0xE9, 0xD0, 0xE3, 0xB5, 0x70, 0xF3, 0x1A, 0x07, 0x55, 0x90, 0xA2, 0x16, 0x9A, 0xD4, 0x1E, 0x81, 0x90, 0x92, 0x62, 0x8A, 0xC9, 0xED, 0x51, 0x9F, 0xFF, 0xC4, 0x00, 0x23, 0x11, 0x00, 0x02, 0x02, 0x02, 0x01, 0x04, 0x03, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x11, 0x03, 0x21, 0x04, 0x13, 0x31, 0x32, 0x41, 0x12, 0x22, 0x51, 0x71, 0xB1, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x00, 0x55, 0x2E, 0x68, 0x42, 0xA4, 0x12, 0x0C, 0x01, 0x32, 0x63, 0xA7, 0xEC, 0x28, 0x18, 0xE5, 0x14, 0x95, 0xC4, 0xBA, 0xAE, 0xE7, 0xB9, 0x98, 0xEC, 0xB8, 0xA9, 0x7F, 0x34, 0x75, 0x44, 0x0B, 0x42, 0xCD, 0x98, 0x0D, 0x1D, 0xF8, 0xCC, 0x83, 0xE3, 0x93, 0xF4, 0x1A, 0x31, 0x49, 0x53, 0x62, 0x3A, 0x36, 0x40, 0xC4, 0x55, 0x8D, 0xD4, 0x55, 0x15, 0x44, 0xFF, 0x00, 0x63, 0xB6, 0xB1, 0x8D, 0xFD, 0x45, 0x45, 0x5C, 0x66, 0xFA, 0x84, 0xCC, 0x88, 0xE9, 0x91, 0x42, 0x35, 0x82, 0x45, 0x4C, 0x9C, 0x5E, 0x55, 0x07, 0xE8, 0xB0, 0x04, 0x7B, 0x15, 0x7B, 0x8C, 0x3C, 0x6B, 0x57, 0xEB, 0xF0, 0xCC, 0x7B, 0x56, 0x13, 0x0A, 0x03, 0xCB, 0xE3, 0x28, 0x62, 0x3E, 0xF7, 0x63, 0xD5, 0x4E, 0x56, 0x5C, 0x81, 0x30, 0x53, 0x9D, 0xE5, 0x20, 0xCC, 0xC0, 0x75, 0xF3, 0xE8, 0x79, 0xB7, 0xFB, 0x3F, 0xFF, 0xD9 }; ================================================ FILE: src/samples/i18n.h ================================================ #pragma once // String ids must be sequential starting from 1 (not zero!) and // be compact sequential integers set (should be compact) #define str_do_not_use 0 #define str_restore 1 #define str_maximize 2 #define str_minimize 3 #define str_quit 4 #define str_close 5 #define str_cancel 6 #define str_ok 7 #define str_full_screen 8 #define str_yes 9 #define str_no 10 #define str_hello 11 #define str_slider_accel 12 #define str_file_open 13 #define str_about 14 #define str_button_messagebox 15 #define str_scroll 16 #define str_reverse 17 #define str_natural 18 #define str_mbx 19 #define str_window 20 #define str_monitor 21 #define str_client_area 22 #define str_mouse 23 #define str_header 24 #define str_left_top 25 #define str_zoom 26 #define str_mandelbrot 27 #define str_help 28 #define str_proportional 29 #define str_monospaced 30 #define str_millisecond 31 #define str_minimum 32 #define str_average 33 #define str_maximum 34 #define str_restore_from_fs 35 #define str_about_hint 36 #define str_yes_no_hint 37 #define str_fs_label 38 #define str_copied 39 ================================================ FILE: src/samples/rt.c ================================================ #define rt_implementation #include "single_file_lib/rt/rt.h" ================================================ FILE: src/samples/sample.rc ================================================ #ifdef RC_INVOKED #ifndef _INC_WINDOWS #define _INC_WINDOWS #include "winres.h" #endif #endif #include "i18n.h" 1 RT_MANIFEST "manifest.xml" 101 ICON "sample.ico" sample_png RCDATA "sample.png" // https://en.wikipedia.org/wiki/File:Philips_PM5544.svg #define company_name "Insert Company Name Here" #define copyright "Copyright (C) YYYY Insert Name Here. All rights reserved." #define file_description "UI sample app" #define original_file_name "sample.exe" #define product_name "sample" #include "..\inc\rt\version.rc.in" LANGUAGE LANG_CHINESE, SUBLANG_NEUTRAL // to localize Chinese variants: // use LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL, SUBLANG_CHINESE_HONGKONG // SUBLANG_CHINESE_SINGAPORE, SUBLANG_CHINESE_MACAU ... STRINGTABLE { str_restore , L"恢复" str_maximize , L"最大化" str_minimize , L"最小化" str_quit , L"退出" str_close , L"关闭" str_cancel , L"取消" str_ok , L"确定" str_full_screen , L"全屏 (ESC 恢复)" str_yes , L"&Y 是的" str_no , L"&N 不" str_hello , L"你好" str_slider_accel , L" 点击时按住\n Ctrl:x 10 Shift:x 100 \n Ctrl+Shift:x 1000 \n 来调整步进倍数。" str_file_open , L"&O 已连接" str_about , L"&A 盖" str_button_messagebox , L"&M 消息框" str_scroll , L"滚动方向:" str_natural , L"自然" str_reverse , L"反向" str_mbx , L"""Pneumonoultramicroscopicsilicovolcanoconiosis""\n它是最长的英语单词还是不是?" str_window , L"窗口" str_monitor , L"显示器" str_client_area , L"客户区" str_mouse , L"鼠标坐标" str_header , L"标题" str_left_top , L"左上" str_zoom , L"飞涨: 1 / (2^%d)" str_mandelbrot , L"曼德勃罗特探险家" str_help , L"单击内部或+/-放大;\n右键单击缩小;\n使用触摸板或键盘←↑↓→移动" str_proportional , L"成比例的" str_monospaced , L"等宽" str_millisecond , L"毫秒" str_minimum , L"最低的" str_average , L"平均" str_maximum , L"最大" str_restore_from_fs , L"从全屏恢复" str_about_hint , L"显示关于消息框" str_yes_no_hint , L"显示是/否消息框" str_fs_label , L"全屏" // str_copied , L"copied to clipboard" str_copied , L"复制到剪贴板" } // Order of languages is important. (ENGLISH, NEUTRAL) must be last. // It is assumed that application messaging is expressed in Neutral // English and strings used in the body of code match character by // character with the strings in the NEUTRAL ENGLISH STRINGTABLE. // It is also assumed that same str_ids are used for all string tables // are the same for each unique string in the code. LANGUAGE LANG_AFRIKAANS, SUBLANG_NEUTRAL // locale: "af-ZA" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_ALBANIAN, SUBLANG_NEUTRAL // locale: "sq-AL" STRINGTABLE { str_hello , L"Tungjatjeta" } LANGUAGE LANG_ALSATIAN, SUBLANG_NEUTRAL // locale: "gsw-FR" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_AMHARIC, SUBLANG_NEUTRAL // locale: "am-ET" STRINGTABLE { str_hello , L"ሰላም" } LANGUAGE LANG_ARABIC, SUBLANG_NEUTRAL // locale: "ar-SA" STRINGTABLE { str_hello , L"مرحبًا" } LANGUAGE LANG_ARMENIAN, SUBLANG_NEUTRAL // locale: "hy-AM" STRINGTABLE { str_hello , L"Բարեւ ծով" } LANGUAGE LANG_ASSAMESE, SUBLANG_NEUTRAL // locale: "as-IN" STRINGTABLE { str_hello , L"নমস্কাৰ" } LANGUAGE LANG_AZERI, SUBLANG_NEUTRAL // locale: "az-AZ" STRINGTABLE { str_hello , L"Salam" } LANGUAGE LANG_BANGLA, SUBLANG_NEUTRAL // locale: "bn-BD" STRINGTABLE { str_hello , L"হ্যালো" } LANGUAGE LANG_BASHKIR, SUBLANG_NEUTRAL // locale: "ba-RU" STRINGTABLE { str_hello , L"Хәйерсез" } LANGUAGE LANG_BASQUE, SUBLANG_NEUTRAL // locale: "eu-ES" STRINGTABLE { str_hello , L"Kaixo" } LANGUAGE LANG_BELARUSIAN, SUBLANG_NEUTRAL // locale: "be-BY" STRINGTABLE { str_hello , L"Прывітанне" } LANGUAGE LANG_BRETON, SUBLANG_NEUTRAL // locale: "br-FR" STRINGTABLE { str_hello , L"Salud" } LANGUAGE LANG_BOSNIAN, SUBLANG_NEUTRAL // locale: "bs-BA" STRINGTABLE { str_hello , L"Zdravo" } LANGUAGE LANG_BOSNIAN_NEUTRAL, SUBLANG_NEUTRAL // locale: "bs-Latn-BA" STRINGTABLE { str_hello , L"Zdravo" } LANGUAGE LANG_BULGARIAN, SUBLANG_NEUTRAL // locale: "bg-BG" STRINGTABLE { str_hello , L"Здравейте" } LANGUAGE LANG_CATALAN, SUBLANG_NEUTRAL // locale: "ca-ES" STRINGTABLE { str_hello , L"Hola" } LANGUAGE LANG_CENTRAL_KURDISH, SUBLANG_NEUTRAL // locale: "ku-CK" STRINGTABLE { str_hello , L"هەڵۆ" } LANGUAGE LANG_CZECH, SUBLANG_NEUTRAL // locale: "cs-CZ" STRINGTABLE { str_hello , L"Ahoj" } LANGUAGE LANG_DANISH, SUBLANG_NEUTRAL // locale: "da-DK" STRINGTABLE { str_hello , L"Hej" } LANGUAGE LANG_DARI, SUBLANG_NEUTRAL // locale: "prs-AF" STRINGTABLE { str_hello , L"سلام" } LANGUAGE LANG_DIVEHI, SUBLANG_NEUTRAL // locale: "dv-MV" STRINGTABLE { str_hello , L"ଆଲା" } LANGUAGE LANG_DUTCH, SUBLANG_NEUTRAL // locale: "nl-NL" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_ESTONIAN, SUBLANG_NEUTRAL // locale: "et-EE" STRINGTABLE { str_hello , L"Tere" } LANGUAGE LANG_FAEROESE, SUBLANG_NEUTRAL // locale: "fo-FO" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_FARSI, SUBLANG_NEUTRAL // locale: "fa-IR" STRINGTABLE { str_hello , L"سلام" } LANGUAGE LANG_FILIPINO, SUBLANG_NEUTRAL // locale: "fil-PH" STRINGTABLE { str_hello , L"Hello" } LANGUAGE LANG_FINNISH, SUBLANG_NEUTRAL // locale: "fi-FI" STRINGTABLE { str_hello , L"Hei" } LANGUAGE LANG_FRENCH, SUBLANG_NEUTRAL // locale: "fr-FR" STRINGTABLE { str_hello , L"Bonjour" } LANGUAGE LANG_FRISIAN, SUBLANG_NEUTRAL // locale: "fy-NL" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_GALICIAN, SUBLANG_NEUTRAL // locale: "gl-ES" STRINGTABLE { str_hello , L"Ola" } LANGUAGE LANG_GEORGIAN, SUBLANG_NEUTRAL // locale: "ka-GE" STRINGTABLE { str_hello , L"გამარჯობა" } LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL // locale: "de-DE" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_GREEK, SUBLANG_NEUTRAL // locale: "el-GR" STRINGTABLE { str_hello , L"Γεια σας" } LANGUAGE LANG_GREENLANDIC, SUBLANG_NEUTRAL // locale: "kl-GL" STRINGTABLE { str_hello , L"Aliorneq" } LANGUAGE LANG_GUJARATI, SUBLANG_NEUTRAL // locale: "gu-IN" STRINGTABLE { str_hello , L"કેમ છો" } LANGUAGE LANG_HAUSA, SUBLANG_NEUTRAL // locale: "ha-Latn-NG" STRINGTABLE { str_hello , L"Sannu" } LANGUAGE LANG_HAWAIIAN, SUBLANG_NEUTRAL // locale: "haw-US" STRINGTABLE { str_hello , L"Aloha" } LANGUAGE LANG_HEBREW, SUBLANG_NEUTRAL // locale: "he-IL" STRINGTABLE { str_hello , L"שלום" } LANGUAGE LANG_HINDI, SUBLANG_NEUTRAL // locale: "hi-IN" STRINGTABLE { str_hello , L"नमस्ते" } LANGUAGE LANG_HUNGARIAN, SUBLANG_NEUTRAL // locale: "hu-HU" STRINGTABLE { str_hello , L"Helló" } LANGUAGE LANG_ICELANDIC, SUBLANG_NEUTRAL // locale: "is-IS" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_IGBO, SUBLANG_NEUTRAL // locale: "ig-NG" STRINGTABLE { str_hello , L"Ndeewọ" } LANGUAGE LANG_INDONESIAN, SUBLANG_NEUTRAL // locale: "id-ID" STRINGTABLE { str_hello , L"Halo" } LANGUAGE LANG_INUKTITUT, SUBLANG_NEUTRAL // locale: "iu-Latn-CA" STRINGTABLE { str_hello , L"Hi" } LANGUAGE LANG_IRISH, SUBLANG_NEUTRAL // locale: "ga-IE" STRINGTABLE { str_hello , L"Dia dhuit" } LANGUAGE LANG_ITALIAN, SUBLANG_NEUTRAL // locale: "it-IT" STRINGTABLE { str_hello , L"Ciao" } LANGUAGE LANG_JAPANESE, SUBLANG_NEUTRAL // locale: "ja-JP" STRINGTABLE { str_hello , L"こんにちは" } LANGUAGE LANG_KANNADA, SUBLANG_NEUTRAL // locale: "kn-IN" STRINGTABLE { str_hello , L"ಹಲೋ" } LANGUAGE LANG_KAZAK, SUBLANG_NEUTRAL // locale: "kk-KZ" STRINGTABLE { str_hello , L"Сәлем" } LANGUAGE LANG_KHMER, SUBLANG_NEUTRAL // locale: "km-KH" STRINGTABLE { str_hello , L"សួស្ត" } LANGUAGE LANG_KINYARWANDA, SUBLANG_NEUTRAL // locale: "rw-RW" STRINGTABLE { str_hello , L"Murakaza" } LANGUAGE LANG_KONKANI, SUBLANG_NEUTRAL // locale: "kok-IN" STRINGTABLE { str_hello , L"Khallô" } LANGUAGE LANG_KOREAN, SUBLANG_NEUTRAL // locale: "ko-KR" STRINGTABLE { str_hello , L"안녕하세요" } LANGUAGE LANG_KYRGYZ, SUBLANG_NEUTRAL // locale: "ky-KG" STRINGTABLE { str_hello , L"Салам" } LANGUAGE LANG_LAO, SUBLANG_NEUTRAL // locale: "lo-LA" STRINGTABLE { str_hello , L"ສະບາຍດີ" } LANGUAGE LANG_LATVIAN, SUBLANG_NEUTRAL // locale: "lv-LV" STRINGTABLE { str_hello , L"Sveiki" } LANGUAGE LANG_LITHUANIAN, SUBLANG_NEUTRAL // locale: "lt-LT" STRINGTABLE { str_hello , L"Labas" } LANGUAGE LANG_LUXEMBOURGISH, SUBLANG_NEUTRAL // locale: "lb-LU" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_MACEDONIAN, SUBLANG_NEUTRAL // locale: "mk-MK" STRINGTABLE { str_hello , L"Здраво" } LANGUAGE LANG_MALAY, SUBLANG_NEUTRAL // locale: "ms-MY" STRINGTABLE { str_hello , L"Hello" } LANGUAGE LANG_MALAYALAM, SUBLANG_NEUTRAL // locale: "ml-IN" STRINGTABLE { str_hello , L"ഹായ്" } LANGUAGE LANG_MALTESE, SUBLANG_NEUTRAL // locale: "mt-MT" STRINGTABLE { str_hello , L"Hello" } LANGUAGE LANG_MANIPURI, SUBLANG_NEUTRAL // locale: "mni-IN" STRINGTABLE { str_hello , L"আনৰ এইদিনৰ " } LANGUAGE LANG_MAORI, SUBLANG_NEUTRAL // locale: "mi-NZ" STRINGTABLE { str_hello , L"Kia ora" } LANGUAGE LANG_MARATHI, SUBLANG_NEUTRAL // locale: "mr-IN" STRINGTABLE { str_hello , L"नमस्कार" } LANGUAGE LANG_MOHAWK, SUBLANG_NEUTRAL // locale: "moh-CA" STRINGTABLE { str_hello , L"E:nen" } LANGUAGE LANG_MONGOLIAN, SUBLANG_NEUTRAL // locale: "mn-MN" STRINGTABLE { str_hello , L"Сайн уу" } LANGUAGE LANG_NEPALI, SUBLANG_NEUTRAL // locale: "ne-NP" STRINGTABLE { str_hello , L"नमस्ते" } LANGUAGE LANG_NORWEGIAN, SUBLANG_NEUTRAL // locale: "nb-NO" STRINGTABLE { str_hello , L"Hallo" } LANGUAGE LANG_OCCITAN, SUBLANG_NEUTRAL // locale: "oc-FR" STRINGTABLE { str_hello , L"Alau" } LANGUAGE LANG_ODIA, SUBLANG_NEUTRAL // locale: "or-IN" STRINGTABLE { str_hello , L"ନମସ୍କାର" } LANGUAGE LANG_POLISH, SUBLANG_NEUTRAL // locale: "pl-PL" STRINGTABLE { str_hello , L"Cześć" } LANGUAGE LANG_PORTUGUESE, SUBLANG_NEUTRAL // locale: "pt-PT" STRINGTABLE { str_hello , L"Olá" } LANGUAGE LANG_PUNJABI, SUBLANG_NEUTRAL // locale: "pa-IN" STRINGTABLE { str_hello , L"ਸਤਿ ਸ੍ਰੀ ਅਕਾਲ" } LANGUAGE LANG_ROMANIAN, SUBLANG_NEUTRAL // locale: "ro-RO" STRINGTABLE { str_hello , L"Bună ziua" } LANGUAGE LANG_ROMANSH, SUBLANG_NEUTRAL // locale: "rm-CH" STRINGTABLE { str_hello , L"Bun di" } LANGUAGE LANG_RUSSIAN, SUBLANG_NEUTRAL // locale: "ru-RU" STRINGTABLE { str_hello , L"Привет" } LANGUAGE LANG_SAKHA, SUBLANG_NEUTRAL // locale: "sah-RU" STRINGTABLE { str_hello , L"Саламыт" } LANGUAGE LANG_SAMI, SUBLANG_NEUTRAL // locale: "se-NO" STRINGTABLE { str_hello , L"Bures" } LANGUAGE LANG_SANSKRIT, SUBLANG_NEUTRAL // locale: "sa-IN" STRINGTABLE { str_hello , L"नमस्ते" } LANGUAGE LANG_SCOTTISH_GAELIC, SUBLANG_NEUTRAL // locale: "gd-GB" STRINGTABLE { str_hello , L"Halò" } LANGUAGE LANG_SERBIAN_NEUTRAL, SUBLANG_NEUTRAL // locale: "sr-Latn-RS" STRINGTABLE { str_hello , L"Zdravo" } LANGUAGE LANG_SINHALESE, SUBLANG_NEUTRAL // locale: "si-LK" STRINGTABLE { str_hello , L"හෙලෝ" } LANGUAGE LANG_SLOVAK, SUBLANG_NEUTRAL // locale: "sk-SK" STRINGTABLE { str_hello , L"Ahoj" } LANGUAGE LANG_SLOVENIAN, SUBLANG_NEUTRAL // locale: "sl-SI" STRINGTABLE { str_hello , L"Zdravo" } LANGUAGE LANG_SOTHO, SUBLANG_NEUTRAL // locale: "st-ZA" STRINGTABLE { str_hello , L"Dumela" } LANGUAGE LANG_SPANISH, SUBLANG_NEUTRAL // locale: "es-ES" STRINGTABLE { str_hello , L"Hola" } LANGUAGE LANG_SWAHILI, SUBLANG_NEUTRAL // locale: "sw-KE" STRINGTABLE { str_hello , L"Jambo" } LANGUAGE LANG_SWEDISH, SUBLANG_NEUTRAL // locale: "sv-SE" STRINGTABLE { str_hello , L"Hej" } LANGUAGE LANG_TAJIK, SUBLANG_NEUTRAL // locale: "tg-TJ" STRINGTABLE { str_hello , L"Салом" } LANGUAGE LANG_TAMIL, SUBLANG_NEUTRAL // locale: "ta-IN" STRINGTABLE { str_hello , L"வணக்கம்" } LANGUAGE LANG_TATAR, SUBLANG_NEUTRAL // locale: "tt-RU" STRINGTABLE { str_hello , L"Сәлам" } LANGUAGE LANG_TELUGU, SUBLANG_NEUTRAL // locale: "te-IN" STRINGTABLE { str_hello , L"హలో" } LANGUAGE LANG_THAI, SUBLANG_NEUTRAL // locale: "th-TH" STRINGTABLE { str_hello , L"สวัสดี" } LANGUAGE LANG_TIBETAN, SUBLANG_NEUTRAL // locale: "bo-CN" STRINGTABLE { str_hello , L"དེ་ལེགས་སོགས་པ།" } LANGUAGE LANG_TIGRIGNA, SUBLANG_NEUTRAL // locale: "ti-ER" STRINGTABLE { str_hello , L"ሰላም" } LANGUAGE LANG_TSWANA, SUBLANG_NEUTRAL // locale: "tn-ZA" STRINGTABLE { str_hello , L"Dumela" } LANGUAGE LANG_TURKISH, SUBLANG_NEUTRAL // locale: "tr-TR" STRINGTABLE { str_hello , L"Merhaba" } LANGUAGE LANG_TURKMEN, SUBLANG_NEUTRAL // locale: "tk-TM" STRINGTABLE { str_hello , L"Günaydın" } LANGUAGE LANG_UIGHUR, SUBLANG_NEUTRAL // locale: "ug-CN" STRINGTABLE { str_hello , L"سالام" } LANGUAGE LANG_UKRAINIAN, SUBLANG_NEUTRAL // locale: "uk-UA" STRINGTABLE { str_hello , L"Привіт" } LANGUAGE LANG_URDU, SUBLANG_NEUTRAL // locale: "ur-PK" STRINGTABLE { str_hello , L"ہیلو" } LANGUAGE LANG_UZBEK, SUBLANG_NEUTRAL // locale: "uz-UZ" STRINGTABLE { str_hello , L"Salom" } LANGUAGE LANG_VIETNAMESE, SUBLANG_NEUTRAL // locale: "vi-VN" STRINGTABLE { str_hello , L"Xin chào" } LANGUAGE LANG_WELSH, SUBLANG_NEUTRAL // locale: "cy-GB" STRINGTABLE { str_hello , L"Helo" } LANGUAGE LANG_WOLOF, SUBLANG_NEUTRAL // locale: "wo-SN" STRINGTABLE { str_hello , L"Ndamli" } LANGUAGE LANG_XHOSA, SUBLANG_NEUTRAL // locale: "xh-ZA" STRINGTABLE { str_hello , L"Molo" } LANGUAGE LANG_YORUBA, SUBLANG_NEUTRAL // locale: "yo-NG" STRINGTABLE { str_hello , L"Mo ke fun o" } LANGUAGE LANG_ZULU, SUBLANG_NEUTRAL // locale: "zu-ZA" STRINGTABLE { str_hello , L"Sawubona" } LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL // to localize English variants: // use LANG_ENGLISH, SUBLANG_ENGLISH_US, SUBLANG_ENGLISH_UK ... STRINGTABLE { str_restore , L"Restore" str_maximize , L"Maximize" str_minimize , L"Minimize" str_quit , L"Quit" str_close , L"Close" str_cancel , L"Cancel" str_ok , L"OK" str_full_screen , L"Full Screen (ESC to restore)" str_yes , L"&Yes" str_no , L"&No" str_hello , L"Hello" str_slider_accel , L" Hold key while clicking\n Ctrl: x 10 Shift: x 100 \n Ctrl+Shift: x 1000 \n for step multiplier." str_file_open , L"&Open" str_about , L"&About" str_button_messagebox , L"&Message Box" str_scroll , L"Scroll &Direction:" str_natural , L"Natural" str_reverse , L"Reverse" str_mbx , L"""Pneumonoultramicroscopicsilicovolcanoconiosis""\nis it the longest English language word or not?" str_window , L"Window" str_monitor , L"Monitor" str_client_area , L"Client area" str_mouse , L"Mouse" str_header , L"Header" str_left_top , L"Left Top" str_zoom , L"Zoom: 1 / (2^%d)" str_mandelbrot , L"Mandelbrot Explorer" str_help , L"Click inside or +/- to zoom;\nright mouse click to zoom out;\nuse touchpad or keyboard ←↑↓→ to pan" str_proportional , L"Proportional" str_monospaced , L"Monospaced" str_millisecond , L"ms" str_minimum , L"min" str_average , L"avg" str_maximum , L"max" str_restore_from_fs , L"Restore from &Full Screen" str_about_hint , L"Show About message box" str_yes_no_hint , L"Show Yes/No message box" str_fs_label , L"&Full Screen" str_copied , L"copied to clipboard" } ================================================ FILE: src/samples/sample1.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" static char title[128] = "Polyglot"; // https://youtu.be/D36zd8yNTbQ static const char* locales[] = { // 123 languages "af-ZA", "am-ET", "ar-SA", "as-IN", "az-AZ", "ba-RU", "be-BY", "bg-BG", "bn-BD", "bo-CN", "br-FR", "bs-BA", "bs-Latn-BA", "ca-ES", "ca-ES-Valencia", "cs-CZ", "cy-GB", "da-DK", "de-DE", "dv-MV", "el-GR", "en-US", "es-ES", "et-EE", "eu-ES", "fa-IR", "ff-Latn-SN", "fi-FI", "fil-PH", "fo-FO", "fr-FR", "fy-NL", "ga-IE", "gd-GB", "gl-ES", "gsw-FR", "gu-IN", "ha-Latn-NG", "haw-US", "he-IL", "hi-IN", "hr-HR", "hsb-DE", "hu-HU", "hy-AM", "ig-NG", "id-ID", "is-IS", "it-IT", "iu-Latn-CA", "ja-JP", "kk-KZ", "kl-GL", "km-KH", "kn-IN", "kok-IN", "ko-KR", "ku-CK", "ky-KG", "lb-LU", "lo-LA", "lt-LT", "lv-LV", "mni-IN", "mk-MK", "ml-IN", "mn-MN", "moh-CA", "mr-IN", "ms-MY", "mt-MT", "mi-NZ", "ne-NP", "nb-NO", "nl-NL", "oc-FR", "or-IN", "pa-IN", "pl-PL", "prs-AF", "ps-AF", "pt-PT", "ro-RO", "rm-CH", "ru-RU", "rw-RW", "sa-IN", "sah-RU", "se-NO", "si-LK", "sk-SK", "sl-SI", "sr-RS", "sr-Latn-RS", "st-ZA", "sv-SE", "sw-KE", "ta-IN", "te-IN", "tg-TJ", "th-TH", "ti-ER", "tk-TM", "tn-ZA", "tr-TR", "tt-RU", "ug-CN", "uk-UA", "ur-PK", "uz-UZ", "vi-VN", "wo-SN", "xh-ZA", "yo-NG", "zu-ZA" }; static int32_t locale; static ui_label_t label = ui_label(0.0, "Hello"); static void every_sec(ui_view_t* rt_unused(v)) { rt_nls.set_locale(locales[locale]); rt_str_printf(title, "Polyglot [%s]", locales[locale]); ui_app.set_title(title); ui_app.request_layout(); locale = (locale + 1) % rt_countof(locales); } static bool tap(ui_view_t* v, int32_t ix, bool pressed) { const bool inside = ui_view.inside(v, &ui_app.mouse); rt_println("ix: %d inside: %d %s", ix, inside, pressed ? "dw" : "up"); return inside; } static bool long_press(ui_view_t* v, int32_t ix) { const bool inside = ui_view.inside(v, &ui_app.mouse); rt_println("ix: %d inside: %d", ix, inside); return inside; } static bool double_tap(ui_view_t* v, int32_t ix) { const bool inside = ui_view.inside(v, &ui_app.mouse); rt_println("ix: %d inside: %d", ix, inside); return inside; } static void opened(void) { static ui_fm_t fm; ui_gdi.update_fm(&fm, ui_gdi.create_font("Segoe Script", ui_app.in2px(0.5), -1)); ui_app.content->every_sec = every_sec; label.fm = &fm; ui_view.add(ui_app.content, &label, null); locale = (int32_t)(rt_clock.nanoseconds() & 0xFFFF % rt_countof(locales)); label.tap = tap; label.long_press = long_press; label.double_tap = double_tap; } static void init(void) { ui_app.title = title; ui_app.opened = opened; } ui_app_t ui_app = { .class_name = "sample1", .dark_mode = true, .init = init, .window_sizing = { .min_w = 4.0f, // 4.0x1.5 inches .min_h = 1.5f, .ini_w = 4.0f, // 4x2 inches .ini_h = 2.0f } }; ================================================ FILE: src/samples/sample2.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" static int64_t hit_test(const ui_view_t* v, ui_point_t pt) { rt_swear(v == ui_app.content); if (ui_view.inside(v, &pt)) { if (pt.y < v->fm->em.h && ui_app.caption->state.hidden) { ui_app.caption->state.hidden = false; ui_app.request_layout(); } else if (pt.y > v->fm->em.h && !ui_app.caption->state.hidden) { ui_app.caption->state.hidden = true; ui_app.request_layout(); } return ui.hit_test.caption; } return ui.hit_test.nowhere; } static void opened(void) { // ui_app.content->insets = (ui_margins_t){ 0, 0, 0, 0 }; static ui_label_t hello = ui_label(0.0, "Hello"); // hello.padding = (ui_margins_t){ 0, 0, 0, 0 }; // hello.insets = (ui_margins_t){ 0, 0, 0, 0 }; static ui_fm_t fm; ui_gdi.update_fm(&fm, ui_gdi.create_font("Segoe Script", ui_app.in2px(0.5f), -1)); hello.fm = &fm; ui_app.set_layered_window(ui_color_rgb(30, 30, 30), 0.75f); ui_view.add_last(ui_app.content, &hello); ui_app.caption->state.hidden = true; ui_app.content->hit_test = hit_test; } static void character(ui_view_t* rt_unused(v), const char* utf8) { if (utf8[0] == 033) { // escape if (!ui_app.is_full_screen) { ui_app.quit(0); } if ( ui_app.is_full_screen) { ui_app.full_screen(false); } } } static bool key_pressed(ui_view_t* rt_unused(v), int64_t key) { bool swallow = key == ui.key.f11; if (swallow) { ui_app.full_screen(!ui_app.is_full_screen); } return swallow; } static void init(void) { ui_app.title = "Sample2: translucent"; ui_app.opened = opened; ui_app.root->character = character; ui_app.root->key_pressed = key_pressed; // for custom caption or no caption .no_decor can be set to true ui_app.no_decor = true; ui_caption.menu.state.hidden = true; } ui_app_t ui_app = { .class_name = "sample2", .dark_mode = true, .init = init, .window_sizing = { .min_w = 1.8f, // 2x1 inches .min_h = 1.0f, .ini_w = 4.0f, // 4x2 inches .ini_h = 2.0f } }; ================================================ FILE: src/samples/sample3.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" static volatile int32_t index; // index of image to paint, !ix to render static ui_bitmap_t image[2]; static uint8_t pixels[2][4 * 4096 * 4096]; static rt_thread_t thread; static rt_event_t wake; static rt_event_t quit; static volatile bool rendering; static volatile bool stop; static volatile fp64_t render_time; static void toggle_full_screen(ui_button_t* b) { b->state.pressed = !b->state.pressed; ui_app.full_screen(b->state.pressed); ui_view.set_text(b, "%s", !b->state.pressed ? rt_glyph_square_four_corners : rt_glyph_two_joined_squares); } ui_button_clicked(button_fs, rt_glyph_square_four_corners, 1.0, { toggle_full_screen(button_fs); }); static void paint(ui_view_t* view) { int32_t k = index; ui_gdi.bitmap(0, 0, view->w, view->h, 0, 0, image[k].w, image[k].h, &image[k]); int32_t tx = view->fm->em.w; int32_t ty = view->fm->em.h / 4; const ui_gdi_ta_t ta = { .fm = view->fm, .color = ui_colors.orange }; ui_gdi.text(&ta, tx, ty, "%s", "Try Full Screen Button there --->"); ty = view->h - view->fm->em.h * 3 / 2; ui_gdi.text(&ta, tx, ty, "render time %.1f ms / avg paint time %.1f ms", render_time * 1000, ui_app.paint_avg * 1000); if (!rendering) { ui_app.set_cursor(ui_app.cursors.arrow); } } static void request_rendering(void) { ui_app.set_cursor(ui_app.cursors.wait); rendering = true; rt_event.set(wake); } static void stop_rendering(void) { if (rendering) { stop = true; while (rendering || stop) { rt_thread.sleep_for(0.01); } ui_app.set_cursor(ui_app.cursors.arrow); } } static void measure(ui_view_t* view) { view->w = ui_app.root->w; view->h = ui_app.root->h; const int32_t w = view->w; const int32_t h = view->h; ui_bitmap_t* im = &image[index]; if (w != im->w || h != im->h) { stop_rendering(); im = &image[!index]; ui_gdi.bitmap_dispose(im); rt_fatal_if(w * h * 4 > rt_countof(pixels[!index]), "increase size of pixels[][%d * %d * 4]", w, h); ui_gdi.bitmap_init(im, w, h, 4, pixels[!index]); request_rendering(); } } static void layout(ui_view_t* v) { button_fs.x = v->w - button_fs.w - v->fm->em.w / 4; button_fs.y = v->fm->em.h / 4; } static void renderer(void* unused); // renderer thread static void character(ui_view_t* rt_unused(view), const char* utf8) { char ch = utf8[0]; if (ch == 'q' || ch == 'Q') { ui_app.close(); } if (ui_app.is_full_screen && ch == 033) { toggle_full_screen(&button_fs); } } static void closed(void) { rt_event.set(quit); rt_thread.join(thread, -1); thread = null; ui_gdi.bitmap_dispose(&image[0]); ui_gdi.bitmap_dispose(&image[1]); } static void fini(void) { rt_event.dispose(wake); rt_event.dispose(quit); wake = null; quit = null; } static void opened(void) { rt_fatal_if(ui_app.root->w * ui_app.root->h * 4 > rt_countof(pixels[0]), "increase size of pixels[][%d * %d * 4]", ui_app.root->w, ui_app.root->h); ui_app.fini = fini; ui_app.closed = closed; ui_view.add(ui_app.content, &button_fs, null); ui_app.content->layout = layout; ui_app.content->measure = measure; ui_app.content->paint = paint; ui_app.content->character = character; wake = rt_event.create(); quit = rt_event.create(); // images: ui_gdi.bitmap_init(&image[0], ui_app.root->w, ui_app.root->h, 4, pixels[0]); ui_gdi.bitmap_init(&image[1], ui_app.root->w, ui_app.root->h, 4, pixels[1]); thread = rt_thread.start(renderer, null); request_rendering(); rt_str_printf(button_fs.hint, "&Full Screen"); button_fs.shortcut = 'F'; } static void init(void) { ui_app.opened = opened; } ui_app_t ui_app = { .class_name = "sample3", .title = "Sample3: Mandelbrot", .dark_mode = true, .init = init, // 6x4 inches. Thinking of 6x4 timbers columns, beams, supporting posts :) .window_sizing = { .min_w = 6.0f, .min_h = 4.0f, .ini_w = 6.0f, .ini_h = 4.0f } }; static fp64_t scale(int32_t x, int32_t n, fp64_t low, fp64_t hi) { return x / (fp64_t)(n - 1) * (hi - low) + low; } static void mandelbrot(ui_bitmap_t* im) { fp64_t time = rt_clock.seconds(); for (int32_t r = 0; r < im->h && !stop; r++) { fp64_t y0 = scale(r, im->h, -1.12, 1.12); for (int32_t c = 0; c < im->w && !stop; c++) { fp64_t x0 = scale(c, im->w, -2.00, 0.47); fp64_t x = 0; fp64_t y = 0; int32_t iteration = 0; enum { max_iteration = 100 }; while (x* x + y * y <= 2 * 2 && iteration < max_iteration && !stop) { fp64_t t = x * x - y * y + x0; y = 2 * x * y + y0; x = t; iteration++; } static ui_color_t palette[16] = { ui_color_rgb( 66, 30, 15), ui_color_rgb( 25, 7, 26), ui_color_rgb( 9, 1, 47), ui_color_rgb( 4, 4, 73), ui_color_rgb( 0, 7, 100), ui_color_rgb( 12, 44, 138), ui_color_rgb( 24, 82, 177), ui_color_rgb( 57, 125, 209), ui_color_rgb(134, 181, 229), ui_color_rgb(211, 236, 248), ui_color_rgb(241, 233, 191), ui_color_rgb(248, 201, 95), ui_color_rgb(255, 170, 0), ui_color_rgb(204, 128, 0), ui_color_rgb(153, 87, 0), ui_color_rgb(106, 52, 3) }; ui_color_t color = palette[iteration % rt_countof(palette)]; uint8_t* px = &((uint8_t*)im->pixels)[r * im->w * 4 + c * 4]; px[3] = 0xFF; px[0] = (color >> 16) & 0xFF; px[1] = (color >> 8) & 0xFF; px[2] = (color >> 0) & 0xFF; } } render_time = rt_clock.seconds() - time; } static void renderer(void* unused) { (void)unused; rt_thread.name("renderer"); rt_thread.realtime(); rt_event_t es[2] = {wake, quit}; for (;;) { int32_t ix = rt_event.wait_any(rt_countof(es), es); if (ix != 0) { break; } int32_t k = !index; mandelbrot(&image[k]); if (!stop) { index = !index; ui_app.request_redraw(); } stop = false; rendering = false; } } ================================================ FILE: src/samples/sample4.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" #include "stb_image.h" const char* title = "Sample4"; static ui_bitmap_t image[2]; static char filename[260]; // c:\Users\user\Pictures\mandrill-4.2.03.png static void init(void); static int console(void) { rt_fatal_if(true, "%s only SUBSYSTEM:WINDOWS", rt_args.basename()); return 1; } ui_app_t ui_app = { .class_name = "sample4", .init = init, .dark_mode = true, .main = console, .window_sizing = { .min_w = 4.0f, .min_h = 4.0f, .ini_w = 6.0f, .ini_h = 6.0f } }; static void* load_image(const uint8_t* data, int64_t bytes, int32_t* w, int32_t* h, int32_t* bpp, int32_t preferred_bytes_per_pixel); static void load_images(void) { int r = 0; void* data = null; int64_t bytes = 0; for (int i = 0; i < rt_countof(image); i++) { if (i == 0) { r = rt_mem.map_ro(filename, &data, &bytes); } else { r = rt_mem.map_resource("sample_png", &data, &bytes); } rt_fatal_if_error(r); int w = 0; int h = 0; int bpp = 0; // bytes (!) per pixel void* pixels = load_image(data, bytes, &w, &h, &bpp, 0); rt_not_null(pixels); ui_gdi.bitmap_init(&image[i], w, h, bpp, pixels); stbi_image_free(pixels); // do not unmap resources: if (i == 0) { rt_mem.unmap(data, bytes); } } } static void paint(ui_view_t* view) { ui_gdi.fill(0, 0, view->w, view->h, ui_colors.black); if (image[1].w > 0 && image[1].h > 0) { int w = rt_min(view->w, image[1].w); int h = rt_min(view->h, image[1].h); int x = (view->w - w) / 2; int y = (view->h - h) / 2; ui_gdi.set_clip(0, 0, view->w, view->h); ui_gdi.bitmap(x, y, w, h, 0, 0, image[1].w, image[1].h, &image[1]); ui_gdi.set_clip(0, 0, 0, 0); } if (image[0].w > 0 && image[0].h > 0) { int x = (view->w - image[0].w) / 2; int y = (view->h - image[0].h) / 2; ui_gdi.bitmap(x, y, image[0].w, image[0].h, 0, 0, image[0].w, image[0].h, &image[0]); } } static void download(void) { static const char* url = "https://upload.wikimedia.org/wikipedia/commons/c/c1/" "Wikipedia-sipi-image-db-mandrill-4.2.03.png"; if (!rt_files.exists(filename)) { char cmd[256]; rt_str_printf(cmd, "curl.exe --silent --fail --create-dirs " "\"%s\" --output \"%s\" 2>nul >nul", url, filename); int r = system(cmd); if (r != 0) { rt_println("download %s failed %d %s", filename, r, rt_strerr(r)); } } } static void init(void) { ui_app.title = title; ui_app.content->paint = paint; rt_str_printf(filename, "%s\\mandrill-4.2.03.png", rt_files.known_folder(rt_files.folder.pictures)); download(); load_images(); } static void* load_image(const uint8_t* data, int64_t bytes, int32_t* w, int32_t* h, int32_t* bpp, int32_t preferred_bytes_per_pixel) { void* pixels = stbi_load_from_memory((uint8_t const*)data, (int32_t)bytes, w, h, bpp, preferred_bytes_per_pixel); return pixels; } ================================================ FILE: src/samples/sample5.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" // #include "single_file_lib/rt/rt.h" // #include "single_file_lib/ui/ui.h" const char* title = "Sample5"; static ui_view_t left = ui_view(list); static ui_view_t right = ui_view(list); static ui_view_t bottom = ui_view(stack); // font scale: static const fp64_t fs[] = {0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0}; // font scale index static int32_t fx = 2; // fs[2] == 1.0 static ui_fm_t mf; // mono font static ui_fm_t pf; // proportional font static ui_edit_view_t edit0; static ui_edit_view_t edit1; static ui_edit_view_t edit2; static ui_edit_doc_t edit_doc_0; static ui_edit_doc_t edit_doc_1; static ui_edit_doc_t edit_doc_2; static ui_edit_view_t* edit[] = { &edit0, &edit1, &edit2 }; static ui_edit_doc_t* doc[] = { &edit_doc_0, &edit_doc_1, &edit_doc_2 }; static int32_t focused(void) { // ui_app.focus can point to a button, thus see which edit // control was focused last int32_t ix = -1; for (int32_t i = 0; i < rt_countof(edit) && ix < 0; i++) { if (ui_app.focus == &edit[i]->view) { ix = i; } if (edit[i]->focused) { ix = i; } } static int32_t last_ix = -1; if (ix < 0) { ix = last_ix; } last_ix = ix; return ix; } static void focus_back_to_edit(void) { const int32_t ix = focused(); if (ix >= 0) { ui_view.set_focus(&edit[ix]->view); // return focus where it was } ui_app.request_layout(); } static void scaled_fonts(void) { rt_assert(0 <= fx && fx < rt_countof(fs)); if (mf.font != null) { ui_gdi.delete_font(mf.font); } int32_t h = (int32_t)(ui_app.fm.mono.normal.height * fs[fx] + 0.5); ui_gdi.update_fm(&mf, ui_gdi.font(ui_app.fm.mono.normal.font, h, -1)); if (pf.font != null) { ui_gdi.delete_font(pf.font); } h = (int32_t)(ui_app.fm.prop.normal.height * fs[fx] + 0.5); ui_gdi.update_fm(&pf, ui_gdi.font(ui_app.fm.prop.normal.font, h, -1)); } ui_button_clicked(full_screen, "&Full Screen", 7.0f, { ui_app.full_screen(!ui_app.is_full_screen); }); ui_button_clicked(quit, "&Quit", 7.0f, { if (!ui_fuzzing.from_inside()) { // ignore fuzzer clicks ui_app.close(); } }); ui_button_clicked(fuzz, "Fu&zz", 7.0f, { if (!ui_fuzzing.from_inside()) { // ignore fuzzer clicks if (!ui_fuzzing.is_running()) { ui_fuzzing.start(0x1); } else { ui_fuzzing.stop(); } fuzz->state.pressed = ui_fuzzing.is_running(); focus_back_to_edit(); } }); ui_toggle_on_off(ro, "&Read Only", 7.0f, { if (!ui_fuzzing.from_inside()) { // ignore fuzzer clicks int32_t ix = focused(); if (ix >= 0) { edit[ix]->ro = ro->state.pressed; // rt_println("edit[%d].readonly: %d", ix, edit[ix]->ro); focus_back_to_edit(); } } }); ui_toggle_on_off(ww, "Hide &Word Wrap", 7.0f, { int32_t ix = focused(); if (ix >= 0) { edit[ix]->hide_word_wrap = ww->state.pressed; // rt_println("edit[%d].hide_word_wrap: %d", ix, edit[ix]->hide_word_wrap); focus_back_to_edit(); } }); ui_toggle_on_off(mono, "&Mono", 7.0f, { int32_t ix = focused(); if (ix >= 0) { ui_edit_view.set_font(edit[ix], mono->state.pressed ? &mf : &pf); focus_back_to_edit(); } else { mono->state.pressed = !mono->state.pressed; } }); ui_toggle_on_off(sl, "&Single Line", 7.0f, { if (!ui_fuzzing.from_inside()) { // ignore fuzzer clicks int32_t ix = focused(); if (ix == 2) { sl->state.pressed = true; // always single line } else if (0 <= ix && ix < 2) { ui_edit_view_t* e = edit[ix]; e->sle = sl->state.pressed; // rt_println("edit[%d].multiline: %d", ix, e->multiline); if (e->sle) { ui_edit_view.select_all(e); ui_edit_view.replace(e, "Hello World! Single Line Edit", -1); } ui_app.request_layout(); focus_back_to_edit(); } } }); static void font_plus(void) { if (fx < rt_countof(fs) - 1) { fx++; scaled_fonts(); ui_app.request_layout(); } } static void font_minus(void) { if (fx > 0) { fx--; scaled_fonts(); ui_app.request_layout(); } } static void font_reset(void) { fx = 2; scaled_fonts(); ui_app.request_layout(); } ui_button_clicked(fp, "Font Ctrl+", 7.0f, { font_plus(); }); ui_button_clicked(fm, "Font Ctrl-", 7.0f, { font_minus(); }); static ui_label_t label = ui_label(0.0, "..."); static void set_text(int32_t ix) { static char last[128]; ui_view.set_text(&label, "%d:%d %d:%d %dx%d\n" "scroll %03d:%03d", edit[ix]->selection.a[0].pn, edit[ix]->selection.a[0].gp, edit[ix]->selection.a[1].pn, edit[ix]->selection.a[1].gp, edit[ix]->view.w, edit[ix]->view.h, edit[ix]->scroll.pn, edit[ix]->scroll.rn); if (0) { rt_println("%d:%d %d:%d %dx%d scroll %03d:%03d", edit[ix]->selection.a[0].pn, edit[ix]->selection.a[0].gp, edit[ix]->selection.a[1].pn, edit[ix]->selection.a[1].gp, edit[ix]->view.w, edit[ix]->view.h, edit[ix]->scroll.pn, edit[ix]->scroll.rn); } // can be called before text.ui initialized if (strcmp(last, ui_view.string(&label)) != 0) { ui_view.invalidate(&label, null); } rt_str_printf(last, "%s", ui_view.string(&label)); } static void paint(ui_view_t* v) { ui_gdi.fill(0, 0, v->w, v->h, ui_colors.black); int32_t ix = focused(); for (int32_t i = 0; i < rt_countof(edit); i++) { ui_view_t* e = &edit[i]->view; ui_color_t c = edit[i]->ro ? ui_colors.tone_red : ui_colors.blue; ui_gdi.frame(e->x - 1, e->y - 1, e->w + 2, e->h + 2, i == ix ? c : ui_color_rgb(63, 63, 70)); } if (ix >= 0) { set_text(ix); } if (ix >= 0) { ro.state.pressed = edit[ix]->ro; sl.state.pressed = edit[ix]->sle; mono.state.pressed = edit[ix]->view.fm->font == mf.font; } } static void open_file(const char* pathname) { char* file = null; int64_t bytes = 0; if (rt_mem.map_ro(pathname, &file, &bytes) == 0) { if (0 < bytes && bytes <= INT64_MAX) { ui_edit_view.select_all(edit[0]); ui_edit_view.replace(edit[0], file, (int32_t)bytes); ui_edit_pg_t start = { .pn = 0, .gp = 0 }; ui_edit_view.move(edit[0], start); } rt_mem.unmap(file, bytes); } else { ui_app.toast(5.3, "\nFailed to open file \"%s\".\n%s\n", pathname, rt_strerr(rt_core.err())); } } static void every_100ms(void) { // rt_println(""); static ui_view_t* last; if (last != ui_app.focus) { ui_app.request_redraw(); } // last = ui_app.focus; } static bool key_pressed(ui_view_t* rt_unused(view), int64_t key) { bool swallow = false; if (ui_app.focused() && key == ui.key.escape) { ui_app.close(); } int32_t ix = focused(); if (key == ui.key.f5) { if (ui_app.ctrl && ui_app.shift && !ui_fuzzing.is_running()) { ui_fuzzing.start(0); // on Ctrl+Shift+F5 } else if (ui_fuzzing.is_running()) { ui_fuzzing.stop(); // on F5 } swallow = true; } if (ui_app.ctrl) { if (key == ui.key.minus) { font_minus(); swallow = true; } else if (key == ui.key.plus) { font_plus(); swallow = true; } else if (key == '0') { font_reset(); swallow = true; } } if (ix >= 0) { set_text(ix); } return swallow; } static void edit_enter(ui_edit_view_t* e) { rt_assert(e->sle); if (!ui_app.shift) { // ignore shift ENTER: rt_println("text: %.*s", e->doc->text.ps[0].b, e->doc->text.ps[0].u); } } // see edit.test.c void ui_edit_init_with_lorem_ipsum(ui_edit_text_t* t); static bool can_close(void) { if (!ui_fuzzing.from_inside()) { if (ui_fuzzing.is_running()) { ui_fuzzing.stop(); } return true; } else { return false; // ignore Quit if fuzzing clicked on it } } static void opened(void) { // ui_app.view->measure = measure; // ui_app.view->layout = layout; ui_app.content->paint = paint; ui_app.content->key_pressed = key_pressed; scaled_fonts(); label.fm = &ui_app.fm.mono.normal; rt_str_printf(fuzz.hint, "Ctrl+Shift+F5 to start / F5 to stop Fuzzing"); for (int32_t i = 0; i < rt_countof(edit); i++) { ui_edit_doc.init(doc[i], null, 0, false); if (i < 2) { ui_edit_init_with_lorem_ipsum(&doc[i]->text); } ui_edit_view.init(edit[i], doc[i]); edit[i]->view.max_w = ui.infinity; if (i < 2) { edit[i]->view.max_h = ui.infinity; } edit[i]->view.fm = &pf; } ui_app.every_100ms = every_100ms; edit[2]->sle = true; rt_str_printf(edit[0]->view.p.text, "edit.#0#"); rt_str_printf(edit[1]->view.p.text, "edit.#1#"); rt_str_printf(edit[2]->view.p.text, "edit.sle"); // edit[2]->select_all(edit[2]); // edit[2]->paste(edit[2], "Single line", -1); ui_edit_view.enter = edit_enter; static ui_view_t span = ui_view(span); static ui_view_t spacer1 = ui_view(spacer); static ui_view_t spacer2 = ui_view(spacer); ui_view.add(ui_app.content, ui_view.add(&span, ui_view.add(&left, &edit0, &edit1, &label, null), &spacer1, ui_view.add(&right, &full_screen, &quit, &fuzz, &fp, &fm, &mono, &sl, &ro, &ww, &edit2, &spacer2, null), null), null); ui_view_for_each(&right, it, { it->align = ui.align.left; }); edit2.view.max_w = ui.infinity; span.max_w = ui.infinity; span.max_h = ui.infinity; label.align = ui.align.left; edit2.view.align = ui.align.left; left.max_w = ui.infinity; left.max_h = ui.infinity; right.max_h = ui.infinity; set_text(0); // need to be two lines for measure ui_view.set_focus(&edit[0]->view); edit0.debug.id = "#edit0"; edit1.debug.id = "#edit1"; edit2.debug.id = "#edit2"; if (rt_args.c > 1) { open_file(rt_args.v[1]); } } static void init(void) { ui_app.title = title; ui_app.opened = opened; ui_app.can_close = can_close; } ui_app_t ui_app = { .class_name = "sample5", .dark_mode = true, .init = init, .window_sizing = { .min_w = 3.0f, .min_h = 2.5f, .ini_w = 4.0f, .ini_h = 5.0f } }; ================================================ FILE: src/samples/sample6.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" #include "stb_image.h" // Code in this sample illustrates how to load animated gifs // and use them as a movie backdrop and sprites. // Sample coed uses ui_app.post() to execute animation steps on the // dispatch thread. // It also plays mutable MIDI song in a loop. // Simple mute button implemented by hand to avoid containers layout // logic. const char* title = "Sample6: I am groot"; typedef struct animated_gif_s { int32_t bpp; // bytes per pixel int32_t w; int32_t h; int32_t frames; int32_t* delays; // delays[frames]; uint8_t* pixels; } animated_gif_t; enum { max_speed = 3 }; typedef struct animation_s { animated_gif_t* gif; int32_t index; // animated_groot index 0..groot.frames - 1 uint32_t seed; // for rt_num.random32() int32_t x; int32_t y; int32_t w; int32_t h; int32_t speed_x; int32_t speed_y; } animation_t; static animated_gif_t groot; static animation_t animated_groot = { .gif = &groot }; static animated_gif_t movie; static animation_t animated_movie = { .gif = &movie }; static rt_thread_t thread; // animated gifs loader thread static bool muted; static fp64_t volume; // be mute static ui_midi_t midi; static ui_bitmap_t background; static void init(void); static void fini(void); static void character(ui_view_t* view, const char* utf8); static void stop_and_close(void); static void open_and_play(void); static void* load_image(const uint8_t* data, int64_t bytes, int32_t* w, int32_t* h, int32_t* bpp, int32_t preferred_bytes_per_pixel); static void* load_animated_gif(const uint8_t* data, int64_t bytes, int32_t** delays, int32_t* w, int32_t* h, int32_t* frames, int32_t* bpp, int32_t preferred_bytes_per_pixel); static const char* midi_file(void); static void paint_groot(animation_t* a) { const animated_gif_t* g = a->gif; const uint8_t* p = g->pixels + g->w * g->h * g->bpp * a->index; ui_bitmap_t frame = { 0 }; // alpha blend needs GPu allocated bitmap ui_gdi.bitmap_init(&frame, g->w, g->h, g->bpp, p); const int32_t x = a->x - a->w / 2; const int32_t y = a->y - a->h / 2; ui_gdi.alpha(x, y, a->w, a->h, 0, 0, frame.w, frame.h, &frame, 1.0); ui_gdi.bitmap_dispose(&frame); } static void paint_movie(animation_t* a) { ui_gdi.fill(0, 0, ui_app.crc.w, ui_app.crc.h, ui_colors.black); const animated_gif_t* g = a->gif; const uint8_t* p = g->pixels + g->w * g->h * g->bpp * a->index; ui_gdi.pixels(a->x, a->y, a->w, a->h, 0, 0, g->w, g->h, g->w, g->h, g->w * g->bpp, g->bpp, p); } static void paint_mute_unmute(ui_view_t* v) { ui_gdi_ta_t ta = ui_gdi.ta.prop.H3; ta.color_id = 0; ta.color = muted ? ui_colors.green : ui_colors.red; #define str_unmuted rt_glyph_mute " mute" #define str_muted rt_glyph_speaker " unmute" const int32_t mx = v->x + ui_app.fm.prop.H3.em.w / 16; const int32_t my = v->y + ui_app.fm.prop.H3.em.h / 16; const int32_t mw = ui_app.fm.prop.H3.em.w * 5; const int32_t mh = ui_app.fm.prop.H3.em.h; ui_gdi.rounded(mx, my, mw, mh, (mh / 3) | 0x1, ta.color, ui_colors.transparent); ui_gdi.text(&ta, ui_app.fm.prop.H3.em.w / 4, 0, "%s", muted ? str_muted : str_unmuted); } static void paint(ui_view_t* v) { const int32_t w = rt_min(v->w, background.w); const int32_t h = rt_min(v->h, background.h); const int32_t x = (v->w - w) / 2; const int32_t y = (v->h - h) / 2; ui_gdi.bitmap(x, y, w, h, 0, 0, background.w, background.h, &background); if (animated_movie.gif->pixels != null) { paint_movie(&animated_movie); } if (animated_groot.gif->pixels != null) { paint_groot(&animated_groot); } paint_mute_unmute(v); } static void character(ui_view_t* rt_unused(v), const char* utf8) { if (utf8[0] == 'q' || utf8[0] == 'Q' || utf8[0] == 033) { ui_app.close(); } } static bool tap(ui_view_t* rt_unused(v), int32_t ix, bool pressed) { const int32_t w = ui_app.fm.prop.H3.em.w * 5; const int32_t h = ui_app.fm.prop.H3.em.h; const bool inside = 0 <= ui_app.mouse.x && ui_app.mouse.x < w && 0 <= ui_app.mouse.y && ui_app.mouse.y < h; const bool swallow = inside && pressed; if (swallow) { muted = !muted; if (muted) { if (ui_midi.is_playing(&midi)) { rt_fatal_if_error(ui_midi.get_volume(&midi, &volume)); rt_fatal_if_error(ui_midi.set_volume(&midi, 0)); } } else { rt_fatal_if_error(ui_midi.set_volume(&midi, volume)); } } return swallow; // swallows taps inside mute `button` } static int64_t notify(ui_midi_t* m, int64_t f) { // f: f rt_swear(&midi == m); #ifdef UI_MIDI_DEBUG if (f & ui_midi.success) { rt_println("success"); } if (f & ui_midi.failure) { rt_println("failure"); } if (f & ui_midi.aborted) { rt_println("aborted"); } if (f & ui_midi.superseded) { rt_println("superseded"); } #endif // UI_MIDI_DEBUG if ((f & (ui_midi.aborted|ui_midi.failure)) != 0) { stop_and_close(); } else if ((f & ui_midi.success) != 0) { // success : is received on the end of mini sequence playback // "when the music over..." rewind and start playing again rt_fatal_if_error(ui_midi.stop(&midi)); rt_fatal_if_error(ui_midi.rewind(&midi)); rt_fatal_if_error(ui_midi.play(&midi)); } return 0; } static void stop_and_close(void) { if (ui_midi.is_open(&midi)) { if (ui_midi.is_playing(&midi)) { midi.notify = null; rt_fatal_if_error(ui_midi.stop(&midi)); ui_midi.close(&midi); } } } static void open_and_play(void) { if (!ui_midi.is_open(&midi)) { // first call to MIDI Sequencer .open() takes 1.122 seconds // next attempt to .open() after .close() takes 4.237 seconds! // what can possibly take 1 second on 2GH cpu? rt_fatal_if_error(ui_midi.open(&midi, midi_file())); } if (!ui_midi.is_playing(&midi)) { midi.notify = notify; rt_fatal_if_error(ui_midi.play(&midi)); // it is possible that last mute call leaves the volume to 0 rt_fatal_if_error(ui_midi.set_volume(&midi, 0.5)); } } static void delete_midi_file(void) { rt_fatal_if_error(rt_files.unlink(midi_file())); } static void load_gif(animated_gif_t* g, const char* name) { void* data = null; int64_t bytes = 0; errno_t r = rt_mem.map_resource(name, &data, &bytes); rt_fatal_if_error(r); // load_animated_gif() calls realloc(delays) w/o first alloc() r = rt_heap.allocate(null, (void**)&g->delays, sizeof(int32_t), false); rt_swear(r == 0 && g->delays != null); g->pixels = load_animated_gif(data, bytes, &g->delays, &g->w, &g->h, &g->frames, &g->bpp, 4); // resources cannot be unmapped do not call rt_mem.unmap() } static void load_gifs(void) { load_gif(&movie, "gotg_gif"); rt_swear(movie.pixels != null && movie.bpp == 4 && movie.frames >= 1, "%s", stbi_failure_reason()); load_gif(&groot, "groot_gif"); rt_swear(groot.pixels != null && groot.bpp == 4 && groot.frames >= 1, "%s", stbi_failure_reason()); } static void schedule_next_animation(rt_work_t* work) { animation_t* a = (animation_t*)work->data; rt_swear(0 <= a->index && a->index < a->gif->frames); // milliseconds to seconds: fp64_t ds = a->gif->delays[a->index] * 0.001; // delay in seconds work->when = rt_clock.seconds() + ds; ui_app.post(work); } static void dancing_step(rt_work_t* work) { rt_swear(rt_thread.id() == ui_app.tid); animation_t* a = (animation_t*)work->data; animated_gif_t* g = (animated_gif_t*)a->gif; int32_t multiplier = 1; while (g->w * multiplier < ui_app.crc.w / 2 && g->h * multiplier < ui_app.crc.h / 2) { multiplier++; } a->w = g->w * multiplier; a->h = g->h * multiplier; if (a->x < 0 && a->y < 0) { a->x = (ui_app.crc.w - a->w) / 2; a->y = (ui_app.crc.h - a->h) / 2; } // rt_println("%d %d speed: %d %d", a->x, a->y, // a->speed_x, // a->speed_y); a->index = (a->index + 1) % g->frames; while (a->speed_x == 0) { const uint32_t r = rt_num.random32(&a->seed); a->speed_x = r % (max_speed * 2 + 1) - max_speed; } while (a->speed_y == 0) { const uint32_t r = rt_num.random32(&a->seed); a->speed_y = r % (max_speed * 2 + 1) - max_speed; } a->x += a->speed_x; a->y += a->speed_y; if (a->x - a->w / 2 < 0) { a->x = a->w / 2; a->speed_x = -a->speed_x; } else if (a->x + a->w / 2 >= ui_app.root->w) { a->x = ui_app.root->w - a->w / 2 - 1; a->speed_x = -a->speed_x; } if (a->y - a->h / 2 < 0) { a->y = a->h / 2; a->speed_y = -a->speed_y; } else if (a->y + a->h / 2 >= ui_app.root->h) { a->y = ui_app.root->h - a->h / 2 - 1; a->speed_y = -a->speed_y; } int inc = rt_num.random32(&a->seed) % 2 == 0 ? -1 : +1; if (rt_num.random32(&a->seed) % 2 == 0) { if (1 <= a->speed_x + inc && a->speed_x + inc < max_speed) { a->speed_x += inc; } } else { if (1 <= a->speed_y + inc && a->speed_y + inc < max_speed) { a->speed_y += inc; } } ui_app.request_redraw(); schedule_next_animation(work); } static void movie_step(rt_work_t* work) { rt_swear(rt_thread.id() == ui_app.tid); animation_t* a = (animation_t*)work->data; animated_gif_t* g = (animated_gif_t*)a->gif; int32_t multiplier = 1; while (g->w * multiplier < ui_app.crc.w && g->h * multiplier < ui_app.crc.h) { multiplier++; } a->w = g->w * multiplier; a->h = g->h * multiplier; a->x = (ui_app.crc.w - a->w) / 2; a->y = (ui_app.crc.h - a->h) / 2; a->index = (a->index + 1) % g->frames; ui_app.request_redraw(); schedule_next_animation(work); } static void animated_gifs_loader(void* rt_unused(unused)) { ui_cursor_t cursor = ui_app.cursor; ui_app.set_cursor(ui_app.cursors.wait); load_gifs(); ui_app.set_cursor(cursor); static rt_work_t dancing = { .work = dancing_step, .data = &animated_groot }; ui_app.post(&dancing); static rt_work_t cinema = { .work = movie_step, .data = &animated_movie }; ui_app.post(&cinema); } static void load_png(void) { // from resources void* data = null; int64_t bytes = 0; rt_fatal_if_error(rt_mem.map_resource("sample_png", &data, &bytes)); int w = 0; int h = 0; int bpp = 0; // bytes (!) per pixel void* pixels = load_image(data, bytes, &w, &h, &bpp, 0); if (pixels == null) { rt_println("%s", stbi_failure_reason()); } rt_not_null(pixels); ui_gdi.bitmap_init(&background, w, h, bpp, pixels); stbi_image_free(pixels); } static void start_music(rt_work_t* rt_unused(w)) { open_and_play(); // 1+ second long expensive call } static void opened(void) { animated_groot.seed = (uint32_t)rt_clock.nanoseconds(); animated_groot.x = -1; animated_groot.y = -1; animated_groot.gif = &groot; thread = rt_thread.start(animated_gifs_loader, null); // start music after first paint() call: static rt_work_t music = { .work = start_music }; music.when = rt_clock.seconds() + 0.125; ui_app.post(&music); } static void closed(void) { rt_thread.join(thread, -1); if (ui_midi.is_open(&midi)) { // restore pre-muted volume: rt_fatal_if_error(ui_midi.set_volume(&midi, volume != 0 ? volume : 0.5)); } stop_and_close(); } static void init(void) { ui_app.title = title; ui_app.content->paint = paint; ui_app.content->character = character; ui_app.content->tap = tap; ui_app.opened = opened; ui_app.closed = closed; load_png(); } static void fini(void) { ui_gdi.bitmap_dispose(&background); stbi_image_free(groot.pixels); stbi_image_free(groot.delays); stbi_image_free(movie.pixels); stbi_image_free(movie.delays); delete_midi_file(); } static void* load_image(const uint8_t* data, int64_t bytes, int32_t* w, int32_t* h, int32_t* bpp, int32_t preferred_bpp) { void* pixels = stbi_load_from_memory(data, (int32_t)bytes, w, h, bpp, preferred_bpp); return pixels; } static void* load_animated_gif(const uint8_t* data, int64_t bytes, int32_t** delays, int32_t* w, int32_t* h, int32_t* frames, int32_t* bpp, int32_t preferred_bpp) { stbi_uc* pixels = stbi_load_gif_from_memory(data, (int32_t)bytes, delays, w, h, frames, bpp, preferred_bpp); return pixels; } static const char* midi_file(void) { // resource -> temporary file unpacking because ancient MIDI Win32 API // does not support memory buffers (or I didn't find it) static char filename[rt_files_max_path]; if (filename[0] == 0) { void* data = null; int64_t bytes = 0; int r = rt_mem.map_resource("mr_blue_sky_midi", &data, &bytes); rt_fatal_if_error(r); rt_fatal_if_error(rt_files.create_tmp(filename, rt_countof(filename))); rt_assert(filename[0] != 0); int64_t written = 0; rt_fatal_if_error(rt_files.write_fully(filename, data, bytes, &written)); rt_assert(written == bytes); } return filename; } ui_app_t ui_app = { .class_name = "sample6", .dark_mode = true, .init = init, .fini = fini, .window_sizing = { .min_w = 4.0f, .min_h = 3.0f, .ini_w = 4.0f, .ini_h = 3.0f } }; ================================================ FILE: src/samples/sample6.rc ================================================ #include "sample.rc" groot_gif RCDATA "groot.gif" // animated dancing groot gotg_gif RCDATA "gotg.gif" // animated Guardians of The Galaxy 2 movie mr_blue_sky_midi RCDATA "mr_blue_sky.midi" // free music // groot.gif made with https://ezgif.com/ ffmpeg and GIMP (re export with Dispose every frame // https://tinyurl.com/dancing-baby-groot // Mr. Blue Sky Midi: https://freemidi.org/download3-11210-mr-blue-sky-electric-light-orchestra ================================================ FILE: src/samples/sample7.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" #include const char* title = "Sample7 : timers"; enum { max_count = 1800 }; static ui_timer_t timer10ms; static rt_thread_t thread; static bool quit; typedef struct { fp64_t time[max_count]; // circular buffer of timestamps int pos; // writing position in the buffer for next timer event int samples; // number of samples collected fp64_t dt[max_count]; // delta(t) between 2 timer events fp64_t min_dt; fp64_t max_dt; fp64_t avg; fp64_t spread; } time_stats_t; static volatile time_stats_t ts[2]; static ui_point_t points[max_count]; // graph polyline coordinates static int32_t N = max_count; static void composed(ui_view_t* view) { if (view->w > 0) { N = rt_min(view->w, N); } rt_println("M: %d", N); } static void stats(int32_t ix) { volatile time_stats_t* t = &ts[ix]; rt_assert(t->samples >= 2, "no samples"); int n = rt_min(N, t->samples); t->min_dt = 1.0; // 1 second is 100x of 10ms t->max_dt = 0; int j = 0; fp64_t sum = 0; for (int i = 0; i < n - 1; i++) { int p0 = (t->pos - i - 1 + n) % n; int p1 = (p0 - 1 + n) % n; t->dt[j] = t->time[p0] - (t->time[p1] + 0.01); // expected 10ms t->min_dt = rt_min(t->dt[j], t->min_dt); t->max_dt = rt_max(t->dt[j], t->max_dt); sum += t->time[p0] - t->time[p1]; j++; } t->avg = sum / (n - 1); j = 0; fp64_t d0 = fabs(t->min_dt); fp64_t d1 = fabs(t->max_dt); fp64_t spread = rt_max_fp64(d0, d1) * 2; t->spread = rt_max(t->spread, spread); // if (t->samples % 1000 == 0) { // rt_println("[%d] samples: %6d spread: %.6f min %.6f max %.6f", // ix, t->samples, t->spread, t->min_dt, t->max_dt); // } } static void print(int32_t *x, int32_t *y, const char* format, ...) { va_list va; va_start(va, format); *x += ui_gdi.text_va(&ui_gdi.ta.mono.normal, *x, *y, format, va).w; va_end(va); } static void println(int32_t *x, int32_t *y, const char* format, ...) { va_list va; va_start(va, format); *y += ui_gdi.text_va(&ui_gdi.ta.mono.normal, *x, *y, format, va).h; va_end(va); } static void graph(ui_view_t* v, int ix, ui_color_t c, int y) { volatile time_stats_t* t = &ts[ix]; const int h2 = ui_app.root->h / 2; const int h4 = h2 / 2; const int h8 = h4 / 2; ui_gdi.line(0, y, ui_app.root->w, y, ui_colors.white); if (t->samples > 2) { const fp64_t spread = ts[ix].spread; int n = rt_min(N, t->samples); int j = 0; for (int i = 0; i < n; i++) { points[j].x = n - 1 - i; points[j].y = y - (int32_t)(t->dt[j] * h8 / spread); j++; } ui_gdi.poly(points, n - 1, c); int32_t tx = v->fm->em.w; int32_t ty = y - h8 - v->fm->em.h; println(&tx, &ty, "min %.3f max %.3f avg %.3f ms " "%.1f sps", t->min_dt * 1000, t->max_dt * 1000, t->avg, 1 / t->avg); } } static void paint(ui_view_t* v) { for (int i = 0; i < rt_countof(ts); i++) { if (ts[i].samples >= 2) { stats(i); } } if (ts[0].spread > 0 && ts[1].spread > 0) { char paint_stats[256]; rt_str_printf(paint_stats, "avg paint time %.1f ms %.1f fps", ui_app.paint_avg * 1000, ui_app.paint_fps); ui_gdi_ta_t ta = ui_gdi.ta.mono.normal; ta.measure = true; ui_wh_t wh = ui_view.text_metrics(0, 0, false, 0, &ui_app.fm.mono.normal, "%s", paint_stats); int32_t x = v->w - wh.w - v->fm->em.w; int32_t y = v->fm->em.h; print(&x, &y, "%s", paint_stats); x = v->fm->em.w; print(&x, &y, "10ms window timer jitter "); print(&x, &y, "(\"sps\" is samples per second)"); const int h2 = ui_app.root->h / 2; const int h4 = h2 / 2; graph(v, 0, ui_colors.tone_red, h4); y = h2; print(&x, &y, "10ms r/t thread sleep jitter"); graph(v, 1, ui_colors.tone_green, h2 + h4); y = h2 - h4; } } static void timer_thread(void* p) { bool* done = (bool*)p; rt_thread.name("r/t timer"); rt_thread.realtime(); while (!*done) { rt_thread.sleep_for(0.0094); ts[1].time[ts[1].pos] = rt_clock.seconds(); ts[1].pos = (ts[1].pos + 1) % N; (ts[1].samples)++; ui_app.request_redraw(); } } static void timer(ui_view_t* view, ui_timer_t id) { rt_swear(view == ui_app.content); // there are at least 3 timers notifications coming here: // 1 seconds, 100ms and 10ms: if (id == timer10ms) { ts[0].time[ts[0].pos] = ui_app.now; ts[0].pos = (ts[0].pos + 1) % N; (ts[0].samples)++; ui_app.request_redraw(); } } static void opened(void) { timer10ms = ui_app.set_timer((uintptr_t)&timer10ms, 10); rt_fatal_if(timer10ms == 0); thread = rt_thread.start(timer_thread, &quit); rt_not_null(thread); } static void detached_sleep(void* rt_unused(p)) { rt_thread.sleep_for(100.0); // seconds } static void detached_loop(void* rt_unused(p)) { uint64_t sum = 0; for (uint64_t i = 0; i < UINT64_MAX; i++) { sum += i; } // making sure that compiler won't get rid of the loop: rt_println("%lld", sum); } static void closed(void) { ui_app.kill_timer(timer10ms); quit = true; rt_fatal_if_error(rt_thread.join(thread, -1)); thread = null; quit = false; // just to test that ExitProcess(0) works when there is // are detached threads rt_thread_t detached = rt_thread.start(detached_sleep, null); rt_thread.detach(detached); detached = rt_thread.start(detached_loop, null); rt_thread.detach(detached); } static void do_not_start_minimized(void) { // This sample does not start minimized but some applications may. if (ui_app.last_visibility != ui.visibility.minimize) { ui_app.visibility = ui_app.last_visibility; } else { ui_app.visibility = ui.visibility.defau1t; ui_app.last_visibility = ui.visibility.defau1t; } } static void init(void) { ui_app.title = title; rt_thread.realtime(); // both main thread and timer thread ui_app.closed = closed; ui_app.opened = opened; ui_app.content->timer = timer; ui_app.content->paint = paint; ui_app.content->composed = composed; // no minimize/maximize title bar and system menu ui_app.no_min = true; ui_app.no_max = true; do_not_start_minimized(); } ui_app_t ui_app = { .class_name = "sample7", .init = init, .dark_mode = true, .window_sizing = { .min_w = 9.0f, // 9x5 inches .min_h = 5.0f, .ini_w = 10.0f, // 10x6 inches - fits 11" laptops .ini_h = 6.0f } }; ================================================ FILE: src/samples/sample8.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" static const char* title = "Sample8: Panels"; enum { version = 0x102 }; typedef rt_begin_packed struct app_data_t { int32_t version; int32_t menu_used; int32_t selected_view; int32_t light; int32_t debug; int32_t large; // show large H3 controls int32_t margins; // draw controls padding and insets int32_t fm; // draw controls font metrics } rt_end_packed app_data_t; static app_data_t app_data = { .version = version }; static void init(void); static void opened(void); static void stack_test(ui_view_t* parent); static void span_test(ui_view_t* parent); static void list_test(ui_view_t* parent); static void controls_test(ui_view_t* parent); static void edit1_test(ui_view_t* parent); static void fini(void) { ui_app.data_save("sample8", &app_data, sizeof(app_data)); } static void init(void) { app_data_t data = {0}; if (ui_app.data_load("sample8", &data, sizeof(data)) == sizeof(data) && data.version == version) { app_data = data; } ui_app.title = title; ui_app.fini = fini; ui_app.opened = opened; } ui_app_t ui_app = { .class_name = "sample8", .no_decor = true, .no_max = true, // TODO: should be implied by no_decor .dark_mode = false, .light_mode = false, .init = init, .window_sizing = { .ini_w = 10.0f, .ini_h = 7.0f } }; static ui_view_t test = ui_view(stack); static ui_view_t tools_list = ui_view(list); static void tools(ui_button_t* b) { rt_println("b->state.pressed: %d", b->state.pressed); // menu is "flip" button. It will do this: // b->state.pressed = !b->state.pressed; // automatically before callback. tools_list.state.hidden = !b->state.pressed; app_data.menu_used = 1; ui_app.request_layout(); } static void switch_view(ui_button_t* b, int32_t ix, void (*build_view)(ui_view_t* v)) { if (!b->state.pressed) { tools_list.state.hidden = true; ui_caption.menu.state.pressed = false; ui_view_for_each(b->parent, c, { c->state.pressed = false; }); b->state.pressed = !b->state.pressed; app_data.selected_view = ix; build_view(&test); } } static void stack(ui_button_t* b) { switch_view(b, 0, stack_test); } static void span(ui_button_t* b) { switch_view(b, 1, span_test); } static void list(ui_button_t* b) { switch_view(b, 2, list_test); } static void controls(ui_button_t* b) { switch_view(b, 3, controls_test); } static void edit1(ui_button_t* b) { switch_view(b, 4, edit1_test); } static void debug(ui_button_t* b) { b->state.pressed = !b->state.pressed; app_data.debug = b->state.pressed; } static ui_button_t button_bugs = ui_button(rt_glyph_lady_beetle, 0.0f, debug); static ui_mbx_t mbx = ui_mbx( // message box "Orange frames represent stack, span, or list\n" "components. Green frames indicate padding for\n" "children.\n" "\n" "These insets and padding are intentionally\n" "varied on different sides.\n" "\n" "By default, a container centers its children \n" "unless an alignment is specified by a child.\n" "\n" "When child.max_w = " rt_glyph_infinity "or child.max_h = " rt_glyph_infinity ",\n" "the child expands in the specified direction.\n" "\n" "Span aligns children horizontally, while List\n" "aligns them vertically.\n" "\n" "Overflows are permissible.\n" "\n" "Experiment with resizing the application window.\n" "\n" "Press ESC to close this message." "\n", null, null); static void about(ui_button_t* rt_unused(b)) { ui_app.show_toast(&mbx.view, 10.0); } static char* nil; static void crash(ui_button_t* rt_unused(b)) { // two random ways to crash in release configuration if (rt_clock.nanoseconds() % 2 == 0) { rt_swear(false, "should crash in release configuration"); } else { #if 0 // cl.exe compains even with disabled warnings #pragma warning(push) // this is intentional for testing #pragma warning(disable: 4723) // potential division by zero int32_t a[5]; int32_t* p = a; rt_println("%d\n", rt_countof(a)); rt_println("%d\n", rt_countof(p)); // expected "division by zero" #pragma warning(pop) #endif (*nil)++; // expected "access violation" } } static void insert_into_caption(ui_button_t* b, const char* hint) { rt_str_printf(b->hint, "%s", hint); b->flat = true; b->padding = (ui_margins_t){0,0,0,0}; b->insets = (ui_margins_t){0,0,0,0}; b->align = ui.align.top; ui_view.add_before(b, &ui_caption.mini); } static void ui_app_root_composed(ui_view_t* rt_unused(v)) { app_data.light = !ui_theme.is_app_dark(); } static void opened(void) { static ui_view_t list_view = ui_view(list); static ui_view_t span_view = ui_view(span); static ui_button_t button_stack = ui_button("&Stack", 4.25f, stack); static ui_button_t button_span = ui_button("&Span", 4.25f, span); static ui_button_t button_list = ui_button("&List", 4.25f, list); static ui_button_t button_controls = ui_button("Con&trols", 4.25f, controls); static ui_button_t button_edit1 = ui_button("Edit &1", 4.25f, edit1); ui_view.add(ui_app.content, ui_view.add(&list_view, ui_view.add(&span_view, ui_view.add(&tools_list, &button_stack, &button_span, &button_list, &button_controls, &button_edit1, null), &test, null), null), null); list_view.max_w = ui.infinity; list_view.max_h = ui.infinity; list_view.insets = (ui_margins_t){ 0, 0, 0, 0 }; span_view.max_w = ui.infinity; span_view.max_h = ui.infinity; span_view.insets = (ui_margins_t){ 0, 0, 0, 0 }; test.max_w = ui.infinity; test.max_h = ui.infinity; test.color = ui_colors.transparent; test.insets = (ui_margins_t){ 0, 0, 0, 0 }; test.background_id = ui_color_id_window; ui_view.set_text(&test, "%s", "test"); // test.paint = ui_view.debug_paint; test.debug.paint.margins = true; // buttons to switch test content tools_list.max_h = ui.infinity; tools_list.color_id = ui_color_id_window; ui_view.set_text(&tools_list, "%s", "Tools"); // tools_list.paint = ui_view.debug_paint; ui_view_for_each(&tools_list, it, { it->align = ui.align.left; it->padding.bottom = 0; }); rt_str_printf(button_stack.hint, "Shows ui_view(stack) layout\n" "Resizing Window will allow\n" "too see how it behaves"); switch (app_data.selected_view) { case 1: span(&button_span); break; case 2: list(&button_list); break; case 3: controls(&button_controls); break; case 4: edit1(&button_edit1); break; case 0: // drop to default: default: stack(&button_stack); break; } ui_caption.menu.callback = tools; // 2-state button automatically flip .pressed state: ui_caption.menu.flip = true; ui_caption.icon.state.hidden = true; tools_list.state.hidden = true; if (app_data.menu_used == 0) { ui_app.toast(4.5, rt_glyph_leftward_arrow " click " rt_glyph_trigram_for_heaven " menu button"); } // caption buttons: static ui_button_t button_info = ui_button(rt_glyph_circled_information_source, 0.0f, about); static ui_button_t button_bomb = ui_button(rt_glyph_bomb, 0.0f, crash); insert_into_caption(&button_info, "About"); insert_into_caption(&button_bugs, "Debug"); insert_into_caption(&button_bomb, "Intentionally Crash"); button_info.debug.id = "#caption.info"; button_bomb.debug.id = "#caption.bomb"; button_bugs.debug.id = "#caption.bug"; if (app_data.debug) { debug(&button_bugs); } ui_app.root->composed = ui_app_root_composed; if (!ui_theme.is_app_dark() != app_data.light) { ui_caption.mode.callback(&ui_caption.mode); } } static ui_view_t* align(ui_view_t* v, int32_t align) { v->align = align; return v; } static void stack_test(ui_view_t* parent) { // TODO: do not need to disband everything just remove children // and switch. list_test() becomes init() like switching views // removing and adding child ui_view.disband(parent); static ui_view_t stack = ui_view(stack); static ui_label_t left = ui_label(0, " left "); static ui_label_t right = ui_label(0, " right "); static ui_label_t top = ui_label(0, " top "); static ui_label_t bottom = ui_label(0, " bottom "); static ui_label_t left_top = ui_label(0, " left|top "); static ui_label_t right_bottom = ui_label(0, " right|bottom "); static ui_label_t right_top = ui_label(0, " right|top "); static ui_label_t left_bottom = ui_label(0, " left|bottom "); static ui_label_t center = ui_label(0, " center "); stack.insets = (ui_margins_t){ 1.0, 0.5, 0.25, 2.0 }; ui_view.add(parent, ui_view.add(&stack, align(&left, ui.align.left), align(&right, ui.align.right), align(&top, ui.align.top), align(&bottom, ui.align.bottom), align(&left_top, ui.align.left |ui.align.top), align(&right_bottom, ui.align.right|ui.align.bottom), align(&right_top, ui.align.right|ui.align.top), align(&left_bottom, ui.align.left |ui.align.bottom), align(¢er, ui.align.center), null), null); stack.debug.paint.margins = true; stack.max_w = ui.infinity; stack.max_h = ui.infinity; stack.insets = (ui_margins_t){ 1.0, 0.5, 0.25, 2.0 }; stack.background_id = ui_color_id_window; ui_view.set_text(&stack, "#stack"); ui_view_for_each(&stack, it, { it->debug.paint.margins = true; it->color = ui_colors.onyx; // it->fm = &ui_app.fm.prop.H1; it->padding = (ui_margins_t){ 2.0, 0.25, 0.5, 1.0 }; }); } static void span_test(ui_view_t* parent) { // TODO: do not need to disband everything just remove children // and switch. list_test() becomes init() like switching views // removing and adding child ui_view.disband(parent); static ui_view_t span = ui_view(span); static ui_label_t left = ui_label(0, " left "); static ui_label_t right = ui_label(0, " right "); static ui_view_t spacer = ui_view(spacer); static ui_label_t top = ui_label(0, " top "); static ui_label_t bottom = ui_label(0, " bottom "); ui_view.add(parent, ui_view.add(&span, align(&left, ui.align.center), align(&top, ui.align.top), align(&spacer, ui.align.center), align(&bottom, ui.align.bottom), align(&right, ui.align.center), null), null); span.debug.paint.margins = true; span.max_w = ui.infinity; span.max_h = ui.infinity; span.insets = (ui_margins_t){ 1.0, 0.5, 0.25, 2.0 }; ui_view.set_text(&span, "#span"); span.background_id = ui_color_id_window; ui_view_for_each(&span, it, { it->debug.paint.margins = true; it->color = ui_colors.onyx; it->padding = (ui_margins_t){ 2.0, 0.25, 0.5, 1.0 }; it->max_h = ui.infinity; // it->fm = &ui_app.fm.prop.H1; // rt_println("%s 0x%02X", it->text, it->align); }); top.max_h = 0; bottom.max_h = 0; } static void list_test(ui_view_t* parent) { // TODO: do not need to disband everything just remove children // and switch. list_test() becomes init() like switching views // removing and adding child ui_view.disband(parent); static ui_view_t list = ui_view(list); static ui_label_t left = ui_label(0, " left "); static ui_label_t right = ui_label(0, " right "); static ui_view_t spacer = ui_view(spacer); static ui_label_t top = ui_label(0, " top "); static ui_label_t bottom = ui_label(0, " bottom "); ui_view.add(&test, ui_view.add(&list, align(&top, ui.align.center), align(&left, ui.align.left), align(&spacer, ui.align.center), align(&right, ui.align.right), align(&bottom, ui.align.center), null), null); list.debug.paint.margins = true; list.max_w = ui.infinity; list.max_h = ui.infinity; list.insets = (ui_margins_t){ 1.0, 0.5, 0.25, 2.0 }; list.background_id = ui_color_id_window; ui_view.set_text(&list, "#list"); ui_view_for_each(&list, it, { it->debug.paint.margins = true; it->color = ui_colors.onyx; // TODO: labels, buttons etc should define their own default padding != 0 it->padding = (ui_margins_t){ 2.0, 0.25, 0.5, 1.0 }; it->max_w = ui.infinity; // it->fm = &ui_app.fm.prop.H1; }); left.max_w = 0; right.max_w = 0; } // controls test static void slider_format(ui_view_t* v) { ui_slider_t* slider = (ui_slider_t*)v; ui_view.set_text(v, "%s", rt_str.uint64(slider->value)); } static void slider_callback(ui_view_t* v) { ui_slider_t* slider = (ui_slider_t*)v; rt_println("value: %d", slider->value); } static void controls_set_margins(ui_view_t* v, bool on_off) { ui_view_for_each(v, it, { controls_set_margins(it, on_off); it->debug.paint.margins = on_off; } ); } static void controls_margins(ui_view_t* v) { controls_set_margins(v->parent->parent->parent, v->state.pressed); ui_app.request_redraw(); app_data.margins = v->state.pressed; } static void controls_set_fm(ui_view_t* v, bool on_off) { ui_view_for_each(v, it, { controls_set_fm(it, on_off); it->debug.paint.fm = on_off; } ); } static void controls_fm(ui_view_t* v) { controls_set_fm(v->parent->parent->parent, v->state.pressed); ui_app.request_redraw(); app_data.fm = v->state.pressed; } static void controls_set_large(ui_view_t* v, bool on_off) { ui_view_for_each(v, it, { controls_set_large(it, on_off); it->fm = on_off ? &ui_app.fm.prop.H1 : &ui_app.fm.prop.normal; }); } static void controls_large(ui_view_t* v) { controls_set_large(v->parent->parent->parent, v->state.pressed); app_data.large = v->state.pressed; ui_app.request_layout(); } static void button_pressed(ui_view_t* v) { if (v->shortcut != 0) { rt_println("'%c' 0x%02X %d, %s \"%s\"", v->shortcut, v->shortcut, v->shortcut, ui_view_debug_id(v), v->p.text); } else { rt_println("%s \"%s\"", ui_view_debug_id(v), v->p.text); } } static void controls_test(ui_view_t* parent) { #define wild_string \ "A" rt_glyph_zwsp \ rt_glyph_combining_enclosing_circle \ "B" rt_glyph_box_drawings_light_diagonal_cross \ rt_glyph_E_with_cedilla_and_breve // TODO: do not need to disband everything just remove children // and switch. list_test() becomes init() like switching views // removing and adding child ui_view.disband(parent); static ui_view_t list = ui_view(list); static ui_view_t span = ui_view(span); // horizontal inside span static ui_toggle_t large = ui_toggle("&Large", 3.0f, controls_large); static ui_label_t left = ui_label(0, "Left"); static ui_button_t button1 = ui_button("&Button", 0, button_pressed); static ui_label_t right = ui_label(0, "Right"); static ui_slider_t slider1 = ui_slider("%d", 6.0f, 0, UINT16_MAX, slider_format, slider_callback); static ui_toggle_t toggle1 = ui_toggle("Toggle: ___", 4.0f, null); static ui_button_t egypt = ui_button("\xC3\x84\x67\x79\x70\x74\x65\x6E", 0, null); static ui_label_t wild = ui_label(1, wild_string); // vertical inside list #define min_w_in_em (8.5f) static ui_label_t label = ui_label(min_w_in_em, "Label"); static ui_button_t button2 = ui_button("Button", min_w_in_em, null); static ui_slider_t slider2 = ui_slider("%d", min_w_in_em, 0, UINT16_MAX, slider_format, slider_callback); static ui_slider_t slider3 = ui_slider("%d", min_w_in_em, 0, UINT16_MAX, slider_format, slider_callback); static ui_toggle_t margins = ui_toggle("&margins", min_w_in_em, controls_margins); static ui_toggle_t fm = ui_toggle("&Font Metrics", min_w_in_em, controls_fm); static ui_view_t spacer = ui_view(spacer); ui_view.add(&test, ui_view.add(&list, ui_view.add(&span, align(&large, ui.align.top), align(&left, ui.align.top), align(&button1, ui.align.top), align(&right, ui.align.top), align(&slider1.view, ui.align.top), align(&toggle1, ui.align.top), align(&egypt, ui.align.top), align(&wild, ui.align.top), null), align(&label, ui.align.left), align(&button2, ui.align.left), align(&slider2.view, ui.align.left), align(&slider3.view, ui.align.left), align(&margins, ui.align.left), align(&fm, ui.align.left), align(&spacer, ui.align.left), null), null); list.debug.paint.margins = true; span.align = ui.align.left; list.max_w = ui.infinity; list.max_h = ui.infinity; ui_view.set_text(&list, "#list"); list.background_id = ui_color_id_window; slider2.dec.state.hidden = true; slider2.inc.state.hidden = true; fm.state.pressed = app_data.fm; controls_fm(&fm); margins.state.pressed = app_data.margins; controls_margins(&margins); large.state.pressed = app_data.large; controls_large(&large); toggle1.debug.id = "#toggle.1"; slider1.debug.id = "#slider.1"; // slider1.debug.trace.mt = true; // slider1.inc.debug.trace.mt = true; // slider1.dec.debug.trace.mt = true; slider1.inc.debug.id = "#slider.1.inc"; slider1.dec.debug.id = "#slider.1.dec"; } // edit1 test static void edit1_test(ui_view_t* parent) { // TODO: do not need to disband everything just remove children // and switch. list_test() becomes init() like switching views // removing and adding child ui_view.disband(parent); static void* text; static int64_t bytes; if (text == null) { if (rt_args.c > 1) { if (rt_files.exists(rt_args.v[1])) { errno_t r = rt_mem.map_ro(rt_args.v[1], &text, &bytes); if (r != 0) { rt_println("rt_mem.map_ro(%s) failed %s", rt_args.v[1], rt_str.error(r)); } } else { rt_println("file \"%s\" does not exist", rt_args.v[1]); } } } static ui_view_t list = ui_view(list); static ui_edit_view_t edit = {0}; static ui_edit_doc_t doc = {0}; if (doc.text.np == 0) { rt_swear(ui_edit_doc.init(&doc, text, (int32_t)bytes, false)); ui_edit_view.init(&edit, &doc); } ui_view.add(&test, ui_view.add(&list, &edit.view, null), null); list.max_w = ui.infinity; list.max_h = ui.infinity; edit.view.fm = &ui_app.fm.mono.normal; edit.view.max_w = ui.infinity; edit.view.max_h = ui.infinity; // edit.view.debug.paint.margins = true; // edit.view.debug.trace.prc = true; rt_str_printf(edit.p.text, "#edit"); ui_view.set_focus(&edit.view); } ================================================ FILE: src/samples/sample9.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "single_file_lib/rt/rt.h" #include "single_file_lib/ui/ui.h" #include "i18n.h" #define TITLE "Sample9" static void init(void); ui_app_t ui_app = { .class_name = "sample9", .init = init, .dark_mode = true, .window_sizing = { .min_w = 9.0f, .min_h = 5.5f, .ini_w = 10.0f, .ini_h = 6.0f } }; static int32_t panel_border = 1; static int32_t frame_border = 1; static ui_bitmap_t image; static uint32_t pixels[1024][1024]; static fp64_t zoom = 0.5; static fp64_t sx = 0.25; // [0..1] static fp64_t sy = 0.25; // [0..1] static struct { fp64_t x; fp64_t y; } stack[52]; static int top = 1; // because it is already zoomed in once above static ui_slider_t zoomer; static ui_label_t toast_filename = ui_label(0.0, "filename placeholder"); static ui_label_t label_single_line = ui_label(0.0, "Mandelbrot Explorer"); static ui_label_t label_multiline = ui_label(19.0, "Click inside or +/- to zoom;\n" "right mouse click to zoom out;\n" "use touchpad or keyboard " rt_glyph_leftwards_white_arrow rt_glyph_upwards_white_arrow rt_glyph_downwards_white_arrow rt_glyph_rightwards_white_arrow " to pan"); static ui_label_t about = ui_label(34.56f, "\nClick inside Mandelbrot Julia Set fractal to zoom in into interesting " "areas. Right mouse click to zoom out.\n" "Use Win + Shift + S to take a screenshot of something " "beautiful that caught your eye." "\n\n" "This sample also a showcase of controls like toggle, message box, " "tooltips, clipboard copy, full screen switching, open file " "dialog and on-the-fly locale switching for simple and possibly " "incorrect Simplified Chinese localization." "\n\n" "Press ESC or click the " rt_glyph_multiplication_sign " button in right top corner " "to dismiss this message or just wait - it will disappear by " "itself in 10 seconds.\n"); #ifdef SAMPLE9_USE_STATIC_UI_VIEW_MACROS ui_mbx_chosen(mbx, // message box "\"Pneumonoultramicroscopicsilicovolcanoconiosis\"\n" "is it the longest English language word or not?", { rt_println("option=%d", option); // -1 or index of { "&Yes", "&No" } }, "&Yes", "&No"); #else static void mbx_callback(ui_view_t* v) { ui_mbx_t* mbx = (ui_mbx_t*)v; rt_assert(-1 <= mbx->option && mbx->option < 2); static const char* name[] = { "Cancel", "Yes", "No" }; rt_println("option: %d \"%s\"", mbx->option, name[mbx->option + 1]); } static ui_mbx_t mbx = ui_mbx( // message box "\"Pneumonoultramicroscopicsilicovolcanoconiosis\"\n" "is it the longest English language word or not?", mbx_callback, "&Yes", "&No"); #endif static const char* filter[] = { "All Files", "*", "Image Files", "*.png;*.jpg", "Text Files", "*.txt;*.doc;*.ini", "Executables", "*.exe" }; static void open_file(ui_button_t* rt_unused(b)) { const char* home = rt_files.known_folder(rt_files.folder.home); // all files filer: null, 0 const char* fn = ui_app.open_file(home, filter, rt_countof(filter)); if (fn[0] != 0) { ui_view.set_text(&toast_filename, "\n%s\n", fn); rt_println("\"%s\"", fn); ui_app.show_toast(&toast_filename, 3.3); } } ui_button_t button_open_file = ui_button("&Open", 7.5, open_file); static void flip_full_clicked(ui_button_t* b) { b->state.pressed = !b->state.pressed; ui_app.full_screen(b->state.pressed); if (b->state.pressed) { ui_app.toast(1.75, "Press ESC to exit full screen"); } } static ui_button_t button_full_screen = ui_button( rt_glyph_square_four_corners, 1, flip_full_clicked); static void flip_locale(ui_button_t* b) { b->state.pressed = !b->state.pressed; rt_fatal_if_error(rt_nls.set_locale(b->state.pressed ? "zh-CN" : "en-US")); ui_app.request_layout(); // because center panel layout changed } static ui_button_t button_locale = ui_button( rt_glyph_kanji_onna_female "A", 1, flip_locale); static void about_clicked(ui_button_t* rt_unused(b)) { ui_app.show_toast(&about, 10.0); } static ui_button_t button_about = ui_button("&About", 7.5, about_clicked); ui_button_clicked(button_mbx, "&Message Box", 7.5, { ui_app.show_toast(&mbx.view, 0); }); static void scroll_toggle(ui_button_t* rt_unused(b)) { ui_app.request_redraw(); } static ui_toggle_t scroll = ui_toggle("Scroll &Direction:", /* min_w_em: */ 3.0f, /* callback:*/ scroll_toggle); static ui_view_t panel_top = ui_view(stack); static ui_view_t panel_bottom = ui_view(stack); static ui_view_t panel_center = ui_view(stack); static ui_view_t panel_right = ui_view(stack); static const ui_gdi_ta_t* ta = &ui_gdi.ta.prop.normal; static void print(int32_t *x, int32_t *y, const char* format, ...) { va_list va; va_start(va, format); *x += ui_gdi.text_va(ta, *x, *y, format, va).w; va_end(va); } static void println(int32_t *x, int32_t *y, const char* format, ...) { va_list va; va_start(va, format); *y += ui_gdi.text_va(ta, *x, *y, format, va).h; va_end(va); } static void after(ui_view_t* v, const char* format, ...) { const ui_ltrb_t insets = ui_view.margins(v, &v->insets); int32_t x = v->x + v->w + v->fm->em.w; int32_t y = v->y + insets.top; va_list va; va_start(va, format); ui_gdi.text_va(ta, x, y, format, va); va_end(va); } static void panel_paint(ui_view_t* v) { if (v->color == ui_colors.transparent) { v->color = ui_app.content->color; } ui_gdi.fill(v->x, v->y, v->w, v->h, ui_color_rgb(30, 30, 30)); ui_color_t c = ui_color_rgb(63, 63, 70); if (v == &panel_right) { ui_gdi.line(v->x, v->y, v->x + v->w, v->y, c); ui_gdi.line(v->x + v->w, v->y, v->x + v->w, v->y + v->h, c); ui_gdi.line(v->x + v->w, v->y + v->h, v->x, v->y + v->h, c); ui_gdi.line(v->x, v->y + v->h, v->x, v->y, c); } else if (v == &panel_top || v == &panel_bottom) { ui_gdi.line(v->x, v->y, v->x, v->y + v->h, c); ui_gdi.line(v->x, v->y + v->h, v->x + v->w, v->y + v->h, c); ui_gdi.line(v->x + v->w, v->y, v->x, v->y, c); } else { rt_assert(v == &panel_center); ui_gdi.line(v->x, v->y, v->x, v->y + v->h, c); } int32_t x = v->x + panel_border + rt_max(1, v->fm->em.w / 8); int32_t y = v->y + panel_border + rt_max(1, v->fm->em.h / 4); const int32_t radius = (v->fm->em.h / 4) | 0x1; ui_gdi.rounded(x, y, v->fm->em.w * 12, v->fm->em.h, radius, v->color, ui_colors.transparent); x = v->x + panel_border + rt_max(1, v->fm->em.w / 2); y = v->y + panel_border + rt_max(1, v->fm->em.h / 4); ui_gdi.text(ta, x, y, "%d,%d %dx%d %s", v->x, v->y, v->w, v->h, ui_view.string(v)); } static void right_layout(ui_view_t* v) { int x = v->x + v->fm->em.w; int y = v->y + v->fm->em.h * 2; ui_view_for_each(v, c, { c->x = x; c->y = y; y += c->h + v->fm->em.h / 2; }); } static void right_paint(ui_view_t* v) { panel_paint(v); const ui_gdi_ta_t* restore = ta; after(&button_locale, "&Locale %s", button_locale.state.pressed ? "zh-CN" : "en-US"); after(&button_full_screen, "%s", ui_app.is_full_screen ? rt_nls.str("Restore from &Full Screen") : rt_nls.str("&Full Screen")); int32_t x = label_multiline.x; int32_t y = label_multiline.y + label_multiline.h + v->fm->em.h / 4; // rt_println("%d,%d %dx%d", // label_multiline.x, // label_multiline.y, // label_multiline.w, // label_multiline.h // ); println(&x, &y, "%s", rt_nls.str("Proportional")); ta = &ui_gdi.ta.mono.normal; println(&x, &y, "%s", rt_nls.str("Monospaced")); ta = &ui_gdi.ta.prop.H1; println(&x, &y, "H1 %s", rt_nls.str("Header")); ta = &ui_gdi.ta.prop.H2; println(&x, &y, "H2 %s", rt_nls.str("Header")); ta = &ui_gdi.ta.prop.H3; println(&x, &y, "H3 %s", rt_nls.str("Header")); ta = &ui_gdi.ta.prop.normal; println(&x, &y, "%s %dx%d root: %d,%d %dx%d", rt_nls.str("Client area"), ui_app.crc.w, ui_app.crc.h, ui_app.root->x, ui_app.root->y, ui_app.root->w, ui_app.root->h); println(&x, &y, "%s %dx%d dpi: %d", rt_nls.str("Window"), ui_app.wrc.w, ui_app.wrc.h, ui_app.dpi.window); println(&x, &y, "%s %dx%d dpi: %d ang %d raw %d", rt_nls.str("Monitor"), ui_app.mrc.w, ui_app.mrc.h, ui_app.dpi.monitor_effective, ui_app.dpi.monitor_angular, ui_app.dpi.monitor_raw); println(&x, &y, "%s %d %d", rt_nls.str("Left Top"), ui_app.wrc.x, ui_app.wrc.y); println(&x, &y, "%s %d %d", rt_nls.str("Mouse"), ui_app.mouse.x, ui_app.mouse.y); println(&x, &y, "%d x paint()", ui_app.paint_count); println(&x, &y, "%.1fms (%s %.1f %s %.1f)", ui_app.paint_time * 1000.0, rt_nls.str("max"), ui_app.paint_max * 1000.0, rt_nls.str("avg"), ui_app.paint_avg * 1000.0); after(&zoomer.view, "%.16f", zoom); after(&scroll, "%s", scroll.state.pressed ? rt_nls.str("Natural") : rt_nls.str("Reverse")); ta = restore; } static void center_paint(ui_view_t* view) { // ui_gdi.set_clip(view->x, view->y, view->w, view->h); ui_gdi.fill(view->x, view->y, view->w, view->h, ui_colors.black); int x = (view->w - image.w) / 2; int y = (view->h - image.h) / 2; // ui_gdi.alpha(view->x + x, view->y + y, image.w, image.h, &image, 0.5); ui_gdi.bitmap(view->x + x, view->y + y, image.w, image.h, 0, 0, image.w, image.h, &image); // ui_gdi.set_clip(0, 0, 0, 0); } static void measure(ui_view_t* v) { v->fm = &ui_app.fm.mono.normal; panel_border = rt_max(1, v->fm->em.h / 4); frame_border = rt_max(1, v->fm->em.h / 8); rt_assert(panel_border > 0 && frame_border > 0); const int32_t w = ui_app.root->w; const int32_t h = ui_app.root->h; // measure ui elements panel_top.w = (int32_t)(0.70 * w); panel_top.h = v->fm->em.h * 2; panel_bottom.w = panel_top.w; panel_bottom.h = v->fm->em.h * 2; panel_right.w = w - panel_bottom.w; panel_right.h = h; panel_center.w = panel_bottom.w; panel_center.h = h - panel_bottom.h - panel_top.h; } static void layout(ui_view_t* rt_unused(view)) { rt_assert(view->fm->em.w > 0 && view->fm->em.h > 0); const int32_t h = ui_app.root->h; panel_top.x = 0; panel_top.y = 0; panel_bottom.x = 0; panel_bottom.y = h - panel_bottom.h; panel_right.x = panel_bottom.w; panel_right.y = 0; panel_center.x = 0; panel_center.y = panel_top.h; } static void refresh(void); static void zoom_out(void) { rt_assert(top > 0); top--; sx = stack[top].x; sy = stack[top].y; zoom *= 2; } static void zoom_in(int x, int y) { rt_assert(top < rt_countof(stack)); stack[top].x = sx; stack[top].y = sy; top++; zoom /= 2; sx += zoom * x / image.w; sy += zoom * y / image.h; } static bool tap(ui_view_t* rt_unused(v), int32_t ix, bool pressed) { const bool inside = ui_view.inside(&panel_center, &ui_app.mouse); if (pressed && inside) { int x = ui_app.mouse.x - (panel_center.w - image.w) / 2 - panel_center.x; int y = ui_app.mouse.y - (panel_center.h - image.h) / 2 - panel_center.y; if (0 <= x && x < image.w && 0 <= y && y < image.h) { if (pressed && ix == 2) { if (zoom < 1) { zoom_out(); refresh(); } } else if (pressed && ix == 0) { if (top < rt_countof(stack)) { zoom_in(x, y); refresh(); } } } ui_app.request_redraw(); } return pressed && inside; } static void slider_format(ui_view_t* v) { ui_slider_t* slider = (ui_slider_t*)v; ui_view.set_text(v, "%s", rt_str.uint64(slider->value)); } static void zoomer_callback(ui_view_t* v) { ui_slider_t* slider = (ui_slider_t*)v; fp64_t z = 1; for (int i = 0; i < slider->value; i++) { z /= 2; } while (zoom > z) { zoom_in(image.w / 2, image.h / 2); } while (zoom < z) { zoom_out(); } refresh(); } static void mouse_scroll(ui_view_t* unused, ui_point_t dx_dy) { (void)unused; if (!scroll.state.pressed) { dx_dy.y = -dx_dy.y; } if (!scroll.state.pressed) { dx_dy.x = -dx_dy.x; } sx = sx + zoom * dx_dy.x / image.w; sy = sy + zoom * dx_dy.y / image.h; refresh(); } static void character(ui_view_t* view, const char* utf8) { char ch = utf8[0]; if (ch == 'q' || ch == 'Q') { ui_app.close(); } else if (ch == 033 && ui_app.is_full_screen) { flip_full_clicked(&button_full_screen); } else if (ch == '+' || ch == '=') { zoom /= 2; refresh(); } else if (ch == '-' || ch == '_') { zoom = rt_min(zoom * 2, 1.0); refresh(); } else if (ch == '<' || ch == ',') { ui_point_t pt = {+image.w / 8, 0}; mouse_scroll(view, pt); } else if (ch == '>' || ch == '.') { ui_point_t pt = {-image.w / 8, 0}; mouse_scroll(view, pt); } else if (ch == 3) { // Ctrl+C rt_clipboard.put_image(&image); } } static bool keyboard(ui_view_t* view, int64_t vk) { bool swallow = true; if (vk == ui.key.up) { ui_point_t pt = {0, +image.h / 8}; mouse_scroll(view, pt); } else if (vk == ui.key.down) { ui_point_t pt = {0, -image.h / 8}; mouse_scroll(view, pt); } else if (vk == ui.key.left) { ui_point_t pt = {+image.w / 8, 0}; mouse_scroll(view, pt); } else if (vk == ui.key.right) { ui_point_t pt = {-image.w / 8, 0}; mouse_scroll(view, pt); } else { swallow = false; } return swallow; } static void init_panel(ui_view_t* panel, const char* text, ui_color_t color, void (*paint)(ui_view_t*)) { ui_view.set_text(panel, "%s", text); panel->color = color; panel->paint = paint; } static void opened(void) { ui_app.content->measure = measure; ui_app.content->layout = layout; ui_app.content->character = character; ui_app.content->key_pressed = keyboard; // virtual_keys ui_app.content->mouse_scroll = mouse_scroll; panel_center.tap = tap; int n = rt_countof(pixels); static_assert(sizeof(pixels[0][0]) == 4, "4 bytes per pixel"); static_assert(rt_countof(pixels) == rt_countof(pixels[0]), "square"); ui_gdi.bitmap_init(&image, n, n, (int32_t)sizeof(pixels[0][0]), (uint8_t*)pixels); init_panel(&panel_top, "top", ui_colors.orange, panel_paint); init_panel(&panel_center, "center", ui_colors.white, center_paint); init_panel(&panel_bottom, "bottom", ui_colors.tone_blue, panel_paint); init_panel(&panel_right, "right", ui_colors.tone_green, right_paint); panel_right.layout = right_layout; label_single_line.highlightable = true; label_single_line.flat = true; label_multiline.highlightable = true; rt_str_printf(label_multiline.hint, "%s", "Ctrl+C or Right Mouse click to copy text to clipboard"); ui_view.set_text(&label_multiline, "%s", rt_nls.string(str_help, "")); button_locale.shortcut = 'l'; button_full_screen.shortcut = 'f'; #ifdef SAMPLE9_USE_STATIC_UI_VIEW_MACROS ui_slider_init(&zoomer, "Zoom: 1 / (2^%d)", 7.0, 0, rt_countof(stack) - 1, zoomer_callback); #else zoomer = (ui_slider_t)ui_slider("Zoom: 1 / (2^%d)", 7.0, 0, rt_countof(stack) - 1, slider_format, zoomer_callback); #endif rt_str_printf(button_mbx.hint, "Show Yes/No message box"); rt_str_printf(button_about.hint, "Show About message box"); ui_view.add(&panel_right, &button_locale, &button_full_screen, &zoomer, &scroll, &button_open_file, &button_about, &button_mbx, &label_single_line, &label_multiline, null ); ui_view.add(ui_app.content, &panel_top, &panel_center, &panel_right, &panel_bottom, null); refresh(); } static void init(void) { ui_app.title = TITLE; ui_app.opened = opened; } static fp64_t scale0to1(int v, int range, fp64_t sh, fp64_t zm) { return sh + zm * v / range; } static fp64_t into(fp64_t v, fp64_t lo, fp64_t hi) { rt_assert(0 <= v && v <= 1); return v * (hi - lo) + lo; } static void mandelbrot(ui_bitmap_t* im) { for (int r = 0; r < im->h; r++) { fp64_t y0 = into(scale0to1(r, im->h, sy, zoom), -1.12, 1.12); for (int c = 0; c < im->w; c++) { fp64_t x0 = into(scale0to1(c, im->w, sx, zoom), -2.00, 0.47); fp64_t x = 0; fp64_t y = 0; int iteration = 0; enum { max_iteration = 100 }; while (x* x + y * y <= 2 * 2 && iteration < max_iteration) { fp64_t t = x * x - y * y + x0; y = 2 * x * y + y0; x = t; iteration++; } static ui_color_t palette[16] = { ui_color_rgb( 66, 30, 15), ui_color_rgb( 25, 7, 26), ui_color_rgb( 9, 1, 47), ui_color_rgb( 4, 4, 73), ui_color_rgb( 0, 7, 100), ui_color_rgb( 12, 44, 138), ui_color_rgb( 24, 82, 177), ui_color_rgb( 57, 125, 209), ui_color_rgb(134, 181, 229), ui_color_rgb(211, 236, 248), ui_color_rgb(241, 233, 191), ui_color_rgb(248, 201, 95), ui_color_rgb(255, 170, 0), ui_color_rgb(204, 128, 0), ui_color_rgb(153, 87, 0), ui_color_rgb(106, 52, 3) }; ui_color_t color = palette[iteration % rt_countof(palette)]; uint8_t* px = &((uint8_t*)im->pixels)[r * im->w * 4 + c * 4]; px[3] = 0xFF; px[0] = (color >> 16) & 0xFF; px[1] = (color >> 8) & 0xFF; px[2] = (color >> 0) & 0xFF; } } } static void refresh(void) { if (sx < 0) { sx = 0; } if (sx > 1 - zoom) { sx = 1 - zoom; } if (sy < 0) { sy = 0; } if (sy > 1 - zoom) { sy = 1 - zoom; } if (zoom == 1) { sx = 0; sy = 0; } zoomer.value = 0; fp64_t z = 1; while (z != zoom) { zoomer.value++; z /= 2; } zoomer.value = rt_min(zoomer.value, zoomer.value_max); mandelbrot(&image); ui_app.request_redraw(); } ================================================ FILE: src/samples/stb_image.c ================================================ #include "rt/rt.h" static void* stb_malloc(size_t n) { rt_assert(n > 0); void* a = null; errno_t r = rt_heap.allocate(null, &a, n, false); rt_swear(r == 0 && a != null); // rt_println("%p : %8lld", a, n); return a; } static void* stb_realloc(void* p, size_t n) { rt_assert(n > 0); void* a = p; errno_t r = rt_heap.reallocate(null, &a, n, false); rt_swear(r == 0 && a != null); // rt_println("%p -> %p : %8lld", p, a, n); return a; } static void* stb_realloc_sized(void* p, size_t rt_unused(s), size_t n) { rt_assert(n > 0); void* a = p; errno_t r = rt_heap.reallocate(null, &a, n, false); rt_swear(r == 0 && a != null); // rt_println("%p : %8lld -> %p : %8lld", p, s, a, n); return a; } static void stb_free(void* p) { // rt_println("%p", p); rt_heap.deallocate(null, p); } #pragma warning(disable: 4459) // declaration of '...' hides global declaration #define STBI_ASSERT(...) rt_assert(__VA_ARGS__) #define STBI_MALLOC(sz) stb_malloc(sz) #define STBI_REALLOC(p,newsz) stb_realloc((p), (newsz)) #define STBI_FREE(p) stb_free(p) #define STBI_REALLOC_SIZED(p,oldsz,newsz) stb_realloc_sized((p),(oldsz),(newsz)) #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" ================================================ FILE: src/samples/stb_image.h ================================================ /* stb_image - v2.29 - public domain image loader - http://nothings.org/stb no warranty implied; use at your own risk Do this: #define STB_IMAGE_IMPLEMENTATION before you include this file in *one* C or C++ file to create the implementation. // i.e. it should look like this: #include ... #include ... #include ... #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" You can #define STBI_ASSERT(x) before the #include to avoid using assert.h. And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using malloc,realloc,free QUICK NOTES: Primarily of interest to game developers and other people who can avoid problematic images and only need the trivial interface JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib) PNG 1/2/4/8/16-bit-per-channel TGA (not sure what subset, if a subset) BMP non-1bpp, non-RLE PSD (composited view only, no extra channels, 8/16 bit-per-channel) GIF (*comp always reports as 4-channel) HDR (radiance rgbE format) PIC (Softimage PIC) PNM (PPM and PGM binary only) Animated GIF still needs a proper API, but here's one way to do it: http://gist.github.com/urraka/685d9a6340b26b830d49 - decode from memory or through FILE (define STBI_NO_STDIO to remove code) - decode from arbitrary I/O callbacks - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON) Full documentation under "DOCUMENTATION" below. LICENSE See end of file for license information. RECENT REVISION HISTORY: 2.29 (2023-05-xx) optimizations 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes 2.26 (2020-07-13) many minor fixes 2.25 (2020-02-02) fix warnings 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically 2.23 (2019-08-11) fix clang static analysis warning 2.22 (2019-03-04) gif fixes, fix warnings 2.21 (2019-02-25) fix typo in comment 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs 2.19 (2018-02-11) fix warning 2.18 (2018-01-30) fix warnings 2.17 (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings 2.16 (2017-07-23) all functions have 16-bit variants; optimizations; bugfixes 2.15 (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE detection on GCC 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs 2.13 (2016-12-04) experimental 16-bit API, only for PNG so far; fixes 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes 2.11 (2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64 RGB-format JPEG; remove white matting in PSD; allocate large structures on the stack; correct channel count for PNG & BMP 2.10 (2016-01-22) avoid warning introduced in 2.09 2.09 (2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED See end of file for full revision history. ============================ Contributors ========================= Image formats Extensions, features Sean Barrett (jpeg, png, bmp) Jetro Lauha (stbi_info) Nicolas Schulz (hdr, psd) Martin "SpartanJ" Golini (stbi_info) Jonathan Dummer (tga) James "moose2000" Brown (iPhone PNG) Jean-Marc Lienher (gif) Ben "Disch" Wenger (io callbacks) Tom Seddon (pic) Omar Cornut (1/2/4-bit PNG) Thatcher Ulrich (psd) Nicolas Guillemot (vertical flip) Ken Miller (pgm, ppm) Richard Mitton (16-bit PSD) github:urraka (animated gif) Junggon Kim (PNM comments) Christopher Forseth (animated gif) Daniel Gibson (16-bit TGA) socks-the-fox (16-bit PNG) Jeremy Sawicki (handle all ImageNet JPGs) Optimizations & bugfixes Mikhail Morozov (1-bit BMP) Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) Arseny Kapoulkine Simon Breuss (16-bit PNM) John-Mark Allen Carmelo J Fdez-Aguera Bug & warning fixes Marc LeBlanc David Woo Guillaume George Martins Mozeiko Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski Phil Jordan Dave Moore Roy Eltham Hayaki Saito Nathan Reed Won Chun Luke Graham Johan Duparc Nick Verigakis the Horde3D community Thomas Ruf Ronny Chevalier github:rlyeh Janez Zemva John Bartholomew Michal Cichon github:romigrou Jonathan Blow Ken Hamada Tero Hanninen github:svdijk Eugene Golushkov Laurent Gomila Cort Stratton github:snagar Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex Cass Everitt Ryamond Barbiero github:grim210 Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus Josh Tobin Neil Bickford Matthew Gregan github:poppolopoppo Julian Raschke Gregory Mullen Christian Floisand github:darealshinji Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 Brad Weinberger Matvey Cherevko github:mosra Luca Sas Alexander Veselov Zack Middleton [reserved] Ryan C. Gordon [reserved] [reserved] DO NOT ADD YOUR NAME HERE Jacko Dirks To add your name to the credits, pick a random blank space in the middle and fill it. 80% of merge conflicts on stb PRs are due to people adding their name at the end of the credits. */ #ifndef STBI_INCLUDE_STB_IMAGE_H #define STBI_INCLUDE_STB_IMAGE_H // DOCUMENTATION // // Limitations: // - no 12-bit-per-channel JPEG // - no JPEGs with arithmetic coding // - GIF always returns *comp=4 // // Basic usage (see HDR discussion below for HDR usage): // int x,y,n; // unsigned char *data = stbi_load(filename, &x, &y, &n, 0); // // ... process data if not NULL ... // // ... x = width, y = height, n = # 8-bit components per pixel ... // // ... replace '0' with '1'..'4' to force that many components per pixel // // ... but 'n' will always be the number that it would have been if you said 0 // stbi_image_free(data); // // Standard parameters: // int *x -- outputs image width in pixels // int *y -- outputs image height in pixels // int *channels_in_file -- outputs # of image components in image file // int desired_channels -- if non-zero, # of image components requested in result // // The return value from an image loader is an 'unsigned char *' which points // to the pixel data, or NULL on an allocation failure or if the image is // corrupt or invalid. The pixel data consists of *y scanlines of *x pixels, // with each pixel consisting of N interleaved 8-bit components; the first // pixel pointed to is top-left-most in the image. There is no padding between // image scanlines or between pixels, regardless of format. The number of // components N is 'desired_channels' if desired_channels is non-zero, or // *channels_in_file otherwise. If desired_channels is non-zero, // *channels_in_file has the number of components that _would_ have been // output otherwise. E.g. if you set desired_channels to 4, you will always // get RGBA output, but you can check *channels_in_file to see if it's trivially // opaque because e.g. there were only 3 channels in the source image. // // An output image with N components has the following components interleaved // in this order in each pixel: // // N=#comp components // 1 grey // 2 grey, alpha // 3 red, green, blue // 4 red, green, blue, alpha // // If image loading fails for any reason, the return value will be NULL, // and *x, *y, *channels_in_file will be unchanged. The function // stbi_failure_reason() can be queried for an extremely brief, end-user // unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS // to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly // more user-friendly ones. // // Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. // // To query the width, height and component count of an image without having to // decode the full file, you can use the stbi_info family of functions: // // int x,y,n,ok; // ok = stbi_info(filename, &x, &y, &n); // // returns ok=1 and sets x, y, n if image is a supported format, // // 0 otherwise. // // Note that stb_image pervasively uses ints in its public API for sizes, // including sizes of memory buffers. This is now part of the API and thus // hard to change without causing breakage. As a result, the various image // loaders all have certain limits on image size; these differ somewhat // by format but generally boil down to either just under 2GB or just under // 1GB. When the decoded image would be larger than this, stb_image decoding // will fail. // // Additionally, stb_image will reject image files that have any of their // dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS, // which defaults to 2**24 = 16777216 pixels. Due to the above memory limit, // the only way to have an image with such dimensions load correctly // is for it to have a rather extreme aspect ratio. Either way, the // assumption here is that such larger images are likely to be malformed // or malicious. If you do need to load an image with individual dimensions // larger than that, and it still fits in the overall size limit, you can // #define STBI_MAX_DIMENSIONS on your own to be something larger. // // =========================================================================== // // UNICODE: // // If compiling for Windows and you wish to use Unicode filenames, compile // with // #define STBI_WINDOWS_UTF8 // and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert // Windows uint16_t filenames to utf8. // // =========================================================================== // // Philosophy // // stb libraries are designed with the following priorities: // // 1. easy to use // 2. easy to maintain // 3. good performance // // Sometimes I let "good performance" creep up in priority over "easy to maintain", // and for best performance I may provide less-easy-to-use APIs that give higher // performance, in addition to the easy-to-use ones. Nevertheless, it's important // to keep in mind that from the standpoint of you, a client of this library, // all you care about is #1 and #3, and stb libraries DO NOT emphasize #3 above all. // // Some secondary priorities arise directly from the first two, some of which // provide more explicit reasons why performance can't be emphasized. // // - Portable ("ease of use") // - Small source code footprint ("easy to maintain") // - No dependencies ("ease of use") // // =========================================================================== // // I/O callbacks // // I/O callbacks allow you to read from arbitrary sources, like packaged // files or some other source. Data read from callbacks are processed // through a small internal buffer (currently 128 bytes) to try to reduce // overhead. // // The three functions you must define are "read" (reads some bytes of data), // "skip" (skips some bytes of data), "eof" (reports if the stream is at the end). // // =========================================================================== // // SIMD support // // The JPEG decoder will try to automatically use SIMD kernels on x86 when // supported by the compiler. For ARM Neon support, you must explicitly // request it. // // (The old do-it-yourself SIMD API is no longer supported in the current // code.) // // On x86, SSE2 will automatically be used when available based on a run-time // test; if not, the generic C versions are used as a fall-back. On ARM targets, // the typical path is to have separate builds for NEON and non-NEON devices // (at least this is true for iOS and Android). Therefore, the NEON support is // toggled by a build flag: define STBI_NEON to get NEON loops. // // If for some reason you do not want to use any of SIMD code, or if // you have issues compiling it, you can disable it entirely by // defining STBI_NO_SIMD. // // =========================================================================== // // HDR image support (disable by defining STBI_NO_HDR) // // stb_image supports loading HDR images in general, and currently the Radiance // .HDR file format specifically. You can still load any file through the existing // interface; if you attempt to load an HDR file, it will be automatically remapped // to LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1; // both of these constants can be reconfigured through this interface: // // stbi_hdr_to_ldr_gamma(2.2f); // stbi_hdr_to_ldr_scale(1.0f); // // (note, do not use _inverse_ constants; stbi_image will invert them // appropriately). // // Additionally, there is a new, parallel interface for loading files as // (linear) floats to preserve the full dynamic range: // // float *data = stbi_loadf(filename, &x, &y, &n, 0); // // If you load LDR images through this interface, those images will // be promoted to floating point values, run through the inverse of // constants corresponding to the above: // // stbi_ldr_to_hdr_scale(1.0f); // stbi_ldr_to_hdr_gamma(2.2f); // // Finally, given a filename (or an open file or memory block--see header // file for details) containing image data, you can query for the "most // appropriate" interface to use (that is, whether the image is HDR or // not), using: // // stbi_is_hdr(char *filename); // // =========================================================================== // // iPhone PNG support: // // We optionally support converting iPhone-formatted PNGs (which store // premultiplied BGRA) back to RGB, even though they're internally encoded // differently. To enable this conversion, call // stbi_convert_iphone_png_to_rgb(1). // // Call stbi_set_unpremultiply_on_load(1) as well to force a divide per // pixel to remove any premultiplied alpha *only* if the image file explicitly // says there's premultiplied data (currently only happens in iPhone images, // and only if iPhone convert-to-rgb processing is on). // // =========================================================================== // // ADDITIONAL CONFIGURATION // // - You can suppress implementation of any of the decoders to reduce // your code footprint by #defining one or more of the following // symbols before creating the implementation. // // STBI_NO_JPEG // STBI_NO_PNG // STBI_NO_BMP // STBI_NO_PSD // STBI_NO_TGA // STBI_NO_GIF // STBI_NO_HDR // STBI_NO_PIC // STBI_NO_PNM (.ppm and .pgm) // // - You can request *only* certain decoders and suppress all other ones // (this will be more forward-compatible, as addition of new decoders // doesn't require you to disable them explicitly): // // STBI_ONLY_JPEG // STBI_ONLY_PNG // STBI_ONLY_BMP // STBI_ONLY_PSD // STBI_ONLY_TGA // STBI_ONLY_GIF // STBI_ONLY_HDR // STBI_ONLY_PIC // STBI_ONLY_PNM (.ppm and .pgm) // // - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still // want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB // // - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater // than that size (in either width or height) without further processing. // This is to let programs in the wild set an upper bound to prevent // denial-of-service attacks on untrusted data, as one could generate a // valid image of gigantic dimensions and force stb_image to allocate a // huge block of memory and spend disproportionate time decoding it. By // default this is set to (1 << 24), which is 16777216, but that's still // very big. #ifndef STBI_NO_STDIO #include #endif // STBI_NO_STDIO #define STBI_VERSION 1 enum { STBI_default = 0, // only used for desired_channels STBI_grey = 1, STBI_grey_alpha = 2, STBI_rgb = 3, STBI_rgb_alpha = 4 }; #include typedef unsigned char stbi_uc; typedef unsigned short stbi_us; #ifdef __cplusplus extern "C" { #endif #ifndef STBIDEF #ifdef STB_IMAGE_STATIC #define STBIDEF static #else #define STBIDEF extern #endif #endif ////////////////////////////////////////////////////////////////////////////// // // PRIMARY API - works on images of any type // // // load image by filename, open file, or memory buffer // typedef struct { int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read void (*skip) (void *user,int n); // skip the next 'n' bytes, or 'unget' the last -n bytes if negative int (*eof) (void *user); // returns nonzero if we are at end of file/data } stbi_io_callbacks; //////////////////////////////////// // // 8-bits-per-channel interface // STBIDEF stbi_uc *stbi_load_from_memory (stbi_uc const *buffer, int len , int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk , void *user, int *x, int *y, int *channels_in_file, int desired_channels); #ifndef STBI_NO_STDIO STBIDEF stbi_uc *stbi_load (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); // for stbi_load_from_file, file pointer is left pointing immediately after image #endif #ifndef STBI_NO_GIF STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); #endif #ifdef STBI_WINDOWS_UTF8 STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const uint16_t* input); #endif //////////////////////////////////// // // 16-bits-per-channel interface // STBIDEF stbi_us *stbi_load_16_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); #ifndef STBI_NO_STDIO STBIDEF stbi_us *stbi_load_16 (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); #endif //////////////////////////////////// // // float-per-channel interface // #ifndef STBI_NO_LINEAR STBIDEF float *stbi_loadf_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); #ifndef STBI_NO_STDIO STBIDEF float *stbi_loadf (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); STBIDEF float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); #endif #endif #ifndef STBI_NO_HDR STBIDEF void stbi_hdr_to_ldr_gamma(float gamma); STBIDEF void stbi_hdr_to_ldr_scale(float scale); #endif // STBI_NO_HDR #ifndef STBI_NO_LINEAR STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); STBIDEF void stbi_ldr_to_hdr_scale(float scale); #endif // STBI_NO_LINEAR // stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); #ifndef STBI_NO_STDIO STBIDEF int stbi_is_hdr (char const *filename); STBIDEF int stbi_is_hdr_from_file(FILE *f); #endif // STBI_NO_STDIO // get a VERY brief reason for failure // on most compilers (and ALL modern mainstream compilers) this is threadsafe STBIDEF const char *stbi_failure_reason (void); // free the loaded image -- this is just free() STBIDEF void stbi_image_free (void *retval_from_stbi_load); // get image dimensions & components without fully decoding STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len); STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user); #ifndef STBI_NO_STDIO STBIDEF int stbi_info (char const *filename, int *x, int *y, int *comp); STBIDEF int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); STBIDEF int stbi_is_16_bit (char const *filename); STBIDEF int stbi_is_16_bit_from_file(FILE *f); #endif // for image formats that explicitly notate that they have premultiplied alpha, // we just return the colors as stored in the file. set this flag to force // unpremultiplication. results are undefined if the unpremultiply overflow. STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); // indicate whether we should process iphone images back to canonical format, // or just pass them through "as-is" STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); // flip the image vertically, so the first pixel in the output array is the bottom left STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); // as above, but only applies to images loaded on the thread that calls the function // this function is only available if your compiler supports thread-local variables; // calling it will fail to link if your compiler doesn't STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); // ZLIB client - used by PNG, available for other purposes STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header); STBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); STBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); #ifdef __cplusplus } #endif // // //// end header file ///////////////////////////////////////////////////// #endif // STBI_INCLUDE_STB_IMAGE_H #ifdef STB_IMAGE_IMPLEMENTATION #if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \ || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \ || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \ || defined(STBI_ONLY_ZLIB) #ifndef STBI_ONLY_JPEG #define STBI_NO_JPEG #endif #ifndef STBI_ONLY_PNG #define STBI_NO_PNG #endif #ifndef STBI_ONLY_BMP #define STBI_NO_BMP #endif #ifndef STBI_ONLY_PSD #define STBI_NO_PSD #endif #ifndef STBI_ONLY_TGA #define STBI_NO_TGA #endif #ifndef STBI_ONLY_GIF #define STBI_NO_GIF #endif #ifndef STBI_ONLY_HDR #define STBI_NO_HDR #endif #ifndef STBI_ONLY_PIC #define STBI_NO_PIC #endif #ifndef STBI_ONLY_PNM #define STBI_NO_PNM #endif #endif #if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB) #define STBI_NO_ZLIB #endif #include #include // ptrdiff_t on osx #include #include #include #if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) #include // ldexp, pow #endif #ifndef STBI_NO_STDIO #include #endif #ifndef STBI_ASSERT #include #define STBI_ASSERT(x) assert(x) #endif #ifdef __cplusplus #define STBI_EXTERN extern "C" #else #define STBI_EXTERN extern #endif #ifndef _MSC_VER #ifdef __cplusplus #define stbi_inline inline #else #define stbi_inline #endif #else #define stbi_inline __forceinline #endif #ifndef STBI_NO_THREAD_LOCALS #if defined(__cplusplus) && __cplusplus >= 201103L #define STBI_THREAD_LOCAL thread_local #elif defined(__GNUC__) && __GNUC__ < 5 #define STBI_THREAD_LOCAL __thread #elif defined(_MSC_VER) #define STBI_THREAD_LOCAL __declspec(thread) #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) #define STBI_THREAD_LOCAL _Thread_local #endif #ifndef STBI_THREAD_LOCAL #if defined(__GNUC__) #define STBI_THREAD_LOCAL __thread #endif #endif #endif #if defined(_MSC_VER) || defined(__SYMBIAN32__) typedef unsigned short stbi__uint16; typedef signed short stbi__int16; typedef unsigned int stbi__uint32; typedef signed int stbi__int32; #else #include typedef uint16_t stbi__uint16; typedef int16_t stbi__int16; typedef uint32_t stbi__uint32; typedef int32_t stbi__int32; #endif // should produce compiler error if size is wrong typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; #ifdef _MSC_VER #define STBI_NOTUSED(v) (void)(v) #else #define STBI_NOTUSED(v) (void)sizeof(v) #endif #ifdef _MSC_VER #define STBI_HAS_LROTL #endif #ifdef STBI_HAS_LROTL #define stbi_lrot(x,y) _lrotl(x,y) #else #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) #endif #if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) // ok #elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) // ok #else #error "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." #endif #ifndef STBI_MALLOC #define STBI_MALLOC(sz) malloc(sz) #define STBI_REALLOC(p,newsz) realloc(p,newsz) #define STBI_FREE(p) free(p) #endif #ifndef STBI_REALLOC_SIZED #define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz) #endif // x86/x64 detection #if defined(__x86_64__) || defined(_M_X64) #define STBI__X64_TARGET #elif defined(__i386) || defined(_M_IX86) #define STBI__X86_TARGET #endif #if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD) // gcc doesn't support sse2 intrinsics unless you compile with -msse2, // which in turn means it gets to use SSE2 everywhere. This is unfortunate, // but previous attempts to provide the SSE2 functions with runtime // detection caused numerous issues. The way architecture extensions are // exposed in GCC/Clang is, sadly, not really suited for one-file libs. // New behavior: if compiled with -msse2, we use SSE2 without any // detection; if not, we don't use it at all. #define STBI_NO_SIMD #endif #if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) // Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid STBI__X64_TARGET // // 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the // Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant. // As a result, enabling SSE2 on 32-bit MinGW is dangerous when not // simultaneously enabling "-mstackrealign". // // See https://github.com/nothings/stb/issues/81 for more information. // // So default to no SSE2 on 32-bit MinGW. If you've read this far and added // -mstackrealign to your build settings, feel free to #define STBI_MINGW_ENABLE_SSE2. #define STBI_NO_SIMD #endif #if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) #define STBI_SSE2 #include #ifdef _MSC_VER #if _MSC_VER >= 1400 // not VC6 #include // __cpuid static int stbi__cpuid3(void) { int info[4]; __cpuid(info,1); return info[3]; } #else static int stbi__cpuid3(void) { int res; __asm { mov eax,1 cpuid mov res,edx } return res; } #endif #define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name #if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) static int stbi__sse2_available(void) { int info3 = stbi__cpuid3(); return ((info3 >> 26) & 1) != 0; } #endif #else // assume GCC-style if not VC++ #define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) #if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) static int stbi__sse2_available(void) { // If we're even attempting to compile this on GCC/Clang, that means // -msse2 is on, which means the compiler is allowed to use SSE2 // instructions at will, and so are we. return 1; } #endif #endif #endif // ARM NEON #if defined(STBI_NO_SIMD) && defined(STBI_NEON) #undef STBI_NEON #endif #ifdef STBI_NEON #include #ifdef _MSC_VER #define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name #else #define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) #endif #endif #ifndef STBI_SIMD_ALIGN #define STBI_SIMD_ALIGN(type, name) type name #endif #ifndef STBI_MAX_DIMENSIONS #define STBI_MAX_DIMENSIONS (1 << 24) #endif /////////////////////////////////////////////// // // stbi__context struct and start_xxx functions // stbi__context structure is our basic context used by all images, so it // contains all the IO context, plus some basic image information typedef struct { stbi__uint32 img_x, img_y; int img_n, img_out_n; stbi_io_callbacks io; void *io_user_data; int read_from_callbacks; int buflen; stbi_uc buffer_start[128]; int callback_already_read; stbi_uc *img_buffer, *img_buffer_end; stbi_uc *img_buffer_original, *img_buffer_original_end; } stbi__context; static void stbi__refill_buffer(stbi__context *s); // initialize a memory-decode context static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) { s->io.read = NULL; s->read_from_callbacks = 0; s->callback_already_read = 0; s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; } // initialize a callback-based context static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user) { s->io = *c; s->io_user_data = user; s->buflen = sizeof(s->buffer_start); s->read_from_callbacks = 1; s->callback_already_read = 0; s->img_buffer = s->img_buffer_original = s->buffer_start; stbi__refill_buffer(s); s->img_buffer_original_end = s->img_buffer_end; } #ifndef STBI_NO_STDIO static int stbi__stdio_read(void *user, char *data, int size) { return (int) fread(data,1,size,(FILE*) user); } static void stbi__stdio_skip(void *user, int n) { int ch; fseek((FILE*) user, n, SEEK_CUR); ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ if (ch != EOF) { ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ } } static int stbi__stdio_eof(void *user) { return feof((FILE*) user) || ferror((FILE *) user); } static stbi_io_callbacks stbi__stdio_callbacks = { stbi__stdio_read, stbi__stdio_skip, stbi__stdio_eof, }; static void stbi__start_file(stbi__context *s, FILE *f) { stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f); } //static void stop_file(stbi__context *s) { } #endif // !STBI_NO_STDIO static void stbi__rewind(stbi__context *s) { // conceptually rewind SHOULD rewind to the beginning of the stream, // but we just rewind to the beginning of the initial buffer, because // we only use it after doing 'test', which only ever looks at at most 92 bytes s->img_buffer = s->img_buffer_original; s->img_buffer_end = s->img_buffer_original_end; } enum { STBI_ORDER_RGB, STBI_ORDER_BGR }; typedef struct { int bits_per_channel; int num_channels; int channel_order; } stbi__result_info; #ifndef STBI_NO_JPEG static int stbi__jpeg_test(stbi__context *s); static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_PNG static int stbi__png_test(stbi__context *s); static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp); static int stbi__png_is16(stbi__context *s); #endif #ifndef STBI_NO_BMP static int stbi__bmp_test(stbi__context *s); static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_TGA static int stbi__tga_test(stbi__context *s); static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_PSD static int stbi__psd_test(stbi__context *s); static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc); static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp); static int stbi__psd_is16(stbi__context *s); #endif #ifndef STBI_NO_HDR static int stbi__hdr_test(stbi__context *s); static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_PIC static int stbi__pic_test(stbi__context *s); static void *stbi__pic_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_GIF static int stbi__gif_test(stbi__context *s); static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp); static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); #endif #ifndef STBI_NO_PNM static int stbi__pnm_test(stbi__context *s); static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp); static int stbi__pnm_is16(stbi__context *s); #endif static #ifdef STBI_THREAD_LOCAL STBI_THREAD_LOCAL #endif const char *stbi__g_failure_reason; STBIDEF const char *stbi_failure_reason(void) { return stbi__g_failure_reason; } #ifndef STBI_NO_FAILURE_STRINGS static int stbi__err(const char *str) { stbi__g_failure_reason = str; return 0; } #endif static void *stbi__malloc(size_t size) { return STBI_MALLOC(size); } // stb_image uses ints pervasively, including for offset calculations. // therefore the largest decoded image size we can support with the // current code, even on 64-bit targets, is INT_MAX. this is not a // significant limitation for the intended use case. // // we do, however, need to make sure our size calculations don't // overflow. hence a few helper functions for size calculations that // multiply integers together, making sure that they're non-negative // and no overflow occurs. // return 1 if the sum is valid, 0 on overflow. // negative terms are considered invalid. static int stbi__addsizes_valid(int a, int b) { if (b < 0) return 0; // now 0 <= b <= INT_MAX, hence also // 0 <= INT_MAX - b <= INTMAX. // And "a + b <= INT_MAX" (which might overflow) is the // same as a <= INT_MAX - b (no overflow) return a <= INT_MAX - b; } // returns 1 if the product is valid, 0 on overflow. // negative factors are considered invalid. static int stbi__mul2sizes_valid(int a, int b) { if (a < 0 || b < 0) return 0; if (b == 0) return 1; // mul-by-0 is always safe // portable way to check for no overflows in a*b return a <= INT_MAX/b; } #if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) // returns 1 if "a*b + add" has no negative terms/factors and doesn't overflow static int stbi__mad2sizes_valid(int a, int b, int add) { return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); } #endif // returns 1 if "a*b*c + add" has no negative terms/factors and doesn't overflow static int stbi__mad3sizes_valid(int a, int b, int c, int add) { return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && stbi__addsizes_valid(a*b*c, add); } // returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow #if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) { return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add); } #endif #if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) // mallocs with size overflow checking static void *stbi__malloc_mad2(int a, int b, int add) { if (!stbi__mad2sizes_valid(a, b, add)) return NULL; return stbi__malloc(a*b + add); } #endif static void *stbi__malloc_mad3(int a, int b, int c, int add) { if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL; return stbi__malloc(a*b*c + add); } #if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) { if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; return stbi__malloc(a*b*c*d + add); } #endif // returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow. static int stbi__addints_valid(int a, int b) { if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. return a <= INT_MAX - b; } // returns 1 if the product of two ints fits in a signed short, 0 on overflow. static int stbi__mul2shorts_valid(int a, int b) { if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN return a >= SHRT_MIN / b; } // stbi__err - error // stbi__errpf - error returning pointer to float // stbi__errpuc - error returning pointer to unsigned char #ifdef STBI_NO_FAILURE_STRINGS #define stbi__err(x,y) 0 #elif defined(STBI_FAILURE_USERMSG) #define stbi__err(x,y) stbi__err(y) #else #define stbi__err(x,y) stbi__err(x) #endif #define stbi__errpf(x,y) ((float *)(size_t) (stbi__err(x,y)?NULL:NULL)) #define stbi__errpuc(x,y) ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL)) STBIDEF void stbi_image_free(void *retval_from_stbi_load) { STBI_FREE(retval_from_stbi_load); } #ifndef STBI_NO_LINEAR static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp); #endif #ifndef STBI_NO_HDR static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp); #endif static int stbi__vertically_flip_on_load_global = 0; STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) { stbi__vertically_flip_on_load_global = flag_true_if_should_flip; } #ifndef STBI_THREAD_LOCAL #define stbi__vertically_flip_on_load stbi__vertically_flip_on_load_global #else static STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set; STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip) { stbi__vertically_flip_on_load_local = flag_true_if_should_flip; stbi__vertically_flip_on_load_set = 1; } #define stbi__vertically_flip_on_load (stbi__vertically_flip_on_load_set \ ? stbi__vertically_flip_on_load_local \ : stbi__vertically_flip_on_load_global) #endif // STBI_THREAD_LOCAL static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) { memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order ri->num_channels = 0; // test the formats with a very explicit header first (at least a FOURCC // or distinctive magic number first) #ifndef STBI_NO_PNG if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); #endif #ifndef STBI_NO_BMP if (stbi__bmp_test(s)) return stbi__bmp_load(s,x,y,comp,req_comp, ri); #endif #ifndef STBI_NO_GIF if (stbi__gif_test(s)) return stbi__gif_load(s,x,y,comp,req_comp, ri); #endif #ifndef STBI_NO_PSD if (stbi__psd_test(s)) return stbi__psd_load(s,x,y,comp,req_comp, ri, bpc); #else STBI_NOTUSED(bpc); #endif #ifndef STBI_NO_PIC if (stbi__pic_test(s)) return stbi__pic_load(s,x,y,comp,req_comp, ri); #endif // then the formats that can end up attempting to load with just 1 or 2 // bytes matching expectations; these are prone to false positives, so // try them later #ifndef STBI_NO_JPEG if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); #endif #ifndef STBI_NO_PNM if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); #endif #ifndef STBI_NO_HDR if (stbi__hdr_test(s)) { float *hdr = stbi__hdr_load(s, x,y,comp,req_comp, ri); return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); } #endif #ifndef STBI_NO_TGA // test tga last because it's a crappy test! if (stbi__tga_test(s)) return stbi__tga_load(s,x,y,comp,req_comp, ri); #endif return stbi__errpuc("unknown image type", "Image not of any known type, or corrupt"); } static stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels) { int i; int img_len = w * h * channels; stbi_uc *reduced; reduced = (stbi_uc *) stbi__malloc(img_len); if (reduced == NULL) return stbi__errpuc("outofmem", "Out of memory"); for (i = 0; i < img_len; ++i) reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling STBI_FREE(orig); return reduced; } static stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels) { int i; int img_len = w * h * channels; stbi__uint16 *enlarged; enlarged = (stbi__uint16 *) stbi__malloc(img_len*2); if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); for (i = 0; i < img_len; ++i) enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff STBI_FREE(orig); return enlarged; } static void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel) { int row; size_t bytes_per_row = (size_t)w * bytes_per_pixel; stbi_uc temp[2048]; stbi_uc *bytes = (stbi_uc *)image; for (row = 0; row < (h>>1); row++) { stbi_uc *row0 = bytes + row*bytes_per_row; stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row; // swap row0 with row1 size_t bytes_left = bytes_per_row; while (bytes_left) { size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); memcpy(temp, row0, bytes_copy); memcpy(row0, row1, bytes_copy); memcpy(row1, temp, bytes_copy); row0 += bytes_copy; row1 += bytes_copy; bytes_left -= bytes_copy; } } } #ifndef STBI_NO_GIF static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel) { int slice; int slice_size = w * h * bytes_per_pixel; stbi_uc *bytes = (stbi_uc *)image; for (slice = 0; slice < z; ++slice) { stbi__vertical_flip(bytes, w, h, bytes_per_pixel); bytes += slice_size; } } #endif static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) { stbi__result_info ri; void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); if (result == NULL) return NULL; // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); if (ri.bits_per_channel != 8) { result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); ri.bits_per_channel = 8; } // @TODO: move stbi__convert_format to here if (stbi__vertically_flip_on_load) { int channels = req_comp ? req_comp : *comp; stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); } return (unsigned char *) result; } static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) { stbi__result_info ri; void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); if (result == NULL) return NULL; // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); if (ri.bits_per_channel != 16) { result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); ri.bits_per_channel = 16; } // @TODO: move stbi__convert_format16 to here // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to keep more precision if (stbi__vertically_flip_on_load) { int channels = req_comp ? req_comp : *comp; stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); } return (stbi__uint16 *) result; } #if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp) { if (stbi__vertically_flip_on_load && result != NULL) { int channels = req_comp ? req_comp : *comp; stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); } } #endif #ifndef STBI_NO_STDIO #if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, uint16_t *widestr, int cchwide); STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const uint16_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); #endif #if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const uint16_t* input) { return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); } #endif static FILE *stbi__fopen(char const *filename, char const *mode) { FILE *f; #if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) uint16_t wMode[64]; uint16_t wFilename[1024]; if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) return 0; if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) return 0; #if defined(_MSC_VER) && _MSC_VER >= 1400 if (0 != _wfopen_s(&f, wFilename, wMode)) f = 0; #else f = _wfopen(wFilename, wMode); #endif #elif defined(_MSC_VER) && _MSC_VER >= 1400 if (0 != fopen_s(&f, filename, mode)) f=0; #else f = fopen(filename, mode); #endif return f; } STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) { FILE *f = stbi__fopen(filename, "rb"); unsigned char *result; if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); result = stbi_load_from_file(f,x,y,comp,req_comp); fclose(f); return result; } STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) { unsigned char *result; stbi__context s; stbi__start_file(&s,f); result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); if (result) { // need to 'unget' all the characters in the IO buffer fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); } return result; } STBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp) { stbi__uint16 *result; stbi__context s; stbi__start_file(&s,f); result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp); if (result) { // need to 'unget' all the characters in the IO buffer fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); } return result; } STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp) { FILE *f = stbi__fopen(filename, "rb"); stbi__uint16 *result; if (!f) return (stbi_us *) stbi__errpuc("can't fopen", "Unable to open file"); result = stbi_load_from_file_16(f,x,y,comp,req_comp); fclose(f); return result; } #endif //!STBI_NO_STDIO STBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels) { stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); } STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels) { stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user); return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); } STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) { stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); } STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) { stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); } #ifndef STBI_NO_GIF STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp) { unsigned char *result; stbi__context s; stbi__start_mem(&s,buffer,len); result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp); if (stbi__vertically_flip_on_load) { stbi__vertical_flip_slices( result, *x, *y, *z, *comp ); } return result; } #endif #ifndef STBI_NO_LINEAR static float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp) { unsigned char *data; #ifndef STBI_NO_HDR if (stbi__hdr_test(s)) { stbi__result_info ri; float *hdr_data = stbi__hdr_load(s,x,y,comp,req_comp, &ri); if (hdr_data) stbi__float_postprocess(hdr_data,x,y,comp,req_comp); return hdr_data; } #endif data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); if (data) return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); return stbi__errpf("unknown image type", "Image not of any known type, or corrupt"); } STBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) { stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__loadf_main(&s,x,y,comp,req_comp); } STBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) { stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); return stbi__loadf_main(&s,x,y,comp,req_comp); } #ifndef STBI_NO_STDIO STBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) { float *result; FILE *f = stbi__fopen(filename, "rb"); if (!f) return stbi__errpf("can't fopen", "Unable to open file"); result = stbi_loadf_from_file(f,x,y,comp,req_comp); fclose(f); return result; } STBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) { stbi__context s; stbi__start_file(&s,f); return stbi__loadf_main(&s,x,y,comp,req_comp); } #endif // !STBI_NO_STDIO #endif // !STBI_NO_LINEAR // these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is // defined, for API simplicity; if STBI_NO_LINEAR is defined, it always // reports false! STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) { #ifndef STBI_NO_HDR stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__hdr_test(&s); #else STBI_NOTUSED(buffer); STBI_NOTUSED(len); return 0; #endif } #ifndef STBI_NO_STDIO STBIDEF int stbi_is_hdr (char const *filename) { FILE *f = stbi__fopen(filename, "rb"); int result=0; if (f) { result = stbi_is_hdr_from_file(f); fclose(f); } return result; } STBIDEF int stbi_is_hdr_from_file(FILE *f) { #ifndef STBI_NO_HDR long pos = ftell(f); int res; stbi__context s; stbi__start_file(&s,f); res = stbi__hdr_test(&s); fseek(f, pos, SEEK_SET); return res; #else STBI_NOTUSED(f); return 0; #endif } #endif // !STBI_NO_STDIO STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) { #ifndef STBI_NO_HDR stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); return stbi__hdr_test(&s); #else STBI_NOTUSED(clbk); STBI_NOTUSED(user); return 0; #endif } #ifndef STBI_NO_LINEAR static float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f; STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } #endif static float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f; STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; } STBIDEF void stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; } ////////////////////////////////////////////////////////////////////////////// // // Common code used by all image loaders // enum { STBI__SCAN_load=0, STBI__SCAN_type, STBI__SCAN_header }; static void stbi__refill_buffer(stbi__context *s) { int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); if (n == 0) { // at end of file, treat same as if from memory, but need to handle case // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file s->read_from_callbacks = 0; s->img_buffer = s->buffer_start; s->img_buffer_end = s->buffer_start+1; *s->img_buffer = 0; } else { s->img_buffer = s->buffer_start; s->img_buffer_end = s->buffer_start + n; } } stbi_inline static stbi_uc stbi__get8(stbi__context *s) { if (s->img_buffer < s->img_buffer_end) return *s->img_buffer++; if (s->read_from_callbacks) { stbi__refill_buffer(s); return *s->img_buffer++; } return 0; } #if defined(STBI_NO_JPEG) && defined(STBI_NO_HDR) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) // nothing #else stbi_inline static int stbi__at_eof(stbi__context *s) { if (s->io.read) { if (!(s->io.eof)(s->io_user_data)) return 0; // if feof() is true, check if buffer = end // special case: we've only got the special 0 character at the end if (s->read_from_callbacks == 0) return 1; } return s->img_buffer >= s->img_buffer_end; } #endif #if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) // nothing #else static void stbi__skip(stbi__context *s, int n) { if (n == 0) return; // already there! if (n < 0) { s->img_buffer = s->img_buffer_end; return; } if (s->io.read) { int blen = (int) (s->img_buffer_end - s->img_buffer); if (blen < n) { s->img_buffer = s->img_buffer_end; (s->io.skip)(s->io_user_data, n - blen); return; } } s->img_buffer += n; } #endif #if defined(STBI_NO_PNG) && defined(STBI_NO_TGA) && defined(STBI_NO_HDR) && defined(STBI_NO_PNM) // nothing #else static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) { if (s->io.read) { int blen = (int) (s->img_buffer_end - s->img_buffer); if (blen < n) { int res, count; memcpy(buffer, s->img_buffer, blen); count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); res = (count == (n-blen)); s->img_buffer = s->img_buffer_end; return res; } } if (s->img_buffer+n <= s->img_buffer_end) { memcpy(buffer, s->img_buffer, n); s->img_buffer += n; return 1; } else return 0; } #endif #if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) // nothing #else static int stbi__get16be(stbi__context *s) { int z = stbi__get8(s); return (z << 8) + stbi__get8(s); } #endif #if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) // nothing #else static stbi__uint32 stbi__get32be(stbi__context *s) { stbi__uint32 z = stbi__get16be(s); return (z << 16) + stbi__get16be(s); } #endif #if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) // nothing #else static int stbi__get16le(stbi__context *s) { int z = stbi__get8(s); return z + (stbi__get8(s) << 8); } #endif #ifndef STBI_NO_BMP static stbi__uint32 stbi__get32le(stbi__context *s) { stbi__uint32 z = stbi__get16le(s); z += (stbi__uint32)stbi__get16le(s) << 16; return z; } #endif #define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) // truncate int to byte without warnings #if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) // nothing #else ////////////////////////////////////////////////////////////////////////////// // // generic converter from built-in img_n to req_comp // individual types do this automatically as much as possible (e.g. jpeg // does all cases internally since it needs to colorspace convert anyway, // and it never has alpha, so very few cases ). png can automatically // interleave an alpha=255 channel, but falls back to this for other cases // // assume data buffer is malloced, so malloc a new one and free that one // only failure mode is malloc failing static stbi_uc stbi__compute_y(int r, int g, int b) { return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); } #endif #if defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) // nothing #else static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) { int i,j; unsigned char *good; if (req_comp == img_n) return data; STBI_ASSERT(req_comp >= 1 && req_comp <= 4); good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0); if (good == NULL) { STBI_FREE(data); return stbi__errpuc("outofmem", "Out of memory"); } for (j=0; j < (int) y; ++j) { unsigned char *src = data + j * x * img_n ; unsigned char *dest = good + j * x * req_comp; #define STBI__COMBO(a,b) ((a)*8+(b)) #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) // convert source image with img_n components to one with req_comp components; // avoid switch per pixel, so use switch per scanline and massive macros switch (STBI__COMBO(img_n, req_comp)) { STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255; } break; STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255; } break; STBI__CASE(2,1) { dest[0]=src[0]; } break; STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255; } break; STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255; } break; STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); } #undef STBI__CASE } STBI_FREE(data); return good; } #endif #if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) // nothing #else static stbi__uint16 stbi__compute_y_16(int r, int g, int b) { return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); } #endif #if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) // nothing #else static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) { int i,j; stbi__uint16 *good; if (req_comp == img_n) return data; STBI_ASSERT(req_comp >= 1 && req_comp <= 4); good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2); if (good == NULL) { STBI_FREE(data); return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); } for (j=0; j < (int) y; ++j) { stbi__uint16 *src = data + j * x * img_n ; stbi__uint16 *dest = good + j * x * req_comp; #define STBI__COMBO(a,b) ((a)*8+(b)) #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) // convert source image with img_n components to one with req_comp components; // avoid switch per pixel, so use switch per scanline and massive macros switch (STBI__COMBO(img_n, req_comp)) { STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff; } break; STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff; } break; STBI__CASE(2,1) { dest[0]=src[0]; } break; STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff; } break; STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break; STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); } #undef STBI__CASE } STBI_FREE(data); return good; } #endif #ifndef STBI_NO_LINEAR static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp) { int i,k,n; float *output; if (!data) return NULL; output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0); if (output == NULL) { STBI_FREE(data); return stbi__errpf("outofmem", "Out of memory"); } // compute number of non-alpha components if (comp & 1) n = comp; else n = comp-1; for (i=0; i < x*y; ++i) { for (k=0; k < n; ++k) { output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale); } } if (n < comp) { for (i=0; i < x*y; ++i) { output[i*comp + n] = data[i*comp + n]/255.0f; } } STBI_FREE(data); return output; } #endif #ifndef STBI_NO_HDR #define stbi__float2int(x) ((int) (x)) static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp) { int i,k,n; stbi_uc *output; if (!data) return NULL; output = (stbi_uc *) stbi__malloc_mad3(x, y, comp, 0); if (output == NULL) { STBI_FREE(data); return stbi__errpuc("outofmem", "Out of memory"); } // compute number of non-alpha components if (comp & 1) n = comp; else n = comp-1; for (i=0; i < x*y; ++i) { for (k=0; k < n; ++k) { float z = (float) pow(data[i*comp+k]*stbi__h2l_scale_i, stbi__h2l_gamma_i) * 255 + 0.5f; if (z < 0) z = 0; if (z > 255) z = 255; output[i*comp + k] = (stbi_uc) stbi__float2int(z); } if (k < comp) { float z = data[i*comp+k] * 255 + 0.5f; if (z < 0) z = 0; if (z > 255) z = 255; output[i*comp + k] = (stbi_uc) stbi__float2int(z); } } STBI_FREE(data); return output; } #endif ////////////////////////////////////////////////////////////////////////////// // // "baseline" JPEG/JFIF decoder // // simple implementation // - doesn't support delayed output of y-dimension // - simple interface (only one output format: 8-bit interleaved RGB) // - doesn't try to recover corrupt jpegs // - doesn't allow partial loading, loading multiple at once // - still fast on x86 (copying globals into locals doesn't help x86) // - allocates lots of intermediate memory (full size of all components) // - non-interleaved case requires this anyway // - allows good upsampling (see next) // high-quality // - upsampled channels are bilinearly interpolated, even across blocks // - quality integer IDCT derived from IJG's 'slow' // performance // - fast huffman; reasonable integer IDCT // - some SIMD kernels for common paths on targets with SSE2/NEON // - uses a lot of intermediate memory, could cache poorly #ifndef STBI_NO_JPEG // huffman decoding acceleration #define FAST_BITS 9 // larger handles more cases; smaller stomps less cache typedef struct { stbi_uc fast[1 << FAST_BITS]; // weirdly, repacking this into AoS is a 10% speed loss, instead of a win stbi__uint16 code[256]; stbi_uc values[256]; stbi_uc size[257]; unsigned int maxcode[18]; int delta[17]; // old 'firstsymbol' - old 'firstcode' } stbi__huffman; typedef struct { stbi__context *s; stbi__huffman huff_dc[4]; stbi__huffman huff_ac[4]; stbi__uint16 dequant[4][64]; stbi__int16 fast_ac[4][1 << FAST_BITS]; // sizes for components, interleaved MCUs int img_h_max, img_v_max; int img_mcu_x, img_mcu_y; int img_mcu_w, img_mcu_h; // definition of jpeg image component struct { int id; int h,v; int tq; int hd,ha; int dc_pred; int x,y,w2,h2; stbi_uc *data; void *raw_data, *raw_coeff; stbi_uc *linebuf; short *coeff; // progressive only int coeff_w, coeff_h; // number of 8x8 coefficient blocks } img_comp[4]; stbi__uint32 code_buffer; // jpeg entropy-coded buffer int code_bits; // number of valid bits unsigned char marker; // marker seen while filling entropy buffer int nomore; // flag if we saw a marker so must stop int progressive; int spec_start; int spec_end; int succ_high; int succ_low; int eob_run; int jfif; int app14_color_transform; // Adobe APP14 tag int rgb; int scan_n, order[4]; int restart_interval, todo; // kernels void (*idct_block_kernel)(stbi_uc *out, int ort_stride, short data[64]); void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step); stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs); } stbi__jpeg; static int stbi__build_huffman(stbi__huffman *h, int *count) { int i,j,k=0; unsigned int code; // build size list for each symbol (from JPEG spec) for (i=0; i < 16; ++i) { for (j=0; j < count[i]; ++j) { h->size[k++] = (stbi_uc) (i+1); if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); } } h->size[k] = 0; // compute actual symbols (from jpeg spec) code = 0; k = 0; for(j=1; j <= 16; ++j) { // compute delta to add to code to compute symbol id h->delta[j] = k - code; if (h->size[k] == j) { while (h->size[k] == j) h->code[k++] = (stbi__uint16) (code++); if (code-1 >= (1u << j)) return stbi__err("bad code lengths","Corrupt JPEG"); } // compute largest code + 1 for this size, preshifted as needed later h->maxcode[j] = code << (16-j); code <<= 1; } h->maxcode[j] = 0xffffffff; // build non-spec acceleration table; 255 is flag for not-accelerated memset(h->fast, 255, 1 << FAST_BITS); for (i=0; i < k; ++i) { int s = h->size[i]; if (s <= FAST_BITS) { int c = h->code[i] << (FAST_BITS-s); int m = 1 << (FAST_BITS-s); for (j=0; j < m; ++j) { h->fast[c+j] = (stbi_uc) i; } } } return 1; } // build a table that decodes both magnitude and value of small ACs in // one go. static void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h) { int i; for (i=0; i < (1 << FAST_BITS); ++i) { stbi_uc fast = h->fast[i]; fast_ac[i] = 0; if (fast < 255) { int rs = h->values[fast]; int run = (rs >> 4) & 15; int magbits = rs & 15; int len = h->size[fast]; if (magbits && len + magbits <= FAST_BITS) { // magnitude code followed by receive_extend code int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits); int m = 1 << (magbits - 1); if (k < m) k += (~0U << magbits) + 1; // if the result is small enough, we can fit it in fast_ac table if (k >= -128 && k <= 127) fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits)); } } } } static void stbi__grow_buffer_unsafe(stbi__jpeg *j) { do { unsigned int b = j->nomore ? 0 : stbi__get8(j->s); if (b == 0xff) { int c = stbi__get8(j->s); while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes if (c != 0) { j->marker = (unsigned char) c; j->nomore = 1; return; } } j->code_buffer |= b << (24 - j->code_bits); j->code_bits += 8; } while (j->code_bits <= 24); } // (1 << n) - 1 static const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; // decode a jpeg huffman value from the bitstream stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) { unsigned int temp; int c,k; if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); // look at the top FAST_BITS and determine what symbol ID it is, // if the code is <= FAST_BITS c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); k = h->fast[c]; if (k < 255) { int s = h->size[k]; if (s > j->code_bits) return -1; j->code_buffer <<= s; j->code_bits -= s; return h->values[k]; } // naive test is to shift the code_buffer down so k bits are // valid, then test against maxcode. To speed this up, we've // preshifted maxcode left so that it has (16-k) 0s at the // end; in other words, regardless of the number of bits, it // wants to be compared against something shifted to have 16; // that way we don't need to shift inside the loop. temp = j->code_buffer >> 16; for (k=FAST_BITS+1 ; ; ++k) if (temp < h->maxcode[k]) break; if (k == 17) { // error! code not found j->code_bits -= 16; return -1; } if (k > j->code_bits) return -1; // convert the huffman code to the symbol id c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; if(c < 0 || c >= 256) // symbol id out of bounds! return -1; STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); // convert the id to a symbol j->code_bits -= k; j->code_buffer <<= k; return h->values[c]; } // bias[n] = (-1<code_bits < n) stbi__grow_buffer_unsafe(j); if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) k = stbi_lrot(j->code_buffer, n); j->code_buffer = k & ~stbi__bmask[n]; k &= stbi__bmask[n]; j->code_bits -= n; return k + (stbi__jbias[n] & (sgn - 1)); } // get some unsigned bits stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) { unsigned int k; if (j->code_bits < n) stbi__grow_buffer_unsafe(j); if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing k = stbi_lrot(j->code_buffer, n); j->code_buffer = k & ~stbi__bmask[n]; k &= stbi__bmask[n]; j->code_bits -= n; return k; } stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) { unsigned int k; if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing k = j->code_buffer; j->code_buffer <<= 1; --j->code_bits; return k & 0x80000000; } // given a value that's at position X in the zigzag stream, // where does it appear in the 8x8 matrix coded as row-major? static const stbi_uc stbi__jpeg_dezigzag[64+15] = { 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, // let corrupt input sample past end 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 }; // decode one 64-entry block-- static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant) { int diff,dc,k; int t; if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); t = stbi__jpeg_huff_decode(j, hdc); if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); // 0 all the ac values now so we can do it 32-bits at a time memset(data,0,64*sizeof(data[0])); diff = t ? stbi__extend_receive(j, t) : 0; if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); dc = j->img_comp[b].dc_pred + diff; j->img_comp[b].dc_pred = dc; if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); data[0] = (short) (dc * dequant[0]); // decode AC components, see JPEG spec k = 1; do { unsigned int zig; int c,r,s; if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); r = fac[c]; if (r) { // fast-AC path k += (r >> 4) & 15; // run s = r & 15; // combined length if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); j->code_buffer <<= s; j->code_bits -= s; // decode into unzigzag'd location zig = stbi__jpeg_dezigzag[k++]; data[zig] = (short) ((r >> 8) * dequant[zig]); } else { int rs = stbi__jpeg_huff_decode(j, hac); if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); s = rs & 15; r = rs >> 4; if (s == 0) { if (rs != 0xf0) break; // end block k += 16; } else { k += r; // decode into unzigzag'd location zig = stbi__jpeg_dezigzag[k++]; data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]); } } } while (k < 64); return 1; } static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b) { int diff,dc; int t; if (j->spec_end != 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); if (j->succ_high == 0) { // first scan for DC coefficient, must be first memset(data,0,64*sizeof(data[0])); // 0 all the ac values now t = stbi__jpeg_huff_decode(j, hdc); if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); diff = t ? stbi__extend_receive(j, t) : 0; if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); dc = j->img_comp[b].dc_pred + diff; j->img_comp[b].dc_pred = dc; if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); data[0] = (short) (dc * (1 << j->succ_low)); } else { // refinement scan for DC coefficient if (stbi__jpeg_get_bit(j)) data[0] += (short) (1 << j->succ_low); } return 1; } // @OPTIMIZE: store non-zigzagged during the decode passes, // and only de-zigzag when dequantizing static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac) { int k; if (j->spec_start == 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); if (j->succ_high == 0) { int shift = j->succ_low; if (j->eob_run) { --j->eob_run; return 1; } k = j->spec_start; do { unsigned int zig; int c,r,s; if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); r = fac[c]; if (r) { // fast-AC path k += (r >> 4) & 15; // run s = r & 15; // combined length if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); j->code_buffer <<= s; j->code_bits -= s; zig = stbi__jpeg_dezigzag[k++]; data[zig] = (short) ((r >> 8) * (1 << shift)); } else { int rs = stbi__jpeg_huff_decode(j, hac); if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); s = rs & 15; r = rs >> 4; if (s == 0) { if (r < 15) { j->eob_run = (1 << r); if (r) j->eob_run += stbi__jpeg_get_bits(j, r); --j->eob_run; break; } k += 16; } else { k += r; zig = stbi__jpeg_dezigzag[k++]; data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); } } } while (k <= j->spec_end); } else { // refinement scan for these AC coefficients short bit = (short) (1 << j->succ_low); if (j->eob_run) { --j->eob_run; for (k = j->spec_start; k <= j->spec_end; ++k) { short *p = &data[stbi__jpeg_dezigzag[k]]; if (*p != 0) if (stbi__jpeg_get_bit(j)) if ((*p & bit)==0) { if (*p > 0) *p += bit; else *p -= bit; } } } else { k = j->spec_start; do { int r,s; int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); s = rs & 15; r = rs >> 4; if (s == 0) { if (r < 15) { j->eob_run = (1 << r) - 1; if (r) j->eob_run += stbi__jpeg_get_bits(j, r); r = 64; // force end of block } else { // r=15 s=0 should write 16 0s, so we just do // a run of 15 0s and then write s (which is 0), // so we don't have to do anything special here } } else { if (s != 1) return stbi__err("bad huffman code", "Corrupt JPEG"); // sign bit if (stbi__jpeg_get_bit(j)) s = bit; else s = -bit; } // advance by r while (k <= j->spec_end) { short *p = &data[stbi__jpeg_dezigzag[k++]]; if (*p != 0) { if (stbi__jpeg_get_bit(j)) if ((*p & bit)==0) { if (*p > 0) *p += bit; else *p -= bit; } } else { if (r == 0) { *p = (short) s; break; } --r; } } } while (k <= j->spec_end); } } return 1; } // take a -128..127 value and stbi__clamp it and convert to 0..255 stbi_inline static stbi_uc stbi__clamp(int x) { // trick to use a single test to catch both cases if ((unsigned int) x > 255) { if (x < 0) return 0; if (x > 255) return 255; } return (stbi_uc) x; } #define stbi__f2f(x) ((int) (((x) * 4096 + 0.5))) #define stbi__fsh(x) ((x) * 4096) // derived from jidctint -- DCT_ISLOW #define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ p2 = s2; \ p3 = s6; \ p1 = (p2+p3) * stbi__f2f(0.5411961f); \ t2 = p1 + p3*stbi__f2f(-1.847759065f); \ t3 = p1 + p2*stbi__f2f( 0.765366865f); \ p2 = s0; \ p3 = s4; \ t0 = stbi__fsh(p2+p3); \ t1 = stbi__fsh(p2-p3); \ x0 = t0+t3; \ x3 = t0-t3; \ x1 = t1+t2; \ x2 = t1-t2; \ t0 = s7; \ t1 = s5; \ t2 = s3; \ t3 = s1; \ p3 = t0+t2; \ p4 = t1+t3; \ p1 = t0+t3; \ p2 = t1+t2; \ p5 = (p3+p4)*stbi__f2f( 1.175875602f); \ t0 = t0*stbi__f2f( 0.298631336f); \ t1 = t1*stbi__f2f( 2.053119869f); \ t2 = t2*stbi__f2f( 3.072711026f); \ t3 = t3*stbi__f2f( 1.501321110f); \ p1 = p5 + p1*stbi__f2f(-0.899976223f); \ p2 = p5 + p2*stbi__f2f(-2.562915447f); \ p3 = p3*stbi__f2f(-1.961570560f); \ p4 = p4*stbi__f2f(-0.390180644f); \ t3 += p1+p4; \ t2 += p2+p3; \ t1 += p2+p4; \ t0 += p1+p3; static void stbi__idct_block(stbi_uc *out, int ort_stride, short data[64]) { int i,val[64],*v=val; stbi_uc *o; short *d = data; // columns for (i=0; i < 8; ++i,++d, ++v) { // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 && d[40]==0 && d[48]==0 && d[56]==0) { // no shortcut 0 seconds // (1|2|3|4|5|6|7)==0 0 seconds // all separate -0.047 seconds // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds int dcterm = d[0]*4; v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; } else { STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56]) // constants scaled things up by 1<<12; let's bring them back // down, but keep 2 extra bits of precision x0 += 512; x1 += 512; x2 += 512; x3 += 512; v[ 0] = (x0+t3) >> 10; v[56] = (x0-t3) >> 10; v[ 8] = (x1+t2) >> 10; v[48] = (x1-t2) >> 10; v[16] = (x2+t1) >> 10; v[40] = (x2-t1) >> 10; v[24] = (x3+t0) >> 10; v[32] = (x3-t0) >> 10; } } for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=ort_stride) { // no fast case since the first 1D IDCT spread components out STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) // constants scaled things up by 1<<12, plus we had 1<<2 from first // loop, plus horizontal and vertical each scale by sqrt(8) so together // we've got an extra 1<<3, so 1<<17 total we need to remove. // so we want to round that, which means adding 0.5 * 1<<17, // aka 65536. Also, we'll end up with -128 to 127 that we want // to encode as 0..255 by adding 128, so we'll add that before the shift x0 += 65536 + (128<<17); x1 += 65536 + (128<<17); x2 += 65536 + (128<<17); x3 += 65536 + (128<<17); // tried computing the shifts into temps, or'ing the temps to see // if any were out of range, but that was slower o[0] = stbi__clamp((x0+t3) >> 17); o[7] = stbi__clamp((x0-t3) >> 17); o[1] = stbi__clamp((x1+t2) >> 17); o[6] = stbi__clamp((x1-t2) >> 17); o[2] = stbi__clamp((x2+t1) >> 17); o[5] = stbi__clamp((x2-t1) >> 17); o[3] = stbi__clamp((x3+t0) >> 17); o[4] = stbi__clamp((x3-t0) >> 17); } } #ifdef STBI_SSE2 // sse2 integer IDCT. not the fastest possible implementation but it // produces bit-identical results to the generic C version so it's // fully "transparent". static void stbi__idct_simd(stbi_uc *out, int ort_stride, short data[64]) { // This is constructed to match our regular (generic) integer IDCT exactly. __m128i row0, row1, row2, row3, row4, row5, row6, row7; __m128i tmp; // dot product constant: even elems=x, odd elems=y #define dct_const(x,y) _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y)) // out(0) = c0[even]*x + c0[odd]*y (c0, x, y 16-bit, out 32-bit) // out(1) = c1[even]*x + c1[odd]*y #define dct_rot(out0,out1, x,y,c0,c1) \ __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \ __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \ __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) // out = in << 12 (in 16-bit, out 32-bit) #define dct_widen(out, in) \ __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) // wide add #define dct_wadd(out, a, b) \ __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ __m128i out##_h = _mm_add_epi32(a##_h, b##_h) // wide sub #define dct_wsub(out, a, b) \ __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) // butterfly a/b, add bias, then shift by "s" and pack #define dct_bfly32o(out0, out1, a,b,bias,s) \ { \ __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ dct_wadd(sum, abiased, b); \ dct_wsub(dif, abiased, b); \ out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \ out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \ } // 8-bit interleave step (for transposes) #define dct_interleave8(a, b) \ tmp = a; \ a = _mm_unpacklo_epi8(a, b); \ b = _mm_unpackhi_epi8(tmp, b) // 16-bit interleave step (for transposes) #define dct_interleave16(a, b) \ tmp = a; \ a = _mm_unpacklo_epi16(a, b); \ b = _mm_unpackhi_epi16(tmp, b) #define dct_pass(bias,shift) \ { \ /* even part */ \ dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \ __m128i sum04 = _mm_add_epi16(row0, row4); \ __m128i dif04 = _mm_sub_epi16(row0, row4); \ dct_widen(t0e, sum04); \ dct_widen(t1e, dif04); \ dct_wadd(x0, t0e, t3e); \ dct_wsub(x3, t0e, t3e); \ dct_wadd(x1, t1e, t2e); \ dct_wsub(x2, t1e, t2e); \ /* odd part */ \ dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \ dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \ __m128i sum17 = _mm_add_epi16(row1, row7); \ __m128i sum35 = _mm_add_epi16(row3, row5); \ dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \ dct_wadd(x4, y0o, y4o); \ dct_wadd(x5, y1o, y5o); \ dct_wadd(x6, y2o, y5o); \ dct_wadd(x7, y3o, y4o); \ dct_bfly32o(row0,row7, x0,x7,bias,shift); \ dct_bfly32o(row1,row6, x1,x6,bias,shift); \ dct_bfly32o(row2,row5, x2,x5,bias,shift); \ dct_bfly32o(row3,row4, x3,x4,bias,shift); \ } __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f)); __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f)); __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f)); __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f)); __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f)); __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f)); // rounding biases in column/row passes, see stbi__idct_block for explanation. __m128i bias_0 = _mm_set1_epi32(512); __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17)); // load row0 = _mm_load_si128((const __m128i *) (data + 0*8)); row1 = _mm_load_si128((const __m128i *) (data + 1*8)); row2 = _mm_load_si128((const __m128i *) (data + 2*8)); row3 = _mm_load_si128((const __m128i *) (data + 3*8)); row4 = _mm_load_si128((const __m128i *) (data + 4*8)); row5 = _mm_load_si128((const __m128i *) (data + 5*8)); row6 = _mm_load_si128((const __m128i *) (data + 6*8)); row7 = _mm_load_si128((const __m128i *) (data + 7*8)); // column pass dct_pass(bias_0, 10); { // 16bit 8x8 transpose pass 1 dct_interleave16(row0, row4); dct_interleave16(row1, row5); dct_interleave16(row2, row6); dct_interleave16(row3, row7); // transpose pass 2 dct_interleave16(row0, row2); dct_interleave16(row1, row3); dct_interleave16(row4, row6); dct_interleave16(row5, row7); // transpose pass 3 dct_interleave16(row0, row1); dct_interleave16(row2, row3); dct_interleave16(row4, row5); dct_interleave16(row6, row7); } // row pass dct_pass(bias_1, 17); { // pack __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 __m128i p1 = _mm_packus_epi16(row2, row3); __m128i p2 = _mm_packus_epi16(row4, row5); __m128i p3 = _mm_packus_epi16(row6, row7); // 8bit 8x8 transpose pass 1 dct_interleave8(p0, p2); // a0e0a1e1... dct_interleave8(p1, p3); // c0g0c1g1... // transpose pass 2 dct_interleave8(p0, p1); // a0c0e0g0... dct_interleave8(p2, p3); // b0d0f0h0... // transpose pass 3 dct_interleave8(p0, p2); // a0b0c0d0... dct_interleave8(p1, p3); // a4b4c4d4... // store _mm_storel_epi64((__m128i *) out, p0); out += ort_stride; _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += ort_stride; _mm_storel_epi64((__m128i *) out, p2); out += ort_stride; _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += ort_stride; _mm_storel_epi64((__m128i *) out, p1); out += ort_stride; _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += ort_stride; _mm_storel_epi64((__m128i *) out, p3); out += ort_stride; _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e)); } #undef dct_const #undef dct_rot #undef dct_widen #undef dct_wadd #undef dct_wsub #undef dct_bfly32o #undef dct_interleave8 #undef dct_interleave16 #undef dct_pass } #endif // STBI_SSE2 #ifdef STBI_NEON // NEON integer IDCT. should produce bit-identical // results to the generic C version. static void stbi__idct_simd(stbi_uc *out, int ort_stride, short data[64]) { int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f)); int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f)); int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f)); int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f)); int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f)); int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f)); #define dct_long_mul(out, inq, coeff) \ int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) #define dct_long_mac(out, acc, inq, coeff) \ int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) #define dct_widen(out, inq) \ int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) // wide add #define dct_wadd(out, a, b) \ int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ int32x4_t out##_h = vaddq_s32(a##_h, b##_h) // wide sub #define dct_wsub(out, a, b) \ int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ int32x4_t out##_h = vsubq_s32(a##_h, b##_h) // butterfly a/b, then shift using "shiftop" by "s" and pack #define dct_bfly32o(out0,out1, a,b,shiftop,s) \ { \ dct_wadd(sum, a, b); \ dct_wsub(dif, a, b); \ out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ } #define dct_pass(shiftop, shift) \ { \ /* even part */ \ int16x8_t sum26 = vaddq_s16(row2, row6); \ dct_long_mul(p1e, sum26, rot0_0); \ dct_long_mac(t2e, p1e, row6, rot0_1); \ dct_long_mac(t3e, p1e, row2, rot0_2); \ int16x8_t sum04 = vaddq_s16(row0, row4); \ int16x8_t dif04 = vsubq_s16(row0, row4); \ dct_widen(t0e, sum04); \ dct_widen(t1e, dif04); \ dct_wadd(x0, t0e, t3e); \ dct_wsub(x3, t0e, t3e); \ dct_wadd(x1, t1e, t2e); \ dct_wsub(x2, t1e, t2e); \ /* odd part */ \ int16x8_t sum15 = vaddq_s16(row1, row5); \ int16x8_t sum17 = vaddq_s16(row1, row7); \ int16x8_t sum35 = vaddq_s16(row3, row5); \ int16x8_t sum37 = vaddq_s16(row3, row7); \ int16x8_t sumodd = vaddq_s16(sum17, sum35); \ dct_long_mul(p5o, sumodd, rot1_0); \ dct_long_mac(p1o, p5o, sum17, rot1_1); \ dct_long_mac(p2o, p5o, sum35, rot1_2); \ dct_long_mul(p3o, sum37, rot2_0); \ dct_long_mul(p4o, sum15, rot2_1); \ dct_wadd(sump13o, p1o, p3o); \ dct_wadd(sump24o, p2o, p4o); \ dct_wadd(sump23o, p2o, p3o); \ dct_wadd(sump14o, p1o, p4o); \ dct_long_mac(x4, sump13o, row7, rot3_0); \ dct_long_mac(x5, sump24o, row5, rot3_1); \ dct_long_mac(x6, sump23o, row3, rot3_2); \ dct_long_mac(x7, sump14o, row1, rot3_3); \ dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \ dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \ dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \ dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \ } // load row0 = vld1q_s16(data + 0*8); row1 = vld1q_s16(data + 1*8); row2 = vld1q_s16(data + 2*8); row3 = vld1q_s16(data + 3*8); row4 = vld1q_s16(data + 4*8); row5 = vld1q_s16(data + 5*8); row6 = vld1q_s16(data + 6*8); row7 = vld1q_s16(data + 7*8); // add DC bias row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); // column pass dct_pass(vrshrn_n_s32, 10); // 16bit 8x8 transpose { // these three map to a single VTRN.16, VTRN.32, and VSWP, respectively. // whether compilers actually get this is another story, sadly. #define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; } #define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); } #define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); } // pass 1 dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 dct_trn16(row2, row3); dct_trn16(row4, row5); dct_trn16(row6, row7); // pass 2 dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 dct_trn32(row1, row3); dct_trn32(row4, row6); dct_trn32(row5, row7); // pass 3 dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 dct_trn64(row1, row5); dct_trn64(row2, row6); dct_trn64(row3, row7); #undef dct_trn16 #undef dct_trn32 #undef dct_trn64 } // row pass // vrshrn_n_s32 only supports shifts up to 16, we need // 17. so do a non-rounding shift of 16 first then follow // up with a rounding shift by 1. dct_pass(vshrn_n_s32, 16); { // pack and round uint8x8_t p0 = vqrshrun_n_s16(row0, 1); uint8x8_t p1 = vqrshrun_n_s16(row1, 1); uint8x8_t p2 = vqrshrun_n_s16(row2, 1); uint8x8_t p3 = vqrshrun_n_s16(row3, 1); uint8x8_t p4 = vqrshrun_n_s16(row4, 1); uint8x8_t p5 = vqrshrun_n_s16(row5, 1); uint8x8_t p6 = vqrshrun_n_s16(row6, 1); uint8x8_t p7 = vqrshrun_n_s16(row7, 1); // again, these can translate into one instruction, but often don't. #define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; } #define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); } #define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); } // sadly can't use interleaved stores here since we only write // 8 bytes to each scan line! // 8x8 8-bit transpose pass 1 dct_trn8_8(p0, p1); dct_trn8_8(p2, p3); dct_trn8_8(p4, p5); dct_trn8_8(p6, p7); // pass 2 dct_trn8_16(p0, p2); dct_trn8_16(p1, p3); dct_trn8_16(p4, p6); dct_trn8_16(p5, p7); // pass 3 dct_trn8_32(p0, p4); dct_trn8_32(p1, p5); dct_trn8_32(p2, p6); dct_trn8_32(p3, p7); // store vst1_u8(out, p0); out += ort_stride; vst1_u8(out, p1); out += ort_stride; vst1_u8(out, p2); out += ort_stride; vst1_u8(out, p3); out += ort_stride; vst1_u8(out, p4); out += ort_stride; vst1_u8(out, p5); out += ort_stride; vst1_u8(out, p6); out += ort_stride; vst1_u8(out, p7); #undef dct_trn8_8 #undef dct_trn8_16 #undef dct_trn8_32 } #undef dct_long_mul #undef dct_long_mac #undef dct_widen #undef dct_wadd #undef dct_wsub #undef dct_bfly32o #undef dct_pass } #endif // STBI_NEON #define STBI__MARKER_none 0xff // if there's a pending marker from the entropy stream, return that // otherwise, fetch from the stream and get a marker. if there's no // marker, return 0xff, which is never a valid marker value static stbi_uc stbi__get_marker(stbi__jpeg *j) { stbi_uc x; if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; } x = stbi__get8(j->s); if (x != 0xff) return STBI__MARKER_none; while (x == 0xff) x = stbi__get8(j->s); // consume repeated 0xff fill bytes return x; } // in each scan, we'll have scan_n components, and the order // of the components is specified by order[] #define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) // after a restart interval, stbi__jpeg_reset the entropy decoder and // the dc prediction static void stbi__jpeg_reset(stbi__jpeg *j) { j->code_bits = 0; j->code_buffer = 0; j->nomore = 0; j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0; j->marker = STBI__MARKER_none; j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; j->eob_run = 0; // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, // since we don't even allow 1<<30 pixels } static int stbi__parse_entropy_coded_data(stbi__jpeg *z) { stbi__jpeg_reset(z); if (!z->progressive) { if (z->scan_n == 1) { int i,j; STBI_SIMD_ALIGN(short, data[64]); int n = z->order[0]; // non-interleaved data, we just need to process one block at a time, // in trivial scanline order // number of blocks to do just depends on how many actual "pixels" this // component has, independent of interleaved MCU blocking and such int w = (z->img_comp[n].x+7) >> 3; int h = (z->img_comp[n].y+7) >> 3; for (j=0; j < h; ++j) { for (i=0; i < w; ++i) { int ha = z->img_comp[n].ha; if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); // every data block is an MCU, so countdown the restart interval if (--z->todo <= 0) { if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); // if it's NOT a restart, then just bail, so we get corrupt data // rather than no data if (!STBI__RESTART(z->marker)) return 1; stbi__jpeg_reset(z); } } } return 1; } else { // interleaved int i,j,k,x,y; STBI_SIMD_ALIGN(short, data[64]); for (j=0; j < z->img_mcu_y; ++j) { for (i=0; i < z->img_mcu_x; ++i) { // scan an interleaved mcu... process scan_n components in order for (k=0; k < z->scan_n; ++k) { int n = z->order[k]; // scan out an mcu's worth of this component; that's just determined // by the basic H and V specified for the component for (y=0; y < z->img_comp[n].v; ++y) { for (x=0; x < z->img_comp[n].h; ++x) { int x2 = (i*z->img_comp[n].h + x)*8; int y2 = (j*z->img_comp[n].v + y)*8; int ha = z->img_comp[n].ha; if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data); } } } // after all interleaved components, that's an interleaved MCU, // so now count down the restart interval if (--z->todo <= 0) { if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); if (!STBI__RESTART(z->marker)) return 1; stbi__jpeg_reset(z); } } } return 1; } } else { if (z->scan_n == 1) { int i,j; int n = z->order[0]; // non-interleaved data, we just need to process one block at a time, // in trivial scanline order // number of blocks to do just depends on how many actual "pixels" this // component has, independent of interleaved MCU blocking and such int w = (z->img_comp[n].x+7) >> 3; int h = (z->img_comp[n].y+7) >> 3; for (j=0; j < h; ++j) { for (i=0; i < w; ++i) { short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); if (z->spec_start == 0) { if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) return 0; } else { int ha = z->img_comp[n].ha; if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha])) return 0; } // every data block is an MCU, so countdown the restart interval if (--z->todo <= 0) { if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); if (!STBI__RESTART(z->marker)) return 1; stbi__jpeg_reset(z); } } } return 1; } else { // interleaved int i,j,k,x,y; for (j=0; j < z->img_mcu_y; ++j) { for (i=0; i < z->img_mcu_x; ++i) { // scan an interleaved mcu... process scan_n components in order for (k=0; k < z->scan_n; ++k) { int n = z->order[k]; // scan out an mcu's worth of this component; that's just determined // by the basic H and V specified for the component for (y=0; y < z->img_comp[n].v; ++y) { for (x=0; x < z->img_comp[n].h; ++x) { int x2 = (i*z->img_comp[n].h + x); int y2 = (j*z->img_comp[n].v + y); short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w); if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) return 0; } } } // after all interleaved components, that's an interleaved MCU, // so now count down the restart interval if (--z->todo <= 0) { if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); if (!STBI__RESTART(z->marker)) return 1; stbi__jpeg_reset(z); } } } return 1; } } } static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) { int i; for (i=0; i < 64; ++i) data[i] *= dequant[i]; } static void stbi__jpeg_finish(stbi__jpeg *z) { if (z->progressive) { // dequantize and idct the data int i,j,n; for (n=0; n < z->s->img_n; ++n) { int w = (z->img_comp[n].x+7) >> 3; int h = (z->img_comp[n].y+7) >> 3; for (j=0; j < h; ++j) { for (i=0; i < w; ++i) { short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); } } } } } static int stbi__process_marker(stbi__jpeg *z, int m) { int L; switch (m) { case STBI__MARKER_none: // no marker found return stbi__err("expected marker","Corrupt JPEG"); case 0xDD: // DRI - specify restart interval if (stbi__get16be(z->s) != 4) return stbi__err("bad DRI len","Corrupt JPEG"); z->restart_interval = stbi__get16be(z->s); return 1; case 0xDB: // DQT - define quantization table L = stbi__get16be(z->s)-2; while (L > 0) { int q = stbi__get8(z->s); int p = q >> 4, sixteen = (p != 0); int t = q & 15,i; if (p != 0 && p != 1) return stbi__err("bad DQT type","Corrupt JPEG"); if (t > 3) return stbi__err("bad DQT table","Corrupt JPEG"); for (i=0; i < 64; ++i) z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s)); L -= (sixteen ? 129 : 65); } return L==0; case 0xC4: // DHT - define huffman table L = stbi__get16be(z->s)-2; while (L > 0) { stbi_uc *v; int sizes[16],i,n=0; int q = stbi__get8(z->s); int tc = q >> 4; int th = q & 15; if (tc > 1 || th > 3) return stbi__err("bad DHT header","Corrupt JPEG"); for (i=0; i < 16; ++i) { sizes[i] = stbi__get8(z->s); n += sizes[i]; } if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! L -= 17; if (tc == 0) { if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; v = z->huff_dc[th].values; } else { if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0; v = z->huff_ac[th].values; } for (i=0; i < n; ++i) v[i] = stbi__get8(z->s); if (tc != 0) stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); L -= n; } return L==0; } // check for comment block or APP blocks if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { L = stbi__get16be(z->s); if (L < 2) { if (m == 0xFE) return stbi__err("bad COM len","Corrupt JPEG"); else return stbi__err("bad APP len","Corrupt JPEG"); } L -= 2; if (m == 0xE0 && L >= 5) { // JFIF APP0 segment static const unsigned char tag[5] = {'J','F','I','F','\0'}; int ok = 1; int i; for (i=0; i < 5; ++i) if (stbi__get8(z->s) != tag[i]) ok = 0; L -= 5; if (ok) z->jfif = 1; } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment static const unsigned char tag[6] = {'A','d','o','b','e','\0'}; int ok = 1; int i; for (i=0; i < 6; ++i) if (stbi__get8(z->s) != tag[i]) ok = 0; L -= 6; if (ok) { stbi__get8(z->s); // version stbi__get16be(z->s); // flags0 stbi__get16be(z->s); // flags1 z->app14_color_transform = stbi__get8(z->s); // color transform L -= 6; } } stbi__skip(z->s, L); return 1; } return stbi__err("unknown marker","Corrupt JPEG"); } // after we see SOS static int stbi__process_scan_header(stbi__jpeg *z) { int i; int Ls = stbi__get16be(z->s); z->scan_n = stbi__get8(z->s); if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err("bad SOS component count","Corrupt JPEG"); if (Ls != 6+2*z->scan_n) return stbi__err("bad SOS len","Corrupt JPEG"); for (i=0; i < z->scan_n; ++i) { int id = stbi__get8(z->s), which; int q = stbi__get8(z->s); for (which = 0; which < z->s->img_n; ++which) if (z->img_comp[which].id == id) break; if (which == z->s->img_n) return 0; // no match z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return stbi__err("bad DC huff","Corrupt JPEG"); z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return stbi__err("bad AC huff","Corrupt JPEG"); z->order[i] = which; } { int aa; z->spec_start = stbi__get8(z->s); z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 aa = stbi__get8(z->s); z->succ_high = (aa >> 4); z->succ_low = (aa & 15); if (z->progressive) { if (z->spec_start > 63 || z->spec_end > 63 || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13) return stbi__err("bad SOS", "Corrupt JPEG"); } else { if (z->spec_start != 0) return stbi__err("bad SOS","Corrupt JPEG"); if (z->succ_high != 0 || z->succ_low != 0) return stbi__err("bad SOS","Corrupt JPEG"); z->spec_end = 63; } } return 1; } static int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why) { int i; for (i=0; i < ncomp; ++i) { if (z->img_comp[i].raw_data) { STBI_FREE(z->img_comp[i].raw_data); z->img_comp[i].raw_data = NULL; z->img_comp[i].data = NULL; } if (z->img_comp[i].raw_coeff) { STBI_FREE(z->img_comp[i].raw_coeff); z->img_comp[i].raw_coeff = 0; z->img_comp[i].coeff = 0; } if (z->img_comp[i].linebuf) { STBI_FREE(z->img_comp[i].linebuf); z->img_comp[i].linebuf = NULL; } } return why; } static int stbi__process_frame_header(stbi__jpeg *z, int scan) { stbi__context *s = z->s; int Lf,p,i,q, h_max=1,v_max=1,c; Lf = stbi__get16be(s); if (Lf < 11) return stbi__err("bad SOF len","Corrupt JPEG"); // JPEG p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); c = stbi__get8(s); if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); s->img_n = c; for (i=0; i < c; ++i) { z->img_comp[i].data = NULL; z->img_comp[i].linebuf = NULL; } if (Lf != 8+3*s->img_n) return stbi__err("bad SOF len","Corrupt JPEG"); z->rgb = 0; for (i=0; i < s->img_n; ++i) { static const unsigned char rgb[3] = { 'R', 'G', 'B' }; z->img_comp[i].id = stbi__get8(s); if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) ++z->rgb; q = stbi__get8(s); z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err("bad H","Corrupt JPEG"); z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err("bad V","Corrupt JPEG"); z->img_comp[i].tq = stbi__get8(s); if (z->img_comp[i].tq > 3) return stbi__err("bad TQ","Corrupt JPEG"); } if (scan != STBI__SCAN_load) return 1; if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err("too large", "Image too large to decode"); for (i=0; i < s->img_n; ++i) { if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; } // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios // and I've never seen a non-corrupted JPEG file actually use them for (i=0; i < s->img_n; ++i) { if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); } // compute interleaved mcu info z->img_h_max = h_max; z->img_v_max = v_max; z->img_mcu_w = h_max * 8; z->img_mcu_h = v_max * 8; // these sizes can't be more than 17 bits z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; for (i=0; i < s->img_n; ++i) { // number of effective pixels (e.g. for non-interleaved MCU) z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; // to simplify generation, we'll allocate enough memory to decode // the bogus oversized data from using interleaved MCUs and their // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't // discard the extra data until colorspace conversion // // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked earlier) // so these muls can't overflow with 32-bit ints (which we require) z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; z->img_comp[i].coeff = 0; z->img_comp[i].raw_coeff = 0; z->img_comp[i].linebuf = NULL; z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); if (z->img_comp[i].raw_data == NULL) return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); // align blocks for idct using mmx/sse z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); if (z->progressive) { // w2, h2 are multiples of 8 (see above) z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); if (z->img_comp[i].raw_coeff == NULL) return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15); } } return 1; } // use comparisons since in some cases we handle more than one case (e.g. SOF) #define stbi__DNL(x) ((x) == 0xdc) #define stbi__SOI(x) ((x) == 0xd8) #define stbi__EOI(x) ((x) == 0xd9) #define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) #define stbi__SOS(x) ((x) == 0xda) #define stbi__SOF_progressive(x) ((x) == 0xc2) static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) { int m; z->jfif = 0; z->app14_color_transform = -1; // valid values are 0,1,2 z->marker = STBI__MARKER_none; // initialize cached marker to empty m = stbi__get_marker(z); if (!stbi__SOI(m)) return stbi__err("no SOI","Corrupt JPEG"); if (scan == STBI__SCAN_type) return 1; m = stbi__get_marker(z); while (!stbi__SOF(m)) { if (!stbi__process_marker(z,m)) return 0; m = stbi__get_marker(z); while (m == STBI__MARKER_none) { // some files have extra padding after their blocks, so ok, we'll scan if (stbi__at_eof(z->s)) return stbi__err("no SOF", "Corrupt JPEG"); m = stbi__get_marker(z); } } z->progressive = stbi__SOF_progressive(m); if (!stbi__process_frame_header(z, scan)) return 0; return 1; } static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) { // some JPEGs have junk at end, skip over it but if we find what looks // like a valid marker, resume there while (!stbi__at_eof(j->s)) { stbi_uc x = stbi__get8(j->s); while (x == 0xff) { // might be a marker if (stbi__at_eof(j->s)) return STBI__MARKER_none; x = stbi__get8(j->s); if (x != 0x00 && x != 0xff) { // not a stuffed zero or lead-in to another marker, looks // like an actual marker, return it return x; } // stuffed zero has x=0 now which ends the loop, meaning we go // back to regular scan loop. // repeated 0xff keeps trying to read the next byte of the marker. } } return STBI__MARKER_none; } // decode image to YCbCr format static int stbi__decode_jpeg_image(stbi__jpeg *j) { int m; for (m = 0; m < 4; m++) { j->img_comp[m].raw_data = NULL; j->img_comp[m].raw_coeff = NULL; } j->restart_interval = 0; if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0; m = stbi__get_marker(j); while (!stbi__EOI(m)) { if (stbi__SOS(m)) { if (!stbi__process_scan_header(j)) return 0; if (!stbi__parse_entropy_coded_data(j)) return 0; if (j->marker == STBI__MARKER_none ) { j->marker = stbi__skip_jpeg_junk_at_end(j); // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0 } m = stbi__get_marker(j); if (STBI__RESTART(m)) m = stbi__get_marker(j); } else if (stbi__DNL(m)) { int Ld = stbi__get16be(j->s); stbi__uint32 NL = stbi__get16be(j->s); if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); m = stbi__get_marker(j); } else { if (!stbi__process_marker(j, m)) return 1; m = stbi__get_marker(j); } } if (j->progressive) stbi__jpeg_finish(j); return 1; } // static jfif-centered resampling (across block boundaries) typedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1, int w, int hs); #define stbi__div4(x) ((stbi_uc) ((x) >> 2)) static stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { STBI_NOTUSED(out); STBI_NOTUSED(in_far); STBI_NOTUSED(w); STBI_NOTUSED(hs); return in_near; } static stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { // need to generate two samples vertically for every one in input int i; STBI_NOTUSED(hs); for (i=0; i < w; ++i) out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2); return out; } static stbi_uc* stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { // need to generate two samples horizontally for every one in input int i; stbi_uc *input = in_near; if (w == 1) { // if only one sample, can't do any interpolation out[0] = out[1] = input[0]; return out; } out[0] = input[0]; out[1] = stbi__div4(input[0]*3 + input[1] + 2); for (i=1; i < w-1; ++i) { int n = 3*input[i]+2; out[i*2+0] = stbi__div4(n+input[i-1]); out[i*2+1] = stbi__div4(n+input[i+1]); } out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2); out[i*2+1] = input[w-1]; STBI_NOTUSED(in_far); STBI_NOTUSED(hs); return out; } #define stbi__div16(x) ((stbi_uc) ((x) >> 4)) static stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { // need to generate 2x2 samples for every one in input int i,t0,t1; if (w == 1) { out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); return out; } t1 = 3*in_near[0] + in_far[0]; out[0] = stbi__div4(t1+2); for (i=1; i < w; ++i) { t0 = t1; t1 = 3*in_near[i]+in_far[i]; out[i*2-1] = stbi__div16(3*t0 + t1 + 8); out[i*2 ] = stbi__div16(3*t1 + t0 + 8); } out[w*2-1] = stbi__div4(t1+2); STBI_NOTUSED(hs); return out; } #if defined(STBI_SSE2) || defined(STBI_NEON) static stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { // need to generate 2x2 samples for every one in input int i=0,t0,t1; if (w == 1) { out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); return out; } t1 = 3*in_near[0] + in_far[0]; // process groups of 8 pixels for as long as we can. // note we can't handle the last pixel in a row in this loop // because we need to handle the filter boundary conditions. for (; i < ((w-1) & ~7); i += 8) { #if defined(STBI_SSE2) // load and perform the vertical filtering pass // this uses 3*x + y = 4*x + (y - x) __m128i zero = _mm_setzero_si128(); __m128i farb = _mm_loadl_epi64((__m128i *) (in_far + i)); __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i)); __m128i farw = _mm_unpacklo_epi8(farb, zero); __m128i nearw = _mm_unpacklo_epi8(nearb, zero); __m128i diff = _mm_sub_epi16(farw, nearw); __m128i nears = _mm_slli_epi16(nearw, 2); __m128i curr = _mm_add_epi16(nears, diff); // current row // horizontal filter works the same based on shifted vers of current // row. "prev" is current row shifted right by 1 pixel; we need to // insert the previous pixel value (from t1). // "next" is current row shifted left by 1 pixel, with first pixel // of next block of 8 pixels added in. __m128i prv0 = _mm_slli_si128(curr, 2); __m128i nxt0 = _mm_srli_si128(curr, 2); __m128i prev = _mm_insert_epi16(prv0, t1, 0); __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7); // horizontal filter, polyphase implementation since it's convenient: // even pixels = 3*cur + prev = cur*4 + (prev - cur) // odd pixels = 3*cur + next = cur*4 + (next - cur) // note the shared term. __m128i bias = _mm_set1_epi16(8); __m128i curs = _mm_slli_epi16(curr, 2); __m128i prvd = _mm_sub_epi16(prev, curr); __m128i nxtd = _mm_sub_epi16(next, curr); __m128i curb = _mm_add_epi16(curs, bias); __m128i even = _mm_add_epi16(prvd, curb); __m128i odd = _mm_add_epi16(nxtd, curb); // interleave even and odd pixels, then undo scaling. __m128i int0 = _mm_unpacklo_epi16(even, odd); __m128i int1 = _mm_unpackhi_epi16(even, odd); __m128i de0 = _mm_srli_epi16(int0, 4); __m128i de1 = _mm_srli_epi16(int1, 4); // pack and write output __m128i outv = _mm_packus_epi16(de0, de1); _mm_storeu_si128((__m128i *) (out + i*2), outv); #elif defined(STBI_NEON) // load and perform the vertical filtering pass // this uses 3*x + y = 4*x + (y - x) uint8x8_t farb = vld1_u8(in_far + i); uint8x8_t nearb = vld1_u8(in_near + i); int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); int16x8_t curr = vaddq_s16(nears, diff); // current row // horizontal filter works the same based on shifted vers of current // row. "prev" is current row shifted right by 1 pixel; we need to // insert the previous pixel value (from t1). // "next" is current row shifted left by 1 pixel, with first pixel // of next block of 8 pixels added in. int16x8_t prv0 = vextq_s16(curr, curr, 7); int16x8_t nxt0 = vextq_s16(curr, curr, 1); int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7); // horizontal filter, polyphase implementation since it's convenient: // even pixels = 3*cur + prev = cur*4 + (prev - cur) // odd pixels = 3*cur + next = cur*4 + (next - cur) // note the shared term. int16x8_t curs = vshlq_n_s16(curr, 2); int16x8_t prvd = vsubq_s16(prev, curr); int16x8_t nxtd = vsubq_s16(next, curr); int16x8_t even = vaddq_s16(curs, prvd); int16x8_t odd = vaddq_s16(curs, nxtd); // undo scaling and round, then store with even/odd phases interleaved uint8x8x2_t o; o.val[0] = vqrshrun_n_s16(even, 4); o.val[1] = vqrshrun_n_s16(odd, 4); vst2_u8(out + i*2, o); #endif // "previous" value for next iter t1 = 3*in_near[i+7] + in_far[i+7]; } t0 = t1; t1 = 3*in_near[i] + in_far[i]; out[i*2] = stbi__div16(3*t1 + t0 + 8); for (++i; i < w; ++i) { t0 = t1; t1 = 3*in_near[i]+in_far[i]; out[i*2-1] = stbi__div16(3*t0 + t1 + 8); out[i*2 ] = stbi__div16(3*t1 + t0 + 8); } out[w*2-1] = stbi__div4(t1+2); STBI_NOTUSED(hs); return out; } #endif static stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) { // resample with nearest-neighbor int i,j; STBI_NOTUSED(in_far); for (i=0; i < w; ++i) for (j=0; j < hs; ++j) out[i*hs+j] = in_near[i]; return out; } // this is a reduced-precision calculation of YCbCr-to-RGB introduced // to make sure the code produces the same results in both SIMD and scalar #define stbi__float2fixed(x) (((int) ((x) * 4096.0f + 0.5f)) << 8) static void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step) { int i; for (i=0; i < count; ++i) { int y_fixed = (y[i] << 20) + (1<<19); // rounding int r,g,b; int cr = pcr[i] - 128; int cb = pcb[i] - 128; r = y_fixed + cr* stbi__float2fixed(1.40200f); g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); b = y_fixed + cb* stbi__float2fixed(1.77200f); r >>= 20; g >>= 20; b >>= 20; if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } out[0] = (stbi_uc)r; out[1] = (stbi_uc)g; out[2] = (stbi_uc)b; out[3] = 255; out += step; } } #if defined(STBI_SSE2) || defined(STBI_NEON) static void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step) { int i = 0; #ifdef STBI_SSE2 // step == 3 is pretty ugly on the final interleave, and i'm not convinced // it's useful in practice (you wouldn't use it for textures, for example). // so just accelerate step == 4 case. if (step == 4) { // this is a fairly straightforward implementation and not super-optimized. __m128i signflip = _mm_set1_epi8(-0x80); __m128i cr_const0 = _mm_set1_epi16( (short) ( 1.40200f*4096.0f+0.5f)); __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f)); __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f)); __m128i cb_const1 = _mm_set1_epi16( (short) ( 1.77200f*4096.0f+0.5f)); __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128); __m128i xw = _mm_set1_epi16(255); // alpha channel for (; i+7 < count; i += 8) { // load __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i)); __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i)); __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i)); __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 // unpack to short (and left-shift cr, cb by 8) __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); // color transform __m128i yws = _mm_srli_epi16(yw, 4); __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); __m128i rws = _mm_add_epi16(cr0, yws); __m128i gwt = _mm_add_epi16(cb0, yws); __m128i bws = _mm_add_epi16(yws, cb1); __m128i gws = _mm_add_epi16(gwt, cr1); // descale __m128i rw = _mm_srai_epi16(rws, 4); __m128i bw = _mm_srai_epi16(bws, 4); __m128i gw = _mm_srai_epi16(gws, 4); // back to byte, set up for transpose __m128i brb = _mm_packus_epi16(rw, bw); __m128i gxb = _mm_packus_epi16(gw, xw); // transpose to interleave channels __m128i t0 = _mm_unpacklo_epi8(brb, gxb); __m128i t1 = _mm_unpackhi_epi8(brb, gxb); __m128i o0 = _mm_unpacklo_epi16(t0, t1); __m128i o1 = _mm_unpackhi_epi16(t0, t1); // store _mm_storeu_si128((__m128i *) (out + 0), o0); _mm_storeu_si128((__m128i *) (out + 16), o1); out += 32; } } #endif #ifdef STBI_NEON // in this version, step=3 support would be easy to add. but is there demand? if (step == 4) { // this is a fairly straightforward implementation and not super-optimized. uint8x8_t signflip = vdup_n_u8(0x80); int16x8_t cr_const0 = vdupq_n_s16( (short) ( 1.40200f*4096.0f+0.5f)); int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f)); int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f)); int16x8_t cb_const1 = vdupq_n_s16( (short) ( 1.77200f*4096.0f+0.5f)); for (; i+7 < count; i += 8) { // load uint8x8_t y_bytes = vld1_u8(y + i); uint8x8_t cr_bytes = vld1_u8(pcr + i); uint8x8_t cb_bytes = vld1_u8(pcb + i); int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); // expand to s16 int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); int16x8_t crw = vshll_n_s8(cr_biased, 7); int16x8_t cbw = vshll_n_s8(cb_biased, 7); // color transform int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); int16x8_t rws = vaddq_s16(yws, cr0); int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); int16x8_t bws = vaddq_s16(yws, cb1); // undo scaling, round, convert to byte uint8x8x4_t o; o.val[0] = vqrshrun_n_s16(rws, 4); o.val[1] = vqrshrun_n_s16(gws, 4); o.val[2] = vqrshrun_n_s16(bws, 4); o.val[3] = vdup_n_u8(255); // store, interleaving r/g/b/a vst4_u8(out, o); out += 8*4; } } #endif for (; i < count; ++i) { int y_fixed = (y[i] << 20) + (1<<19); // rounding int r,g,b; int cr = pcr[i] - 128; int cb = pcb[i] - 128; r = y_fixed + cr* stbi__float2fixed(1.40200f); g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); b = y_fixed + cb* stbi__float2fixed(1.77200f); r >>= 20; g >>= 20; b >>= 20; if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } out[0] = (stbi_uc)r; out[1] = (stbi_uc)g; out[2] = (stbi_uc)b; out[3] = 255; out += step; } } #endif // set up the kernels static void stbi__setup_jpeg(stbi__jpeg *j) { j->idct_block_kernel = stbi__idct_block; j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; #ifdef STBI_SSE2 if (stbi__sse2_available()) { j->idct_block_kernel = stbi__idct_simd; j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; } #endif #ifdef STBI_NEON j->idct_block_kernel = stbi__idct_simd; j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; #endif } // clean up the temporary component buffers static void stbi__cleanup_jpeg(stbi__jpeg *j) { stbi__free_jpeg_components(j, j->s->img_n, 0); } typedef struct { resample_row_func resample; stbi_uc *line0,*line1; int hs,vs; // expansion factor in each axis int w_lores; // horizontal pixels pre-expansion int ystep; // how far through vertical expansion we are int ypos; // which pre-expansion row we're on } stbi__resample; // fast 0..255 * 0..255 => 0..255 rounded multiplication static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) { unsigned int t = x*y + 128; return (stbi_uc) ((t + (t >>8)) >> 8); } static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) { int n, decode_n, is_rgb; z->s->img_n = 0; // make stbi__cleanup_jpeg safe // validate req_comp if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); // load a jpeg image from whichever source, but leave in YCbCr format if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; } // determine actual number of components to generate n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); if (z->s->img_n == 3 && n < 3 && !is_rgb) decode_n = 1; else decode_n = z->s->img_n; // nothing to do if no components requested; check this now to avoid // accessing uninitialized coutput[0] later if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } // resample and color-convert { int k; unsigned int i,j; stbi_uc *output; stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; stbi__resample res_comp[4]; for (k=0; k < decode_n; ++k) { stbi__resample *r = &res_comp[k]; // allocate line buffer big enough for upsampling off the edges // with upsample factor of 4 z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3); if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } r->hs = z->img_h_max / z->img_comp[k].h; r->vs = z->img_v_max / z->img_comp[k].v; r->ystep = r->vs >> 1; r->w_lores = (z->s->img_x + r->hs-1) / r->hs; r->ypos = 0; r->line0 = r->line1 = z->img_comp[k].data; if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2; else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2; else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel; else r->resample = stbi__resample_row_generic; } // can't error after this so, this is safe output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } // now go ahead and resample for (j=0; j < z->s->img_y; ++j) { stbi_uc *out = output + n * z->s->img_x * j; for (k=0; k < decode_n; ++k) { stbi__resample *r = &res_comp[k]; int y_bot = r->ystep >= (r->vs >> 1); coutput[k] = r->resample(z->img_comp[k].linebuf, y_bot ? r->line1 : r->line0, y_bot ? r->line0 : r->line1, r->w_lores, r->hs); if (++r->ystep >= r->vs) { r->ystep = 0; r->line0 = r->line1; if (++r->ypos < z->img_comp[k].y) r->line1 += z->img_comp[k].w2; } } if (n >= 3) { stbi_uc *y = coutput[0]; if (z->s->img_n == 3) { if (is_rgb) { for (i=0; i < z->s->img_x; ++i) { out[0] = y[i]; out[1] = coutput[1][i]; out[2] = coutput[2][i]; out[3] = 255; out += n; } } else { z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); } } else if (z->s->img_n == 4) { if (z->app14_color_transform == 0) { // CMYK for (i=0; i < z->s->img_x; ++i) { stbi_uc m = coutput[3][i]; out[0] = stbi__blinn_8x8(coutput[0][i], m); out[1] = stbi__blinn_8x8(coutput[1][i], m); out[2] = stbi__blinn_8x8(coutput[2][i], m); out[3] = 255; out += n; } } else if (z->app14_color_transform == 2) { // YCCK z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); for (i=0; i < z->s->img_x; ++i) { stbi_uc m = coutput[3][i]; out[0] = stbi__blinn_8x8(255 - out[0], m); out[1] = stbi__blinn_8x8(255 - out[1], m); out[2] = stbi__blinn_8x8(255 - out[2], m); out += n; } } else { // YCbCr + alpha? Ignore the fourth channel for now z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); } } else for (i=0; i < z->s->img_x; ++i) { out[0] = out[1] = out[2] = y[i]; out[3] = 255; // not used if n==3 out += n; } } else { if (is_rgb) { if (n == 1) for (i=0; i < z->s->img_x; ++i) *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); else { for (i=0; i < z->s->img_x; ++i, out += 2) { out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); out[1] = 255; } } } else if (z->s->img_n == 4 && z->app14_color_transform == 0) { for (i=0; i < z->s->img_x; ++i) { stbi_uc m = coutput[3][i]; stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); out[0] = stbi__compute_y(r, g, b); out[1] = 255; out += n; } } else if (z->s->img_n == 4 && z->app14_color_transform == 2) { for (i=0; i < z->s->img_x; ++i) { out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); out[1] = 255; out += n; } } else { stbi_uc *y = coutput[0]; if (n == 1) for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; else for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } } } } stbi__cleanup_jpeg(z); *out_x = z->s->img_x; *out_y = z->s->img_y; if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output return output; } } static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { unsigned char* result; stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); if (!j) return stbi__errpuc("outofmem", "Out of memory"); memset(j, 0, sizeof(stbi__jpeg)); STBI_NOTUSED(ri); j->s = s; stbi__setup_jpeg(j); result = load_jpeg_image(j, x,y,comp,req_comp); STBI_FREE(j); return result; } static int stbi__jpeg_test(stbi__context *s) { int r; stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); if (!j) return stbi__err("outofmem", "Out of memory"); memset(j, 0, sizeof(stbi__jpeg)); j->s = s; stbi__setup_jpeg(j); r = stbi__decode_jpeg_header(j, STBI__SCAN_type); stbi__rewind(s); STBI_FREE(j); return r; } static int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp) { if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { stbi__rewind( j->s ); return 0; } if (x) *x = j->s->img_x; if (y) *y = j->s->img_y; if (comp) *comp = j->s->img_n >= 3 ? 3 : 1; return 1; } static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) { int result; stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); if (!j) return stbi__err("outofmem", "Out of memory"); memset(j, 0, sizeof(stbi__jpeg)); j->s = s; result = stbi__jpeg_info_raw(j, x, y, comp); STBI_FREE(j); return result; } #endif // public domain zlib decode v0.2 Sean Barrett 2006-11-18 // simple implementation // - all input must be provided in an upfront buffer // - all output is written to a single output buffer (can malloc/realloc) // performance // - fast huffman #ifndef STBI_NO_ZLIB // fast-way is faster to check than jpeg huffman, but slow way is slower #define STBI__ZFAST_BITS 9 // accelerate all cases in default tables #define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) #define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet // zlib-style huffman encoding // (jpegs packs from left, zlib from right, so can't share code) typedef struct { stbi__uint16 fast[1 << STBI__ZFAST_BITS]; stbi__uint16 firstcode[16]; int maxcode[17]; stbi__uint16 firstsymbol[16]; stbi_uc size[STBI__ZNSYMS]; stbi__uint16 value[STBI__ZNSYMS]; } stbi__zhuffman; stbi_inline static int stbi__bitreverse16(int n) { n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); return n; } stbi_inline static int stbi__bit_reverse(int v, int bits) { STBI_ASSERT(bits <= 16); // to bit reverse n bits, reverse 16 and shift // e.g. 11 bits, bit reverse and shift away 5 return stbi__bitreverse16(v) >> (16-bits); } static int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num) { int i,k=0; int code, next_code[16], sizes[17]; // DEFLATE spec for generating codes memset(sizes, 0, sizeof(sizes)); memset(z->fast, 0, sizeof(z->fast)); for (i=0; i < num; ++i) ++sizes[sizelist[i]]; sizes[0] = 0; for (i=1; i < 16; ++i) if (sizes[i] > (1 << i)) return stbi__err("bad sizes", "Corrupt PNG"); code = 0; for (i=1; i < 16; ++i) { next_code[i] = code; z->firstcode[i] = (stbi__uint16) code; z->firstsymbol[i] = (stbi__uint16) k; code = (code + sizes[i]); if (sizes[i]) if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); z->maxcode[i] = code << (16-i); // preshift for inner loop code <<= 1; k += sizes[i]; } z->maxcode[16] = 0x10000; // sentinel for (i=0; i < num; ++i) { int s = sizelist[i]; if (s) { int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i); z->size [c] = (stbi_uc ) s; z->value[c] = (stbi__uint16) i; if (s <= STBI__ZFAST_BITS) { int j = stbi__bit_reverse(next_code[s],s); while (j < (1 << STBI__ZFAST_BITS)) { z->fast[j] = fastv; j += (1 << s); } } ++next_code[s]; } } return 1; } // zlib-from-memory implementation for PNG reading // because PNG allows splitting the zlib stream arbitrarily, // and it's annoying structurally to have PNG call ZLIB call PNG, // we require PNG read all the IDATs and combine them into a single // memory buffer typedef struct { stbi_uc *zbuffer, *zbuffer_end; int num_bits; int hit_zeof_once; stbi__uint32 code_buffer; char *zout; char *zout_start; char *zort_end; int z_expandable; stbi__zhuffman z_length, z_distance; } stbi__zbuf; stbi_inline static int stbi__zeof(stbi__zbuf *z) { return (z->zbuffer >= z->zbuffer_end); } stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) { return stbi__zeof(z) ? 0 : *z->zbuffer++; } static void stbi__fill_bits(stbi__zbuf *z) { do { if (z->code_buffer >= (1U << z->num_bits)) { z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ return; } z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; z->num_bits += 8; } while (z->num_bits <= 24); } stbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) { unsigned int k; if (z->num_bits < n) stbi__fill_bits(z); k = z->code_buffer & ((1 << n) - 1); z->code_buffer >>= n; z->num_bits -= n; return k; } static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) { int b,s,k; // not resolved by fast table, so compute it the slow way // use jpeg approach, which requires MSbits at top k = stbi__bit_reverse(a->code_buffer, 16); for (s=STBI__ZFAST_BITS+1; ; ++s) if (k < z->maxcode[s]) break; if (s >= 16) return -1; // invalid code! // code size is s, so: b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. a->code_buffer >>= s; a->num_bits -= s; return z->value[b]; } stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) { int b,s; if (a->num_bits < 16) { if (stbi__zeof(a)) { if (!a->hit_zeof_once) { // This is the first time we hit eof, insert 16 extra padding btis // to allow us to keep going; if we actually consume any of them // though, that is invalid data. This is caught later. a->hit_zeof_once = 1; a->num_bits += 16; // add 16 implicit zero bits } else { // We already inserted our extra 16 padding bits and are again // out, this stream is actually prematurely terminated. return -1; } } else { stbi__fill_bits(a); } } b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; if (b) { s = b >> 9; a->code_buffer >>= s; a->num_bits -= s; return b & 511; } return stbi__zhuffman_decode_slowpath(a, z); } static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes { char *q; unsigned int cur, limit, old_limit; z->zout = zout; if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); cur = (unsigned int) (z->zout - z->zout_start); limit = old_limit = (unsigned) (z->zort_end - z->zout_start); if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); while (cur + n > limit) { if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); limit *= 2; } q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); STBI_NOTUSED(old_limit); if (q == NULL) return stbi__err("outofmem", "Out of memory"); z->zout_start = q; z->zout = q + cur; z->zort_end = q + limit; return 1; } static const int stbi__zlength_base[31] = { 3,4,5,6,7,8,9,10,11,13, 15,17,19,23,27,31,35,43,51,59, 67,83,99,115,131,163,195,227,258,0,0 }; static const int stbi__zlength_extra[31]= { 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; static const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; static const int stbi__zdist_extra[32] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; static int stbi__parse_huffman_block(stbi__zbuf *a) { char *zout = a->zout; for(;;) { int z = stbi__zhuffman_decode(a, &a->z_length); if (z < 256) { if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); // error in huffman codes if (zout >= a->zort_end) { if (!stbi__zexpand(a, zout, 1)) return 0; zout = a->zout; } *zout++ = (char) z; } else { stbi_uc *p; int len,dist; if (z == 256) { a->zout = zout; if (a->hit_zeof_once && a->num_bits < 16) { // The first time we hit zeof, we inserted 16 extra zero bits into our bit // buffer so the decoder can just do its speculative decoding. But if we // actually consumed any of those bits (which is the case when num_bits < 16), // the stream actually read past the end so it is malformed. return stbi__err("unexpected end","Corrupt PNG"); } return 1; } if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data z -= 257; len = stbi__zlength_base[z]; if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); z = stbi__zhuffman_decode(a, &a->z_distance); if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data dist = stbi__zdist_base[z]; if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); if (len > a->zort_end - zout) { if (!stbi__zexpand(a, zout, len)) return 0; zout = a->zout; } p = (stbi_uc *) (zout - dist); if (dist == 1) { // run of one byte; common in images. stbi_uc v = *p; if (len) { do *zout++ = v; while (--len); } } else { if (len) { do *zout++ = *p++; while (--len); } } } } } static int stbi__compute_huffman_codes(stbi__zbuf *a) { static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; stbi__zhuffman z_codelength; stbi_uc lencodes[286+32+137];//padding for maximum single op stbi_uc codelength_sizes[19]; int i,n; int hlit = stbi__zreceive(a,5) + 257; int hdist = stbi__zreceive(a,5) + 1; int hclen = stbi__zreceive(a,4) + 4; int ntot = hlit + hdist; memset(codelength_sizes, 0, sizeof(codelength_sizes)); for (i=0; i < hclen; ++i) { int s = stbi__zreceive(a,3); codelength_sizes[length_dezigzag[i]] = (stbi_uc) s; } if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; n = 0; while (n < ntot) { int c = stbi__zhuffman_decode(a, &z_codelength); if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG"); if (c < 16) lencodes[n++] = (stbi_uc) c; else { stbi_uc fill = 0; if (c == 16) { c = stbi__zreceive(a,2)+3; if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); fill = lencodes[n-1]; } else if (c == 17) { c = stbi__zreceive(a,3)+3; } else if (c == 18) { c = stbi__zreceive(a,7)+11; } else { return stbi__err("bad codelengths", "Corrupt PNG"); } if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); memset(lencodes+n, fill, c); n += c; } } if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG"); if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; return 1; } static int stbi__parse_uncompressed_block(stbi__zbuf *a) { stbi_uc header[4]; int len,nlen,k; if (a->num_bits & 7) stbi__zreceive(a, a->num_bits & 7); // discard // drain the bit-packed data into header k = 0; while (a->num_bits > 0) { header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check a->code_buffer >>= 8; a->num_bits -= 8; } if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); // now fill header the normal way while (k < 4) header[k++] = stbi__zget8(a); len = header[1] * 256 + header[0]; nlen = header[3] * 256 + header[2]; if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG"); if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG"); if (a->zout + len > a->zort_end) if (!stbi__zexpand(a, a->zout, len)) return 0; memcpy(a->zout, a->zbuffer, len); a->zbuffer += len; a->zout += len; return 1; } static int stbi__parse_zlib_header(stbi__zbuf *a) { int cmf = stbi__zget8(a); int cm = cmf & 15; /* int cinfo = cmf >> 4; */ int flg = stbi__zget8(a); if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png // window = 1 << (8 + cinfo)... but who cares, we fully buffer output return 1; } static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = { 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 }; static const stbi_uc stbi__zdefault_distance[32] = { 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 }; /* Init algorithm: { int i; // use <= to match clearly with spec for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; } */ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) { int final, type; if (parse_header) if (!stbi__parse_zlib_header(a)) return 0; a->num_bits = 0; a->code_buffer = 0; a->hit_zeof_once = 0; do { final = stbi__zreceive(a,1); type = stbi__zreceive(a,2); if (type == 0) { if (!stbi__parse_uncompressed_block(a)) return 0; } else if (type == 3) { return 0; } else { if (type == 1) { // use fixed code lengths if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; } else { if (!stbi__compute_huffman_codes(a)) return 0; } if (!stbi__parse_huffman_block(a)) return 0; } } while (!final); return 1; } static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header) { a->zout_start = obuf; a->zout = obuf; a->zort_end = obuf + olen; a->z_expandable = exp; return stbi__parse_zlib(a, parse_header); } STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) { stbi__zbuf a; char *p = (char *) stbi__malloc(initial_size); if (p == NULL) return NULL; a.zbuffer = (stbi_uc *) buffer; a.zbuffer_end = (stbi_uc *) buffer + len; if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { if (outlen) *outlen = (int) (a.zout - a.zout_start); return a.zout_start; } else { STBI_FREE(a.zout_start); return NULL; } } STBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) { return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); } STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) { stbi__zbuf a; char *p = (char *) stbi__malloc(initial_size); if (p == NULL) return NULL; a.zbuffer = (stbi_uc *) buffer; a.zbuffer_end = (stbi_uc *) buffer + len; if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { if (outlen) *outlen = (int) (a.zout - a.zout_start); return a.zout_start; } else { STBI_FREE(a.zout_start); return NULL; } } STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) { stbi__zbuf a; a.zbuffer = (stbi_uc *) ibuffer; a.zbuffer_end = (stbi_uc *) ibuffer + ilen; if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) return (int) (a.zout - a.zout_start); else return -1; } STBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) { stbi__zbuf a; char *p = (char *) stbi__malloc(16384); if (p == NULL) return NULL; a.zbuffer = (stbi_uc *) buffer; a.zbuffer_end = (stbi_uc *) buffer+len; if (stbi__do_zlib(&a, p, 16384, 1, 0)) { if (outlen) *outlen = (int) (a.zout - a.zout_start); return a.zout_start; } else { STBI_FREE(a.zout_start); return NULL; } } STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) { stbi__zbuf a; a.zbuffer = (stbi_uc *) ibuffer; a.zbuffer_end = (stbi_uc *) ibuffer + ilen; if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) return (int) (a.zout - a.zout_start); else return -1; } #endif // public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 // simple implementation // - only 8-bit samples // - no CRC checking // - allocates lots of intermediate memory // - avoids problem of streaming data between subsystems // - avoids explicit window management // performance // - uses stb_zlib, a PD zlib implementation with fast huffman decoding #ifndef STBI_NO_PNG typedef struct { stbi__uint32 length; stbi__uint32 type; } stbi__pngchunk; static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) { stbi__pngchunk c; c.length = stbi__get32be(s); c.type = stbi__get32be(s); return c; } static int stbi__check_png_header(stbi__context *s) { static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; int i; for (i=0; i < 8; ++i) if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG"); return 1; } typedef struct { stbi__context *s; stbi_uc *idata, *expanded, *out; int depth; } stbi__png; enum { STBI__F_none=0, STBI__F_sub=1, STBI__F_up=2, STBI__F_avg=3, STBI__F_paeth=4, // synthetic filter used for first scanline to avoid needing a dummy row of 0s STBI__F_avg_first }; static stbi_uc first_row_filter[5] = { STBI__F_none, STBI__F_sub, STBI__F_none, STBI__F_avg_first, STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub }; static int stbi__paeth(int a, int b, int c) { // This formulation looks very different from the reference in the PNG spec, but is // actually equivalent and has favorable data dependencies and admits straightforward // generation of branch-free code, which helps performance significantly. int thresh = c*3 - (a + b); int lo = a < b ? a : b; int hi = a < b ? b : a; int t0 = (hi <= thresh) ? lo : c; int t1 = (thresh <= lo) ? hi : t0; return t1; } static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; // adds an extra all-255 alpha channel // dest == src is legal // img_n must be 1 or 3 static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) { int i; // must process data backwards since we allow dest==src if (img_n == 1) { for (i=x-1; i >= 0; --i) { dest[i*2+1] = 255; dest[i*2+0] = src[i]; } } else { STBI_ASSERT(img_n == 3); for (i=x-1; i >= 0; --i) { dest[i*4+3] = 255; dest[i*4+2] = src[i*3+2]; dest[i*4+1] = src[i*3+1]; dest[i*4+0] = src[i*3+0]; } } } // create the png data from post-deflated data static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) { int bytes = (depth == 16 ? 2 : 1); stbi__context *s = a->s; stbi__uint32 i,j,stride = x*out_n*bytes; stbi__uint32 img_len, img_width_bytes; stbi_uc *filter_buf; int all_ok = 1; int k; int img_n = s->img_n; // copy it into a local for later int output_bytes = out_n*bytes; int filter_bytes = img_n*bytes; int width = x; STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1); a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into if (!a->out) return stbi__err("outofmem", "Out of memory"); // note: error exits here don't need to clean up a->out individually, // stbi__do_png always does on error. if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); img_width_bytes = (((img_n * x * depth) + 7) >> 3); if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); img_len = (img_width_bytes + 1) * y; // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, // but issue #276 reported a PNG in the wild that had extra data at the end (all zeros), // so just check for raw_len < img_len always. if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); // Allocate two scan lines worth of filter workspace buffer. filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); if (!filter_buf) return stbi__err("outofmem", "Out of memory"); // Filtering for low-bit-depth images if (depth < 8) { filter_bytes = 1; width = img_width_bytes; } for (j=0; j < y; ++j) { // cur/prior filter buffers alternate stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; stbi_uc *dest = a->out + stride*j; int nk = width * filter_bytes; int filter = *raw++; // check filter type if (filter > 4) { all_ok = stbi__err("invalid filter","Corrupt PNG"); break; } // if first row, use special filter that doesn't sample previous row if (j == 0) filter = first_row_filter[filter]; // perform actual filtering switch (filter) { case STBI__F_none: memcpy(cur, raw, nk); break; case STBI__F_sub: memcpy(cur, raw, filter_bytes); for (k = filter_bytes; k < nk; ++k) cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); break; case STBI__F_up: for (k = 0; k < nk; ++k) cur[k] = STBI__BYTECAST(raw[k] + prior[k]); break; case STBI__F_avg: for (k = 0; k < filter_bytes; ++k) cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); for (k = filter_bytes; k < nk; ++k) cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); break; case STBI__F_paeth: for (k = 0; k < filter_bytes; ++k) cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) for (k = filter_bytes; k < nk; ++k) cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); break; case STBI__F_avg_first: memcpy(cur, raw, filter_bytes); for (k = filter_bytes; k < nk; ++k) cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); break; } raw += nk; // expand decoded bits in cur to dest, also adding an extra alpha channel if desired if (depth < 8) { stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale gs values to 0..255 range stbi_uc *in = cur; stbi_uc *out = dest; stbi_uc inb = 0; stbi__uint32 nsmp = x*img_n; // expand bits to bytes first if (depth == 4) { for (i=0; i < nsmp; ++i) { if ((i & 1) == 0) inb = *in++; *out++ = scale * (inb >> 4); inb <<= 4; } } else if (depth == 2) { for (i=0; i < nsmp; ++i) { if ((i & 3) == 0) inb = *in++; *out++ = scale * (inb >> 6); inb <<= 2; } } else { STBI_ASSERT(depth == 1); for (i=0; i < nsmp; ++i) { if ((i & 7) == 0) inb = *in++; *out++ = scale * (inb >> 7); inb <<= 1; } } // insert alpha=255 values if desired if (img_n != out_n) stbi__create_png_alpha_expand8(dest, dest, x, img_n); } else if (depth == 8) { if (img_n == out_n) memcpy(dest, cur, x*img_n); else stbi__create_png_alpha_expand8(dest, cur, x, img_n); } else if (depth == 16) { // convert the image data from big-endian to platform-native stbi__uint16 *dest16 = (stbi__uint16*)dest; stbi__uint32 nsmp = x*img_n; if (img_n == out_n) { for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) *dest16 = (cur[0] << 8) | cur[1]; } else { STBI_ASSERT(img_n+1 == out_n); if (img_n == 1) { for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { dest16[0] = (cur[0] << 8) | cur[1]; dest16[1] = 0xffff; } } else { STBI_ASSERT(img_n == 3); for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { dest16[0] = (cur[0] << 8) | cur[1]; dest16[1] = (cur[2] << 8) | cur[3]; dest16[2] = (cur[4] << 8) | cur[5]; dest16[3] = 0xffff; } } } } } STBI_FREE(filter_buf); if (!all_ok) return 0; return 1; } static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced) { int bytes = (depth == 16 ? 2 : 1); int out_bytes = out_n * bytes; stbi_uc *final; int p; if (!interlaced) return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color); // de-interlacing final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); if (!final) return stbi__err("outofmem", "Out of memory"); for (p=0; p < 7; ++p) { int xorig[] = { 0,4,0,2,0,1,0 }; int yorig[] = { 0,0,4,0,2,0,1 }; int xspc[] = { 8,8,4,4,2,2,1 }; int yspc[] = { 8,8,8,4,4,2,2 }; int i,j,x,y; // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; if (x && y) { stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { STBI_FREE(final); return 0; } for (j=0; j < y; ++j) { for (i=0; i < x; ++i) { int out_y = j*yspc[p]+yorig[p]; int out_x = i*xspc[p]+xorig[p]; memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes, a->out + (j*x+i)*out_bytes, out_bytes); } } STBI_FREE(a->out); image_data += img_len; image_data_len -= img_len; } } a->out = final; return 1; } static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) { stbi__context *s = z->s; stbi__uint32 i, pixel_count = s->img_x * s->img_y; stbi_uc *p = z->out; // compute color-based transparency, assuming we've // already got 255 as the alpha value in the output STBI_ASSERT(out_n == 2 || out_n == 4); if (out_n == 2) { for (i=0; i < pixel_count; ++i) { p[1] = (p[0] == tc[0] ? 0 : 255); p += 2; } } else { for (i=0; i < pixel_count; ++i) { if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) p[3] = 0; p += 4; } } return 1; } static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) { stbi__context *s = z->s; stbi__uint32 i, pixel_count = s->img_x * s->img_y; stbi__uint16 *p = (stbi__uint16*) z->out; // compute color-based transparency, assuming we've // already got 65535 as the alpha value in the output STBI_ASSERT(out_n == 2 || out_n == 4); if (out_n == 2) { for (i = 0; i < pixel_count; ++i) { p[1] = (p[0] == tc[0] ? 0 : 65535); p += 2; } } else { for (i = 0; i < pixel_count; ++i) { if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) p[3] = 0; p += 4; } } return 1; } static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) { stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; stbi_uc *p, *temp_out, *orig = a->out; p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0); if (p == NULL) return stbi__err("outofmem", "Out of memory"); // between here and free(out) below, exitting would leak temp_out = p; if (pal_img_n == 3) { for (i=0; i < pixel_count; ++i) { int n = orig[i]*4; p[0] = palette[n ]; p[1] = palette[n+1]; p[2] = palette[n+2]; p += 3; } } else { for (i=0; i < pixel_count; ++i) { int n = orig[i]*4; p[0] = palette[n ]; p[1] = palette[n+1]; p[2] = palette[n+2]; p[3] = palette[n+3]; p += 4; } } STBI_FREE(a->out); a->out = temp_out; STBI_NOTUSED(len); return 1; } static int stbi__unpremultiply_on_load_global = 0; static int stbi__de_iphone_flag_global = 0; STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) { stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; } STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) { stbi__de_iphone_flag_global = flag_true_if_should_convert; } #ifndef STBI_THREAD_LOCAL #define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global #define stbi__de_iphone_flag stbi__de_iphone_flag_global #else static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) { stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; stbi__unpremultiply_on_load_set = 1; } STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) { stbi__de_iphone_flag_local = flag_true_if_should_convert; stbi__de_iphone_flag_set = 1; } #define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ ? stbi__unpremultiply_on_load_local \ : stbi__unpremultiply_on_load_global) #define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ ? stbi__de_iphone_flag_local \ : stbi__de_iphone_flag_global) #endif // STBI_THREAD_LOCAL static void stbi__de_iphone(stbi__png *z) { stbi__context *s = z->s; stbi__uint32 i, pixel_count = s->img_x * s->img_y; stbi_uc *p = z->out; if (s->img_out_n == 3) { // convert bgr to rgb for (i=0; i < pixel_count; ++i) { stbi_uc t = p[0]; p[0] = p[2]; p[2] = t; p += 3; } } else { STBI_ASSERT(s->img_out_n == 4); if (stbi__unpremultiply_on_load) { // convert bgr to rgb and unpremultiply for (i=0; i < pixel_count; ++i) { stbi_uc a = p[3]; stbi_uc t = p[0]; if (a) { stbi_uc half = a / 2; p[0] = (p[2] * 255 + half) / a; p[1] = (p[1] * 255 + half) / a; p[2] = ( t * 255 + half) / a; } else { p[0] = p[2]; p[2] = t; } p += 4; } } else { // convert bgr to rgb for (i=0; i < pixel_count; ++i) { stbi_uc t = p[0]; p[0] = p[2]; p[2] = t; p += 4; } } } } #define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) { stbi_uc palette[1024], pal_img_n=0; stbi_uc has_trans=0, tc[3]={0}; stbi__uint16 tc16[3]; stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; int first=1,k,interlace=0, color=0, is_iphone=0; stbi__context *s = z->s; z->expanded = NULL; z->idata = NULL; z->out = NULL; if (!stbi__check_png_header(s)) return 0; if (scan == STBI__SCAN_type) return 1; for (;;) { stbi__pngchunk c = stbi__get_chunk_header(s); switch (c.type) { case STBI__PNG_TYPE('C','g','B','I'): is_iphone = 1; stbi__skip(s, c.length); break; case STBI__PNG_TYPE('I','H','D','R'): { int comp,filter; if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); first = 0; if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); s->img_x = stbi__get32be(s); s->img_y = stbi__get32be(s); if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG"); comp = stbi__get8(s); if (comp) return stbi__err("bad comp method","Corrupt PNG"); filter= stbi__get8(s); if (filter) return stbi__err("bad filter method","Corrupt PNG"); interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG"); if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG"); if (!pal_img_n) { s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); } else { // if paletted, then pal_n is our final components, and // img_n is # components to decompress/filter. s->img_n = 1; if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); } // even with SCAN_header, have to scan to see if we have a tRNS break; } case STBI__PNG_TYPE('P','L','T','E'): { if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG"); pal_len = c.length / 3; if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); for (i=0; i < pal_len; ++i) { palette[i*4+0] = stbi__get8(s); palette[i*4+1] = stbi__get8(s); palette[i*4+2] = stbi__get8(s); palette[i*4+3] = 255; } break; } case STBI__PNG_TYPE('t','R','N','S'): { if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG"); if (pal_img_n) { if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; } if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG"); if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG"); pal_img_n = 4; for (i=0; i < c.length; ++i) palette[i*4+3] = stbi__get8(s); } else { if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); has_trans = 1; // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } if (z->depth == 16) { for (k = 0; k < s->img_n; ++k) tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is } else { for (k = 0; k < s->img_n; ++k) tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger } } break; } case STBI__PNG_TYPE('I','D','A','T'): { if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); if (scan == STBI__SCAN_header) { // header scan definitely stops at first IDAT if (pal_img_n) s->img_n = pal_img_n; return 1; } if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); if ((int)(ioff + c.length) < (int)ioff) return 0; if (ioff + c.length > idata_limit) { stbi__uint32 idata_limit_old = idata_limit; stbi_uc *p; if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; while (ioff + c.length > idata_limit) idata_limit *= 2; STBI_NOTUSED(idata_limit_old); p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory"); z->idata = p; } if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); ioff += c.length; break; } case STBI__PNG_TYPE('I','E','N','D'): { stbi__uint32 raw_len, bpl; if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if (scan != STBI__SCAN_load) return 1; if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); // initial guess for decoded data size to avoid unnecessary reallocs bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */; z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone); if (z->expanded == NULL) return 0; // zlib should set error STBI_FREE(z->idata); z->idata = NULL; if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) s->img_out_n = s->img_n+1; else s->img_out_n = s->img_n; if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0; if (has_trans) { if (z->depth == 16) { if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; } else { if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; } } if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) stbi__de_iphone(z); if (pal_img_n) { // pal_img_n == 3 or 4 s->img_n = pal_img_n; // record the actual colors we had s->img_out_n = pal_img_n; if (req_comp >= 3) s->img_out_n = req_comp; if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) return 0; } else if (has_trans) { // non-paletted image with tRNS -> source image has (constant) alpha ++s->img_n; } STBI_FREE(z->expanded); z->expanded = NULL; // end of PNG chunk, read and skip CRC stbi__get32be(s); return 1; } default: // if critical, fail if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if ((c.type & (1 << 29)) == 0) { #ifndef STBI_NO_FAILURE_STRINGS // not threadsafe static char invalid_chunk[] = "XXXX PNG chunk not known"; invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); #endif return stbi__err(invalid_chunk, "PNG not supported: unknown PNG chunk type"); } stbi__skip(s, c.length); break; } // end of PNG chunk, read and skip CRC stbi__get32be(s); } } static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) { void *result=NULL; if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { if (p->depth <= 8) ri->bits_per_channel = 8; else if (p->depth == 16) ri->bits_per_channel = 16; else return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); result = p->out; p->out = NULL; if (req_comp && req_comp != p->s->img_out_n) { if (ri->bits_per_channel == 8) result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); else result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); p->s->img_out_n = req_comp; if (result == NULL) return result; } *x = p->s->img_x; *y = p->s->img_y; if (n) *n = p->s->img_n; } STBI_FREE(p->out); p->out = NULL; STBI_FREE(p->expanded); p->expanded = NULL; STBI_FREE(p->idata); p->idata = NULL; return result; } static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { stbi__png p; p.s = s; return stbi__do_png(&p, x,y,comp,req_comp, ri); } static int stbi__png_test(stbi__context *s) { int r; r = stbi__check_png_header(s); stbi__rewind(s); return r; } static int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp) { if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { stbi__rewind( p->s ); return 0; } if (x) *x = p->s->img_x; if (y) *y = p->s->img_y; if (comp) *comp = p->s->img_n; return 1; } static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp) { stbi__png p; p.s = s; return stbi__png_info_raw(&p, x, y, comp); } static int stbi__png_is16(stbi__context *s) { stbi__png p; p.s = s; if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) return 0; if (p.depth != 16) { stbi__rewind(p.s); return 0; } return 1; } #endif // Microsoft/Windows BMP image #ifndef STBI_NO_BMP static int stbi__bmp_test_raw(stbi__context *s) { int r; int sz; if (stbi__get8(s) != 'B') return 0; if (stbi__get8(s) != 'M') return 0; stbi__get32le(s); // discard filesize stbi__get16le(s); // discard reserved stbi__get16le(s); // discard reserved stbi__get32le(s); // discard data offset sz = stbi__get32le(s); r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124); return r; } static int stbi__bmp_test(stbi__context *s) { int r = stbi__bmp_test_raw(s); stbi__rewind(s); return r; } // returns 0..31 for the highest set bit static int stbi__high_bit(unsigned int z) { int n=0; if (z == 0) return -1; if (z >= 0x10000) { n += 16; z >>= 16; } if (z >= 0x00100) { n += 8; z >>= 8; } if (z >= 0x00010) { n += 4; z >>= 4; } if (z >= 0x00004) { n += 2; z >>= 2; } if (z >= 0x00002) { n += 1;/* >>= 1;*/ } return n; } static int stbi__bitcount(unsigned int a) { a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits a = (a + (a >> 8)); // max 16 per 8 bits a = (a + (a >> 16)); // max 32 per 8 bits return a & 0xff; } // extract an arbitrarily-aligned N-bit value (N=bits) // from v, and then make it 8-bits long and fractionally // extend it to full full range. static int stbi__shiftsigned(unsigned int v, int shift, int bits) { static unsigned int mul_table[9] = { 0, 0xff/*0b11111111*/, 0x55/*0b01010101*/, 0x49/*0b01001001*/, 0x11/*0b00010001*/, 0x21/*0b00100001*/, 0x41/*0b01000001*/, 0x81/*0b10000001*/, 0x01/*0b00000001*/, }; static unsigned int shift_table[9] = { 0, 0,0,1,0,2,4,6,0, }; if (shift < 0) v <<= -shift; else v >>= shift; STBI_ASSERT(v < 256); v >>= (8-bits); STBI_ASSERT(bits >= 0 && bits <= 8); return (int) ((unsigned) v * mul_table[bits]) >> shift_table[bits]; } typedef struct { int bpp, offset, hsz; unsigned int mr,mg,mb,ma, all_a; int extra_read; } stbi__bmp_data; static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress) { // BI_BITFIELDS specifies masks explicitly, don't override if (compress == 3) return 1; if (compress == 0) { if (info->bpp == 16) { info->mr = 31u << 10; info->mg = 31u << 5; info->mb = 31u << 0; } else if (info->bpp == 32) { info->mr = 0xffu << 16; info->mg = 0xffu << 8; info->mb = 0xffu << 0; info->ma = 0xffu << 24; info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 } else { // otherwise, use defaults, which is all-0 info->mr = info->mg = info->mb = info->ma = 0; } return 1; } return 0; // error } static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) { int hsz; if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') return stbi__errpuc("not BMP", "Corrupt BMP"); stbi__get32le(s); // discard filesize stbi__get16le(s); // discard reserved stbi__get16le(s); // discard reserved info->offset = stbi__get32le(s); info->hsz = hsz = stbi__get32le(s); info->mr = info->mg = info->mb = info->ma = 0; info->extra_read = 14; if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP"); if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); if (hsz == 12) { s->img_x = stbi__get16le(s); s->img_y = stbi__get16le(s); } else { s->img_x = stbi__get32le(s); s->img_y = stbi__get32le(s); } if (stbi__get16le(s) != 1) return stbi__errpuc("bad BMP", "bad BMP"); info->bpp = stbi__get16le(s); if (hsz != 12) { int compress = stbi__get32le(s); if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel stbi__get32le(s); // discard sizeof stbi__get32le(s); // discard hres stbi__get32le(s); // discard vres stbi__get32le(s); // discard colorsused stbi__get32le(s); // discard max important if (hsz == 40 || hsz == 56) { if (hsz == 56) { stbi__get32le(s); stbi__get32le(s); stbi__get32le(s); stbi__get32le(s); } if (info->bpp == 16 || info->bpp == 32) { if (compress == 0) { stbi__bmp_set_mask_defaults(info, compress); } else if (compress == 3) { info->mr = stbi__get32le(s); info->mg = stbi__get32le(s); info->mb = stbi__get32le(s); info->extra_read += 12; // not documented, but generated by photoshop and handled by mspaint if (info->mr == info->mg && info->mg == info->mb) { // ?!?!? return stbi__errpuc("bad BMP", "bad BMP"); } } else return stbi__errpuc("bad BMP", "bad BMP"); } } else { // V4/V5 header int i; if (hsz != 108 && hsz != 124) return stbi__errpuc("bad BMP", "bad BMP"); info->mr = stbi__get32le(s); info->mg = stbi__get32le(s); info->mb = stbi__get32le(s); info->ma = stbi__get32le(s); if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs stbi__bmp_set_mask_defaults(info, compress); stbi__get32le(s); // discard color space for (i=0; i < 12; ++i) stbi__get32le(s); // discard color space parameters if (hsz == 124) { stbi__get32le(s); // discard rendering intent stbi__get32le(s); // discard offset of profile data stbi__get32le(s); // discard size of profile data stbi__get32le(s); // discard reserved } } } return (void *) 1; } static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { stbi_uc *out; unsigned int mr=0,mg=0,mb=0,ma=0, all_a; stbi_uc pal[256][4]; int psize=0,i,j,width; int flip_vertically, pad, target; stbi__bmp_data info; STBI_NOTUSED(ri); info.all_a = 255; if (stbi__bmp_parse_header(s, &info) == NULL) return NULL; // error code already set flip_vertically = ((int) s->img_y) > 0; s->img_y = abs((int) s->img_y); if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); mr = info.mr; mg = info.mg; mb = info.mb; ma = info.ma; all_a = info.all_a; if (info.hsz == 12) { if (info.bpp < 24) psize = (info.offset - info.extra_read - 24) / 3; } else { if (info.bpp < 16) psize = (info.offset - info.extra_read - info.hsz) >> 2; } if (psize == 0) { // accept some number of extra bytes after the header, but if the offset points either to before // the header ends or implies a large amount of extra data, reject the file as malformed int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original); int header_limit = 1024; // max we actually read is below 256 bytes currently. int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size. if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) { return stbi__errpuc("bad header", "Corrupt BMP"); } // we established that bytes_read_so_far is positive and sensible. // the first half of this test rejects offsets that are either too small positives, or // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn // ensures the number computed in the second half of the test can't overflow. if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) { return stbi__errpuc("bad offset", "Corrupt BMP"); } else { stbi__skip(s, info.offset - bytes_read_so_far); } } if (info.bpp == 24 && ma == 0xff000000) s->img_n = 3; else s->img_n = ma ? 4 : 3; if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 target = req_comp; else target = s->img_n; // if they want monochrome, we'll post-convert // sanity-check size if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0)) return stbi__errpuc("too large", "Corrupt BMP"); out = (stbi_uc *) stbi__malloc_mad3(target, s->img_x, s->img_y, 0); if (!out) return stbi__errpuc("outofmem", "Out of memory"); if (info.bpp < 16) { int z=0; if (psize == 0 || psize > 256) { STBI_FREE(out); return stbi__errpuc("invalid", "Corrupt BMP"); } for (i=0; i < psize; ++i) { pal[i][2] = stbi__get8(s); pal[i][1] = stbi__get8(s); pal[i][0] = stbi__get8(s); if (info.hsz != 12) stbi__get8(s); pal[i][3] = 255; } stbi__skip(s, info.offset - info.extra_read - info.hsz - psize * (info.hsz == 12 ? 3 : 4)); if (info.bpp == 1) width = (s->img_x + 7) >> 3; else if (info.bpp == 4) width = (s->img_x + 1) >> 1; else if (info.bpp == 8) width = s->img_x; else { STBI_FREE(out); return stbi__errpuc("bad bpp", "Corrupt BMP"); } pad = (-width)&3; if (info.bpp == 1) { for (j=0; j < (int) s->img_y; ++j) { int bit_offset = 7, v = stbi__get8(s); for (i=0; i < (int) s->img_x; ++i) { int color = (v>>bit_offset)&0x1; out[z++] = pal[color][0]; out[z++] = pal[color][1]; out[z++] = pal[color][2]; if (target == 4) out[z++] = 255; if (i+1 == (int) s->img_x) break; if((--bit_offset) < 0) { bit_offset = 7; v = stbi__get8(s); } } stbi__skip(s, pad); } } else { for (j=0; j < (int) s->img_y; ++j) { for (i=0; i < (int) s->img_x; i += 2) { int v=stbi__get8(s),v2=0; if (info.bpp == 4) { v2 = v & 15; v >>= 4; } out[z++] = pal[v][0]; out[z++] = pal[v][1]; out[z++] = pal[v][2]; if (target == 4) out[z++] = 255; if (i+1 == (int) s->img_x) break; v = (info.bpp == 8) ? stbi__get8(s) : v2; out[z++] = pal[v][0]; out[z++] = pal[v][1]; out[z++] = pal[v][2]; if (target == 4) out[z++] = 255; } stbi__skip(s, pad); } } } else { int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0; int z = 0; int easy=0; stbi__skip(s, info.offset - info.extra_read - info.hsz); if (info.bpp == 24) width = 3 * s->img_x; else if (info.bpp == 16) width = 2*s->img_x; else /* bpp = 32 and pad = 0 */ width=0; pad = (-width) & 3; if (info.bpp == 24) { easy = 1; } else if (info.bpp == 32) { if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000) easy = 2; } if (!easy) { if (!mr || !mg || !mb) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } // right shift amt to put high bit in position #7 rshift = stbi__high_bit(mr)-7; rcount = stbi__bitcount(mr); gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } } for (j=0; j < (int) s->img_y; ++j) { if (easy) { for (i=0; i < (int) s->img_x; ++i) { unsigned char a; out[z+2] = stbi__get8(s); out[z+1] = stbi__get8(s); out[z+0] = stbi__get8(s); z += 3; a = (easy == 2 ? stbi__get8(s) : 255); all_a |= a; if (target == 4) out[z++] = a; } } else { int bpp = info.bpp; for (i=0; i < (int) s->img_x; ++i) { stbi__uint32 v = (bpp == 16 ? (stbi__uint32) stbi__get16le(s) : stbi__get32le(s)); unsigned int a; out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mr, rshift, rcount)); out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mg, gshift, gcount)); out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mb, bshift, bcount)); a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255); all_a |= a; if (target == 4) out[z++] = STBI__BYTECAST(a); } } stbi__skip(s, pad); } } // if alpha channel is all 0s, replace with all 255s if (target == 4 && all_a == 0) for (i=4*s->img_x*s->img_y-1; i >= 0; i -= 4) out[i] = 255; if (flip_vertically) { stbi_uc t; for (j=0; j < (int) s->img_y>>1; ++j) { stbi_uc *p1 = out + j *s->img_x*target; stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target; for (i=0; i < (int) s->img_x*target; ++i) { t = p1[i]; p1[i] = p2[i]; p2[i] = t; } } } if (req_comp && req_comp != target) { out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y); if (out == NULL) return out; // stbi__convert_format frees input on failure } *x = s->img_x; *y = s->img_y; if (comp) *comp = s->img_n; return out; } #endif // Targa Truevision - TGA // by Jonathan Dummer #ifndef STBI_NO_TGA // returns STBI_rgb or whatever, 0 on error static int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16) { // only RGB or RGBA (incl. 16bit) or grey allowed if (is_rgb16) *is_rgb16 = 0; switch(bits_per_pixel) { case 8: return STBI_grey; case 16: if(is_grey) return STBI_grey_alpha; // fallthrough case 15: if(is_rgb16) *is_rgb16 = 1; return STBI_rgb; case 24: // fallthrough case 32: return bits_per_pixel/8; default: return 0; } } static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp) { int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, tga_colormap_bpp; int sz, tga_colormap_type; stbi__get8(s); // discard Offset tga_colormap_type = stbi__get8(s); // colormap type if( tga_colormap_type > 1 ) { stbi__rewind(s); return 0; // only RGB or indexed allowed } tga_image_type = stbi__get8(s); // image type if ( tga_colormap_type == 1 ) { // colormapped (paletted) image if (tga_image_type != 1 && tga_image_type != 9) { stbi__rewind(s); return 0; } stbi__skip(s,4); // skip index of first colormap entry and number of entries sz = stbi__get8(s); // check bits per palette color entry if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) { stbi__rewind(s); return 0; } stbi__skip(s,4); // skip image x and y origin tga_colormap_bpp = sz; } else { // "normal" image w/o colormap - only RGB or grey allowed, +/- RLE if ( (tga_image_type != 2) && (tga_image_type != 3) && (tga_image_type != 10) && (tga_image_type != 11) ) { stbi__rewind(s); return 0; // only RGB or grey allowed, +/- RLE } stbi__skip(s,9); // skip colormap specification and image x/y origin tga_colormap_bpp = 0; } tga_w = stbi__get16le(s); if( tga_w < 1 ) { stbi__rewind(s); return 0; // test width } tga_h = stbi__get16le(s); if( tga_h < 1 ) { stbi__rewind(s); return 0; // test height } tga_bits_per_pixel = stbi__get8(s); // bits per pixel stbi__get8(s); // ignore alpha bits if (tga_colormap_bpp != 0) { if((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) { // when using a colormap, tga_bits_per_pixel is the size of the indexes // I don't think anything but 8 or 16bit indexes makes sense stbi__rewind(s); return 0; } tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL); } else { tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), NULL); } if(!tga_comp) { stbi__rewind(s); return 0; } if (x) *x = tga_w; if (y) *y = tga_h; if (comp) *comp = tga_comp; return 1; // seems to have passed everything } static int stbi__tga_test(stbi__context *s) { int res = 0; int sz, tga_color_type; stbi__get8(s); // discard Offset tga_color_type = stbi__get8(s); // color type if ( tga_color_type > 1 ) goto errorEnd; // only RGB or indexed allowed sz = stbi__get8(s); // image type if ( tga_color_type == 1 ) { // colormapped (paletted) image if (sz != 1 && sz != 9) goto errorEnd; // colortype 1 demands image type 1 or 9 stbi__skip(s,4); // skip index of first colormap entry and number of entries sz = stbi__get8(s); // check bits per palette color entry if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; stbi__skip(s,4); // skip image x and y origin } else { // "normal" image w/o colormap if ( (sz != 2) && (sz != 3) && (sz != 10) && (sz != 11) ) goto errorEnd; // only RGB or grey allowed, +/- RLE stbi__skip(s,9); // skip colormap specification and image x/y origin } if ( stbi__get16le(s) < 1 ) goto errorEnd; // test width if ( stbi__get16le(s) < 1 ) goto errorEnd; // test height sz = stbi__get8(s); // bits per pixel if ( (tga_color_type == 1) && (sz != 8) && (sz != 16) ) goto errorEnd; // for colormapped images, bpp is size of an index if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; res = 1; // if we got this far, everything's good and we can return 1 instead of 0 errorEnd: stbi__rewind(s); return res; } // read 16bit value and convert to 24bit RGB static void stbi__tga_read_rgb16(stbi__context *s, stbi_uc* out) { stbi__uint16 px = (stbi__uint16)stbi__get16le(s); stbi__uint16 fiveBitMask = 31; // we have 3 channels with 5bits each int r = (px >> 10) & fiveBitMask; int g = (px >> 5) & fiveBitMask; int b = px & fiveBitMask; // Note that this saves the data in RGB(A) order, so it doesn't need to be swapped later out[0] = (stbi_uc)((r * 255)/31); out[1] = (stbi_uc)((g * 255)/31); out[2] = (stbi_uc)((b * 255)/31); // some people claim that the most significant bit might be used for alpha // (possibly if an alpha-bit is set in the "image descriptor byte") // but that only made 16bit test images completely translucent.. // so let's treat all 15 and 16bit TGAs as RGB with no alpha. } static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { // read in the TGA header stuff int tga_offset = stbi__get8(s); int tga_indexed = stbi__get8(s); int tga_image_type = stbi__get8(s); int tga_is_RLE = 0; int tga_palette_start = stbi__get16le(s); int tga_palette_len = stbi__get16le(s); int tga_palette_bits = stbi__get8(s); int tga_x_origin = stbi__get16le(s); int tga_y_origin = stbi__get16le(s); int tga_width = stbi__get16le(s); int tga_height = stbi__get16le(s); int tga_bits_per_pixel = stbi__get8(s); int tga_comp, tga_rgb16=0; int tga_inverted = stbi__get8(s); // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused (useless?) // image data unsigned char *tga_data; unsigned char *tga_palette = NULL; int i, j; unsigned char raw_data[4] = {0}; int RLE_count = 0; int RLE_repeating = 0; int read_next_pixel = 1; STBI_NOTUSED(ri); STBI_NOTUSED(tga_x_origin); // @TODO STBI_NOTUSED(tga_y_origin); // @TODO if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); // do a tiny bit of precessing if ( tga_image_type >= 8 ) { tga_image_type -= 8; tga_is_RLE = 1; } tga_inverted = 1 - ((tga_inverted >> 5) & 1); // If I'm paletted, then I'll use the number of bits from the palette if ( tga_indexed ) tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16); else tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), &tga_rgb16); if(!tga_comp) // shouldn't really happen, stbi__tga_test() should have ensured basic consistency return stbi__errpuc("bad format", "Can't find out TGA pixelformat"); // tga info *x = tga_width; *y = tga_height; if (comp) *comp = tga_comp; if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0)) return stbi__errpuc("too large", "Corrupt TGA"); tga_data = (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0); if (!tga_data) return stbi__errpuc("outofmem", "Out of memory"); // skip to the data's starting position (offset usually = 0) stbi__skip(s, tga_offset ); if ( !tga_indexed && !tga_is_RLE && !tga_rgb16 ) { for (i=0; i < tga_height; ++i) { int row = tga_inverted ? tga_height -i - 1 : i; stbi_uc *tga_row = tga_data + row*tga_width*tga_comp; stbi__getn(s, tga_row, tga_width * tga_comp); } } else { // do I need to load a palette? if ( tga_indexed) { if (tga_palette_len == 0) { /* you have to have at least one entry! */ STBI_FREE(tga_data); return stbi__errpuc("bad palette", "Corrupt TGA"); } // any data to skip? (offset usually = 0) stbi__skip(s, tga_palette_start ); // load the palette tga_palette = (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0); if (!tga_palette) { STBI_FREE(tga_data); return stbi__errpuc("outofmem", "Out of memory"); } if (tga_rgb16) { stbi_uc *pal_entry = tga_palette; STBI_ASSERT(tga_comp == STBI_rgb); for (i=0; i < tga_palette_len; ++i) { stbi__tga_read_rgb16(s, pal_entry); pal_entry += tga_comp; } } else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) { STBI_FREE(tga_data); STBI_FREE(tga_palette); return stbi__errpuc("bad palette", "Corrupt TGA"); } } // load the data for (i=0; i < tga_width * tga_height; ++i) { // if I'm in RLE mode, do I need to get a RLE stbi__pngchunk? if ( tga_is_RLE ) { if ( RLE_count == 0 ) { // yep, get the next byte as a RLE command int RLE_cmd = stbi__get8(s); RLE_count = 1 + (RLE_cmd & 127); RLE_repeating = RLE_cmd >> 7; read_next_pixel = 1; } else if ( !RLE_repeating ) { read_next_pixel = 1; } } else { read_next_pixel = 1; } // OK, if I need to read a pixel, do it now if ( read_next_pixel ) { // load however much data we did have if ( tga_indexed ) { // read in index, then perform the lookup int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) : stbi__get16le(s); if ( pal_idx >= tga_palette_len ) { // invalid index pal_idx = 0; } pal_idx *= tga_comp; for (j = 0; j < tga_comp; ++j) { raw_data[j] = tga_palette[pal_idx+j]; } } else if(tga_rgb16) { STBI_ASSERT(tga_comp == STBI_rgb); stbi__tga_read_rgb16(s, raw_data); } else { // read in the data raw for (j = 0; j < tga_comp; ++j) { raw_data[j] = stbi__get8(s); } } // clear the reading flag for the next pixel read_next_pixel = 0; } // end of reading a pixel // copy data for (j = 0; j < tga_comp; ++j) tga_data[i*tga_comp+j] = raw_data[j]; // in case we're in RLE mode, keep counting down --RLE_count; } // do I need to invert the image? if ( tga_inverted ) { for (j = 0; j*2 < tga_height; ++j) { int index1 = j * tga_width * tga_comp; int index2 = (tga_height - 1 - j) * tga_width * tga_comp; for (i = tga_width * tga_comp; i > 0; --i) { unsigned char temp = tga_data[index1]; tga_data[index1] = tga_data[index2]; tga_data[index2] = temp; ++index1; ++index2; } } } // clear my palette, if I had one if ( tga_palette != NULL ) { STBI_FREE( tga_palette ); } } // swap RGB - if the source data was RGB16, it already is in the right order if (tga_comp >= 3 && !tga_rgb16) { unsigned char* tga_pixel = tga_data; for (i=0; i < tga_width * tga_height; ++i) { unsigned char temp = tga_pixel[0]; tga_pixel[0] = tga_pixel[2]; tga_pixel[2] = temp; tga_pixel += tga_comp; } } // convert to target component count if (req_comp && req_comp != tga_comp) tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, tga_height); // the things I do to get rid of an error message, and yet keep // Microsoft's C compilers happy... [8^( tga_palette_start = tga_palette_len = tga_palette_bits = tga_x_origin = tga_y_origin = 0; STBI_NOTUSED(tga_palette_start); // OK, done return tga_data; } #endif // ************************************************************************************************* // Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB #ifndef STBI_NO_PSD static int stbi__psd_test(stbi__context *s) { int r = (stbi__get32be(s) == 0x38425053); stbi__rewind(s); return r; } static int stbi__psd_decode_rle(stbi__context *s, stbi_uc *p, int pixelCount) { int count, nleft, len; count = 0; while ((nleft = pixelCount - count) > 0) { len = stbi__get8(s); if (len == 128) { // No-op. } else if (len < 128) { // Copy next len+1 bytes literally. len++; if (len > nleft) return 0; // corrupt data count += len; while (len) { *p = stbi__get8(s); p += 4; len--; } } else if (len > 128) { stbi_uc val; // Next -len+1 bytes in the dest are replicated from next source byte. // (Interpret len as a negative 8-bit int.) len = 257 - len; if (len > nleft) return 0; // corrupt data val = stbi__get8(s); count += len; while (len) { *p = val; p += 4; len--; } } } return 1; } static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) { int pixelCount; int channelCount, compression; int channel, i; int bitdepth; int w,h; stbi_uc *out; STBI_NOTUSED(ri); // Check identifier if (stbi__get32be(s) != 0x38425053) // "8BPS" return stbi__errpuc("not PSD", "Corrupt PSD image"); // Check file type version. if (stbi__get16be(s) != 1) return stbi__errpuc("wrong version", "Unsupported version of PSD image"); // Skip 6 reserved bytes. stbi__skip(s, 6 ); // Read the number of channels (R, G, B, A, etc). channelCount = stbi__get16be(s); if (channelCount < 0 || channelCount > 16) return stbi__errpuc("wrong channel count", "Unsupported number of channels in PSD image"); // Read the rows and columns of the image. h = stbi__get32be(s); w = stbi__get32be(s); if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); // Make sure the depth is 8 bits. bitdepth = stbi__get16be(s); if (bitdepth != 8 && bitdepth != 16) return stbi__errpuc("unsupported bit depth", "PSD bit depth is not 8 or 16 bit"); // Make sure the color mode is RGB. // Valid options are: // 0: Bitmap // 1: Grayscale // 2: Indexed color // 3: RGB color // 4: CMYK color // 7: Multichannel // 8: Duotone // 9: Lab color if (stbi__get16be(s) != 3) return stbi__errpuc("wrong color format", "PSD is not in RGB color format"); // Skip the Mode Data. (It's the palette for indexed color; other info for other modes.) stbi__skip(s,stbi__get32be(s) ); // Skip the image resources. (resolution, pen tool paths, etc) stbi__skip(s, stbi__get32be(s) ); // Skip the reserved data. stbi__skip(s, stbi__get32be(s) ); // Find out if the data is compressed. // Known values: // 0: no compression // 1: RLE compressed compression = stbi__get16be(s); if (compression > 1) return stbi__errpuc("bad compression", "PSD has an unknown compression format"); // Check size if (!stbi__mad3sizes_valid(4, w, h, 0)) return stbi__errpuc("too large", "Corrupt PSD"); // Create the destination image. if (!compression && bitdepth == 16 && bpc == 16) { out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0); ri->bits_per_channel = 16; } else out = (stbi_uc *) stbi__malloc(4 * w*h); if (!out) return stbi__errpuc("outofmem", "Out of memory"); pixelCount = w*h; // Initialize the data to zero. //memset( out, 0, pixelCount * 4 ); // Finally, the image data. if (compression) { // RLE as used by .PSD and .TIFF // Loop until you get the number of unpacked bytes you are expecting: // Read the next source byte into n. // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. // Else if n is 128, noop. // Endloop // The RLE-compressed data is preceded by a 2-byte data count for each row in the data, // which we're going to just skip. stbi__skip(s, h * channelCount * 2 ); // Read the RLE data by channel. for (channel = 0; channel < 4; channel++) { stbi_uc *p; p = out+channel; if (channel >= channelCount) { // Fill this channel with default data. for (i = 0; i < pixelCount; i++, p += 4) *p = (channel == 3 ? 255 : 0); } else { // Read the RLE data. if (!stbi__psd_decode_rle(s, p, pixelCount)) { STBI_FREE(out); return stbi__errpuc("corrupt", "bad RLE data"); } } } } else { // We're at the raw image data. It's each channel in order (Red, Green, Blue, Alpha, ...) // where each channel consists of an 8-bit (or 16-bit) value for each pixel in the image. // Read the data by channel. for (channel = 0; channel < 4; channel++) { if (channel >= channelCount) { // Fill this channel with default data. if (bitdepth == 16 && bpc == 16) { stbi__uint16 *q = ((stbi__uint16 *) out) + channel; stbi__uint16 val = channel == 3 ? 65535 : 0; for (i = 0; i < pixelCount; i++, q += 4) *q = val; } else { stbi_uc *p = out+channel; stbi_uc val = channel == 3 ? 255 : 0; for (i = 0; i < pixelCount; i++, p += 4) *p = val; } } else { if (ri->bits_per_channel == 16) { // output bpc stbi__uint16 *q = ((stbi__uint16 *) out) + channel; for (i = 0; i < pixelCount; i++, q += 4) *q = (stbi__uint16) stbi__get16be(s); } else { stbi_uc *p = out+channel; if (bitdepth == 16) { // input bpc for (i = 0; i < pixelCount; i++, p += 4) *p = (stbi_uc) (stbi__get16be(s) >> 8); } else { for (i = 0; i < pixelCount; i++, p += 4) *p = stbi__get8(s); } } } } } // remove weird white matte from PSD if (channelCount >= 4) { if (ri->bits_per_channel == 16) { for (i=0; i < w*h; ++i) { stbi__uint16 *pixel = (stbi__uint16 *) out + 4*i; if (pixel[3] != 0 && pixel[3] != 65535) { float a = pixel[3] / 65535.0f; float ra = 1.0f / a; float inv_a = 65535.0f * (1 - ra); pixel[0] = (stbi__uint16) (pixel[0]*ra + inv_a); pixel[1] = (stbi__uint16) (pixel[1]*ra + inv_a); pixel[2] = (stbi__uint16) (pixel[2]*ra + inv_a); } } } else { for (i=0; i < w*h; ++i) { unsigned char *pixel = out + 4*i; if (pixel[3] != 0 && pixel[3] != 255) { float a = pixel[3] / 255.0f; float ra = 1.0f / a; float inv_a = 255.0f * (1 - ra); pixel[0] = (unsigned char) (pixel[0]*ra + inv_a); pixel[1] = (unsigned char) (pixel[1]*ra + inv_a); pixel[2] = (unsigned char) (pixel[2]*ra + inv_a); } } } } // convert to desired output format if (req_comp && req_comp != 4) { if (ri->bits_per_channel == 16) out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, 4, req_comp, w, h); else out = stbi__convert_format(out, 4, req_comp, w, h); if (out == NULL) return out; // stbi__convert_format frees input on failure } if (comp) *comp = 4; *y = h; *x = w; return out; } #endif // ************************************************************************************************* // Softimage PIC loader // by Tom Seddon // // See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format // See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ #ifndef STBI_NO_PIC static int stbi__pic_is4(stbi__context *s,const char *str) { int i; for (i=0; i<4; ++i) if (stbi__get8(s) != (stbi_uc)str[i]) return 0; return 1; } static int stbi__pic_test_core(stbi__context *s) { int i; if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) return 0; for(i=0;i<84;++i) stbi__get8(s); if (!stbi__pic_is4(s,"PICT")) return 0; return 1; } typedef struct { stbi_uc size,type,channel; } stbi__pic_packet; static stbi_uc *stbi__readval(stbi__context *s, int channel, stbi_uc *dest) { int mask=0x80, i; for (i=0; i<4; ++i, mask>>=1) { if (channel & mask) { if (stbi__at_eof(s)) return stbi__errpuc("bad file","PIC file too short"); dest[i]=stbi__get8(s); } } return dest; } static void stbi__copyval(int channel,stbi_uc *dest,const stbi_uc *src) { int mask=0x80,i; for (i=0;i<4; ++i, mask>>=1) if (channel&mask) dest[i]=src[i]; } static stbi_uc *stbi__pic_load_core(stbi__context *s,int width,int height,int *comp, stbi_uc *result) { int act_comp=0,num_packets=0,y,chained; stbi__pic_packet packets[10]; // this will (should...) cater for even some bizarre stuff like having data // for the same channel in multiple packets. do { stbi__pic_packet *packet; if (num_packets==sizeof(packets)/sizeof(packets[0])) return stbi__errpuc("bad format","too many packets"); packet = &packets[num_packets++]; chained = stbi__get8(s); packet->size = stbi__get8(s); packet->type = stbi__get8(s); packet->channel = stbi__get8(s); act_comp |= packet->channel; if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (reading packets)"); if (packet->size != 8) return stbi__errpuc("bad format","packet isn't 8bpp"); } while (chained); *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? for(y=0; ytype) { default: return stbi__errpuc("bad format","packet has bad compression type"); case 0: {//uncompressed int x; for(x=0;xchannel,dest)) return 0; break; } case 1://Pure RLE { int left=width, i; while (left>0) { stbi_uc count,value[4]; count=stbi__get8(s); if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pure read count)"); if (count > left) count = (stbi_uc) left; if (!stbi__readval(s,packet->channel,value)) return 0; for(i=0; ichannel,dest,value); left -= count; } } break; case 2: {//Mixed RLE int left=width; while (left>0) { int count = stbi__get8(s), i; if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (mixed read count)"); if (count >= 128) { // Repeated stbi_uc value[4]; if (count==128) count = stbi__get16be(s); else count -= 127; if (count > left) return stbi__errpuc("bad file","scanline overrun"); if (!stbi__readval(s,packet->channel,value)) return 0; for(i=0;ichannel,dest,value); } else { // Raw ++count; if (count>left) return stbi__errpuc("bad file","scanline overrun"); for(i=0;ichannel,dest)) return 0; } left-=count; } break; } } } } return result; } static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_comp, stbi__result_info *ri) { stbi_uc *result; int i, x,y, internal_comp; STBI_NOTUSED(ri); if (!comp) comp = &internal_comp; for (i=0; i<92; ++i) stbi__get8(s); x = stbi__get16be(s); y = stbi__get16be(s); if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); stbi__get32be(s); //skip `ratio' stbi__get16be(s); //skip `fields' stbi__get16be(s); //skip `pad' // intermediate buffer is RGBA result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0); if (!result) return stbi__errpuc("outofmem", "Out of memory"); memset(result, 0xff, x*y*4); if (!stbi__pic_load_core(s,x,y,comp, result)) { STBI_FREE(result); result=0; } *px = x; *py = y; if (req_comp == 0) req_comp = *comp; result=stbi__convert_format(result,4,req_comp,x,y); return result; } static int stbi__pic_test(stbi__context *s) { int r = stbi__pic_test_core(s); stbi__rewind(s); return r; } #endif // ************************************************************************************************* // GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb #ifndef STBI_NO_GIF typedef struct { stbi__int16 prefix; stbi_uc first; stbi_uc suffix; } stbi__gif_lzw; typedef struct { int w,h; stbi_uc *out; // output buffer (always 4 components) stbi_uc *background; // The current "background" as far as a gif is concerned stbi_uc *history; int flags, bgindex, ratio, transparent, eflags; stbi_uc pal[256][4]; stbi_uc lpal[256][4]; stbi__gif_lzw codes[8192]; stbi_uc *color_table; int parse, step; int lflags; int start_x, start_y; int max_x, max_y; int cur_x, cur_y; int line_size; int delay; } stbi__gif; static int stbi__gif_test_raw(stbi__context *s) { int sz; if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0; sz = stbi__get8(s); if (sz != '9' && sz != '7') return 0; if (stbi__get8(s) != 'a') return 0; return 1; } static int stbi__gif_test(stbi__context *s) { int r = stbi__gif_test_raw(s); stbi__rewind(s); return r; } static void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp) { int i; for (i=0; i < num_entries; ++i) { pal[i][2] = stbi__get8(s); pal[i][1] = stbi__get8(s); pal[i][0] = stbi__get8(s); pal[i][3] = transp == i ? 0 : 255; } } static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info) { stbi_uc version; if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return stbi__err("not GIF", "Corrupt GIF"); version = stbi__get8(s); if (version != '7' && version != '9') return stbi__err("not GIF", "Corrupt GIF"); if (stbi__get8(s) != 'a') return stbi__err("not GIF", "Corrupt GIF"); stbi__g_failure_reason = ""; g->w = stbi__get16le(s); g->h = stbi__get16le(s); g->flags = stbi__get8(s); g->bgindex = stbi__get8(s); g->ratio = stbi__get8(s); g->transparent = -1; if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments if (is_info) return 1; if (g->flags & 0x80) stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); return 1; } static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) { stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); if (!g) return stbi__err("outofmem", "Out of memory"); if (!stbi__gif_header(s, g, comp, 1)) { STBI_FREE(g); stbi__rewind( s ); return 0; } if (x) *x = g->w; if (y) *y = g->h; STBI_FREE(g); return 1; } static void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code) { stbi_uc *p, *c; int idx; // recurse to decode the prefixes, since the linked-list is backwards, // and working backwards through an interleaved image would be nasty if (g->codes[code].prefix >= 0) stbi__out_gif_code(g, g->codes[code].prefix); if (g->cur_y >= g->max_y) return; idx = g->cur_x + g->cur_y; p = &g->out[idx]; g->history[idx / 4] = 1; c = &g->color_table[g->codes[code].suffix * 4]; if (c[3] > 128) { // don't render transparent pixels; p[0] = c[2]; p[1] = c[1]; p[2] = c[0]; p[3] = c[3]; } g->cur_x += 4; if (g->cur_x >= g->max_x) { g->cur_x = g->start_x; g->cur_y += g->step; while (g->cur_y >= g->max_y && g->parse > 0) { g->step = (1 << g->parse) * g->line_size; g->cur_y = g->start_y + (g->step >> 1); --g->parse; } } } static stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g) { stbi_uc lzw_cs; stbi__int32 len, init_code; stbi__uint32 first; stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; stbi__gif_lzw *p; lzw_cs = stbi__get8(s); if (lzw_cs > 12) return NULL; clear = 1 << lzw_cs; first = 1; codesize = lzw_cs + 1; codemask = (1 << codesize) - 1; bits = 0; valid_bits = 0; for (init_code = 0; init_code < clear; init_code++) { g->codes[init_code].prefix = -1; g->codes[init_code].first = (stbi_uc) init_code; g->codes[init_code].suffix = (stbi_uc) init_code; } // support no starting clear code avail = clear+2; oldcode = -1; len = 0; for(;;) { if (valid_bits < codesize) { if (len == 0) { len = stbi__get8(s); // start new block if (len == 0) return g->out; } --len; bits |= (stbi__int32) stbi__get8(s) << valid_bits; valid_bits += 8; } else { stbi__int32 code = bits & codemask; bits >>= codesize; valid_bits -= codesize; // @OPTIMIZE: is there some way we can accelerate the non-clear path? if (code == clear) { // clear code codesize = lzw_cs + 1; codemask = (1 << codesize) - 1; avail = clear + 2; oldcode = -1; first = 0; } else if (code == clear + 1) { // end of stream code stbi__skip(s, len); while ((len = stbi__get8(s)) > 0) stbi__skip(s,len); return g->out; } else if (code <= avail) { if (first) { return stbi__errpuc("no clear code", "Corrupt GIF"); } if (oldcode >= 0) { p = &g->codes[avail++]; if (avail > 8192) { return stbi__errpuc("too many codes", "Corrupt GIF"); } p->prefix = (stbi__int16) oldcode; p->first = g->codes[oldcode].first; p->suffix = (code == avail) ? p->first : g->codes[code].first; } else if (code == avail) return stbi__errpuc("illegal code in raster", "Corrupt GIF"); stbi__out_gif_code(g, (stbi__uint16) code); if ((avail & codemask) == 0 && avail <= 0x0FFF) { codesize++; codemask = (1 << codesize) - 1; } oldcode = code; } else { return stbi__errpuc("illegal code in raster", "Corrupt GIF"); } } } } // this function is designed to support animated gifs, although stb_image doesn't support it // two back is the image from two frames ago, used for a very specific disposal format static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back) { int dispose; int first_frame; int pi; int pcount; STBI_NOTUSED(req_comp); // on first frame, any non-written pixels get the background colour (non-transparent) first_frame = 0; if (g->out == 0) { if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header if (!stbi__mad3sizes_valid(4, g->w, g->h, 0)) return stbi__errpuc("too large", "GIF image is too large"); pcount = g->w * g->h; g->out = (stbi_uc *) stbi__malloc(4 * pcount); g->background = (stbi_uc *) stbi__malloc(4 * pcount); g->history = (stbi_uc *) stbi__malloc(pcount); if (!g->out || !g->background || !g->history) return stbi__errpuc("outofmem", "Out of memory"); // image is treated as "transparent" at the start - ie, nothing overwrites the current background; // background colour is only used for pixels that are not rendered first frame, after that "background" // color refers to the color that was there the previous frame. memset(g->out, 0x00, 4 * pcount); memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent) memset(g->history, 0x00, pcount); // pixels that were affected previous frame first_frame = 1; } else { // second frame - how do we dispose of the previous one? dispose = (g->eflags & 0x1C) >> 2; pcount = g->w * g->h; if ((dispose == 3) && (two_back == 0)) { dispose = 2; // if I don't have an image to revert back to, default to the old background } if (dispose == 3) { // use previous graphic for (pi = 0; pi < pcount; ++pi) { if (g->history[pi]) { memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 ); } } } else if (dispose == 2) { // restore what was changed last frame to background before that frame; for (pi = 0; pi < pcount; ++pi) { if (g->history[pi]) { memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 ); } } } else { // This is a non-disposal case eithe way, so just // leave the pixels as is, and they will become the new background // 1: do not dispose // 0: not specified. } // background is what out is after the undoing of the previou frame; memcpy( g->background, g->out, 4 * g->w * g->h ); } // clear my history; memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame for (;;) { int tag = stbi__get8(s); switch (tag) { case 0x2C: /* Image Descriptor */ { stbi__int32 x, y, w, h; stbi_uc *o; x = stbi__get16le(s); y = stbi__get16le(s); w = stbi__get16le(s); h = stbi__get16le(s); if (((x + w) > (g->w)) || ((y + h) > (g->h))) return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); g->line_size = g->w * 4; g->start_x = x * 4; g->start_y = y * g->line_size; g->max_x = g->start_x + w * 4; g->max_y = g->start_y + h * g->line_size; g->cur_x = g->start_x; g->cur_y = g->start_y; // if the width of the specified rectangle is 0, that means // we may not see *any* pixels or the image is malformed; // to make sure this is caught, move the current y down to // max_y (which is what out_gif_code checks). if (w == 0) g->cur_y = g->max_y; g->lflags = stbi__get8(s); if (g->lflags & 0x40) { g->step = 8 * g->line_size; // first interlaced spacing g->parse = 3; } else { g->step = g->line_size; g->parse = 0; } if (g->lflags & 0x80) { stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); g->color_table = (stbi_uc *) g->lpal; } else if (g->flags & 0x80) { g->color_table = (stbi_uc *) g->pal; } else return stbi__errpuc("missing color table", "Corrupt GIF"); o = stbi__process_gif_raster(s, g); if (!o) return NULL; // if this was the first frame, pcount = g->w * g->h; if (first_frame && (g->bgindex > 0)) { // if first frame, any pixel not drawn to gets the background color for (pi = 0; pi < pcount; ++pi) { if (g->history[pi] == 0) { g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be; memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 ); } } } return o; } case 0x21: // Comment Extension. { int len; int ext = stbi__get8(s); if (ext == 0xF9) { // Graphic Control Extension. len = stbi__get8(s); if (len == 4) { g->eflags = stbi__get8(s); g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths. // unset old transparent if (g->transparent >= 0) { g->pal[g->transparent][3] = 255; } if (g->eflags & 0x01) { g->transparent = stbi__get8(s); if (g->transparent >= 0) { g->pal[g->transparent][3] = 0; } } else { // don't need transparent stbi__skip(s, 1); g->transparent = -1; } } else { stbi__skip(s, len); break; } } while ((len = stbi__get8(s)) != 0) { stbi__skip(s, len); } break; } case 0x3B: // gif stream termination code return (stbi_uc *) s; // using '1' causes warning on some compilers default: return stbi__errpuc("unknown code", "Corrupt GIF"); } } } static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) { STBI_FREE(g->out); STBI_FREE(g->history); STBI_FREE(g->background); if (out) STBI_FREE(out); if (delays && *delays) STBI_FREE(*delays); return stbi__errpuc("outofmem", "Out of memory"); } static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) { if (stbi__gif_test(s)) { int layers = 0; stbi_uc *u = 0; stbi_uc *out = 0; stbi_uc *two_back = 0; stbi__gif g; int stride; int out_size = 0; int delays_size = 0; STBI_NOTUSED(out_size); STBI_NOTUSED(delays_size); memset(&g, 0, sizeof(g)); if (delays) { *delays = 0; } do { u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); if (u == (stbi_uc *) s) u = 0; // end of animated gif marker if (u) { *x = g.w; *y = g.h; ++layers; stride = g.w * g.h * 4; if (out) { void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); if (!tmp) return stbi__load_gif_main_outofmem(&g, out, delays); else { out = (stbi_uc*) tmp; out_size = layers * stride; } if (delays) { int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); if (!new_delays) return stbi__load_gif_main_outofmem(&g, out, delays); *delays = new_delays; delays_size = layers * sizeof(int); } } else { out = (stbi_uc*)stbi__malloc( layers * stride ); if (!out) return stbi__load_gif_main_outofmem(&g, out, delays); out_size = layers * stride; if (delays) { *delays = (int*) stbi__malloc( layers * sizeof(int) ); if (!*delays) return stbi__load_gif_main_outofmem(&g, out, delays); delays_size = layers * sizeof(int); } } memcpy( out + ((layers - 1) * stride), u, stride ); if (layers >= 2) { two_back = out - 2 * stride; } if (delays) { (*delays)[layers - 1U] = g.delay; } } } while (u != 0); // free temp buffer; STBI_FREE(g.out); STBI_FREE(g.history); STBI_FREE(g.background); // do the final conversion after loading everything; if (req_comp && req_comp != 4) out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); *z = layers; return out; } else { return stbi__errpuc("not GIF", "Image was not as a gif type."); } } static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { stbi_uc *u = 0; stbi__gif g; memset(&g, 0, sizeof(g)); STBI_NOTUSED(ri); u = stbi__gif_load_next(s, &g, comp, req_comp, 0); if (u == (stbi_uc *) s) u = 0; // end of animated gif marker if (u) { *x = g.w; *y = g.h; // moved conversion to after successful load so that the same // can be done for multiple frames. if (req_comp && req_comp != 4) u = stbi__convert_format(u, 4, req_comp, g.w, g.h); } else if (g.out) { // if there was an error and we allocated an image buffer, free it! STBI_FREE(g.out); } // free buffers needed for multiple frame loading; STBI_FREE(g.history); STBI_FREE(g.background); return u; } static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp) { return stbi__gif_info_raw(s,x,y,comp); } #endif // ************************************************************************************************* // Radiance RGBE HDR loader // originally by Nicolas Schulz #ifndef STBI_NO_HDR static int stbi__hdr_test_core(stbi__context *s, const char *signature) { int i; for (i=0; signature[i]; ++i) if (stbi__get8(s) != signature[i]) return 0; stbi__rewind(s); return 1; } static int stbi__hdr_test(stbi__context* s) { int r = stbi__hdr_test_core(s, "#?RADIANCE\n"); stbi__rewind(s); if(!r) { r = stbi__hdr_test_core(s, "#?RGBE\n"); stbi__rewind(s); } return r; } #define STBI__HDR_BUFLEN 1024 static char *stbi__hdr_gettoken(stbi__context *z, char *buffer) { int len=0; char c = '\0'; c = (char) stbi__get8(z); while (!stbi__at_eof(z) && c != '\n') { buffer[len++] = c; if (len == STBI__HDR_BUFLEN-1) { // flush to end of line while (!stbi__at_eof(z) && stbi__get8(z) != '\n') ; break; } c = (char) stbi__get8(z); } buffer[len] = 0; return buffer; } static void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp) { if ( input[3] != 0 ) { float f1; // Exponent f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); if (req_comp <= 2) output[0] = (input[0] + input[1] + input[2]) * f1 / 3; else { output[0] = input[0] * f1; output[1] = input[1] * f1; output[2] = input[2] * f1; } if (req_comp == 2) output[1] = 1; if (req_comp == 4) output[3] = 1; } else { switch (req_comp) { case 4: output[3] = 1; /* fallthrough */ case 3: output[0] = output[1] = output[2] = 0; break; case 2: output[1] = 1; /* fallthrough */ case 1: output[0] = 0; break; } } } static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { char buffer[STBI__HDR_BUFLEN]; char *token; int valid = 0; int width, height; stbi_uc *scanline; float *hdr_data; int len; unsigned char count, value; int i, j, k, c1,c2, z; const char *headerToken; STBI_NOTUSED(ri); // Check identifier headerToken = stbi__hdr_gettoken(s,buffer); if (strcmp(headerToken, "#?RADIANCE") != 0 && strcmp(headerToken, "#?RGBE") != 0) return stbi__errpf("not HDR", "Corrupt HDR image"); // Parse header for(;;) { token = stbi__hdr_gettoken(s,buffer); if (token[0] == 0) break; if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; } if (!valid) return stbi__errpf("unsupported format", "Unsupported HDR format"); // Parse width and height // can't use sscanf() if we're not using stdio! token = stbi__hdr_gettoken(s,buffer); if (strncmp(token, "-Y ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); token += 3; height = (int) strtol(token, &token, 10); while (*token == ' ') ++token; if (strncmp(token, "+X ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); token += 3; width = (int) strtol(token, NULL, 10); if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); *x = width; *y = height; if (comp) *comp = 3; if (req_comp == 0) req_comp = 3; if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0)) return stbi__errpf("too large", "HDR image is too large"); // Read data hdr_data = (float *) stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0); if (!hdr_data) return stbi__errpf("outofmem", "Out of memory"); // Load image data // image data is stored as some number of sca if ( width < 8 || width >= 32768) { // Read flat data for (j=0; j < height; ++j) { for (i=0; i < width; ++i) { stbi_uc rgbe[4]; main_decode_loop: stbi__getn(s, rgbe, 4); stbi__hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp); } } } else { // Read RLE-encoded data scanline = NULL; for (j = 0; j < height; ++j) { c1 = stbi__get8(s); c2 = stbi__get8(s); len = stbi__get8(s); if (c1 != 2 || c2 != 2 || (len & 0x80)) { // not run-length encoded, so we have to actually use THIS data as a decoded // pixel (note this can't be a valid pixel--one of RGB must be >= 128) stbi_uc rgbe[4]; rgbe[0] = (stbi_uc) c1; rgbe[1] = (stbi_uc) c2; rgbe[2] = (stbi_uc) len; rgbe[3] = (stbi_uc) stbi__get8(s); stbi__hdr_convert(hdr_data, rgbe, req_comp); i = 1; j = 0; STBI_FREE(scanline); goto main_decode_loop; // yes, this makes no sense } len <<= 8; len |= stbi__get8(s); if (len != width) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("invalid decoded scanline length", "corrupt HDR"); } if (scanline == NULL) { scanline = (stbi_uc *) stbi__malloc_mad2(width, 4, 0); if (!scanline) { STBI_FREE(hdr_data); return stbi__errpf("outofmem", "Out of memory"); } } for (k = 0; k < 4; ++k) { int nleft; i = 0; while ((nleft = width - i) > 0) { count = stbi__get8(s); if (count > 128) { // Run value = stbi__get8(s); count -= 128; if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } for (z = 0; z < count; ++z) scanline[i++ * 4 + k] = value; } else { // Dump if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } for (z = 0; z < count; ++z) scanline[i++ * 4 + k] = stbi__get8(s); } } } for (i=0; i < width; ++i) stbi__hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp); } if (scanline) STBI_FREE(scanline); } return hdr_data; } static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp) { char buffer[STBI__HDR_BUFLEN]; char *token; int valid = 0; int dummy; if (!x) x = &dummy; if (!y) y = &dummy; if (!comp) comp = &dummy; if (stbi__hdr_test(s) == 0) { stbi__rewind( s ); return 0; } for(;;) { token = stbi__hdr_gettoken(s,buffer); if (token[0] == 0) break; if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; } if (!valid) { stbi__rewind( s ); return 0; } token = stbi__hdr_gettoken(s,buffer); if (strncmp(token, "-Y ", 3)) { stbi__rewind( s ); return 0; } token += 3; *y = (int) strtol(token, &token, 10); while (*token == ' ') ++token; if (strncmp(token, "+X ", 3)) { stbi__rewind( s ); return 0; } token += 3; *x = (int) strtol(token, NULL, 10); *comp = 3; return 1; } #endif // STBI_NO_HDR #ifndef STBI_NO_BMP static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) { void *p; stbi__bmp_data info; info.all_a = 255; p = stbi__bmp_parse_header(s, &info); if (p == NULL) { stbi__rewind( s ); return 0; } if (x) *x = s->img_x; if (y) *y = s->img_y; if (comp) { if (info.bpp == 24 && info.ma == 0xff000000) *comp = 3; else *comp = info.ma ? 4 : 3; } return 1; } #endif #ifndef STBI_NO_PSD static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp) { int channelCount, dummy, depth; if (!x) x = &dummy; if (!y) y = &dummy; if (!comp) comp = &dummy; if (stbi__get32be(s) != 0x38425053) { stbi__rewind( s ); return 0; } if (stbi__get16be(s) != 1) { stbi__rewind( s ); return 0; } stbi__skip(s, 6); channelCount = stbi__get16be(s); if (channelCount < 0 || channelCount > 16) { stbi__rewind( s ); return 0; } *y = stbi__get32be(s); *x = stbi__get32be(s); depth = stbi__get16be(s); if (depth != 8 && depth != 16) { stbi__rewind( s ); return 0; } if (stbi__get16be(s) != 3) { stbi__rewind( s ); return 0; } *comp = 4; return 1; } static int stbi__psd_is16(stbi__context *s) { int channelCount, depth; if (stbi__get32be(s) != 0x38425053) { stbi__rewind( s ); return 0; } if (stbi__get16be(s) != 1) { stbi__rewind( s ); return 0; } stbi__skip(s, 6); channelCount = stbi__get16be(s); if (channelCount < 0 || channelCount > 16) { stbi__rewind( s ); return 0; } STBI_NOTUSED(stbi__get32be(s)); STBI_NOTUSED(stbi__get32be(s)); depth = stbi__get16be(s); if (depth != 16) { stbi__rewind( s ); return 0; } return 1; } #endif #ifndef STBI_NO_PIC static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp) { int act_comp=0,num_packets=0,chained,dummy; stbi__pic_packet packets[10]; if (!x) x = &dummy; if (!y) y = &dummy; if (!comp) comp = &dummy; if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) { stbi__rewind(s); return 0; } stbi__skip(s, 88); *x = stbi__get16be(s); *y = stbi__get16be(s); if (stbi__at_eof(s)) { stbi__rewind( s); return 0; } if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) { stbi__rewind( s ); return 0; } stbi__skip(s, 8); do { stbi__pic_packet *packet; if (num_packets==sizeof(packets)/sizeof(packets[0])) return 0; packet = &packets[num_packets++]; chained = stbi__get8(s); packet->size = stbi__get8(s); packet->type = stbi__get8(s); packet->channel = stbi__get8(s); act_comp |= packet->channel; if (stbi__at_eof(s)) { stbi__rewind( s ); return 0; } if (packet->size != 8) { stbi__rewind( s ); return 0; } } while (chained); *comp = (act_comp & 0x10 ? 4 : 3); return 1; } #endif // ************************************************************************************************* // Portable Gray Map and Portable Pixel Map loader // by Ken Miller // // PGM: http://netpbm.sourceforge.net/doc/pgm.html // PPM: http://netpbm.sourceforge.net/doc/ppm.html // // Known limitations: // Does not support comments in the header section // Does not support ASCII image data (formats P2 and P3) #ifndef STBI_NO_PNM static int stbi__pnm_test(stbi__context *s) { char p, t; p = (char) stbi__get8(s); t = (char) stbi__get8(s); if (p != 'P' || (t != '5' && t != '6')) { stbi__rewind( s ); return 0; } return 1; } static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) { stbi_uc *out; STBI_NOTUSED(ri); ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n); if (ri->bits_per_channel == 0) return 0; if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); *x = s->img_x; *y = s->img_y; if (comp) *comp = s->img_n; if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0)) return stbi__errpuc("too large", "PNM too large"); out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0); if (!out) return stbi__errpuc("outofmem", "Out of memory"); if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) { STBI_FREE(out); return stbi__errpuc("bad PNM", "PNM file truncated"); } if (req_comp && req_comp != s->img_n) { if (ri->bits_per_channel == 16) { out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y); } else { out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); } if (out == NULL) return out; // stbi__convert_format frees input on failure } return out; } static int stbi__pnm_isspace(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; } static void stbi__pnm_skip_whitespace(stbi__context *s, char *c) { for (;;) { while (!stbi__at_eof(s) && stbi__pnm_isspace(*c)) *c = (char) stbi__get8(s); if (stbi__at_eof(s) || *c != '#') break; while (!stbi__at_eof(s) && *c != '\n' && *c != '\r' ) *c = (char) stbi__get8(s); } } static int stbi__pnm_isdigit(char c) { return c >= '0' && c <= '9'; } static int stbi__pnm_getinteger(stbi__context *s, char *c) { int value = 0; while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { value = value*10 + (*c - '0'); *c = (char) stbi__get8(s); if((value > 214748364) || (value == 214748364 && *c > '7')) return stbi__err("integer parse overflow", "Parsing an integer in the PPM header overflowed a 32-bit int"); } return value; } static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp) { int maxv, dummy; char c, p, t; if (!x) x = &dummy; if (!y) y = &dummy; if (!comp) comp = &dummy; stbi__rewind(s); // Get identifier p = (char) stbi__get8(s); t = (char) stbi__get8(s); if (p != 'P' || (t != '5' && t != '6')) { stbi__rewind(s); return 0; } *comp = (t == '6') ? 3 : 1; // '5' is 1-component .pgm; '6' is 3-component .ppm c = (char) stbi__get8(s); stbi__pnm_skip_whitespace(s, &c); *x = stbi__pnm_getinteger(s, &c); // read width if(*x == 0) return stbi__err("invalid width", "PPM image header had zero or overflowing width"); stbi__pnm_skip_whitespace(s, &c); *y = stbi__pnm_getinteger(s, &c); // read height if (*y == 0) return stbi__err("invalid width", "PPM image header had zero or overflowing width"); stbi__pnm_skip_whitespace(s, &c); maxv = stbi__pnm_getinteger(s, &c); // read max value if (maxv > 65535) return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images"); else if (maxv > 255) return 16; else return 8; } static int stbi__pnm_is16(stbi__context *s) { if (stbi__pnm_info(s, NULL, NULL, NULL) == 16) return 1; return 0; } #endif static int stbi__info_main(stbi__context *s, int *x, int *y, int *comp) { #ifndef STBI_NO_JPEG if (stbi__jpeg_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_PNG if (stbi__png_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_GIF if (stbi__gif_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_BMP if (stbi__bmp_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_PSD if (stbi__psd_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_PIC if (stbi__pic_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_PNM if (stbi__pnm_info(s, x, y, comp)) return 1; #endif #ifndef STBI_NO_HDR if (stbi__hdr_info(s, x, y, comp)) return 1; #endif // test tga last because it's a crappy test! #ifndef STBI_NO_TGA if (stbi__tga_info(s, x, y, comp)) return 1; #endif return stbi__err("unknown image type", "Image not of any known type, or corrupt"); } static int stbi__is_16_main(stbi__context *s) { #ifndef STBI_NO_PNG if (stbi__png_is16(s)) return 1; #endif #ifndef STBI_NO_PSD if (stbi__psd_is16(s)) return 1; #endif #ifndef STBI_NO_PNM if (stbi__pnm_is16(s)) return 1; #endif return 0; } #ifndef STBI_NO_STDIO STBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp) { FILE *f = stbi__fopen(filename, "rb"); int result; if (!f) return stbi__err("can't fopen", "Unable to open file"); result = stbi_info_from_file(f, x, y, comp); fclose(f); return result; } STBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) { int r; stbi__context s; long pos = ftell(f); stbi__start_file(&s, f); r = stbi__info_main(&s,x,y,comp); fseek(f,pos,SEEK_SET); return r; } STBIDEF int stbi_is_16_bit(char const *filename) { FILE *f = stbi__fopen(filename, "rb"); int result; if (!f) return stbi__err("can't fopen", "Unable to open file"); result = stbi_is_16_bit_from_file(f); fclose(f); return result; } STBIDEF int stbi_is_16_bit_from_file(FILE *f) { int r; stbi__context s; long pos = ftell(f); stbi__start_file(&s, f); r = stbi__is_16_main(&s); fseek(f,pos,SEEK_SET); return r; } #endif // !STBI_NO_STDIO STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) { stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__info_main(&s,x,y,comp); } STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) { stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); return stbi__info_main(&s,x,y,comp); } STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len) { stbi__context s; stbi__start_mem(&s,buffer,len); return stbi__is_16_main(&s); } STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user) { stbi__context s; stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); return stbi__is_16_main(&s); } #endif // STB_IMAGE_IMPLEMENTATION /* revision history: 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs 2.19 (2018-02-11) fix warning 2.18 (2018-01-30) fix warnings 2.17 (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug 1-bit BMP *_is_16_bit api avoid warnings 2.16 (2017-07-23) all functions have 16-bit variants; STBI_NO_STDIO works again; compilation fixes; fix rounding in unpremultiply; optimize vertical flip; disable raw_len validation; documentation fixes 2.15 (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode; warning fixes; disable run-time SSE detection on gcc; uniform handling of optional "return" values; thread-safe initialization of zlib tables 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs 2.13 (2016-11-29) add 16-bit API, only supported for PNG right now 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes 2.11 (2016-04-02) allocate large structures on the stack remove white matting for transparent PSD fix reported channel count for PNG & BMP re-enable SSE2 in non-gcc 64-bit support RGB-formatted JPEG read 16-bit PNGs (only as 8-bit) 2.10 (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED 2.09 (2016-01-16) allow comments in PNM files 16-bit-per-pixel TGA (not bit-per-component) info() for TGA could break due to .hdr handling info() for BMP to shares code instead of sloppy parse can use STBI_REALLOC_SIZED if allocator doesn't support realloc code cleanup 2.08 (2015-09-13) fix to 2.07 cleanup, reading RGB PSD as RGBA 2.07 (2015-09-13) fix compiler warnings partial animated GIF support limited 16-bpc PSD support #ifdef unused functions bug with < 92 byte PIC,PNM,HDR,TGA 2.06 (2015-04-19) fix bug where PSD returns wrong '*comp' value 2.05 (2015-04-19) fix bug in progressive JPEG handling, fix warning 2.04 (2015-04-15) try to re-enable SIMD on MinGW 64-bit 2.03 (2015-04-12) extra corruption checking (mmozeiko) stbi_set_flip_vertically_on_load (nguillemot) fix NEON support; fix mingw support 2.02 (2015-01-19) fix incorrect assert, fix warning 2.01 (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit without -msse2 2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG 2.00 (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg) progressive JPEG (stb) PGM/PPM support (Ken Miller) STBI_MALLOC,STBI_REALLOC,STBI_FREE GIF bugfix -- seemingly never worked STBI_NO_*, STBI_ONLY_* 1.48 (2014-12-14) fix incorrectly-named assert() 1.47 (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar Cornut & stb) optimize PNG (ryg) fix bug in interlaced PNG with user-specified channel count (stb) 1.46 (2014-08-26) fix broken tRNS chunk (colorkey-style transparency) in non-paletted PNG 1.45 (2014-08-16) fix MSVC-ARM internal compiler error by wrapping malloc 1.44 (2014-08-07) various warning fixes from Ronny Chevalier 1.43 (2014-07-15) fix MSVC-only compiler problem in code changed in 1.42 1.42 (2014-07-09) don't define _CRT_SECURE_NO_WARNINGS (affects user code) fixes to stbi__cleanup_jpeg path added STBI_ASSERT to avoid requiring assert.h 1.41 (2014-06-25) fix search&replace from 1.36 that messed up comments/error messages 1.40 (2014-06-22) fix gcc struct-initialization warning 1.39 (2014-06-15) fix to TGA optimization when req_comp != number of components in TGA; fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my test suite) add support for BMP version 5 (more ignored fields) 1.38 (2014-06-06) suppress MSVC warnings on integer casts truncating values fix accidental rename of 'skip' field of I/O 1.37 (2014-06-04) remove duplicate typedef 1.36 (2014-06-03) convert to header file single-file library if de-iphone isn't set, load iphone images color-swapped instead of returning NULL 1.35 (2014-05-27) various warnings fix broken STBI_SIMD path fix bug where stbi_load_from_file no longer left file pointer in correct place fix broken non-easy path for 32-bit BMP (possibly never used) TGA optimization by Arseny Kapoulkine 1.34 (unknown) use STBI_NOTUSED in stbi__resample_row_generic(), fix one more leak in tga failure case 1.33 (2011-07-14) make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements 1.32 (2011-07-13) support for "info" function for all supported filetypes (SpartanJ) 1.31 (2011-06-20) a few more leak fixes, bug in PNG handling (SpartanJ) 1.30 (2011-06-11) added ability to load files via callbacks to accomidate custom input streams (Ben Wenger) removed deprecated format-specific test/load functions removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha) fix inefficiency in decoding 32-bit BMP (David Woo) 1.29 (2010-08-16) various warning fixes from Aurelien Pocheville 1.28 (2010-08-01) fix bug in GIF palette transparency (SpartanJ) 1.27 (2010-08-01) cast-to-stbi_uc to fix warnings 1.26 (2010-07-24) fix bug in file buffering for PNG reported by SpartanJ 1.25 (2010-07-17) refix trans_data warning (Won Chun) 1.24 (2010-07-12) perf improvements reading from files on platforms with lock-heavy fgetc() minor perf improvements for jpeg deprecated type-specific functions so we'll get feedback if they're needed attempt to fix trans_data warning (Won Chun) 1.23 fixed bug in iPhone support 1.22 (2010-07-10) removed image *writing* support stbi_info support from Jetro Lauha GIF support from Jean-Marc Lienher iPhone PNG-extensions from James Brown warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. Janez (U+017D)emva) 1.21 fix use of 'stbi_uc' in header (reported by jon blow) 1.20 added support for Softimage PIC, by Tom Seddon 1.19 bug in interlaced PNG corruption check (found by ryg) 1.18 (2008-08-02) fix a threading bug (local mutable static) 1.17 support interlaced PNG 1.16 major bugfix - stbi__convert_format converted one too many pixels 1.15 initialize some fields for thread safety 1.14 fix threadsafe conversion bug header-file-only version (#define STBI_HEADER_FILE_ONLY before including) 1.13 threadsafe 1.12 const qualifiers in the API 1.11 Support installable IDCT, colorspace conversion routines 1.10 Fixes for 64-bit (don't use "unsigned long") optimized upsampling by Fabian "ryg" Giesen 1.09 Fix format-conversion for PSD code (bad global variables!) 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz 1.07 attempt to fix C++ warning/errors again 1.06 attempt to fix C++ warning/errors again 1.05 fix TGA loading to return correct *comp and use good luminance calc 1.04 default float alpha is 1, not 255; use 'void *' for stbi_image_free 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR 1.02 support for (subset of) HDR files, float interface for preferred access to them 1.01 fix bug: possible bug in handling right-side up bmps... not sure fix bug: the stbi__bmp_load() and stbi__tga_load() functions didn't work at all 1.00 interface to zlib that skips zlib header 0.99 correct handling of alpha in palette 0.98 TGA loader by lonesock; dynamically add loaders (untested) 0.97 jpeg errors on too large a file; also catch another malloc failure 0.96 fix detection of invalid v value - particleman@mollyrocket forum 0.95 during header scan, seek to markers in case of padding 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same 0.93 handle jpegtran output; verbose errors 0.92 read 4,8,16,24,32-bit BMP files of several formats 0.91 output 24-bit Windows 3.0 BMP files 0.90 fix a few more warnings; bump version number to approach 1.0 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd 0.60 fix compiling as c++ 0.59 fix warnings: merge Dave Moore's -Wall fixes 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available 0.56 fix bug: zlib uncompressed mode len vs. nlen 0.55 fix bug: restart_interval not initialized to 0 0.54 allow NULL for 'int *comp' 0.53 fix bug in png 3->4; speedup png decoding 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments 0.51 obey req_comp requests, 1-component jpegs return as 1-component, on 'test' only check type, not whether we support this variant 0.50 (2006-11-19) first released version */ /* ------------------------------------------------------------------------------ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2017 Sean Barrett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */ ================================================ FILE: src/samples/ui.c ================================================ #include "single_file_lib/rt/rt.h" #define ui_implementation #include "single_file_lib/ui/ui.h" ================================================ FILE: src/tools/amalgamate.c ================================================ #include #include #include #include #include #include #include "dirent.h" #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #pragma GCC diagnostic ignored "-Wfour-char-constants" #pragma GCC diagnostic ignored "-Wmissing-field-initializers" #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wmissing-noreturn" #pragma GCC diagnostic ignored "-Wdouble-promotion" #pragma GCC diagnostic ignored "-Wcast-align" #pragma GCC diagnostic ignored "-Waddress-of-packed-member" #pragma GCC diagnostic ignored "-Wused-but-marked-unused" // because in debug only #endif #define null NULL #ifndef countof #define rt_countof(a) ((int)(sizeof(a) / sizeof((a)[0]))) #endif #define strequ(s1, s2) (strcmp((s1), (s2)) == 0) static const char* exe; static const char* name; static int32_t strlen_name; static char inc[256]; static char src[256]; static char mem[1024 * 1023]; static char* brk = mem; #define rt_fatal_if(x, ...) do { \ if (x) { \ fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); \ fprintf(stderr, "" __VA_ARGS__); \ fprintf(stderr, "\nFATAL\n"); \ exit(1); \ } \ } while (0) static const char* basename(const char* filename) { const char* s = strrchr(filename, '\\'); if (s == null) { s = strrchr(filename, '/'); } return s != null ? s + 1 : filename; } static char* dup(const char* s) { // strdup() like to avoid leaks reporting int n = (int)strlen(s) + 1; rt_fatal_if(brk + n > mem + sizeof(mem), "out of memory"); char* c = (char*)memcpy(brk, s, (size_t)n); brk += n; return c; } static char* concat(const char* s1, const char* s2) { int n1 = (int)strlen(s1); int n2 = (int)strlen(s2); rt_fatal_if(brk + n1 + n2 + 1 > mem + sizeof(mem), "out of memory"); char* c = brk; memcpy((char*)memcpy(brk, s1, (size_t)n1) + n1, s2, (size_t)n2 + 1); brk += n1 + n2 + 1; return c; } static bool ends_with(const char* s1, const char* s2) { int32_t n1 = (int)strlen(s1); int32_t n2 = (int)strlen(s2); return n1 >= n2 && strequ(s1 + n1 - n2, s2); } typedef struct { const char* a[1024]; int32_t n; } set_t; static set_t files; static set_t includes; static bool set_has(set_t* set, const char* s) { for (int32_t i = 0; i < set->n; i++) { if (strequ(set->a[i], s)) { return true; } } return false; } static void set_add(set_t* set, const char* s) { assert(!set_has(set, s)); if (!set_has(set, s)) { rt_fatal_if(set->n == rt_countof(set->a), "too many files"); set->a[set->n] = dup(s); set->n++; } } static int32_t usage(void) { fprintf(stderr, "\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "%s \n", exe); fprintf(stderr, "Assumes src/name and inc/name folders exist\n"); fprintf(stderr, "and inc// contain .h\n"); fprintf(stderr, "\n"); return 1; } static void tail_trim(char* s) { char* p = s + strlen(s) - 1; while (p >= s && *p < 0x20) { *p-- = 0x00; } } static void divider(const char* fn) { char underscores[40] = {0}; memset(underscores, '_', rt_countof(underscores) - 1); int32_t i = (int)(74 - strlen(fn)) / 2; int32_t j = (int)(74 - i - (int)strlen(fn)); printf("// %.*s %s %.*s\n\n", i, underscores, fn, j, underscores); } static const char* include(char* s) { char fn[256] = {0}; const char* include = "#include\x20\""; if (strstr(s, include) == s) { s += strlen(include); const char* q = strchr(s, '"'); if (q != null) { snprintf(fn, rt_countof(fn) - 1, "%.*s", (int)(q - s), s); return strstr(fn, name) == fn && fn[strlen_name] == '/' ? dup(fn + strlen_name + 1) : null; } } return null; } static bool already_included(const char* s) { const char* include = "#include\x20\""; if (strstr(s, include) != s) { return false; } if (set_has(&includes, s)) { return true; } set_add(&includes, s); return false; } static bool ignore(const char* s) { return strequ(s, "#pragma once") || already_included(s); } static void parse(const char* fn) { FILE* f = fopen(fn, "r"); rt_fatal_if(f == null, "file not found: `%s`", fn); static char line[16 * 1024]; bool first = true; while (fgets(line, rt_countof(line) - 1, f) != null) { tail_trim(line); const char* in = include(line); if (in != null) { if (!set_has(&files, in)) { set_add(&files, in); parse(concat(inc, concat("/", in))); } } else if (ends_with(fn, ".c") || !ignore(line)) { if (first && line[0] != 0) { divider(fn + 5 + strlen_name); first = false; } printf("%s\n", line); } } fclose(f); } static void definition(void) { printf("#ifndef %s_definition\n", name); printf("#define %s_definition\n", name); printf("\n"); parse(concat(inc, concat("/", concat(name, ".h")))); printf("\n"); printf("#endif // %s_definition\n", name); const char* name_h = concat(name, ".h"); // because name.h is fully processed do not include it again: if (!set_has(&files, name_h)) { set_add(&files, concat(name, ".h")); } } static void implementation(void) { printf("\n"); printf("#ifdef %s_implementation\n", name); DIR* d = opendir(src); rt_fatal_if(d == null, "folder not found: `%s`", src); struct dirent* e = readdir(d); while (e != null) { if (ends_with(e->d_name, ".c")) { parse(concat(src, concat("/", e->d_name))); } e = readdir(d); } rt_fatal_if(closedir(d) != 0); printf("\n"); printf("#endif // %s_implementation\n", name); printf("\n"); } int main(int argc, const char* argv[]) { exe = basename(argv[0]); if (argc < 2) { exit(usage()); } name = argv[1]; strlen_name = (int)strlen(name); snprintf(inc, rt_countof(inc) - 1, "inc/%s", name); snprintf(src, rt_countof(inc) - 1, "src/%s", name); definition(); implementation(); return 0; } ================================================ FILE: src/tools/dirent.c ================================================ #include "dirent.h" #include #include #include #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #endif #define null ((void*)0) #ifndef countof #define rt_countof(a) ((int)(sizeof(a) / sizeof((a)[0]))) #endif typedef struct dir_s { HANDLE handle; WIN32_FIND_DATAA find; // 320 bytes struct dirent entry; } dir_t; DIR *opendir(const char *dirname) { dir_t *d = calloc(1, sizeof(dir_t)); if (d != null) { char spec[NAME_MAX + 2]; // extra room for "\*" suffix snprintf(spec, rt_countof(spec) - 1, "%s\\*", dirname); spec[rt_countof(spec) - 1] = 0; d->handle = FindFirstFileA(spec, &d->find); if (d->handle == INVALID_HANDLE_VALUE) { free(d); d = null; } } return (DIR*)d; } struct dirent* readdir(DIR* dir) { dir_t* d = (dir_t*)dir; struct dirent* de = null; if (d->handle != INVALID_HANDLE_VALUE && FindNextFileA(d->handle, &d->find)) { enum { n = rt_countof(d->entry.d_name) }; strncpy(d->entry.d_name, d->find.cFileName, n - 1); d->entry.d_name[n - 1] = 0x00; // Ensure zero termination de = &d->entry; } return de; } int closedir(DIR* dir) { errno_t e = 0; dir_t *d = (dir_t*)dir; if (d->handle != INVALID_HANDLE_VALUE) { if (!FindClose(d->handle)) { e = EINVAL; } } if (e == 0) { free(d); } return e; } ================================================ FILE: src/tools/dirent.h ================================================ #pragma once #ifdef __cplusplus extern "C" { #endif // https://pubs.opengroup.org/onlinepubs/009604599/basedefs/dirent.h.html #define NAME_MAX 260 typedef struct DIR DIR; struct dirent { char d_name[NAME_MAX]; }; DIR* opendir(const char *dirname); struct dirent *readdir(DIR *dirp); int closedir(DIR* dir); #ifdef __cplusplus } // extern "C" #endif ================================================ FILE: src/tools/version.c ================================================ #include #include #include #include #include #ifdef _MSC_FULL_VER #define rt_countof(a) _countof(a) #define popen(c, m) _popen(c, m) #define pclose(f) _pclose(f) #endif #if defined(__GNUC__) || defined(__clang__) // TODO: remove and fix code #pragma GCC diagnostic ignored "-Wunsafe-buffer-usage" #pragma GCC diagnostic ignored "-Wdeclaration-after-statement" #endif enum { max_command_output = 16 * 1024 }; static errno_t run_command(const char* command, char* output, size_t max_output) { FILE* f = popen(command, "r"); errno_t r = f != NULL ? 1 : -1; size_t total = 0; while (r > 0) { size_t seen = fread(output + total, 1, max_output - total, f); if (seen <= 0) { r = 0; } else if (total + seen + 1 >= max_output) { r = -1; } else { total += seen; } } if (f != NULL) { pclose(f); } if (total < max_output) { output[total] = 0; } return r; } int main(void) { printf("// Automatically generated by version.c (project prebuild).\n"); printf("// DO NOT EDIT.\n"); printf("\n"); static char hash[max_command_output]; strcpy(hash, "BADF00D"); if (run_command("git rev-parse --short HEAD", hash, rt_countof(hash)) != 0) { fprintf(stderr, "Failed to get git hash.\n"); return 1; } if (hash[strlen(hash) - 1] == '\n') { hash[strlen(hash) - 1] = 0; } time_t t = time(NULL); struct tm* utc = gmtime(&t); static char tag[max_command_output]; strcpy(tag, "C0DEFEED"); if (run_command("git describe --tags HEAD 2>nul", tag, rt_countof(tag)) != 0) { fprintf(stderr, "Failed to get git tag.\n"); return 1; } if (tag[strlen(tag) - 1] == '\n') { tag[strlen(tag) - 1] = 0; } printf("#pragma once\n"); printf("#define version_hash \"%s\"\n", hash); printf("#define version_tag \"%s\"\n", tag); printf("#define version_yy (%02d)\n", utc->tm_year % 100); printf("#define version_mm (%02d)\n", utc->tm_mon + 1); printf("#define version_dd (%02d)\n", utc->tm_mday); printf("#define version_hh (%02d)\n", utc->tm_hour); printf("#define version_str \"%02d.%02d.%02d.%02dUTC %s\"\n", utc->tm_year % 100, utc->tm_mon + 1, utc->tm_mday, utc->tm_hour, hash); printf("#define version_int32 (0x%02d%02d%02d%02d)\n", utc->tm_year % 100, utc->tm_mon + 1, utc->tm_mday, utc->tm_hour); printf("#define version_hash_int64 (0x%sLL)\n", hash); return 0; } ================================================ FILE: src/ui/attic/ui_theme.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "ut/ut.h" #include "ui/ui.h" #include "ui/ut_win32.h" #pragma push_macro("ux_theme_reg_cv") #pragma push_macro("ux_theme_reg_default_colors") #define ux_theme_reg_cv "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\" #define ux_theme_reg_default_colors ux_theme_reg_cv "Themes\\DefaultColors\\" static HMODULE ui_theme_ux_theme(void) { static HMODULE ux_theme; if (ux_theme == null) { ux_theme = GetModuleHandleA("uxtheme.dll"); } if (ux_theme == null) { ux_theme = (HMODULE)ut_loader.open("uxtheme.dll", ut_loader.local); } not_null(ux_theme); return ux_theme; } static errno_t ui_theme_reg_get_uint32(HKEY root, const char* path, const char* key, DWORD *v) { *v = 0; DWORD type = REG_DWORD; DWORD light_theme = 0; DWORD bytes = sizeof(light_theme); errno_t r = RegGetValueA(root, path, key, RRF_RT_DWORD, &type, v, &bytes); if (r != 0) { traceln("RegGetValueA(%s\\%s) failed %s", path, key, ut_str.error(r)); } return r; } static errno_t ui_theme_reg_get_bin(HKEY root, const char* path, const char* key, void *data, uint32_t *bytes) { memset(data, 0, *bytes); DWORD type = REG_BINARY; DWORD n = *bytes; errno_t r = RegGetValueA(root, path, key, RRF_RT_REG_BINARY, &type, data, &n); if (r == 0) { *bytes = n; } if (r != 0) { traceln("RegGetValueA(%s\\%s) failed %s", path, key, ut_str.error(r)); } return r; } typedef struct { int32_t id; const char* name; ui_color_t dark; ui_color_t light; } ui_theme_color_map_t; // ActiveTitle : dark 0x006E0037 light 0x00D1B499 // ButtonFace : dark 0x00000000 light 0x00F0F0F0 // ButtonText : dark 0x00FFFFFF light 0x00000000 // GrayText : dark 0x003FF23F light 0x006D6D6D // Hilight : dark 0x00FFEB1A light 0x00D77800 // HilightText : dark 0x00000000 light 0x00FFFFFF // HotTrackingColor : dark 0x0000FFFF light 0x00CC6600 // InactiveTitle : dark 0x002F0000 light 0x00DBCDBF // InactiveTitleText : dark 0x00FFFFFF light 0x00000000 // MenuHilight : dark 0x00800080 light 0x00FF9933 // TitleText : dark 0x00FFFFFF light 0x00000000 // Window : dark 0x00000000 light 0x00FFFFFF // WindowText : dark 0x00FFFFFF light 0x00000000 static int32_t ui_theme_dark = -1; // -1 unknown static ui_theme_color_map_t ui_theme_colors[13]; static void ui_theme_init_colors(void) { // this is empirically determined for the dark theme, and from Win10 // the registry in standard light theme. // Because all of the: // HKEY_CURRENT_USER\Control Panel\Desktop\Colors // HKEY_CURRENT_USER\Control Panel\Colors // HKEY_USERS\.DEFAULT\Control Panel\Colors // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\DefaultColors\Standard // Have different colors even for Light mode and none for the Dark Mode simply // keep it hardcoded for time being: ui_theme_colors[ 0] = (ui_theme_color_map_t){ .id = ui.colors.active_title, .name = "ActiveTitle" ,.dark = 0x00000000, .light = 0x00D1B499 }; ui_theme_colors[ 1] = (ui_theme_color_map_t){ .id = ui.colors.button_face, .name = "ButtonFace" ,.dark = 0x00333333, .light = 0x00F0F0F0 }; ui_theme_colors[ 2] = (ui_theme_color_map_t){ .id = ui.colors.button_text, .name = "ButtonText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }; ui_theme_colors[ 3] = (ui_theme_color_map_t){ .id = ui.colors.gray_text, .name = "GrayText" ,.dark = 0x00666666, .light = 0x006D6D6D }; ui_theme_colors[ 4] = (ui_theme_color_map_t){ .id = ui.colors.highlight, .name = "Hilight" ,.dark = 0x00626262, .light = 0x00D77800 }; ui_theme_colors[ 5] = (ui_theme_color_map_t){ .id = ui.colors.highlight_text, .name = "HilightText" ,.dark = 0x00000000, .light = 0x00FFFFFF }; ui_theme_colors[ 6] = (ui_theme_color_map_t){ .id = ui.colors.hot_tracking_color, .name = "HotTrackingColor" ,.dark = 0x004D8DFA, .light = 0x00CC6600 }; ui_theme_colors[ 7] = (ui_theme_color_map_t){ .id = ui.colors.inactive_title, .name = "InactiveTitle" ,.dark = 0x002B2B2B, .light = 0x00DBCDBF }; ui_theme_colors[ 8] = (ui_theme_color_map_t){ .id = ui.colors.inactive_title_text, .name = "InactiveTitleText",.dark = 0x00969696, .light = 0x00000000 }; ui_theme_colors[ 9] = (ui_theme_color_map_t){ .id = ui.colors.menu_highlight, .name = "MenuHilight" ,.dark = 0x00002642, .light = 0x00FF9933 }; ui_theme_colors[10] = (ui_theme_color_map_t){ .id = ui.colors.title_text, .name = "TitleText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }; ui_theme_colors[11] = (ui_theme_color_map_t){ .id = ui.colors.window, .name = "Window" ,.dark = 0x00000000, .light = 0x00FFFFFF }; ui_theme_colors[12] = (ui_theme_color_map_t){ .id = ui.colors.window_text, .name = "WindowText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }; #ifdef UX_THEME_READ_COLORS_FROM_REGISTRY // when the dust of Win11 settles. errno_t r = 0; const char* dark = ux_theme_reg_default_colors "HighContrast"; const char* light = ux_theme_reg_default_colors "Standard"; for (int32_t i = 0; i < countof(ui_theme_colors); i++) { const char* name = ui_theme_colors[i].name; DWORD dc = 0; DWORD lc = 0; r = ui_theme_reg_get_uint32(HKEY_LOCAL_MACHINE, dark, name, &dc); if (r == 0) { ui_theme_colors[i].dark = dc; } ui_theme_reg_get_uint32(HKEY_LOCAL_MACHINE, light, name, &lc); if (r == 0) { ui_theme_colors[i].light = lc; } // traceln("%-20s: dark %08X light %08X", name, dc, lc); } #endif } static bool ui_theme_use_light_theme(const char* key) { const char* personalize = ux_theme_reg_cv "Themes\\Personalize"; DWORD light_theme = 0; ui_theme_reg_get_uint32(HKEY_CURRENT_USER, personalize, key, &light_theme); return light_theme != 0; } static bool ui_theme_are_apps_light(void) { return ui_theme_use_light_theme("AppsUseLightTheme"); } static bool ui_theme_is_system_light(void) { return ui_theme_use_light_theme("SystemUsesLightTheme"); } static ui_color_t ui_theme_get_color(int32_t color_id) { swear(0 <= color_id && color_id < countof(ui_theme_colors)); static bool initialized; if (!initialized) { ui_theme_init_colors(); initialized = true; } if (ui_theme_dark < 0) { bool are_apps_light = ui_theme.are_apps_light(); bool is_system_light = ui_theme.is_system_light(); bool allowed = ui_theme.is_dark_mode_allowed_for_app(); bool dark = ui_theme.should_apps_use_dark_mode(); ui_theme_dark = !is_system_light && !are_apps_light && allowed && dark; if (ui_theme_dark) { ui_theme.set_preferred_app_mode(ui_theme.mode_force_dark); } } return ui_theme_dark ? ui_theme_colors[color_id].dark : ui_theme_colors[color_id].light; } static void ui_theme_refresh(void* window) { ui_theme_dark = -1; BOOL dark_mode = !ui_theme.are_apps_light(); static const DWORD DWMWA_USE_IMMERSIVE_DARK_MODE = 20; /* 20 == DWMWA_USE_IMMERSIVE_DARK_MODE in Windows 11 SDK. This value was undocumented for Windows 10 versions 2004 and later, supported for Windows 11 Build 22000 and later. */ errno_t r = DwmSetWindowAttribute((HWND)window, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark_mode, sizeof(dark_mode)); if (r != 0) { traceln("DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) " "failed %s", ut_str.error(r)); } ui_app.layout(); } static bool ui_theme_is_dark_mode_allowed_for_app(void) { typedef BOOL (__stdcall *IsDarkModeAllowedForApp_t)(void); IsDarkModeAllowedForApp_t IsDarkModeAllowedForApp = (IsDarkModeAllowedForApp_t) (void*)GetProcAddress(ui_theme_ux_theme(), MAKEINTRESOURCE(136)); if (IsDarkModeAllowedForApp != null) { return IsDarkModeAllowedForApp(); } return false; } static bool ui_theme_should_apps_use_dark_mode(void) { typedef BOOL (__stdcall *ShouldAppsUseDarkMode_t)(void); ShouldAppsUseDarkMode_t ShouldAppsUseDarkMode = (ShouldAppsUseDarkMode_t) (void*)GetProcAddress(ui_theme_ux_theme(), MAKEINTRESOURCE(132)); if (ShouldAppsUseDarkMode != null) { return ShouldAppsUseDarkMode(); } return false; } static void ui_theme_set_preferred_app_mode(int32_t mode) { typedef BOOL (__stdcall *SetPreferredAppMode_t)(bool allow); SetPreferredAppMode_t SetPreferredAppMode = (SetPreferredAppMode_t) (void*)GetProcAddress(ui_theme_ux_theme(), MAKEINTRESOURCE(135)); if (SetPreferredAppMode != null) { errno_t r = b2e(SetPreferredAppMode(mode)); // fails on Windows 10 with: ERROR_RESOURCE_NAME_NOT_FOUND (1814) if (r != 0 && r != ERROR_RESOURCE_NAME_NOT_FOUND) { // ignore traceln("SetPreferredAppMode(%d) failed %s", mode, ut_str.error(r)); } } } static ui_color_t ui_theme_explorer_accents[] = { 0x00FFD8A6, 0x00EDB976, 0x00E39C42, 0x00D77800, 0x009E5A00, 0x00754200, 0x00422600, 0x00981788 }; static void ui_theme_test(void) { DWORD window = GetSysColor(COLOR_WINDOW); DWORD text = GetSysColor(COLOR_WINDOWTEXT); traceln("COLOR_WINDOW: 0x%08X COLOR_WINDOWTEXT: 0x%08X", window, text); DWORD colors[8] = {0}; uint32_t bytes = sizeof(colors); ui_theme_reg_get_bin(HKEY_CURRENT_USER, ux_theme_reg_cv "Explorer\\Accent", "AccentPalette", &colors, &bytes); ui_theme_init_colors(); HMODULE ux_theme = ui_theme_ux_theme(); traceln("ux_theme: %p", ux_theme); bool are_apps_light = ui_theme.are_apps_light(); bool is_system_light = ui_theme.is_system_light(); bool dark = ui_theme.should_apps_use_dark_mode(); bool allowed = ui_theme.is_dark_mode_allowed_for_app(); traceln("light is_system_light(): %d are_apps_light(): %d " "should_apps_use_dark_mode(): %d " "is_dark_mode_allowed_for_app(): %d", is_system_light, are_apps_light, dark, allowed); if (dark) { ui_theme.set_preferred_app_mode(ui_theme.mode_force_dark); } for (int32_t i = 0; i < countof(ui_theme_colors); i++) { ui_color_t c = ui_theme.get_color(ui_theme_colors[i].id); traceln("%-20s 0x%08X", ui_theme_colors[i].name, ui_color_rgb(c)); } } ui_theme_if ui_theme = { .mode_default = 0, .mode_allow_dark = 1, .mode_force_dark = 2, .mode_force_light = 3, .is_system_light = ui_theme_is_system_light, .are_apps_light = ui_theme_are_apps_light, .should_apps_use_dark_mode = ui_theme_should_apps_use_dark_mode, .is_dark_mode_allowed_for_app = ui_theme_is_dark_mode_allowed_for_app, .set_preferred_app_mode = ui_theme_set_preferred_app_mode, .get_color = ui_theme_get_color, .refresh = ui_theme_refresh, .test = ui_theme_test }; ut_static_init(ui_theme) { ui_theme.test(); } // Experimental Dark: // ActiveTitle : dark 0x00000000 *** // ButtonFace : dark 0x00333333 *** // ButtonText : dark 0x00FFFFFF *** // GrayText : dark 0x00666666 *** // Hilight : dark 0x00626262 *** // HilightText : dark 0x00000000 *** // HotTrackingColor : dark 0x004D8DFA *** 0x00D77800 is light // InactiveTitle : dark 0x002B2B2B *** // InactiveTitleText : dark 0x00969696 *** (alt A1A1A1 or AAAAAA) // MenuHilight : dark 0x00002642 *** // TitleText : dark 0x00FFFFFF *** // Window : dark 0x00000000 *** // WindowText : dark 0x00FFFFFF *** // // Computer\HKEY_CURRENT_USER\Control Panel\Desktop\ // AutoColorization=1 // // Computer\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\DWM // ColorizationColor=0xC43A3A3A // (alpha: C4!) // EnableWindowColorization=0x84 // (bitset of something) // // TODO: these values differ: // // Computer\HKEY_CURRENT_USER\Control Panel\Desktop\Colors // Computer\HKEY_CURRENT_USER\Control Panel\Colors // // TODO: may be relevant too: // HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\DWM\ // absent: // AccentColorInactive // present: // AccentColor ff3a3a3a // ColorizationAfterglow // ColorizationColor // // HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Accent // AccentColorMenu // StartColorMenu // AccentPalette binary 8x4byte colors // // Computer\HKEY_USERS\.DEFAULT\Control Panel\Colors // a lot of colors // https://superuser.com/questions/1245923/registry-keys-to-change-personalization-settings/1395560#1395560 // Active Window Border // [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent] // "AccentColorMenu"=dword:ffb16300 // Active Window Title Bar // [HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM] // "AccentColor"=dword:ffb16300 // Inactive Window Title Bar // [HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM] // "AccentColorInactive"=dword:ffb16300 // // // // // // // // // // // #pragma pop_macro("ux_theme_reg_cv") #pragma pop_macro("ux_theme_reg_default_colors") ================================================ FILE: src/ui/ui_app.c ================================================ #include "rt/rt.h" #include "ui/ui.h" #include "rt/rt_win32.h" #pragma push_macro("ui_app_window") #pragma push_macro("ui_app_canvas") static bool ui_app_trace_utf16_keyboard_input; #define ui_app_window() ((HWND)ui_app.window) #define ui_app_canvas() ((HDC)ui_app.canvas) static WNDCLASSW ui_app_wc; // window class static NONCLIENTMETRICSW ui_app_ncm = { sizeof(NONCLIENTMETRICSW) }; static MONITORINFO ui_app_mi = {sizeof(MONITORINFO)}; static rt_event_t ui_app_event_quit; static rt_event_t ui_app_event_invalidate; static rt_event_t ui_app_wt; // waitable timer; static rt_work_queue_t ui_app_queue; static uintptr_t ui_app_timer_1s_id; static uintptr_t ui_app_timer_100ms_id; static bool ui_app_layout_dirty; // call layout() before paint static char ui_app_decoded_pressed[16]; // utf8 of last decoded pressed key static char ui_app_decoded_released[16]; // utf8 of last decoded released key static uint16_t ui_app_high_surrogate; typedef void (*ui_app_animate_function_t)(int32_t step); static struct { ui_app_animate_function_t f; int32_t count; int32_t step; ui_timer_t timer; } ui_app_animate; // Animation timer is Windows minimum of 10ms, but in reality the timer // messages are far from isochronous and more likely to arrive at 16 or // 32ms intervals and can be delayed. static void ui_app_post_message(int32_t m, int64_t wp, int64_t lp) { rt_fatal_win32err(PostMessageA(ui_app_window(), (UINT)m, (WPARAM)wp, (LPARAM)lp)); } static void ui_app_update_wt_timeout(void) { fp64_t next_due_at = -1.0; rt_atomics.spinlock_acquire(&ui_app_queue.lock); if (ui_app_queue.head != null) { next_due_at = ui_app_queue.head->when; } rt_atomics.spinlock_release(&ui_app_queue.lock); if (next_due_at >= 0) { static fp64_t last_next_due_at; fp64_t dt = next_due_at - rt_clock.seconds(); if (dt <= 0) { ui_app_post_message(WM_NULL, 0, 0); } else if (last_next_due_at != next_due_at) { // Negative values indicate relative time in 100ns intervals LARGE_INTEGER rt = {0}; // relative negative time rt.QuadPart = (LONGLONG)(-dt * 1.0E+7); rt_swear(rt.QuadPart < 0, "dt: %.6f %lld", dt, rt.QuadPart); rt_fatal_win32err( SetWaitableTimer(ui_app_wt, &rt, 0, null, null, 0) ); } last_next_due_at = next_due_at; } } static void ui_app_post(rt_work_t* w) { if (w->queue == null) { w->queue = &ui_app_queue; } // work item can be reused but only with the same queue rt_assert(w->queue == &ui_app_queue); rt_work_queue.post(w); ui_app_update_wt_timeout(); } static void ui_app_alarm_thread(void* rt_unused(p)) { rt_thread.realtime(); rt_thread.name("ui_app.alarm"); for (;;) { rt_event_t es[] = { ui_app_wt, ui_app_event_quit }; int32_t ix = rt_event.wait_any(rt_countof(es), es); if (ix == 0) { ui_app_post_message(WM_NULL, 0, 0); } else { break; } } } // InvalidateRect() may wait for up to 30 milliseconds // which is unacceptable for video drawing at monitor // refresh rate static void ui_app_redraw_thread(void* rt_unused(p)) { rt_thread.realtime(); rt_thread.name("ui_app.redraw"); for (;;) { rt_event_t es[] = { ui_app_event_invalidate, ui_app_event_quit }; int32_t ix = rt_event.wait_any(rt_countof(es), es); if (ix == 0) { if (ui_app_window() != null) { InvalidateRect(ui_app_window(), null, false); } } else { break; } } } // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown static void ui_app_alt_ctrl_shift(bool down, int64_t key) { if (key == VK_MENU) { ui_app.alt = down; } if (key == VK_CONTROL) { ui_app.ctrl = down; } if (key == VK_SHIFT) { ui_app.shift = down; } } static inline ui_point_t ui_app_point2ui(const POINT* p) { ui_point_t u = { p->x, p->y }; return u; } static inline POINT ui_app_ui2point(const ui_point_t* u) { POINT p = { u->x, u->y }; return p; } static ui_rect_t ui_app_rect2ui(const RECT* r) { ui_rect_t u = { r->left, r->top, r->right - r->left, r->bottom - r->top }; return u; } static RECT ui_app_ui2rect(const ui_rect_t* u) { RECT r = { u->x, u->y, u->x + u->w, u->y + u->h }; return r; } static void ui_app_update_ncm(int32_t dpi) { // Only UTF-16 version supported SystemParametersInfoForDpi rt_fatal_win32err(SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(ui_app_ncm), &ui_app_ncm, 0, (DWORD)dpi)); } static void ui_app_update_monitor_dpi(HMONITOR monitor, ui_dpi_t* dpi) { dpi->monitor_max = 72; for (int32_t mtd = MDT_EFFECTIVE_DPI; mtd <= MDT_RAW_DPI; mtd++) { uint32_t dpi_x = 0; uint32_t dpi_y = 0; // GetDpiForMonitor() may return ERROR_GEN_FAILURE 0x8007001F when // system wakes up from sleep: // ""A device attached to the system is not functioning." // docs say: // "May be used to indicate that the device has stopped responding // or a general failure has occurred on the device. // The device may need to be manually reset." int32_t r = GetDpiForMonitor(monitor, (MONITOR_DPI_TYPE)mtd, &dpi_x, &dpi_y); if (r != 0) { rt_thread.sleep_for(1.0 / 32); // and retry: r = GetDpiForMonitor(monitor, (MONITOR_DPI_TYPE)mtd, &dpi_x, &dpi_y); } if (r == 0) { // EFFECTIVE_DPI 168 168 (with regard of user scaling) // ANGULAR_DPI 247 248 (diagonal) // RAW_DPI 283 284 (horizontal, vertical) // Parallels Desktop 16.5.0 (49183) on macOS Mac Book Air // EFFECTIVE_DPI 192 192 (with regard of user scaling) // ANGULAR_DPI 224 224 (diagonal) // RAW_DPI 72 72 const int32_t max_xy = (int32_t)rt_max(dpi_x, dpi_y); switch (mtd) { case MDT_EFFECTIVE_DPI: dpi->monitor_effective = max_xy; // rt_println("ui_app.dpi.monitor_effective := max(%d,%d)", dpi_x, dpi_y); break; case MDT_ANGULAR_DPI: dpi->monitor_angular = max_xy; // rt_println("ui_app.dpi.monitor_angular := max(%d,%d)", dpi_x, dpi_y); break; case MDT_RAW_DPI: dpi->monitor_raw = max_xy; // rt_println("ui_app.dpi.monitor_raw := max(%d,%d)", dpi_x, dpi_y); break; default: rt_assert(false); } dpi->monitor_max = rt_max(dpi->monitor_max, max_xy); } } // rt_println("ui_app.dpi.monitor_max := %d", dpi->monitor_max); } #ifndef UI_APP_DEBUG static void ui_app_dump_dpi(void) { rt_println("ui_app.dpi.monitor_effective: %d", ui_app.dpi.monitor_effective ); rt_println("ui_app.dpi.monitor_angular : %d", ui_app.dpi.monitor_angular ); rt_println("ui_app.dpi.monitor_raw : %d", ui_app.dpi.monitor_raw ); rt_println("ui_app.dpi.monitor_max : %d", ui_app.dpi.monitor_max ); rt_println("ui_app.dpi.window : %d", ui_app.dpi.window ); rt_println("ui_app.dpi.system : %d", ui_app.dpi.system ); rt_println("ui_app.dpi.process : %d", ui_app.dpi.process ); rt_println("ui_app.mrc : %d,%d %dx%d", ui_app.mrc.x, ui_app.mrc.y, ui_app.mrc.w, ui_app.mrc.h); rt_println("ui_app.wrc : %d,%d %dx%d", ui_app.wrc.x, ui_app.wrc.y, ui_app.wrc.w, ui_app.wrc.h); rt_println("ui_app.crc : %d,%d %dx%d", ui_app.crc.x, ui_app.crc.y, ui_app.crc.w, ui_app.crc.h); rt_println("ui_app.work_area: %d,%d %dx%d", ui_app.work_area.x, ui_app.work_area.y, ui_app.work_area.w, ui_app.work_area.h); int32_t mxt_x = GetSystemMetrics(SM_CXMAXTRACK); int32_t mxt_y = GetSystemMetrics(SM_CYMAXTRACK); rt_println("MAXTRACK: %d, %d", mxt_x, mxt_y); int32_t scr_x = GetSystemMetrics(SM_CXSCREEN); int32_t scr_y = GetSystemMetrics(SM_CYSCREEN); fp64_t monitor_x = (fp64_t)scr_x / (fp64_t)ui_app.dpi.monitor_max; fp64_t monitor_y = (fp64_t)scr_y / (fp64_t)ui_app.dpi.monitor_max; rt_println("SCREEN: %d, %d %.1fx%.1f\"", scr_x, scr_y, monitor_x, monitor_y); } #endif static bool ui_app_update_mi(const ui_rect_t* r, uint32_t flags) { RECT rc = ui_app_ui2rect(r); HMONITOR monitor = MonitorFromRect(&rc, flags); // TODO: moving between monitors with different DPIs // HMONITOR mw = MonitorFromWindow(ui_app_window(), flags); if (monitor != null) { ui_app_update_monitor_dpi(monitor, &ui_app.dpi); rt_fatal_win32err(GetMonitorInfoA(monitor, &ui_app_mi)); ui_app.work_area = ui_app_rect2ui(&ui_app_mi.rcWork); ui_app.mrc = ui_app_rect2ui(&ui_app_mi.rcMonitor); // ui_app_dump_dpi(); } return monitor != null; } static void ui_app_update_crc(void) { RECT rc = {0}; rt_fatal_win32err(GetClientRect(ui_app_window(), &rc)); ui_app.crc = ui_app_rect2ui(&rc); } static void ui_app_dispose_fonts(void) { ui_gdi.delete_font(ui_app.fm.prop.normal.font); ui_gdi.delete_font(ui_app.fm.prop.tiny.font); ui_gdi.delete_font(ui_app.fm.prop.title.font); ui_gdi.delete_font(ui_app.fm.prop.rubric.font); ui_gdi.delete_font(ui_app.fm.prop.H1.font); ui_gdi.delete_font(ui_app.fm.prop.H2.font); ui_gdi.delete_font(ui_app.fm.prop.H3.font); memset(&ui_app.fm.prop, 0x00, sizeof(ui_app.fm.prop)); ui_gdi.delete_font(ui_app.fm.mono.normal.font); ui_gdi.delete_font(ui_app.fm.mono.tiny.font); ui_gdi.delete_font(ui_app.fm.mono.title.font); ui_gdi.delete_font(ui_app.fm.mono.rubric.font); ui_gdi.delete_font(ui_app.fm.mono.H1.font); ui_gdi.delete_font(ui_app.fm.mono.H2.font); ui_gdi.delete_font(ui_app.fm.mono.H3.font); memset(&ui_app.fm.mono, 0x00, sizeof(ui_app.fm.mono)); } static fp64_t ui_app_px2pt(fp64_t px) { rt_assert(ui_app.dpi.window >= 72.0); return px * 72.0 / (fp64_t)ui_app.dpi.window; } static int32_t ui_app_pt2px(fp64_t pt) { // rounded return (int32_t)(pt * (fp64_t)ui_app.dpi.window / 72.0 + 0.5); } static void ui_app_init_cursors(void) { if (ui_app.cursors.arrow == null) { ui_app.cursors.arrow = (ui_cursor_t)LoadCursorW(null, IDC_ARROW); ui_app.cursors.wait = (ui_cursor_t)LoadCursorW(null, IDC_WAIT); ui_app.cursors.ibeam = (ui_cursor_t)LoadCursorW(null, IDC_IBEAM); ui_app.cursors.size_nwse = (ui_cursor_t)LoadCursorW(null, IDC_SIZENWSE); ui_app.cursors.size_nesw = (ui_cursor_t)LoadCursorW(null, IDC_SIZENESW); ui_app.cursors.size_we = (ui_cursor_t)LoadCursorW(null, IDC_SIZEWE); ui_app.cursors.size_ns = (ui_cursor_t)LoadCursorW(null, IDC_SIZENS); ui_app.cursors.size_all = (ui_cursor_t)LoadCursorW(null, IDC_SIZEALL); ui_app.cursor = ui_app.cursors.arrow; } } static void ui_app_ncm_dump_fonts(void) { // Win10/Win11 all 5 fonts are exactly the same: // Caption : Segoe UI 0x-12 weight: 400 quality: 0 // SmCaption: Segoe UI 0x-12 weight: 400 quality: 0 // Menu : Segoe UI 0x-12 weight: 400 quality: 0 // Status : Segoe UI 0x-12 weight: 400 quality: 0 // Message : Segoe UI 0x-12 weight: 400 quality: 0 #if 0 const LOGFONTW* fonts[] = { &ui_app_ncm.lfCaptionFont, &ui_app_ncm.lfSmCaptionFont, &ui_app_ncm.lfMenuFont, &ui_app_ncm.lfStatusFont, &ui_app_ncm.lfMessageFont }; const char* font_names[] = { "Caption", "SmCaption", "Menu", "Status", "Message" }; for (int32_t i = 0; i < rt_countof(fonts); i++) { const LOGFONTW* lf = fonts[i]; char fn[128]; rt_str.utf16to8(fn, rt_countof(fn), lf->lfFaceName, -1); rt_println("%-9s: %s %dx%d weight: %d quality: %d", font_names[i], fn, lf->lfWidth, lf->lfHeight, lf->lfWeight, lf->lfQuality); } #endif } static void ui_app_dump_font_size(const char* name, const LOGFONTW* lf, ui_fm_t* fm) { rt_swear(abs(lf->lfHeight) == fm->height - fm->internal_leading); rt_swear(fm->external_leading == 0); // "Segoe UI" and "Cascadia Mono" rt_swear(ui_app.dpi.window >= 72); // "The height, in logical units, of the font's character cell or character. // The character height value (also known as the em height) is the // character cell height value minus the internal-leading value." #ifdef UI_APP_DUMP_FONT_SIZE int32_t ascender = fm->baseline - fm->ascent; int32_t cell = fm->height - ascender - fm->descent; fp64_t pt = fm->height * 72.0 / (fp64_t)ui_app.dpi.window; rt_println("%-6s .lfH: %+3d h: %d pt: %6.3f " "a: %2d c: %2d d: %d bl: %2d il: %2d lg: %d", name, lf->lfHeight, fm->height, pt, ascender, cell, fm->descent, fm->baseline, fm->internal_leading, fm->line_gap); #if 0 // TODO: need better understanding of box geometry in // "design units" // box scale factor: design units -> pixels fp64_t sf = pt * 72.0 / (fp64_t)fm->design_units_per_em; sf *= (fp64_t)ui_app.dpi.window / 72.0; // into pixels (unclear???) int32_t bx = (int32_t)(fm->box.x * sf + 0.5); int32_t by = (int32_t)(fm->box.y * sf + 0.5); int32_t bw = (int32_t)(fm->box.w * sf + 0.5); int32_t bh = (int32_t)(fm->box.h * sf + 0.5); rt_println("%-6s .box: %d,%d %dx%d", name, bx, by, bw, bh); #endif #else (void)name; // unused #endif } static void ui_app_init_fms(ui_fms_t* fms, const LOGFONTW* base) { LOGFONTW lf = *base; // lf.lfQuality is zero (DEFAULT_QUALITY) that gets internally // interpreted as CLEARTYPE_QUALITY (if clear type is enabled // system wide and it looks really bad on 4K monitors // Experimentally it looks like Windows UI is using PROOF_QUALITY // which is anti-aliased w/o ClearType rainbows // TODO: maybe DEFAULT_QUALITY on 96DPI, // PROOF_QUALITY below 4K // ANTIALIASED_QUALITY on 4K and ? lf.lfQuality = ANTIALIASED_QUALITY; ui_gdi.update_fm(&fms->normal, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("normal", &lf, &fms->normal); const fp64_t fh = lf.lfHeight; rt_swear(fh != 0); lf.lfHeight = (int32_t)(fh * 8.0 / 11.0 + 0.5); ui_gdi.update_fm(&fms->tiny, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("tiny", &lf, &fms->tiny); lf.lfWeight = FW_SEMIBOLD; lf.lfHeight = (int32_t)(fh * 2.25 + 0.5); ui_gdi.update_fm(&fms->title, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("title", &lf, &fms->title); lf.lfHeight = (int32_t)(fh * 2.00 + 0.5); ui_gdi.update_fm(&fms->rubric, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("rubric", &lf, &fms->rubric); lf.lfHeight = (int32_t)(fh * 1.75 + 0.5); ui_gdi.update_fm(&fms->H1, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H1", &lf, &fms->H1); lf.lfHeight = (int32_t)(fh * 1.4 + 0.5); ui_gdi.update_fm(&fms->H2, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H2", &lf, &fms->H2); lf.lfHeight = (int32_t)(fh * 1.15 + 0.5); ui_gdi.update_fm(&fms->H3, (ui_font_t)CreateFontIndirectW(&lf)); ui_app_dump_font_size("H3", &lf, &fms->H3); } static void ui_app_init_fonts(int32_t dpi) { ui_app_update_ncm(dpi); ui_app_ncm_dump_fonts(); if (ui_app.fm.prop.normal.font != null) { ui_app_dispose_fonts(); } LOGFONTW mono = ui_app_ncm.lfMessageFont; // TODO: how to get name of monospaced from Win32 API? wcscpy_s(mono.lfFaceName, rt_countof(mono.lfFaceName), L"Cascadia Mono"); mono.lfPitchAndFamily |= FIXED_PITCH; // rt_println("ui_app.fm.mono"); ui_app_init_fms(&ui_app.fm.mono, &mono); LOGFONTW prop = ui_app_ncm.lfMessageFont; prop.lfHeight--; // inc by 1 // rt_println("ui_app.fm.prop"); ui_app_init_fms(&ui_app.fm.prop, &ui_app_ncm.lfMessageFont); } static void ui_app_data_save(const char* name, const void* data, int32_t bytes) { rt_config.save(ui_app.class_name, name, data, bytes); } static int32_t ui_app_data_size(const char* name) { return rt_config.size(ui_app.class_name, name); } static int32_t ui_app_data_load(const char* name, void* data, int32_t bytes) { return rt_config.load(ui_app.class_name, name, data, bytes); } typedef rt_begin_packed struct ui_app_wiw_s { // "where is window" // coordinates in pixels relative (0,0) top left corner // of primary monitor from GetWindowPlacement int32_t bytes; int32_t padding; // to align rectangles and points to 8 bytes ui_rect_t placement; ui_rect_t mrc; // monitor rectangle ui_rect_t work_area; // monitor work area (mrc sans taskbar etc) ui_point_t min_position; // not used (-1, -1) ui_point_t max_position; // not used (-1, -1) ui_point_t max_track; // maximum window size (spawning all monitors) ui_rect_t space; // surrounding rect x,y,w,h of all monitors int32_t dpi; // of the monitor on which window (x,y) is located int32_t flags; // WPF_SETMINPOSITION. WPF_RESTORETOMAXIMIZED int32_t show; // show command } rt_end_packed ui_app_wiw_t; static BOOL CALLBACK ui_app_monitor_enum_proc(HMONITOR monitor, HDC rt_unused(hdc), RECT* rt_unused(rc1), LPARAM that) { ui_app_wiw_t* wiw = (ui_app_wiw_t*)(uintptr_t)that; MONITORINFOEXA mi = { .cbSize = sizeof(MONITORINFOEXA) }; rt_fatal_win32err(GetMonitorInfoA(monitor, (MONITORINFO*)&mi)); // monitors can be in negative coordinate spaces and even rotated upside-down const int32_t min_x = rt_min(mi.rcMonitor.left, mi.rcMonitor.right); const int32_t min_y = rt_min(mi.rcMonitor.top, mi.rcMonitor.bottom); const int32_t max_w = rt_max(mi.rcMonitor.left, mi.rcMonitor.right); const int32_t max_h = rt_max(mi.rcMonitor.top, mi.rcMonitor.bottom); wiw->space.x = rt_min(wiw->space.x, min_x); wiw->space.y = rt_min(wiw->space.y, min_y); wiw->space.w = rt_max(wiw->space.w, max_w); wiw->space.h = rt_max(wiw->space.h, max_h); return true; // keep going } static void ui_app_enum_monitors(ui_app_wiw_t* wiw) { EnumDisplayMonitors(null, null, ui_app_monitor_enum_proc, (LPARAM)(uintptr_t)wiw); // because ui_app_monitor_enum_proc() puts max into w,h: wiw->space.w -= wiw->space.x; wiw->space.h -= wiw->space.y; } static void ui_app_save_window_pos(ui_window_t wnd, const char* name, bool dump) { RECT wr = {0}; rt_fatal_win32err(GetWindowRect((HWND)wnd, &wr)); ui_rect_t wrc = ui_app_rect2ui(&wr); ui_app_update_mi(&wrc, MONITOR_DEFAULTTONEAREST); WINDOWPLACEMENT wpl = { .length = sizeof(wpl) }; rt_fatal_win32err(GetWindowPlacement((HWND)wnd, &wpl)); // note the replacement of wpl.rcNormalPosition with wrc: ui_app_wiw_t wiw = { // where is window .bytes = sizeof(ui_app_wiw_t), .placement = wrc, .mrc = ui_app.mrc, .work_area = ui_app.work_area, .min_position = ui_app_point2ui(&wpl.ptMinPosition), .max_position = ui_app_point2ui(&wpl.ptMaxPosition), .max_track = { .x = GetSystemMetrics(SM_CXMAXTRACK), .y = GetSystemMetrics(SM_CYMAXTRACK) }, .dpi = ui_app.dpi.monitor_max, .flags = (int32_t)wpl.flags, .show = (int32_t)wpl.showCmd }; ui_app_enum_monitors(&wiw); if (dump) { rt_println("wiw.space: %d,%d %dx%d", wiw.space.x, wiw.space.y, wiw.space.w, wiw.space.h); rt_println("MAXTRACK: %d, %d", wiw.max_track.x, wiw.max_track.y); rt_println("wpl.rcNormalPosition: %d,%d %dx%d", wpl.rcNormalPosition.left, wpl.rcNormalPosition.top, wpl.rcNormalPosition.right - wpl.rcNormalPosition.left, wpl.rcNormalPosition.bottom - wpl.rcNormalPosition.top); rt_println("wpl.ptMinPosition: %d,%d", wpl.ptMinPosition.x, wpl.ptMinPosition.y); rt_println("wpl.ptMaxPosition: %d,%d", wpl.ptMaxPosition.x, wpl.ptMaxPosition.y); rt_println("wpl.showCmd: %d", wpl.showCmd); // WPF_SETMINPOSITION. WPF_RESTORETOMAXIMIZED WPF_ASYNCWINDOWPLACEMENT rt_println("wpl.flags: %d", wpl.flags); } // rt_println("%d,%d %dx%d show=%d", wiw.placement.x, wiw.placement.y, // wiw.placement.w, wiw.placement.h, wiw.show); rt_config.save(ui_app.class_name, name, &wiw, sizeof(wiw)); ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); } static void ui_app_save_console_pos(void) { HWND cw = GetConsoleWindow(); if (cw != null) { ui_app_save_window_pos((ui_window_t)cw, "wic", false); HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int32_t r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); if (r != 0) { rt_println("GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); } else { rt_config.save(ui_app.class_name, "console_screen_buffer_infoex", &info, (int32_t)sizeof(info)); // rt_println("info: %dx%d", info.dwSize.X, info.dwSize.Y); // rt_println("%d,%d %dx%d", info.srWindow.Left, info.srWindow.Top, // info.srWindow.Right - info.srWindow.Left, // info.srWindow.Bottom - info.srWindow.Top); } } int32_t v = ui_app.is_console_visible(); // "icv" "is console visible" rt_config.save(ui_app.class_name, "icv", &v, (int32_t)sizeof(v)); } static bool ui_app_is_fully_inside(const ui_rect_t* inner, const ui_rect_t* outer) { return outer->x <= inner->x && inner->x + inner->w <= outer->x + outer->w && outer->y <= inner->y && inner->y + inner->h <= outer->y + outer->h; } static void ui_app_bring_window_inside_monitor(const ui_rect_t* mrc, ui_rect_t* wrc) { rt_assert(mrc->w > 0 && mrc->h > 0); // Check if window rect is inside monitor rect if (!ui_app_is_fully_inside(wrc, mrc)) { // Move window into monitor rect wrc->x = rt_max(mrc->x, rt_min(mrc->x + mrc->w - wrc->w, wrc->x)); wrc->y = rt_max(mrc->y, rt_min(mrc->y + mrc->h - wrc->h, wrc->y)); // Adjust size to fit into monitor rect wrc->w = rt_min(wrc->w, mrc->w); wrc->h = rt_min(wrc->h, mrc->h); } } static bool ui_app_load_window_pos(ui_rect_t* rect, int32_t *visibility) { ui_app_wiw_t wiw = {0}; // where is window bool loaded = rt_config.load(ui_app.class_name, "wiw", &wiw, sizeof(wiw)) == sizeof(wiw); if (loaded) { #ifdef UI_APP_DEBUG rt_println("wiw.placement: %d,%d %dx%d", wiw.placement.x, wiw.placement.y, wiw.placement.w, wiw.placement.h); rt_println("wiw.mrc: %d,%d %dx%d", wiw.mrc.x, wiw.mrc.y, wiw.mrc.w, wiw.mrc.h); rt_println("wiw.work_area: %d,%d %dx%d", wiw.work_area.x, wiw.work_area.y, wiw.work_area.w, wiw.work_area.h); rt_println("wiw.min_position: %d,%d", wiw.min_position.x, wiw.min_position.y); rt_println("wiw.max_position: %d,%d", wiw.max_position.x, wiw.max_position.y); rt_println("wiw.max_track: %d,%d", wiw.max_track.x, wiw.max_track.y); rt_println("wiw.dpi: %d", wiw.dpi); rt_println("wiw.flags: %d", wiw.flags); rt_println("wiw.show: %d", wiw.show); #endif ui_app_update_mi(&wiw.placement, MONITOR_DEFAULTTONEAREST); bool same_monitor = memcmp(&wiw.mrc, &ui_app.mrc, sizeof(wiw.mrc)) == 0; // rt_println("%d,%d %dx%d", p->x, p->y, p->w, p->h); if (same_monitor) { *rect = wiw.placement; } else { // moving to another monitor rect->x = (wiw.placement.x - wiw.mrc.x) * ui_app.mrc.w / wiw.mrc.w; rect->y = (wiw.placement.y - wiw.mrc.y) * ui_app.mrc.h / wiw.mrc.h; // adjust according to monitors DPI difference: // (w, h) theoretically could be as large as 0xFFFF const int64_t w = (int64_t)wiw.placement.w * ui_app.dpi.monitor_max; const int64_t h = (int64_t)wiw.placement.h * ui_app.dpi.monitor_max; rect->w = (int32_t)(w / wiw.dpi); rect->h = (int32_t)(h / wiw.dpi); } *visibility = wiw.show; } // rt_println("%d,%d %dx%d show=%d", rect->x, rect->y, rect->w, rect->h, *visibility); ui_app_bring_window_inside_monitor(&ui_app.mrc, rect); // rt_println("%d,%d %dx%d show=%d", rect->x, rect->y, rect->w, rect->h, *visibility); return loaded; } static bool ui_app_load_console_pos(ui_rect_t* rect, int32_t *visibility) { ui_app_wiw_t wiw = {0}; // where is window *visibility = 0; // boolean bool loaded = rt_config.load(ui_app.class_name, "wic", &wiw, sizeof(wiw)) == sizeof(wiw); if (loaded) { ui_app_update_mi(&wiw.placement, MONITOR_DEFAULTTONEAREST); bool same_monitor = memcmp(&wiw.mrc, &ui_app.mrc, sizeof(wiw.mrc)) == 0; // rt_println("%d,%d %dx%d", p->x, p->y, p->w, p->h); if (same_monitor) { *rect = wiw.placement; } else { // moving to another monitor rect->x = (wiw.placement.x - wiw.mrc.x) * ui_app.mrc.w / wiw.mrc.w; rect->y = (wiw.placement.y - wiw.mrc.y) * ui_app.mrc.h / wiw.mrc.h; // adjust according to monitors DPI difference: // (w, h) theoretically could be as large as 0xFFFF const int64_t w = (int64_t)wiw.placement.w * ui_app.dpi.monitor_max; const int64_t h = (int64_t)wiw.placement.h * ui_app.dpi.monitor_max; rect->w = (int32_t)(w / wiw.dpi); rect->h = (int32_t)(h / wiw.dpi); } *visibility = wiw.show != 0; ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); } return loaded; } static void ui_app_timer_kill(ui_timer_t timer) { rt_fatal_win32err(KillTimer(ui_app_window(), timer)); } static ui_timer_t ui_app_timer_set(uintptr_t id, int32_t ms) { rt_not_null(ui_app_window()); rt_assert(10 <= ms && ms < 0x7FFFFFFF); ui_timer_t tid = (ui_timer_t)SetTimer(ui_app_window(), id, (uint32_t)ms, null); rt_fatal_if(tid == 0); rt_assert(tid == id); return tid; } static void ui_app_timer(ui_view_t* view, ui_timer_t id) { ui_view.timer(view, id); if (id == ui_app_timer_1s_id) { ui_view.every_sec(view); } if (id == ui_app_timer_100ms_id) { ui_view.every_100ms(view); } } static void ui_app_animate_timer(void) { ui_app_post_message(ui.message.animate, (int64_t)ui_app_animate.step + 1, (int64_t)(uintptr_t)ui_app_animate.f); } static void ui_app_wm_timer(ui_timer_t id) { if (ui_app.animating.time != 0 && ui_app.now > ui_app.animating.time) { ui_app.show_toast(null, 0); } if (ui_app_animate.timer == id) { ui_app_animate_timer(); } ui_app_timer(ui_app.root, id); } static void ui_app_window_dpi(void) { int32_t dpi = (int32_t)GetDpiForWindow(ui_app_window()); if (dpi == 0) { dpi = (int32_t)GetDpiForWindow(GetParent(ui_app_window())); } if (dpi == 0) { dpi = (int32_t)GetDpiForWindow(GetDesktopWindow()); } if (dpi == 0) { dpi = (int32_t)GetSystemDpiForProcess(GetCurrentProcess()); } if (dpi == 0) { dpi = (int32_t)GetDpiForSystem(); } ui_app.dpi.window = dpi; } static void ui_app_window_opening(void) { ui_app_window_dpi(); ui_app_init_fonts(ui_app.dpi.window); ui_app_init_cursors(); ui_app_timer_1s_id = ui_app.set_timer((uintptr_t)&ui_app_timer_1s_id, 1000); ui_app_timer_100ms_id = ui_app.set_timer((uintptr_t)&ui_app_timer_100ms_id, 100); rt_assert(ui_app.cursors.arrow != null); ui_app.set_cursor(ui_app.cursors.arrow); ui_app.canvas = (ui_canvas_t)GetDC(ui_app_window()); rt_not_null(ui_app.canvas); if (ui_app.opened != null) { ui_app.opened(); } ui_view.set_text(ui_app.root, "ui_app.root"); // debugging ui_app_wm_timer(ui_app_timer_100ms_id); ui_app_wm_timer(ui_app_timer_1s_id); rt_fatal_if(ReleaseDC(ui_app_window(), ui_app_canvas()) == 0); ui_app.canvas = null; ui_app.request_layout(); // request layout if (ui_app.last_visibility == ui.visibility.maximize) { ShowWindow(ui_app_window(), ui.visibility.maximize); } // ui_app_dump_dpi(); // if (forced_locale != 0) { // SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (uintptr_t)"intl", 0, 1000, null); // } } static void ui_app_window_closing(void) { if (ui_app.can_close == null || ui_app.can_close()) { if (ui_app.is_full_screen) { ui_app.full_screen(false); } ui_app.kill_timer(ui_app_timer_1s_id); ui_app.kill_timer(ui_app_timer_100ms_id); ui_app_timer_1s_id = 0; ui_app_timer_100ms_id = 0; if (ui_app.closed != null) { ui_app.closed(); } ui_app_save_window_pos(ui_app.window, "wiw", false); ui_app_save_console_pos(); DestroyWindow(ui_app_window()); ui_app.window = null; } } static void ui_app_get_min_max_info(MINMAXINFO* mmi) { const ui_window_sizing_t* ws = &ui_app.window_sizing; const ui_rect_t* wa = &ui_app.work_area; const int32_t min_w = ws->min_w > 0 ? ui_app.in2px(ws->min_w) : ui_app.in2px(1.0); const int32_t min_h = ws->min_h > 0 ? ui_app.in2px(ws->min_h) : ui_app.in2px(0.5); mmi->ptMinTrackSize.x = min_w; mmi->ptMinTrackSize.y = min_h; const int32_t max_w = ws->max_w > 0 ? ui_app.in2px(ws->max_w) : wa->w; const int32_t max_h = ws->max_h > 0 ? ui_app.in2px(ws->max_h) : wa->h; if (ui_app.no_clip) { mmi->ptMaxTrackSize.x = max_w; mmi->ptMaxTrackSize.y = max_h; } else { // clip max_w and max_h to monitor work area mmi->ptMaxTrackSize.x = rt_min(max_w, wa->w); mmi->ptMaxTrackSize.y = rt_min(max_h, wa->h); } mmi->ptMaxSize.x = mmi->ptMaxTrackSize.x; mmi->ptMaxSize.y = mmi->ptMaxTrackSize.y; } static void ui_app_paint(ui_view_t* view) { rt_assert(ui_app_window() != null); // crc = {0,0} on minimized windows but paint is still called if (ui_app.crc.w > 0 && ui_app.crc.h > 0) { ui_view.paint(view); } } static void ui_app_measure_and_layout(ui_view_t* view) { // restore from minimized calls ui_app.crc.w,h == 0 if (ui_app.crc.w > 0 && ui_app.crc.h > 0 && ui_app_window() != null) { ui_view.measure(view); ui_view.layout(view); ui_app_layout_dirty = false; } } static void ui_app_toast_character(const char* utf8); static bool ui_app_toast_key_pressed(int64_t key); static bool ui_app_toast_tap(ui_view_t* v, int32_t ix, bool pressed); static void ui_app_dispatch_wm_char(ui_view_t* view, const uint16_t* utf16) { char utf8[32 + 1]; int32_t utf8bytes = rt_str.utf8_bytes(utf16, -1); rt_swear(utf8bytes < rt_countof(utf8) - 1); // 32 bytes + 0x00 rt_str.utf16to8(utf8, rt_countof(utf8), utf16, -1); utf8[utf8bytes] = 0x00; if (ui_app.animating.view != null) { ui_app_toast_character(utf8); } else { ui_view.character(view, utf8); } ui_app_high_surrogate = 0x0000; } static void ui_app_wm_char(ui_view_t* view, const uint16_t* utf16) { int32_t utf16chars = rt_str.len16(utf16); rt_swear(0 < utf16chars && utf16chars < 4); // wParam is 64bits const uint16_t utf16char = utf16[0]; if (utf16chars == 1 && rt_str.utf16_is_high_surrogate(utf16char)) { ui_app_high_surrogate = utf16char; } else if (utf16chars == 1 && rt_str.utf16_is_low_surrogate(utf16char)) { if (ui_app_high_surrogate != 0) { uint16_t utf16_surrogate_pair[3] = { ui_app_high_surrogate, utf16char, 0x0000 }; ui_app_dispatch_wm_char(view, utf16_surrogate_pair); } } else { ui_app_dispatch_wm_char(view, utf16); } } static bool ui_app_wm_key_pressed(ui_view_t* v, int64_t key) { if (ui_app.animating.view != null) { return ui_app_toast_key_pressed(key); } else { return ui_view.key_pressed(v, key); } } static bool ui_app_mouse(ui_view_t* v, int32_t m, int64_t f) { bool swallow = false; // override ui_app_update_mouse_buttons_state() (sic): // because mouse message can be from the past ui_app.mouse_left = f & (ui_app.mouse_swapped ? MK_RBUTTON : MK_LBUTTON); ui_app.mouse_middle = f & MK_MBUTTON; ui_app.mouse_right = f & (ui_app.mouse_swapped ? MK_LBUTTON : MK_RBUTTON); ui_view_t* av = ui_app.animating.view; if (m == WM_MOUSEHOVER) { ui_view.mouse_hover(av != null && av->mouse_hover != null ? av : v); } else if (m == WM_MOUSEMOVE) { ui_view.mouse_move(av != null && av->mouse_move != null ? av : v); } else if (m == WM_LBUTTONDOWN || m == WM_LBUTTONUP || m == WM_MBUTTONDOWN || m == WM_MBUTTONUP || m == WM_RBUTTONDOWN || m == WM_RBUTTONUP) { const int i = (m == WM_LBUTTONDOWN || m == WM_LBUTTONUP) ? 0 : ((m == WM_MBUTTONDOWN || m == WM_MBUTTONUP) ? 1 : ((m == WM_RBUTTONDOWN || m == WM_RBUTTONUP) ? 2 : -1)); rt_swear(i >= 0); const int32_t ix = ui_app.mouse_swapped ? 2 - i : i; const bool pressed = m == WM_LBUTTONDOWN || m == WM_MBUTTONDOWN || m == WM_RBUTTONDOWN; if (av != null) { // because of "micro" close button: swallow = ui_app_toast_tap(ui_app.animating.view, ix, pressed); } else { if (av != null && av->tap != null) { swallow = ui_view.tap(av, ix, pressed); } else { // tap detector will handle the tap() calling } } } else if (m == WM_LBUTTONDBLCLK || m == WM_MBUTTONDBLCLK || m == WM_RBUTTONDBLCLK) { const int i = (m == WM_LBUTTONDBLCLK) ? 0 : ((m == WM_MBUTTONDBLCLK) ? 1 : ((m == WM_RBUTTONDBLCLK) ? 2 : -1)); rt_swear(i >= 0); if (av != null && av->double_tap != null) { const int32_t ix = ui_app.mouse_swapped ? 2 - i : i; swallow = ui_view.double_tap(av, ix); } // otherwise tap detector will do the double_tap() call } else { rt_assert(false, "m: 0x%04X", m); } return swallow; } static void ui_app_show_sys_menu(int32_t x, int32_t y) { HMENU sys_menu = GetSystemMenu(ui_app_window(), false); if (sys_menu != null) { // TPM_RIGHTBUTTON means both left and right click to select menu item const DWORD flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_VERPOSANIMATION; int32_t sys_cmd = TrackPopupMenu(sys_menu, flags, x, y, 0, ui_app_window(), null); if (sys_cmd != 0) { ui_app_post_message(WM_SYSCOMMAND, sys_cmd, 0); } } } static int32_t ui_app_nc_mouse_message(int32_t m) { switch (m) { case WM_NCMOUSEMOVE : return WM_MOUSEMOVE; case WM_NCLBUTTONDOWN : return WM_LBUTTONDOWN; case WM_NCLBUTTONUP : return WM_LBUTTONUP; case WM_NCLBUTTONDBLCLK : return WM_LBUTTONDBLCLK; case WM_NCMBUTTONDOWN : return WM_MBUTTONDOWN; case WM_NCMBUTTONUP : return WM_MBUTTONUP; case WM_NCMBUTTONDBLCLK : return WM_MBUTTONDBLCLK; case WM_NCRBUTTONDOWN : return WM_RBUTTONDOWN; case WM_NCRBUTTONUP : return WM_RBUTTONUP; case WM_NCRBUTTONDBLCLK : return WM_RBUTTONDBLCLK; default: rt_swear(false, "fix me m: %d", m); } return -1; } static bool ui_app_nc_mouse_buttons(int32_t m, int64_t wp, int64_t lp) { bool swallow = false; POINT screen = {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; POINT client = screen; ScreenToClient(ui_app_window(), &client); ui_app.mouse = ui_app_point2ui(&client); const bool inside = ui_view.inside(ui_app.caption, &ui_app.mouse); if (!ui_view.is_hidden(ui_app.caption) && inside) { uint16_t lr = ui_app.mouse_swapped ? WM_NCLBUTTONDOWN : WM_NCRBUTTONDOWN; if (m == lr) { // rt_println("WM_NC*BUTTONDOWN %d %d", ui_app.mouse.x, ui_app.mouse.y); swallow = true; ui_app_show_sys_menu(screen.x, screen.y); } } else { swallow = ui_app_mouse(ui_app.root, ui_app_nc_mouse_message(m), wp); } return swallow; } enum { ui_app_animation_steps = 63 }; static void ui_app_toast_paint(void) { static ui_bitmap_t image_dark; if (image_dark.texture == null) { uint8_t pixels[4] = { 0x3F, 0x3F, 0x3F }; ui_gdi.bitmap_init(&image_dark, 1, 1, 3, pixels); } static ui_bitmap_t image_light; if (image_dark.texture == null) { uint8_t pixels[4] = { 0xC0, 0xC0, 0xC0 }; ui_gdi.bitmap_init(&image_light, 1, 1, 3, pixels); } ui_view_t* av = ui_app.animating.view; if (av != null) { ui_view.measure(av); bool hint = ui_app.animating.x >= 0 && ui_app.animating.y >= 0; const int32_t em_w = av->fm->em.w; const int32_t em_h = av->fm->em.h; if (!hint) { rt_assert(0 <= ui_app.animating.step && ui_app.animating.step < ui_app_animation_steps); int32_t step = ui_app.animating.step - (ui_app_animation_steps - 1); av->y = av->h * step / (ui_app_animation_steps - 1); // rt_println("step=%d of %d y=%d", ui_app.animating.step, // ui_app_toast_steps, av->y); ui_app_measure_and_layout(av); // dim main window (as `disabled`): fp64_t alpha = rt_min(0.40, 0.40 * ui_app.animating.step / (fp64_t)ui_app_animation_steps); ui_gdi.alpha(0, 0, ui_app.crc.w, ui_app.crc.h, 0, 0, image_dark.w, image_dark.h, &image_dark, alpha); av->x = (ui_app.root->w - av->w) / 2; // rt_println("ui_app.animating.y: %d av->y: %d", // ui_app.animating.y, av->y); } else { av->x = ui_app.animating.x; av->y = ui_app.animating.y; ui_app_measure_and_layout(av); int32_t mx = ui_app.root->w - av->w - em_w; int32_t cx = ui_app.animating.x - av->w / 2; av->x = rt_min(mx, rt_max(0, cx)); av->y = rt_min( ui_app.root->h - em_h, rt_max(0, ui_app.animating.y)); // rt_println("ui_app.animating.y: %d av->y: %d", // ui_app.animating.y, av->y); } int32_t x = av->x - em_w / 4; int32_t y = av->y - em_h / 8; int32_t w = av->w + em_w / 2; int32_t h = av->h + em_h / 4; int32_t radius = em_w / 2; if (radius % 2 == 0) { radius++; } ui_color_t color = ui_theme.is_app_dark() ? ui_color_rgb(45, 45, 48) : // TODO: hard coded ui_colors.get_color(ui_color_id_button_face); ui_color_t tint = ui_colors.interpolate(color, ui_colors.yellow, 0.5f); ui_gdi.rounded(x, y, w, h, radius, tint, tint); if (!hint) { av->y += em_h / 4; } ui_app_paint(av); if (!hint) { if (av->y == em_h / 4) { // micro "close" toast button: int32_t r = av->x + av->w; const int32_t tx = r - em_w / 2; const int32_t ty = 0; const ui_gdi_ta_t ta = { .fm = &ui_app.fm.prop.normal, .color = ui_color_undefined, .color_id = ui_color_id_window_text }; ui_gdi.text(&ta, tx, ty, "%s", rt_glyph_multiplication_sign); } } } } static void ui_app_toast_cancel(void) { if (ui_app.animating.view != null) { if (ui_app.animating.view->type == ui_view_mbx) { ui_mbx_t* mx = (ui_mbx_t*)ui_app.animating.view; if (mx->option < 0 && mx->callback != null) { mx->callback(&mx->view); } } ui_app.animating.view->parent = null; ui_app.animating.step = 0; ui_app.animating.view = null; ui_app.animating.time = 0; ui_app.animating.x = -1; ui_app.animating.y = -1; if (ui_app.animating.focused != null) { ui_view.set_focus(ui_app.animating.focused->focusable && !ui_view.is_hidden(ui_app.animating.focused) && !ui_view.is_disabled(ui_app.animating.focused) ? ui_app.animating.focused : null); ui_app.animating.focused = null; } else { ui_view.set_focus(null); } ui_app.request_redraw(); } } static bool ui_app_toast_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; rt_swear(v == ui_app.animating.view); if (pressed) { const ui_fm_t* fm = v->fm; const int32_t right = v->x + v->w; const int32_t x = right - fm->em.w / 2; const int32_t mx = ui_app.mouse.x; const int32_t my = ui_app.mouse.y; // micro close button which is not a button if (x <= mx && mx <= x + fm->em.w && 0 <= my && my <= fm->em.h) { ui_app_toast_cancel(); } } if (ui_app.animating.view != null) { // could have been canceled above swallow = ui_view.tap(v, ix, pressed); // TODO: do we need it? } return swallow; } static void ui_app_toast_character(const char* utf8) { char ch = utf8[0]; if (ui_app.animating.view != null && ch == 033) { // ESC traditionally in octal ui_app_toast_cancel(); ui_app.show_toast(null, 0); } else { ui_view.character(ui_app.animating.view, utf8); } } static bool ui_app_toast_key_pressed(int64_t key) { if (ui_app.animating.view != null && key == 033) { // ESC traditionally in octal ui_app_toast_cancel(); ui_app.show_toast(null, 0); return true; } else { return ui_view.key_pressed(ui_app.animating.view, key); } } static void ui_app_toast_dim(int32_t step) { ui_app.animating.step = step; ui_app.request_redraw(); UpdateWindow(ui_app_window()); } static void ui_app_animate_step(ui_app_animate_function_t f, int32_t step, int32_t steps) { // calls function(0..step-1) exactly step times bool cancel = false; if (f != null && f != ui_app_animate.f && step == 0 && steps > 0) { // start animated_groot ui_app_animate.count = steps; ui_app_animate.f = f; f(step); ui_app_animate.timer = ui_app.set_timer((uintptr_t)&ui_app_animate.timer, 10); } else if (f != null && ui_app_animate.f == f && step > 0) { cancel = step >= ui_app_animate.count; if (!cancel) { ui_app_animate.step = step; f(step); } } else if (f == null) { cancel = true; } if (cancel) { if (ui_app_animate.timer != 0) { ui_app.kill_timer(ui_app_animate.timer); } ui_app_animate.step = 0; ui_app_animate.timer = 0; ui_app_animate.f = null; ui_app_animate.count = 0; } } static void ui_app_animate_start(ui_app_animate_function_t f, int32_t steps) { // calls f(0..step-1) exactly steps times, unless cancelled with call // animate(null, 0) or animate(other_function, n > 0) ui_app_animate_step(f, 0, steps); } static void ui_app_view_paint(ui_view_t* v) { v->color = ui_colors.get_color(v->color_id); if (v->background_id > 0) { v->background = ui_colors.get_color(v->background_id); } if (!ui_color_is_undefined(v->background) && !ui_color_is_transparent(v->background)) { ui_gdi.fill(v->x, v->y, v->w, v->h, v->background); } } static void ui_app_view_layout(void) { rt_not_null(ui_app.window); rt_not_null(ui_app.canvas); if (ui_app.no_decor) { ui_app.root->x = ui_app.border.w; ui_app.root->y = ui_app.border.h; ui_app.root->w = ui_app.crc.w - ui_app.border.w * 2; ui_app.root->h = ui_app.crc.h - ui_app.border.h * 2; } else { ui_app.root->x = 0; ui_app.root->y = 0; ui_app.root->w = ui_app.crc.w; ui_app.root->h = ui_app.crc.h; } ui_app_measure_and_layout(ui_app.root); } static void ui_app_view_active_frame_paint(void) { ui_color_t c = ui_app.is_active() ? ui_colors.get_color(ui_color_id_highlight) : // ui_colors.btn_hover_highlight ui_colors.get_color(ui_color_id_inactive_title); rt_assert(ui_app.border.w == ui_app.border.h); const int32_t w = ui_app.wrc.w; const int32_t h = ui_app.wrc.h; for (int32_t i = 0; i < ui_app.border.w; i++) { ui_gdi.frame(i, i, w - i * 2, h - i * 2, c); } } static void ui_app_paint_stats(void) { if (ui_app.paint_count % 128 == 0) { ui_app.paint_max = 0; } ui_app.paint_time = rt_clock.seconds() - ui_app.now; ui_app.paint_max = rt_max(ui_app.paint_time, ui_app.paint_max); if (ui_app.paint_avg == 0) { ui_app.paint_avg = ui_app.paint_time; } else { // EMA over 32 paint() calls ui_app.paint_avg = ui_app.paint_avg * (1.0 - 1.0 / 32.0) + ui_app.paint_time / 32.0; } static fp64_t first_paint; if (first_paint == 0) { first_paint = ui_app.now; } fp64_t since_first_paint = ui_app.now - first_paint; if (since_first_paint > 0) { double fps = (double)ui_app.paint_count / since_first_paint; if (ui_app.paint_fps == 0) { ui_app.paint_fps = fps; } else { ui_app.paint_fps = ui_app.paint_fps * (1.0 - 1.0 / 32.0) + fps / 32.0; } } if (ui_app.paint_last == 0) { ui_app.paint_dt_min = 1.0 / 60.0; // 60Hz monitor } else { fp64_t since_last = ui_app.now - ui_app.paint_last; if (since_last > 1.0 / 120.0) { // 240Hz monitor ui_app.paint_dt_min = rt_min(ui_app.paint_dt_min, since_last); } // rt_println("paint_dt_min: %.6f since_last: %.6f", // ui_app.paint_dt_min, since_last); } ui_app.paint_last = ui_app.now; } static void ui_app_paint_on_canvas(HDC hdc) { ui_canvas_t canvas = ui_app.canvas; ui_app.canvas = (ui_canvas_t)hdc; ui_app_update_crc(); if (ui_app_layout_dirty) { ui_app_view_layout(); } ui_gdi.begin(null); ui_app_paint(ui_app.root); if (ui_app.animating.view != null) { ui_app_toast_paint(); } // active frame on top of everything: if (ui_app.no_decor && !ui_app.is_full_screen && !ui_app.is_maximized()) { ui_app_view_active_frame_paint(); } ui_gdi.end(); ui_app.paint_count++; ui_app.canvas = canvas; ui_app_paint_stats(); } static void ui_app_wm_paint(void) { // it is possible to receive WM_PAINT when window is not closed if (ui_app.window != null) { PAINTSTRUCT ps = {0}; BeginPaint(ui_app_window(), &ps); ui_app.prc = ui_app_rect2ui(&ps.rcPaint); // rt_println("%d,%d %dx%d", ui_app.prc.x, ui_app.prc.y, ui_app.prc.w, ui_app.prc.h); ui_app_paint_on_canvas(ps.hdc); EndPaint(ui_app_window(), &ps); } } // about (x,y) being (-32000,-32000) see: // https://chromium.googlesource.com/chromium/src.git/+/62.0.3178.1/ui/views/win/hwnd_message_handler.cc#1847 static void ui_app_window_position_changed(const WINDOWPOS* wp) { ui_app.root->state.hidden = !IsWindowVisible(ui_app_window()); const bool moved = (wp->flags & SWP_NOMOVE) == 0; const bool sized = (wp->flags & SWP_NOSIZE) == 0; const bool hiding = (wp->flags & SWP_HIDEWINDOW) != 0 || (wp->x == -32000 && wp->y == -32000); HMONITOR monitor = MonitorFromWindow(ui_app_window(), MONITOR_DEFAULTTONULL); if (!ui_app.root->state.hidden && (moved || sized) && !hiding && monitor != null) { RECT wrc = ui_app_ui2rect(&ui_app.wrc); rt_fatal_win32err(GetWindowRect(ui_app_window(), &wrc)); ui_app.wrc = ui_app_rect2ui(&wrc); ui_app_update_mi(&ui_app.wrc, MONITOR_DEFAULTTONEAREST); ui_app_update_crc(); if (ui_app_timer_1s_id != 0) { ui_app.request_layout(); } } } static void ui_app_setting_change(uintptr_t wp, uintptr_t lp) { // wp: SPI_SETWORKAREA ... SPI_SETDOCKMOVING // SPI_GETACTIVEWINDOWTRACKING ... SPI_SETGESTUREVISUALIZATION if (wp == SPI_SETLOGICALDPIOVERRIDE) { ui_app_init_fonts(ui_app.dpi.window); // font scale changed ui_app.request_layout(); } else if (lp != 0 && (strcmp((const char*)lp, "ImmersiveColorSet") == 0 || wcscmp((const uint16_t*)lp, L"ImmersiveColorSet") == 0)) { // expected: // SPI_SETICONTITLELOGFONT 0x22 ? // SPI_SETNONCLIENTMETRICS 0x2A ? // rt_println("wp: 0x%08X", wp); // actual wp == 0x0000 ui_theme.refresh(); } else if (wp == 0 && lp != 0 && strcmp((const char*)lp, "intl") == 0) { rt_println("wp: 0x%04X", wp); // SPI_SETLOCALEINFO 0x24 ? uint16_t ln[LOCALE_NAME_MAX_LENGTH + 1]; int32_t n = GetUserDefaultLocaleName(ln, rt_countof(ln)); rt_fatal_if(n <= 0); uint16_t rln[LOCALE_NAME_MAX_LENGTH + 1]; n = ResolveLocaleName(ln, rln, rt_countof(rln)); rt_fatal_if(n <= 0); LCID lc_id = LocaleNameToLCID(rln, LOCALE_ALLOW_NEUTRAL_NAMES); rt_fatal_win32err(SetThreadLocale(lc_id)); } } static void ui_app_show_task_bar(bool show) { HWND taskbar = FindWindowA("Shell_TrayWnd", null); if (taskbar != null) { ShowWindow(taskbar, show ? SW_SHOW : SW_HIDE); UpdateWindow(taskbar); } } static bool ui_app_click_detector(uint32_t msg, WPARAM wp, LPARAM lp) { bool swallow = false; enum { tap = 1, long_press = 2, double_tap = 3 }; // TODO: click detector does not handle WM_NCLBUTTONDOWN, ... // it can be modified to do so if needed #pragma push_macro("ui_set_timer") #pragma push_macro("ui_kill_timer") #pragma push_macro("ui_timers_done") #define ui_set_timer(t, ms) do { \ rt_assert(t == 0); \ t = ui_app_timer_set((uintptr_t)&t, ms); \ } while (0) #define ui_kill_timer(t) do { \ if (t != 0) { ui_app_timer_kill(t); t = 0; } \ } while (0) #define ui_timers_done(ix) do { \ clicked[ix] = 0; \ pressed[ix] = false; \ click_at[ix] = (ui_point_t){0, 0}; \ ui_kill_timer(timer_p[ix]); \ ui_kill_timer(timer_d[ix]); \ } while (0) // This function should work regardless to CS_BLKCLK being present // 0: Left, 1: Middle, 2: Right static ui_point_t click_at[3]; static fp64_t clicked[3]; // click time static bool pressed[3]; static ui_timer_t timer_d[3]; // double tap static ui_timer_t timer_p[3]; // long press bool up = false; int32_t ix = -1; int32_t m = 0; switch (msg) { case WM_LBUTTONDOWN : ix = 0; m = tap; break; case WM_MBUTTONDOWN : ix = 1; m = tap; break; case WM_RBUTTONDOWN : ix = 2; m = tap; break; case WM_LBUTTONDBLCLK: ix = 0; m = double_tap; break; case WM_MBUTTONDBLCLK: ix = 1; m = double_tap; break; case WM_RBUTTONDBLCLK: ix = 2; m = double_tap; break; case WM_LBUTTONUP : ix = 0; m = tap; up = true; break; case WM_MBUTTONUP : ix = 1; m = tap; up = true; break; case WM_RBUTTONUP : ix = 2; m = tap; up = true; break; } if (msg == WM_TIMER) { // long press && double tap for (int i = 0; i < 3; i++) { if (wp == timer_p[i]) { ui_app.mouse = (ui_point_t){ click_at[i].x, click_at[i].y }; ui_view.long_press(ui_app.root, i); // rt_println("timer_p[%d] _d && _p timers done", i); ui_timers_done(i); } if (wp == timer_d[i]) { // rt_println("timer_p[%d] _d && _p timers done", i); ui_timers_done(i); } } } if (ix != -1) { ui_app.show_hint(null, -1, -1, 0); // dismiss hint on any click const int32_t double_click_msec = (int32_t)GetDoubleClickTime(); const fp64_t double_click_dt = double_click_msec / 1000.0; // seconds // rt_println("double_click_msec: %d double_click_dt: %.3fs", // double_click_msec, double_click_dt); const int double_click_x = GetSystemMetrics(SM_CXDOUBLECLK) / 2; const int double_click_y = GetSystemMetrics(SM_CYDOUBLECLK) / 2; ui_point_t pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) }; if (m == tap && !up) { swallow = ui_view.tap(ui_app.root, ix, !up); if (ui_app.now - clicked[ix] <= double_click_dt && abs(pt.x - click_at[ix].x) <= double_click_x && abs(pt.y - click_at[ix].y) <= double_click_y) { ui_app.mouse = (ui_point_t){ click_at[ix].x, click_at[ix].y }; ui_view.double_tap(ui_app.root, ix); // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); } else { // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); // clear timers clicked[ix] = ui_app.now; click_at[ix] = pt; pressed[ix] = true; // rt_println("clicked[%d] := %.1f %d,%d pressed[%d] := true", // ix, clicked[ix], pt.x, pt.y, ix); if ((ui_app_wc.style & CS_DBLCLKS) == 0) { // only if Windows are not detecting DLBCLKs // rt_println("ui_set_timer(timer_d[%d])", ix); ui_set_timer(timer_d[ix], double_click_msec); // 0.5s } ui_set_timer(timer_p[ix], double_click_msec * 3 / 4); // 0.375s } } else if (up) { fp64_t since_clicked = ui_app.now - clicked[ix]; // rt_println("pressed[%d]: %d %.3f", ix, pressed[ix], since_clicked); // only if Windows are not detecting DLBCLKs if ((ui_app_wc.style & CS_DBLCLKS) == 0 && pressed[ix] && since_clicked > double_click_dt) { ui_view.double_tap(ui_app.root, ix); // rt_println("timer_p[%d] _d && _p timers done", ix); ui_timers_done(ix); } swallow = ui_view.tap(ui_app.root, ix, !up); ui_kill_timer(timer_p[ix]); // long press is not the case } else if (m == double_tap) { rt_assert((ui_app_wc.style & CS_DBLCLKS) != 0); swallow = ui_view.double_tap(ui_app.root, ix); ui_timers_done(ix); // rt_println("timer_p[%d] _d && _p timers done", ix); } } #pragma pop_macro("ui_timers_done") #pragma pop_macro("ui_kill_timer") #pragma pop_macro("ui_set_timer") return swallow; } static int64_t ui_app_root_hit_test(const ui_view_t* v, ui_point_t pt) { rt_swear(v == ui_app.root); if (ui_app.no_decor) { rt_assert(ui_app.border.w == ui_app.border.h); // on 96dpi monitors ui_app.border is 1x1 // make it easier for the user to resize window int32_t border = rt_max(4, ui_app.border.w * 2); if (ui_app.animating.view != null) { return ui.hit_test.client; // message box or toast is up } else if (!ui_view.is_hidden(&ui_caption.view) && ui_view.inside(&ui_caption.view, &pt)) { return ui_caption.view.hit_test(&ui_caption.view, pt); } else if (ui_app.is_maximized()) { int64_t ht = ui_view.hit_test(ui_app.content, pt); return ht == ui.hit_test.nowhere ? ui.hit_test.client : ht; } else if (ui_app.is_full_screen) { return ui.hit_test.client; } else if (pt.x < border && pt.y < border) { return ui.hit_test.top_left; } else if (pt.x > ui_app.crc.w - border && pt.y < border) { return ui.hit_test.top_right; } else if (pt.y < border) { return ui.hit_test.top; } else if (pt.x > ui_app.crc.w - border && pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom_right; } else if (pt.x < border && pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom_left; } else if (pt.x < border) { return ui.hit_test.left; } else if (pt.x > ui_app.crc.w - border) { return ui.hit_test.right; } else if (pt.y > ui_app.crc.h - border) { return ui.hit_test.bottom; } else { // drop down to content hit test } } return ui.hit_test.nowhere; } static void ui_app_wm_activate(int64_t wp) { bool activate = LOWORD(wp) != WA_INACTIVE; if (!IsWindowVisible(ui_app_window()) && activate) { ui_app.show_window(ui.visibility.restore); SwitchToThisWindow(ui_app_window(), true); } ui_app.request_redraw(); // needed for windows changing active frame color } static void ui_app_update_mouse_buttons_state(void) { ui_app.mouse_swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0; ui_app.mouse_left = (GetAsyncKeyState(ui_app.mouse_swapped ? VK_RBUTTON : VK_LBUTTON) & 0x8000) != 0; ui_app.mouse_right = (GetAsyncKeyState(ui_app.mouse_swapped ? VK_LBUTTON : VK_RBUTTON) & 0x8000) != 0; } static int64_t ui_app_wm_nc_hit_test(int64_t wp, int64_t lp) { ui_point_t pt = { GET_X_LPARAM(lp) - ui_app.wrc.x, GET_Y_LPARAM(lp) - ui_app.wrc.y }; int64_t ht = ui_view.hit_test(ui_app.root, pt); if (ht != ui.hit_test.nowhere) { return ht; } else { return DefWindowProcW(ui_app_window(), WM_NCHITTEST, wp, lp); } } static int64_t ui_app_wm_sys_key_down(int64_t wp, int64_t lp) { ui_app_alt_ctrl_shift(true, wp); if (ui_app_wm_key_pressed(ui_app.root, wp) || wp == VK_MENU) { return 0; // no DefWindowProcW() } else { return DefWindowProcW(ui_app_window(), WM_SYSKEYDOWN, wp, lp); } } static void ui_app_wm_set_focus(void) { if (!ui_app.root->state.hidden) { rt_assert(GetActiveWindow() == ui_app_window()); if (ui_app.focus != null && ui_app.focus->focus_lost != null) { ui_app.focus->focus_gained(ui_app.focus); } } } static void ui_app_wm_kill_focus(void) { if (!ui_app.root->state.hidden && ui_app.focus != null && ui_app.focus->focus_lost != null) { ui_app.focus->focus_lost(ui_app.focus); } } static int64_t ui_app_wm_nc_calculate_size(int64_t wp, int64_t lp) { // NCCALCSIZE_PARAMS* szp = (NCCALCSIZE_PARAMS*)lp; // rt_println("WM_NCCALCSIZE wp: %lld is_max: %d (%d %d %d %d) (%d %d %d %d) (%d %d %d %d)", // wp, ui_app.is_maximized(), // szp->rgrc[0].left, szp->rgrc[0].top, szp->rgrc[0].right, szp->rgrc[0].bottom, // szp->rgrc[1].left, szp->rgrc[1].top, szp->rgrc[1].right, szp->rgrc[1].bottom, // szp->rgrc[2].left, szp->rgrc[2].top, szp->rgrc[2].right, szp->rgrc[2].bottom); // adjust window client area frame for no_decor windows if (wp == true && ui_app.no_decor && !ui_app.is_maximized()) { return 0; } else { return DefWindowProcW(ui_app_window(), WM_NCCALCSIZE, wp, lp); } } static int64_t ui_app_wm_get_dpi_scaled_size(int64_t wp) { // sent before WM_DPICHANGED #ifdef UI_APP_DEBUG int32_t dpi = wp; SIZE* sz = (SIZE*)lp; // in/out ui_point_t cell = { sz->cx, sz->cy }; rt_println("WM_GETDPISCALEDSIZE dpi %d := %d " "size %d,%d *may/must* be adjusted", ui_app.dpi.window, dpi, cell.x, cell.y); #else (void)wp; // unused #endif if (ui_app_timer_1s_id != 0 && !ui_app.root->state.hidden) { ui_app.request_layout(); } // IMPORTANT: return true because: // "Returning TRUE indicates that a new size has been computed. // Returning FALSE indicates that the message will not be handled, // and the default linear DPI scaling will apply to the window." // https://learn.microsoft.com/en-us/windows/win32/hidpi/wm-getdpiscaledsize return true; } static void ui_app_wm_dpi_changed(void) { ui_app_window_dpi(); ui_app_init_fonts(ui_app.dpi.window); if (ui_app_timer_1s_id != 0 && !ui_app.root->state.hidden) { ui_app.request_layout(); } else { ui_app_layout_dirty = true; } } static bool ui_app_wm_sys_command(int64_t wp, int64_t lp) { uint16_t sys_cmd = (uint16_t)(wp & 0xFF0); // rt_println("WM_SYSCOMMAND wp: 0x%08llX lp: 0x%016llX %lld sys: 0x%04X", // wp, lp, lp, sys_cmd); if (sys_cmd == SC_MINIMIZE && ui_app.hide_on_minimize) { ui_app.show_window(ui.visibility.min_na); ui_app.show_window(ui.visibility.hide); } else if (sys_cmd == SC_MINIMIZE && ui_app.no_decor) { ui_app.show_window(ui.visibility.min_na); } // if (sys_cmd == SC_KEYMENU) { rt_println("SC_KEYMENU lp: %lld", lp); } // If the selection is in menu handle the key event if (sys_cmd == SC_KEYMENU && lp != 0x20) { return true; // handled: This prevents the error/beep sound } if (sys_cmd == SC_MAXIMIZE && ui_app.no_decor) { return true; // handled: prevent maximizing no decorations window } // if (sys_cmd == SC_MOUSEMENU) { // rt_println("SC_KEYMENU.SC_MOUSEMENU 0x%00llX %lld", wp, lp); // } return false; // drop down to to DefWindowProc } static void ui_app_wm_window_position_changing(int64_t wp, int64_t lp) { #ifdef UI_APP_DEBUG // TODO: ui_app.debug.trace.window_position? WINDOWPOS* pos = (WINDOWPOS*)lp; rt_println("WM_WINDOWPOSCHANGING flags: 0x%08X", pos->flags); if (pos->flags & SWP_SHOWWINDOW) { rt_println("SWP_SHOWWINDOW"); } else if (pos->flags & SWP_HIDEWINDOW) { rt_println("SWP_HIDEWINDOW"); } #else (void)wp; // unused (void)lp; // unused #endif } static bool ui_app_wm_mouse(int32_t m, int64_t wp, int64_t lp) { // note: x, y is already in client coordinates ui_app.mouse.x = GET_X_LPARAM(lp); ui_app.mouse.y = GET_Y_LPARAM(lp); return ui_app_mouse(ui_app.root, m, wp); } static void ui_app_wm_mouse_wheel(bool vertical, int64_t wp) { if (vertical) { ui_point_t dx_dy = { 0, GET_WHEEL_DELTA_WPARAM(wp) }; ui_view.mouse_scroll(ui_app.root, dx_dy); } else { ui_point_t dx_dy = { GET_WHEEL_DELTA_WPARAM(wp), 0 }; ui_view.mouse_scroll(ui_app.root, dx_dy); } } static void ui_app_wm_input_language_change(uint64_t wp) { #ifdef UI_APP_TRACE_WM_INPUT_LANGUAGE_CHANGE static struct { uint8_t charset; const char* name; } cs[] = { { ANSI_CHARSET , "ANSI_CHARSET " }, { DEFAULT_CHARSET , "DEFAULT_CHARSET " }, { SYMBOL_CHARSET , "SYMBOL_CHARSET " }, { MAC_CHARSET , "MAC_CHARSET " }, { SHIFTJIS_CHARSET , "SHIFTJIS_CHARSET " }, { HANGEUL_CHARSET , "HANGEUL_CHARSET " }, { HANGUL_CHARSET , "HANGUL_CHARSET " }, { GB2312_CHARSET , "GB2312_CHARSET " }, { CHINESEBIG5_CHARSET, "CHINESEBIG5_CHARSET" }, { OEM_CHARSET , "OEM_CHARSET " }, { JOHAB_CHARSET , "JOHAB_CHARSET " }, { HEBREW_CHARSET , "HEBREW_CHARSET " }, { ARABIC_CHARSET , "ARABIC_CHARSET " }, { GREEK_CHARSET , "GREEK_CHARSET " }, { TURKISH_CHARSET , "TURKISH_CHARSET " }, { VIETNAMESE_CHARSET , "VIETNAMESE_CHARSET " }, { THAI_CHARSET , "THAI_CHARSET " }, { EASTEUROPE_CHARSET , "EASTEUROPE_CHARSET " }, { RUSSIAN_CHARSET , "RUSSIAN_CHARSET " }, { BALTIC_CHARSET , "BALTIC_CHARSET " } }; for (int32_t i = 0; i < rt_countof(cs); i++) { if (cs[i].charset == wp) { rt_println("WM_INPUTLANGCHANGE: 0x%08X %s", wp, cs[i].name); break; } } #else (void)wp; // unused #endif } static void ui_app_decode_keyboard(int32_t m, int64_t wp, int64_t lp) { // https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags rt_swear(m == WM_KEYDOWN || m == WM_SYSKEYDOWN || m == WM_KEYUP || m == WM_SYSKEYUP); uint16_t vk_code = LOWORD(wp); uint16_t key_flags = HIWORD(lp); uint16_t scan_code = LOBYTE(key_flags); if ((key_flags & KF_EXTENDED) == KF_EXTENDED) { scan_code = MAKEWORD(scan_code, 0xE0); } // previous key-state flag, 1 on autorepeat bool was_key_down = (key_flags & KF_REPEAT) == KF_REPEAT; // repeat count, > 0 if several key down messages was combined into one uint16_t repeat_count = LOWORD(lp); // transition-state flag, 1 on key up bool is_key_released = (key_flags & KF_UP) == KF_UP; // if we want to distinguish these keys: switch (vk_code) { case VK_SHIFT: // converts to VK_LSHIFT or VK_RSHIFT case VK_CONTROL: // converts to VK_LCONTROL or VK_RCONTROL case VK_MENU: // converts to VK_LMENU or VK_RMENU vk_code = LOWORD(MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX)); break; default: break; } static BYTE keyboard_state[256]; uint16_t utf16[3] = {0}; rt_fatal_win32err(GetKeyboardState(keyboard_state)); // HKL low word Language Identifier // high word device handle to the physical layout of the keyboard const HKL kl = GetKeyboardLayout(0); // Map virtual key to scan code UINT vk = MapVirtualKeyEx(scan_code, MAPVK_VSC_TO_VK_EX, kl); // rt_println("virtual_key: %02X keyboard layout: %08X", // virtual_key, kl); memset(ui_app_decoded_released, 0x00, sizeof(ui_app_decoded_released)); memset(ui_app_decoded_pressed, 0x00, sizeof(ui_app_decoded_pressed)); // Translate scan code to character int32_t r = ToUnicodeEx(vk, scan_code, keyboard_state, utf16, rt_countof(utf16), 0, kl); if (r > 0) { rt_static_assertion(rt_countof(ui_app_decoded_pressed) == rt_countof(ui_app_decoded_released)); enum { capacity = (int32_t)rt_countof(ui_app_decoded_released) }; char* utf8 = is_key_released ? ui_app_decoded_released : ui_app_decoded_pressed; rt_str.utf16to8(utf8, capacity, utf16, -1); if (ui_app_trace_utf16_keyboard_input) { rt_println("0x%04X%04X released: %d down: %d repeat: %d \"%s\"", utf16[0], utf16[1], is_key_released, was_key_down, repeat_count, utf8); } } else if (r == 0) { // The specified virtual key has no translation for the // current state of the keyboard. (E.g. arrows, enter etc) } else { rt_assert(r < 0); // The specified virtual key is a dead key character (accent or diacritic). if (ui_app_trace_utf16_keyboard_input) { rt_println("dead key"); } } } static void ui_app_ime_composition(int64_t lp) { if (lp & GCS_RESULTSTR) { HIMC imc = ImmGetContext(ui_app_window()); if (imc != null) { char utf8[16]; uint16_t utf16[4] = {0}; uint32_t bytes = ImmGetCompositionStringW(imc, GCS_RESULTSTR, null, 0); uint32_t count = bytes / sizeof(uint16_t); if (0 < count && count < rt_countof(utf16) - 1) { ImmGetCompositionStringW(imc, GCS_RESULTSTR, utf16, bytes); utf16[count] = 0x00; rt_str.utf16to8(utf8, rt_countof(utf8), utf16, -1); rt_println("bytes: %d 0x%04X 0x%04X %s", bytes, utf16[0], utf16[1], utf8); } rt_fatal_win32err(ImmReleaseContext(ui_app_window(), imc)); } } } static LRESULT CALLBACK ui_app_window_proc(HWND window, UINT message, WPARAM w_param, LPARAM l_param) { ui_app.now = rt_clock.seconds(); if (ui_app.window == null) { ui_app.window = (ui_window_t)window; } else { rt_assert(ui_app_window() == window); } rt_work_queue.dispatch(&ui_app_queue); ui_app_update_wt_timeout(); // because head might have changed const int32_t m = (int32_t)message; const int64_t wp = (int64_t)w_param; const int64_t lp = (int64_t)l_param; int64_t ret = 0; ui_app_update_mouse_buttons_state(); ui_view.lose_hidden_focus(ui_app.root); if (ui_app_click_detector((uint32_t)m, (WPARAM)wp, (LPARAM)lp)) { return 0; } if (ui_view.message(ui_app.root, m, wp, lp, &ret)) { return (LRESULT)ret; } if (m == ui.message.opening) { ui_app_window_opening(); return 0; } if (m == ui.message.closing) { ui_app_window_closing(); return 0; } if (m == ui.message.animate) { ui_app_animate_step((ui_app_animate_function_t)lp, (int32_t)wp, -1); return 0; } ui_app_message_handler_t* handler = ui_app.handlers; while (handler != null) { if (handler->callback(handler, m, wp, lp, &ret)) { return ret; } handler = handler->next; } switch (m) { case WM_GETMINMAXINFO: ui_app_get_min_max_info((MINMAXINFO*)lp); break; case WM_CLOSE : ui_view.set_focus(null); // before WM_CLOSING ui_app_post_message(ui.message.closing, 0, 0); return 0; case WM_DESTROY : PostQuitMessage(ui_app.exit_code); break; case WM_ACTIVATE : ui_app_wm_activate(wp); break; case WM_SYSCOMMAND : if (ui_app_wm_sys_command(wp, lp)) { return 0; } break; case WM_WINDOWPOSCHANGING: ui_app_wm_window_position_changing(wp, lp); break; case WM_WINDOWPOSCHANGED: ui_app_window_position_changed((WINDOWPOS*)lp); break; case WM_NCHITTEST : return ui_app_wm_nc_hit_test(wp, lp); case WM_SYSKEYDOWN : return ui_app_wm_sys_key_down(wp, lp); case WM_SYSCHAR : if (wp == VK_MENU) { return 0; } // swallow - no DefWindowProc() break; case WM_KEYDOWN : ui_app_alt_ctrl_shift(true, wp); if (ui_app_wm_key_pressed(ui_app.root, wp)) { return 0; } // swallow break; case WM_SYSKEYUP: case WM_KEYUP : ui_app_alt_ctrl_shift(false, wp); ui_view.key_released(ui_app.root, wp); break; case WM_TIMER : ui_app_wm_timer((ui_timer_t)wp); break; case WM_ERASEBKGND : return true; // no DefWindowProc() case WM_INPUTLANGCHANGE: ui_app_wm_input_language_change(wp); break; case WM_CHAR : ui_app_wm_char(ui_app.root, (const uint16_t*)&wp); break; case WM_PRINTCLIENT : ui_app_paint_on_canvas((HDC)wp); break; case WM_SETFOCUS : ui_app_wm_set_focus(); break; case WM_KILLFOCUS : ui_app_wm_kill_focus(); break; case WM_NCCALCSIZE: return ui_app_wm_nc_calculate_size(wp, lp); case WM_PAINT : ui_app_wm_paint(); break; case WM_CONTEXTMENU : (void)ui_view.context_menu(ui_app.root); break; case WM_THEMECHANGED : ui_theme.refresh(); break; case WM_SETTINGCHANGE: ui_app_setting_change((uintptr_t)wp, (uintptr_t)lp); break; case WM_GETDPISCALEDSIZE: // sent before WM_DPICHANGED return ui_app_wm_get_dpi_scaled_size(wp); case WM_DPICHANGED : ui_app_wm_dpi_changed(); break; case WM_NCLBUTTONDOWN : case WM_NCRBUTTONDOWN : case WM_NCMBUTTONDOWN : case WM_NCLBUTTONUP : case WM_NCRBUTTONUP : case WM_NCMBUTTONUP : case WM_NCLBUTTONDBLCLK : case WM_NCRBUTTONDBLCLK: case WM_NCMBUTTONDBLCLK: case WM_NCMOUSEMOVE : ui_app_nc_mouse_buttons(m, wp, lp); break; case WM_LBUTTONDOWN : case WM_RBUTTONDOWN : case WM_MBUTTONDOWN : case WM_LBUTTONUP : case WM_RBUTTONUP : case WM_MBUTTONUP : case WM_LBUTTONDBLCLK : case WM_RBUTTONDBLCLK: case WM_MBUTTONDBLCLK: // if (m == WM_LBUTTONDOWN) { rt_println("WM_LBUTTONDOWN"); } // if (m == WM_LBUTTONUP) { rt_println("WM_LBUTTONUP"); } // if (m == WM_LBUTTONDBLCLK) { rt_println("WM_LBUTTONDBLCLK"); } if (ui_app_wm_mouse(m, wp, lp)) { return 0; } break; case WM_MOUSEHOVER : case WM_MOUSEMOVE : if (ui_app_wm_mouse(m, wp, lp)) { return 0; } break; case WM_MOUSEWHEEL : ui_app_wm_mouse_wheel(true, wp); break; case WM_MOUSEHWHEEL : ui_app_wm_mouse_wheel(false, wp); break; // debugging: #ifdef UI_APP_DEBUGING_ALT_KEYBOARD_SHORTCUTS case WM_PARENTNOTIFY : rt_println("WM_PARENTNOTIFY"); break; case WM_ENTERMENULOOP : rt_println("WM_ENTERMENULOOP"); return 0; case WM_EXITMENULOOP : rt_println("WM_EXITMENULOOP"); return 0; case WM_INITMENU : rt_println("WM_INITMENU"); return 0; case WM_MENUCHAR : rt_println("WM_MENUCHAR"); return MNC_CLOSE << 16; case WM_CAPTURECHANGED: rt_println("WM_CAPTURECHANGED"); break; case WM_MENUSELECT : rt_println("WM_MENUSELECT"); return 0; #else // ***Important***: prevents annoying beeps on Alt+Shortcut case WM_MENUCHAR : return MNC_CLOSE << 16; // TODO: may be beeps are good if no UI controls reacted #endif // TODO: investigate WM_SETCURSOR in regards to wait cursor case WM_SETCURSOR : if (LOWORD(lp) == HTCLIENT) { // see WM_NCHITTEST SetCursor((HCURSOR)ui_app.cursor); return true; // must NOT call DefWindowProc() } break; #ifdef UI_APP_USE_WM_IME case WM_IME_CHAR: rt_println("WM_IME_CHAR: 0x%04X", wp); break; case WM_IME_NOTIFY: rt_println("WM_IME_NOTIFY"); break; case WM_IME_REQUEST: rt_println("WM_IME_REQUEST"); break; case WM_IME_STARTCOMPOSITION: rt_println("WM_IME_STARTCOMPOSITION"); break; case WM_IME_ENDCOMPOSITION: rt_println("WM_IME_ENDCOMPOSITION"); break; case WM_IME_COMPOSITION: rt_println("WM_IME_COMPOSITION"); ui_app_ime_composition(lp); break; #endif // UI_APP_USE_WM_IME // TODO: case WM_UNICHAR : // only UTF-32 via PostMessage? rt_println("???"); // see: https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-tounicode break; default: break; } return DefWindowProcW(ui_app_window(), (UINT)m, (WPARAM)wp, lp); } static long ui_app_get_window_long(int32_t index) { rt_core.set_err(0); long v = GetWindowLongA(ui_app_window(), index); rt_fatal_if_error(rt_core.err()); return v; } static long ui_app_set_window_long(int32_t index, long value) { rt_core.set_err(0); long r = SetWindowLongA(ui_app_window(), index, value); // r previous value rt_fatal_if_error(rt_core.err()); return r; } static void ui_app_modify_window_style(uint32_t include, uint32_t exclude) { long s = ui_app_get_window_long(GWL_STYLE); s &= ~exclude; s |= include; ui_app_set_window_long(GWL_STYLE, s); } static DWORD ui_app_window_style(void) { return ui_app.no_decor ? WS_POPUPWINDOW| WS_THICKFRAME| WS_MINIMIZEBOX : WS_OVERLAPPEDWINDOW; } static errno_t ui_app_set_layered_window(ui_color_t color, fp32_t alpha) { uint8_t a = 0; // alpha 0..255 uint32_t c = 0; // R8G8B8 DWORD mask = 0; if (0 <= alpha && alpha <= 1.0f) { mask |= LWA_ALPHA; a = (uint8_t)(alpha * 255 + 0.5f); } if (color != ui_color_undefined) { mask |= LWA_COLORKEY; rt_assert(ui_color_is_8bit(color)); c = ui_gdi.color_rgb(color); } return rt_b2e(SetLayeredWindowAttributes(ui_app_window(), c, a, mask)); } static void ui_app_set_dwm_attribute(uint32_t mode, void* a, DWORD bytes) { rt_fatal_if_error(DwmSetWindowAttribute(ui_app_window(), mode, a, bytes)); } static void ui_app_init_dwm(void) { if (IsWindowsVersionOrGreater(10, 0, 22000)) { // do not call on Win10 - will fail DWM_WINDOW_CORNER_PREFERENCE c = DWMWCP_ROUND; ui_app_set_dwm_attribute(DWMWA_WINDOW_CORNER_PREFERENCE, &c, sizeof(c)); COLORREF cc = (COLORREF)ui_gdi.color_rgb(ui_color_rgb(45, 45, 48)); ui_app_set_dwm_attribute(DWMWA_CAPTION_COLOR, &cc, sizeof(cc)); } BOOL e = true; // must be 32-bit BOOL because of sizeof() ui_app_set_dwm_attribute(DWMWA_USE_IMMERSIVE_DARK_MODE, &e, sizeof(e)); // kudos for double negatives - so easy to make mistakes: ui_app_set_dwm_attribute(DWMWA_TRANSITIONS_FORCEDISABLED, &e, sizeof(e)); enum DWMNCRENDERINGPOLICY rp = DWMNCRP_USEWINDOWSTYLE; ui_app_set_dwm_attribute(DWMWA_NCRENDERING_POLICY, &rp, sizeof(rp)); if (ui_app.no_decor) { ui_app_set_dwm_attribute(DWMWA_ALLOW_NCPAINT, &e, sizeof(e)); MARGINS margins = { 0, 0, 0, 0 }; rt_fatal_if_error( DwmExtendFrameIntoClientArea(ui_app_window(), &margins) ); } } static void ui_app_swp(HWND top, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t f) { rt_fatal_win32err(SetWindowPos(ui_app_window(), top, x, y, w, h, f)); } static void ui_app_swp_flags(uint32_t f) { rt_fatal_win32err(SetWindowPos(ui_app_window(), null, 0, 0, 0, 0, f)); } static void ui_app_disable_sys_menu_item(HMENU sys_menu, uint32_t item) { const uint32_t f = MF_BYCOMMAND | MF_DISABLED; rt_fatal_win32err(EnableMenuItem(sys_menu, item, f)); } static void ui_app_init_sys_menu(void) { // tried to remove unused items from system menu which leads to // AllowDarkModeForWindow() failed 0x000005B0(1456) "A menu item was not found." // SetPreferredAppMode() failed 0x000005B0(1456) "A menu item was not found." // this is why they just disabled instead. HMENU sys_menu = GetSystemMenu(ui_app_window(), false); rt_not_null(sys_menu); if (ui_app.no_min || ui_app.no_max) { int32_t exclude = WS_SIZEBOX; if (ui_app.no_min) { exclude = WS_MINIMIZEBOX; } if (ui_app.no_max) { exclude = WS_MAXIMIZEBOX; } ui_app_modify_window_style(0, exclude); if (ui_app.no_min) { ui_app_disable_sys_menu_item(sys_menu, SC_MINIMIZE); } if (ui_app.no_max) { ui_app_disable_sys_menu_item(sys_menu, SC_MAXIMIZE); } } if (ui_app.no_size) { ui_app_disable_sys_menu_item(sys_menu, SC_SIZE); ui_app_modify_window_style(0, WS_SIZEBOX); const uint32_t f = SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE; ui_app_swp_flags(f); } } static void ui_app_create_window(const ui_rect_t r) { uint16_t class_name[256]; rt_str.utf8to16(class_name, rt_countof(class_name), ui_app.class_name, -1); WNDCLASSW* wc = &ui_app_wc; // CS_DBLCLKS no longer needed. Because code detects long-press // it does double click too. Editor uses both for word and paragraph select. wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_SAVEBITS; wc->lpfnWndProc = ui_app_window_proc; wc->cbClsExtra = 0; wc->cbWndExtra = 256 * 1024; wc->hInstance = GetModuleHandleA(null); wc->hIcon = (HICON)ui_app.icon; wc->hCursor = (HCURSOR)ui_app.cursor; wc->hbrBackground = null; wc->lpszMenuName = null; wc->lpszClassName = class_name; ATOM atom = RegisterClassW(wc); rt_fatal_if(atom == 0); uint16_t title[256]; rt_str.utf8to16(title, rt_countof(title), ui_app.title, -1); HWND window = CreateWindowExW(WS_EX_COMPOSITED | WS_EX_LAYERED, class_name, title, ui_app_window_style(), r.x, r.y, r.w, r.h, null, null, wc->hInstance, null); rt_not_null(ui_app.window); rt_swear(window == ui_app_window()); ui_app.show_window(ui.visibility.hide); ui_view.set_text(&ui_caption.title, "%s", ui_app.title); ui_app.dpi.window = (int32_t)GetDpiForWindow(ui_app_window()); RECT wrc = ui_app_ui2rect(&r); rt_fatal_win32err(GetWindowRect(ui_app_window(), &wrc)); ui_app.wrc = ui_app_rect2ui(&wrc); ui_app_init_dwm(); ui_app_init_sys_menu(); ui_theme.refresh(); if (ui_app.visibility != ui.visibility.hide) { AnimateWindow(ui_app_window(), 250, AW_ACTIVATE); ui_app.show_window(ui_app.visibility); ui_app_update_crc(); } // even if it is hidden: ui_app_post_message(ui.message.opening, 0, 0); // SetWindowTheme(ui_app_window(), L"DarkMode_Explorer", null); ??? } static void ui_app_full_screen(bool on) { static long style; static WINDOWPLACEMENT wp; if (on != ui_app.is_full_screen) { ui_app_show_task_bar(!on); if (on) { ui_app_modify_window_style(0, WS_OVERLAPPEDWINDOW|WS_POPUPWINDOW); ui_app_modify_window_style(WS_POPUP | WS_VISIBLE, 0); wp.length = sizeof(wp); rt_fatal_win32err(GetWindowPlacement(ui_app_window(), &wp)); WINDOWPLACEMENT nwp = wp; nwp.showCmd = SW_SHOWNORMAL; nwp.rcNormalPosition = (RECT){ui_app.mrc.x, ui_app.mrc.y, ui_app.mrc.x + ui_app.mrc.w, ui_app.mrc.y + ui_app.mrc.h}; rt_fatal_win32err(SetWindowPlacement(ui_app_window(), &nwp)); } else { rt_fatal_win32err(SetWindowPlacement(ui_app_window(), &wp)); ui_app_set_window_long(GWL_STYLE, ui_app_window_style()); enum { flags = SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER }; ui_app_swp_flags(flags); } ui_app.is_full_screen = on; } } static bool ui_app_set_focus(ui_view_t* rt_unused(v)) { return false; } static void ui_app_request_redraw(void) { // < 2us SetEvent(ui_app_event_invalidate); } static void ui_app_draw(void) { rt_println("avoid at all cost. bad performance, bad UX"); UpdateWindow(ui_app_window()); } static void ui_app_invalidate_rect(const ui_rect_t* r) { RECT rc = ui_app_ui2rect(r); InvalidateRect(ui_app_window(), &rc, false); // rt_backtrace_here(); } static int32_t ui_app_message_loop(void) { MSG msg = {0}; while (GetMessageW(&msg, null, 0, 0)) { if (msg.message == WM_KEYDOWN || msg.message == WM_KEYUP || msg.message == WM_SYSKEYDOWN || msg.message == WM_SYSKEYUP) { // before TranslateMessage(): ui_app_decode_keyboard(msg.message, msg.wParam, msg.lParam); } TranslateMessage(&msg); DispatchMessageW(&msg); } rt_work_queue.flush(&ui_app_queue); rt_assert(msg.message == WM_QUIT); return (int32_t)msg.wParam; } static void ui_app_dispose(void) { ui_app_dispose_fonts(); rt_event.dispose(ui_app_event_invalidate); ui_app_event_invalidate = null; } static void ui_app_cursor_set(ui_cursor_t c) { // https://docs.microsoft.com/en-us/windows/win32/menurc/using-cursors ui_app.cursor = c; SetClassLongPtr(ui_app_window(), GCLP_HCURSOR, (LONG_PTR)c); POINT pt = {0}; if (GetCursorPos(&pt)) { SetCursorPos(pt.x + 1, pt.y); SetCursorPos(pt.x, pt.y); } } static void ui_app_close_window(void) { // TODO: fix me. Band aid - start up with maximized no_decor window is broken if (ui_app.is_maximized()) { ui_app.show_window(ui.visibility.restore); } ui_app_post_message(WM_CLOSE, 0, 0); } static void ui_app_quit(int32_t exit_code) { ui_app.exit_code = exit_code; if (ui_app.can_close != null) { (void)ui_app.can_close(); // and deliberately ignore result } ui_app.can_close = null; // will not be called again ui_app.close(); // close and destroy app only window } static void ui_app_show_hint_or_toast(ui_view_t* v, int32_t x, int32_t y, fp64_t timeout) { if (v != null) { ui_app.animating.x = x; ui_app.animating.y = y; ui_app.animating.focused = ui_app.focus; if (v->type == ui_view_mbx) { ((ui_mbx_t*)v)->option = -1; if (v->focusable) { ui_view.set_focus(v); } } // allow unparented ui for toast and hint ui_view_call_init(v); const int32_t steps = x < 0 && y < 0 ? ui_app_animation_steps : 1; ui_app_animate_start(ui_app_toast_dim, steps); ui_app.animating.view = v; v->parent = ui_app.root; if (v->focusable) { ui_view.set_focus(v); } ui_app.animating.time = timeout > 0 ? ui_app.now + timeout : 0; } else { ui_app_toast_cancel(); } } static void ui_app_show_toast(ui_view_t* view, fp64_t timeout) { ui_app_show_hint_or_toast(view, -1, -1, timeout); } static void ui_app_show_hint(ui_view_t* view, int32_t x, int32_t y, fp64_t timeout) { if (view != null) { ui_app_show_hint_or_toast(view, x, y, timeout); } else if (ui_app.animating.view != null && ui_app.animating.x >= 0 && ui_app.animating.y >= 0) { ui_app_toast_cancel(); // only cancel hints not toasts } } static void ui_app_formatted_toast_va(fp64_t timeout, const char* format, va_list va) { ui_app_show_toast(null, 0); static ui_label_t label = ui_label(0.0, ""); ui_label_init_va(&label, 0.0, format, va); ui_app_show_toast(&label, timeout); } static void ui_app_formatted_toast(fp64_t timeout, const char* format, ...) { va_list va; va_start(va, format); ui_app_formatted_toast_va(timeout, format, va); va_end(va); } static int32_t ui_app_caret_w; static int32_t ui_app_caret_h; static int32_t ui_app_caret_x = -1; static int32_t ui_app_caret_y = -1; static bool ui_app_caret_shown; static void ui_app_create_caret(int32_t w, int32_t h) { ui_app_caret_w = w; ui_app_caret_h = h; rt_fatal_win32err(CreateCaret(ui_app_window(), null, w, h)); rt_assert(GetSystemMetrics(SM_CARETBLINKINGENABLED)); } static void ui_app_invalidate_caret(void) { if (ui_app_caret_w > 0 && ui_app_caret_h > 0 && ui_app_caret_x >= 0 && ui_app_caret_y >= 0 && ui_app_caret_shown) { RECT rc = { ui_app_caret_x, ui_app_caret_y, ui_app_caret_x + ui_app_caret_w, ui_app_caret_y + ui_app_caret_h }; rt_fatal_win32err(InvalidateRect(ui_app_window(), &rc, false)); } } static void ui_app_show_caret(void) { rt_assert(!ui_app_caret_shown); rt_fatal_win32err(ShowCaret(ui_app_window())); ui_app_caret_shown = true; ui_app_invalidate_caret(); } static void ui_app_move_caret(int32_t x, int32_t y) { ui_app_invalidate_caret(); // where is was ui_app_caret_x = x; ui_app_caret_y = y; rt_fatal_win32err(SetCaretPos(x, y)); ui_app_invalidate_caret(); // where it is now } static void ui_app_hide_caret(void) { rt_assert(ui_app_caret_shown); rt_fatal_win32err(HideCaret(ui_app_window())); ui_app_invalidate_caret(); ui_app_caret_shown = false; } static void ui_app_destroy_caret(void) { ui_app_caret_w = 0; ui_app_caret_h = 0; rt_fatal_win32err(DestroyCaret()); } static void ui_app_beep(int32_t kind) { static int32_t beep_id[] = { MB_OK, MB_ICONINFORMATION, MB_ICONQUESTION, MB_ICONWARNING, MB_ICONERROR}; rt_swear(0 <= kind && kind < rt_countof(beep_id)); rt_fatal_win32err(MessageBeep(beep_id[kind])); } static void ui_app_enable_sys_command_close(void) { EnableMenuItem(GetSystemMenu(GetConsoleWindow(), false), SC_CLOSE, MF_BYCOMMAND | MF_ENABLED); } static void ui_app_console_disable_close(void) { EnableMenuItem(GetSystemMenu(GetConsoleWindow(), false), SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); (void)freopen("CONOUT$", "w", stdout); (void)freopen("CONOUT$", "w", stderr); atexit(ui_app_enable_sys_command_close); } static int ui_app_console_attach(void) { int r = AttachConsole(ATTACH_PARENT_PROCESS) ? 0 : rt_core.err(); if (r == 0) { ui_app_console_disable_close(); rt_thread.sleep_for(0.1); // give cmd.exe a chance to print prompt again printf("\n"); } return r; } static bool ui_app_is_stdout_redirected(void) { // https://stackoverflow.com/questions/30126490/how-to-check-if-stdout-is-redirected-to-a-file-or-to-a-console HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE); DWORD type = out == null ? FILE_TYPE_UNKNOWN : GetFileType(out); type &= ~(DWORD)FILE_TYPE_REMOTE; // FILE_TYPE_DISK or FILE_TYPE_CHAR or FILE_TYPE_PIPE return type != FILE_TYPE_UNKNOWN; } static bool ui_app_is_console_visible(void) { HWND cw = GetConsoleWindow(); return cw != null && IsWindowVisible(cw); } static int ui_app_set_console_size(int16_t w, int16_t h) { // width/height in characters HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); if (r != 0) { rt_println("GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); } else { // tricky because correct order of the calls // SetConsoleWindowInfo() SetConsoleScreenBufferSize() depends on // current Window Size (in pixels) ConsoleWindowSize(in characters) // and SetConsoleScreenBufferSize(). // After a lot of experimentation and reading docs most sensible option // is to try both calls in two different orders. COORD c = {w, h}; SMALL_RECT const min_win = { 0, 0, c.X - 1, c.Y - 1 }; c.Y = 9001; // maximum buffer number of rows at the moment of implementation int r0 = SetConsoleWindowInfo(console, true, &min_win) ? 0 : rt_core.err(); // if (r0 != 0) { rt_println("SetConsoleWindowInfo() %s", rt_strerr(r0)); } int r1 = SetConsoleScreenBufferSize(console, c) ? 0 : rt_core.err(); // if (r1 != 0) { rt_println("SetConsoleScreenBufferSize() %s", rt_strerr(r1)); } if (r0 != 0 || r1 != 0) { // try in reverse order (which expected to work): r0 = SetConsoleScreenBufferSize(console, c) ? 0 : rt_core.err(); if (r0 != 0) { rt_println("SetConsoleScreenBufferSize() %s", rt_strerr(r0)); } r1 = SetConsoleWindowInfo(console, true, &min_win) ? 0 : rt_core.err(); if (r1 != 0) { rt_println("SetConsoleWindowInfo() %s", rt_strerr(r1)); } } r = r0 == 0 ? r1 : r0; // first of two errors } return r; } static void ui_app_console_largest(void) { HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); // User have to manual uncheck "[x] Let system position window" in console // Properties -> Layout -> Window Position because I did not find the way // to programmatically unchecked it. // commented code below does not work. // see: https://www.os2museum.com/wp/disabling-quick-edit-mode/ // and: https://learn.microsoft.com/en-us/windows/console/setconsolemode /* DOES NOT WORK: DWORD mode = 0; r = GetConsoleMode(console, &mode) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleMode() %s", rt_strerr(r)); mode &= ~ENABLE_AUTO_POSITION; r = SetConsoleMode(console, &mode) ? 0 : rt_core.err(); rt_fatal_if_error(r, "SetConsoleMode() %s", rt_strerr(r)); */ CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); COORD c = GetLargestConsoleWindowSize(console); if (c.X > 80) { c.X &= ~0x7; } if (c.Y > 24) { c.Y &= ~0x3; } if (c.X > 80) { c.X -= 8; } if (c.Y > 24) { c.Y -= 4; } ui_app_set_console_size(c.X, c.Y); r = GetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "GetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); info.dwSize.Y = 9999; // maximum value at the moment of implementation r = SetConsoleScreenBufferInfoEx(console, &info) ? 0 : rt_core.err(); rt_fatal_if_error(r, "SetConsoleScreenBufferInfoEx() %s", rt_strerr(r)); ui_app_save_console_pos(); } static void ui_app_make_topmost(void) { // Places the window above all non-topmost windows. // The window maintains its topmost position even when it is deactivated. enum { swp = SWP_SHOWWINDOW | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOSIZE }; ui_app_swp(HWND_TOPMOST, 0, 0, 0, 0, swp); } static void ui_app_activate(void) { rt_core.set_err(0); HWND previous = SetActiveWindow(ui_app_window()); if (previous == null) { rt_fatal_if_error(rt_core.err()); } } static void ui_app_bring_to_foreground(void) { // SetForegroundWindow() does not activate window: rt_fatal_win32err(SetForegroundWindow(ui_app_window())); } static void ui_app_bring_to_front(void) { ui_app.bring_to_foreground(); ui_app.make_topmost(); ui_app.bring_to_foreground(); // because bring_to_foreground() does not activate ui_app.activate(); ui_app.request_focus(); } static void ui_app_set_title(const char* title) { ui_view.set_text(&ui_caption.title, "%s", title); rt_fatal_win32err(SetWindowTextA(ui_app_window(), rt_nls.str(title))); } static void ui_app_capture_mouse(bool on) { static int32_t mouse_capture; if (on) { rt_swear(mouse_capture == 0); mouse_capture++; SetCapture(ui_app_window()); } else { rt_swear(mouse_capture == 1); mouse_capture--; ReleaseCapture(); } } static void ui_app_move_and_resize(const ui_rect_t* rc) { enum { swp = SWP_NOZORDER | SWP_NOACTIVATE }; ui_app_swp(null, rc->x, rc->y, rc->w, rc->h, swp); } static void ui_app_set_console_title(HWND cw) { rt_swear(rt_thread.id() == ui_app.tid); static char text[256]; text[0] = 0; GetWindowTextA((HWND)ui_app.window, text, rt_countof(text)); text[rt_countof(text) - 1] = 0; char title[256]; rt_str_printf(title, "%s - Console", text); rt_fatal_win32err(SetWindowTextA(cw, title)); } static void ui_app_restore_console(int32_t *visibility) { HWND cw = GetConsoleWindow(); if (cw != null) { RECT wr = {0}; GetWindowRect(cw, &wr); ui_rect_t rc = ui_app_rect2ui(&wr); ui_app_load_console_pos(&rc, visibility); if (rc.w > 0 && rc.h > 0) { // rt_println("%d,%d %dx%d px", rc.x, rc.y, rc.w, rc.h); CONSOLE_SCREEN_BUFFER_INFOEX info = { sizeof(CONSOLE_SCREEN_BUFFER_INFOEX) }; int32_t r = rt_config.load(ui_app.class_name, "console_screen_buffer_infoex", &info, (int32_t)sizeof(info)); if (r == sizeof(info)) { // 24x80 SMALL_RECT sr = info.srWindow; int16_t w = (int16_t)rt_max(sr.Right - sr.Left + 1, 80); int16_t h = (int16_t)rt_max(sr.Bottom - sr.Top + 1, 24); // rt_println("info: %dx%d", info.dwSize.X, info.dwSize.Y); // rt_println("%d,%d %dx%d", sr.Left, sr.Top, w, h); if (w > 0 && h > 0) { ui_app_set_console_size(w, h); } } // do not resize console window just restore it's position enum { flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE }; rt_fatal_win32err(SetWindowPos(cw, null, rc.x, rc.y, rc.w, rc.h, flags)); } else { ui_app_console_largest(); } } } static void ui_app_console_show(bool b) { HWND cw = GetConsoleWindow(); if (cw != null && b != ui_app.is_console_visible()) { if (ui_app.is_console_visible()) { ui_app_save_console_pos(); } if (b) { int32_t ignored_visibility = 0; ui_app_restore_console(&ignored_visibility); ui_app_set_console_title(cw); } // If the window was previously visible, the return value is nonzero. // If the window was previously hidden, the return value is zero. bool unused_was_visible = ShowWindow(cw, b ? SW_SHOWNOACTIVATE : SW_HIDE); (void)unused_was_visible; if (b) { InvalidateRect(cw, null, true); SetActiveWindow(cw); } ui_app_save_console_pos(); // again after visibility changed } } static int ui_app_console_create(void) { int r = AllocConsole() ? 0 : rt_core.err(); if (r == 0) { ui_app_console_disable_close(); int32_t visibility = 0; ui_app_restore_console(&visibility); ui_app.console_show(visibility != 0); } return r; } static fp32_t ui_app_px2in(int32_t pixels) { rt_assert(ui_app.dpi.monitor_max > 0); // rt_println("ui_app.dpi.monitor_raw: %d", ui_app.dpi.monitor_max); return ui_app.dpi.monitor_max > 0 ? (fp32_t)pixels / (fp32_t)ui_app.dpi.monitor_max : 0; } static int32_t ui_app_in2px(fp32_t inches) { rt_assert(ui_app.dpi.monitor_max > 0); // rt_println("ui_app.dpi.monitor_raw: %d", ui_app.dpi.monitor_max); return (int32_t)(inches * (fp64_t)ui_app.dpi.monitor_max + 0.5); } static void ui_app_request_layout(void) { ui_app_layout_dirty = true; ui_app.request_redraw(); } static void ui_app_show_window(int32_t show) { rt_assert(ui.visibility.hide <= show && show <= ui.visibility.force_min); // ShowWindow() does not have documented error reporting bool was_visible = ShowWindow(ui_app_window(), show); (void)was_visible; const bool hiding = show == ui.visibility.hide || show == ui.visibility.minimize || show == ui.visibility.show_na || show == ui.visibility.min_na; if (!hiding) { ui_app.bring_to_foreground(); // this does not make it ActiveWindow enum { flags = SWP_SHOWWINDOW | SWP_NOZORDER | SWP_NOSIZE | SWP_NOREPOSITION | SWP_NOMOVE }; ui_app_swp_flags(flags); ui_app.request_focus(); } else if (show == ui.visibility.hide || show == ui.visibility.minimize || show == ui.visibility.min_na) { ui_app_toast_cancel(); } } static const char* ui_app_open_file(const char* folder, const char* pairs[], int32_t n) { rt_swear(rt_thread.id() == ui_app.tid); rt_assert(pairs == null && n == 0 || n >= 2 && n % 2 == 0); static uint16_t memory[4 * 1024]; uint16_t* filter = memory; if (pairs == null || n == 0) { filter = L"All Files\0*\0\0"; } else { int32_t left = rt_countof(memory) - 2; uint16_t* s = memory; for (int32_t i = 0; i < n; i+= 2) { uint16_t* s0 = s; rt_str.utf8to16(s0, left, pairs[i + 0], -1); int32_t n0 = (int32_t)rt_str.len16(s0); rt_assert(n0 > 0); s += n0 + 1; left -= n0 + 1; uint16_t* s1 = s; rt_str.utf8to16(s1, left, pairs[i + 1], -1); int32_t n1 = (int32_t)rt_str.len16(s1); rt_assert(n1 > 0); s[n1] = 0; s += n1 + 1; left -= n1 + 1; } *s++ = 0; } static uint16_t dir[rt_files_max_path]; dir[0] = 0; rt_str.utf8to16(dir, rt_countof(dir), folder, -1); static uint16_t path[rt_files_max_path]; path[0] = 0; OPENFILENAMEW ofn = { sizeof(ofn) }; ofn.hwndOwner = (HWND)ui_app.window; ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; ofn.lpstrFilter = filter; ofn.lpstrInitialDir = dir; ofn.lpstrFile = path; ofn.nMaxFile = sizeof(path); static rt_file_name_t fn; fn.s[0] = 0; if (GetOpenFileNameW(&ofn) && path[0] != 0) { rt_str.utf16to8(fn.s, rt_countof(fn.s), path, -1); } else { fn.s[0] = 0; } return fn.s; } // TODO: use clipboard instead? static errno_t ui_app_clipboard_put_image(ui_bitmap_t* im) { HDC canvas = GetDC(null); rt_not_null(canvas); HDC src = CreateCompatibleDC(canvas); rt_not_null(src); HDC dst = CreateCompatibleDC(canvas); rt_not_null(dst); // CreateCompatibleBitmap(dst) will create monochrome bitmap! // CreateCompatibleBitmap(canvas) will create display compatible HBITMAP texture = CreateCompatibleBitmap(canvas, im->w, im->h); rt_not_null(texture); HBITMAP s = SelectBitmap(src, im->texture); rt_not_null(s); HBITMAP d = SelectBitmap(dst, texture); rt_not_null(d); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(dst, 0, 0, &pt)); rt_fatal_win32err(StretchBlt(dst, 0, 0, im->w, im->h, src, 0, 0, im->w, im->h, SRCCOPY)); errno_t r = rt_b2e(OpenClipboard(GetDesktopWindow())); if (r != 0) { rt_println("OpenClipboard() failed %s", rt_strerr(r)); } if (r == 0) { r = rt_b2e(EmptyClipboard()); if (r != 0) { rt_println("EmptyClipboard() failed %s", rt_strerr(r)); } } if (r == 0) { r = rt_b2e(SetClipboardData(CF_BITMAP, texture)); if (r != 0) { rt_println("SetClipboardData() failed %s", rt_strerr(r)); } } if (r == 0) { r = rt_b2e(CloseClipboard()); if (r != 0) { rt_println("CloseClipboard() failed %s", rt_strerr(r)); } } rt_not_null(SelectBitmap(dst, d)); rt_not_null(SelectBitmap(src, s)); rt_fatal_win32err(DeleteBitmap(texture)); rt_fatal_win32err(DeleteDC(dst)); rt_fatal_win32err(DeleteDC(src)); rt_fatal_win32err(ReleaseDC(null, canvas)); return r; } static ui_view_t ui_app_view = ui_view(list); static ui_view_t ui_app_content = ui_view(stack); static bool ui_app_is_active(void) { return GetActiveWindow() == ui_app_window(); } static bool ui_app_is_minimized(void) { return IsIconic(ui_app_window()); } static bool ui_app_is_maximized(void) { return IsZoomed(ui_app_window()); } static bool ui_app_focused(void) { return GetFocus() == ui_app_window(); } static void window_request_focus(void* w) { // https://stackoverflow.com/questions/62649124/pywin32-setfocus-resulting-in-access-is-denied-error // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput rt_assert(rt_thread.id() == ui_app.tid, "cannot be called from background thread"); rt_core.set_err(0); HWND previous = SetFocus((HWND)w); // previously focused window if (previous == null) { rt_fatal_if_error(rt_core.err()); } } static void ui_app_request_focus(void) { window_request_focus(ui_app.window); } static void ui_app_init(void) { ui_app_event_quit = rt_event.create_manual(); ui_app_event_invalidate = rt_event.create(); ui_app.request_redraw = ui_app_request_redraw; ui_app.post = ui_app_post; ui_app.draw = ui_app_draw; ui_app.px2in = ui_app_px2in; ui_app.in2px = ui_app_in2px; ui_app.set_layered_window = ui_app_set_layered_window; ui_app.is_active = ui_app_is_active; ui_app.is_minimized = ui_app_is_minimized; ui_app.is_maximized = ui_app_is_maximized; ui_app.focused = ui_app_focused; ui_app.request_focus = ui_app_request_focus; ui_app.activate = ui_app_activate; ui_app.set_title = ui_app_set_title; ui_app.capture_mouse = ui_app_capture_mouse; ui_app.move_and_resize = ui_app_move_and_resize; ui_app.bring_to_foreground = ui_app_bring_to_foreground; ui_app.make_topmost = ui_app_make_topmost; ui_app.bring_to_front = ui_app_bring_to_front; ui_app.request_layout = ui_app_request_layout; ui_app.invalidate = ui_app_invalidate_rect; ui_app.full_screen = ui_app_full_screen; ui_app.set_cursor = ui_app_cursor_set; ui_app.close = ui_app_close_window; ui_app.quit = ui_app_quit; ui_app.set_timer = ui_app_timer_set; ui_app.kill_timer = ui_app_timer_kill; ui_app.show_window = ui_app_show_window; ui_app.show_toast = ui_app_show_toast; ui_app.show_hint = ui_app_show_hint; ui_app.toast_va = ui_app_formatted_toast_va; ui_app.toast = ui_app_formatted_toast; ui_app.create_caret = ui_app_create_caret; ui_app.show_caret = ui_app_show_caret; ui_app.move_caret = ui_app_move_caret; ui_app.hide_caret = ui_app_hide_caret; ui_app.destroy_caret = ui_app_destroy_caret; ui_app.beep = ui_app_beep; ui_app.data_save = ui_app_data_save; ui_app.data_size = ui_app_data_size; ui_app.data_load = ui_app_data_load; ui_app.open_file = ui_app_open_file; ui_app.is_stdout_redirected = ui_app_is_stdout_redirected; ui_app.is_console_visible = ui_app_is_console_visible; ui_app.console_attach = ui_app_console_attach; ui_app.console_create = ui_app_console_create; ui_app.console_show = ui_app_console_show; ui_app.root = &ui_app_view; ui_app.content = &ui_app_content; ui_app.caption = &ui_caption.view; ui_app.root->hit_test = ui_app_root_hit_test; ui_view.add(ui_app.root, ui_app.caption, ui_app.content, null); ui_view_call_init(ui_app.root); // to get done with container_init() rt_assert(ui_app.content->type == ui_view_stack); rt_assert(ui_app.content->background == ui_colors.transparent); ui_app.root->color_id = ui_color_id_window_text; ui_app.root->background_id = ui_color_id_window; ui_app.root->insets = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.root->padding = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.root->paint = ui_app_view_paint; ui_app.root->max_w = ui.infinity; ui_app.root->max_h = ui.infinity; ui_app.content->insets = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.content->padding = (ui_margins_t){ 0, 0, 0, 0 }; ui_app.content->max_w = ui.infinity; ui_app.content->max_h = ui.infinity; ui_app.caption->state.hidden = !ui_app.no_decor; // for ui_view_debug_paint: ui_view.set_text(ui_app.root, "ui_app.root"); ui_view.set_text(ui_app.content, "ui_app.content"); if (ui_app.init != null) { ui_app.init(); } } static void ui_app_set_dpi_awareness(void) { // Mutually exclusive: // BOOL SetProcessDpiAwarenessContext() // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext // and // HRESULT SetProcessDpiAwareness() // https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-setprocessdpiawareness // Plus DPI awareness can be set by APP .exe shell properties, registry // or Windows policy. See: // https://blogs.windows.com/windowsdeveloper/2017/05/19/improving-high-dpi-experience-gdi-based-desktop-apps/ DPI_AWARENESS_CONTEXT dpi_awareness_context_1 = GetThreadDpiAwarenessContext(); // https://blogs.windows.com/windowsdeveloper/2017/05/19/improving-high-dpi-experience-gdi-based-desktop-apps/ errno_t error = rt_b2e(SetProcessDpiAwarenessContext( DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)); if (error == ERROR_ACCESS_DENIED) { rt_println("Warning: SetProcessDpiAwarenessContext(): ERROR_ACCESS_DENIED"); // dpi awareness already set, manifest, registry, windows policy // Try via Shell: HRESULT hr = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); if (hr == E_ACCESSDENIED) { rt_println("Warning: SetProcessDpiAwareness(): E_ACCESSDENIED"); } } DPI_AWARENESS_CONTEXT dpi_awareness_context_2 = GetThreadDpiAwarenessContext(); rt_swear(dpi_awareness_context_1 != dpi_awareness_context_2); } static void ui_app_init_windows(void) { ui_app_set_dpi_awareness(); InitCommonControls(); // otherwise GetOpenFileName does not work ui_app.dpi.process = (int32_t)GetSystemDpiForProcess(GetCurrentProcess()); ui_app.dpi.system = (int32_t)GetDpiForSystem(); // default was 96DPI // monitor dpi will be reinitialized in load_window_pos ui_app.dpi.monitor_effective = ui_app.dpi.system; ui_app.dpi.monitor_angular = ui_app.dpi.system; ui_app.dpi.monitor_raw = ui_app.dpi.system; ui_app.dpi.monitor_max = ui_app.dpi.system; // rt_println("ui_app.dpi.monitor_max := %d", ui_app.dpi.system); static const RECT nowhere = {0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF}; ui_rect_t r = ui_app_rect2ui(&nowhere); ui_app_update_mi(&r, MONITOR_DEFAULTTOPRIMARY); ui_app.dpi.window = ui_app.dpi.monitor_effective; } static ui_rect_t ui_app_window_initial_rectangle(void) { const ui_window_sizing_t* ws = &ui_app.window_sizing; // it is not practical and thus not implemented handling // == (0, 0) and != (0, 0) for sizing half dimension (only w or only h) rt_swear((ws->min_w != 0) == (ws->min_h != 0) && ws->min_w >= 0 && ws->min_h >= 0, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f", ws->min_w, ws->min_h); rt_swear((ws->ini_w != 0) == (ws->ini_h != 0) && ws->ini_w >= 0 && ws->ini_h >= 0, "ui_app.window_sizing .ini_w=%.1f .ini_h=%.1f", ws->ini_w, ws->ini_h); rt_swear((ws->max_w != 0) == (ws->max_h != 0) && ws->max_w >= 0 && ws->max_h >= 0, "ui_app.window_sizing .max_w=%.1f .max_h=%.1f", ws->max_w, ws->max_h); // if max is set then min and ini must be less than max if (ws->max_w != 0 || ws->max_h != 0) { rt_swear(ws->min_w <= ws->max_w && ws->min_h <= ws->max_h, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f .max_w=%1.f .max_h=%.1f", ws->min_w, ws->min_h, ws->max_w, ws->max_h); rt_swear(ws->ini_w <= ws->max_w && ws->ini_h <= ws->max_h, "ui_app.window_sizing .min_w=%.1f .min_h=%.1f .max_w=%1.f .max_h=%.1f", ws->ini_w, ws->ini_h, ws->max_w, ws->max_h); } const int32_t ini_w = ui_app.in2px(ws->ini_w); const int32_t ini_h = ui_app.in2px(ws->ini_h); int32_t min_w = ws->min_w > 0 ? ui_app.in2px(ws->min_w) : ui_app.work_area.w / 4; int32_t min_h = ws->min_h > 0 ? ui_app.in2px(ws->min_h) : ui_app.work_area.h / 4; // (x, y) (-1, -1) means "let Windows manager position the window" ui_rect_t r = {-1, -1, ini_w > 0 ? ini_w : min_w, ini_h > 0 ? ini_h : min_h}; return r; } static FILE* ui_app_crash_log; static bool ui_app_write_backtrace(const char* s, int32_t n) { if (n > 0 && s[n - 1] == 0) { n--; } if (n > 0 && ui_app_crash_log != null) { fwrite(s, n, 1, ui_app_crash_log); } return false; } static LONG ui_app_exception_filter(EXCEPTION_POINTERS* ep) { char fn[1024]; DWORD ex = ep->ExceptionRecord->ExceptionCode; // exception code // T-connector for intercepting rt_debug.output: bool (*tee)(const char* s, int32_t n) = rt_debug.tee; rt_debug.tee = ui_app_write_backtrace; const char* home = rt_files.known_folder(rt_files.folder.home); if (home != null) { const char* name = ui_app.class_name != null ? ui_app.class_name : "ui_app"; rt_str_printf(fn, "%s\\%s_crash_log.txt", home, name); ui_app_crash_log = fopen(fn, "w"); } rt_debug.println(null, 0, null, "To file and issue report copy this log and"); rt_debug.println(null, 0, null, "paste it here: https://github.com/leok7v/ui/discussions/4"); rt_debug.println(null, 0, null, "%s exception: %s", rt_args.basename(), rt_str.error(ex)); rt_backtrace_t bt = {{0}}; rt_backtrace.context(rt_thread.self(), ep->ContextRecord, &bt); rt_backtrace.trace(&bt, "*"); rt_backtrace.trace_all_but_self(); rt_debug.tee = tee; if (ui_app_crash_log != null) { fclose(ui_app_crash_log); char cmd[1024]; rt_str_printf(cmd, "cmd.exe /c start notepad \"%s\"", fn); system(cmd); } return EXCEPTION_CONTINUE_SEARCH; } #undef UI_APP_TEST_POST #ifdef UI_APP_TEST_POST // The dispatch_until() is just for testing purposes. // Usually rt_work_queue.dispatch(q) will be called inside each // iteration of message loop of a dispatch [UI] thread. static void ui_app_test_dispatch_until(rt_work_queue_t* q, int32_t* i, const int32_t n) { while (q->head != null && *i < n) { rt_thread.sleep_for(0.0001); // 100 microseconds rt_work_queue.dispatch(q); } rt_work_queue.flush(q); } // simple way of passing a single pointer to call_later static void ui_app_test_every_100ms(rt_work_t* w) { int32_t* i = (int32_t*)w->data; rt_println("i: %d", *i); (*i)++; w->when = rt_clock.seconds() + 0.100; rt_work_queue.post(w); } static void ui_app_test_work_queue_1(void) { rt_work_queue_t queue = {0}; // if a single pointer will suffice int32_t i = 0; rt_work_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.100, .work = ui_app_test_every_100ms, .data = &i }; rt_work_queue.post(&work); ui_app_test_dispatch_until(&queue, &i, 4); } // extending rt_work_t with extra data: typedef struct rt_work_ex_s { union { rt_work_t base; struct rt_work_s; }; struct { int32_t a; int32_t b; } s; int32_t i; } rt_work_ex_t; static void ui_app_test_every_200ms(rt_work_t* w) { rt_work_ex_t* ex = (rt_work_ex_t*)w; rt_println("ex { .i: %d, .s.a: %d .s.b: %d}", ex->i, ex->s.a, ex->s.b); ex->i++; const int32_t swap = ex->s.a; ex->s.a = ex->s.b; ex->s.b = swap; w->when = rt_clock.seconds() + 0.200; rt_work_queue.post(w); } static void ui_app_test_work_queue_2(void) { rt_work_queue_t queue = {0}; rt_work_ex_t work = { .queue = &queue, .when = rt_clock.seconds() + 0.200, .work = ui_app_test_every_200ms, .data = null, .s = { .a = 1, .b = 2 }, .i = 0 }; rt_work_queue.post(&work.base); ui_app_test_dispatch_until(&queue, &work.i, 4); } static fp64_t ui_app_test_timestamp_0; static fp64_t ui_app_test_timestamp_2; static fp64_t ui_app_test_timestamp_3; static fp64_t ui_app_test_timestamp_4; static void ui_app_test_in_1_second(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_3 = rt_clock.seconds(); rt_println("ETA 3 seconds"); } static void ui_app_test_in_2_seconds(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_2 = rt_clock.seconds(); rt_println("ETA 2 seconds"); static rt_work_t invoke_in_1_seconds; invoke_in_1_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 1.0, // seconds .work = ui_app_test_in_1_second }; ui_app.post(&invoke_in_1_seconds); } static void ui_app_test_in_4_seconds(rt_work_t* rt_unused(work)) { ui_app_test_timestamp_4 = rt_clock.seconds(); rt_println("ETA 4 seconds"); // expected sequence of callbacks: // 2:732 ui_app_test_in_2_seconds ETA 2 seconds // 3:724 ui_app_test_in_1_second ETA 3 seconds // 4:735 ui_app_test_in_4_seconds ETA 4 seconds fp64_t dt2 = ui_app_test_timestamp_2 - ui_app_test_timestamp_0; fp64_t dt3 = ui_app_test_timestamp_3 - ui_app_test_timestamp_0; fp64_t dt4 = ui_app_test_timestamp_4 - ui_app_test_timestamp_0; // Assuming there were no huge startup delays: swear(1.75 < dt2 < 2.25); swear(2.75 < dt3 < 3.25); swear(3.75 < dt4 < 4.25); } static void ui_app_test_post(void) { ui_app_test_work_queue_1(); ui_app_test_work_queue_2(); rt_println("see Output/Timestamps"); static rt_work_t invoke_in_2_seconds; static rt_work_t invoke_in_4_seconds; ui_app_test_timestamp_0 = rt_clock.seconds(); invoke_in_2_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 2.0, // seconds .work = ui_app_test_in_2_seconds }; invoke_in_4_seconds = (rt_work_t){ .queue = null, // &ui_app_queue will be used .when = rt_clock.seconds() + 4.0, // seconds .work = ui_app_test_in_4_seconds }; ui_app.post(&invoke_in_4_seconds); ui_app.post(&invoke_in_2_seconds); } #endif static int ui_app_win_main(HINSTANCE instance) { // IDI_ICON 101: ui_app.icon = (ui_icon_t)LoadIconW(instance, MAKEINTRESOURCE(101)); ui_app_init_windows(); ui_gdi.init(); rt_clipboard.put_image = ui_app_clipboard_put_image; ui_app.last_visibility = ui.visibility.defau1t; ui_app_init(); int r = 0; // ui_app_dump_dpi(); // It is possible (but not trivial) to ask DWM to create taller tittle bar: // https://learn.microsoft.com/en-us/windows/win32/dwm/customframe // TODO: if any app need to make to app store they will probably ask for it // "wr" Window Rect in pixels: default is -1,-1, ini_w, ini_h ui_rect_t wr = ui_app_window_initial_rectangle(); ui_app.caption_height = (int32_t)GetSystemMetricsForDpi(SM_CYCAPTION, (uint32_t)ui_app.dpi.process); ui_app.border.w = (int32_t)GetSystemMetricsForDpi(SM_CXSIZEFRAME, (uint32_t)ui_app.dpi.process); ui_app.border.h = (int32_t)GetSystemMetricsForDpi(SM_CYSIZEFRAME, (uint32_t)ui_app.dpi.process); if (ui_app.no_decor) { // border is too think (5 pixels) narrow down to 3x3 const int32_t max_border = ui_app.dpi.window <= 100 ? 1 : (ui_app.dpi.window >= 192 ? 3 : 2); ui_app.border.w = rt_min(max_border, ui_app.border.w); ui_app.border.h = rt_min(max_border, ui_app.border.h); } // rt_println("frame: %d,%d caption_height: %d", ui_app.border.w, ui_app.border.h, ui_app.caption_height); // TODO: use AdjustWindowRectEx instead // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-adjustwindowrectex wr.x -= ui_app.border.w; wr.w += ui_app.border.w * 2; wr.y -= ui_app.border.h + ui_app.caption_height; wr.h += ui_app.border.h * 2 + ui_app.caption_height; if (!ui_app_load_window_pos(&wr, &ui_app.last_visibility)) { // first time - center window wr.x = ui_app.work_area.x + (ui_app.work_area.w - wr.w) / 2; wr.y = ui_app.work_area.y + (ui_app.work_area.h - wr.h) / 2; ui_app_bring_window_inside_monitor(&ui_app.mrc, &wr); } ui_app.root->state.hidden = true; // start with ui hidden ui_app.root->fm = &ui_app.fm.prop.normal; ui_app.root->w = wr.w - ui_app.border.w * 2; ui_app.root->h = wr.h - ui_app.border.h * 2 - ui_app.caption_height; ui_app_layout_dirty = true; // layout will be done before first paint rt_not_null(ui_app.class_name); ui_app_wt = (rt_event_t)CreateWaitableTimerA(null, false, null); rt_thread_t alarm = rt_thread.start(ui_app_alarm_thread, null); if (!ui_app.no_ui) { ui_app_create_window(wr); ui_app_init_fonts(ui_app.dpi.window); rt_thread_t redraw = rt_thread.start(ui_app_redraw_thread, null); #ifdef UI_APP_TEST_POST ui_app_test_post(); #endif r = ui_app_message_loop(); // ui_app.fini() must be called before ui_app_dispose() if (ui_app.fini != null) { ui_app.fini(); } rt_event.set(ui_app_event_quit); rt_thread.join(redraw, -1); ui_app_dispose(); if (r == 0 && ui_app.exit_code != 0) { r = ui_app.exit_code; } } else { r = ui_app.main(); if (ui_app.fini != null) { ui_app.fini(); } } rt_event.set(ui_app_event_quit); rt_thread.join(alarm, -1); rt_event.dispose(ui_app_event_quit); ui_app_event_quit = null; rt_event.dispose(ui_app_wt); ui_app_wt = null; ui_gdi.fini(); return r; } #pragma warning(disable: 28251) // inconsistent annotations int WINAPI WinMain(HINSTANCE instance, HINSTANCE rt_unused(previous), char* rt_unused(command), int show) { SetUnhandledExceptionFilter(ui_app_exception_filter); const COINIT co_init = COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY; rt_fatal_if_error(CoInitializeEx(0, co_init)); SetConsoleCP(CP_UTF8); // Expected manifest.xml containing UTF-8 code page // for TranslateMessage and WM_CHAR to deliver UTF-8 characters // see: // https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page // .rc file must have: // 1 RT_MANIFEST "manifest.xml" if (GetACP() != 65001) { rt_println("codepage: %d UTF-8 will not be supported", GetACP()); } // at the moment of writing there is no API call to inform Windows about process // preferred codepage except manifest.xml file in resource #1. // Absence of manifest.xml will result to ancient and useless ANSI 1252 codepage // TODO: may need to change CreateWindowA() to CreateWindowW() and // translate UTF16 to UTF8 ui_app.tid = rt_thread.id(); rt_nls.init(); ui_app.visibility = show; rt_args.WinMain(); int32_t r = ui_app_win_main(instance); rt_args.fini(); return r; } int main(int argc, const char* argv[], const char** envp) { SetUnhandledExceptionFilter(ui_app_exception_filter); rt_fatal_if_error(CoInitializeEx(0, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY)); rt_args.main(argc, argv, envp); rt_nls.init(); ui_app.tid = rt_thread.id(); int r = ui_app.main(); rt_args.fini(); return r; } #pragma pop_macro("ui_app_canvas") #pragma pop_macro("ui_app_window") #pragma comment(lib, "comctl32") #pragma comment(lib, "comdlg32") #pragma comment(lib, "dwmapi") #pragma comment(lib, "gdi32") #pragma comment(lib, "msimg32") #pragma comment(lib, "shcore") #pragma comment(lib, "uxtheme") ================================================ FILE: src/ui/ui_button.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static void ui_button_every_100ms(ui_view_t* v) { // every 100ms if (!v->state.hidden) { v->p.armed_until = 0; v->state.armed = false; } else if (v->p.armed_until != 0 && ui_app.now > v->p.armed_until) { v->p.armed_until = 0; v->state.armed = false; ui_view.invalidate(v, null); } if (v->p.armed_until != 0) { ui_app.show_hint(null, -1, -1, 0); } } static void ui_button_paint(ui_view_t* v) { bool pressed = (v->state.armed ^ v->state.pressed) == 0; if (v->p.armed_until != 0) { pressed = true; } const int32_t w = v->w; const int32_t h = v->h; const int32_t x = v->x; const int32_t y = v->y; const int32_t r = (0x1 | rt_max(3, v->fm->em.h / 4)); // odd radius const fp32_t d = ui_theme.is_app_dark() ? 0.50f : 0.25f; ui_color_t d0 = ui_colors.darken(v->background, d); const fp32_t d2 = d / 2; if (v->flat) { if (v->state.hover) { ui_color_t d1 = ui_theme.is_app_dark() ? ui_colors.lighten(v->background, d2) : ui_colors.darken(v->background, d2); if (!pressed) { ui_gdi.gradient(x, y, w, h, d0, d1, true); } else { ui_gdi.gradient(x, y, w, h, d1, d0, true); } } } else { // `bc` border color ui_color_t bc = ui_colors.get_color(ui_color_id_gray_text); if (v->state.armed) { bc = ui_colors.lighten(bc, 0.125f); } if (ui_view.is_disabled(v)) { bc = ui_color_rgb(30, 30, 30); } // TODO: hardcoded if (v->state.hover && !v->state.armed) { bc = ui_colors.get_color(ui_color_id_hot_tracking); } ui_color_t d1 = ui_colors.darken(v->background, d2); ui_color_t fc = ui_colors.interpolate(d0, d1, 0.5f); // fill color if (v->state.armed) { fc = ui_colors.lighten(fc, 0.250f); } else if (v->state.hover) { fc = ui_colors.darken(fc, 0.250f); } ui_gdi.rounded(v->x, v->y, v->w, v->h, r, bc, fc); } const int32_t tx = v->x + v->text.xy.x; const int32_t ty = v->y + v->text.xy.y; if (v->icon == null) { ui_color_t c = v->color; if (v->state.hover && !v->state.armed) { c = ui_theme.is_app_dark() ? ui_color_rgb(0xFF, 0xE0, 0xE0) : ui_color_rgb(0x00, 0x40, 0xFF); } if (ui_view.is_disabled(v)) { c = ui_colors.get_color(ui_color_id_gray_text); } if (v->debug.paint.fm) { ui_view.debug_paint_fm(v); } const ui_gdi_ta_t ta = { .fm = v->fm, .color = c }; ui_gdi.text(&ta, tx, ty, "%s", ui_view.string(v)); } else { const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_wh_t i_wh = { .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom }; // TODO: icon text alignment ui_gdi.icon(tx, ty + v->text.xy.y, i_wh.w, i_wh.h, v->icon); } } static void ui_button_callback(ui_button_t* b) { // for flip buttons the state of the button flips // *before* callback. if (b->flip) { b->state.pressed = !b->state.pressed; } const bool pressed = b->state.pressed; if (b->callback != null) { b->callback(b); } if (pressed != b->state.pressed) { if (b->flip) { // warn the client of strange logic: rt_println("strange flip the button with button.flip: true"); // if client wants to flip pressed state manually it // should do it for the button.flip = false } // rt_println("disarmed immediately"); b->p.armed_until = 0; b->state.armed = false; } else { if (b->flip) { // rt_println("disarmed immediately"); b->p.armed_until = 0; b->state.armed = false; } else { // rt_println("will disarm in 1/4 seconds"); b->p.armed_until = ui_app.now + 0.250; } } } static void ui_button_trigger(ui_view_t* v) { ui_button_t* b = (ui_button_t*)v; v->state.armed = true; ui_view.invalidate(v, null); ui_button_callback(b); } static void ui_button_character(ui_view_t* v, const char* utf8) { char ch = utf8[0]; // TODO: multibyte utf8 shortcuts? if (ui_view.is_shortcut_key(v, ch)) { ui_button_trigger(v); } } static bool ui_button_key_pressed(ui_view_t* v, int64_t key) { rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); const bool trigger = ui_app.alt && ui_view.is_shortcut_key(v, key); if (trigger) { ui_button_trigger(v); } return trigger; // swallow if true } static bool ui_button_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { // 'ix' ignored - button index acts on any mouse button const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { ui_view.invalidate(v, null); // always on any press/release inside ui_button_t* b = (ui_button_t*)v; if (pressed && b->flip) { if (b->flip) { ui_button_callback(b); } } else if (pressed) { v->state.armed = true; } else { // released if (!b->flip) { ui_button_callback(b); } } } return pressed && inside; // swallow clicks inside } void ui_view_init_button(ui_view_t* v) { rt_assert(v->type == ui_view_button); v->tap = ui_button_tap; v->paint = ui_button_paint; v->character = ui_button_character; v->every_100ms = ui_button_every_100ms; v->key_pressed = ui_button_key_pressed; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; if (v->debug.id == null) { v->debug.id = "#button"; } } void ui_button_init(ui_button_t* b, const char* label, fp32_t ems, void (*callback)(ui_button_t* b)) { b->type = ui_view_button; ui_view.set_text(b, "%s", label); b->callback = callback; b->min_w_em = ems; ui_view_init_button(b); } ================================================ FILE: src/ui/ui_caption.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" #pragma push_macro("ui_caption_glyph_rest") #pragma push_macro("ui_caption_glyph_menu") #pragma push_macro("ui_caption_glyph_dark") #pragma push_macro("ui_caption_glyph_light") #pragma push_macro("ui_caption_glyph_mini") #pragma push_macro("ui_caption_glyph_maxi") #pragma push_macro("ui_caption_glyph_full") #pragma push_macro("ui_caption_glyph_quit") #define ui_caption_glyph_rest rt_glyph_white_square_with_upper_right_quadrant // instead of rt_glyph_desktop_window #define ui_caption_glyph_menu rt_glyph_trigram_for_heaven #define ui_caption_glyph_dark rt_glyph_crescent_moon #define ui_caption_glyph_light rt_glyph_white_sun_with_rays #define ui_caption_glyph_mini rt_glyph_minimize #define ui_caption_glyph_maxi rt_glyph_white_square_with_lower_left_quadrant // instead of rt_glyph_maximize #define ui_caption_glyph_full rt_glyph_square_four_corners #define ui_caption_glyph_quit rt_glyph_cancellation_x static void ui_caption_toggle_full(void) { ui_app.full_screen(!ui_app.is_full_screen); ui_caption.view.state.hidden = ui_app.is_full_screen; ui_app.request_layout(); } static void ui_caption_esc_full_screen(ui_view_t* v, const char utf8[]) { rt_swear(v == ui_caption.view.parent); // TODO: inside ui_app.c instead of here? if (utf8[0] == 033 && ui_app.is_full_screen) { ui_caption_toggle_full(); } } static void ui_caption_quit(ui_button_t* rt_unused(b)) { ui_app.close(); } static void ui_caption_mini(ui_button_t* rt_unused(b)) { ui_app.show_window(ui.visibility.minimize); } static void ui_caption_mode_appearance(void) { if (ui_theme.is_app_dark()) { ui_view.set_text(&ui_caption.mode, "%s", ui_caption_glyph_light); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Light Mode")); } else { ui_view.set_text(&ui_caption.mode, "%s", ui_caption_glyph_dark); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Dark Mode")); } } static void ui_caption_mode(ui_button_t* rt_unused(b)) { bool was_dark = ui_theme.is_app_dark(); ui_app.light_mode = was_dark; ui_app.dark_mode = !was_dark; ui_theme.refresh(); ui_caption_mode_appearance(); } static void ui_caption_maximize_or_restore(void) { ui_view.set_text(&ui_caption.maxi, "%s", ui_app.is_maximized() ? ui_caption_glyph_rest : ui_caption_glyph_maxi); rt_str_printf(ui_caption.maxi.hint, "%s", ui_app.is_maximized() ? rt_nls.str("Restore") : rt_nls.str("Maximize")); // non-decorated windows on Win32 are "popup" style // that cannot be maximized. Full screen will serve // the purpose of maximization. ui_caption.maxi.state.hidden = ui_app.no_decor; } static void ui_caption_maxi(ui_button_t* rt_unused(b)) { if (!ui_app.is_maximized()) { ui_app.show_window(ui.visibility.maximize); } else if (ui_app.is_maximized() || ui_app.is_minimized()) { ui_app.show_window(ui.visibility.restore); } ui_caption_maximize_or_restore(); } static void ui_caption_full(ui_button_t* rt_unused(b)) { ui_caption_toggle_full(); } static int64_t ui_caption_hit_test(const ui_view_t* v, ui_point_t pt) { rt_swear(v == &ui_caption.view); rt_assert(ui_view.inside(v, &pt)); // rt_println("%d,%d ui_caption.icon: %d,%d %dx%d inside: %d", // x, y, // ui_caption.icon.x, ui_caption.icon.y, // ui_caption.icon.w, ui_caption.icon.h, // ui_view.inside(&ui_caption.icon, &pt)); if (ui_app.is_full_screen) { return ui.hit_test.client; } else if (!ui_caption.icon.state.hidden && ui_view.inside(&ui_caption.icon, &pt)) { return ui.hit_test.system_menu; } else { ui_view_for_each(&ui_caption.view, c, { bool ignore = c->type == ui_view_stack || c->type == ui_view_spacer || c->type == ui_view_label; if (!ignore && ui_view.inside(c, &pt)) { return ui.hit_test.client; } }); return ui.hit_test.caption; } } static ui_color_t ui_caption_color(void) { ui_color_t c = ui_app.is_active() ? ui_colors.get_color(ui_color_id_active_title) : ui_colors.get_color(ui_color_id_inactive_title); return c; } static const ui_margins_t ui_caption_button_button_padding = { .left = 0.25, .top = 0.0, .right = 0.25, .bottom = 0.0}; static void ui_caption_button_measure(ui_view_t* v) { rt_assert(v->type == ui_view_button); ui_view.measure_control(v); const int32_t dx = ui_app.caption_height - v->w; const int32_t dy = ui_app.caption_height - v->h; v->w += dx; v->h += dy; v->text.xy.x += dx / 2; v->text.xy.y += dy / 2; v->padding = ui_caption_button_button_padding; } static void ui_caption_button_icon_paint(ui_view_t* v) { int32_t w = v->w; int32_t h = v->h; while (h > 16 && (h & (h - 1)) != 0) { h--; } w = h; int32_t dx = (v->w - w) / 2; int32_t dy = (v->h - h) / 2; ui_gdi.icon(v->x + dx, v->y + dy, w, h, v->icon); } static void ui_caption_prepare(ui_view_t* rt_unused(v)) { ui_caption.title.state.hidden = false; } static void ui_caption_measured(ui_view_t* v) { // remeasure all child buttons with hard override: int32_t w = 0; ui_view_for_each(v, it, { if (it->type == ui_view_button) { it->fm = &ui_app.fm.mono.normal; it->flat = true; ui_caption_button_measure(it); } if (!it->state.hidden) { const ui_ltrb_t p = ui_view.margins(it, &it->padding); w += it->w + p.left + p.right; } }); const ui_ltrb_t p = ui_view.margins(v, &v->padding); w += p.left + p.right; // do not show title if there is not enough space ui_caption.title.state.hidden = w > ui_app.root->w; v->w = ui_app.root->w; const ui_ltrb_t insets = ui_view.margins(v, &v->insets); v->h = insets.top + ui_app.caption_height + insets.bottom; } static void ui_caption_composed(ui_view_t* v) { v->x = ui_app.root->x; v->y = ui_app.root->y; } static void ui_caption_paint(ui_view_t* v) { ui_color_t background = ui_caption_color(); ui_gdi.fill(v->x, v->y, v->w, v->h, background); } static void ui_caption_init(ui_view_t* v) { rt_swear(v == &ui_caption.view, "caption is a singleton"); ui_view_init_span(v); ui_caption.view.insets = (ui_margins_t){ 0.125, 0.0, 0.125, 0.0 }; ui_caption.view.state.hidden = false; v->parent->character = ui_caption_esc_full_screen; // ESC for full screen ui_view.add(&ui_caption.view, &ui_caption.icon, &ui_caption.menu, &ui_caption.title, &ui_caption.spacer, &ui_caption.mode, &ui_caption.mini, &ui_caption.maxi, &ui_caption.full, &ui_caption.quit, null); ui_caption.view.color_id = ui_color_id_window_text; static const ui_margins_t p0 = { .left = 0.0, .top = 0.0, .right = 0.0, .bottom = 0.0}; static const ui_margins_t pd = { .left = 0.25, .top = 0.0, .right = 0.25, .bottom = 0.0}; static const ui_margins_t in = { .left = 0.0, .top = 0.0, .right = 0.0, .bottom = 0.0}; ui_view_for_each(&ui_caption.view, c, { c->fm = &ui_app.fm.prop.normal; c->color_id = ui_caption.view.color_id; if (c->type != ui_view_button) { c->padding = pd; } c->insets = in; c->h = ui_app.caption_height; c->min_w_em = 0.5f; c->min_h_em = 0.5f; }); rt_str_printf(ui_caption.menu.hint, "%s", rt_nls.str("Menu")); rt_str_printf(ui_caption.mode.hint, "%s", rt_nls.str("Switch to Light Mode")); rt_str_printf(ui_caption.mini.hint, "%s", rt_nls.str("Minimize")); rt_str_printf(ui_caption.maxi.hint, "%s", rt_nls.str("Maximize")); rt_str_printf(ui_caption.full.hint, "%s", rt_nls.str("Full Screen (ESC to restore)")); rt_str_printf(ui_caption.quit.hint, "%s", rt_nls.str("Close")); ui_caption.icon.icon = ui_app.icon; ui_caption.icon.padding = p0; ui_caption.icon.paint = ui_caption_button_icon_paint; ui_caption.view.align = ui.align.left; ui_caption.view.prepare = ui_caption_prepare; ui_caption.view.measured = ui_caption_measured; ui_caption.view.composed = ui_caption_composed; ui_view.set_text(&ui_caption.view, "#ui_caption"); // for debugging ui_caption_maximize_or_restore(); ui_caption.view.paint = ui_caption_paint; ui_caption_mode_appearance(); ui_caption.icon.debug.id = "#caption.icon"; ui_caption.menu.debug.id = "#caption.menu"; ui_caption.mode.debug.id = "#caption.mode"; ui_caption.mini.debug.id = "#caption.mini"; ui_caption.maxi.debug.id = "#caption.maxi"; ui_caption.full.debug.id = "#caption.full"; ui_caption.quit.debug.id = "#caption.quit"; ui_caption.title.debug.id = "#caption.title"; ui_caption.spacer.debug.id = "#caption.spacer"; } ui_caption_t ui_caption = { .view = { .type = ui_view_span, .fm = &ui_app.fm.prop.normal, .init = ui_caption_init, .hit_test = ui_caption_hit_test, .state.hidden = true }, .icon = ui_button(rt_glyph_nbsp, 0.0, null), .title = ui_label(0, ""), .spacer = ui_view(spacer), .menu = ui_button(ui_caption_glyph_menu, 0.0, null), .mode = ui_button(ui_caption_glyph_mini, 0.0, ui_caption_mode), .mini = ui_button(ui_caption_glyph_mini, 0.0, ui_caption_mini), .maxi = ui_button(ui_caption_glyph_maxi, 0.0, ui_caption_maxi), .full = ui_button(ui_caption_glyph_full, 0.0, ui_caption_full), .quit = ui_button(ui_caption_glyph_quit, 0.0, ui_caption_quit), }; #pragma pop_macro("ui_caption_glyph_rest") #pragma pop_macro("ui_caption_glyph_menu") #pragma pop_macro("ui_caption_glyph_dark") #pragma pop_macro("ui_caption_glyph_light") #pragma pop_macro("ui_caption_glyph_mini") #pragma pop_macro("ui_caption_glyph_maxi") #pragma pop_macro("ui_caption_glyph_full") #pragma pop_macro("ui_caption_glyph_quit") ================================================ FILE: src/ui/ui_colors.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static inline uint8_t ui_color_clamp_uint8(fp64_t value) { return value < 0 ? 0 : (value > 255 ? 255 : (uint8_t)value); } static inline fp64_t ui_color_fp64_min(fp64_t x, fp64_t y) { return x < y ? x : y; } static inline fp64_t ui_color_fp64_max(fp64_t x, fp64_t y) { return x > y ? x : y; } static void ui_color_rgb_to_hsi(fp64_t r, fp64_t g, fp64_t b, fp64_t *h, fp64_t *s, fp64_t *i) { r /= 255.0; g /= 255.0; b /= 255.0; fp64_t min_val = ui_color_fp64_min(r, ui_color_fp64_min(g, b)); *i = (r + g + b) / 3; fp64_t chroma = ui_color_fp64_max(r, ui_color_fp64_max(g, b)) - min_val; if (chroma == 0) { *h = 0; *s = 0; } else { *s = 1 - min_val / *i; if (*i > 0) { *s = chroma / (*i * 3); } if (r == ui_color_fp64_max(r, ui_color_fp64_max(g, b))) { *h = (g - b) / chroma + (g < b ? 6 : 0); } else if (g == ui_color_fp64_max(r, ui_color_fp64_max(g, b))) { *h = (b - r) / chroma + 2; } else { *h = (r - g) / chroma + 4; } *h *= 60; } } static ui_color_t ui_color_hsi_to_rgb(fp64_t h, fp64_t s, fp64_t i, uint8_t a) { h /= 60.0; fp64_t f = h - (int32_t)h; fp64_t p = i * (1 - s); fp64_t q = i * (1 - s * f); fp64_t t = i * (1 - s * (1 - f)); fp64_t r = 0, g = 0, b = 0; switch ((int32_t)h) { case 0: case 6: r = i * 255; g = t * 255; b = p * 255; break; case 1: r = q * 255; g = i * 255; b = p * 255; break; case 2: r = p * 255; g = i * 255; b = t * 255; break; case 3: r = p * 255; g = q * 255; b = i * 255; break; case 4: r = t * 255; g = p * 255; b = i * 255; break; case 5: r = i * 255; g = p * 255; b = q * 255; break; default: rt_swear(false); break; } rt_assert(0 <= r && r <= 255); rt_assert(0 <= g && g <= 255); rt_assert(0 <= b && b <= 255); return ui_color_rgba((uint8_t)r, (uint8_t)g, (uint8_t)b, a); } static ui_color_t ui_color_brightness(ui_color_t c, fp32_t multiplier) { fp64_t h, s, i; ui_color_rgb_to_hsi(ui_color_r(c), ui_color_g(c), ui_color_b(c), &h, &s, &i); i = ui_color_fp64_max(0, ui_color_fp64_min(1, i * (fp64_t)multiplier)); return ui_color_hsi_to_rgb(h, s, i, ui_color_a(c)); } static ui_color_t ui_color_saturation(ui_color_t c, fp32_t multiplier) { fp64_t h, s, i; ui_color_rgb_to_hsi(ui_color_r(c), ui_color_g(c), ui_color_b(c), &h, &s, &i); s = ui_color_fp64_max(0, ui_color_fp64_min(1, s * (fp64_t)multiplier)); return ui_color_hsi_to_rgb(h, s, i, ui_color_a(c)); } // Using the ui_color_interpolate function to blend colors toward // black or white can effectively adjust brightness and saturation, // offering more flexibility and potentially better results in // terms of visual transitions between colors. static ui_color_t ui_color_interpolate(ui_color_t c0, ui_color_t c1, fp32_t multiplier) { rt_assert(0.0f < multiplier && multiplier < 1.0f); fp64_t h0, s0, i0, h1, s1, i1; ui_color_rgb_to_hsi(ui_color_r(c0), ui_color_g(c0), ui_color_b(c0), &h0, &s0, &i0); ui_color_rgb_to_hsi(ui_color_r(c1), ui_color_g(c1), ui_color_b(c1), &h1, &s1, &i1); fp64_t h = h0 + (h1 - h0) * (fp64_t)multiplier; fp64_t s = s0 + (s1 - s0) * (fp64_t)multiplier; fp64_t i = i0 + (i1 - i0) * (fp64_t)multiplier; // Interpolate alphas only if differ uint8_t a0 = ui_color_a(c0); uint8_t a1 = ui_color_a(c1); uint8_t a = a0 == a1 ? a0 : ui_color_clamp_uint8(a0 + (a1 - a0) * (fp64_t)multiplier); return ui_color_hsi_to_rgb(h, s, i, a); } // Helper to get a neutral gray with the same intensity static ui_color_t ui_color_gray_with_same_intensity(ui_color_t c) { uint8_t intensity = (ui_color_r(c) + ui_color_g(c) + ui_color_b(c)) / 3; return ui_color_rgba(intensity, intensity, intensity, ui_color_a(c)); } // Adjust brightness by interpolating towards black or white // using interpolation: // // To darken the color: Interpolate between // the color and black (rgba(0,0,0,255)). // // To lighten the color: Interpolate between // the color and white (rgba(255,255,255,255)). // // This approach allows you to manipulate the // brightness by specifying how close the color // should be to either black or white, // providing a smooth transition. static ui_color_t ui_color_adjust_brightness(ui_color_t c, fp32_t multiplier, bool lighten) { ui_color_t target = lighten ? ui_color_rgba(255, 255, 255, ui_color_a(c)) : ui_color_rgba( 0, 0, 0, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } static ui_color_t ui_color_lighten(ui_color_t c, fp32_t multiplier) { const ui_color_t target = ui_color_rgba(255, 255, 255, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } static ui_color_t ui_color_darken(ui_color_t c, fp32_t multiplier) { const ui_color_t target = ui_color_rgba(0, 0, 0, ui_color_a(c)); return ui_color_interpolate(c, target, multiplier); } // Adjust saturation by interpolating towards a gray of the same intensity // // To adjust saturation, the approach is similar but slightly // more nuanced because saturation involves both the color's // purity and its brightness: static ui_color_t ui_color_adjust_saturation(ui_color_t c, fp32_t multiplier) { ui_color_t gray = ui_color_gray_with_same_intensity(c); return ui_color_interpolate(c, gray, 1 - multiplier); } static struct { const char* name; ui_color_t dark; ui_color_t light; } ui_theme_colors[] = { // empirical { .name = "Undefiled" ,.dark = ui_color_undefined, .light = ui_color_undefined }, { .name = "ActiveTitle" ,.dark = 0x001F1F1F, .light = 0x00D1B499 }, { .name = "ButtonFace" ,.dark = 0x00333333, .light = 0x00F0F0F0 }, { .name = "ButtonText" ,.dark = 0x00C8C8C8, .light = 0x00161616 }, // { .name = "ButtonText" ,.dark = 0x00F6F3EE, .light = 0x00000000 }, { .name = "GrayText" ,.dark = 0x00666666, .light = 0x006D6D6D }, { .name = "Hilight" ,.dark = 0x00626262, .light = 0x00D77800 }, { .name = "HilightText" ,.dark = 0x00000000, .light = 0x00FFFFFF }, { .name = "HotTrackingColor" ,.dark = 0x00B16300, .light = 0x00FF0000 }, // automatic Win11 "accent" ABRG: 0xFFB16300 // { .name = "HotTrackingColor" ,.dark = 0x00B77878, .light = 0x00CC6600 }, { .name = "InactiveTitle" ,.dark = 0x002B2B2B, .light = 0x00DBCDBF }, { .name = "InactiveTitleText",.dark = 0x00969696, .light = 0x00000000 }, { .name = "MenuHilight" ,.dark = 0x00002642, .light = 0x00FF9933 }, { .name = "TitleText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }, // { .name = "Window" ,.dark = 0x00000000, .light = 0x00FFFFFF }, // too contrast // { .name = "Window" ,.dark = 0x00121212, .light = 0x00E0E0E0 }, { .name = "Window" ,.dark = 0x002E2E2E, .light = 0x00E0E0E0 }, { .name = "WindowText" ,.dark = 0x00FFFFFF, .light = 0x00000000 }, }; // TODO: add // Accent Color BGR: B16300 RGB: 0063B1 light blue // [HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM] // "AccentColor"=dword:ffb16300 // Windows used as accent almost on everything // see here: https://github.com/leok7v/ui/discussions/5 static ui_color_t ui_colors_get_color(int32_t color_id) { // SysGetColor() does not work on Win10 rt_swear(0 < color_id && color_id < rt_countof(ui_theme_colors)); return ui_theme.is_app_dark() ? ui_theme_colors[color_id].dark : ui_theme_colors[color_id].light; } ui_colors_if ui_colors = { .get_color = ui_colors_get_color, .rgb_to_hsi = ui_color_rgb_to_hsi, .hsi_to_rgb = ui_color_hsi_to_rgb, .interpolate = ui_color_interpolate, .gray_with_same_intensity = ui_color_gray_with_same_intensity, .lighten = ui_color_lighten, .darken = ui_color_darken, .adjust_saturation = ui_color_adjust_saturation, .multiply_brightness = ui_color_brightness, .multiply_saturation = ui_color_saturation, .transparent = ui_color_transparent, .none = (ui_color_t)0xFFFFFFFFU, // aka CLR_INVALID in wingdi .text = ui_color_rgb(240, 231, 220), .white = ui_color_rgb(255, 255, 255), .black = ui_color_rgb(0, 0, 0), .red = ui_color_rgb(255, 0, 0), .green = ui_color_rgb(0, 255, 0), .blue = ui_color_rgb(0, 0, 255), .yellow = ui_color_rgb(255, 255, 0), .cyan = ui_color_rgb(0, 255, 255), .magenta = ui_color_rgb(255, 0, 255), .gray = ui_color_rgb(128, 128, 128), // tone down RGB colors: .tone_white = ui_color_rgb(164, 164, 164), .tone_red = ui_color_rgb(192, 64, 64), .tone_green = ui_color_rgb(64, 192, 64), .tone_blue = ui_color_rgb(64, 64, 192), .tone_yellow = ui_color_rgb(192, 192, 64), .tone_cyan = ui_color_rgb(64, 192, 192), .tone_magenta = ui_color_rgb(192, 64, 192), // miscellaneous: .orange = ui_color_rgb(255, 165, 0), // 0xFFA500 .dark_green = ui_color_rgb( 1, 50, 32), // 0x013220 .pink = ui_color_rgb(255, 192, 203), // 0xFFC0CB .ochre = ui_color_rgb(204, 119, 34), // 0xCC7722 .gold = ui_color_rgb(255, 215, 0), // 0xFFD700 .teal = ui_color_rgb( 0, 128, 128), // 0x008080 .wheat = ui_color_rgb(245, 222, 179), // 0xF5DEB3 .tan = ui_color_rgb(210, 180, 140), // 0xD2B48C .brown = ui_color_rgb(165, 42, 42), // 0xA52A2A .maroon = ui_color_rgb(128, 0, 0), // 0x800000 .barbie_pink = ui_color_rgb(224, 33, 138), // 0xE0218A .steel_pink = ui_color_rgb(204, 51, 204), // 0xCC33CC .salmon_pink = ui_color_rgb(255, 145, 164), // 0xFF91A4 .gainsboro = ui_color_rgb(220, 220, 220), // 0xDCDCDC .light_gray = ui_color_rgb(211, 211, 211), // 0xD3D3D3 .silver = ui_color_rgb(192, 192, 192), // 0xC0C0C0 .dark_gray = ui_color_rgb(169, 169, 169), // 0xA9A9A9 .dim_gray = ui_color_rgb(105, 105, 105), // 0x696969 .light_slate_gray = ui_color_rgb(119, 136, 153), // 0x778899 .slate_gray = ui_color_rgb(112, 128, 144), // 0x708090 /* Main Panel Backgrounds */ .ennui_black = ui_color_rgb( 18, 18, 18), // 0x1212121 .charcoal = ui_color_rgb( 54, 69, 79), // 0x36454F .onyx = ui_color_rgb( 53, 56, 57), // 0x353839 .gunmetal = ui_color_rgb( 42, 52, 57), // 0x2A3439 .jet_black = ui_color_rgb( 52, 52, 52), // 0x343434 .outer_space = ui_color_rgb( 65, 74, 76), // 0x414A4C .eerie_black = ui_color_rgb( 27, 27, 27), // 0x1B1B1B .oil = ui_color_rgb( 59, 60, 54), // 0x3B3C36 .black_coral = ui_color_rgb( 84, 98, 111), // 0x54626F .obsidian = ui_color_rgb( 58, 50, 45), // 0x3A322D /* Secondary Panels or Sidebars */ .raisin_black = ui_color_rgb( 39, 38, 53), // 0x272635 .dark_charcoal = ui_color_rgb( 48, 48, 48), // 0x303030 .dark_jungle_green = ui_color_rgb( 26, 36, 33), // 0x1A2421 .pine_tree = ui_color_rgb( 42, 47, 35), // 0x2A2F23 .rich_black = ui_color_rgb( 0, 64, 64), // 0x004040 .eclipse = ui_color_rgb( 63, 57, 57), // 0x3F3939 .cafe_noir = ui_color_rgb( 75, 54, 33), // 0x4B3621 /* Flat Buttons */ .prussian_blue = ui_color_rgb( 0, 49, 83), // 0x003153 .midnight_green = ui_color_rgb( 0, 73, 83), // 0x004953 .charleston_green = ui_color_rgb( 35, 43, 43), // 0x232B2B .rich_black_fogra = ui_color_rgb( 10, 15, 13), // 0x0A0F0D .dark_liver = ui_color_rgb( 83, 75, 79), // 0x534B4F .dark_slate_gray = ui_color_rgb( 47, 79, 79), // 0x2F4F4F .black_olive = ui_color_rgb( 59, 60, 54), // 0x3B3C36 .cadet = ui_color_rgb( 83, 104, 114), // 0x536872 /* Button highlights (hover) */ .dark_sienna = ui_color_rgb( 60, 20, 20), // 0x3C1414 .bistre_brown = ui_color_rgb(150, 113, 23), // 0x967117 .dark_puce = ui_color_rgb( 79, 58, 60), // 0x4F3A3C .wenge = ui_color_rgb(100, 84, 82), // 0x645452 /* Raised button effects */ .dark_scarlet = ui_color_rgb( 86, 3, 25), // 0x560319 .burnt_umber = ui_color_rgb(138, 51, 36), // 0x8A3324 .caput_mortuum = ui_color_rgb( 89, 39, 32), // 0x592720 .barn_red = ui_color_rgb(124, 10, 2), // 0x7C0A02 /* Text and Icons */ .platinum = ui_color_rgb(229, 228, 226), // 0xE5E4E2 .anti_flash_white = ui_color_rgb(242, 243, 244), // 0xF2F3F4 .silver_sand = ui_color_rgb(191, 193, 194), // 0xBFC1C2 .quick_silver = ui_color_rgb(166, 166, 166), // 0xA6A6A6 /* Links and Selections */ .dark_powder_blue = ui_color_rgb( 0, 51, 153), // 0x003399 .sapphire_blue = ui_color_rgb( 15, 82, 186), // 0x0F52BA .international_klein_blue = ui_color_rgb( 0, 47, 167), // 0x002FA7 .zaffre = ui_color_rgb( 0, 20, 168), // 0x0014A8 /* Additional Colors */ .fish_belly = ui_color_rgb(232, 241, 212), // 0xE8F1D4 .rusty_red = ui_color_rgb(218, 44, 67), // 0xDA2C43 .falu_red = ui_color_rgb(128, 24, 24), // 0x801818 .cordovan = ui_color_rgb(137, 63, 69), // 0x893F45 .dark_raspberry = ui_color_rgb(135, 38, 87), // 0x872657 .deep_magenta = ui_color_rgb(204, 0, 204), // 0xCC00CC .byzantium = ui_color_rgb(112, 41, 99), // 0x702963 .amethyst = ui_color_rgb(153, 102, 204), // 0x9966CC .wisteria = ui_color_rgb(201, 160, 220), // 0xC9A0DC .lavender_purple = ui_color_rgb(150, 123, 182), // 0x967BB6 .opera_mauve = ui_color_rgb(183, 132, 167), // 0xB784A7 .mauve_taupe = ui_color_rgb(145, 95, 109), // 0x915F6D .rich_lavender = ui_color_rgb(167, 107, 207), // 0xA76BCF .pansy_purple = ui_color_rgb(120, 24, 74), // 0x78184A .violet_eggplant = ui_color_rgb(153, 17, 153), // 0x991199 .jazzberry_jam = ui_color_rgb(165, 11, 94), // 0xA50B5E .dark_orchid = ui_color_rgb(153, 50, 204), // 0x9932CC .electric_purple = ui_color_rgb(191, 0, 255), // 0xBF00FF .sky_magenta = ui_color_rgb(207, 113, 175), // 0xCF71AF .brilliant_rose = ui_color_rgb(230, 103, 206), // 0xE667CE .fuchsia_purple = ui_color_rgb(204, 57, 123), // 0xCC397B .french_raspberry = ui_color_rgb(199, 44, 72), // 0xC72C48 .wild_watermelon = ui_color_rgb(252, 108, 133), // 0xFC6C85 .neon_carrot = ui_color_rgb(255, 163, 67), // 0xFFA343 .burnt_orange = ui_color_rgb(204, 85, 0), // 0xCC5500 .carrot_orange = ui_color_rgb(237, 145, 33), // 0xED9121 .tiger_orange = ui_color_rgb(253, 106, 2), // 0xFD6A02 .giant_onion = ui_color_rgb(176, 181, 137), // 0xB0B589 .rust = ui_color_rgb(183, 65, 14), // 0xB7410E .copper_red = ui_color_rgb(203, 109, 81), // 0xCB6D51 .dark_tangerine = ui_color_rgb(255, 168, 18), // 0xFFA812 .bright_marigold = ui_color_rgb(252, 192, 6), // 0xFCC006 .bone = ui_color_rgb(227, 218, 201), // 0xE3DAC9 /* Earthy Tones */ .sienna = ui_color_rgb(160, 82, 45), // 0xA0522D .sandy_brown = ui_color_rgb(244, 164, 96), // 0xF4A460 .golden_brown = ui_color_rgb(153, 101, 21), // 0x996515 .camel = ui_color_rgb(193, 154, 107), // 0xC19A6B .burnt_sienna = ui_color_rgb(238, 124, 88), // 0xEE7C58 .khaki = ui_color_rgb(195, 176, 145), // 0xC3B091 .dark_khaki = ui_color_rgb(189, 183, 107), // 0xBDB76B /* Greens */ .fern_green = ui_color_rgb( 79, 121, 66), // 0x4F7942 .moss_green = ui_color_rgb(138, 154, 91), // 0x8A9A5B .myrtle_green = ui_color_rgb( 49, 120, 115), // 0x317873 .pine_green = ui_color_rgb( 1, 121, 111), // 0x01796F .jungle_green = ui_color_rgb( 41, 171, 135), // 0x29AB87 .sacramento_green = ui_color_rgb( 4, 57, 39), // 0x043927 /* Blues */ .yale_blue = ui_color_rgb( 15, 77, 146), // 0x0F4D92 .cobalt_blue = ui_color_rgb( 0, 71, 171), // 0x0047AB .persian_blue = ui_color_rgb( 28, 57, 187), // 0x1C39BB .royal_blue = ui_color_rgb( 65, 105, 225), // 0x4169E1 .iceberg = ui_color_rgb(113, 166, 210), // 0x71A6D2 .blue_yonder = ui_color_rgb( 80, 114, 167), // 0x5072A7 /* Miscellaneous */ .cocoa_brown = ui_color_rgb(210, 105, 30), // 0xD2691E .cinnamon_satin = ui_color_rgb(205, 96, 126), // 0xCD607E .fallow = ui_color_rgb(193, 154, 107), // 0xC19A6B .cafe_au_lait = ui_color_rgb(166, 123, 91), // 0xA67B5B .liver = ui_color_rgb(103, 76, 71), // 0x674C47 .shadow = ui_color_rgb(138, 121, 93), // 0x8A795D .cool_grey = ui_color_rgb(140, 146, 172), // 0x8C92AC .payne_grey = ui_color_rgb( 83, 104, 120), // 0x536878 /* Lighter Tones for Contrast */ .timberwolf = ui_color_rgb(219, 215, 210), // 0xDBD7D2 .silver_chalice = ui_color_rgb(172, 172, 172), // 0xACACAC .roman_silver = ui_color_rgb(131, 137, 150), // 0x838996 /* Dark Mode Specific Highlights */ .electric_lavender = ui_color_rgb(244, 191, 255), // 0xF4BFFF .magenta_haze = ui_color_rgb(159, 69, 118), // 0x9F4576 .cyber_grape = ui_color_rgb( 88, 66, 124), // 0x58427C .purple_navy = ui_color_rgb( 78, 81, 128), // 0x4E5180 .liberty = ui_color_rgb( 84, 90, 167), // 0x545AA7 .purple_mountain_majesty = ui_color_rgb(150, 120, 182), // 0x9678B6 .ceil = ui_color_rgb(146, 161, 207), // 0x92A1CF .moonstone_blue = ui_color_rgb(115, 169, 194), // 0x73A9C2 .independence = ui_color_rgb( 76, 81, 109) // 0x4C516D }; ================================================ FILE: src/ui/ui_containers.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" static bool ui_containers_debug; #pragma push_macro("debugln") #pragma push_macro("ui_layout_dump") #pragma push_macro("ui_layout_enter") #pragma push_macro("ui_layout_exit") // Usage of: ui_view_for_each_begin(p, c) { ... } ui_view_for_each_end(p, c) // makes code inside iterator debugger friendly and ensures correct __LINE__ #define debugln(...) do { \ if (ui_containers_debug) { rt_println(__VA_ARGS__); } \ } while (0) static int32_t ui_layout_nesting; #define ui_layout_enter(v) do { \ ui_ltrb_t i_ = ui_view.margins(v, &v->insets); \ ui_ltrb_t p_ = ui_view.margins(v, &v->padding); \ debugln("%*c> %4d,%-4d %4dx%-4d p: %d %d %d %d i: %d %d %d %d %s", \ ui_layout_nesting, 0x20, \ v->x, v->y, v->w, v->h, \ p_.left, p_.top, p_.right, p_.bottom, \ i_.left, i_.top, i_.right, i_.bottom, \ ui_view_debug_id(v)); \ ui_layout_nesting += 4; \ } while (0) #define ui_layout_exit(v) do { \ ui_layout_nesting -= 4; \ debugln("%*c< %4d,%-4d %4dx%-4d %s", \ ui_layout_nesting, 0x20, \ v->x, v->y, v->w, v->h, ui_view_debug_id(v)); \ } while (0) #define ui_layout_clild(v) do { \ debugln("%*c %4d,%-4d %4dx%-4d %s", ui_layout_nesting, 0x20, \ c->x, c->y, c->w, c->h, ui_view_debug_id(v)); \ } while (0) static const char* ui_stack_finite_int(int32_t v, char* text, int32_t count) { rt_swear(v >= 0); if (v == ui.infinity) { rt_str.format(text, count, "%s", rt_glyph_infinity); } else { rt_str.format(text, count, "%d", v); } return text; } #define ui_layout_dump(v) do { \ char maxw[32]; \ char maxh[32]; \ debugln("%s[%4.4s] %4d,%-4d %4dx%-4d, max[%sx%s] " \ "padding { %.3f %.3f %.3f %.3f } " \ "insets { %.3f %.3f %.3f %.3f } align: 0x%02X", \ ui_view_debug_id(v), \ &v->type, v->x, v->y, v->w, v->h, \ ui_stack_finite_int(v->max_w, maxw, rt_countof(maxw)), \ ui_stack_finite_int(v->max_h, maxh, rt_countof(maxh)), \ v->padding.left, v->padding.top, v->padding.right, v->padding.bottom, \ v->insets.left, v->insets.top, v->insets.right, v->insets.bottom, \ v->align); \ } while (0) static void ui_span_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_span, "type %4.4s 0x%08X", &p->type, p->type); ui_ltrb_t insets; ui_view.inbox(p, null, &insets); int32_t w = insets.left; int32_t h = 0; int32_t max_w = w; ui_view_for_each_begin(p, c) { rt_swear(c->max_w == 0 || c->max_w >= c->w, "max_w: %d w: %d", c->max_w, c->w); if (ui_view.is_hidden(c)) { // nothing } else if (c->type == ui_view_spacer) { c->padding = (ui_margins_t){ 0, 0, 0, 0 }; c->w = 0; // layout will distribute excess here c->h = 0; // starts with zero max_w = ui.infinity; // spacer make width greedy } else { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); h = rt_max(h, cbx.h); if (c->max_w == ui.infinity) { max_w = ui.infinity; } else if (max_w < ui.infinity && c->max_w != 0) { rt_swear(c->max_w >= c->w, "c->max_w %d < c->w %d ", c->max_w, c->w); max_w += c->max_w; } else if (max_w < ui.infinity) { rt_swear(0 <= max_w + cbx.w && (int64_t)max_w + (int64_t)cbx.w < (int64_t)ui.infinity, "max_w:%d + cbx.w:%d = %d", max_w, cbx.w, max_w + cbx.w); max_w += cbx.w; } w += cbx.w; } ui_layout_clild(c); } ui_view_for_each_end(p, c); if (0 < max_w && max_w < ui.infinity) { rt_swear(0 <= max_w + insets.right && (int64_t)max_w + (int64_t)insets.right < (int64_t)ui.infinity, "max_w:%d + right:%d = %d", max_w, insets.right, max_w + insets.right); max_w += insets.right; } rt_swear(max_w == 0 || max_w >= w, "max_w: %d w: %d", max_w, w); if (ui_view.is_hidden(p)) { p->w = 0; p->h = 0; } else { p->w = w + insets.right; p->h = insets.top + h + insets.bottom; rt_swear(p->max_w == 0 || p->max_w >= p->w, "max_w: %d is less than actual width: %d", p->max_w, p->w); } ui_layout_exit(p); } // after measure of the subtree is concluded the parent ui_span // may adjust span_w wider number depending on it's own width // and ui_span.max_w agreement static int32_t ui_span_place_child(ui_view_t* c, ui_rect_t pbx, int32_t x) { ui_ltrb_t padding = ui_view.margins(c, &c->padding); // setting child`s max_h to infinity means that child`s height is // *always* fill vertical view size of the parent // childs.h can exceed parent.h (vertical overflow) - is not // encouraged but allowed if (c->max_h == ui.infinity) { // important c->h changed, cbx.h is no longer valid c->h = rt_max(c->h, pbx.h - padding.top - padding.bottom); } int32_t min_y = pbx.y + padding.top; if ((c->align & ui.align.top) != 0) { rt_assert(c->align == ui.align.top); c->y = min_y; } else if ((c->align & ui.align.bottom) != 0) { rt_assert(c->align == ui.align.bottom); c->y = rt_max(min_y, pbx.y + pbx.h - c->h - padding.bottom); } else { // effective height (c->h might have been changed) rt_assert(c->align == ui.align.center, "only top, center, bottom alignment for span"); const int32_t ch = padding.top + c->h + padding.bottom; c->y = rt_max(min_y, pbx.y + (pbx.h - ch) / 2 + padding.top); } c->x = x + padding.left; return c->x + c->w + padding.right; } static void ui_span_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_span, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t spacers = 0; // Number of spacers int32_t max_w_count = 0; int32_t x = p->x + insets.left; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { if (c->type == ui_view_spacer) { c->x = x; c->y = pbx.y; c->h = pbx.h; c->w = 0; spacers++; } else { x = ui_span_place_child(c, pbx, x); rt_swear(c->max_w == 0 || c->max_w >= c->w, "max_w:%d < w:%d", c->max_w, c->w); if (c->max_w > 0) { max_w_count++; } } ui_layout_clild(c); } } ui_view_for_each_end(p, c); int32_t xw = rt_max(0, pbx.x + pbx.w - x); // excess width int32_t max_w_sum = 0; if (xw > 0 && max_w_count > 0) { ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c) && c->type != ui_view_spacer && c->max_w > 0) { max_w_sum += rt_min(c->max_w, xw); ui_layout_clild(c); } } ui_view_for_each_end(p, c); } if (xw > 0 && max_w_count > 0) { debugln("%*c pass 2: fill parent", ui_layout_nesting, 0x20); x = p->x + insets.left; int32_t k = 0; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { rt_swear(padding.left == 0 && padding.right == 0); } else if (c->max_w > 0) { const int32_t max_w = rt_min(c->max_w, xw); int64_t proportional = (xw * (int64_t)max_w) / max_w_sum; rt_assert(proportional <= (int64_t)INT32_MAX); int32_t cw = (int32_t)proportional; c->w = rt_min(c->max_w, c->w + cw); k++; } // TODO: take into account .align of a child and adjust x // depending on ui.align.left/right/center // distributing excess width on the left and right of a child c->x = padding.left + x; x = c->x + padding.left + c->w + padding.right; ui_layout_clild(c); } } ui_view_for_each_end(p, c); rt_swear(k == max_w_count); } // excess width after max_w of non-spacers taken into account xw = rt_max(0, pbx.x + pbx.w - x); if (xw > 0 && spacers > 0) { // evenly distribute excess among spacers debugln("%*c pass 3: expand spacers", ui_layout_nesting, 0x20); int32_t partial = xw / spacers; x = p->x + insets.left; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { c->y = pbx.y; c->w = partial; c->h = pbx.h; spacers--; } c->x = x + padding.left; x = c->x + c->w + padding.right; ui_layout_clild(c); } } ui_view_for_each_end(p, c); } ui_layout_exit(p); } static void ui_list_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_list, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t max_h = insets.top; int32_t h = insets.top; int32_t w = 0; ui_view_for_each_begin(p, c) { rt_swear(c->max_h == 0 || c->max_h >= c->h, "max_h: %d h: %d", c->max_h, c->h); if (!ui_view.is_hidden(c)) { if (c->type == ui_view_spacer) { c->padding = (ui_margins_t){ 0, 0, 0, 0 }; c->h = 0; // layout will distribute excess here max_h = ui.infinity; // spacer make height greedy } else { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); w = rt_max(w, cbx.w); if (c->max_h == ui.infinity) { max_h = ui.infinity; } else if (max_h < ui.infinity && c->max_h != 0) { rt_swear(c->max_h >= c->h, "c->max_h:%d < c->h: %d", c->max_h, c->h); max_h += c->max_h; } else if (max_h < ui.infinity) { rt_swear(0 <= max_h + cbx.h && (int64_t)max_h + (int64_t)cbx.h < (int64_t)ui.infinity, "max_h:%d + ch:%d = %d", max_h, cbx.h, max_h + cbx.h); max_h += cbx.h; } h += cbx.h; } ui_layout_clild(c); } } ui_view_for_each_end(p, c); if (max_h < ui.infinity) { rt_swear(0 <= max_h + insets.bottom && (int64_t)max_h + (int64_t)insets.bottom < (int64_t)ui.infinity, "max_h:%d + bottom:%d = %d", max_h, insets.bottom, max_h + insets.bottom); max_h += insets.bottom; } if (ui_view.is_hidden(p)) { p->w = 0; p->h = 0; } else if (p == ui_app.root) { // ui_app.root is special occupying whole window client rectangle // sans borders and caption thus it should not be re-measured } else { p->h = h + insets.bottom; p->w = insets.left + w + insets.right; } ui_layout_exit(p); } static int32_t ui_list_place_child(ui_view_t* c, ui_rect_t pbx, int32_t y) { ui_ltrb_t padding = ui_view.margins(c, &c->padding); // setting child`s max_w to infinity means that child`s height is // *always* fill vertical view size of the parent // childs.w can exceed parent.w (horizontal overflow) - not encouraged but allowed if (c->max_w == ui.infinity) { c->w = rt_max(c->w, pbx.w - padding.left - padding.right); } int32_t min_x = pbx.x + padding.left; if ((c->align & ui.align.left) != 0) { rt_assert(c->align == ui.align.left); c->x = min_x; } else if ((c->align & ui.align.right) != 0) { rt_assert(c->align == ui.align.right); c->x = rt_max(min_x, pbx.x + pbx.w - c->w - padding.right); } else { rt_assert(c->align == ui.align.center, "only left, center, right, alignment for list"); const int32_t cw = padding.left + c->w + padding.right; c->x = rt_max(min_x, pbx.x + (pbx.w - cw) / 2 + padding.left); } c->y = y + padding.top; return c->y + c->h + padding.bottom; } static void ui_list_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_list, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); int32_t spacers = 0; // Number of spacers int32_t max_h_sum = 0; int32_t max_h_count = 0; int32_t y = pbx.y; ui_view_for_each_begin(p, c) { if (ui_view.is_hidden(c)) { // nothing } else if (c->type == ui_view_spacer) { c->x = pbx.x; c->y = y; c->w = pbx.w; c->h = 0; spacers++; } else { y = ui_list_place_child(c, pbx, y); rt_swear(c->max_h == 0 || c->max_h >= c->h, "max_h:%d < h:%d", c->max_h, c->h); if (c->max_h > 0) { // clamp max_h to the effective parent height max_h_count++; } } } ui_view_for_each_end(p, c); int32_t xh = rt_max(0, pbx.y + pbx.h - y); // excess height if (xh > 0 && max_h_count > 0) { ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c) && c->type != ui_view_spacer && c->max_h > 0) { max_h_sum += rt_min(c->max_h, xh); } } ui_view_for_each_end(p, c); } if (xh > 0 && max_h_count > 0) { debugln("%*c pass 2: fill parent", ui_layout_nesting, 0x20); y = pbx.y; int32_t k = 0; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type != ui_view_spacer && c->max_h > 0) { const int32_t max_h = rt_min(c->max_h, xh); int64_t proportional = (xh * (int64_t)max_h) / max_h_sum; rt_assert(proportional <= (int64_t)INT32_MAX); int32_t ch = (int32_t)proportional; c->h = rt_min(c->max_h, c->h + ch); k++; } int32_t ch = padding.top + c->h + padding.bottom; c->y = y + padding.top; y += ch; ui_layout_clild(c); } } ui_view_for_each_end(p, c); rt_swear(k == max_h_count); } // excess height after max_h of non-spacers taken into account xh = rt_max(0, pbx.y + pbx.h - y); // excess height if (xh > 0 && spacers > 0) { // evenly distribute excess among spacers debugln("%*c pass 3: expand spacers", ui_layout_nesting, 0x20); int32_t partial = xh / spacers; y = pbx.y; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); if (c->type == ui_view_spacer) { c->x = pbx.x; c->w = pbx.x + pbx.w - pbx.x; c->h = partial; // TODO: last? spacers--; } int32_t ch = padding.top + c->h + padding.bottom; c->y = y + padding.top; y += ch; ui_layout_clild(c); } } ui_view_for_each_end(p, c); } ui_layout_exit(p); } static void ui_stack_child_3x3(ui_view_t* c, int32_t *row, int32_t *col) { *row = 0; *col = 0; // makes code analysis happier if (c->align == (ui.align.left|ui.align.top)) { *row = 0; *col = 0; } else if (c->align == ui.align.top) { *row = 0; *col = 1; } else if (c->align == (ui.align.right|ui.align.top)) { *row = 0; *col = 2; } else if (c->align == ui.align.left) { *row = 1; *col = 0; } else if (c->align == ui.align.center) { *row = 1; *col = 1; } else if (c->align == ui.align.right) { *row = 1; *col = 2; } else if (c->align == (ui.align.left|ui.align.bottom)) { *row = 2; *col = 0; } else if (c->align == ui.align.bottom) { *row = 2; *col = 1; } else if (c->align == (ui.align.right|ui.align.bottom)) { *row = 2; *col = 2; } else { rt_swear(false, "invalid child align: 0x%02X", c->align); } } static void ui_stack_measure(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_stack, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); ui_wh_t sides[3][3] = { {0, 0} }; ui_view_for_each_begin(p, c) { if (!ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); int32_t row = 0; int32_t col = 0; ui_stack_child_3x3(c, &row, &col); sides[row][col].w = rt_max(sides[row][col].w, cbx.w); sides[row][col].h = rt_max(sides[row][col].h, cbx.h); ui_layout_clild(c); } } ui_view_for_each_end(p, c); if (ui_containers_debug) { for (int32_t r = 0; r < rt_countof(sides); r++) { char text[1024]; text[0] = 0; for (int32_t c = 0; c < rt_countof(sides[r]); c++) { char line[128]; rt_str_printf(line, " %4dx%-4d", sides[r][c].w, sides[r][c].h); strcat(text, line); } debugln("%*c sides[%d] %s", ui_layout_nesting, 0x20, r, text); } } ui_wh_t wh = {0, 0}; for (int32_t r = 0; r < 3; r++) { int32_t sum_w = 0; for (int32_t c = 0; c < 3; c++) { sum_w += sides[r][c].w; } wh.w = rt_max(wh.w, sum_w); } for (int32_t c = 0; c < 3; c++) { int32_t sum_h = 0; for (int32_t r = 0; r < 3; r++) { sum_h += sides[r][c].h; } wh.h = rt_max(wh.h, sum_h); } debugln("%*c wh %4dx%-4d", ui_layout_nesting, 0x20, wh.w, wh.h); p->w = insets.left + wh.w + insets.right; p->h = insets.top + wh.h + insets.bottom; ui_layout_exit(p); } static void ui_stack_layout(ui_view_t* p) { ui_layout_enter(p); rt_swear(p->type == ui_view_stack, "type %4.4s 0x%08X", &p->type, p->type); ui_rect_t pbx; // parent "in" box (sans insets) ui_ltrb_t insets; ui_view.inbox(p, &pbx, &insets); ui_view_for_each_begin(p, c) { if (c->type != ui_view_spacer && !ui_view.is_hidden(c)) { ui_rect_t cbx; // child "out" box expanded by padding ui_ltrb_t padding; ui_view.outbox(c, &cbx, &padding); const int32_t pw = p->w - insets.left - insets.right - padding.left - padding.right; const int32_t ph = p->h - insets.top - insets.bottom - padding.top - padding.bottom; int32_t cw = c->max_w == ui.infinity ? pw : c->max_w; if (cw > 0) { c->w = rt_min(cw, pw); } int32_t ch = c->max_h == ui.infinity ? ph : c->max_h; if (ch > 0) { c->h = rt_min(ch, ph); } rt_swear((c->align & (ui.align.left|ui.align.right)) != (ui.align.left|ui.align.right), "align: left|right 0x%02X", c->align); rt_swear((c->align & (ui.align.top|ui.align.bottom)) != (ui.align.top|ui.align.bottom), "align: top|bottom 0x%02X", c->align); int32_t min_x = pbx.x + padding.left; if ((c->align & ui.align.left) != 0) { c->x = min_x; } else if ((c->align & ui.align.right) != 0) { c->x = rt_max(min_x, pbx.x + pbx.w - c->w - padding.right); } else { c->x = rt_max(min_x, min_x + (pbx.w - (padding.left + c->w + padding.right)) / 2); } int32_t min_y = pbx.y + padding.top; if ((c->align & ui.align.top) != 0) { c->y = min_y; } else if ((c->align & ui.align.bottom) != 0) { c->y = rt_max(min_y, pbx.y + pbx.h - c->h - padding.bottom); } else { c->y = rt_max(min_y, min_y + (pbx.h - (padding.top + c->h + padding.bottom)) / 2); } ui_layout_clild(c); } } ui_view_for_each_end(p, c); ui_layout_exit(p); } static void ui_container_paint(ui_view_t* v) { if (!ui_color_is_undefined(v->background) && !ui_color_is_transparent(v->background)) { ui_gdi.fill(v->x, v->y, v->w, v->h, v->background); } else { // rt_println("%s undefined", ui_view_debug_id(v)); } } static void ui_view_container_init(ui_view_t* v) { v->background = ui_colors.transparent; v->insets = (ui_margins_t){ .left = 0.25, .top = 0.125, .right = 0.25, .bottom = 0.125 // .left = 0.25, .top = 0.0625, // TODO: why? // .right = 0.25, .bottom = 0.1875 }; } void ui_view_init_span(ui_view_t* v) { rt_swear(v->type == ui_view_span, "type %4.4s 0x%08X", &v->type, v->type); ui_view_container_init(v); if (v->measure == null) { v->measure = ui_span_measure; } if (v->layout == null) { v->layout = ui_span_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_span"); } if (v->debug.id == null) { v->debug.id = "#ui_span"; } } void ui_view_init_list(ui_view_t* v) { rt_swear(v->type == ui_view_list, "type %4.4s 0x%08X", &v->type, v->type); ui_view_container_init(v); if (v->measure == null) { v->measure = ui_list_measure; } if (v->layout == null) { v->layout = ui_list_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_list"); } if (v->debug.id == null) { v->debug.id = "#ui_list"; } } void ui_view_init_spacer(ui_view_t* v) { rt_swear(v->type == ui_view_spacer, "type %4.4s 0x%08X", &v->type, v->type); v->w = 0; v->h = 0; v->max_w = ui.infinity; v->max_h = ui.infinity; if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_spacer"); } if (v->debug.id == null) { v->debug.id = "#ui_spacer"; } } void ui_view_init_stack(ui_view_t* v) { ui_view_container_init(v); if (v->measure == null) { v->measure = ui_stack_measure; } if (v->layout == null) { v->layout = ui_stack_layout; } if (v->paint == null) { v->paint = ui_container_paint; } if (ui_view.string(v)[0] == 0) { ui_view.set_text(v, "ui_stack"); } if (v->debug.id == null) { v->debug.id = "#ui_stack"; } } #pragma pop_macro("ui_layout_exit") #pragma pop_macro("ui_layout_enter") #pragma pop_macro("ui_layout_dump") #pragma pop_macro("debugln") ================================================ FILE: src/ui/ui_core.c ================================================ #include "rt/rt.h" #include "ui/ui.h" #include "rt/rt_win32.h" #define UI_WM_ANIMATE (WM_APP + 0x7FFF) #define UI_WM_OPENING (WM_APP + 0x7FFE) #define UI_WM_CLOSING (WM_APP + 0x7FFD) #define UI_WM_TAP (WM_APP + 0x7FFC) #define UI_WM_DTAP (WM_APP + 0x7FFB) // double tap (aka click) #define UI_WM_PRESS (WM_APP + 0x7FFA) static bool ui_point_in_rect(const ui_point_t* p, const ui_rect_t* r) { return r->x <= p->x && p->x < r->x + r->w && r->y <= p->y && p->y < r->y + r->h; } static bool ui_intersect_rect(ui_rect_t* i, const ui_rect_t* r0, const ui_rect_t* r1) { ui_rect_t r = {0}; r.x = rt_max(r0->x, r1->x); // Maximum of left edges r.y = rt_max(r0->y, r1->y); // Maximum of top edges r.w = rt_min(r0->x + r0->w, r1->x + r1->w) - r.x; // Width of overlap r.h = rt_min(r0->y + r0->h, r1->y + r1->h) - r.y; // Height of overlap bool b = r.w > 0 && r.h > 0; if (!b) { r.w = 0; r.h = 0; } if (i != null) { *i = r; } return b; } static ui_rect_t ui_combine_rect(const ui_rect_t* r0, const ui_rect_t* r1) { return (ui_rect_t) { .x = rt_min(r0->x, r1->x), .y = rt_min(r0->y, r1->y), .w = rt_max(r0->x + r0->w, r1->x + r1->w) - rt_min(r0->x, r1->x), .h = rt_max(r0->y + r0->h, r1->y + r1->h) - rt_min(r0->y, r1->y) }; } ui_if ui = { .point_in_rect = ui_point_in_rect, .intersect_rect = ui_intersect_rect, .combine_rect = ui_combine_rect, .infinity = INT32_MAX, .align = { .center = 0, .left = 0x01, .top = 0x02, .right = 0x10, .bottom = 0x20 }, .visibility = { // window visibility see ShowWindow link below .hide = SW_HIDE, .normal = SW_SHOWNORMAL, .minimize = SW_SHOWMINIMIZED, .maximize = SW_SHOWMAXIMIZED, .normal_na = SW_SHOWNOACTIVATE, .show = SW_SHOW, .min_next = SW_MINIMIZE, .min_na = SW_SHOWMINNOACTIVE, .show_na = SW_SHOWNA, .restore = SW_RESTORE, .defau1t = SW_SHOWDEFAULT, .force_min = SW_FORCEMINIMIZE }, .message = { .animate = UI_WM_ANIMATE, .opening = UI_WM_OPENING, .closing = UI_WM_CLOSING }, .mouse = { .button = { .left = MK_LBUTTON, .right = MK_RBUTTON } }, .hit_test = { .error = HTERROR, .transparent = HTTRANSPARENT, .nowhere = HTNOWHERE, .client = HTCLIENT, .caption = HTCAPTION, .system_menu = HTSYSMENU, .grow_box = HTGROWBOX, .menu = HTMENU, .horizontal_scroll = HTHSCROLL, .vertical_scroll = HTVSCROLL, .min_button = HTMINBUTTON, .max_button = HTMAXBUTTON, .left = HTLEFT, .right = HTRIGHT, .top = HTTOP, .top_left = HTTOPLEFT, .top_right = HTTOPRIGHT, .bottom = HTBOTTOM, .bottom_left = HTBOTTOMLEFT, .bottom_right = HTBOTTOMRIGHT, .border = HTBORDER, .object = HTOBJECT, .close = HTCLOSE, .help = HTHELP }, .key = { .up = VK_UP, .down = VK_DOWN, .left = VK_LEFT, .right = VK_RIGHT, .home = VK_HOME, .end = VK_END, .page_up = VK_PRIOR, .page_down = VK_NEXT, .insert = VK_INSERT, .del = VK_DELETE, .back = VK_BACK, .escape = VK_ESCAPE, .enter = VK_RETURN, .minus = VK_OEM_MINUS, .plus = VK_OEM_PLUS, .f1 = VK_F1, .f2 = VK_F2, .f3 = VK_F3, .f4 = VK_F4, .f5 = VK_F5, .f6 = VK_F6, .f7 = VK_F7, .f8 = VK_F8, .f9 = VK_F9, .f10 = VK_F10, .f11 = VK_F11, .f12 = VK_F12, .f13 = VK_F13, .f14 = VK_F14, .f15 = VK_F15, .f16 = VK_F16, .f17 = VK_F17, .f18 = VK_F18, .f19 = VK_F19, .f20 = VK_F20, .f21 = VK_F21, .f22 = VK_F22, .f23 = VK_F23, .f24 = VK_F24, }, .beep = { .ok = 0, .info = 1, .question = 2, .warning = 3, .error = 4 } }; // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow ================================================ FILE: src/ui/ui_edit_doc.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" #undef UI_EDIT_STR_TEST #undef UI_EDIT_DOC_TEST #undef UI_STR_TEST_REPLACE_ALL_PERMUTATIONS #undef UI_EDIT_DOC_TEST_PARAGRAPHS #if 0 // flip to 1 to run tests #define UI_EDIT_STR_TEST #define UI_EDIT_DOC_TEST #if 0 // flip to 1 to run exhausting lengthy tests #define UI_STR_TEST_REPLACE_ALL_PERMUTATIONS #define UI_EDIT_DOC_TEST_PARAGRAPHS #endif #endif #pragma push_macro("ui_edit_check_zeros") #pragma push_macro("ui_edit_check_pg_inside_text") #pragma push_macro("ui_edit_check_range_inside_text") #pragma push_macro("ui_edit_pg_dump") #pragma push_macro("ui_edit_range_dump") #pragma push_macro("ui_edit_text_dump") #pragma push_macro("ui_edit_doc_dump") #define ui_edit_pg_dump(pg) \ rt_debug.println(__FILE__, __LINE__, __func__, \ "pn:%d gp:%d", (pg)->pn, (pg)->gp) #define ui_edit_range_dump(r) \ rt_debug.println(__FILE__, __LINE__, __func__, \ "from {pn:%d gp:%d} to {pn:%d gp:%d}", \ (r)->from.pn, (r)->from.gp, (r)->to.pn, (r)->to.gp); #define ui_edit_text_dump(t) do { \ for (int32_t i_ = 0; i_ < (t)->np; i_++) { \ const ui_edit_str_t* p_ = &t->ps[i_]; \ rt_debug.println(__FILE__, __LINE__, __func__, \ "ps[%d].%d: %.*s", i_, p_->b, p_->b, p_->u); \ } \ } while (0) // TODO: undo/redo stacks and listeners #define ui_edit_doc_dump(d) do { \ for (int32_t i_ = 0; i_ < (d)->text.np; i_++) { \ const ui_edit_str_t* p_ = &(d)->text.ps[i_]; \ rt_debug.println(__FILE__, __LINE__, __func__, \ "ps[%d].b:%d.c:%d: %p %.*s", i_, p_->b, p_->c, \ p_, p_->b, p_->u); \ } \ } while (0) #ifdef DEBUG // ui_edit_check_zeros only works for packed structs: #define ui_edit_check_zeros(a_, b_) do { \ for (int32_t i_ = 0; i_ < (b_); i_++) { \ rt_assert(((const uint8_t*)(a_))[i_] == 0x00); \ } \ } while (0) #define ui_edit_check_pg_inside_text(t_, pg_) \ rt_assert(0 <= (pg_)->pn && (pg_)->pn < (t_)->np && \ 0 <= (pg_)->gp && (pg_)->gp <= (t_)->ps[(pg_)->pn].g) #define ui_edit_check_range_inside_text(t_, r_) do { \ rt_assert((r_)->from.pn <= (r_)->to.pn); \ rt_assert((r_)->from.pn < (r_)->to.pn || (r_)->from.gp <= (r_)->to.gp); \ ui_edit_check_pg_inside_text(t_, (&(r_)->from)); \ ui_edit_check_pg_inside_text(t_, (&(r_)->to)); \ } while (0) #else #define ui_edit_check_zeros(a, b) do { } while (0) #define ui_edit_check_pg_inside_text(t, pg) do { } while (0) #define ui_edit_check_range_inside_text(t, r) do { } while (0) #endif static ui_edit_range_t ui_edit_text_all_on_null(const ui_edit_text_t* t, const ui_edit_range_t* range) { ui_edit_range_t r; if (range != null) { r = *range; } else { rt_assert(t->np >= 1); r.from.pn = 0; r.from.gp = 0; r.to.pn = t->np - 1; r.to.gp = t->ps[r.to.pn].g; } return r; } static int ui_edit_range_compare(const ui_edit_pg_t pg1, const ui_edit_pg_t pg2) { int64_t d = (((int64_t)pg1.pn << 32) | pg1.gp) - (((int64_t)pg2.pn << 32) | pg2.gp); return d < 0 ? -1 : d > 0 ? 1 : 0; } static ui_edit_range_t ui_edit_range_order(const ui_edit_range_t range) { ui_edit_range_t r = range; uint64_t f = ((uint64_t)r.from.pn << 32) | r.from.gp; uint64_t t = ((uint64_t)r.to.pn << 32) | r.to.gp; if (ui_edit_range.compare(r.from, r.to) > 0) { uint64_t swap = t; t = f; f = swap; r.from.pn = (int32_t)(f >> 32); r.from.gp = (int32_t)(f); r.to.pn = (int32_t)(t >> 32); r.to.gp = (int32_t)(t); } return r; } static ui_edit_range_t ui_edit_text_ordered(const ui_edit_text_t* t, const ui_edit_range_t* r) { return ui_edit_range.order(ui_edit_text.all_on_null(t, r)); } static bool ui_edit_range_is_valid(const ui_edit_range_t r) { if (0 <= r.from.pn && 0 <= r.to.pn && 0 <= r.from.gp && 0 <= r.to.gp) { ui_edit_range_t o = ui_edit_range.order(r); return ui_edit_range.compare(o.from, o.to) <= 0; } else { return false; } } static bool ui_edit_range_is_empty(const ui_edit_range_t r) { return r.from.pn == r.to.pn && r.from.gp == r.to.gp; } static ui_edit_pg_t ui_edit_text_end(const ui_edit_text_t* t) { return (ui_edit_pg_t){ .pn = t->np - 1, .gp = t->ps[t->np - 1].g }; } static ui_edit_range_t ui_edit_text_end_range(const ui_edit_text_t* t) { ui_edit_pg_t e = (ui_edit_pg_t){ .pn = t->np - 1, .gp = t->ps[t->np - 1].g }; return (ui_edit_range_t){ .from = e, .to = e }; } static uint64_t ui_edit_range_uint64(const ui_edit_pg_t pg) { rt_assert(pg.pn >= 0 && pg.gp >= 0); return ((uint64_t)pg.pn << 32) | (uint64_t)pg.gp; } static ui_edit_pg_t ui_edit_range_pg(uint64_t uint64) { rt_assert((int32_t)(uint64 >> 32) >= 0 && (int32_t)uint64 >= 0); return (ui_edit_pg_t){ .pn = (int32_t)(uint64 >> 32), .gp = (int32_t)uint64 }; } static bool ui_edit_range_inside_text(const ui_edit_text_t* t, const ui_edit_range_t r) { return ui_edit_range.is_valid(r) && 0 <= r.from.pn && r.from.pn <= r.to.pn && r.to.pn < t->np && 0 <= r.from.gp && r.from.gp <= r.to.gp && r.to.gp <= t->ps[r.to.pn - 1].g; } static ui_edit_range_t ui_edit_range_intersect(const ui_edit_range_t r1, const ui_edit_range_t r2) { if (ui_edit_range.is_valid(r1) && ui_edit_range.is_valid(r2)) { ui_edit_range_t o1 = ui_edit_range.order(r1); ui_edit_range_t o2 = ui_edit_range.order(r1); uint64_t f1 = ((uint64_t)o1.from.pn << 32) | o1.from.gp; uint64_t t1 = ((uint64_t)o1.to.pn << 32) | o1.to.gp; uint64_t f2 = ((uint64_t)o2.from.pn << 32) | o2.from.gp; uint64_t t2 = ((uint64_t)o2.to.pn << 32) | o2.to.gp; if (f1 <= f2 && f2 <= t1) { // f2 is inside r1 if (t2 <= t1) { // r2 is fully inside r1 return r2; } else { // r2 is partially inside r1 ui_edit_range_t r = {0}; r.from.pn = (int32_t)(f2 >> 32); r.from.gp = (int32_t)(f2); r.to.pn = (int32_t)(t1 >> 32); r.to.gp = (int32_t)(t1); return r; } } else if (f2 <= f1 && f1 <= t2) { // f1 is inside r2 if (t1 <= t2) { // r1 is fully inside r2 return r1; } else { // r1 is partially inside r2 ui_edit_range_t r = {0}; r.from.pn = (int32_t)(f1 >> 32); r.from.gp = (int32_t)(f1); r.to.pn = (int32_t)(t2 >> 32); r.to.gp = (int32_t)(t2); return r; } } else { return *ui_edit_range.invalid_range; } } else { return *ui_edit_range.invalid_range; } } static bool ui_edit_doc_realloc_ps_no_init(ui_edit_str_t* *ps, int32_t old_np, int32_t new_np) { // reallocate paragraphs for (int32_t i = new_np; i < old_np; i++) { ui_edit_str.free(&(*ps)[i]); } bool ok = true; if (new_np == 0) { rt_heap.free(*ps); *ps = null; } else { ok = rt_heap.realloc_zero((void**)ps, new_np * sizeof(ui_edit_str_t)) == 0; } return ok; } static bool ui_edit_doc_realloc_ps(ui_edit_str_t* *ps, int32_t old_np, int32_t new_np) { // reallocate paragraphs bool ok = ui_edit_doc_realloc_ps_no_init(ps, old_np, new_np); if (ok) { for (int32_t i = old_np; i < new_np; i++) { ok = ui_edit_str.init(&(*ps)[i], null, 0, false); rt_swear(ok, "because .init(\"\", 0) does NOT allocate memory"); } } return ok; } static bool ui_edit_text_init(ui_edit_text_t* t, const char* s, int32_t b, bool heap) { // When text comes from the source that lifetime is shorter // than text itself (e.g. paste from clipboard) the parameter // heap: true allows to make a copy of data on the heap ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); if (b < 0) { b = (int32_t)strlen(s); } // if caller is concerned with best performance - it should pass b >= 0 int32_t np = 0; // number of paragraphs int32_t n = rt_max(b / 64, 2); // initial number of allocated paragraphs ui_edit_str_t* ps = null; // ps[n] bool ok = ui_edit_doc_realloc_ps(&ps, 0, n); if (ok) { bool lf = false; int32_t i = 0; while (ok && i < b) { int32_t k = i; while (k < b && s[k] != '\n') { k++; } lf = k < b && s[k] == '\n'; if (np >= n) { int32_t n1_5 = n * 3 / 2; // n * 1.5 rt_assert(n1_5 > n); ok = ui_edit_doc_realloc_ps(&ps, n, n1_5); if (ok) { n = n1_5; } } if (ok) { // insider knowledge about ui_edit_str allocation behaviour: rt_assert(ps[np].c == 0 && ps[np].b == 0 && ps[np].g2b[0] == 0); ui_edit_str.free(&ps[np]); // process "\r\n" strings const int32_t e = k > i && s[k - 1] == '\r' ? k - 1 : k; const int32_t bytes = e - i; rt_assert(bytes >= 0); const char* u = bytes == 0 ? null : s + i; // str.init may allocate str.g2b[] on the heap and may fail ok = ui_edit_str.init(&ps[np], u, bytes, heap && bytes > 0); if (ok) { np++; } } i = k + lf; } if (ok && lf) { // last paragraph ended with line feed if (np + 1 >= n) { ok = ui_edit_doc_realloc_ps(&ps, n, n + 1); if (ok) { n = n + 1; } } if (ok) { np++; } } } if (ok && np == 0) { // special case empty string to a single paragraph rt_assert(b <= 0 && (b == 0 || s[0] == 0x00)); np = 1; // ps[0] is already initialized as empty str ok = ui_edit_doc_realloc_ps(&ps, n, 1); rt_swear(ok, "shrinking ps[] above"); } if (ok) { rt_assert(np > 0); t->np = np; t->ps = ps; } else if (ps != null) { bool shrink = ui_edit_doc_realloc_ps(&ps, n, 0); // free() rt_swear(shrink); rt_heap.free(ps); t->np = 0; t->ps = null; } return ok; } static void ui_edit_text_dispose(ui_edit_text_t* t) { if (t->np != 0) { ui_edit_doc_realloc_ps(&t->ps, t->np, 0); rt_assert(t->ps == null); t->np = 0; } else { rt_assert(t->np == 0 && t->ps == null); } } static void ui_edit_doc_dispose_to_do(ui_edit_to_do_t* to_do) { if (to_do->text.np > 0) { ui_edit_text_dispose(&to_do->text); } memset(&to_do->range, 0x00, sizeof(to_do->range)); ui_edit_check_zeros(to_do, sizeof(*to_do)); } static int32_t ui_edit_text_bytes(const ui_edit_text_t* t, const ui_edit_range_t* range) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); int32_t bytes = 0; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; if (pn == r.from.pn && pn == r.to.pn) { bytes += p->g2b[r.to.gp] - p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes += p->b - p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes += p->g2b[r.to.gp]; } else { bytes += p->b; } } return bytes; } static int32_t ui_edit_doc_bytes(const ui_edit_doc_t* d, const ui_edit_range_t* r) { return ui_edit_text.bytes(&d->text, r); } static int32_t ui_edit_doc_utf8bytes(const ui_edit_doc_t* d, const ui_edit_range_t* range) { const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); int32_t bytes = ui_edit_text.bytes(&d->text, &r); // "\n" after each paragraph and 0x00 return bytes + r.to.pn - r.from.pn + 1; } static void ui_edit_notify_before(ui_edit_doc_t* d, const ui_edit_notify_info_t* ni) { ui_edit_listener_t* o = d->listeners; while (o != null) { if (o->notify != null && o->notify->before != null) { o->notify->before(o->notify, ni); } o = o->next; } } static void ui_edit_notify_after(ui_edit_doc_t* d, const ui_edit_notify_info_t* ni) { ui_edit_listener_t* o = d->listeners; while (o != null) { if (o->notify != null && o->notify->after != null) { o->notify->after(o->notify, ni); } o = o->next; } } static bool ui_edit_doc_subscribe(ui_edit_doc_t* t, ui_edit_notify_t* notify) { // TODO: not sure about double linked list. // heap allocated resizable array may serve better and may be easier to maintain bool ok = true; ui_edit_listener_t* o = t->listeners; if (o == null) { ok = rt_heap.alloc_zero((void**)&t->listeners, sizeof(*o)) == 0; if (ok) { o = t->listeners; } } else { while (o->next != null) { rt_swear(o->notify != notify); o = o->next; } ok = rt_heap.alloc_zero((void**)&o->next, sizeof(*o)) == 0; if (ok) { o->next->prev = o; o = o->next; } } if (ok) { o->notify = notify; } return ok; } static void ui_edit_doc_unsubscribe(ui_edit_doc_t* t, ui_edit_notify_t* notify) { ui_edit_listener_t* o = t->listeners; bool removed = false; while (o != null) { ui_edit_listener_t* n = o->next; if (o->notify == notify) { rt_assert(!removed); if (o->prev != null) { o->prev->next = n; } if (o->next != null) { o->next->prev = o->prev; } if (o == t->listeners) { t->listeners = n; } rt_heap.free(o); removed = true; } o = n; } rt_swear(removed); } static bool ui_edit_doc_copy_text(const ui_edit_doc_t* d, const ui_edit_range_t* range, ui_edit_text_t* t) { ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); ui_edit_check_range_inside_text(&d->text, &r); int32_t np = r.to.pn - r.from.pn + 1; bool ok = ui_edit_doc_realloc_ps(&t->ps, 0, np); if (ok) { t->np = np; } for (int32_t pn = r.from.pn; ok && pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &d->text.ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } rt_assert(t->ps[pn - r.from.pn].g == 0); const char* u_or_null = bytes == 0 ? null : u; ui_edit_str.replace(&t->ps[pn - r.from.pn], 0, 0, u_or_null, bytes); } if (!ok) { ui_edit_text.dispose(t); ui_edit_check_zeros(t, sizeof(*t)); } return ok; } static void ui_edit_doc_copy(const ui_edit_doc_t* d, const ui_edit_range_t* range, char* text, int32_t b) { const ui_edit_range_t r = ui_edit_text.ordered(&d->text, range); ui_edit_check_range_inside_text(&d->text, &r); char* to = text; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &d->text.ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } const int32_t c = (int32_t)(uintptr_t)(to - text); if (bytes > 0) { rt_swear(c + bytes < b, "c: %d bytes: %d b: %d", c, bytes, b); memmove(to, u, (size_t)bytes); to += bytes; } if (pn < r.to.pn) { rt_swear(c + bytes < b, "c: %d bytes: %d b: %d", c, bytes, b); *to++ = '\n'; } } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + 1 == b, "c: %d b: %d", c, b); *to++ = 0x00; } static bool ui_edit_text_insert_2_or_more(ui_edit_text_t* t, int32_t pn, const ui_edit_str_t* s, const ui_edit_text_t* insert, const ui_edit_str_t* e) { // insert 2 or more paragraphs rt_assert(0 <= pn && pn < t->np); const int32_t np = t->np + insert->np - 1; rt_assert(np > 0); ui_edit_str_t* ps = null; // ps[np] bool ok = ui_edit_doc_realloc_ps_no_init(&ps, 0, np); if (ok) { memmove(ps, t->ps, (size_t)pn * sizeof(ui_edit_str_t)); // `s` first line of `insert` ok = ui_edit_str.init(&ps[pn], s->u, s->b, true); // lines of `insert` between `s` and `e` for (int32_t i = 1; ok && i < insert->np - 1; i++) { ok = ui_edit_str.init(&ps[pn + i], insert->ps[i].u, insert->ps[i].b, true); } // `e` last line of `insert` if (ok) { const int32_t ix = pn + insert->np - 1; // last `insert` index ok = ui_edit_str.init(&ps[ix], e->u, e->b, true); } rt_assert(t->np - pn - 1 >= 0); memmove(ps + pn + insert->np, t->ps + pn + 1, (size_t)(t->np - pn - 1) * sizeof(ui_edit_str_t)); if (ok) { // this two regions where moved to `ps` memset(t->ps, 0x00, pn * sizeof(ui_edit_str_t)); memset(t->ps + pn + 1, 0x00, (size_t)(t->np - pn - 1) * sizeof(ui_edit_str_t)); // deallocate what was copied from `insert` ui_edit_doc_realloc_ps_no_init(&t->ps, t->np, 0); t->np = np; t->ps = ps; } else { // free allocated memory: ui_edit_doc_realloc_ps_no_init(&ps, np, 0); } } return ok; } static bool ui_edit_text_insert_1(ui_edit_text_t* t, const ui_edit_pg_t ip, // insertion point const ui_edit_text_t* insert) { rt_assert(0 <= ip.pn && ip.pn < t->np); ui_edit_str_t* str = &t->ps[ip.pn]; // string in document text rt_assert(insert->np == 1); ui_edit_str_t* ins = &insert->ps[0]; // string to insert rt_assert(0 <= ip.gp && ip.gp <= str->g); // ui_edit_str.replace() is all or nothing: return ui_edit_str.replace(str, ip.gp, ip.gp, ins->u, ins->b); } static bool ui_edit_substr_append(ui_edit_str_t* d, const ui_edit_str_t* s1, int32_t gp1, const ui_edit_str_t* s2) { // s1[0:gp1] + s2 rt_assert(d != s1 && d != s2); const int32_t b = s1->g2b[gp1]; bool ok = ui_edit_str.init(d, b == 0 ? null : s1->u, b, true); if (ok) { ok = ui_edit_str.replace(d, d->g, d->g, s2->u, s2->b); } else { *d = *ui_edit_str.empty; } return ok; } static bool ui_edit_append_substr(ui_edit_str_t* d, const ui_edit_str_t* s1, const ui_edit_str_t* s2, int32_t gp2) { // s1 + s2[gp1:*] rt_assert(d != s1 && d != s2); bool ok = ui_edit_str.init(d, s1->b == 0 ? null : s1->u, s1->b, true); if (ok) { const int32_t o = s2->g2b[gp2]; // offset (bytes) const int32_t b = s2->b - o; ok = ui_edit_str.replace(d, d->g, d->g, b == 0 ? null : s2->u + o, b); } else { *d = *ui_edit_str.empty; } return ok; } static bool ui_edit_text_insert(ui_edit_text_t* t, const ui_edit_pg_t ip, const ui_edit_text_t* i) { bool ok = true; if (ok) { if (i->np == 1) { ok = ui_edit_text_insert_1(t, ip, i); } else { ui_edit_str_t* str = &t->ps[ip.pn]; ui_edit_str_t s = {0}; // start line of insert text `i` ui_edit_str_t e = {0}; // end line if (ui_edit_substr_append(&s, str, ip.gp, &i->ps[0])) { if (ui_edit_append_substr(&e, &i->ps[i->np - 1], str, ip.gp)) { ok = ui_edit_text_insert_2_or_more(t, ip.pn, &s, i, &e); ui_edit_str.free(&e); } ui_edit_str.free(&s); } } } return ok; } static bool ui_edit_text_remove_lines(ui_edit_text_t* t, ui_edit_str_t* merge, int32_t from, int32_t to) { bool ok = true; for (int32_t pn = from + 1; pn <= to; pn++) { ui_edit_str.free(&t->ps[pn]); } if (t->np - to - 1 > 0) { memmove(&t->ps[from + 1], &t->ps[to + 1], (size_t)(t->np - to - 1) * sizeof(ui_edit_str_t)); } t->np -= to - from; if (ok) { ui_edit_str.swap(&t->ps[from], merge); } return ok; } static bool ui_edit_text_insert_remove(ui_edit_text_t* t, const ui_edit_range_t r, const ui_edit_text_t* i) { bool ok = true; ui_edit_str_t merge = {0}; const ui_edit_str_t* s = &t->ps[r.from.pn]; const ui_edit_str_t* e = &t->ps[r.to.pn]; const int32_t o = e->g2b[r.to.gp]; const int32_t b = e->b - o; const char* u = b == 0 ? null : e->u + o; ok = ui_edit_substr_append(&merge, s, r.from.gp, &i->ps[i->np - 1]) && ui_edit_str.replace(&merge, merge.g, merge.g, u, b); if (ok) { const bool empty_text = i->np == 1 && i->ps[0].g == 0; if (!empty_text) { ok = ui_edit_text_insert(t, r.to, i); } if (ok) { ok = ui_edit_text_remove_lines(t, &merge, r.from.pn, r.to.pn); } } if (merge.c > 0 || merge.g > 0) { ui_edit_str.free(&merge); } return ok; } static bool ui_edit_text_copy_text(const ui_edit_text_t* t, const ui_edit_range_t* range, ui_edit_text_t* to) { ui_edit_check_zeros(to, sizeof(*to)); memset(to, 0x00, sizeof(*to)); const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); int32_t np = r.to.pn - r.from.pn + 1; bool ok = ui_edit_doc_realloc_ps(&to->ps, 0, np); if (ok) { to->np = np; } for (int32_t pn = r.from.pn; ok && pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } rt_assert(to->ps[pn - r.from.pn].g == 0); const char* u_or_null = bytes == 0 ? null : u; ui_edit_str.replace(&to->ps[pn - r.from.pn], 0, 0, u_or_null, bytes); } if (!ok) { ui_edit_text.dispose(to); ui_edit_check_zeros(to, sizeof(*to)); } return ok; } static void ui_edit_text_copy(const ui_edit_text_t* t, const ui_edit_range_t* range, char* text, int32_t b) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_check_range_inside_text(t, &r); char* to = text; for (int32_t pn = r.from.pn; pn <= r.to.pn; pn++) { const ui_edit_str_t* p = &t->ps[pn]; const char* u = p->u; int32_t bytes = 0; if (pn == r.from.pn && pn == r.to.pn) { bytes = p->g2b[r.to.gp] - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.from.pn) { bytes = p->b - p->g2b[r.from.gp]; u += p->g2b[r.from.gp]; } else if (pn == r.to.pn) { bytes = p->g2b[r.to.gp]; } else { bytes = p->b; } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + bytes < b, "d: %d bytes:%d b: %d", c, bytes, b); if (bytes > 0) { memmove(to, u, (size_t)bytes); to += bytes; } if (pn < r.to.pn) { rt_swear(c + bytes + 1 < b, "d: %d bytes:%d b: %d", c, bytes, b); *to++ = '\n'; } } const int32_t c = (int32_t)(uintptr_t)(to - text); rt_swear(c + 1 == b, "d: %d b: %d", c, b); *to++ = 0x00; } static bool ui_edit_text_replace(ui_edit_text_t* t, const ui_edit_range_t* range, const ui_edit_text_t* i, ui_edit_to_do_t* undo) { const ui_edit_range_t r = ui_edit_text.ordered(t, range); bool ok = undo == null ? true : ui_edit_text.copy_text(t, &r, &undo->text); ui_edit_range_t x = r; if (ok) { if (ui_edit_range.is_empty(r)) { x.to.pn = r.from.pn + i->np - 1; x.to.gp = i->np == 1 ? r.from.gp + i->ps[0].g : i->ps[i->np - 1].g; ok = ui_edit_text_insert(t, r.from, i); } else if (i->np == 1 && r.from.pn == r.to.pn) { x.to.pn = r.from.pn + i->np - 1; x.to.gp = r.from.gp + i->ps[0].g; ok = ui_edit_str.replace(&t->ps[r.from.pn], r.from.gp, r.to.gp, i->ps[0].u, i->ps[0].b); } else { x.to.pn = r.from.pn + i->np - 1; x.to.gp = i->np == 1 ? r.from.gp + i->ps[0].g : i->ps[0].g; ok = ui_edit_text_insert_remove(t, r, i); } } if (undo != null) { undo->range = x; } return ok; } static bool ui_edit_text_replace_utf8(ui_edit_text_t* t, const ui_edit_range_t* range, const char* utf8, int32_t b, ui_edit_to_do_t* undo) { if (b < 0) { b = (int32_t)strlen(utf8); } ui_edit_text_t i = {0}; bool ok = ui_edit_text.init(&i, utf8, b, false); if (ok) { ok = ui_edit_text.replace(t, range, &i, undo); ui_edit_text.dispose(&i); } return ok; } static bool ui_edit_text_dup(ui_edit_text_t* t, const ui_edit_text_t* s) { ui_edit_check_zeros(t, sizeof(*t)); memset(t, 0x00, sizeof(*t)); bool ok = ui_edit_doc_realloc_ps(&t->ps, 0, s->np); if (ok) { t->np = s->np; for (int32_t i = 0; ok && i < s->np; i++) { const ui_edit_str_t* p = &s->ps[i]; ok = ui_edit_str.replace(&t->ps[i], 0, 0, p->u, p->b); } } if (!ok) { ui_edit_text.dispose(t); } return ok; } static bool ui_edit_text_equal(const ui_edit_text_t* t1, const ui_edit_text_t* t2) { bool equal = t1->np != t2->np; for (int32_t i = 0; equal && i < t1->np; i++) { const ui_edit_str_t* p1 = &t1->ps[i]; const ui_edit_str_t* p2 = &t2->ps[i]; equal = p1->b == p2->b && memcmp(p1->u, p2->u, p1->b) == 0; } return equal; } static void ui_edit_doc_before_replace_text(ui_edit_doc_t* d, const ui_edit_range_t r, const ui_edit_text_t* t) { ui_edit_check_range_inside_text(&d->text, &r); ui_edit_range_t x = r; x.to.pn = r.from.pn + t->np - 1; if (r.from.pn == r.to.pn && t->np == 1) { x.to.gp = r.from.gp + t->ps[0].g; } else { x.to.gp = t->ps[t->np - 1].g; } const ui_edit_notify_info_t ni_before = { .ok = true, .d = d, .r = &r, .x = &x, .t = t, .pnf = r.from.pn, .pnt = r.to.pn, .deleted = 0, .inserted = 0 }; ui_edit_notify_before(d, &ni_before); } static void ui_edit_doc_after_replace_text(ui_edit_doc_t* d, bool ok, const ui_edit_range_t r, const ui_edit_range_t x, const ui_edit_text_t* t) { const ui_edit_notify_info_t ni_after = { .ok = ok, .d = d, .r = &r, .x = &x, .t = t, .pnf = r.from.pn, .pnt = x.to.pn, .deleted = r.to.pn - r.from.pn, .inserted = t->np - 1 }; ui_edit_notify_after(d, &ni_after); } static bool ui_edit_doc_replace_text(ui_edit_doc_t* d, const ui_edit_range_t* range, const ui_edit_text_t* i, ui_edit_to_do_t* undo) { ui_edit_text_t* t = &d->text; const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_doc_before_replace_text(d, r, i); bool ok = ui_edit_text.replace(t, &r, i, undo); ui_edit_doc_after_replace_text(d, ok, r, undo->range, i); return ok; } static bool ui_edit_doc_replace_undoable(ui_edit_doc_t* d, const ui_edit_range_t* r, const ui_edit_text_t* t, ui_edit_to_do_t* undo) { bool ok = ui_edit_doc_replace_text(d, r, t, undo); if (ok && undo != null) { undo->next = d->undo; d->undo = undo; // redo stack is not valid after new replace, empty it: while (d->redo != null) { ui_edit_to_do_t* next = d->redo->next; d->redo->next = null; ui_edit_doc.dispose_to_do(d->redo); rt_heap.free(d->redo); d->redo = next; } } return ok; } static bool ui_edit_utf8_to_heap_text(const char* u, int32_t b, ui_edit_text_t* it) { rt_assert((b == 0) == (u == null || u[0] == 0x00)); return ui_edit_text.init(it, b != 0 ? u : null, b, true); } static bool ui_edit_doc_coalesce_undo(ui_edit_doc_t* d, ui_edit_text_t* i) { ui_edit_to_do_t* undo = d->undo; ui_edit_to_do_t* next = undo->next; // rt_println("i: %.*s", i->ps[0].b, i->ps[0].u); // if (i->np == 1 && i->ps[0].g == 1) { // rt_println("an: %d", ui_edit_str.is_letter(rt_str.utf32(i->ps[0].u, i->ps[0].b))); // } bool coalesced = false; const bool alpha_numeric = i->np == 1 && i->ps[0].g == 1 && ui_edit_str.is_letter(rt_str.utf32(i->ps[0].u, i->ps[0].b)); if (alpha_numeric && next != null) { const ui_edit_range_t ur = undo->range; const ui_edit_text_t* ut = &undo->text; const ui_edit_range_t nr = next->range; const ui_edit_text_t* nt = &next->text; // rt_println("next: \"%.*s\" %d:%d..%d:%d undo: \"%.*s\" %d:%d..%d:%d", // nt->ps[0].b, nt->ps[0].u, nr.from.pn, nr.from.gp, nr.to.pn, nr.to.gp, // ut->ps[0].b, ut->ps[0].u, ur.from.pn, ur.from.gp, ur.to.pn, ur.to.gp); const bool c = nr.from.pn == nr.to.pn && ur.from.pn == ur.to.pn && nr.from.pn == ur.from.pn && ut->np == 1 && ut->ps[0].g == 0 && nt->np == 1 && nt->ps[0].g == 0 && nr.to.gp == ur.from.gp && nr.to.gp > 0; if (c) { const ui_edit_str_t* str = &d->text.ps[nr.from.pn]; const int32_t* g2b = str->g2b; const char* utf8 = str->u + g2b[nr.to.gp - 1]; uint32_t utf32 = rt_str.utf32(utf8, g2b[nr.to.gp] - g2b[nr.to.gp - 1]); coalesced = ui_edit_str.is_letter(utf32); } if (coalesced) { // rt_println("coalesced"); next->range.to.gp++; d->undo = next; undo->next = null; coalesced = true; } } return coalesced; } static bool ui_edit_doc_replace(ui_edit_doc_t* d, const ui_edit_range_t* range, const char* u, int32_t b) { ui_edit_text_t* t = &d->text; const ui_edit_range_t r = ui_edit_text.ordered(t, range); ui_edit_to_do_t* undo = null; bool ok = rt_heap.alloc_zero((void**)&undo, sizeof(ui_edit_to_do_t)) == 0; if (ok) { ui_edit_text_t i = {0}; ok = ui_edit_utf8_to_heap_text(u, b, &i); if (ok) { ok = ui_edit_doc_replace_undoable(d, &r, &i, undo); if (ok) { if (ui_edit_doc_coalesce_undo(d, &i)) { ui_edit_doc.dispose_to_do(undo); rt_heap.free(undo); undo = null; } } ui_edit_text.dispose(&i); } if (!ok) { ui_edit_doc.dispose_to_do(undo); rt_heap.free(undo); undo = null; } } return ok; } static bool ui_edit_doc_do(ui_edit_doc_t* d, ui_edit_to_do_t* to_do, ui_edit_to_do_t* *stack) { const ui_edit_range_t* r = &to_do->range; ui_edit_to_do_t* redo = null; bool ok = rt_heap.alloc_zero((void**)&redo, sizeof(ui_edit_to_do_t)) == 0; if (ok) { ok = ui_edit_doc_replace_text(d, r, &to_do->text, redo); if (ok) { ui_edit_doc.dispose_to_do(to_do); rt_heap.free(to_do); } if (ok) { redo->next = *stack; *stack = redo; } else { if (redo != null) { ui_edit_doc.dispose_to_do(redo); rt_heap.free(redo); } } } return ok; } static bool ui_edit_doc_redo(ui_edit_doc_t* d) { ui_edit_to_do_t* to_do = d->redo; if (to_do == null) { return false; } else { d->redo = d->redo->next; to_do->next = null; return ui_edit_doc_do(d, to_do, &d->undo); } } static bool ui_edit_doc_undo(ui_edit_doc_t* d) { ui_edit_to_do_t* to_do = d->undo; if (to_do == null) { return false; } else { d->undo = d->undo->next; to_do->next = null; return ui_edit_doc_do(d, to_do, &d->redo); } } static bool ui_edit_doc_init(ui_edit_doc_t* d, const char* utf8, int32_t bytes, bool heap) { bool ok = true; ui_edit_check_zeros(d, sizeof(*d)); memset(d, 0x00, sizeof(d)); if (bytes < 0) { size_t n = strlen(utf8); rt_swear(n < INT32_MAX); bytes = (int32_t)n; } rt_assert((utf8 == null) == (bytes == 0)); if (ok) { if (bytes == 0) { // empty string ok = rt_heap.alloc_zero((void**)&d->text.ps, sizeof(ui_edit_str_t)) == 0; if (ok) { d->text.np = 1; ok = ui_edit_str.init(&d->text.ps[0], null, 0, false); } } else { ok = ui_edit_text.init(&d->text, utf8, bytes, heap); } } return ok; } static void ui_edit_doc_dispose(ui_edit_doc_t* d) { for (int32_t i = 0; i < d->text.np; i++) { ui_edit_str.free(&d->text.ps[i]); } if (d->text.ps != null) { rt_heap.free(d->text.ps); d->text.ps = null; } d->text.np = 0; while (d->undo != null) { ui_edit_to_do_t* next = d->undo->next; d->undo->next = null; ui_edit_doc.dispose_to_do(d->undo); rt_heap.free(d->undo); d->undo = next; } while (d->redo != null) { ui_edit_to_do_t* next = d->redo->next; d->redo->next = null; ui_edit_doc.dispose_to_do(d->redo); rt_heap.free(d->redo); d->redo = next; } rt_assert(d->listeners == null, "unsubscribe listeners?"); while (d->listeners != null) { ui_edit_listener_t* next = d->listeners->next; d->listeners->next = null; rt_heap.free(d->listeners->next); d->listeners = next; } ui_edit_check_zeros(d, sizeof(*d)); } // ui_edit_str static int32_t ui_edit_str_g2b_ascii[1024]; // ui_edit_str_g2b_ascii[i] == i for all "i" static char ui_edit_str_empty_utf8[1] = {0x00}; static const ui_edit_str_t ui_edit_str_empty = { .u = ui_edit_str_empty_utf8, .g2b = ui_edit_str_g2b_ascii, .c = 0, .b = 0, .g = 0 }; static bool ui_edit_str_init(ui_edit_str_t* s, const char* u, int32_t b, bool heap); static void ui_edit_str_swap(ui_edit_str_t* s1, ui_edit_str_t* s2); static int32_t ui_edit_str_gp_to_bp(const char* s, int32_t bytes, int32_t gp); static int32_t ui_edit_str_bytes(ui_edit_str_t* s, int32_t f, int32_t t); static bool ui_edit_str_expand(ui_edit_str_t* s, int32_t c); static void ui_edit_str_shrink(ui_edit_str_t* s); static bool ui_edit_str_replace(ui_edit_str_t* s, int32_t f, int32_t t, const char* u, int32_t b); // bool (*is_zwj)(uint32_t utf32); // zero width joiner // bool (*is_letter)(uint32_t utf32); // in European Alphabets // bool (*is_digit)(uint32_t utf32); // bool (*is_symbol)(uint32_t utf32); // bool (*is_alphanumeric)(uint32_t utf32); // bool (*is_blank)(uint32_t utf32); // white space // bool (*is_punctuation)(uint32_t utf32); // bool (*is_combining)(uint32_t utf32); // bool (*is_spacing)(uint32_t utf32); // spacing modifiers // bool (*is_cjk_or_emoji)(uint32_t utf32); static bool ui_edit_str_is_zwj(uint32_t utf32); static bool ui_edit_str_is_letter(uint32_t utf32); static bool ui_edit_str_is_digit(uint32_t utf32); static bool ui_edit_str_is_symbol(uint32_t utf32); static bool ui_edit_str_is_alphanumeric(uint32_t utf32); static bool ui_edit_str_is_blank(uint32_t utf32); static bool ui_edit_str_is_punctuation(uint32_t utf32); static bool ui_edit_str_is_combining(uint32_t utf32); static bool ui_edit_str_is_spacing(uint32_t utf32); static bool ui_edit_str_is_blank(uint32_t utf32); static bool ui_edit_str_is_cjk_or_emoji(uint32_t utf32); static bool ui_edit_str_can_break(uint32_t cp1, uint32_t cp2); static void ui_edit_str_test(void); static void ui_edit_str_free(ui_edit_str_t* s); ui_edit_str_if ui_edit_str = { .init = ui_edit_str_init, .swap = ui_edit_str_swap, .gp_to_bp = ui_edit_str_gp_to_bp, .bytes = ui_edit_str_bytes, .expand = ui_edit_str_expand, .shrink = ui_edit_str_shrink, .replace = ui_edit_str_replace, .is_zwj = ui_edit_str_is_zwj, .is_letter = ui_edit_str_is_letter, .is_digit = ui_edit_str_is_digit, .is_symbol = ui_edit_str_is_symbol, .is_alphanumeric = ui_edit_str_is_alphanumeric, .is_blank = ui_edit_str_is_blank, .is_punctuation = ui_edit_str_is_punctuation, .is_combining = ui_edit_str_is_combining, .is_spacing = ui_edit_str_is_spacing, .is_punctuation = ui_edit_str_is_punctuation, .is_cjk_or_emoji = ui_edit_str_is_cjk_or_emoji, .can_break = ui_edit_str_can_break, .test = ui_edit_str_test, .free = ui_edit_str_free, .empty = &ui_edit_str_empty }; #pragma push_macro("ui_edit_str_check") #pragma push_macro("ui_edit_str_check_from_to") #pragma push_macro("ui_edit_check_zeros") #pragma push_macro("ui_edit_str_check_empty") #pragma push_macro("ui_edit_str_parameters") #ifdef DEBUG #define ui_edit_str_check(s) do { \ /* check the s struct constrains */ \ rt_assert(s->b >= 0); \ rt_assert(s->c == 0 || s->c >= s->b); \ rt_assert(s->g >= 0); \ /* s->g2b[] may be null (not heap allocated) when .b == 0 */ \ if (s->g == 0) { rt_assert(s->b == 0); } \ if (s->g > 0) { \ rt_assert(s->g2b[0] == 0 && s->g2b[s->g] == s->b); \ } \ for (int32_t i = 1; i < s->g; i++) { \ rt_assert(0 < s->g2b[i] - s->g2b[i - 1] && \ s->g2b[i] - s->g2b[i - 1] <= 4); \ rt_assert(s->g2b[i] - s->g2b[i - 1] == \ rt_str.utf8bytes( \ s->u + s->g2b[i - 1], s->g2b[i] - s->g2b[i - 1])); \ } \ } while (0) #define ui_edit_str_check_from_to(s, f, t) do { \ rt_assert(0 <= f && f <= s->g); \ rt_assert(0 <= t && t <= s->g); \ rt_assert(f <= t); \ } while (0) #define ui_edit_str_check_empty(u, b) do { \ if (b == 0) { rt_assert(u != null && u[0] == 0x00); } \ if (u == null || u[0] == 0x00) { rt_assert(b == 0); } \ } while (0) #else #define ui_edit_str_check(s) do { } while (0) #define ui_edit_str_check_from_to(s, f, t) do { } while (0) #define ui_edit_str_check_empty(u, b) do { } while (0) #endif // ui_edit_str_foo(*, "...", -1) treat as 0x00 terminated // ui_edit_str_foo(*, null, 0) treat as ("", 0) #define ui_edit_str_parameters(u, b) do { \ if (u == null) { u = ui_edit_str_empty_utf8; } \ if (b < 0) { \ rt_assert(strlen(u) < INT32_MAX); \ b = (int32_t)strlen(u); \ } \ ui_edit_str_check_empty(u, b); \ } while (0) static int32_t ui_edit_str_gp_to_bp(const char* utf8, int32_t bytes, int32_t gp) { rt_swear(bytes >= 0); bool ok = true; int32_t c = 0; int32_t i = 0; if (bytes > 0) { while (c < gp && ok) { rt_assert(i < bytes); const int32_t b = rt_str.utf8bytes(utf8 + i, bytes - i); ok = 0 < b && i + b <= bytes; if (ok) { i += b; c++; } } } rt_assert(i <= bytes); return ok ? i : -1; } static void ui_edit_str_free(ui_edit_str_t* s) { if (s->g2b != null && s->g2b != ui_edit_str_g2b_ascii) { rt_heap.free(s->g2b); } else { #ifdef UI_EDIT_STR_TEST // check ui_edit_str_g2b_ascii integrity for (int32_t i = 0; i < rt_countof(ui_edit_str_g2b_ascii); i++) { rt_assert(ui_edit_str_g2b_ascii[i] == i); } #endif } s->g2b = null; s->g = 0; if (s->c > 0) { rt_heap.free(s->u); s->u = null; s->c = 0; s->b = 0; } else { s->u = null; s->b = 0; } ui_edit_check_zeros(s, sizeof(*s)); } static bool ui_edit_str_init_g2b(ui_edit_str_t* s) { const int64_t _4_bytes = (int64_t)sizeof(int32_t); // start with number of glyphs == number of bytes (ASCII text): bool ok = rt_heap.alloc(&s->g2b, (size_t)(s->b + 1) * _4_bytes) == 0; int32_t i = 0; // index in u[] string int32_t k = 1; // glyph number // g2b[k] start postion in uint8_t offset from utf8 text of glyph[k] while (i < s->b && ok) { const int32_t b = rt_str.utf8bytes(s->u + i, s->b - i); ok = b > 0 && i + b <= s->b; if (ok) { i += b; s->g2b[k] = i; k++; } } if (ok) { rt_assert(0 < k && k <= s->b + 1); s->g2b[0] = 0; rt_assert(s->g2b[k - 1] == s->b); s->g = k - 1; if (k < s->b + 1) { ok = rt_heap.realloc(&s->g2b, k * _4_bytes) == 0; rt_assert(ok, "shrinking - should always be ok"); } } return ok; } static bool ui_edit_str_init(ui_edit_str_t* s, const char* u, int32_t b, bool heap) { enum { n = rt_countof(ui_edit_str_g2b_ascii) }; if (ui_edit_str_g2b_ascii[n - 1] != n - 1) { for (int32_t i = 0; i < n; i++) { ui_edit_str_g2b_ascii[i] = i; } } bool ok = true; ui_edit_check_zeros(s, sizeof(*s)); // caller must zero out memset(s, 0x00, sizeof(*s)); ui_edit_str_parameters(u, b); if (b == 0) { // cast below intentionally removes "const" qualifier s->g2b = (int32_t*)ui_edit_str_g2b_ascii; s->u = (char*)u; rt_assert(s->c == 0 && u[0] == 0x00); } else { if (heap) { ok = rt_heap.alloc((void**)&s->u, b) == 0; if (ok) { s->c = b; memmove(s->u, u, (size_t)b); } } else { s->u = (char*)u; } if (ok) { s->b = b; if (b == 1 && u[0] <= 0x7F) { s->g2b = (int32_t*)ui_edit_str_g2b_ascii; s->g = 1; } else { ok = ui_edit_str_init_g2b(s); } } } if (ok) { ui_edit_str.shrink(s); } else { ui_edit_str.free(s); } return ok; } static void ui_edit_str_swap(ui_edit_str_t* s1, ui_edit_str_t* s2) { ui_edit_str_t s = *s1; *s1 = *s2; *s2 = s; } static int32_t ui_edit_str_bytes(ui_edit_str_t* s, int32_t f, int32_t t) { // glyph positions ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); return s->g2b[t] - s->g2b[f]; } static bool ui_edit_str_move_g2b_to_heap(ui_edit_str_t* s) { bool ok = true; if (s->g2b == ui_edit_str_g2b_ascii) { // even for s->g == 0 if (s->b == s->g && s->g < rt_countof(ui_edit_str_g2b_ascii) - 1) { // rt_println("forcefully moving to heap"); // this is usually done in the process of concatenation // of 2 ascii strings when result is known to be longer // than rt_countof(ui_edit_str_g2b_ascii) - 1 but the // first string in concatenation is short. It's OK. } const int32_t bytes = (s->g + 1) * (int32_t)sizeof(int32_t); ok = rt_heap.alloc(&s->g2b, bytes) == 0; if (ok) { memmove(s->g2b, ui_edit_str_g2b_ascii, (size_t)bytes); } } return ok; } static bool ui_edit_str_move_to_heap(ui_edit_str_t* s, int32_t c) { bool ok = true; rt_assert(c >= s->b, "can expand cannot shrink"); if (s->c == 0) { // s->u points outside of the heap const char* o = s->u; ok = rt_heap.alloc((void**)&s->u, c) == 0; if (ok) { memmove(s->u, o, (size_t)s->b); } } else if (s->c < c) { ok = rt_heap.realloc((void**)&s->u, c) == 0; } if (ok) { s->c = c; } return ok; } static bool ui_edit_str_expand(ui_edit_str_t* s, int32_t c) { rt_swear(c > 0); bool ok = ui_edit_str_move_to_heap(s, c); if (ok && c > s->c) { if (rt_heap.realloc((void**)&s->u, c) == 0) { s->c = c; } else { ok = false; } } return ok; } static void ui_edit_str_shrink(ui_edit_str_t* s) { if (s->c > s->b) { // s->c == 0 for empty and single byte ASCII strings rt_assert(s->u != ui_edit_str_empty_utf8); if (s->b == 0) { rt_heap.free(s->u); s->u = ui_edit_str_empty_utf8; } else { bool ok = rt_heap.realloc((void**)&s->u, s->b) == 0; rt_swear(ok, "smaller size is always expected to be ok"); } s->c = s->b; } // Optimize memory for short ASCII only strings: if (s->g2b != ui_edit_str_g2b_ascii) { if (s->g == s->b && s->g < rt_countof(ui_edit_str_g2b_ascii) - 1) { // If this is an ascii only utf8 string shorter than // ui_edit_str_g2b_ascii it does not need .g2b[] allocated: if (s->g2b != ui_edit_str_g2b_ascii) { rt_heap.free(s->g2b); s->g2b = ui_edit_str_g2b_ascii; } } else { // const int32_t b64 = rt_min(s->b, 64); // rt_println("none ASCII: .b:%d .g:%d %*.*s", s->b, s->g, b64, b64, s->u); } } } static bool ui_edit_str_remove(ui_edit_str_t* s, int32_t f, int32_t t) { bool ok = true; // optimistic approach ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); const int32_t bytes_to_remove = s->g2b[t] - s->g2b[f]; rt_assert(bytes_to_remove >= 0); if (bytes_to_remove > 0) { ok = ui_edit_str_move_to_heap(s, s->b); if (ok) { const int32_t bytes_to_shift = s->b - s->g2b[t]; rt_assert(0 <= bytes_to_shift && bytes_to_shift <= s->b); memmove(s->u + s->g2b[f], s->u + s->g2b[t], (size_t)bytes_to_shift); if (s->g2b != ui_edit_str_g2b_ascii) { memmove(s->g2b + f, s->g2b + t, (size_t)(s->g - t + 1) * sizeof(int32_t)); for (int32_t i = f; i <= s->g; i++) { s->g2b[i] -= bytes_to_remove; } } else { // no need to shrink g2b[] for ASCII only strings: for (int32_t i = 0; i <= s->g; i++) { rt_assert(s->g2b[i] == i); } } s->b -= bytes_to_remove; s->g -= t - f; } } ui_edit_str_check(s); return ok; } static bool ui_edit_str_replace(ui_edit_str_t* s, int32_t f, int32_t t, const char* u, int32_t b) { const int64_t _4_bytes = (int64_t)sizeof(int32_t); bool ok = true; // optimistic approach ui_edit_str_check_from_to(s, f, t); ui_edit_str_check(s); ui_edit_str_parameters(u, b); // we are inserting "b" bytes and removing "t - f" glyphs const int32_t bytes_to_remove = s->g2b[t] - s->g2b[f]; const int32_t bytes_to_insert = b; // only for readability if (b == 0) { // just remove glyphs ok = ui_edit_str_remove(s, f, t); } else { // remove and insert ui_edit_str_t ins = {0}; // ui_edit_str_init_ro() verifies utf-8 and calculates g2b[]: ok = ui_edit_str_init(&ins, u, b, false); const int32_t glyphs_to_insert = ins.g; // only for readability const int32_t glyphs_to_remove = t - f; // only for readability if (ok) { const int32_t bytes = s->b + bytes_to_insert - bytes_to_remove; rt_assert(ins.g2b != null); // pacify code analysis rt_assert(bytes > 0); const int32_t c = rt_max(s->b, bytes); // keep g2b == ui_edit_str_g2b_ascii as much as possible const bool all_ascii = s->g2b == ui_edit_str_g2b_ascii && ins.g2b == ui_edit_str_g2b_ascii && bytes < rt_countof(ui_edit_str_g2b_ascii) - 1; ok = ui_edit_str_move_to_heap(s, c); if (ok) { if (!all_ascii) { ui_edit_str_move_g2b_to_heap(s); } // insert ui_edit_str_t "ins" at glyph position "f" // reusing ins.u[0..ins.b-1] and ins.g2b[0..ins.g] // moving memory using memmove() left to right: if (bytes_to_insert <= bytes_to_remove) { memmove(s->u + s->g2b[f] + bytes_to_insert, s->u + s->g2b[f] + bytes_to_remove, (size_t)(s->b - s->g2b[f] - bytes_to_remove)); if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); memmove(s->g2b + f + glyphs_to_insert, s->g2b + f + glyphs_to_remove, (size_t)(s->g - t + 1) * _4_bytes); } memmove(s->u + s->g2b[f], ins.u, (size_t)ins.b); } else { if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); const int32_t g = s->g + glyphs_to_insert - glyphs_to_remove; rt_assert(g > s->g); ok = rt_heap.realloc(&s->g2b, (size_t)(g + 1) * _4_bytes) == 0; } // need to shift bytes staring with s.g2b[t] toward the end if (ok) { memmove(s->u + s->g2b[f] + bytes_to_insert, s->u + s->g2b[f] + bytes_to_remove, (size_t)(s->b - s->g2b[f] - bytes_to_remove)); if (all_ascii) { rt_assert(s->g2b == ui_edit_str_g2b_ascii); } else { rt_assert(s->g2b != ui_edit_str_g2b_ascii); memmove(s->g2b + f + glyphs_to_insert, s->g2b + f + glyphs_to_remove, (size_t)(s->g - t + 1) * _4_bytes); } memmove(s->u + s->g2b[f], ins.u, (size_t)ins.b); } } if (ok) { if (!all_ascii) { rt_assert(s->g2b != null && s->g2b != ui_edit_str_g2b_ascii); for (int32_t i = f; i <= f + glyphs_to_insert; i++) { s->g2b[i] = ins.g2b[i - f] + s->g2b[f]; } } else { rt_assert(s->g2b == ui_edit_str_g2b_ascii); for (int32_t i = f; i <= f + glyphs_to_insert; i++) { rt_assert(ui_edit_str_g2b_ascii[i] == i); rt_assert(ins.g2b[i - f] + s->g2b[f] == i); } } s->b += bytes_to_insert - bytes_to_remove; s->g += glyphs_to_insert - glyphs_to_remove; rt_assert(s->b == bytes); if (!all_ascii) { rt_assert(s->g2b != ui_edit_str_g2b_ascii); for (int32_t i = f + glyphs_to_insert + 1; i <= s->g; i++) { s->g2b[i] += bytes_to_insert - bytes_to_remove; } s->g2b[s->g] = s->b; } else { rt_assert(s->g2b == ui_edit_str_g2b_ascii); for (int32_t i = f + glyphs_to_insert + 1; i <= s->g; i++) { rt_assert(s->g2b[i] == i); rt_assert(ui_edit_str_g2b_ascii[i] == i); } rt_assert(s->g2b[s->g] == s->b); } } } ui_edit_str_free(&ins); } } ui_edit_str_shrink(s); ui_edit_str_check(s); return ok; } static bool ui_edit_str_is_zwj(uint32_t utf32) { return utf32 == 0x200D; } static bool ui_edit_str_is_punctuation(uint32_t utf32) { return (utf32 >= 0x0021 && utf32 <= 0x0023) || // !"# (utf32 >= 0x0025 && utf32 <= 0x002A) || // %&'()*+ (utf32 >= 0x002C && utf32 <= 0x002F) || // ,-./ (utf32 >= 0x003A && utf32 <= 0x003B) || //:; (utf32 >= 0x003F && utf32 <= 0x0040) || // ?@ (utf32 >= 0x005B && utf32 <= 0x005D) || // [\] (utf32 == 0x005F) || // _ (utf32 == 0x007B) || // { (utf32 == 0x007D) || // } (utf32 == 0x007E) || // ~ (utf32 >= 0x2000 && utf32 <= 0x206F) || // General Punctuation (utf32 >= 0x3000 && utf32 <= 0x303F) || // CJK Symbols and Punctuation (utf32 >= 0xFE30 && utf32 <= 0xFE4F) || // CJK Compatibility Forms (utf32 >= 0xFE50 && utf32 <= 0xFE6F) || // Small Form Variants (utf32 >= 0xFF01 && utf32 <= 0xFF0F) || // Fullwidth ASCII variants (utf32 >= 0xFF1A && utf32 <= 0xFF1F) || // Fullwidth ASCII variants (utf32 >= 0xFF3B && utf32 <= 0xFF3D) || // Fullwidth ASCII variants (utf32 == 0xFF3F) || // Fullwidth _ (utf32 >= 0xFF5B && utf32 <= 0xFF65); // Fullwidth ASCII variants and halfwidth forms } static bool ui_edit_str_is_letter(uint32_t utf32) { return (utf32 >= 0x0041 && utf32 <= 0x005A) || // Latin uppercase (utf32 >= 0x0061 && utf32 <= 0x007A) || // Latin lowercase (utf32 >= 0x00C0 && utf32 <= 0x00D6) || // Latin-1 uppercase (utf32 >= 0x00D8 && utf32 <= 0x00F6) || // Latin-1 lowercase (utf32 >= 0x00F8 && utf32 <= 0x00FF) || // Latin-1 lowercase (utf32 >= 0x0100 && utf32 <= 0x017F) || // Latin Extended-A (utf32 >= 0x0180 && utf32 <= 0x024F) || // Latin Extended-B (utf32 >= 0x0250 && utf32 <= 0x02AF) || // IPA Extensions (utf32 >= 0x0370 && utf32 <= 0x03FF) || // Greek and Coptic (utf32 >= 0x0400 && utf32 <= 0x04FF) || // Cyrillic (utf32 >= 0x0500 && utf32 <= 0x052F) || // Cyrillic Supplement (utf32 >= 0x0530 && utf32 <= 0x058F) || // Armenian (utf32 >= 0x10A0 && utf32 <= 0x10FF) || // Georgian (utf32 >= 0x0600 && utf32 <= 0x06FF) || // Arabic (covers Arabic, Kurdish, and Pashto) (utf32 >= 0x0900 && utf32 <= 0x097F) || // Devanagari (covers Hindi) (utf32 >= 0x0980 && utf32 <= 0x09FF) || // Bengali (utf32 >= 0x0A00 && utf32 <= 0x0A7F) || // Gurmukhi (common in Northern India, related to Punjabi) (utf32 >= 0x0B80 && utf32 <= 0x0BFF) || // Tamil (utf32 >= 0x0C00 && utf32 <= 0x0C7F) || // Telugu (utf32 >= 0x0C80 && utf32 <= 0x0CFF) || // Kannada (utf32 >= 0x0D00 && utf32 <= 0x0D7F) || // Malayalam (utf32 >= 0x0D80 && utf32 <= 0x0DFF) || // Sinhala (utf32 >= 0x3040 && utf32 <= 0x309F) || // Hiragana (because it is syllabic) (utf32 >= 0x30A0 && utf32 <= 0x30FF) || // Katakana (utf32 >= 0x1E00 && utf32 <= 0x1EFF); // Latin Extended Additional } static bool ui_edit_str_is_spacing(uint32_t utf32) { return (utf32 >= 0x02B0 && utf32 <= 0x02FF) || // Spacing Modifier Letters (utf32 >= 0xA700 && utf32 <= 0xA71F); // Modifier Tone Letters } static bool ui_edit_str_is_combining(uint32_t utf32) { return (utf32 >= 0x0300 && utf32 <= 0x036F) || // Combining Diacritical Marks (utf32 >= 0x1AB0 && utf32 <= 0x1AFF) || // Combining Diacritical Marks Extended (utf32 >= 0x1DC0 && utf32 <= 0x1DFF) || // Combining Diacritical Marks Supplement (utf32 >= 0x20D0 && utf32 <= 0x20FF) || // Combining Diacritical Marks for Symbols (utf32 >= 0xFE20 && utf32 <= 0xFE2F); // Combining Half Marks } static bool ui_edit_str_is_blank(uint32_t utf32) { return (utf32 == 0x0009) || // Horizontal Tab (utf32 == 0x000A) || // Line Feed (utf32 == 0x000B) || // Vertical Tab (utf32 == 0x000C) || // Form Feed (utf32 == 0x000D) || // Carriage Return (utf32 == 0x0020) || // Space (utf32 == 0x0085) || // Next Line (utf32 == 0x00A0) || // Non-breaking Space (utf32 == 0x1680) || // Ogham Space Mark (utf32 >= 0x2000 && utf32 <= 0x200A) || // En Quad to Hair Space (utf32 == 0x2028) || // Line Separator (utf32 == 0x2029) || // Paragraph Separator (utf32 == 0x202F) || // Narrow No-Break Space (utf32 == 0x205F) || // Medium Mathematical Space (utf32 == 0x3000); // Ideographic Space } static bool ui_edit_str_is_symbol(uint32_t utf32) { return (utf32 >= 0x0024 && utf32 <= 0x0024) || // Dollar sign (utf32 >= 0x00A2 && utf32 <= 0x00A5) || // Cent sign to Yen sign (utf32 >= 0x20A0 && utf32 <= 0x20CF) || // Currency Symbols (utf32 >= 0x2100 && utf32 <= 0x214F) || // Letter like Symbols (utf32 >= 0x2190 && utf32 <= 0x21FF) || // Arrows (utf32 >= 0x2200 && utf32 <= 0x22FF) || // Mathematical Operators (utf32 >= 0x2300 && utf32 <= 0x23FF) || // Miscellaneous Technical (utf32 >= 0x2400 && utf32 <= 0x243F) || // Control Pictures (utf32 >= 0x2440 && utf32 <= 0x245F) || // Optical Character Recognition (utf32 >= 0x2460 && utf32 <= 0x24FF) || // Enclosed Alphanumeric (utf32 >= 0x2500 && utf32 <= 0x257F) || // Box Drawing (utf32 >= 0x2580 && utf32 <= 0x259F) || // Block Elements (utf32 >= 0x25A0 && utf32 <= 0x25FF) || // Geometric Shapes (utf32 >= 0x2600 && utf32 <= 0x26FF) || // Miscellaneous Symbols (utf32 >= 0x2700 && utf32 <= 0x27BF) || // Dingbats (utf32 >= 0x2900 && utf32 <= 0x297F) || // Supplemental Arrows-B (utf32 >= 0x2B00 && utf32 <= 0x2BFF) || // Miscellaneous Symbols and Arrows (utf32 >= 0xFB00 && utf32 <= 0xFB4F) || // Alphabetic Presentation Forms (utf32 >= 0xFE50 && utf32 <= 0xFE6F) || // Small Form Variants (utf32 >= 0xFF01 && utf32 <= 0xFF20) || // Fullwidth ASCII variants (utf32 >= 0xFF3B && utf32 <= 0xFF40) || // Fullwidth ASCII variants (utf32 >= 0xFF5B && utf32 <= 0xFF65); // Fullwidth ASCII variants } static bool ui_edit_str_is_digit(uint32_t utf32) { return (utf32 >= 0x0030 && utf32 <= 0x0039) || // ASCII digits 0-9 (utf32 >= 0x0660 && utf32 <= 0x0669) || // Arabic-Indic digits (utf32 >= 0x06F0 && utf32 <= 0x06F9) || // Extended Arabic-Indic digits (utf32 >= 0x07C0 && utf32 <= 0x07C9) || // N'Ko digits (utf32 >= 0x0966 && utf32 <= 0x096F) || // Devanagari digits (utf32 >= 0x09E6 && utf32 <= 0x09EF) || // Bengali digits (utf32 >= 0x0A66 && utf32 <= 0x0A6F) || // Gurmukhi digits (utf32 >= 0x0AE6 && utf32 <= 0x0AEF) || // Gujarati digits (utf32 >= 0x0B66 && utf32 <= 0x0B6F) || // Oriya digits (utf32 >= 0x0BE6 && utf32 <= 0x0BEF) || // Tamil digits (utf32 >= 0x0C66 && utf32 <= 0x0C6F) || // Telugu digits (utf32 >= 0x0CE6 && utf32 <= 0x0CEF) || // Kannada digits (utf32 >= 0x0D66 && utf32 <= 0x0D6F) || // Malayalam digits (utf32 >= 0x0E50 && utf32 <= 0x0E59) || // Thai digits (utf32 >= 0x0ED0 && utf32 <= 0x0ED9) || // Lao digits (utf32 >= 0x0F20 && utf32 <= 0x0F29) || // Tibetan digits (utf32 >= 0x1040 && utf32 <= 0x1049) || // Myanmar digits (utf32 >= 0x17E0 && utf32 <= 0x17E9) || // Khmer digits (utf32 >= 0x1810 && utf32 <= 0x1819) || // Mongolian digits (utf32 >= 0xFF10 && utf32 <= 0xFF19); // Fullwidth digits } static bool ui_edit_str_is_alphanumeric(uint32_t utf32) { return ui_edit_str.is_letter(utf32) || ui_edit_str.is_digit(utf32); } static bool ui_edit_str_is_cjk_or_emoji(uint32_t utf32) { return !ui_edit_str_is_letter(utf32) && ((utf32 >= 0x4E00 && utf32 <= 0x9FFF) || // CJK Unified Ideographs (utf32 >= 0x3400 && utf32 <= 0x4DBF) || // CJK Unified Ideographs Extension A (utf32 >= 0x20000 && utf32 <= 0x2A6DF) || // CJK Unified Ideographs Extension B (utf32 >= 0x2A700 && utf32 <= 0x2B73F) || // CJK Unified Ideographs Extension C (utf32 >= 0x2B740 && utf32 <= 0x2B81F) || // CJK Unified Ideographs Extension D (utf32 >= 0x2B820 && utf32 <= 0x2CEAF) || // CJK Unified Ideographs Extension E (utf32 >= 0x2CEB0 && utf32 <= 0x2EBEF) || // CJK Unified Ideographs Extension F (utf32 >= 0xF900 && utf32 <= 0xFAFF) || // CJK Compatibility Ideographs (utf32 >= 0x2F800 && utf32 <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement (utf32 >= 0x1F600 && utf32 <= 0x1F64F) || // Emoticons (utf32 >= 0x1F300 && utf32 <= 0x1F5FF) || // Misc Symbols and Pictographs (utf32 >= 0x1F680 && utf32 <= 0x1F6FF) || // Transport and Map (utf32 >= 0x1F700 && utf32 <= 0x1F77F) || // Alchemical Symbols (utf32 >= 0x1F780 && utf32 <= 0x1F7FF) || // Geometric Shapes Extended (utf32 >= 0x1F800 && utf32 <= 0x1F8FF) || // Supplemental Arrows-C (utf32 >= 0x1F900 && utf32 <= 0x1F9FF) || // Supplemental Symbols and Pictographs (utf32 >= 0x1FA00 && utf32 <= 0x1FA6F) || // Chess Symbols (utf32 >= 0x1FA70 && utf32 <= 0x1FAFF) || // Symbols and Pictographs Extended-A (utf32 >= 0x1FB00 && utf32 <= 0x1FBFF)); // Symbols for Legacy Computing } static bool ui_edit_str_can_break(uint32_t cp1, uint32_t cp2) { return !ui_edit_str.is_zwj(cp2) && (ui_edit_str.is_cjk_or_emoji(cp1) || ui_edit_str.is_cjk_or_emoji(cp2) || ui_edit_str.is_punctuation(cp1) || ui_edit_str.is_punctuation(cp2) || ui_edit_str.is_blank(cp1) || ui_edit_str.is_blank(cp2) || ui_edit_str.is_combining(cp1) || ui_edit_str.is_combining(cp2) || ui_edit_str.is_spacing(cp1) || ui_edit_str.is_spacing(cp2)); } #pragma push_macro("ui_edit_usd") #pragma push_macro("ui_edit_gbp") #pragma push_macro("ui_edit_euro") #pragma push_macro("ui_edit_money_bag") #pragma push_macro("ui_edit_pot_of_honey") #pragma push_macro("ui_edit_gothic_hwair") #define ui_edit_usd "\x24" #define ui_edit_gbp "\xC2\xA3" #define ui_edit_euro "\xE2\x82\xAC" // https://www.compart.com/en/unicode/U+1F4B0 #define ui_edit_money_bag "\xF0\x9F\x92\xB0" // https://www.compart.com/en/unicode/U+1F36F #define ui_edit_pot_of_honey "\xF0\x9F\x8D\xAF" // https://www.compart.com/en/unicode/U+10348 #define ui_edit_gothic_hwair "\xF0\x90\x8D\x88" // Gothic Letter Hwair static void ui_edit_str_test_replace(void) { // exhaustive permutations // Exhaustive 9,765,625 replace permutations may take // up to 5 minutes of CPU time in release. // Recommended to be invoked at least once after making any // changes to ui_edit_str.replace and around. // Menu: Debug / Windows / Show Diagnostic Tools allows to watch // memory pressure for whole 3 minutes making sure code is // not leaking memory profusely. const char* gs[] = { // glyphs "", ui_edit_usd, ui_edit_gbp, ui_edit_euro, ui_edit_money_bag }; const int32_t gb[] = {0, 1, 2, 3, 4}; // number of bytes per codepoint enum { n = rt_countof(gs) }; int32_t npn = 1; // n to the power of n for (int32_t i = 0; i < n; i++) { npn *= n; } int32_t gix_src[n] = {0}; // 5^5 = 3,125 3,125 * 3,125 = 9,765,625 for (int32_t i = 0; i < npn; i++) { int32_t vi = i; for (int32_t j = 0; j < n; j++) { gix_src[j] = vi % n; vi /= n; } int32_t g2p[n + 1] = {0}; int32_t ngx = 1; // next glyph index char src[128] = {0}; for (int32_t j = 0; j < n; j++) { if (gix_src[j] > 0) { strcat(src, gs[gix_src[j]]); rt_assert(1 <= ngx && ngx <= n); g2p[ngx] = g2p[ngx - 1] + gb[gix_src[j]]; ngx++; } } if (i % 100 == 99) { rt_println("%2d%% [%d][%d][%d][%d][%d] " "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\": \"%s\"", (i * 100) / npn, gix_src[0], gix_src[1], gix_src[2], gix_src[3], gix_src[4], gs[gix_src[0]], gs[gix_src[1]], gs[gix_src[2]], gs[gix_src[3]], gs[gix_src[4]], src); } ui_edit_str_t s = {0}; // reference constructor does not copy to heap: bool ok = ui_edit_str_init(&s, src, -1, false); rt_swear(ok); for (int32_t f = 0; f <= s.g; f++) { // from for (int32_t t = f; t <= s.g; t++) { // to int32_t gix_rep[n] = {0}; // replace range [f, t] with all possible glyphs sequences: for (int32_t k = 0; k < npn; k++) { int32_t vk = i; for (int32_t j = 0; j < n; j++) { gix_rep[j] = vk % n; vk /= n; } char rep[128] = {0}; for (int32_t j = 0; j < n; j++) { strcat(rep, gs[gix_rep[j]]); } char e1[128] = {0}; // expected based on s.g2b[] snprintf(e1, rt_countof(e1), "%.*s%s%.*s", s.g2b[f], src, rep, s.b - s.g2b[t], src + s.g2b[t] ); char e2[128] = {0}; // expected based on gs[] snprintf(e2, rt_countof(e1), "%.*s%s%.*s", g2p[f], src, rep, (int32_t)strlen(src) - g2p[t], src + g2p[t] ); rt_swear(strcmp(e1, e2) == 0, "s.u[%d:%d]: \"%.*s\" g:%d [%d:%d] rep=\"%s\" " "e1: \"%s\" e2: \"%s\"", s.b, s.c, s.b, s.u, s.g, f, t, rep, e1, e2); ui_edit_str_t c = {0}; // copy ok = ui_edit_str_init(&c, src, -1, true); rt_swear(ok); ok = ui_edit_str_replace(&c, f, t, rep, -1); rt_swear(ok); rt_swear(memcmp(c.u, e1, c.b) == 0, "s.u[%d:%d]: \"%.*s\" g:%d [%d:%d] rep=\"%s\" " "expected: \"%s\"", s.b, s.c, s.b, s.u, s.g, f, t, rep, e1); ui_edit_str_free(&c); } } } ui_edit_str_free(&s); } } static void ui_edit_str_test_glyph_bytes(void) { #pragma push_macro("glyph_bytes_test") #define glyph_bytes_test(s, b, expectancy) \ rt_swear(rt_str.utf8bytes(s, b) == expectancy) // Valid Sequences glyph_bytes_test("a", 1, 1); glyph_bytes_test(ui_edit_gbp, 2, 2); glyph_bytes_test(ui_edit_euro, 3, 3); glyph_bytes_test(ui_edit_gothic_hwair, 4, 4); // Invalid Continuation Bytes glyph_bytes_test("\xC2\x00", 2, 0); glyph_bytes_test("\xE0\x80\x00", 3, 0); glyph_bytes_test("\xF0\x80\x80\x00", 4, 0); // Overlong Encodings glyph_bytes_test("\xC0\xAF", 2, 0); // '!' glyph_bytes_test("\xE0\x9F\xBF", 3, 0); // upside down '?' glyph_bytes_test("\xF0\x80\x80\xBF", 4, 0); // '~' // UTF-16 Surrogates glyph_bytes_test("\xED\xA0\x80", 3, 0); // High surrogate glyph_bytes_test("\xED\xBF\xBF", 3, 0); // Low surrogate // Code Points Outside Valid Range glyph_bytes_test("\xF4\x90\x80\x80", 4, 0); // U+110000 // Invalid Initial Bytes glyph_bytes_test("\xC0", 1, 0); glyph_bytes_test("\xC1", 1, 0); glyph_bytes_test("\xF5", 1, 0); glyph_bytes_test("\xFF", 1, 0); // 5-byte sequence (always invalid) glyph_bytes_test("\xF8\x88\x80\x80\x80", 5, 0); #pragma pop_macro("glyph_bytes_test") } static void ui_edit_str_test(void) { ui_edit_str_test_glyph_bytes(); { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "hello", -1, false); rt_swear(ok); rt_swear(s.b == 5 && s.c == 0 && memcmp(s.u, "hello", 5) == 0); rt_swear(s.g == 5 && s.g2b != null); for (int32_t i = 0; i <= s.g; i++) { rt_swear(s.g2b[i] == i); } ui_edit_str_free(&s); } const char* currencies = ui_edit_usd ui_edit_gbp ui_edit_euro ui_edit_money_bag; const char* money = currencies; { ui_edit_str_t s = {0}; const int32_t n = (int32_t)strlen(currencies); bool ok = ui_edit_str_init(&s, money, n, true); rt_swear(ok); rt_swear(s.b == n && s.c == s.b && memcmp(s.u, money, s.b) == 0); rt_swear(s.g == 4 && s.g2b != null); const int32_t g2b[] = {0, 1, 3, 6, 10}; for (int32_t i = 0; i <= s.g; i++) { rt_swear(s.g2b[i] == g2b[i]); } ui_edit_str_free(&s); } { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "hello", -1, false); rt_swear(ok); ok = ui_edit_str_replace(&s, 1, 4, null, 0); rt_swear(ok); rt_swear(s.b == 2 && memcmp(s.u, "ho", 2) == 0); rt_swear(s.g == 2 && s.g2b[0] == 0 && s.g2b[1] == 1 && s.g2b[2] == 2); ui_edit_str_free(&s); } { ui_edit_str_t s = {0}; bool ok = ui_edit_str_init(&s, "Hello world", -1, false); rt_swear(ok); ok = ui_edit_str_replace(&s, 5, 6, " cruel ", -1); rt_swear(ok); ok = ui_edit_str_replace(&s, 0, 5, "Goodbye", -1); rt_swear(ok); ok = ui_edit_str_replace(&s, s.g - 5, s.g, "Universe", -1); rt_swear(ok); rt_swear(s.g == 22 && s.g2b[0] == 0 && s.g2b[s.g] == s.b); for (int32_t i = 1; i < s.g; i++) { rt_swear(s.g2b[i] == i); // because every glyph is ASCII } rt_swear(memcmp(s.u, "Goodbye cruel Universe", 22) == 0); ui_edit_str_free(&s); } #ifdef UI_STR_TEST_REPLACE_ALL_PERMUTATIONS ui_edit_str_test_replace(); #else (void)(void*)ui_edit_str_test_replace; // mitigate unused warning #endif } #pragma push_macro("ui_edit_gothic_hwair") #pragma push_macro("ui_edit_pot_of_honey") #pragma push_macro("ui_edit_money_bag") #pragma push_macro("ui_edit_euro") #pragma push_macro("ui_edit_gbp") #pragma push_macro("ui_edit_usd") #pragma pop_macro("ui_edit_str_parameters") #pragma pop_macro("ui_edit_str_check_empty") #pragma pop_macro("ui_edit_check_zeros") #pragma pop_macro("ui_edit_str_check_from_to") #pragma pop_macro("ui_edit_str_check") #ifdef UI_EDIT_STR_TEST rt_static_init(ui_edit_str) { ui_edit_str.test(); } #endif // tests: static void ui_edit_doc_test_big_text(void) { enum { MB10 = 10 * 1000 * 1000 }; char* text = null; rt_heap.alloc(&text, MB10); memset(text, 'a', (size_t)MB10 - 1); char* p = text; uint32_t seed = 0x1; for (;;) { int32_t n = rt_num.random32(&seed) % 40 + 40; if (p + n >= text + MB10) { break; } p += n; *p = '\n'; } text[MB10 - 1] = 0x00; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, text, MB10, false); rt_swear(ok); ui_edit_text.dispose(&t); rt_heap.free(text); } static void ui_edit_doc_test_paragraphs(void) { // ui_edit_doc_to_paragraphs() is about 1 microsecond for (int i = 0; i < 100; i++) { { // empty string to paragraphs: ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, null, 0, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 1); rt_swear(t.ps[0].u[0] == 0 && t.ps[0].c == 0); rt_swear(t.ps[0].b == 0 && t.ps[0].g == 0); ui_edit_text.dispose(&t); } { // string without "\n" const char* hello = "hello"; const int32_t n = (int32_t)strlen(hello); ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, n, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 1); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == n); rt_swear(t.ps[0].g == n); ui_edit_text.dispose(&t); } { // string with "\n" at the end const char* hello = "hello\n"; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, -1, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 2); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(t.ps[1].u[0] == 0x00); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[1].b == 0); rt_swear(t.ps[1].g == 0); ui_edit_text.dispose(&t); } { // two string separated by "\n" const char* hello = "hello\nworld"; const char* world = hello + 6; ui_edit_text_t t = {0}; bool ok = ui_edit_text.init(&t, hello, -1, false); rt_swear(ok); rt_swear(t.ps != null && t.np == 2); rt_swear(t.ps[0].u == hello); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(t.ps[1].u == world); rt_swear(t.ps[0].c == 0); rt_swear(t.ps[1].b == 5); rt_swear(t.ps[1].g == 5); ui_edit_text.dispose(&t); } } for (int i = 0; i < 10; i++) { ui_edit_doc_test_big_text(); } } typedef struct ui_edit_doc_test_notify_s { ui_edit_notify_t notify; int32_t count_before; int32_t count_after; } ui_edit_doc_test_notify_t; static void ui_edit_doc_test_before(ui_edit_notify_t* n, const ui_edit_notify_info_t* rt_unused(ni)) { ui_edit_doc_test_notify_t* notify = (ui_edit_doc_test_notify_t*)n; notify->count_before++; } static void ui_edit_doc_test_after(ui_edit_notify_t* n, const ui_edit_notify_info_t* rt_unused(ni)) { ui_edit_doc_test_notify_t* notify = (ui_edit_doc_test_notify_t*)n; notify->count_after++; } static struct { ui_edit_notify_t notify; } ui_edit_doc_test_notify; static void ui_edit_doc_test_0(void) { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_text_t ins_text = {0}; rt_swear(ui_edit_text.init(&ins_text, "a", 1, false)); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, null, &ins_text, &undo)); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } static void ui_edit_doc_test_1(void) { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_text_t ins_text = {0}; rt_swear(ui_edit_text.init(&ins_text, "a", 1, false)); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, null, &ins_text, &undo)); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } static void ui_edit_doc_test_2(void) { { // two string separated by "\n" ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_notify_t notify1 = {0}; ui_edit_notify_t notify2 = {0}; ui_edit_doc_test_notify_t before_and_after = {0}; before_and_after.notify.before = ui_edit_doc_test_before; before_and_after.notify.after = ui_edit_doc_test_after; ui_edit_doc.subscribe(d, ¬ify1); ui_edit_doc.subscribe(d, &before_and_after.notify); ui_edit_doc.subscribe(d, ¬ify2); rt_swear(ui_edit_doc.bytes(d, null) == 0, "expected empty"); const char* hello = "hello\nworld"; rt_swear(ui_edit_doc.replace(d, null, hello, -1)); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); rt_swear(t.np == 2); rt_swear(t.ps[0].b == 5); rt_swear(t.ps[0].g == 5); rt_swear(memcmp(t.ps[0].u, "hello", 5) == 0); rt_swear(t.ps[1].b == 5); rt_swear(t.ps[1].g == 5); rt_swear(memcmp(t.ps[1].u, "world", 5) == 0); ui_edit_text.dispose(&t); ui_edit_doc.unsubscribe(d, ¬ify1); ui_edit_doc.unsubscribe(d, &before_and_after.notify); ui_edit_doc.unsubscribe(d, ¬ify2); ui_edit_doc.dispose(d); } // TODO: "GoodbyeCruelUniverse" insert 2x"\n" splitting in 3 paragraphs { // three string separated by "\n" ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "Goodbye" "\n" "Cruel" "\n" "Universe"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); ui_edit_text.dispose(&t); ui_edit_range_t r = { .from = {.pn = 0, .gp = 4}, .to = {.pn = 2, .gp = 3} }; rt_swear(ui_edit_doc.replace(d, &r, null, 0)); rt_swear(d->text.np == 1); rt_swear(d->text.ps[0].b == 9); rt_swear(d->text.ps[0].g == 9); rt_swear(memcmp(d->text.ps[0].u, "Goodverse", 9) == 0); rt_swear(ui_edit_doc.replace(d, null, null, 0)); // remove all rt_swear(d->text.np == 1); rt_swear(d->text.ps[0].b == 0); rt_swear(d->text.ps[0].g == 0); ui_edit_doc.dispose(d); } // TODO: "GoodbyeCruelUniverse" insert 2x"\n" splitting in 3 paragraphs { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; const char* ins[] = { "X\nY", "X\n", "\nY", "\n", "X\nY\nZ" }; for (int32_t i = 0; i < rt_countof(ins); i++) { rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "GoodbyeCruelUniverse"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); ui_edit_range_t r = { .from = {.pn = 0, .gp = 7}, .to = {.pn = 0, .gp = 12} }; ui_edit_text_t ins_text = {0}; ui_edit_text.init(&ins_text, ins[i], -1, false); ui_edit_to_do_t undo = {0}; rt_swear(ui_edit_text.replace(&d->text, &r, &ins_text, &undo)); ui_edit_to_do_t redo = {0}; rt_swear(ui_edit_text.replace(&d->text, &undo.range, &undo.text, &redo)); ui_edit_doc.dispose_to_do(&undo); undo.range = (ui_edit_range_t){0}; rt_swear(ui_edit_text.replace(&d->text, &redo.range, &redo.text, &undo)); ui_edit_doc.dispose_to_do(&redo); ui_edit_doc.dispose_to_do(&undo); ui_edit_text.dispose(&ins_text); ui_edit_doc.dispose(d); } } } static void ui_edit_doc_test_3(void) { { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; ui_edit_doc_test_notify_t before_and_after = {0}; before_and_after.notify.before = ui_edit_doc_test_before; before_and_after.notify.after = ui_edit_doc_test_after; rt_swear(ui_edit_doc.init(d, null, 0, false)); rt_swear(ui_edit_doc.subscribe(d, &before_and_after.notify)); const char* s = "Goodbye Cruel Universe"; const int32_t before = before_and_after.count_before; const int32_t after = before_and_after.count_after; rt_swear(ui_edit_doc.replace(d, null, s, -1)); const int32_t bytes = (int32_t)strlen(s); rt_swear(before + 1 == before_and_after.count_before); rt_swear(after + 1 == before_and_after.count_after); rt_swear(d->text.np == 1); rt_swear(ui_edit_doc.bytes(d, null) == bytes); ui_edit_text_t t = {0}; rt_swear(ui_edit_doc.copy_text(d, null, &t)); rt_swear(t.np == 1); rt_swear(t.ps[0].b == bytes); rt_swear(t.ps[0].g == bytes); rt_swear(memcmp(t.ps[0].u, s, t.ps[0].b) == 0); // with "\n" and 0x00 at the end: int32_t utf8bytes = ui_edit_doc.utf8bytes(d, null); char* p = null; rt_swear(rt_heap.alloc((void**)&p, utf8bytes) == 0); p[utf8bytes - 1] = 0xFF; ui_edit_doc.copy(d, null, p, utf8bytes); rt_swear(p[utf8bytes - 1] == 0x00); rt_swear(memcmp(p, s, bytes) == 0); rt_heap.free(p); ui_edit_text.dispose(&t); ui_edit_doc.unsubscribe(d, &before_and_after.notify); ui_edit_doc.dispose(d); } { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); const char* s = "Hello World" "\n" "Goodbye Cruel Universe"; rt_swear(ui_edit_doc.replace(d, null, s, -1)); rt_swear(ui_edit_doc.undo(d)); rt_swear(ui_edit_doc.bytes(d, null) == 0); rt_swear(ui_edit_doc.utf8bytes(d, null) == 1); rt_swear(ui_edit_doc.redo(d)); { int32_t utf8bytes = ui_edit_doc.utf8bytes(d, null); char* p = null; rt_swear(rt_heap.alloc((void**)&p, utf8bytes) == 0); p[utf8bytes - 1] = 0xFF; ui_edit_doc.copy(d, null, p, utf8bytes); rt_swear(p[utf8bytes - 1] == 0x00); rt_swear(memcmp(p, s, utf8bytes) == 0); rt_heap.free(p); } ui_edit_doc.dispose(d); } } static void ui_edit_doc_test_4(void) { { ui_edit_doc_t edit_doc = {0}; ui_edit_doc_t* d = &edit_doc; rt_swear(ui_edit_doc.init(d, null, 0, false)); ui_edit_range_t r = {0}; r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "a", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "b", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "c", -1)); r = ui_edit_text.end_range(&d->text); rt_swear(ui_edit_doc.replace(d, &r, "\n", -1)); ui_edit_doc.dispose(d); } } static void ui_edit_doc_test(void) { { ui_edit_range_t r = { .from = {0,0}, .to = {0,0} }; rt_static_assertion(sizeof(r.from) + sizeof(r.from) == sizeof(r.a)); rt_swear(&r.from == &r.a[0] && &r.to == &r.a[1]); } #ifdef UI_EDIT_DOC_TEST_PARAGRAPHS ui_edit_doc_test_paragraphs(); #else (void)(void*)ui_edit_doc_test_paragraphs; // unused #endif // use n = 10,000,000 and Diagnostic Tools to watch for memory leaks enum { n = 1000 }; // enum { n = 10 * 1000 * 1000 }; for (int32_t i = 0; i < n; i++) { ui_edit_doc_test_0(); ui_edit_doc_test_1(); ui_edit_doc_test_2(); ui_edit_doc_test_3(); ui_edit_doc_test_4(); } } static const ui_edit_range_t ui_edit_invalid_range = { .from = { .pn = -1, .gp = -1}, .to = { .pn = -1, .gp = -1} }; ui_edit_range_if ui_edit_range = { .compare = ui_edit_range_compare, .order = ui_edit_range_order, .is_valid = ui_edit_range_is_valid, .is_empty = ui_edit_range_is_empty, .uint64 = ui_edit_range_uint64, .pg = ui_edit_range_pg, .inside = ui_edit_range_inside_text, .intersect = ui_edit_range_intersect, .invalid_range = &ui_edit_invalid_range }; ui_edit_text_if ui_edit_text = { .init = ui_edit_text_init, .bytes = ui_edit_text_bytes, .all_on_null = ui_edit_text_all_on_null, .ordered = ui_edit_text_ordered, .end = ui_edit_text_end, .end_range = ui_edit_text_end_range, .dup = ui_edit_text_dup, .equal = ui_edit_text_equal, .copy_text = ui_edit_text_copy_text, .copy = ui_edit_text_copy, .replace = ui_edit_text_replace, .replace_utf8 = ui_edit_text_replace_utf8, .dispose = ui_edit_text_dispose }; ui_edit_doc_if ui_edit_doc = { .init = ui_edit_doc_init, .replace = ui_edit_doc_replace, .bytes = ui_edit_doc_bytes, .copy_text = ui_edit_doc_copy_text, .utf8bytes = ui_edit_doc_utf8bytes, .copy = ui_edit_doc_copy, .redo = ui_edit_doc_redo, .undo = ui_edit_doc_undo, .subscribe = ui_edit_doc_subscribe, .unsubscribe = ui_edit_doc_unsubscribe, .dispose_to_do = ui_edit_doc_dispose_to_do, .dispose = ui_edit_doc_dispose, .test = ui_edit_doc_test }; #pragma push_macro("ui_edit_doc_dump") #pragma push_macro("ui_edit_text_dump") #pragma push_macro("ui_edit_range_dump") #pragma push_macro("ui_edit_pg_dump") #pragma push_macro("ui_edit_check_range_inside_text") #pragma push_macro("ui_edit_check_pg_inside_text") #pragma push_macro("ui_edit_check_zeros") #ifdef UI_EDIT_DOC_TEST rt_static_init(ui_edit_doc) { ui_edit_doc.test(); } #endif ================================================ FILE: src/ui/ui_edit_view.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" #include "ui/ui_edit_doc.h" // TODO: find all "== dt->np" it is wrong pn < dt->np fix them all // TODO: undo/redo coalescing // TODO: back/forward navigation // TODO: exit (Ctrl+W?)/save(Ctrl+S, Ctrl+Shift+S) keyboard shortcuts? // TODO: ctrl left, ctrl right jump word ctrl+shift left/right select word? // TODO: iBeam cursor (definitely yes - see how MSVC does it) // TODO: vertical scrollbar ui // TODO: horizontal scroll: trivial to implement: // add horizontal_scroll to e->w and paint // paragraphs in a horizontally shifted clip // http://worrydream.com/refs/Tesler%20-%20A%20Personal%20History%20of%20Modeless%20Text%20Editing%20and%20Cut-Copy-Paste.pdf // https://web.archive.org/web/20221216044359/http://worrydream.com/refs/Tesler%20-%20A%20Personal%20History%20of%20Modeless%20Text%20Editing%20and%20Cut-Copy-Paste.pdf // Rich text options that are not addressed yet: // * Color of ranges (useful for code editing) // * Soft line breaks inside the paragraph (useful for e.g. bullet lists of options) // * Bold/Italic/Underline (along with color ranges) // * Multiple fonts (as long as run vertical size is the maximum of font) // * Kerning (?! like in overhung "Fl") // When implementation and header are amalgamated // into a single file header library name_space is // used to separate different modules namespaces. typedef struct ui_edit_glyph_s { const char* s; int32_t bytes; } ui_edit_glyph_t; static void ui_edit_layout(ui_view_t* v); static ui_point_t ui_edit_pg_to_xy(ui_edit_view_t* e, const ui_edit_pg_t pg); // Glyphs in monospaced Windows fonts may have different width for non-ASCII // characters. Thus even if edit is monospaced glyph measurements are used // in text layout. static void ui_edit_invalidate_parent(const ui_edit_view_t* e, const ui_rect_t* rc) { // For transparent background of edit_view parent must draw background. // In the current implementation invalidate() causes whole stack redraw // in rectangle thus it does not matter much. But if it is ever optimized // it will matter. ui_color_t b = e->background; if (ui_color_is_undefined(b) || ui_color_is_transparent(b)) { ui_view.invalidate(e->parent, rc); } } static void ui_edit_invalidate_rect(const ui_edit_view_t* e, const ui_rect_t rc) { rt_assert(rc.w >= 0 && rc.h > 0); // w may be zero for empty selection if (rc.w > 0 && rc.h > 0) { ui_view.invalidate(&e->view, &rc); ui_edit_invalidate_parent(e, &rc); } } static void ui_edit_invalidate_view(const ui_edit_view_t* e) { ui_view.invalidate(&e->view, null); ui_edit_invalidate_parent(e, null); } static int32_t ui_edit_line_height(ui_edit_view_t* e) { // at 96dpi: // "Segoe UI" height + line_gap: 16 // ui_app.fm.prop h: 15 pt: 11.250 a: 3 c: 9 d: 3 bl: 12 il: 3 lg: 2 // "Cascadia Mono" height + line_gap: 17 // ui_app.fm.mono h: 16 pt: 12.000 a: 2 c: 11 d: 3 bl: 13 il: 4 lg: 0 return e->fm->height + e->fm->line_gap; } static ui_rect_t ui_edit_selection_rect(ui_edit_view_t* e) { const ui_edit_range_t r = ui_edit_range.order(e->selection); const ui_ltrb_t i = ui_view.margins(&e->view, &e->insets); const ui_point_t p0 = ui_edit_pg_to_xy(e, r.from); const ui_point_t p1 = ui_edit_pg_to_xy(e, r.to); if (p0.x < 0 || p1.x < 0) { // selection outside of visible area return (ui_rect_t) { .x = 0, .y = 0, .w = e->w, .h = e->h }; } else if (p0.y == p1.y) { const int32_t max_w = rt_max(e->fm->max_char_width, e->fm->em.w); int32_t w = p1.x - p0.x != 0 ? p1.x - p0.x + max_w : e->caret_width; return (ui_rect_t) { .x = p0.x, .y = i.top + p0.y, .w = w, .h = ui_edit_line_height(e) }; } else { const int32_t h = p1.y - p0.y + ui_edit_line_height(e); return (ui_rect_t) { .x = 0, .y = i.top + p0.y, .w = e->w, .h = h }; } } #if 0 static void ui_edit_text_width_gp(ui_edit_view_t* e, const char* utf8, int32_t bytes) { const int32_t glyphs = rt_str.glyphs(utf8, bytes); rt_println("\"%.*s\" bytes:%d glyphs:%d", bytes, utf8, bytes, glyphs); int32_t* x = (int32_t*)rt_stackalloc((glyphs + 1) * sizeof(int32_t)); const ui_gdi_ta_t ta = { .fm = e->fm }; ui_wh_t wh = ui_gdi.glyphs_placement(&ta, utf8, bytes, x, glyphs); // rt_println("wh: %dx%d", wh.w, wh.h); } #endif static int32_t ui_edit_text_width(ui_edit_view_t* e, const char* s, int32_t n) { // fp64_t time = rt_clock.seconds(); // average GDI measure_text() performance per character: // "ui_app.fm.mono" ~500us (microseconds) // "ui_app.fm.prop.normal" ~250us (microseconds) DirectWrite ~100us const ui_gdi_ta_t ta = { .fm = e->fm, .color = e->color, .measure = true }; int32_t x = n == 0 ? 0 : ui_gdi.text(&ta, 0, 0, "%.*s", n, s).w; // time = (rt_clock.seconds() - time) * 1000.0; // static fp64_t time_sum; // static fp64_t length_sum; // time_sum += time; // length_sum += n; // rt_println("avg=%.6fms per char total %.3fms", time_sum / length_sum, time_sum); return x; } static int32_t ui_edit_word_break_at(ui_edit_view_t* e, int32_t pn, int32_t rn, const int32_t width, bool allow_zero) { // TODO: in sqlite.c 257,674 lines it takes 11 seconds to get all runs() // on average ui_edit_word_break_at() takes 4 x ui_edit_text_width() // measurements and they are slow. If we can reduce this amount // (not clear how) at least 2 times it will be a win. // Another way is background thread runs() processing but this is // involving a lot of complexity. // MSVC devenv.exe edits sqlite3.c w/o any visible delays int32_t count = 0; // stats logging int32_t chars = 0; ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); ui_edit_paragraph_t* p = &e->para[pn]; const ui_edit_str_t* str = &dt->ps[pn]; int32_t k = 1; // at least 1 glyph // offsets inside a run in glyphs and bytes from start of the paragraph: int32_t gp = p->run[rn].gp; int32_t bp = p->run[rn].bp; if (gp < str->g - 1) { const char* text = str->u + bp; const int32_t glyphs_in_this_run = str->g - gp; int32_t* g2b = &str->g2b[gp]; // 4 is maximum number of bytes in a UTF-8 sequence int32_t gc = rt_min(4, glyphs_in_this_run); int32_t w = ui_edit_text_width(e, text, g2b[gc] - bp); count++; chars += g2b[gc] - bp; while (gc < glyphs_in_this_run && w < width) { gc = rt_min(gc * 4, glyphs_in_this_run); w = ui_edit_text_width(e, text, g2b[gc] - bp); count++; chars += g2b[gc] - bp; } if (w < width) { k = gc; rt_assert(1 <= k && k <= str->g - gp); } else { int32_t i = 0; int32_t j = gc; k = (i + j) / 2; while (i < j) { rt_assert(allow_zero || 1 <= k && k < gc + 1); const int32_t n = g2b[k + 1] - bp; int32_t px = ui_edit_text_width(e, text, n); count++; chars += n; if (px == width) { break; } if (px < width) { i = k + 1; } else { j = k; } if (!allow_zero && (i + j) / 2 == 0) { break; } k = (i + j) / 2; rt_assert(allow_zero || 1 <= k && k <= str->g - gp); } } } rt_assert(allow_zero || 1 <= k && k <= str->g - gp); return k; } static int32_t ui_edit_word_break(ui_edit_view_t* e, int32_t pn, int32_t rn) { return ui_edit_word_break_at(e, pn, rn, e->edit.w, false); } static int32_t ui_edit_glyph_at_x(ui_edit_view_t* e, int32_t pn, int32_t rn, int32_t x) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); if (x == 0 || dt->ps[pn].b == 0) { return 0; } else { return ui_edit_word_break_at(e, pn, rn, x + 1, true); } } static ui_edit_glyph_t ui_edit_glyph_at(ui_edit_view_t* e, ui_edit_pg_t p) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_glyph_t g = { .s = "", .bytes = 0 }; rt_assert(0 <= p.pn && p.pn < dt->np); const ui_edit_str_t* str = &dt->ps[p.pn]; const int32_t bytes = str->b; const char* s = str->u; const int32_t bp = str->g2b[p.gp]; if (bp < bytes) { g.s = s + bp; g.bytes = rt_str.utf8bytes(g.s, bytes - bp); rt_swear(g.bytes > 0); } return g; } // paragraph_runs() breaks paragraph into `runs` according to `width` static const ui_edit_run_t* ui_edit_paragraph_runs(ui_edit_view_t* e, int32_t pn, int32_t* runs) { // fp64_t time = rt_clock.seconds(); rt_assert(e->w > 0); ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); const ui_edit_run_t* r = null; if (e->para[pn].run != null) { *runs = e->para[pn].runs; r = e->para[pn].run; } else { rt_assert(0 <= pn && pn < dt->np); ui_edit_paragraph_t* p = &e->para[pn]; const ui_edit_str_t* str = &dt->ps[pn]; if (p->run == null) { rt_assert(p->runs == 0 && p->run == null); const int32_t max_runs = str->b + 1; bool ok = rt_heap.alloc((void**)&p->run, max_runs * sizeof(ui_edit_run_t)) == 0; rt_swear(ok); ui_edit_run_t* run = p->run; run[0].bp = 0; run[0].gp = 0; int32_t gc = str->b == 0 ? 0 : ui_edit_word_break(e, pn, 0); if (gc == str->g) { // whole paragraph fits into width p->runs = 1; run[0].bytes = str->b; run[0].glyphs = str->g; int32_t pixels = ui_edit_text_width(e, str->u, str->g2b[gc]); run[0].pixels = pixels; } else { rt_assert(gc < str->g); int32_t rc = 0; // runs count int32_t ix = 0; // glyph index from to start of paragraph const char* text = str->u; int32_t bytes = str->b; while (bytes > 0) { rt_assert(rc < max_runs); run[rc].bp = (int32_t)(text - str->u); run[rc].gp = ix; int32_t glyphs = ui_edit_word_break(e, pn, rc); int32_t utf8bytes = str->g2b[ix + glyphs] - run[rc].bp; int32_t pixels = ui_edit_text_width(e, text, utf8bytes); if (glyphs > 1 && utf8bytes < bytes && text[utf8bytes - 1] != 0x20) { // try to find word break SPACE character. utf8 space is 0x20 int32_t i = utf8bytes; while (i > 0 && text[i - 1] != 0x20) { i--; } if (i > 0 && i != utf8bytes) { utf8bytes = i; glyphs = rt_str.glyphs(text, utf8bytes); rt_assert(glyphs >= 0); pixels = ui_edit_text_width(e, text, utf8bytes); } } run[rc].bytes = utf8bytes; run[rc].glyphs = glyphs; run[rc].pixels = pixels; rc++; text += utf8bytes; rt_assert(0 <= utf8bytes && utf8bytes <= bytes); bytes -= utf8bytes; ix += glyphs; } rt_assert(rc > 0); p->runs = rc; // truncate heap capacity array: ok = rt_heap.realloc((void**)&p->run, rc * sizeof(ui_edit_run_t)) == 0; rt_swear(ok); } } *runs = p->runs; r = p->run; } rt_assert(r != null && *runs >= 1); return r; } static int32_t ui_edit_paragraph_run_count(ui_edit_view_t* e, int32_t pn) { rt_swear(e->w > 0); ui_edit_text_t* dt = &e->doc->text; // document text int32_t runs = 0; if (e->w > 0 && 0 <= pn && pn < dt->np) { (void)ui_edit_paragraph_runs(e, pn, &runs); } return runs; } static int32_t ui_edit_glyphs_in_paragraph(ui_edit_view_t* e, int32_t pn) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); (void)ui_edit_paragraph_run_count(e, pn); // word break into runs return dt->ps[pn].g; } static void ui_edit_create_caret(ui_edit_view_t* e) { rt_fatal_if(e->focused); rt_assert(ui_app.is_active()); rt_assert(ui_app.focused()); fp64_t px = ui_app.dpi.monitor_raw / 100.0 + 0.5; e->caret_width = rt_min(3, rt_max(1, (int32_t)px)); ui_app.create_caret(e->caret_width, e->fm->height); // w/o line_gap e->focused = true; // means caret was created // rt_println("e->focused := true %s", ui_view_debug_id(&e->view)); } static void ui_edit_destroy_caret(ui_edit_view_t* e) { rt_fatal_if(!e->focused); ui_app.destroy_caret(); e->focused = false; // means caret was destroyed // rt_println("e->focused := false %s", ui_view_debug_id(&e->view)); } static void ui_edit_show_caret(ui_edit_view_t* e) { if (e->focused) { rt_assert(ui_app.is_active()); rt_assert(ui_app.focused()); rt_assert((e->caret.x < 0) == (e->caret.y < 0)); const ui_ltrb_t insets = ui_view.margins(&e->view, &e->insets); int32_t x = e->caret.x < 0 ? insets.left : e->caret.x; int32_t y = e->caret.y < 0 ? insets.top : e->caret.y; ui_app.move_caret(e->x + x, e->y + y); // TODO: it is possible to support unblinking caret if desired // do not set blink time - use global default // fatal_if_false(SetCaretBlinkTime(500)); ui_app.show_caret(); e->shown++; rt_assert(e->shown == 1); } } static void ui_edit_hide_caret(ui_edit_view_t* e) { if (e->focused) { ui_app.hide_caret(); e->shown--; rt_assert(e->shown == 0); } } static void ui_edit_allocate_runs(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(e->para == null); rt_assert(dt->np > 0); rt_assert(e->para == null); bool done = rt_heap.alloc_zero((void**)&e->para, dt->np * sizeof(e->para[0])) == 0; rt_swear(done, "out of memory - cannot continue"); } static void ui_edit_invalidate_run(ui_edit_view_t* e, int32_t i) { if (e->para[i].run != null) { rt_assert(e->para[i].runs > 0); rt_heap.free(e->para[i].run); e->para[i].run = null; e->para[i].runs = 0; } else { rt_assert(e->para[i].runs == 0); } } static void ui_edit_invalidate_runs(ui_edit_view_t* e, int32_t f, int32_t t, int32_t np) { // [from..to] inclusive inside [0..np - 1] rt_swear(e->para != null && f <= t && 0 <= f && t < np); for (int32_t i = f; i <= t; i++) { ui_edit_invalidate_run(e, i); } } static void ui_edit_invalidate_all_runs(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_invalidate_runs(e, 0, dt->np - 1, dt->np); } static void ui_edit_dispose_runs(ui_edit_view_t* e, int32_t np) { rt_assert(e->para != null); ui_edit_invalidate_runs(e, 0, np - 1, np); rt_heap.free(e->para); e->para = null; } static void ui_edit_dispose_all_runs(ui_edit_view_t* e) { ui_edit_dispose_runs(e, e->doc->text.np); } static void ui_edit_layout_now(ui_edit_view_t* e) { if (e->measure != null && e->layout != null && e->w > 0) { e->layout(&e->view); ui_edit_invalidate_view(e); } } static void ui_edit_if_sle_layout(ui_edit_view_t* e) { // only for single line edit controls that were already initialized // and measured horizontally at least once. if (e->sle && e->layout != null && e->w > 0) { ui_edit_layout_now(e); } } static void ui_edit_view_set_font(ui_edit_view_t* e, ui_fm_t* f) { ui_edit_invalidate_all_runs(e); e->scroll.rn = 0; e->fm = f; ui_edit_layout_now(e); ui_app.request_layout(); } // Paragraph number, glyph number -> run number static ui_edit_pr_t ui_edit_pg_to_pr(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); const ui_edit_str_t* str = &dt->ps[pg.pn]; ui_edit_pr_t pr = { .pn = pg.pn, .rn = -1 }; if (str->b == 0) { // empty rt_assert(pg.gp == 0); pr.rn = 0; } else { rt_assert(0 <= pg.pn && pg.pn < dt->np); int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pg.pn, &runs); if (pg.gp == str->g + 1) { pr.rn = runs - 1; // TODO: past last glyph ??? is this correct? } else { rt_assert(0 <= pg.gp && pg.gp <= str->g); for (int32_t j = 0; j < runs && pr.rn < 0; j++) { const int32_t last_run = j == runs - 1; const int32_t start = run[j].gp; const int32_t end = run[j].gp + run[j].glyphs + last_run; if (start <= pg.gp && pg.gp < end) { pr.rn = j; } } rt_assert(pr.rn >= 0); } } return pr; } static int32_t ui_edit_runs_between(ui_edit_view_t* e, const ui_edit_pg_t pg0, const ui_edit_pg_t pg1) { rt_assert(ui_edit_range.uint64(pg0) <= ui_edit_range.uint64(pg1)); int32_t rn0 = ui_edit_pg_to_pr(e, pg0).rn; int32_t rn1 = ui_edit_pg_to_pr(e, pg1).rn; int32_t rc = 0; if (pg0.pn == pg1.pn) { rt_assert(rn0 <= rn1); rc = rn1 - rn0; } else { rt_assert(pg0.pn < pg1.pn); for (int32_t i = pg0.pn; i < pg1.pn; i++) { const int32_t runs = ui_edit_paragraph_run_count(e, i); if (i == pg0.pn) { rc += runs - rn0; } else { // i < pg1.pn rc += runs; } } rc += rn1; } return rc; } static ui_edit_pg_t ui_edit_scroll_pg(ui_edit_view_t* e) { int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, e->scroll.pn, &runs); // layout may decrease number of runs when view is growing: if (e->scroll.rn >= runs) { e->scroll.rn = runs - 1; } rt_assert(0 <= e->scroll.rn && e->scroll.rn < runs, "e->scroll.rn: %d runs: %d", e->scroll.rn, runs); return (ui_edit_pg_t) { .pn = e->scroll.pn, .gp = run[e->scroll.rn].gp }; } static int32_t ui_edit_first_visible_run(ui_edit_view_t* e, int32_t pn) { return pn == e->scroll.pn ? e->scroll.rn : 0; } // ui_edit::pg_to_xy() paragraph # glyph # -> (x,y) in [0,0 width x height] static ui_point_t ui_edit_pg_to_xy(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); ui_point_t pt = { .x = -1, .y = 0 }; const int32_t spn = e->scroll.pn + 1; const int32_t pn = rt_min(rt_max(spn, pg.pn + 1), dt->np - 1); for (int32_t i = e->scroll.pn; i <= pn && pt.x < 0; i++) { rt_assert(0 <= i && i < dt->np); const ui_edit_str_t* str = &dt->ps[i]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, i, &runs); for (int32_t j = ui_edit_first_visible_run(e, i); j < runs; j++) { const int32_t last_run = j == runs - 1; const int32_t gc = run[j].glyphs; // glyphs count if (i == pg.pn) { // in the last `run` of a paragraph x after last glyph is OK if (run[j].gp <= pg.gp && pg.gp < run[j].gp + gc + last_run) { const char* s = str->u + run[j].bp; const uint32_t bp2e = str->b - run[j].bp; // to end of str int32_t ofs = ui_edit_str.gp_to_bp(s, bp2e, pg.gp - run[j].gp); rt_swear(ofs >= 0); pt.x = ui_edit_text_width(e, s, ofs); break; } } pt.y += ui_edit_line_height(e); } } if (0 <= pt.x && pt.x < e->edit.w && 0 <= pt.y && pt.y < e->edit.h) { // all good, inside visible rectangle or right after it } else { rt_println("%d:%d (%d,%d) outside of %dx%d", pg.pn, pg.gp, pt.x, pt.y, e->edit.w, e->edit.h); pt = (ui_point_t){-1, -1}; } return pt; } static int32_t ui_edit_glyph_width_px(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); const ui_edit_str_t* str = &dt->ps[pg.pn]; const char* text = str->u; int32_t gc = str->g; if (pg.gp == 0 && gc == 0) { return 0; // empty paragraph } else if (pg.gp < gc) { const int32_t bp = ui_edit_str.gp_to_bp(text, str->b, pg.gp); rt_swear(bp >= 0); const char* s = text + bp; int32_t bytes_in_glyph = rt_str.utf8bytes(s, str->b - bp); rt_swear(bytes_in_glyph > 0); int32_t x = ui_edit_text_width(e, s, bytes_in_glyph); return x; } else { rt_assert(pg.gp == gc, "only next position past last glyph is allowed"); return 0; } } // xy_to_pg() (x,y) (0,0, width x height) -> paragraph # glyph # static ui_edit_pg_t ui_edit_xy_to_pg(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = {-1, -1}; int32_t py = 0; // paragraph `y' coordinate for (int32_t i = e->scroll.pn; i < dt->np && pg.pn < 0; i++) { rt_assert(0 <= i && i < dt->np); const ui_edit_str_t* str = &dt->ps[i]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, i, &runs); for (int32_t j = ui_edit_first_visible_run(e, i); j < runs && pg.pn < 0; j++) { const ui_edit_run_t* r = &run[j]; const char* s = str->u + run[j].bp; if (py <= y && y < py + ui_edit_line_height(e)) { int32_t w = ui_edit_text_width(e, s, r->bytes); pg.pn = i; if (x >= w) { pg.gp = r->gp + r->glyphs; } else { pg.gp = r->gp + ui_edit_glyph_at_x(e, i, j, x); if (pg.gp < r->glyphs - 1) { ui_edit_pg_t right = {pg.pn, pg.gp + 1}; int32_t x0 = ui_edit_pg_to_xy(e, pg).x; int32_t x1 = ui_edit_pg_to_xy(e, right).x; if (x1 - x < x - x0) { pg.gp++; // snap to closest glyph's 'x' } } } } else { py += ui_edit_line_height(e); } } if (py > e->h) { break; } } return pg; } static void ui_edit_set_caret(ui_edit_view_t* e, int32_t x, int32_t y) { if (e->caret.x != x || e->caret.y != y) { if (e->focused && ui_app.focused()) { ui_app.move_caret(e->x + x, e->y + y); } const ui_ltrb_t i = ui_view.margins(&e->view, &e->insets); // caret in i.left .. e->view.w - i.right // i.top .. e->view.h - i.bottom // coordinate space rt_swear(i.left <= x && x < e->w && i.top <= y && y < e->h); e->caret.x = x; e->caret.y = y; } } static ui_edit_pg_t ui_edit_view_end_of_text(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text return (ui_edit_pg_t){ .pn = dt->np - 1, .gp = dt->ps[dt->np - 1].g }; } static ui_edit_pg_t ui_edit_view_last_fully_visible(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = ui_edit_scroll_pg(e); int32_t visible_runs = e->visible_runs; while (visible_runs > 0) { int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pg.pn, &runs); int32_t i = 0; pg.gp = 0; while (visible_runs > 0 && i < runs) { pg.gp += run[i].glyphs; visible_runs--; i++; } if (visible_runs > 0) { if (pg.pn < dt->np - 1) { pg.pn++; pg.gp = 0; } else { visible_runs = 0; // reached end of text } } } return pg; } // scroll_up() text moves up (north) in the visible view, // scroll position increments moves down (south) static void ui_edit_scroll_up(ui_edit_view_t* e, int32_t run_count) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 < run_count, "does it make sense to have 0 scroll?"); ui_edit_pg_t eot = ui_edit_view_end_of_text(e); while (run_count > 0) { ui_edit_pg_t lfv = ui_edit_view_last_fully_visible(e); rt_println("eot: %d:%d lfv: %d:%d", eot.pn, eot.gp, lfv.pn, lfv.gp); if (ui_edit_range.compare(lfv, eot) == 0) { run_count = 0; } else { const int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); if (e->scroll.rn < runs - 1) { e->scroll.rn++; run_count--; } else if (e->scroll.pn < dt->np - 1) { e->scroll.pn++; e->scroll.rn = 0; run_count--; } else { rt_println("???"); run_count = 0; // enough } rt_assert(e->scroll.pn >= 0 && e->scroll.rn >= 0); } } ui_edit_if_sle_layout(e); ui_edit_invalidate_view(e); } // scroll_dw() text moves down (south) in the visible view, // scroll position decrements moves up (north) static void ui_edit_scroll_down(ui_edit_view_t* e, int32_t run_count) { rt_assert(0 < run_count, "does it make sense to have 0 scroll?"); while (run_count > 0 && (e->scroll.pn > 0 || e->scroll.rn > 0)) { int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); e->scroll.rn = rt_min(e->scroll.rn, runs - 1); if (e->scroll.rn == 0 && e->scroll.pn > 0) { e->scroll.pn--; e->scroll.rn = ui_edit_paragraph_run_count(e, e->scroll.pn) - 1; } else if (e->scroll.rn > 0) { e->scroll.rn--; } rt_assert(e->scroll.pn >= 0 && e->scroll.rn >= 0); rt_assert(0 <= e->scroll.rn && e->scroll.rn < ui_edit_paragraph_run_count(e, e->scroll.pn)); run_count--; } ui_edit_if_sle_layout(e); ui_edit_invalidate_view(e); } static void ui_edit_scroll_into_view(ui_edit_view_t* e, const ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np && dt->np > 0); if (e->inside.bottom > 0) { if (e->sle) { rt_assert(pg.pn == 0); } const int32_t rn = ui_edit_pg_to_pr(e, pg).rn; const uint64_t scroll = (uint64_t)e->scroll.pn << 32 | e->scroll.rn; const uint64_t caret = (uint64_t)pg.pn << 32 | rn; uint64_t last = 0; int32_t py = 0; const int32_t pn = e->scroll.pn; const int32_t bottom = e->inside.bottom; for (int32_t i = pn; i < dt->np && py < bottom; i++) { int32_t runs = ui_edit_paragraph_run_count(e, i); const int32_t fvr = ui_edit_first_visible_run(e, i); for (int32_t j = fvr; j < runs && py < bottom; j++) { last = (uint64_t)i << 32 | j; py += ui_edit_line_height(e); } } int32_t sle_runs = e->sle && e->w > 0 ? ui_edit_paragraph_run_count(e, 0) : 0; ui_edit_pg_t end = ui_edit_text.end(dt); ui_edit_pr_t lp = ui_edit_pg_to_pr(e, end); uint64_t eof = (uint64_t)(dt->np - 1) << 32 | lp.rn; if (last == eof && py <= bottom - ui_edit_line_height(e)) { // vertical white space for EOF on the screen last = (uint64_t)dt->np << 32 | 0; } if (scroll <= caret && caret < last) { // no scroll } else if (caret < scroll) { ui_edit_invalidate_view(e); e->scroll.pn = pg.pn; e->scroll.rn = rn; } else if (e->sle && sle_runs * ui_edit_line_height(e) <= e->h) { // single line edit control fits vertically - no scroll } else { ui_edit_invalidate_view(e); rt_assert(caret >= last); e->scroll.pn = pg.pn; e->scroll.rn = rn; while (e->scroll.pn > 0 || e->scroll.rn > 0) { ui_point_t pt = ui_edit_pg_to_xy(e, pg); if (pt.y + ui_edit_line_height(e) > bottom - ui_edit_line_height(e)) { break; } if (e->scroll.rn > 0) { e->scroll.rn--; } else { e->scroll.pn--; e->scroll.rn = ui_edit_paragraph_run_count(e, e->scroll.pn) - 1; } } } } } static void ui_edit_caret_to(ui_edit_view_t* e, const ui_edit_pg_t to) { ui_edit_scroll_into_view(e, to); ui_point_t pt = ui_edit_pg_to_xy(e, to); if (pt.x >= 0 && pt.y >= 0) { ui_edit_set_caret(e, pt.x + e->inside.left, pt.y + e->inside.top); } } static void ui_edit_move_caret(ui_edit_view_t* e, const ui_edit_pg_t pg) { if (e->w > 0) { // width == 0 means no measure/layout yet ui_rect_t before = ui_edit_selection_rect(e); ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pg.pn && pg.pn < dt->np); // single line edit control cannot move caret past fist paragraph if (!e->sle || pg.pn < dt->np) { e->selection.a[1] = pg; ui_edit_caret_to(e, pg); if (!ui_app.shift && e->edit.buttons == 0) { e->selection.a[0] = e->selection.a[1]; } } ui_rect_t after = ui_edit_selection_rect(e); ui_edit_invalidate_rect(e, ui.combine_rect(&before, &after)); } } static ui_edit_pg_t ui_edit_insert_inline(ui_edit_view_t* e, ui_edit_pg_t pg, const char* text, int32_t bytes) { // insert_inline() inserts text (not containing '\n' in it) rt_assert(bytes > 0); for (int32_t i = 0; i < bytes; i++) { rt_assert(text[i] != '\n'); } ui_edit_range_t r = { .from = pg, .to = pg }; int32_t g = 0; if (ui_edit_doc.replace(e->doc, &r, text, bytes)) { ui_edit_text_t t = {0}; if (ui_edit_text.init(&t, text, bytes, false)) { rt_assert(t.ps != null && t.np == 1); g = t.np == 1 && t.ps != null ? t.ps[0].g : 0; ui_edit_text.dispose(&t); } } r.from.gp += g; r.to.gp += g; e->selection = r; ui_edit_move_caret(e, e->selection.from); return r.to; } static ui_edit_pg_t ui_edit_insert_paragraph_break(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_range_t r = { .from = pg, .to = pg }; bool ok = ui_edit_doc.replace(e->doc, &r, "\n", 1); ui_edit_pg_t next = {.pn = pg.pn + 1, .gp = 0}; return ok ? next : pg; } static bool ui_edit_is_blank(ui_edit_glyph_t g) { return g.bytes == 0 || ui_edit_str.is_blank(rt_str.utf32(g.s, g.bytes)); } static bool ui_edit_is_punctuation(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && ui_edit_str.is_punctuation(utf32); } static bool ui_edit_is_alphanumeric(ui_edit_glyph_t g) { return g.bytes > 0 && ui_edit_str.is_alphanumeric(rt_str.utf32(g.s, g.bytes)); } static bool ui_edit_is_cjk_or_emoji_or_symbol(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && (ui_edit_str.is_cjk_or_emoji(utf32) || ui_edit_str.is_symbol(utf32)); } static bool ui_edit_is_break(ui_edit_glyph_t g) { uint32_t utf32 = g.bytes > 0 ? rt_str.utf32(g.s, g.bytes) : 0; return utf32 != 0 && (ui_edit_str.is_blank(utf32) || ui_edit_str.is_punctuation(utf32) || ui_edit_str.is_symbol(utf32) || ui_edit_str.is_cjk_or_emoji(utf32)); } static ui_edit_glyph_t ui_edit_left_of(ui_edit_view_t* e, ui_edit_pg_t pg) { if (pg.gp > 0) { pg.gp--; return ui_edit_glyph_at(e, pg); } else { return (ui_edit_glyph_t){ null, 0 }; } } static ui_edit_glyph_t ui_edit_right_of(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text if (pg.gp < dt->ps[pg.pn].g - 1) { pg.gp++; return ui_edit_glyph_at(e, pg); } else { return (ui_edit_glyph_t){ null, 0 }; } } static ui_edit_pg_t ui_edit_skip_left_blanks(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_swear(pg.pn <= dt->np - 1); while (pg.gp > 0) { pg.gp--; ui_edit_glyph_t glyph = ui_edit_glyph_at(e, pg); if (glyph.bytes > 0 && !ui_edit_is_blank(glyph)) { pg.gp++; break; } } return pg; } static ui_edit_pg_t ui_edit_skip_right_blanks(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_text_t* dt = &e->doc->text; // document text rt_swear(pg.pn <= dt->np - 1); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, pg.pn); ui_edit_glyph_t glyph = ui_edit_glyph_at(e, pg); while (pg.gp < glyphs && glyph.bytes > 0 && ui_edit_is_blank(glyph)) { pg.gp++; glyph = ui_edit_glyph_at(e, pg); } return pg; } static ui_edit_range_t ui_edit_word_range(ui_edit_view_t* e, ui_edit_pg_t pg) { ui_edit_range_t r = { .from = pg, .to = pg }; ui_edit_text_t* dt = &e->doc->text; // document text if (0 <= pg.pn && 0 <= pg.gp) { rt_swear(pg.pn <= dt->np - 1); // number of glyphs in paragraph: int32_t ng = ui_edit_glyphs_in_paragraph(e, pg.pn); if (pg.gp > ng) { pg.gp = rt_max(0, ng); } ui_edit_glyph_t g = ui_edit_glyph_at(e, pg); if (ng <= 1) { r.to.gp = ng; } else if (ui_edit_is_cjk_or_emoji_or_symbol(g)) { // r == {pg,pg} } else { ui_edit_pg_t from = pg; ui_edit_pg_t to = pg; if (pg.gp > 0 && ui_edit_is_punctuation(g)) { from.gp--; g = ui_edit_glyph_at(e, from); } else if (pg.gp > 0 && ui_edit_is_blank(g)) { from.gp--; to.gp--; g = ui_edit_glyph_at(e, from); } if (ui_edit_is_blank(g)) { while (from.gp > 0 && ui_edit_is_blank(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_blank(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } else if (ui_edit_is_alphanumeric(g)) { while (from.gp > 0 && ui_edit_is_alphanumeric(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_alphanumeric(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } else { while (from.gp > 0 && ui_edit_is_break(ui_edit_left_of(e, from))) { from.gp--; } r.from = from; while (to.gp < ng && ui_edit_is_break(g)) { to.gp++; g = ui_edit_glyph_at(e, to); } r.to = to; } } } return r; } static void ui_edit_ctrl_left(ui_edit_view_t* e) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); const ui_edit_range_t s = e->selection; ui_edit_pg_t to = e->selection.to; if (to.gp == 0) { if (to.pn > 0) { to.pn--; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, to.pn, &runs); to.gp = run[runs - 1].gp + run[runs - 1].glyphs; } } else { to.gp--; } const ui_edit_pg_t lf = ui_edit_skip_left_blanks(e, to); const ui_edit_range_t w = ui_edit_word_range(e, lf); e->selection.to = w.from; if (ui_app.shift) { e->selection.from = s.from; } else { e->selection.from = w.from; } ui_edit_move_caret(e, e->selection.to); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } static void ui_edit_view_key_left(ui_edit_view_t* e) { ui_edit_pg_t to = e->selection.a[1]; if (to.pn > 0 || to.gp > 0) { if (ui_app.ctrl) { ui_edit_ctrl_left(e); } else { ui_point_t pt = ui_edit_pg_to_xy(e, to); if (pt.x == 0 && pt.y == 0) { ui_edit_scroll_down(e, 1); } if (to.gp > 0) { to.gp--; } else if (to.pn > 0) { to.pn--; to.gp = ui_edit_glyphs_in_paragraph(e, to.pn); } ui_edit_move_caret(e, to); e->last_x = -1; } } } static void ui_edit_ctrl_right(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_range_t s = e->selection; ui_edit_pg_t to = e->selection.to; int32_t glyphs = ui_edit_glyphs_in_paragraph(e, to.pn); if (to.pn < dt->np - 1 || to.gp < glyphs) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); if (to.gp == glyphs) { to.pn++; to.gp = 0; } else { to.gp++; } ui_edit_pg_t rt = ui_edit_skip_right_blanks(e, to); ui_edit_range_t w = ui_edit_word_range(e, rt); e->selection.to = w.to; if (ui_app.shift) { e->selection.from = s.from; } else { e->selection.from = w.to; } ui_edit_move_caret(e, e->selection.to); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } } static void ui_edit_view_key_right(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t to = e->selection.a[1]; if (to.pn < dt->np) { if (ui_app.ctrl) { ui_edit_ctrl_right(e); } else { int32_t glyphs = ui_edit_glyphs_in_paragraph(e, to.pn); if (to.gp < glyphs) { to.gp++; ui_edit_scroll_into_view(e, to); } else if (!e->sle && to.pn < dt->np - 1) { to.pn++; to.gp = 0; ui_edit_scroll_into_view(e, to); } ui_edit_move_caret(e, to); // TODO: last_x does not work! e->last_x = -1; } } } static void ui_edit_reuse_last_x(ui_edit_view_t* e, ui_point_t* pt) { // Vertical caret movement visually tend to move caret horizontally // in proportional font text. Remembering starting `x' value for vertical // movements alleviates this unpleasant UX experience to some degree. if (pt->x > 0) { if (e->last_x > 0) { int32_t prev = e->last_x - e->fm->em.w; int32_t next = e->last_x + e->fm->em.w; if (prev <= pt->x && pt->x <= next) { pt->x = e->last_x; } } e->last_x = pt->x; } } static void ui_edit_view_key_up(ui_edit_view_t* e) { const ui_edit_pg_t pg = e->selection.a[1]; ui_edit_pg_t to = pg; if (to.pn > 0 || ui_edit_pg_to_pr(e, to).rn > 0) { // top of the text ui_point_t pt = ui_edit_pg_to_xy(e, to); rt_assert(pt.x >= 0 && pt.y >= 0); if (pt.y == 0) { ui_edit_scroll_down(e, 1); } else { pt.y -= 1; } ui_edit_reuse_last_x(e, &pt); rt_assert(pt.y >= 0); to = ui_edit_xy_to_pg(e, pt.x, pt.y); if (to.pn >= 0 && to.gp >= 0) { int32_t rn0 = ui_edit_pg_to_pr(e, pg).rn; int32_t rn1 = ui_edit_pg_to_pr(e, to).rn; if (rn1 > 0 && rn0 == rn1) { // same run rt_assert(to.gp > 0, "word break must not break on zero gp"); int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, to.pn, &runs); to.gp = run[rn1].gp; } } } if (to.pn >= 0 && to.gp >= 0) { ui_edit_move_caret(e, to); } } static void ui_edit_view_key_down(ui_edit_view_t* e) { const ui_edit_pg_t pg = e->selection.a[1]; ui_point_t pt = ui_edit_pg_to_xy(e, pg); ui_edit_reuse_last_x(e, &pt); // TODO: does not work! (used to work broken now) // scroll runs guaranteed to be already laid out for current state of view: ui_edit_pg_t scroll = ui_edit_scroll_pg(e); const int32_t run_count = ui_edit_runs_between(e, scroll, pg); if (!e->sle && run_count > e->visible_runs - 1) { ui_edit_scroll_up(e, 1); } else { pt.y += ui_edit_line_height(e); } ui_edit_pg_t to = ui_edit_xy_to_pg(e, pt.x, pt.y); if (to.pn >= 0 && to.gp >= 0) { ui_edit_move_caret(e, to); } } static void ui_edit_view_key_home(ui_edit_view_t* e) { if (ui_app.ctrl) { e->scroll.pn = 0; e->scroll.rn = 0; e->selection.a[1].pn = 0; e->selection.a[1].gp = 0; ui_edit_invalidate_view(e); } const int32_t pn = e->selection.a[1].pn; int32_t runs = ui_edit_paragraph_run_count(e, pn); const ui_edit_paragraph_t* para = &e->para[pn]; if (runs <= 1) { e->selection.a[1].gp = 0; } else { int32_t rn = ui_edit_pg_to_pr(e, e->selection.a[1]).rn; rt_assert(0 <= rn && rn < runs); const int32_t gp = para->run[rn].gp; if (e->selection.a[1].gp != gp) { // first Home keystroke moves caret to start of run e->selection.a[1].gp = gp; } else { // second Home keystroke moves caret start of paragraph e->selection.a[1].gp = 0; if (e->scroll.pn >= e->selection.a[1].pn) { // scroll in e->scroll.pn = e->selection.a[1].pn; e->scroll.rn = 0; ui_edit_invalidate_view(e); } } } if (!ui_app.shift) { e->selection.a[0] = e->selection.a[1]; } ui_edit_move_caret(e, e->selection.a[1]); } static void ui_edit_view_key_eol(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text int32_t pn = e->selection.a[1].pn; int32_t gp = e->selection.a[1].gp; rt_assert(0 <= pn && pn < dt->np); const ui_edit_str_t* str = &dt->ps[pn]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pn, &runs); int32_t rn = ui_edit_pg_to_pr(e, e->selection.a[1]).rn; rt_assert(0 <= rn && rn < runs); if (rn == runs - 1) { e->selection.a[1].gp = str->g; } else if (e->selection.a[1].gp == str->g) { // at the end of paragraph do nothing (or move caret to EOF?) } else if (str->g > 0 && gp != run[rn].glyphs - 1) { e->selection.a[1].gp = run[rn].gp + run[rn].glyphs - 1; } else { e->selection.a[1].gp = str->g; } } static void ui_edit_view_key_end(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text if (ui_app.ctrl) { int32_t py = e->inside.bottom; for (int32_t i = dt->np - 1; i >= 0 && py >= ui_edit_line_height(e); i--) { int32_t runs = ui_edit_paragraph_run_count(e, i); for (int32_t j = runs - 1; j >= 0 && py >= ui_edit_line_height(e); j--) { py -= ui_edit_line_height(e); if (py < ui_edit_line_height(e)) { e->scroll.pn = i; e->scroll.rn = j; } } } e->selection.a[1] = ui_edit_text.end(dt); ui_edit_invalidate_view(e); } else { ui_edit_view_key_eol(e); } if (!ui_app.shift) { e->selection.a[0] = e->selection.a[1]; } ui_edit_move_caret(e, e->selection.a[1]); } static void ui_edit_view_key_page_up(ui_edit_view_t* e) { int32_t n = rt_max(1, e->visible_runs - 1); ui_edit_pg_t scr = ui_edit_scroll_pg(e); const ui_edit_pg_t prev = (ui_edit_pg_t){ .pn = rt_max(scr.pn - e->visible_runs - 1, 0), .gp = 0 }; const int32_t m = ui_edit_runs_between(e, prev, scr); if (m > n) { ui_point_t pt = ui_edit_pg_to_xy(e, e->selection.a[1]); ui_edit_pr_t scroll = e->scroll; ui_edit_scroll_down(e, n); if (scroll.pn != e->scroll.pn || scroll.rn != e->scroll.rn) { ui_edit_pg_t pg = ui_edit_xy_to_pg(e, pt.x, pt.y); ui_edit_move_caret(e, pg); } } else { const ui_edit_pg_t bof = {.pn = 0, .gp = 0}; ui_edit_move_caret(e, bof); } } static void ui_edit_view_key_page_down(ui_edit_view_t* e) { const ui_edit_text_t* dt = &e->doc->text; // document text const int32_t n = rt_max(1, e->visible_runs - 1); const ui_edit_pg_t scr = ui_edit_scroll_pg(e); const ui_edit_pg_t next = (ui_edit_pg_t){ .pn = rt_min(scr.pn + 1, dt->np - 1), .gp = scr.pn + 1 == dt->np - 1 ? dt->ps[dt->np - 1].g : 0 }; const int32_t m = ui_edit_runs_between(e, scr, next); if (m > n) { const ui_point_t pt = ui_edit_pg_to_xy(e, e->selection.a[1]); const ui_edit_pr_t scroll = e->scroll; ui_edit_scroll_up(e, n); if (scroll.pn != e->scroll.pn || scroll.rn != e->scroll.rn) { ui_edit_pg_t pg = ui_edit_xy_to_pg(e, pt.x, pt.y); ui_edit_move_caret(e, pg); } } else { const ui_edit_pg_t end = ui_edit_text.end(dt); ui_edit_move_caret(e, end); } } static void ui_edit_view_key_delete(ui_edit_view_t* e) { ui_edit_text_t* dt = &e->doc->text; // document text uint64_t f = ui_edit_range.uint64(e->selection.a[0]); uint64_t t = ui_edit_range.uint64(e->selection.a[1]); uint64_t end = ui_edit_range.uint64(ui_edit_text.end(dt)); if (f == t && t != end) { ui_edit_pg_t s1 = e->selection.a[1]; ui_edit_view.key_right(e); e->selection.a[1] = s1; } ui_edit_view.erase(e); } static void ui_edit_view_key_backspace(ui_edit_view_t* e) { uint64_t f = ui_edit_range.uint64(e->selection.a[0]); uint64_t t = ui_edit_range.uint64(e->selection.a[1]); if (t != 0 && f == t) { ui_edit_pg_t s1 = e->selection.a[1]; ui_edit_view.key_left(e); e->selection.a[1] = s1; } ui_edit_view.erase(e); } static void ui_edit_view_key_enter(ui_edit_view_t* e) { rt_assert(!e->ro); if (!e->sle) { ui_edit_view.erase(e); e->selection.a[1] = ui_edit_insert_paragraph_break(e, e->selection.a[1]); e->selection.a[0] = e->selection.a[1]; ui_edit_move_caret(e, e->selection.a[1]); } else { // single line edit callback if (ui_edit_view.enter != null) { ui_edit_view.enter(e); } } } static bool ui_edit_view_key_pressed(ui_view_t* v, int64_t key) { bool swallow = false; rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_text_t* dt = &e->doc->text; // document text if (e->focused) { swallow = true; if (key == ui.key.down && e->selection.a[1].pn < dt->np) { ui_edit_view.key_down(e); } else if (key == ui.key.up && dt->np > 1) { ui_edit_view.key_up(e); } else if (key == ui.key.left) { ui_edit_view.key_left(e); } else if (key == ui.key.right) { ui_edit_view.key_right(e); } else if (key == ui.key.page_up) { ui_edit_view.key_page_up(e); } else if (key == ui.key.page_down) { ui_edit_view.key_page_down(e); } else if (key == ui.key.home) { ui_edit_view.key_home(e); } else if (key == ui.key.end) { ui_edit_view.key_end(e); } else if (key == ui.key.del && !e->ro) { ui_edit_view.key_delete(e); } else if (key == ui.key.back && !e->ro) { ui_edit_view.key_backspace(e); } else if (key == ui.key.enter && !e->ro) { ui_edit_view.key_enter(e); } else { swallow = false; // ignore other keys } } return swallow; } static void ui_edit_undo(ui_edit_view_t* e) { if (e->doc->undo != null) { ui_edit_doc.undo(e->doc); } else { ui_app.beep(ui.beep.error); } } static void ui_edit_redo(ui_edit_view_t* e) { if (e->doc->redo != null) { ui_edit_doc.redo(e->doc); } else { ui_app.beep(ui.beep.error); } } static void ui_edit_character(ui_view_t* v, const char* utf8) { rt_assert(v->type == ui_view_text); rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); #pragma push_macro("ui_edit_ctrl") #define ui_edit_ctrl(c) ((char)((c) - 'a' + 1)) ui_edit_view_t* e = (ui_edit_view_t*)v; if (e->focused) { char ch = utf8[0]; if (ui_app.ctrl) { if (ch == ui_edit_ctrl('a')) { ui_edit_view.select_all(e); } if (ch == ui_edit_ctrl('c')) { ui_edit_view.copy(e); } if (!e->ro) { if (ch == ui_edit_ctrl('x')) { ui_edit_view.cut(e); } if (ch == ui_edit_ctrl('v')) { ui_edit_view.paste(e); } if (ch == ui_edit_ctrl('y')) { ui_edit_redo(e); } if (ch == ui_edit_ctrl('z') || ch == ui_edit_ctrl('Z')) { if (ui_app.shift) { // Ctrl+Shift+Z ui_edit_redo(e); } else { // Ctrl+Z ui_edit_undo(e); } } } } if (0x20u <= (uint8_t)ch && !e->ro) { // 0x20 space int32_t len = (int32_t)strlen(utf8); int32_t bytes = rt_str.utf8bytes(utf8, len); if (bytes > 0) { ui_edit_view.erase(e); // remove selected text to be replaced by glyph e->selection.a[1] = ui_edit_insert_inline(e, e->selection.a[1], utf8, bytes); e->selection.a[0] = e->selection.a[1]; ui_edit_move_caret(e, e->selection.a[1]); } else { rt_println("invalid UTF8: 0x%02X%02X%02X%02X", utf8[0], utf8[1], utf8[2], utf8[3]); } } } #pragma pop_macro("ui_edit_ctrl") } static void ui_edit_select_word(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (0 <= pg.pn && 0 <= pg.gp) { ui_edit_range_t r = ui_edit_word_range(e, pg); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, r.to.pn); if (r.to.pn == r.from.pn && r.to.gp == r.from.gp && r.to.gp < glyphs) { r.to.gp++; // at least one glyph to the right } if (ui_edit_range.compare(r.from, pg) != 0 || ui_edit_range.compare(r.to, pg) != 0) { e->selection = r; ui_edit_caret_to(e, r.to); // rt_println("e->selection.a[1] = %d.%d", to.pn, to.gp); ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); e->edit.buttons = 0; } } } static void ui_edit_select_paragraph(ui_edit_view_t* e, int32_t x, int32_t y) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t p = ui_edit_xy_to_pg(e, x, y); if (0 <= p.pn && 0 <= p.gp) { ui_edit_range_t r = ui_edit_text.ordered(dt, &e->selection); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, p.pn); if (p.gp > glyphs) { p.gp = rt_max(0, glyphs); } if (p.pn == r.a[0].pn && r.a[0].pn == r.a[1].pn && r.a[0].gp <= p.gp && p.gp <= r.a[1].gp) { r.a[0].gp = 0; if (p.pn < dt->np - 1) { r.a[1].pn = p.pn + 1; r.a[1].gp = 0; } else { r.a[1].gp = dt->ps[p.pn].g; } e->selection = r; ui_edit_caret_to(e, r.to); } ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); e->edit.buttons = 0; } } static void ui_edit_click(ui_edit_view_t* e, int32_t x, int32_t y) { // x, y in 0..e->w, 0->e.h coordinate space rt_assert(0 <= x && x < e->w && 0 <= y && y < e->h); ui_edit_text_t* dt = &e->doc->text; // document text ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (0 <= pg.pn && 0 <= pg.gp && ui_view.has_focus(&e->view)) { rt_swear(dt->np > 0 && pg.pn < dt->np); int32_t glyphs = ui_edit_glyphs_in_paragraph(e, pg.pn); if (pg.gp > glyphs) { pg.gp = rt_max(0, glyphs); } ui_edit_move_caret(e, pg); } } static void ui_edit_mouse_button_down(ui_edit_view_t* e, int32_t ix) { e->edit.buttons |= (1 << ix); } static void ui_edit_mouse_button_up(ui_edit_view_t* e, int32_t ix) { e->edit.buttons &= ~(1 << ix); } static bool ui_edit_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { // `ix` ignored for now till context menu (copy/paste/select...) ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); // not just inside view but inside insets: bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside) { if (pressed) { e->edit.buttons = 0; ui_edit_click(e, x, y); ui_edit_mouse_button_down(e, ix); } else if (!pressed) { ui_edit_mouse_button_up(e, ix); } } if (!pressed) { ui_edit_mouse_button_up(e, ix); } return true; } static bool ui_edit_long_press(ui_view_t* v, int32_t rt_unused(ix)) { ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside && ui_edit_range.is_empty(e->selection)) { ui_edit_select_paragraph(e, x, y); } return true; } static bool ui_edit_double_tap(ui_view_t* v, int32_t rt_unused(ix)) { ui_edit_view_t* e = (ui_edit_view_t*)v; const int32_t x = ui_app.mouse.x - (v->x + e->inside.left); const int32_t y = ui_app.mouse.y - (v->y + e->inside.top); bool inside = 0 <= x && x < e->w && 0 <= y && y < e->h; if (inside && e->selection.a[0].pn == e->selection.a[1].pn) { ui_edit_select_word(e, x, y); } return false; } static void ui_edit_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { if (v->w > 0 && v->h > 0) { const int32_t dy = dx_dy.y; // TODO: maybe make a use of dx in single line no-word-break edit control? if (ui_app.focus == v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; int32_t lines = (abs(dy) + ui_edit_line_height(e) - 1) / ui_edit_line_height(e); if (dy > 0) { ui_edit_scroll_down(e, lines); } else if (dy < 0) { ui_edit_scroll_up(e, lines); } // TODO: Ctrl UP/DW and caret of out of visible area scrolls are not // implemented. Not sure they are very good UX experience. // MacOS users may be used to scroll with touchpad, take a visual // peek, do NOT click and continue editing at last cursor position. // To me back forward stack navigation is much more intuitive and // much mode "modeless" in spirit of cut/copy/paste. But opinions // and editing habits vary. Easy to implement. const int32_t x = e->caret.x - e->inside.left; const int32_t y = e->caret.y - e->inside.top; ui_edit_pg_t pg = ui_edit_xy_to_pg(e, x, y); if (pg.pn >= 0 && pg.gp >= 0) { rt_assert(pg.gp <= e->doc->text.ps[pg.pn].g); ui_edit_move_caret(e, pg); } else { ui_edit_click(e, x, y); } } } } static bool ui_edit_focus_gained(ui_view_t* v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; rt_assert(v->focusable); if (ui_app.focused() && !e->focused) { ui_edit_create_caret(e); ui_edit_show_caret(e); ui_edit_if_sle_layout(e); } e->edit.buttons = 0; ui_app.request_redraw(); return true; } static void ui_edit_focus_lost(ui_view_t* v) { rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; if (e->focused) { ui_edit_hide_caret(e); ui_edit_destroy_caret(e); ui_edit_if_sle_layout(e); } e->edit.buttons = 0; ui_app.request_redraw(); } static void ui_edit_view_erase(ui_edit_view_t* e) { if (e->selection.from.pn != e->selection.to.pn) { ui_edit_invalidate_view(e); } else { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } ui_edit_range_t r = ui_edit_range.order(e->selection); if (!ui_edit_range.is_empty(r) && ui_edit_doc.replace(e->doc, &r, null, 0)) { e->selection = r; e->selection.to = e->selection.from; ui_edit_move_caret(e, e->selection.from); } } static void ui_edit_select_all(ui_edit_view_t* e) { e->selection = ui_edit_text.all_on_null(&e->doc->text, null); ui_edit_invalidate_view(e); } static int32_t ui_edit_view_save(ui_edit_view_t* e, char* text, int32_t* bytes) { rt_not_null(bytes); enum { error_insufficient_buffer = 122, // ERROR_INSUFFICIENT_BUFFER error_more_data = 234 // ERROR_MORE_DATA }; int32_t r = 0; const int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, null); if (text == null) { *bytes = utf8bytes; r = rt_core.error.more_data; } else if (*bytes < utf8bytes) { r = rt_core.error.insufficient_buffer; } else { ui_edit_doc.copy(e->doc, null, text, utf8bytes); rt_assert(text[utf8bytes - 1] == 0x00); } return r; } static void ui_edit_view_copy(ui_edit_view_t* e) { int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, &e->selection); if (utf8bytes > 0) { char* text = null; bool ok = rt_heap.alloc((void**)&text, utf8bytes) == 0; rt_swear(ok); ui_edit_doc.copy(e->doc, &e->selection, text, utf8bytes); rt_assert(text[utf8bytes - 1] == 0x00); // verify zero termination rt_clipboard.put_text(text); rt_heap.free(text); static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); int32_t x = e->x + e->caret.x; int32_t y = e->y + e->caret.y - ui_edit_line_height(e); if (y < ui_app.content->y) { y += ui_edit_line_height(e) * 2; } if (y > ui_app.content->y + ui_app.content->h - ui_edit_line_height(e)) { y = e->caret.y; } ui_app.show_hint(&hint, x, y, 0.5); } } static void ui_edit_view_cut(ui_edit_view_t* e) { int32_t utf8bytes = ui_edit_doc.utf8bytes(e->doc, &e->selection); if (utf8bytes > 0) { ui_edit_view_copy(e); } if (!e->ro) { ui_edit_view.erase(e); } } static ui_edit_pg_t ui_edit_paste_text(ui_edit_view_t* e, const char* text, int32_t bytes) { rt_assert(!e->ro); ui_edit_text_t t = {0}; ui_edit_text.init(&t, text, bytes, false); ui_edit_range_t r = ui_edit_text.all_on_null(&t, null); ui_edit_doc.replace(e->doc, &e->selection, text, bytes); ui_edit_pg_t pg = e->selection.from; pg.pn += r.to.pn; if (e->selection.from.pn == e->selection.to.pn && r.to.pn == 0) { pg.gp = e->selection.from.gp + r.to.gp; } else { pg.gp = r.to.gp; } ui_edit_text.dispose(&t); return pg; } static void ui_edit_view_replace(ui_edit_view_t* e, const char* s, int32_t n) { if (!e->ro) { if (n < 0) { n = (int32_t)strlen(s); } ui_edit_view.erase(e); e->selection.a[1] = ui_edit_paste_text(e, s, n); e->selection.a[0] = e->selection.a[1]; if (e->w > 0) { ui_edit_move_caret(e, e->selection.a[1]); } } } static void ui_edit_view_paste(ui_edit_view_t* e) { if (!e->ro) { ui_edit_pg_t pg = e->selection.a[1]; int32_t bytes = 0; rt_clipboard.get_text(null, &bytes); if (bytes > 0) { char* text = null; bool ok = rt_heap.alloc((void**)&text, bytes) == 0; rt_swear(ok); int32_t r = rt_clipboard.get_text(text, &bytes); rt_fatal_if_error(r); if (bytes > 0 && text[bytes - 1] == 0) { bytes--; // clipboard includes zero terminator } if (bytes > 0) { ui_edit_view.erase(e); pg = ui_edit_paste_text(e, text, bytes); ui_edit_move_caret(e, pg); } rt_heap.free(text); } } } static void ui_edit_prepare_sle(ui_edit_view_t* e) { ui_view_t* v = &e->view; rt_swear(e->sle && v->w > 0); // shingle line edit is capable of resizing itself to two // lines of text (and shrinking back) to avoid horizontal scroll int32_t runs = rt_max(1, rt_min(2, ui_edit_paragraph_run_count(e, 0))); const ui_ltrb_t insets = ui_view.margins(v, &v->insets); int32_t h = insets.top + ui_edit_line_height(e) * runs + insets.bottom; fp32_t min_h_em = (fp32_t)h / v->fm->em.h; if (v->min_h_em != min_h_em) { v->min_h_em = min_h_em; } } static void ui_edit_insets(ui_edit_view_t* e) { ui_view_t* v = &e->view; const ui_ltrb_t insets = ui_view.margins(v, &v->insets); e->inside = (ui_ltrb_t){ .left = insets.left, .top = insets.top, .right = v->w - insets.right, .bottom = v->h - insets.bottom }; const int32_t width = e->edit.w; // previous width e->edit.w = e->inside.right - e->inside.left; e->edit.h = e->inside.bottom - e->inside.top; if (e->edit.w != width) { ui_edit_invalidate_all_runs(e); } } static void ui_edit_measure(ui_view_t* v) { // bottom up rt_assert(v->type == ui_view_text); ui_edit_view_t* e = (ui_edit_view_t*)v; if (v->w > 0 && e->sle) { ui_edit_prepare_sle(e); } v->w = (int32_t)((fp64_t)v->fm->em.w * (fp64_t)v->min_w_em + 0.5); v->h = (int32_t)((fp64_t)v->fm->em.h * (fp64_t)v->min_h_em + 0.5); const ui_ltrb_t i = ui_view.margins(v, &v->insets); // enforce minimum size - it makes it checking corner cases much simpler // and it's hard to edit anything in a smaller area - will result in bad UX if (v->w < v->fm->em.w * 4) { v->w = i.left + v->fm->em.w * 4 + i.right; } if (v->h < ui_edit_line_height(e)) { v->h = i.top + ui_edit_line_height(e) + i.bottom; } } static void ui_edit_layout(ui_view_t* v) { // top down rt_assert(v->type == ui_view_text); rt_assert(v->w > 0 && v->h > 0); // could be `if' ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_insets(e); // fully visible runs e->visible_runs = e->h / ui_edit_line_height(e); ui_edit_invalidate_run(e, e->scroll.pn); // number of runs in e->scroll.pn may have changed with e->w change int32_t runs = ui_edit_paragraph_run_count(e, e->scroll.pn); // glyph position in scroll_pn paragraph: const ui_edit_pg_t scroll = v->w == 0 ? (ui_edit_pg_t){0, 0} : ui_edit_scroll_pg(e); e->scroll.rn = ui_edit_pg_to_pr(e, scroll).rn; rt_assert(0 <= e->scroll.rn && e->scroll.rn < runs); (void)runs; if (e->sle) { // single line edit (if changed on the fly): e->selection.a[0].pn = 0; // only has single paragraph e->selection.a[1].pn = 0; // scroll line on top of current cursor position into view const ui_edit_run_t* run = ui_edit_paragraph_runs(e, 0, &runs); if (runs <= 2 && e->scroll.rn == 1) { ui_edit_pg_t top = scroll; top.gp = rt_max(0, top.gp - run[e->scroll.rn].glyphs - 1); ui_edit_scroll_into_view(e, top); } } ui_edit_scroll_into_view(e, e->selection.a[1]); ui_edit_caret_to(e, e->selection.a[1]); if (e->focused) { // recreate caret because fm->height may have changed ui_edit_hide_caret(e); ui_edit_destroy_caret(e); ui_edit_create_caret(e); ui_edit_show_caret(e); rt_assert(e->focused); } } static void ui_edit_paint_selection(ui_edit_view_t* e, int32_t y, const ui_edit_run_t* r, const char* text, int32_t pn, int32_t c0, int32_t c1) { uint64_t s0 = ui_edit_range.uint64(e->selection.a[0]); uint64_t e0 = ui_edit_range.uint64(e->selection.a[1]); if (s0 > e0) { uint64_t swap = e0; e0 = s0; s0 = swap; } const ui_edit_pg_t pnc0 = {.pn = pn, .gp = c0}; const ui_edit_pg_t pnc1 = {.pn = pn, .gp = c1}; uint64_t s1 = ui_edit_range.uint64(pnc0); uint64_t e1 = ui_edit_range.uint64(pnc1); if (s0 <= e1 && s1 <= e0) { uint64_t start = rt_max(s0, s1) - (uint64_t)c0; uint64_t end = rt_min(e0, e1) - (uint64_t)c0; if (start < end) { int32_t fro = (int32_t)start; int32_t to = (int32_t)end; int32_t ofs0 = ui_edit_str.gp_to_bp(text, r->bytes, fro); int32_t ofs1 = ui_edit_str.gp_to_bp(text, r->bytes, to); rt_swear(ofs0 >= 0 && ofs1 >= 0); int32_t x0 = ui_edit_text_width(e, text, ofs0); int32_t x1 = ui_edit_text_width(e, text, ofs1); // selection color is MSVC dark mode selection color // TODO: need light mode selection color tpp ui_color_t sc = ui_color_rgb(0x26, 0x4F, 0x78); // selection color if (!e->focused || !ui_app.focused()) { sc = ui_colors.darken(sc, 0.1f); } const ui_ltrb_t insets = ui_view.margins(&e->view, &e->insets); int32_t x = e->x + insets.left; // event if background is transparent ui_gdi.fill(x + x0, y, x1 - x0, ui_edit_line_height(e), sc); } } } static int32_t ui_edit_paint_paragraph(ui_edit_view_t* e, const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t pn, ui_rect_t rc) { static const char* ww = rt_glyph_south_west_arrow_with_hook; ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(0 <= pn && pn < dt->np); const ui_edit_str_t* str = &dt->ps[pn]; int32_t runs = 0; const ui_edit_run_t* run = ui_edit_paragraph_runs(e, pn, &runs); for (int32_t j = ui_edit_first_visible_run(e, pn); j < runs && y < e->y + e->inside.bottom; j++) { // rt_println("[%d.%d] @%d,%d bytes: %d", pn, j, x, y, run[j].bytes); if (rc.y - ui_edit_line_height(e) <= y && y < rc.y + rc.h) { const char* text = str->u + run[j].bp; ui_edit_paint_selection(e, y, &run[j], text, pn, run[j].gp, run[j].gp + run[j].glyphs); ui_gdi.text(ta, x, y, "%.*s", run[j].bytes, text); if (j < runs - 1 && !e->hide_word_wrap) { ui_gdi.text(ta, x + e->edit.w, y, "%s", ww); } } y += ui_edit_line_height(e); } return y; } static void ui_edit_paint(ui_view_t* v) { rt_assert(v->type == ui_view_text); rt_assert(!ui_view.is_hidden(v)); ui_edit_view_t* e = (ui_edit_view_t*)v; ui_edit_text_t* dt = &e->doc->text; // document text // drawing text is really expensive, only paint what's needed: ui_rect_t vrc = (ui_rect_t){v->x, v->y, v->w, v->h}; ui_rect_t rc; if (ui.intersect_rect(&rc, &vrc, &ui_app.prc)) { // because last line of the view may extend over the bottom ui_gdi.set_clip(v->x, v->y, v->w, v->h); ui_color_t b = v->background; if (!ui_color_is_undefined(b) && !ui_color_is_transparent(b)) { ui_gdi.fill(rc.x, rc.y, rc.w, rc.h, b); } const ui_ltrb_t insets = ui_view.margins(v, &v->insets); int32_t x = v->x + insets.left; int32_t y = v->y + insets.top; const ui_gdi_ta_t ta = { .fm = v->fm, .color = v->color }; const int32_t pn = e->scroll.pn; const int32_t bottom = v->y + e->inside.bottom; rt_assert(pn < dt->np); for (int32_t i = pn; i < dt->np && y < bottom; i++) { y = ui_edit_paint_paragraph(e, &ta, x, y, i, rc); } ui_gdi.set_clip(0, 0, 0, 0); } } static void ui_edit_view_move(ui_edit_view_t* e, ui_edit_pg_t pg) { if (e->w > 0) { ui_edit_move_caret(e, pg); // may select text on move } else { e->selection.a[1] = pg; } e->selection.a[0] = e->selection.a[1]; } static bool ui_edit_reallocate_runs(ui_edit_view_t* e, int32_t p, int32_t np) { // This function is called in after() callback when // d->text.np already changed to `new_np`. // It has to manipulate e->para[] array w/o calling // ui_edit_invalidate_runs() ui_edit_dispose_all_runs() // because they assume that e->para[] array is in sync // d->text.np. ui_edit_text_t* dt = &e->doc->text; // document text bool ok = true; int32_t old_np = np; // old (before) number of paragraphs int32_t new_np = dt->np; // new (after) number of paragraphs rt_assert(old_np > 0 && new_np > 0 && e->para != null); rt_assert(0 <= p && p < old_np); if (old_np == new_np) { ui_edit_invalidate_run(e, p); } else if (new_np < old_np) { // shrinking - delete runs const int32_t d = old_np - new_np; // `d` delta > 0 if (p + d < old_np - 1) { const int32_t n = rt_max(0, old_np - p - d - 1); memcpy(e->para + p + 1, e->para + p + 1 + d, n * sizeof(e->para[0])); } if (p < new_np) { ui_edit_invalidate_run(e, p); } ok = rt_heap.realloc((void**)&e->para, new_np * sizeof(e->para[0])) == 0; rt_swear(ok, "shrinking"); } else { // growing - insert runs ui_edit_invalidate_run(e, p); int32_t d = new_np - old_np; // `d` delta > 0 ok = rt_heap.realloc_zero((void**)&e->para, new_np * sizeof(e->para[0])) == 0; if (ok) { const int32_t n = rt_max(0, new_np - p - d - 1); memmove(e->para + p + 1 + d, e->para + p + 1, (size_t)n * sizeof(e->para[0])); const int32_t m = rt_min(new_np, p + 1 + d); for (int32_t i = p + 1; i < m; i++) { e->para[i].run = null; e->para[i].runs = 0; } } } return ok; } static void ui_edit_before(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni) { ui_edit_notify_view_t* n = (ui_edit_notify_view_t*)notify; ui_edit_view_t* e = (ui_edit_view_t*)n->that; rt_swear(e->doc == ni->d); if (e->w > 0 && e->h > 0) { const ui_edit_text_t* dt = &e->doc->text; // document text rt_assert(dt->np > 0); // `n->data` is number of paragraphs before replace(): n->data = (uintptr_t)dt->np; if (e->selection.from.pn != e->selection.to.pn) { ui_edit_invalidate_view(e); } else { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } } } static void ui_edit_after(ui_edit_notify_t* notify, const ui_edit_notify_info_t* ni) { ui_edit_notify_view_t* n = (ui_edit_notify_view_t*)notify; ui_edit_view_t* e = (ui_edit_view_t*)n->that; const ui_edit_text_t* dt = &ni->d->text; // document text rt_assert(ni->d == e->doc && dt->np > 0); if (e->w > 0 && e->h > 0) { // number of paragraphs before replace(): const int32_t np = (int32_t)n->data; rt_swear(dt->np == np - ni->deleted + ni->inserted); ui_edit_reallocate_runs(e, ni->r->from.pn, np); e->selection = *ni->x; // this is needed by undo/redo: trim selection ui_edit_pg_t* pg = e->selection.a; for (int32_t i = 0; i < rt_countof(e->selection.a); i++) { pg[i].pn = rt_max(0, rt_min(dt->np - 1, pg[i].pn)); pg[i].gp = rt_max(0, rt_min(dt->ps[pg[i].pn].g, pg[i].gp)); } if (ni->r->from.pn != ni->r->to.pn && ni->x->from.pn != ni->x->to.pn && ni->r->from.pn == ni->x->from.pn) { ui_edit_invalidate_rect(e, ui_edit_selection_rect(e)); } else { ui_edit_invalidate_view(e); } ui_edit_scroll_into_view(e, e->selection.to); } } static void ui_edit_view_init(ui_edit_view_t* e, ui_edit_doc_t* d) { memset(e, 0, sizeof(*e)); rt_assert(d != null && d->text.np > 0); e->doc = d; rt_assert(d->text.np > 0); e->listener.that = (void*)e; e->listener.data = 0; e->listener.notify.before = ui_edit_before; e->listener.notify.after = ui_edit_after; rt_static_assertion(offsetof(ui_edit_notify_view_t, notify) == 0); ui_edit_doc.subscribe(d, &e->listener.notify); e->color_id = ui_color_id_window_text; e->background_id = ui_color_id_window; e->fm = &ui_app.fm.prop.normal; e->insets = (ui_margins_t){ 0.25, 0.25, 0.50, 0.25 }; e->padding = (ui_margins_t){ 0.25, 0.25, 0.25, 0.25 }; e->min_w_em = 1.0; e->min_h_em = 1.0; e->type = ui_view_text; e->focusable = true; e->last_x = -1; e->focused = false; e->sle = false; e->ro = false; e->caret = (ui_point_t){-1, -1}; e->paint = ui_edit_paint; e->measure = ui_edit_measure; e->layout = ui_edit_layout; e->tap = ui_edit_tap; e->long_press = ui_edit_long_press; e->double_tap = ui_edit_double_tap; e->character = ui_edit_character; e->focus_gained = ui_edit_focus_gained; e->focus_lost = ui_edit_focus_lost; e->key_pressed = ui_edit_view_key_pressed; e->mouse_scroll = ui_edit_mouse_scroll; ui_edit_allocate_runs(e); if (e->debug.id == null) { e->debug.id = "#edit"; } } static void ui_edit_view_dispose(ui_edit_view_t* e) { ui_edit_doc.unsubscribe(e->doc, &e->listener.notify); ui_edit_dispose_all_runs(e); memset(e, 0, sizeof(*e)); } ui_edit_view_if ui_edit_view = { .init = ui_edit_view_init, .set_font = ui_edit_view_set_font, .move = ui_edit_view_move, .replace = ui_edit_view_replace, .save = ui_edit_view_save, .erase = ui_edit_view_erase, .cut = ui_edit_view_cut, .copy = ui_edit_view_copy, .paste = ui_edit_view_paste, .select_all = ui_edit_select_all, .key_down = ui_edit_view_key_down, .key_up = ui_edit_view_key_up, .key_left = ui_edit_view_key_left, .key_right = ui_edit_view_key_right, .key_page_up = ui_edit_view_key_page_up, .key_page_down = ui_edit_view_key_page_down, .key_home = ui_edit_view_key_home, .key_end = ui_edit_view_key_end, .key_delete = ui_edit_view_key_delete, .key_backspace = ui_edit_view_key_backspace, .key_enter = ui_edit_view_key_enter, .fuzz = null, .dispose = ui_edit_view_dispose }; ================================================ FILE: src/ui/ui_fuzzing.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" // TODO: Ctrl+A Ctrl+V Ctrl+C Ctrl+X Ctrl+Z Ctrl+Y static bool ui_fuzzing_debug = true; static uint32_t ui_fuzzing_seed; static bool ui_fuzzing_running; static bool ui_fuzzing_inside; static ui_fuzzing_t ui_fuzzing_work; static const char* lorem_ipsum_words[] = { "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "quisque", "faucibus", "ex", "sapien", "vitae", "pellentesque", "sem", "placerat", "in", "id", "cursus", "mi", "pretium", "tellus", "duis", "convallis", "tempus", "leo", "eu", "aenean", "sed", "diam", "urna", "tempor", "pulvinar", "vivamus", "fringilla", "lacus", "nec", "metus", "bibendum", "egestas", "iaculis", "massa", "nisl", "malesuada", "lacinia", "integer", "nunc", "posuere", "ut", "hendrerit", "semper", "vel", "class", "aptent", "taciti", "sociosqu", "ad", "litora", "torquent", "per", "conubia", "nostra", "inceptos", "himenaeos", "orci", "varius", "natoque", "penatibus", "et", "magnis", "dis", "parturient", "montes", "nascetur", "ridiculus", "mus", "donec", "rhoncus", "eros", "lobortis", "nulla", "molestie", "mattis", "scelerisque", "maximus", "eget", "fermentum", "odio", "phasellus", "non", "purus", "est", "efficitur", "laoreet", "mauris", "pharetra", "vestibulum", "fusce", "dictum", "risus", "blandit", "quis", "suspendisse", "aliquet", "nisi", "sodales", "consequat", "magna", "ante", "condimentum", "neque", "at", "luctus", "nibh", "finibus", "facilisis", "dapibus", "etiam", "interdum", "tortor", "ligula", "congue", "sollicitudin", "erat", "viverra", "ac", "tincidunt", "nam", "porta", "elementum", "a", "enim", "euismod", "quam", "justo", "lectus", "commodo", "augue", "arcu", "dignissim", "velit", "aliquam", "imperdiet", "mollis", "nullam", "volutpat", "porttitor", "ullamcorper", "rutrum", "gravida", "cras", "eleifend", "turpis", "fames", "primis", "vulputate", "ornare", "sagittis", "vehicula", "praesent", "dui", "felis", "venenatis", "ultrices", "proin", "libero", "feugiat", "tristique", "accumsan", "maecenas", "potenti", "ultricies", "habitant", "morbi", "senectus", "netus", "suscipit", "auctor", "curabitur", "facilisi", "cubilia", "curae", "hac", "habitasse", "platea", "dictumst" }; #define ui_fuzzing_lorem_ipsum_canonique \ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " \ "eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad " \ "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " \ "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " \ "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur " \ "sint occaecat cupidatat non proident, sunt in culpa qui officia " \ "deserunt mollit anim id est laborum." #define ui_fuzzing_lorem_ipsum_chinese \ "\xE6\x88\x91\xE6\x98\xAF\xE6\x94\xBE\xE7\xBD\xAE\xE6\x96\x87\xE6\x9C\xAC\xE7\x9A\x84\xE4" \ "\xBD\x8D\xE7\xBD\xAE\xE3\x80\x82\xE8\xBF\x99\xE9\x87\x8C\xE6\x94\xBE\xE7\xBD\xAE\xE4\xBA" \ "\x86\xE5\x81\x87\xE6\x96\x87\xE5\x81\x87\xE5\xAD\x97\xE3\x80\x82\xE5\xB8\x8C\xE6\x9C\x9B" \ "\xE8\xBF\x99\xE4\xBA\x9B\xE6\x96\x87\xE5\xAD\x97\xE5\x8F\xAF\xE4\xBB\xA5\xE5\xA1\xAB\xE5" \ "\x85\x85\xE7\xA9\xBA\xE7\x99\xBD\xE3\x80\x82"; #define ui_fuzzing_lorem_ipsum_japanese \ "\xE3\x81\x93\xE3\x82\x8C\xE3\x81\xAF\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3" \ "\x82\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x81\xA7\xE3\x81\x99\xE3\x80\x82\xE3\x81\x93\xE3\x81" \ "\x93\xE3\x81\xAB\xE6\x96\x87\xE7\xAB\xA0\xE3\x81\x8C\xE5\x85\xA5\xE3\x82\x8A\xE3\x81\xBE" \ "\xE3\x81\x99\xE3\x80\x82\xE8\xAA\xAD\xE3\x81\xBF\xE3\x82\x84\xE3\x81\x99\xE3\x81\x84\xE3" \ "\x82\x88\xE3\x81\x86\xE3\x81\xAB\xE3\x83\x80\xE3\x83\x9F\xE3\x83\xBC\xE3\x83\x86\xE3\x82" \ "\xAD\xE3\x82\xB9\xE3\x83\x88\xE3\x82\x92\xE4\xBD\xBF\xE7\x94\xA8\xE3\x81\x97\xE3\x81\xA6" \ "\xE3\x81\x84\xE3\x81\xBE\xE3\x81\x99\xE3\x80\x82"; #define ui_fuzzing_lorem_ipsum_korean \ "\xEC\x9D\xB4\xEA\xB2\x83\xEC\x9D\x80\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED\x85\x8D\xEC\x8A" \ "\xA4\xED\x8A\xB8\xEC\x9E\x85\xEB\x8B\x88\xEB\x8B\xA4\x2E\x20\xEC\x97\xAC\xEA\xB8\xB0\xEC" \ "\x97\x90\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEB\x93\x9C\xEC\x96\xB4\xEA\xB0\x80" \ "\xEB\x8A\x94\x20\xEB\xAC\xB8\xEC\x9E\x90\xEA\xB0\x80\x20\xEC\x9E\x88\xEB\x8B\xA4\x2E\x20" \ "\xEC\x9D\xBD\xEA\xB8\xB0\x20\xEC\x89\xBD\xEA\xB2\x8C\x20\xEB\x8D\x94\xEB\xAF\xB8\x20\xED" \ "\x85\x8D\xEC\x8A\xA4\xED\x8A\xB8\xEB\xA5\xBC\x20\xEC\x82\xAC\xEC\x9A\xA9\xED\x95\xA9\xEB" \ "\x8B\x88\xEB\x8B\xA4\x2E"; #define ui_fuzzing_lorem_ipsum_emoji \ "\xF0\x9F\x8D\x95\xF0\x9F\x9A\x80\xF0\x9F\xA6\x84\xF0\x9F\x92\xBB\xF0\x9F\x8E\x89\xF0\x9F" \ "\x8C\x88\xF0\x9F\x90\xB1\xF0\x9F\x93\x9A\xF0\x9F\x8E\xA8\xF0\x9F\x8D\x94\xF0\x9F\x8D\xA6" \ "\xF0\x9F\x8E\xB8\xF0\x9F\xA7\xA9\xF0\x9F\x8D\xBF\xF0\x9F\x93\xB7\xF0\x9F\x8E\xA4\xF0\x9F" \ "\x91\xBE\xF0\x9F\x8C\xAE\xF0\x9F\x8E\x88\xF0\x9F\x9A\xB2\xF0\x9F\x8D\xA9\xF0\x9F\x8E\xAE" \ "\xF0\x9F\x8D\x89\xF0\x9F\x8E\xAC\xF0\x9F\x90\xB6\xF0\x9F\x93\xB1\xF0\x9F\x8E\xB9\xF0\x9F" \ "\xA6\x96\xF0\x9F\x8C\x9F\xF0\x9F\x8D\xAD\xF0\x9F\x8E\xA4\xF0\x9F\x8F\x96\xF0\x9F\xA6\x8B" \ "\xF0\x9F\x8E\xB2\xF0\x9F\x8E\xAF\xF0\x9F\x8D\xA3\xF0\x9F\x9A\x81\xF0\x9F\x8E\xAD\xF0\x9F" \ "\x91\x9F\xF0\x9F\x9A\x82\xF0\x9F\x8D\xAA\xF0\x9F\x8E\xBB\xF0\x9F\x9B\xB8\xF0\x9F\x8C\xBD" \ "\xF0\x9F\x93\x80\xF0\x9F\x9A\x80\xF0\x9F\xA7\x81\xF0\x9F\x93\xAF\xF0\x9F\x8C\xAF\xF0\x9F" \ "\x90\xA5\xF0\x9F\xA7\x83\xF0\x9F\x8D\xBB\xF0\x9F\x8E\xAE"; typedef struct { char* text; int32_t count; // at least 1KB uint32_t seed; // seed for random generator int32_t min_paragraphs; // at least 1 int32_t max_paragraphs; int32_t min_sentences; // at least 1 int32_t max_sentences; int32_t min_words; // at least 2 int32_t max_words; const char* append; // append after each paragraph (e.g. extra "\n") } ui_fuzzing_generator_params_t; static uint32_t ui_fuzzing_random(void) { return rt_num.random32(&ui_fuzzing_seed); } static fp64_t ui_fuzzing_random_fp64(void) { uint32_t r = ui_fuzzing_random(); return (fp64_t)r / (fp64_t)UINT32_MAX; } static void ui_fuzzing_generator(ui_fuzzing_generator_params_t p) { rt_fatal_if(p.count < 1024); // at least 1KB expected rt_fatal_if_not(0 < p.min_paragraphs && p.min_paragraphs <= p.max_paragraphs); rt_fatal_if_not(0 < p.min_sentences && p.min_sentences <= p.max_sentences); rt_fatal_if_not(2 < p.min_words && p.min_words <= p.max_words); char* s = p.text; // assume longest word is less than 128 char* end = p.text + p.count - 128; uint32_t paragraphs = p.min_paragraphs + (p.min_paragraphs == p.max_paragraphs ? 0 : rt_num.random32(&p.seed) % (p.max_paragraphs - p.min_paragraphs + 1)); while (paragraphs > 0 && s < end) { uint32_t sentences_in_paragraph = p.min_sentences + (p.min_sentences == p.max_sentences ? 0 : rt_num.random32(&p.seed) % (p.max_sentences - p.min_sentences + 1)); while (sentences_in_paragraph > 0 && s < end) { const uint32_t words_in_sentence = p.min_words + (p.min_words == p.max_words ? 0 : rt_num.random32(&p.seed) % (p.max_words - p.min_words + 1)); for (uint32_t i = 0; i < words_in_sentence && s < end; i++) { const int32_t ix = rt_num.random32(&p.seed) % rt_countof(lorem_ipsum_words); const char* word = lorem_ipsum_words[ix]; memcpy(s, word, strlen(word)); if (i == 0) { *s = (char)toupper(*s); } s += strlen(word); if (i < words_in_sentence - 1 && s < end) { const char* delimiter = "\x20"; int32_t punctuation = rt_num.random32(&p.seed) % 128; switch (punctuation) { case 0: case 1: case 2: delimiter = ", "; break; case 3: case 4: delimiter = "; "; break; case 6: delimiter = ": "; break; case 7: delimiter = " - "; break; default: break; } memcpy(s, delimiter, strlen(delimiter)); s += strlen(delimiter); } } if (sentences_in_paragraph > 1 && s < end) { memcpy(s, ".\x20", 2); s += 2; } else { *s++ = '.'; } sentences_in_paragraph--; } if (paragraphs > 1 && s < end) { *s++ = '\n'; } if (p.append != null && p.append[0] != 0) { memcpy(s, p.append, strlen(p.append)); s += strlen(p.append); } paragraphs--; } *s = 0; // rt_println("%s\n", p.text); } static void ui_fuzzing_next_gibberish(int32_t number_of_characters, char text[]) { static fp64_t freq[96] = { 0.1716, 0.0023, 0.0027, 0.0002, 0.0001, 0.0005, 0.0013, 0.0012, 0.0015, 0.0014, 0.0017, 0.0002, 0.0084, 0.0020, 0.0075, 0.0040, 0.0135, 0.0045, 0.0053, 0.0053, 0.0047, 0.0047, 0.0043, 0.0047, 0.0057, 0.0044, 0.0037, 0.0004, 0.0016, 0.0004, 0.0017, 0.0017, 0.0020, 0.0045, 0.0026, 0.0020, 0.0027, 0.0021, 0.0025, 0.0026, 0.0030, 0.0025, 0.0021, 0.0018, 0.0028, 0.0026, 0.0024, 0.0020, 0.0025, 0.0026, 0.0030, 0.0022, 0.0027, 0.0022, 0.0020, 0.0023, 0.0015, 0.0016, 0.0009, 0.0005, 0.0005, 0.0001, 0.0003, 0.0003, 0.0078, 0.0013, 0.0012, 0.0008, 0.0012, 0.0007, 0.0006, 0.0011, 0.0016, 0.0012, 0.0011, 0.0004, 0.0004, 0.0016, 0.0013, 0.0009, 0.0009, 0.0008, 0.0013, 0.0011, 0.0013, 0.0012, 0.0006, 0.0007, 0.0011, 0.0005, 0.0007, 0.0003, 0.0002, 0.0006, 0.0002, 0.0005 }; static fp64_t cumulative_freq[96]; static bool initialized = 0; if (!initialized) { cumulative_freq[0] = freq[0]; for (int i = 1; i < rt_countof(freq); i++) { cumulative_freq[i] = cumulative_freq[i - 1] + freq[i]; } initialized = 1; } int32_t i = 0; while (i < number_of_characters) { text[i] = 0x00; fp64_t r = ui_fuzzing_random_fp64(); for (int j = 0; j < 96 && text[i] == 0; j++) { if (r < cumulative_freq[j]) { text[i] = (char)(0x20 + j); } } if (text[i] != 0) { i++; } } text[number_of_characters] = 0x00; } static void ui_fuzzing_dispatch(ui_fuzzing_t* work) { rt_swear(work == &ui_fuzzing_work); ui_app.alt = work->alt; ui_app.ctrl = work->ctrl; ui_app.shift = work->shift; if (work->utf8 != null && work->utf8[0] != 0) { ui_view.character(ui_app.content, work->utf8); work->utf8 = work->utf8[1] == 0 ? null : work->utf8++; } else if (work->key != 0) { ui_view.key_pressed(ui_app.content, work->key); ui_view.key_released(ui_app.content, work->key); work->key = 0; } else if (work->pt != null) { const int32_t x = work->pt->x; const int32_t y = work->pt->y; ui_app.mouse.x = x; ui_app.mouse.y = y; // https://stackoverflow.com/questions/22259936/ // https://stackoverflow.com/questions/65691101/ // rt_println("%d,%d", x + ui_app.wrc.x, y + ui_app.wrc.y); // // next line works only when running as administrator: // rt_fatal_win32err(SetCursorPos(x + ui_app.wrc.x, y + ui_app.wrc.y)); const bool l_button = ui_app.mouse_left != work->left; const bool r_button = ui_app.mouse_right != work->right; ui_app.mouse_left = work->left; ui_app.mouse_right = work->right; ui_view.mouse_move(ui_app.content); if (l_button) { ui_view.tap(ui_app.content, 0, work->left); } if (r_button) { ui_view.tap(ui_app.content, 2, work->right); } work->pt = null; } else { rt_assert(false, "TODO: ?"); } if (ui_fuzzing_running) { if (ui_fuzzing.next == null) { ui_fuzzing.next_random(work); } else { ui_fuzzing.next(work); } } } static void ui_fuzzing_do_work(rt_work_t* p) { if (ui_fuzzing_running) { ui_fuzzing_inside = true; if (ui_fuzzing.custom != null) { ui_fuzzing.custom((ui_fuzzing_t*)p); } else { ui_fuzzing.dispatch((ui_fuzzing_t*)p); } ui_fuzzing_inside = false; } else { // fuzzing has been .stop()-ed drop it } } static void ui_fuzzing_post(void) { ui_app.post(&ui_fuzzing_work.base); } static void ui_fuzzing_alt_ctrl_shift(void) { ui_fuzzing_t* w = &ui_fuzzing_work; switch (ui_fuzzing_random() % 8) { case 0: w->alt = 0; w->ctrl = 0; w->shift = 0; break; case 1: w->alt = 1; w->ctrl = 0; w->shift = 0; break; case 2: w->alt = 0; w->ctrl = 1; w->shift = 0; break; case 3: w->alt = 1; w->ctrl = 1; w->shift = 0; break; case 4: w->alt = 0; w->ctrl = 0; w->shift = 1; break; case 5: w->alt = 1; w->ctrl = 0; w->shift = 1; break; case 6: w->alt = 0; w->ctrl = 1; w->shift = 1; break; case 7: w->alt = 1; w->ctrl = 1; w->shift = 1; break; default: rt_assert(false); } } static void ui_fuzzing_character(void) { static char utf8[4 * 1024]; if (ui_fuzzing_work.utf8 == null) { fp64_t r = ui_fuzzing_random_fp64(); if (r < 0.125) { uint32_t rnd = ui_fuzzing_random(); int32_t n = (int32_t)rt_max(1, rnd % 32); ui_fuzzing_next_gibberish(n, utf8); ui_fuzzing_work.utf8 = utf8; if (ui_fuzzing_debug) { // rt_println("%s", utf8); } } else if (r < 0.25) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_chinese; } else if (r < 0.375) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_japanese; } else if (r < 0.5) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_korean; } else if (r < 0.5 + 0.125) { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_emoji; } else { ui_fuzzing_work.utf8 = ui_fuzzing_lorem_ipsum_canonique; } } ui_fuzzing_post(); } static void ui_fuzzing_key(void) { struct { int32_t key; const char* name; } keys[] = { { ui.key.up, "up", }, { ui.key.down, "down", }, { ui.key.left, "left", }, { ui.key.right, "right", }, { ui.key.home, "home", }, { ui.key.end, "end", }, { ui.key.page_up, "pgup", }, { ui.key.page_down, "pgdw", }, { ui.key.insert, "insert" }, { ui.key.enter, "enter" }, { ui.key.del, "delete" }, { ui.key.back, "back" }, }; ui_fuzzing_alt_ctrl_shift(); uint32_t ix = ui_fuzzing_random() % rt_countof(keys); if (ui_fuzzing_debug) { // rt_println("key(%s)", keys[ix].name); } ui_fuzzing_work.key = keys[ix].key; ui_fuzzing_post(); } static void ui_fuzzing_mouse(void) { // mouse events only inside edit control otherwise // they will start clicking buttons around ui_view_t* v = ui_app.content; ui_fuzzing_t* w = &ui_fuzzing_work; int32_t x = ui_fuzzing_random() % v->w; int32_t y = ui_fuzzing_random() % v->h; static ui_point_t pt; pt = (ui_point_t){ x + v->x, y + v->y }; if (ui_fuzzing_random() % 2) { w->left = !w->left; } if (ui_fuzzing_random() % 2) { w->right = !w->right; } if (ui_fuzzing_debug) { // rt_println("mouse(%d,%d) %s%s", pt.x, pt.y, // w->left ? "L" : "_", w->right ? "R" : "_"); } w->pt = &pt; ui_fuzzing_post(); } static void ui_fuzzing_start(uint32_t seed) { ui_fuzzing_seed = seed | 0x1; ui_fuzzing_running = true; if (ui_fuzzing.next == null) { ui_fuzzing.next_random(&ui_fuzzing_work); } else { ui_fuzzing.next(&ui_fuzzing_work); } } static bool ui_fuzzing_is_running(void) { return ui_fuzzing_running; } static bool ui_fuzzing_from_inside(void) { return ui_fuzzing_inside; } static void ui_fuzzing_stop(void) { ui_fuzzing_running = false; } static void ui_fuzzing_next_random(ui_fuzzing_t* f) { rt_swear(f == &ui_fuzzing_work); ui_fuzzing_work = (ui_fuzzing_t){ .base = { .when = rt_clock.seconds() + 0.001, // 1ms .work = ui_fuzzing_do_work }, }; uint32_t rnd = ui_fuzzing_random() % 100; if (rnd < 80) { ui_fuzzing_character(); } else if (rnd < 90) { ui_fuzzing_key(); } else { ui_fuzzing_mouse(); } } ui_fuzzing_if ui_fuzzing = { .start = ui_fuzzing_start, .is_running = ui_fuzzing_is_running, .from_inside = ui_fuzzing_from_inside, .next_random = ui_fuzzing_next_random, .dispatch = ui_fuzzing_dispatch, .next = null, .custom = null, .stop = ui_fuzzing_stop }; ================================================ FILE: src/ui/ui_gdi.c ================================================ #include "rt/rt.h" #include "ui/ui.h" #include "rt/rt_win32.h" #pragma push_macro("ui_gdi_with_hdc") #pragma push_macro("ui_gdi_hdc_with_font") static ui_brush_t ui_gdi_brush_hollow; static ui_brush_t ui_gdi_brush_color; static ui_pen_t ui_gdi_pen_hollow; static ui_region_t ui_gdi_clip; typedef struct ui_gdi_context_s { HDC hdc; // window canvas() or memory DC int32_t background_mode; int32_t stretch_mode; ui_pen_t pen; ui_font_t font; ui_color_t text_color; POINT brush_origin; ui_brush_t brush; HBITMAP texture; } ui_gdi_context_t; static ui_gdi_context_t ui_gdi_context; #define ui_gdi_hdc() (ui_gdi_context.hdc) static void ui_gdi_init(void) { ui_gdi_brush_hollow = (ui_brush_t)GetStockBrush(HOLLOW_BRUSH); ui_gdi_brush_color = (ui_brush_t)GetStockBrush(DC_BRUSH); ui_gdi_pen_hollow = (ui_pen_t)GetStockPen(NULL_PEN); } static void ui_gdi_fini(void) { if (ui_gdi_clip != null) { rt_fatal_win32err(DeleteRgn(ui_gdi_clip)); } ui_gdi_clip = null; } static ui_pen_t ui_gdi_set_pen(ui_pen_t p) { rt_not_null(p); return (ui_pen_t)SelectPen(ui_gdi_hdc(), (HPEN)p); } static ui_brush_t ui_gdi_set_brush(ui_brush_t b) { rt_not_null(b); return (ui_brush_t)SelectBrush(ui_gdi_hdc(), b); } static uint32_t ui_gdi_color_rgb(ui_color_t c) { rt_assert(ui_color_is_8bit(c)); return (COLORREF)(c & 0xFFFFFFFF); } static COLORREF ui_gdi_color_ref(ui_color_t c) { return ui_gdi.color_rgb(c); } static ui_color_t ui_gdi_set_text_color(ui_color_t c) { return SetTextColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); } static ui_font_t ui_gdi_set_font(ui_font_t f) { rt_not_null(f); return (ui_font_t)SelectFont(ui_gdi_hdc(), (HFONT)f); } static void ui_gdi_begin(ui_bitmap_t* image) { rt_swear(ui_gdi_context.hdc == null, "no nested begin()/end()"); if (image != null) { rt_swear(image->texture != null); ui_gdi_context.hdc = CreateCompatibleDC((HDC)ui_app.canvas); ui_gdi_context.texture = SelectBitmap(ui_gdi_hdc(), (HBITMAP)image->texture); } else { ui_gdi_context.hdc = (HDC)ui_app.canvas; rt_swear(ui_gdi_context.texture == null); } ui_gdi_context.font = ui_gdi_set_font(ui_app.fm.prop.normal.font); ui_gdi_context.pen = ui_gdi_set_pen(ui_gdi_pen_hollow); ui_gdi_context.brush = ui_gdi_set_brush(ui_gdi_brush_hollow); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &ui_gdi_context.brush_origin)); ui_color_t tc = ui_colors.get_color(ui_color_id_window_text); ui_gdi_context.text_color = ui_gdi_set_text_color(tc); ui_gdi_context.background_mode = SetBkMode(ui_gdi_hdc(), TRANSPARENT); ui_gdi_context.stretch_mode = SetStretchBltMode(ui_gdi_hdc(), HALFTONE); } static void ui_gdi_end(void) { rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), ui_gdi_context.brush_origin.x, ui_gdi_context.brush_origin.y, null)); ui_gdi_set_brush(ui_gdi_context.brush); ui_gdi_set_pen(ui_gdi_context.pen); ui_gdi_set_text_color(ui_gdi_context.text_color); SetBkMode(ui_gdi_hdc(), ui_gdi_context.background_mode); SetStretchBltMode(ui_gdi_hdc(), ui_gdi_context.stretch_mode); if (ui_gdi_context.hdc != (HDC)ui_app.canvas) { rt_swear(ui_gdi_context.texture != null); // 1x1 bitmap SelectBitmap(ui_gdi_context.hdc, (HBITMAP)ui_gdi_context.texture); rt_fatal_win32err(DeleteDC(ui_gdi_context.hdc)); } memset(&ui_gdi_context, 0x00, sizeof(ui_gdi_context)); } static ui_pen_t ui_gdi_set_colored_pen(ui_color_t c) { ui_pen_t p = (ui_pen_t)SelectPen(ui_gdi_hdc(), GetStockPen(DC_PEN)); SetDCPenColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); return p; } static ui_pen_t ui_gdi_create_pen(ui_color_t c, int32_t width) { rt_assert(width >= 1); ui_pen_t pen = (ui_pen_t)CreatePen(PS_SOLID, width, ui_gdi_color_ref(c)); rt_not_null(pen); return pen; } static void ui_gdi_delete_pen(ui_pen_t p) { rt_fatal_win32err(DeletePen(p)); } static ui_brush_t ui_gdi_create_brush(ui_color_t c) { return (ui_brush_t)CreateSolidBrush(ui_gdi_color_ref(c)); } static void ui_gdi_delete_brush(ui_brush_t b) { DeleteBrush((HBRUSH)b); } static ui_color_t ui_gdi_set_brush_color(ui_color_t c) { return SetDCBrushColor(ui_gdi_hdc(), ui_gdi_color_ref(c)); } static void ui_gdi_set_clip(int32_t x, int32_t y, int32_t w, int32_t h) { if (ui_gdi_clip != null) { DeleteRgn(ui_gdi_clip); ui_gdi_clip = null; } if (w > 0 && h > 0) { ui_gdi_clip = (ui_region_t)CreateRectRgn(x, y, x + w, y + h); rt_not_null(ui_gdi_clip); } rt_fatal_if(SelectClipRgn(ui_gdi_hdc(), (HRGN)ui_gdi_clip) == ERROR); } static void ui_gdi_pixel(int32_t x, int32_t y, ui_color_t c) { rt_not_null(ui_app.canvas); rt_fatal_win32err(SetPixel(ui_gdi_hdc(), x, y, ui_gdi_color_ref(c))); } static void ui_gdi_rectangle(int32_t x, int32_t y, int32_t w, int32_t h) { rt_fatal_win32err(Rectangle(ui_gdi_hdc(), x, y, x + w, y + h)); } static void ui_gdi_line(int32_t x0, int32_t y0, int32_t x1, int32_t y1, ui_color_t c) { POINT pt; rt_fatal_win32err(MoveToEx(ui_gdi_hdc(), x0, y0, &pt)); ui_pen_t p = ui_gdi_set_colored_pen(c); rt_fatal_win32err(LineTo(ui_gdi_hdc(), x1, y1)); ui_gdi_set_pen(p); rt_fatal_win32err(MoveToEx(ui_gdi_hdc(), pt.x, pt.y, null)); } static void ui_gdi_frame(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c) { ui_brush_t b = ui_gdi_set_brush(ui_gdi_brush_hollow); ui_pen_t p = ui_gdi_set_colored_pen(c); ui_gdi_rectangle(x, y, w, h); ui_gdi_set_pen(p); ui_gdi_set_brush(b); } static void ui_gdi_rect(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t border, ui_color_t fill) { const bool tf = ui_color_is_transparent(fill); // transparent fill const bool tb = ui_color_is_transparent(border); // transparent border ui_brush_t b = tf ? ui_gdi_brush_hollow : ui_gdi_brush_color; b = ui_gdi_set_brush(b); ui_color_t c = tf ? ui_colors.transparent : ui_gdi_set_brush_color(fill); ui_pen_t p = tb ? ui_gdi_set_pen(ui_gdi_pen_hollow) : ui_gdi_set_colored_pen(border); ui_gdi_rectangle(x, y, w, h); if (!tf) { ui_gdi_set_brush_color(c); } ui_gdi_set_pen(p); ui_gdi_set_brush(b); } static void ui_gdi_fill(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t c) { // rt_println("%d,%d %dx%d 0x%08X", x, y, w, h, (uint32_t)c); ui_brush_t b = ui_gdi_set_brush(ui_gdi_brush_color); c = ui_gdi_set_brush_color(c); RECT rc = { x, y, x + w, y + h }; HBRUSH brush = (HBRUSH)GetCurrentObject(ui_gdi_hdc(), OBJ_BRUSH); rt_fatal_win32err(FillRect(ui_gdi_hdc(), &rc, brush)); ui_gdi_set_brush_color(c); ui_gdi_set_brush(b); } static void ui_gdi_poly(ui_point_t* points, int32_t count, ui_color_t c) { // make sure ui_point_t and POINT have the same memory layout: static_assert(sizeof(points->x) == sizeof(((POINT*)0)->x), "ui_point_t"); static_assert(sizeof(points->y) == sizeof(((POINT*)0)->y), "ui_point_t"); static_assert(sizeof(points[0]) == sizeof(*((POINT*)0)), "ui_point_t"); rt_assert(ui_gdi_hdc() != null && count > 1); ui_pen_t pen = ui_gdi_set_colored_pen(c); rt_fatal_win32err(Polyline(ui_gdi_hdc(), (POINT*)points, count)); ui_gdi_set_pen(pen); } static void ui_gdi_circle(int32_t x, int32_t y, int32_t radius, ui_color_t border, ui_color_t fill) { rt_swear(!ui_color_is_transparent(border) || ui_color_is_transparent(fill)); // Win32 GDI even radius drawing looks ugly squarish and asymmetrical. rt_swear(radius % 2 == 1, "radius: %d must be odd"); if (ui_color_is_transparent(border)) { rt_assert(!ui_color_is_transparent(fill)); border = fill; } rt_assert(!ui_color_is_transparent(border)); const bool tf = ui_color_is_transparent(fill); // transparent fill ui_brush_t brush = tf ? ui_gdi_set_brush(ui_gdi_brush_hollow) : ui_gdi_set_brush(ui_gdi_brush_color); ui_color_t c = tf ? ui_colors.transparent : ui_gdi_set_brush_color(fill); ui_pen_t p = ui_gdi_set_colored_pen(border); HDC hdc = ui_gdi_context.hdc; int32_t l = x - radius; int32_t t = y - radius; int32_t r = x + radius + 1; int32_t b = y + radius + 1; Ellipse(hdc, l, t, r, b); // SetPixel(hdc, x, y, RGB(255, 255, 255)); ui_gdi_set_pen(p); if (!tf) { ui_gdi_set_brush_color(c); } ui_gdi_set_brush(brush); } static void ui_gdi_fill_rounded(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t fill) { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi_circle(x + radius, y + radius, radius, fill, fill); ui_gdi_circle(r - radius, y + radius, radius, fill, fill); ui_gdi_circle(x + radius, b - radius, radius, fill, fill); ui_gdi_circle(r - radius, b - radius, radius, fill, fill); // rectangles ui_gdi.fill(x + radius, y, w - radius * 2, h, fill); r = x + w - radius; ui_gdi.fill(x, y + radius, radius, h - radius * 2, fill); ui_gdi.fill(r, y + radius, radius, h - radius * 2, fill); } static void ui_gdi_rounded_border(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t border) { { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi.set_clip(x, y, radius + 1, radius + 1); ui_gdi_circle(x + radius, y + radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(r - radius, y, radius + 1, radius + 1); ui_gdi_circle(r - radius, y + radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(x, b - radius, radius + 1, radius + 1); ui_gdi_circle(x + radius, b - radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(r - radius, b - radius, radius + 1, radius + 1); ui_gdi_circle(r - radius, b - radius, radius, border, ui_colors.transparent); ui_gdi.set_clip(0, 0, 0, 0); } { int32_t r = x + w - 1; // right int32_t b = y + h - 1; // bottom ui_gdi.line(x + radius, y, r - radius + 1, y, border); ui_gdi.line(x + radius, b, r - radius + 1, b, border); ui_gdi.line(x - 1, y + radius, x - 1, b - radius + 1, border); ui_gdi.line(r + 1, y + radius, r + 1, b - radius + 1, border); } } static void ui_gdi_rounded(int32_t x, int32_t y, int32_t w, int32_t h, int32_t radius, ui_color_t border, ui_color_t fill) { rt_swear(!ui_color_is_transparent(border) || !ui_color_is_transparent(fill)); if (!ui_color_is_transparent(fill)) { ui_gdi_fill_rounded(x, y, w, h, radius, fill); } if (!ui_color_is_transparent(border)) { ui_gdi_rounded_border(x, y, w, h, radius, border); } } static void ui_gdi_gradient(int32_t x, int32_t y, int32_t w, int32_t h, ui_color_t rgba_from, ui_color_t rgba_to, bool vertical) { TRIVERTEX vertex[2] = {0}; vertex[0].x = x; vertex[0].y = y; // TODO: colors: vertex[0].Red = (COLOR16)(((rgba_from >> 0) & 0xFF) << 8); vertex[0].Green = (COLOR16)(((rgba_from >> 8) & 0xFF) << 8); vertex[0].Blue = (COLOR16)(((rgba_from >> 16) & 0xFF) << 8); vertex[0].Alpha = (COLOR16)(((rgba_from >> 24) & 0xFF) << 8); vertex[1].x = x + w; vertex[1].y = y + h; vertex[1].Red = (COLOR16)(((rgba_to >> 0) & 0xFF) << 8); vertex[1].Green = (COLOR16)(((rgba_to >> 8) & 0xFF) << 8); vertex[1].Blue = (COLOR16)(((rgba_to >> 16) & 0xFF) << 8); vertex[1].Alpha = (COLOR16)(((rgba_to >> 24) & 0xFF) << 8); GRADIENT_RECT gRect = {0, 1}; const uint32_t mode = vertical ? GRADIENT_FILL_RECT_V : GRADIENT_FILL_RECT_H; GradientFill(ui_gdi_hdc(), vertex, 2, &gRect, 1, mode); } static BITMAPINFO* ui_gdi_greyscale_bitmap_info(void) { typedef struct bitmap_rgb_s { BITMAPINFO bi; RGBQUAD rgb[256]; } bitmap_rgb_t; static bitmap_rgb_t storage; // for gs palette static BITMAPINFO* bi = &storage.bi; BITMAPINFOHEADER* bih = &bi->bmiHeader; if (bih->biSize == 0) { // once bih->biSize = sizeof(BITMAPINFOHEADER); for (int32_t i = 0; i < 256; i++) { RGBQUAD* q = &bi->bmiColors[i]; q->rgbReserved = 0; q->rgbBlue = q->rgbGreen = q->rgbRed = (uint8_t)i; } bih->biPlanes = 1; bih->biBitCount = 8; bih->biCompression = BI_RGB; bih->biClrUsed = 256; bih->biClrImportant = 256; } return bi; } static void ui_gdi_pixels(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, int32_t bpp, const uint8_t* pixels) { if (bpp == 1) { ui_gdi.greyscale(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else if (bpp == 3) { ui_gdi.bgr(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else if (bpp == 4) { ui_gdi.bgrx(dx, dy, dw, dh, ix, iy, iw, ih, width, height, stride, pixels); } else { rt_fatal("bpp: %d not {1, 3, 4}", bpp); } } static void ui_gdi_greyscale(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFO *bi = ui_gdi_greyscale_bitmap_info(); // global! not thread safe BITMAPINFOHEADER* bih = &bi->bmiHeader; bih->biWidth = width; bih->biHeight = -height; // top down image bih->biSizeImage = (DWORD)(iw * abs(ih)); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static BITMAPINFOHEADER ui_gdi_bgrx_init_bi(int32_t w, int32_t h, int32_t bpp) { rt_assert(w > 0 && h >= 0); // h cannot be negative? BITMAPINFOHEADER bi = { .biSize = sizeof(BITMAPINFOHEADER), .biPlanes = 1, .biBitCount = (uint16_t)(bpp * 8), .biCompression = BI_RGB, .biWidth = w, .biHeight = -h, // top down image .biSizeImage = (DWORD)(w * abs(h) * bpp), .biClrUsed = 0, .biClrImportant = 0 }; return bi; } // bgr(width) assumes strides are padded and rounded up to 4 bytes // if this is not the case use ui_gdi.bitmap_init() that will unpack // and align scanlines prior to draw static void ui_gdi_bgr(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width * 3 + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFOHEADER bi = ui_gdi_bgrx_init_bi(width, height, 3); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, (BITMAPINFO*)&bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static void ui_gdi_bgrx(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, int32_t width, int32_t height, int32_t stride, const uint8_t* pixels) { rt_fatal_if(stride != ((width * 4 + 3) & ~0x3)); rt_assert(iw > 0 && ih != 0); // h can be negative if (iw > 0 && ih != 0) { BITMAPINFOHEADER bi = ui_gdi_bgrx_init_bi(width, height, 4); POINT pt = { 0 }; rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), 0, 0, &pt)); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, pixels, (BITMAPINFO*)&bi, DIB_RGB_COLORS, SRCCOPY) == 0); rt_fatal_win32err(SetBrushOrgEx(ui_gdi_hdc(), pt.x, pt.y, &pt)); } } static BITMAPINFO* ui_gdi_init_bitmap_info(int32_t w, int32_t h, int32_t bpp, BITMAPINFO* bi) { rt_assert(w > 0 && h >= 0); // h cannot be negative? bi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bi->bmiHeader.biWidth = w; bi->bmiHeader.biHeight = -h; // top down image bi->bmiHeader.biPlanes = 1; bi->bmiHeader.biBitCount = (uint16_t)(bpp * 8); bi->bmiHeader.biCompression = BI_RGB; bi->bmiHeader.biSizeImage = (DWORD)(w * abs(h) * bpp); return bi; } static void ui_gdi_create_dib_section(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp) { rt_fatal_if(image->texture != null, "bitmap_dispose() not called?"); // not using GetWindowDC(ui_app.window) will allow to initialize images // before window is created HDC c = CreateCompatibleDC(null); // GetWindowDC(ui_app.window); BITMAPINFO local = { {sizeof(BITMAPINFOHEADER)} }; BITMAPINFO* bi = bpp == 1 ? ui_gdi_greyscale_bitmap_info() : &local; image->texture = (ui_texture_t)CreateDIBSection(c, ui_gdi_init_bitmap_info(w, h, bpp, bi), DIB_RGB_COLORS, &image->pixels, null, 0x0 ); rt_fatal_if(image->texture == null || image->pixels == null); rt_fatal_win32err(DeleteDC(c)); } static void ui_gdi_bitmap_init_rgbx(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels) { bool swapped = bpp < 0; bpp = abs(bpp); rt_fatal_if(bpp != 4, "bpp: %d", bpp); ui_gdi_create_dib_section(image, w, h, bpp); const int32_t stride = (w * bpp + 3) & ~0x3; uint8_t* scanline = image->pixels; const uint8_t* rgbx = pixels; if (!swapped) { for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { bgra[0] = rgbx[2]; bgra[1] = rgbx[1]; bgra[2] = rgbx[0]; bgra[3] = 0xFF; bgra += 4; rgbx += 4; } pixels += w * 4; scanline += stride; } } else { for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { bgra[0] = rgbx[0]; bgra[1] = rgbx[1]; bgra[2] = rgbx[2]; bgra[3] = 0xFF; bgra += 4; rgbx += 4; } pixels += w * 4; scanline += stride; } } image->w = w; image->h = h; image->bpp = bpp; image->stride = stride; } static void ui_gdi_bitmap_init(ui_bitmap_t* image, int32_t w, int32_t h, int32_t bpp, const uint8_t* pixels) { bool swapped = bpp < 0; bpp = abs(bpp); rt_fatal_if(bpp < 0 || bpp == 2 || bpp > 4, "bpp=%d not {1, 3, 4}", bpp); ui_gdi_create_dib_section(image, w, h, bpp); // Win32 bitmaps stride is rounded up to 4 bytes const int32_t stride = (w * bpp + 3) & ~0x3; uint8_t* scanline = image->pixels; if (bpp == 1) { for (int32_t y = 0; y < h; y++) { memcpy(scanline, pixels, (size_t)w); pixels += w; scanline += stride; } } else if (bpp == 3 && !swapped) { const uint8_t* rgb = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgr = scanline; for (int32_t x = 0; x < w; x++) { bgr[0] = rgb[2]; bgr[1] = rgb[1]; bgr[2] = rgb[0]; bgr += 3; rgb += 3; } pixels += w * bpp; scanline += stride; } } else if (bpp == 3 && swapped) { const uint8_t* rgb = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgr = scanline; for (int32_t x = 0; x < w; x++) { bgr[0] = rgb[0]; bgr[1] = rgb[1]; bgr[2] = rgb[2]; bgr += 3; rgb += 3; } pixels += w * bpp; scanline += stride; } } else if (bpp == 4 && !swapped) { // premultiply alpha, see: // https://stackoverflow.com/questions/24595717/alphablend-generating-incorrect-colors const uint8_t* rgba = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { int32_t alpha = rgba[3]; bgra[0] = (uint8_t)(rgba[2] * alpha / 255); bgra[1] = (uint8_t)(rgba[1] * alpha / 255); bgra[2] = (uint8_t)(rgba[0] * alpha / 255); bgra[3] = rgba[3]; bgra += 4; rgba += 4; } pixels += w * 4; scanline += stride; } } else if (bpp == 4 && swapped) { // premultiply alpha, see: // https://stackoverflow.com/questions/24595717/alphablend-generating-incorrect-colors const uint8_t* rgba = pixels; for (int32_t y = 0; y < h; y++) { uint8_t* bgra = scanline; for (int32_t x = 0; x < w; x++) { int32_t alpha = rgba[3]; bgra[0] = (uint8_t)(rgba[0] * alpha / 255); bgra[1] = (uint8_t)(rgba[1] * alpha / 255); bgra[2] = (uint8_t)(rgba[2] * alpha / 255); bgra[3] = rgba[3]; bgra += 4; rgba += 4; } pixels += w * 4; scanline += stride; } } image->w = w; image->h = h; image->bpp = bpp; image->stride = stride; } static void ui_gdi_alpha(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* image, fp64_t alpha) { rt_assert(image->bpp > 0); rt_assert(0 <= alpha && alpha <= 1); rt_not_null(ui_gdi_hdc()); HDC c = CreateCompatibleDC(ui_gdi_hdc()); rt_not_null(c); HBITMAP zero1x1 = SelectBitmap((HDC)c, (HBITMAP)image->texture); BLENDFUNCTION bf = { 0 }; bf.SourceConstantAlpha = (uint8_t)(0xFF * alpha + 0.49); if (image->bpp == 4) { bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = AC_SRC_ALPHA; } else { bf.BlendOp = AC_SRC_OVER; bf.BlendFlags = 0; bf.AlphaFormat = 0; } rt_assert(0 <= ix && ix < image->w && 0 <= iy && iy < image->h); rt_assert(ix + iw <= image->w && iy + ih <= image->h); rt_fatal_win32err(AlphaBlend(ui_gdi_hdc(), dx, dy, dw, dh, c, ix, iy, iw, ih, bf)); SelectBitmap((HDC)c, zero1x1); rt_fatal_win32err(DeleteDC(c)); } static void ui_gdi_bitmap(int32_t dx, int32_t dy, int32_t dw, int32_t dh, int32_t ix, int32_t iy, int32_t iw, int32_t ih, ui_bitmap_t* image) { rt_assert(image->bpp == 1 || image->bpp == 3 || image->bpp == 4); rt_assert(0 <= ix && ix < image->w && 0 <= iy && iy < image->h); rt_assert(ix + iw <= image->w && iy + ih <= image->h); rt_not_null(ui_gdi_hdc()); if (image->bpp == 1) { // StretchBlt() is bad for greyscale BITMAPINFO* bi = ui_gdi_greyscale_bitmap_info(); BITMAPINFO* info = ui_gdi_init_bitmap_info(image->w, image->h, 1, bi); rt_fatal_if(StretchDIBits(ui_gdi_hdc(), dx, dy, dw, dh, ix, iy, iw, ih, image->pixels, info, DIB_RGB_COLORS, SRCCOPY) == 0); } else { HDC c = CreateCompatibleDC(ui_gdi_hdc()); rt_not_null(c); HBITMAP zero1x1 = SelectBitmap(c, image->texture); rt_fatal_win32err(StretchBlt(ui_gdi_hdc(), dx, dy, dw, dh, c, ix, iy, iw, ih, SRCCOPY)); SelectBitmap(c, zero1x1); rt_fatal_win32err(DeleteDC(c)); } } static void ui_gdi_icon(int32_t x, int32_t y, int32_t w, int32_t h, ui_icon_t icon) { DrawIconEx(ui_gdi_hdc(), x, y, (HICON)icon, w, h, 0, NULL, DI_NORMAL | DI_COMPAT); } static void ui_gdi_cleartype(bool on) { enum { spif = SPIF_UPDATEINIFILE | SPIF_SENDCHANGE }; rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHING, true, 0, spif)); uintptr_t s = on ? FE_FONTSMOOTHINGCLEARTYPE : FE_FONTSMOOTHINGSTANDARD; rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHINGTYPE, 0, (void*)s, spif)); } static void ui_gdi_font_smoothing_contrast(int32_t c) { rt_fatal_if(!(c == -1 || 1000 <= c && c <= 2200), "contrast: %d", c); if (c == -1) { c = 1400; } rt_fatal_win32err(SystemParametersInfoA(SPI_SETFONTSMOOTHINGCONTRAST, 0, (void*)(uintptr_t)c, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE)); } rt_static_assertion(ui_gdi_font_quality_default == DEFAULT_QUALITY); rt_static_assertion(ui_gdi_font_quality_draft == DRAFT_QUALITY); rt_static_assertion(ui_gdi_font_quality_proof == PROOF_QUALITY); rt_static_assertion(ui_gdi_font_quality_nonantialiased == NONANTIALIASED_QUALITY); rt_static_assertion(ui_gdi_font_quality_antialiased == ANTIALIASED_QUALITY); rt_static_assertion(ui_gdi_font_quality_cleartype == CLEARTYPE_QUALITY); rt_static_assertion(ui_gdi_font_quality_cleartype_natural == CLEARTYPE_NATURAL_QUALITY); static ui_font_t ui_gdi_create_font(const char* family, int32_t h, int32_t q) { rt_assert(h > 0); LOGFONTA lf = {0}; int32_t n = GetObjectA(ui_app.fm.prop.normal.font, sizeof(lf), &lf); rt_fatal_if(n != (int32_t)sizeof(lf)); lf.lfHeight = -h; rt_str_printf(lf.lfFaceName, "%s", family); if (ui_gdi_font_quality_default <= q && q <= ui_gdi_font_quality_cleartype_natural) { lf.lfQuality = (uint8_t)q; } else { rt_fatal_if(q != -1, "use -1 for do not care quality"); } return (ui_font_t)CreateFontIndirectA(&lf); } static ui_font_t ui_gdi_font(ui_font_t f, int32_t h, int32_t q) { rt_assert(f != null && h > 0); LOGFONTA lf = {0}; int32_t n = GetObjectA(f, sizeof(lf), &lf); rt_fatal_if(n != (int32_t)sizeof(lf)); lf.lfHeight = -h; if (ui_gdi_font_quality_default <= q && q <= ui_gdi_font_quality_cleartype_natural) { lf.lfQuality = (uint8_t)q; } else { rt_fatal_if(q != -1, "use -1 for do not care quality"); } return (ui_font_t)CreateFontIndirectA(&lf); } static void ui_gdi_delete_font(ui_font_t f) { rt_fatal_win32err(DeleteFont(f)); } // guaranteed to return dc != null even if not painting static HDC ui_gdi_get_dc(void) { rt_not_null(ui_app.window); HDC hdc = ui_gdi_hdc() != null ? ui_gdi_hdc() : GetDC((HWND)ui_app.window); rt_not_null(hdc); return hdc; } static void ui_gdi_release_dc(HDC hdc) { if (ui_gdi_hdc() == null) { ReleaseDC((HWND)ui_app.window, hdc); } } #define ui_gdi_with_hdc(code) do { \ HDC hdc = ui_gdi_get_dc(); \ code \ ui_gdi_release_dc(hdc); \ } while (0) #define ui_gdi_hdc_with_font(f, ...) do { \ rt_not_null(f); \ HDC hdc = ui_gdi_get_dc(); \ HFONT font_ = SelectFont(hdc, (HFONT)f); \ { __VA_ARGS__ } \ SelectFont(hdc, font_); \ ui_gdi_release_dc(hdc); \ } while (0) static void ui_gdi_dump_hdc_fm(HDC hdc) { // https://en.wikipedia.org/wiki/Quad_(typography) // https://learn.microsoft.com/en-us/windows/win32/gdi/string-widths-and-heights // https://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font // Amazingly same since Windows 3.1 1992 // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-textmetrica // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-outlinetextmetrica TEXTMETRICA tm = {0}; rt_fatal_win32err(GetTextMetricsA(hdc, &tm)); char pitch[64] = { 0 }; if (tm.tmPitchAndFamily & TMPF_FIXED_PITCH) { strcat(pitch, "FIXED_PITCH "); } if (tm.tmPitchAndFamily & TMPF_VECTOR) { strcat(pitch, "VECTOR "); } if (tm.tmPitchAndFamily & TMPF_DEVICE) { strcat(pitch, "DEVICE "); } if (tm.tmPitchAndFamily & TMPF_TRUETYPE) { strcat(pitch, "TRUETYPE "); } rt_println("tm: .pitch_and_family: %s", pitch); rt_println(".height : %2d .ascent (baseline) : %2d .descent: %2d", tm.tmHeight, tm.tmAscent, tm.tmDescent); rt_println(".internal_leading : %2d .external_leading : %2d .ave_char_width: %2d", tm.tmInternalLeading, tm.tmExternalLeading, tm.tmAveCharWidth); rt_println(".max_char_width : %2d .weight : %2d .overhang: %2d", tm.tmMaxCharWidth, tm.tmWeight, tm.tmOverhang); rt_println(".digitized_aspect_x: %2d .digitized_aspect_y: %2d", tm.tmDigitizedAspectX, tm.tmDigitizedAspectY); rt_swear(tm.tmPitchAndFamily & TMPF_TRUETYPE); OUTLINETEXTMETRICA otm = { .otmSize = sizeof(OUTLINETEXTMETRICA) }; uint32_t bytes = GetOutlineTextMetricsA(hdc, otm.otmSize, &otm); rt_swear(bytes == sizeof(OUTLINETEXTMETRICA)); // unsupported XHeight CapEmHeight // ignored: MacDescent, MacLineGap, EMSquare, ItalicAngle // CharSlopeRise, CharSlopeRun, ItalicAngle rt_println("otm: .Ascent : %2d .Descent : %2d", otm.otmAscent, otm.otmDescent); rt_println(".otmLineGap : %2u", otm.otmLineGap); rt_println(".FontBox.ltrb : %d,%d %2d,%2d", otm.otmrcFontBox.left, otm.otmrcFontBox.top, otm.otmrcFontBox.right, otm.otmrcFontBox.bottom); rt_println(".MinimumPPEM : %2u (minimum height in pixels)", otm.otmusMinimumPPEM); rt_println(".SubscriptOffset : %d,%d .SubscriptSize.x : %dx%d", otm.otmptSubscriptOffset.x, otm.otmptSubscriptOffset.y, otm.otmptSubscriptSize.x, otm.otmptSubscriptSize.y); rt_println(".SuperscriptOffset : %d,%d .SuperscriptSize.x : %dx%d", otm.otmptSuperscriptOffset.x, otm.otmptSuperscriptOffset.y, otm.otmptSuperscriptSize.x, otm.otmptSuperscriptSize.y); rt_println(".UnderscoreSize : %2d .UnderscorePosition: %2d", otm.otmsUnderscoreSize, otm.otmsUnderscorePosition); rt_println(".StrikeoutSize : %2u .StrikeoutPosition : %2d ", otm.otmsStrikeoutSize, otm.otmsStrikeoutPosition); int32_t h = otm.otmAscent + abs(tm.tmDescent); // without diacritical space above fp32_t pts = (h * 72.0f) / GetDeviceCaps(hdc, LOGPIXELSY); rt_println("height: %.1fpt", pts); } static void ui_gdi_dump_fm(ui_font_t f) { rt_not_null(f); ui_gdi_hdc_with_font(f, { ui_gdi_dump_hdc_fm(hdc); }); } static void ui_gdi_get_fm(HDC hdc, ui_fm_t* fm) { TEXTMETRICA tm = {0}; rt_fatal_win32err(GetTextMetricsA(hdc, &tm)); rt_swear(tm.tmPitchAndFamily & TMPF_TRUETYPE); OUTLINETEXTMETRICA otm = { .otmSize = sizeof(OUTLINETEXTMETRICA) }; uint32_t bytes = GetOutlineTextMetricsA(hdc, otm.otmSize, &otm); rt_swear(bytes == sizeof(OUTLINETEXTMETRICA)); // "tm.tmAscent" The ascent (units above the base line) of characters // and actually is "baseline" in other terminology // "otm.otmAscent" The maximum distance characters in this font extend // above the base line. This is the typographic ascent for the font. // otm.otmEMSquare usually is 2048 which is size of rasterizer fm->height = tm.tmHeight; fm->baseline = tm.tmAscent; fm->ascent = otm.otmAscent; fm->descent = tm.tmDescent; fm->baseline = tm.tmAscent; fm->x_height = otm.otmsXHeight; fm->cap_em_height = otm.otmsCapEmHeight; fm->internal_leading = tm.tmInternalLeading; fm->external_leading = tm.tmExternalLeading; fm->average_char_width = tm.tmAveCharWidth; fm->max_char_width = tm.tmMaxCharWidth; fm->line_gap = otm.otmLineGap; fm->subscript.w = otm.otmptSubscriptSize.x; fm->subscript.h = otm.otmptSubscriptSize.y; fm->subscript_offset.x = otm.otmptSubscriptOffset.x; fm->subscript_offset.y = otm.otmptSubscriptOffset.y; fm->superscript.w = otm.otmptSuperscriptSize.x; fm->superscript.h = otm.otmptSuperscriptSize.y; fm->superscript_offset.x = otm.otmptSuperscriptOffset.x; fm->superscript_offset.y = otm.otmptSuperscriptOffset.y; fm->underscore = otm.otmsUnderscoreSize; fm->underscore_position = otm.otmsUnderscorePosition; fm->strike_through = otm.otmsStrikeoutSize; fm->strike_through_position = otm.otmsStrikeoutPosition; fm->design_units_per_em = (int)otm.otmEMSquare; fm->box = (ui_rect_t){ otm.otmrcFontBox.left, otm.otmrcFontBox.top, otm.otmrcFontBox.right - otm.otmrcFontBox.left, otm.otmrcFontBox.top - otm.otmrcFontBox.bottom // inverted }; // otm.Descent: The maximum distance characters in this font extend below // the base line. This is the typographic descent for the font. // Negative from the bottom (font.height) // tm.Descent: The descent (units below the base line) of characters. // Positive from the baseline down rt_assert(tm.tmDescent >= 0 && otm.otmDescent <= 0 && -otm.otmDescent <= tm.tmDescent, "tm.tmDescent: %d otm.otmDescent: %d", tm.tmDescent, otm.otmDescent); // "Mac" typography is ignored because it's usefulness is unclear. // Italic angle/slant/run is ignored because at the moment edit // view implementation does not support italics and thus does not // need it. Easy to add if necessary. } static void ui_gdi_update_fm(ui_fm_t* fm, ui_font_t f) { rt_not_null(f); SIZE em = {0, 0}; // "m" *fm = (ui_fm_t){ .font = f }; // ui_gdi.dump_fm(f); ui_gdi_hdc_with_font(f, { ui_gdi_get_fm(hdc, fm); // rt_glyph_nbsp and "M" have the same result rt_fatal_win32err(GetTextExtentPoint32A(hdc, "m", 1, &em)); SIZE vl = {0}; // "|" Vertical Line https://www.compart.com/en/unicode/U+007C rt_fatal_win32err(GetTextExtentPoint32A(hdc, "|", 1, &vl)); SIZE e3 = {0}; // Three-Em Dash rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_three_em_dash, 1, &e3)); fm->mono = em.cx == vl.cx && vl.cx == e3.cx; // rt_println("vl: %d %d", vl.cx, vl.cy); // rt_println("e3: %d %d", e3.cx, e3.cy); // rt_println("fm->mono: %d height: %d baseline: %d ascent: %d descent: %d", // fm->mono, fm->height, fm->baseline, fm->ascent, fm->descent); }); rt_assert(fm->baseline <= fm->height); fm->em = (ui_wh_t){ .w = fm->height, .h = fm->height }; // rt_println("fm.em: %dx%d", fm->em.w, fm->em.h); } static int32_t ui_gdi_draw_utf16(ui_font_t font, const char* s, int32_t n, RECT* r, uint32_t format) { // ~70 microsecond Core i-7 3667U 2.0 GHz (2012) // if font == null, draws on HDC with selected font if (0) { HDC hdc = ui_gdi_hdc(); if (hdc != null) { SIZE em = {0, 0}; // "M" rt_fatal_win32err(GetTextExtentPoint32A(hdc, "M", 1, &em)); rt_println("em: %d %d", em.cx, em.cy); rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_em_quad, 1, &em)); rt_println("em: %d %d", em.cx, em.cy); SIZE vl = {0}; // "|" Vertical Line https://www.compart.com/en/unicode/U+007C SIZE e3 = {0}; // Three-Em Dash rt_fatal_win32err(GetTextExtentPoint32A(hdc, "|", 1, &vl)); rt_println("vl: %d %d", vl.cx, vl.cy); rt_fatal_win32err(GetTextExtentPoint32A(hdc, rt_glyph_three_em_dash, 1, &e3)); rt_println("e3: %d %d", e3.cx, e3.cy); } } int32_t count = rt_str.utf16_chars(s, -1); rt_assert(0 < count && count < 4096, "be reasonable count: %d?", count); uint16_t ws[4096]; rt_swear(count <= rt_countof(ws), "find another way to draw!"); rt_str.utf8to16(ws, count, s, -1); int32_t h = 0; // return value is the height of the text if (font != null) { ui_gdi_hdc_with_font(font, { h = DrawTextW(hdc, ws, n, r, format); }); } else { // with already selected font ui_gdi_with_hdc({ h = DrawTextW(hdc, ws, n, r, format); }); } return h; } typedef struct { // draw text parameters const ui_fm_t* fm; const char* format; // format string va_list va; RECT rc; uint32_t flags; // flags: // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtextw // DT_CALCRECT DT_NOCLIP useful for measure // DT_END_ELLIPSIS useful for clipping // DT_LEFT, DT_RIGHT, DT_CENTER useful for paragraphs // DT_WORDBREAK is not good (GDI does not break nicely) // DT_BOTTOM, DT_VCENTER limited usability in weird cases (layout is better) // DT_NOPREFIX not to draw underline at "&Keyboard shortcuts // DT_SINGLELINE versus multiline } ui_gdi_dtp_t; static void ui_gdi_text_draw(ui_gdi_dtp_t* p) { rt_not_null(p); char text[4096]; // expected to be enough for single text draw text[0] = 0; rt_str.format_va(text, rt_countof(text), p->format, p->va); text[rt_countof(text) - 1] = 0; int32_t k = (int32_t)rt_str.len(text); if (k > 0) { rt_swear(k > 0 && k < rt_countof(text), "k=%d n=%d fmt=%s", k, p->format); // rectangle is always calculated - it makes draw text // much slower but UI layer is mostly uses bitmap caching: if ((p->flags & DT_CALCRECT) == 0) { // no actual drawing just calculate rectangle bool b = ui_gdi_draw_utf16(p->fm->font, text, -1, &p->rc, p->flags | DT_CALCRECT); rt_assert(b, "text_utf16(%s) failed", text); (void)b; } bool b = ui_gdi_draw_utf16(p->fm->font, text, -1, &p->rc, p->flags); rt_assert(b, "text_utf16(%s) failed", text); (void)b; } else { p->rc.right = p->rc.left; p->rc.bottom = p->rc.top + p->fm->height; } } enum { sl_draw = DT_LEFT|DT_NOCLIP|DT_SINGLELINE|DT_NOCLIP, sl_measure = sl_draw|DT_CALCRECT, ml_draw_break = DT_LEFT|DT_NOPREFIX|DT_NOCLIP|DT_NOFULLWIDTHCHARBREAK| DT_WORDBREAK, ml_measure_break = ml_draw_break|DT_CALCRECT, ml_draw = DT_LEFT|DT_NOPREFIX|DT_NOCLIP|DT_NOFULLWIDTHCHARBREAK, ml_measure = ml_draw|DT_CALCRECT }; static ui_wh_t ui_gdi_text_with_flags(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va, uint32_t flags) { const int32_t right = w == 0 ? 0 : x + w; ui_gdi_dtp_t p = { .fm = ta->fm, .format = format, .va = va, .rc = {.left = x, .top = y, .right = right, .bottom = 0 }, .flags = flags }; ui_color_t c = ta->color; if (!ta->measure) { if (ui_color_is_undefined(c)) { rt_swear(ta->color_id > 0); c = ui_colors.get_color(ta->color_id); } else { rt_swear(ta->color_id == 0); } c = ui_gdi_set_text_color(c); } ui_gdi_text_draw(&p); if (!ta->measure) { ui_gdi_set_text_color(c); } // restore color return (ui_wh_t){ p.rc.right - p.rc.left, p.rc.bottom - p.rc.top }; } static ui_wh_t ui_gdi_text_va(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, va_list va) { const uint32_t flags = sl_draw | (ta->measure ? sl_measure : 0); return ui_gdi_text_with_flags(ta, x, y, 0, format, va, flags); } static ui_wh_t ui_gdi_text(const ui_gdi_ta_t* ta, int32_t x, int32_t y, const char* format, ...) { const uint32_t flags = sl_draw | (ta->measure ? sl_measure : 0); va_list va; va_start(va, format); ui_wh_t wh = ui_gdi_text_with_flags(ta, x, y, 0, format, va, flags); va_end(va); return wh; } static ui_wh_t ui_gdi_multiline_va(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, va_list va) { const uint32_t flags = ta->measure ? (w <= 0 ? ml_measure : ml_measure_break) : (w <= 0 ? ml_draw : ml_draw_break); return ui_gdi_text_with_flags(ta, x, y, w, format, va, flags); } static ui_wh_t ui_gdi_multiline(const ui_gdi_ta_t* ta, int32_t x, int32_t y, int32_t w, const char* format, ...) { va_list va; va_start(va, format); ui_wh_t wh = ui_gdi_multiline_va(ta, x, y, w, format, va); va_end(va); return wh; } static ui_wh_t ui_gdi_glyphs_placement(const ui_gdi_ta_t* ta, const char* utf8, int32_t bytes, int32_t x[], int32_t glyphs) { rt_swear(bytes >= 0 && glyphs >= 0 && glyphs <= bytes); rt_assert(false, "Does not work for Tamil simplest utf8: \xe0\xae\x9a utf16: 0x0B9A"); x[0] = 0; ui_wh_t wh = { .w = 0, .h = 0 }; if (bytes > 0) { const int32_t chars = rt_str.utf16_chars(utf8, bytes); uint16_t* utf16 = rt_stackalloc((chars + 1) * sizeof(uint16_t)); uint16_t* output = rt_stackalloc((chars + 1) * sizeof(uint16_t)); const errno_t r = rt_str.utf8to16(utf16, chars, utf8, bytes); rt_swear(r == 0); // TODO: remove #if 1 char str[16 * 1024] = {0}; char hex[16 * 1024] = {0}; for (int i = 0; i < chars; i++) { rt_str_printf(hex, "%04X ", utf16[i]); strcat(str, hex); } rt_println("%.*s %s %p bytes:%d glyphs:%d font:%p hdc:%p", bytes, utf8, str, utf8, bytes, glyphs, ta->fm->font, ui_gdi_context.hdc); #endif GCP_RESULTSW gcp = { .lStructSize = sizeof(GCP_RESULTSW), .lpOutString = output, .nGlyphs = glyphs }; gcp.lpDx = (int*)rt_stackalloc((chars + 1) * sizeof(int)); DWORD n = 0; const int mx = INT32_MAX; // max extent const DWORD f = GCP_MAXEXTENT; // |GCP_GLYPHSHAPE|GCP_DIACRITIC|GCP_LIGATE if (ta->fm->font != null) { ui_gdi_hdc_with_font(ta->fm->font, { n = GetCharacterPlacementW(hdc, utf16, chars, mx, &gcp, f); }); } else { // with already selected font ui_gdi_with_hdc({ n = GetCharacterPlacementW(hdc, utf16, chars, mx, &gcp, f); }); } wh = (ui_wh_t){ .w = LOWORD(n), .h = HIWORD(n) }; if (n != 0) { // IS_HIGH_SURROGATE(wch) // IS_LOW_SURROGATE(wch) // IS_SURROGATE_PAIR(hs, ls) int32_t i = 0; int32_t k = 1; while (i < chars) { x[k] = x[k - 1] + gcp.lpDx[i]; // rt_println("%d", x[i]); k++; if (i < chars - 1 && rt_str.utf16_is_high_surrogate(utf16[i]) && rt_str.utf16_is_low_surrogate(utf16[i + 1])) { i += 2; } else { i++; } } rt_assert(k == glyphs + 1); } else { // rt_assert(false, "GetCharacterPlacementW() failed"); rt_println("GetCharacterPlacementW() failed"); } } return wh; } // to enable load_bitmap() function // 1. Add // curl.exe https://raw.githubusercontent.com/nothings/stb/master/stb_bitmap.h stb_bitmap.h // to the project precompile build step // 2. After // #define ui_implementation // include "ui/ui.h" // add // #define STBI_ASSERT(x) assert(x) // #define STB_bitmap_IMPLEMENTATION // #include "stb_bitmap.h" static uint8_t* ui_gdi_load_bitmap(const void* data, int32_t bytes, int* w, int* h, int* bytes_per_pixel, int32_t preferred_bytes_per_pixel) { #ifdef STBI_VERSION return stbi_load_from_memory((uint8_t const*)data, bytes, w, h, bytes_per_pixel, preferred_bytes_per_pixel); #else // see instructions above (void)data; (void)bytes; (void)data; (void)w; (void)h; (void)bytes_per_pixel; (void)preferred_bytes_per_pixel; rt_fatal_if(true, "curl.exe --silent --fail --create-dirs " "https://raw.githubusercontent.com/nothings/stb/master/stb_bitmap.h " "--output ext/stb_bitmap.h"); return null; #endif } static void ui_gdi_bitmap_dispose(ui_bitmap_t* image) { rt_fatal_win32err(DeleteBitmap(image->texture)); memset(image, 0, sizeof(ui_bitmap_t)); } ui_gdi_if ui_gdi = { .ta = { .prop = { .normal = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.normal, .measure = false }, .title = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.title, .measure = false }, .rubric = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.rubric, .measure = false }, .H1 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H1, .measure = false }, .H2 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H2, .measure = false }, .H3 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.prop.H3, .measure = false } }, .mono = { .normal = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.normal, .measure = false }, .title = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.title, .measure = false }, .rubric = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.rubric, .measure = false }, .H1 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H1, .measure = false }, .H2 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H2, .measure = false }, .H3 = { .color_id = ui_color_id_window_text, .color = ui_color_undefined, .fm = &ui_app.fm.mono.H3, .measure = false } }, }, .init = ui_gdi_init, .begin = ui_gdi_begin, .end = ui_gdi_end, .color_rgb = ui_gdi_color_rgb, .bitmap_init = ui_gdi_bitmap_init, .bitmap_init_rgbx = ui_gdi_bitmap_init_rgbx, .bitmap_dispose = ui_gdi_bitmap_dispose, .alpha = ui_gdi_alpha, .bitmap = ui_gdi_bitmap, .icon = ui_gdi_icon, .set_clip = ui_gdi_set_clip, .pixel = ui_gdi_pixel, .line = ui_gdi_line, .frame = ui_gdi_frame, .rect = ui_gdi_rect, .fill = ui_gdi_fill, .poly = ui_gdi_poly, .circle = ui_gdi_circle, .rounded = ui_gdi_rounded, .gradient = ui_gdi_gradient, .pixels = ui_gdi_pixels, .greyscale = ui_gdi_greyscale, .bgr = ui_gdi_bgr, .bgrx = ui_gdi_bgrx, .cleartype = ui_gdi_cleartype, .font_smoothing_contrast = ui_gdi_font_smoothing_contrast, .create_font = ui_gdi_create_font, .font = ui_gdi_font, .delete_font = ui_gdi_delete_font, .dump_fm = ui_gdi_dump_fm, .update_fm = ui_gdi_update_fm, .text_va = ui_gdi_text_va, .text = ui_gdi_text, .multiline_va = ui_gdi_multiline_va, .multiline = ui_gdi_multiline, .glyphs_placement = ui_gdi_glyphs_placement, .fini = ui_gdi_fini }; #pragma pop_macro("ui_gdi_hdc_with_font") #pragma pop_macro("ui_gdi_with_hdc") ================================================ FILE: src/ui/ui_image.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static fp64_t ui_image_scale_of(int32_t nominator, int32_t denominator) { const int32_t zn = 1 << (nominator - 1); const int32_t zd = 1 << (denominator - 1); return (fp64_t)zn / (fp64_t)zd; } static fp64_t ui_image_scale(ui_image_t* iv) { if (iv->fit && iv->w > 0 && iv->h > 0) { return min((fp64_t)iv->w / iv->image.w, (fp64_t)iv->h / iv->image.h); } else if (iv->fill && iv->w > 0 && iv->h > 0) { return max((fp64_t)iv->w / iv->image.w, (fp64_t)iv->h / iv->image.h); } else { return ui_image_scale_of(iv->zn, iv->zd); } } static ui_rect_t ui_image_position(ui_image_t* iv) { ui_rect_t rc = { 0, 0, 0, 0 }; if (iv->image.pixels != null) { int32_t iw = iv->image.w; int32_t ih = iv->image.h; // zoomed image width and height rc.w = (int32_t)((fp64_t)iw * ui_image.scale(iv)); rc.h = (int32_t)((fp64_t)ih * ui_image.scale(iv)); int32_t shift_x = (int32_t)((rc.w - iv->w) * iv->sx); int32_t shift_y = (int32_t)((rc.h - iv->h) * iv->sy); // shift_x and shift_y are in zoomed image coordinates rc.x = iv->x - shift_x; // screen x rc.y = iv->y - shift_y; // screen y } return rc; } static void ui_image_paint(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; // ui_gdi.fill(v->x, v->y, v->w, v->h, ui_colors.black); if (iv->image.pixels != null) { ui_gdi.set_clip(v->x, v->y, v->w, v->h); rt_swear(!iv->fit || !iv->fill, "make up your mind"); rt_swear(0 < iv->zn && iv->zn <= 16); rt_swear(0 < iv->zd && iv->zd <= 16); // only 1:2 and 2:1 etc are supported: if (iv->zn != 1) { rt_swear(iv->zd == 1); } if (iv->zd != 1) { rt_swear(iv->zn == 1); } const int32_t iw = iv->image.w; const int32_t ih = iv->image.h; ui_rect_t rc = ui_image_position(iv); if (iv->image.bpp == 1) { ui_gdi.greyscale(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else if (iv->image.bpp == 3) { ui_gdi.bgr(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else if (iv->image.bpp == 4) { if (iv->image.texture == null) { ui_gdi.bgrx(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, iw, ih, iv->image.stride, iv->image.pixels); } else { ui_gdi.alpha(rc.x, rc.y, rc.w, rc.h, 0, 0, iw, ih, &iv->image, iv->alpha); } } else { rt_swear(false, "unsupported .c: %d", iv->image.bpp); } if (ui_view.has_focus(v)) { ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); ui_gdi.frame(v->x, v->y, v->w, v->h, highlight); } ui_gdi.set_clip(0, 0, 0, 0); } } static void ui_image_tools_background(ui_view_t* v) { ui_color_t face = ui_colors.get_color(ui_color_id_button_face); ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); ui_gdi.fill(v->x, v->y, v->w, v->h, face); ui_gdi.frame(v->x, v->y, v->w, v->h, highlight); } static void ui_image_show_tools(ui_image_t* iv, bool show) { if (iv->focusable) { if (iv->tool.bar.state.hidden != !show) { iv->tool.bar.state.hidden = !show; iv->tool.bar.state.disabled = !show; iv->tool.ratio.state.hidden = !show; ui_app.request_layout(); } if (show) { // hide in 3.3 seconds: iv->when = rt_clock.seconds() + 3.3; } else { iv->when = 0; } } } static void ui_image_fit_fill_scale(ui_image_t* iv) { fp64_t s = ui_image.scale(iv); rt_assert(s != 0); if (s > 1) { ui_view.set_text(&iv->tool.ratio, "1:%.3f", s); } else if (s != 0 && s <= 1) { ui_view.set_text(&iv->tool.ratio, "%.3f:1", 1.0 / s); } else { // s should not be zero ever } } static void ui_image_measure(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (!v->focusable) { v->w = (int32_t)(iv->image.w * ui_image.scale(iv)); v->h = (int32_t)(iv->image.h * ui_image.scale(iv)); if (iv->fit || iv->fill) { ui_image_fit_fill_scale(iv); } } else { v->w = 0; v->h = 0; } } static void ui_image_layout(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (iv->fit || iv->fill) { ui_image_fit_fill_scale(iv); ui_view.measure_control(&iv->tool.ratio); } iv->tool.bar.x = v->x + v->w - iv->tool.bar.w; iv->tool.bar.y = v->y; iv->tool.ratio.x = v->x + v->w - iv->tool.ratio.w; iv->tool.ratio.y = v->y + v->h - iv->tool.ratio.h; } static void ui_image_every_100ms(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; if (iv->when != 0 && rt_clock.seconds() > iv->when) { ui_image_show_tools(iv, false); } } static void ui_image_focus_lost(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; ui_image_show_tools(iv, ui_view.has_focus(v)); } static void ui_image_focus_gained(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; ui_image_show_tools(iv, ui_view.has_focus(v)); } static void ui_image_zoomed(ui_image_t* iv) { iv->fill = false; iv->fit = false; // 0=16:1 1=8:1 2=4:1 3=2:1 4=1:1 5=1:2 6=1:4 7=1:8 8=1:16 int32_t n = iv->zoom - 4; int32_t zn = iv->zn; int32_t zd = iv->zd; fp64_t scale_before = ui_image.scale(iv); if (n > 0) { zn = n + 1; zd = 1; } else if (n < 0) { zn = 1; zd = -n + 1; } else if (n == 0) { zn = 1; zd = 1; } fp64_t scale_after = ui_image_scale_of(zn, zd); if (scale_after != scale_before) { iv->zn = zn; iv->zd = zd; const int32_t nm = 1 << (iv->zn - 1); const int32_t dm = 1 << (iv->zd - 1); ui_view.set_text(&iv->tool.ratio, "%d:%d", nm, dm); } if (iv->zn == 1) { iv->zoom = 4 - (iv->zd - 1); } else if (iv->zd == 1) { iv->zoom = 4 + (iv->zn - 1); } else { rt_swear(false); } // is whole image visible? fp64_t s = ui_image.scale(iv); bool whole = (int32_t)(iv->image.w * s) <= iv->w && (int32_t)(iv->image.h * s) <= iv->h; if (whole) { iv->sx = 0.5; iv->sy = 0.5; } ui_view.invalidate(&iv->view, null); ui_image_show_tools(iv, true); } static void ui_image_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { fp64_t dx = (fp64_t)dx_dy.x; fp64_t dy = (fp64_t)dx_dy.y; ui_image_t* iv = (ui_image_t*)v; if (ui_view.has_focus(v)) { fp64_t s = ui_image.scale(iv); if (iv->image.w * s > iv->w || iv->image.h * s > iv->h) { iv->sx = max(0.0, min(iv->sx + dx / iv->image.w, 1.0)); } else { iv->sx = 0.5; } if (iv->image.h * s > iv->h) { iv->sy = max(0.0, min(iv->sy + dy / iv->image.h, 1.0)); } else { iv->sy = 0.5; } ui_view.invalidate(&iv->view, null); } } static bool ui_image_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; if (v->focusable) { ui_image_t* iv = (ui_image_t*)v; const int32_t x = ui_app.mouse.x - iv->x; const int32_t y = ui_app.mouse.y - iv->y; bool tools = !iv->tool.bar.state.hidden && ui_view.inside(&iv->tool.bar, &ui_app.mouse); bool inside = ui_view.inside(&iv->view, &ui_app.mouse) && !tools; bool left = ix == 0; bool drag_started = iv->drag_start.x >= 0 && iv->drag_start.y >= 0; if (left && inside && !drag_started) { iv->drag_start = (ui_point_t){x, y}; } if (!pressed) { iv->drag_start = (ui_point_t){-1, -1}; } swallow = inside || tools; } // rt_println("inside %s", inside ? "true" : "false"); return swallow; } static bool ui_image_mouse_move(ui_view_t* v) { ui_image_t* iv = (ui_image_t*)v; bool drag_started = iv->drag_start.x >= 0 && iv->drag_start.y >= 0; bool tools = !iv->tool.bar.state.hidden && ui_view.inside(&iv->tool.bar, &ui_app.mouse); bool inside = ui_view.inside(&iv->view, &ui_app.mouse) && !tools; if (drag_started && inside) { ui_image_show_tools(iv, false); const int32_t x = ui_app.mouse.x - iv->x; const int32_t y = ui_app.mouse.y - iv->y; ui_point_t dx_dy = {iv->drag_start.x - x, iv->drag_start.y - y}; ui_image_mouse_scroll(v, dx_dy); iv->drag_start = (ui_point_t){x, y}; } else if (inside) { ui_image_show_tools(iv, true); } else if (!inside && !tools) { ui_image_show_tools(iv, false); } // rt_println("inside %s", inside ? "true" : "false"); return inside; } static bool ui_image_key_pressed(ui_view_t* v, int64_t vk) { ui_image_t* iv = (ui_image_t*)v; bool swallowed = false; if (ui_view.has_focus(v)) { swallowed = true; if (vk == ui.key.up) { ui_image_mouse_scroll(v, (ui_point_t){0, -iv->h / 8}); } else if (vk == ui.key.down) { ui_image_mouse_scroll(v, (ui_point_t){0, +iv->h / 8}); } else if (vk == ui.key.left) { ui_image_mouse_scroll(v, (ui_point_t){-iv->w / 8, 0}); } else if (vk == ui.key.right) { ui_image_mouse_scroll(v, (ui_point_t){+iv->w / 8, 0}); } else if (vk == ui.key.plus) { if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } else if (vk == ui.key.minus) { if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } else { swallowed = false; } } return swallowed; } static void ui_image_zoom_in(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } static void ui_image_zoom_out(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } static void ui_image_fit(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->fit = true; iv->fill = false; ui_image_fit_fill_scale(iv); ui_view.invalidate(&iv->view, null); } static void ui_image_fill(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->fill = true; iv->fit = false; ui_image_fit_fill_scale(iv); ui_view.invalidate(&iv->view, null); } static void ui_image_zoom_1t1(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; iv->zoom = 4; ui_image_zoomed(iv); } static ui_label_t ui_image_about = ui_label(0, "Keyboard shortcuts:\n\n" "Ctrl+C copies image to the clipboard.\n\n" rt_glyph_heavy_plus_sign " zoom in; " rt_glyph_heavy_minus_sign " zoom out;\n" rt_glyph_open_circle_arrows_one_overlay " 1:1.\n\n" rt_glyph_up_down_arrow " Fit;\n" rt_glyph_left_right_arrow " Fill.\n\n" "Left/Right Arrows " rt_glyph_leftward_arrow rt_glyph_rightwards_arrow "Up/Down Arrows " rt_glyph_upwards_arrow rt_glyph_downwards_arrow "\npans the image inside view.\n\n" "Mouse wheel or mouse / touchpad hold and drag to pan.\n" ); static void ui_image_help(ui_button_t* rt_unused(b)) { ui_app.show_toast(&ui_image_about, 7.0); } static void ui_image_copy_to_clipboard(ui_image_t* iv) { ui_bitmap_t image = {0}; if (iv->image.texture != null) { rt_clipboard.put_image(&iv->image); } else { ui_gdi.bitmap_init(&image, iv->image.w, iv->image.h, iv->image.bpp, iv->image.pixels); rt_clipboard.put_image(&image); ui_gdi.bitmap_dispose(&image); } static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); ui_app.show_hint(&hint, ui_app.mouse.x, ui_app.mouse.y + iv->fm->height, 1.5); } static void ui_image_copy(ui_button_t* b) { ui_image_t* iv = (ui_image_t*)b->that; ui_image_copy_to_clipboard(iv); } static void ui_image_character(ui_view_t* v, const char* utf8) { ui_image_t* iv = (ui_image_t*)v; if (ui_view.has_focus(v)) { // && ui_app.ctrl ? char ch = utf8[0]; if (ch == '+' || ch == '=') { if (iv->zoom < 8) { iv->zoom++; ui_image_zoomed(iv); } } else if (ch == '-' || ch == '_') { if (iv->zoom > 0) { iv->zoom--; ui_image_zoomed(iv); } } else if (ch == '<' || ch == ',') { ui_image_mouse_scroll(v, (ui_point_t){-iv->w / 8, 0}); } else if (ch == '>' || ch == '.') { ui_image_mouse_scroll(v, (ui_point_t){+iv->w / 8, 0}); } else if (ch == '0') { iv->zoom = 4; ui_image_zoomed(iv); } else if (ch == 3 && iv->image.pixels != null) { // Ctrl+C ui_image_copy_to_clipboard(iv); } } } static void ui_image_add_button(ui_image_t* iv, ui_button_t* b, const char* label, void (*cb)(ui_button_t* b), const char* hint) { *b = (ui_button_t)ui_button("", 0.0f, cb); ui_view.set_text(b, label); b->that = iv; b->insets.top = 0; b->insets.bottom = 0; b->padding.top = 0; b->padding.bottom = 0; b->insets = (ui_margins_t){0}; b->padding = (ui_margins_t){0}; b->flat = true; b->fm = &ui_app.fm.mono.normal; b->min_w_em = 1.5f; rt_str_printf(b->hint, "%s", hint); ui_view.add_last(&iv->tool.bar, b); } void ui_image_init(ui_image_t* iv) { memset(iv, 0x00, sizeof(*iv)); iv->type = ui_view_image; iv->paint = ui_image_paint; iv->tap = ui_image_tap; iv->mouse_move = ui_image_mouse_move; iv->measure = ui_image_measure; iv->layout = ui_image_layout; iv->every_100ms = ui_image_every_100ms; iv->focus_lost = ui_image_focus_lost; iv->focus_gained = ui_image_focus_gained; iv->mouse_scroll = ui_image_mouse_scroll; iv->character = ui_image_character; iv->key_pressed = ui_image_key_pressed; iv->fm = &ui_app.fm.prop.normal; iv->tool.bar = (ui_view_t)ui_view(span); // buttons: ui_image_add_button(iv, &iv->tool.copy, "\xF0\x9F\x93\x8B", ui_image_copy, "Copy to Clipboard Ctrl+C"); ui_image_add_button(iv, &iv->tool.zoom_out, rt_glyph_heavy_minus_sign, ui_image_zoom_out, "Zoom Out"); ui_image_add_button(iv, &iv->tool.zoom_1t1, rt_glyph_open_circle_arrows_one_overlay, ui_image_zoom_1t1, "Reset to 1:1"); ui_image_add_button(iv, &iv->tool.zoom_in, rt_glyph_heavy_plus_sign, ui_image_zoom_in, "Zoom In"); ui_image_add_button(iv, &iv->tool.fit, rt_glyph_up_down_arrow, ui_image_fit, "Fit"); ui_image_add_button(iv, &iv->tool.fill, rt_glyph_left_right_arrow, ui_image_fill, "Fill"); ui_image_add_button(iv, &iv->tool.help, "?", ui_image_help, "Help"); iv->tool.zoom_1t1.min_w_em = 1.25f; iv->tool.ratio = (ui_label_t)ui_label(0, "1:1"); iv->tool.ratio.color = ui_colors.get_color(ui_color_id_highlight); iv->tool.ratio.color_id = ui_color_id_highlight; ui_view.add_last(&iv->view, &iv->tool.bar); ui_view.add_last(&iv->view, &iv->tool.ratio); iv->tool.bar.state.hidden = true; iv->tool.ratio.state.hidden = true; iv->tool.bar.erase = ui_image_tools_background; iv->tool.ratio.erase = ui_image_tools_background; iv->zoom = 4; iv->zn = 1; iv->zd = 1; iv->sx = 0.5; iv->sy = 0.5; iv->drag_start = (ui_point_t){-1, -1}; iv->debug.id = "#image"; } void ui_image_init_with(ui_image_t* iv, const uint8_t* pixels, int32_t w, int32_t h, int32_t c, int32_t s) { ui_image_init(iv); iv->image.pixels = (uint8_t*)pixels; iv->image.w = w; iv->image.h = h; iv->image.bpp = c; iv->image.stride = s; } static void ui_image_ratio(ui_image_t* iv, int32_t zn, int32_t zd) { rt_swear(0 < zn && zn <= 16); rt_swear(0 < zd && zd <= 16); // only 1:2 and 2:1 etc are supported: if (zn != 1) { rt_swear(zd == 1); } if (zd != 1) { rt_swear(zn == 1); } iv->zn = zn; iv->zd = zd; iv->fit = false; iv->fill = false; } ui_image_if ui_image = { .init = ui_image_init, .init_with = ui_image_init_with, .ratio = ui_image_ratio, .scale = ui_image_scale, .position = ui_image_position }; ================================================ FILE: src/ui/ui_label.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static void ui_label_paint(ui_view_t* v) { rt_assert(v->type == ui_view_label); rt_assert(!ui_view.is_hidden(v)); const char* s = ui_view.string(v); ui_color_t c = v->state.hover && v->highlightable ? ui_colors.interpolate(v->color, ui_colors.blue, 1.0f / 8.0f) : v->color; const int32_t tx = v->x + v->text.xy.x; const int32_t ty = v->y + v->text.xy.y; const ui_gdi_ta_t ta = { .fm = v->fm, .color = c }; const bool multiline = strchr(s, '\n') != null; if (multiline) { int32_t w = (int32_t)((fp64_t)v->min_w_em * (fp64_t)v->fm->em.w + 0.5); ui_gdi.multiline(&ta, tx, ty, w, "%s", ui_view.string(v)); } else { ui_gdi.text(&ta, tx, ty, "%s", ui_view.string(v)); } if (v->state.hover && !v->flat && v->highlightable) { ui_color_t highlight = ui_colors.get_color(ui_color_id_highlight); int32_t radius = (v->fm->em.h / 4) | 0x1; // corner radius int32_t h = multiline ? v->h : v->fm->baseline + v->fm->descent; ui_gdi.rounded(v->x - radius, v->y, v->w + 2 * radius, h, radius, highlight, ui_colors.transparent); } } static bool ui_label_context_menu(ui_view_t* v) { rt_assert(!ui_view.is_hidden(v) && !ui_view.is_disabled(v)); const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { rt_clipboard.put_text(ui_view.string(v)); static ui_label_t hint = ui_label(0.0f, "copied to clipboard"); int32_t x = v->x + v->w / 2; int32_t y = v->y + v->h; ui_app.show_hint(&hint, x, y, 0.75); } return inside; } static void ui_label_character(ui_view_t* v, const char* utf8) { rt_assert(v->type == ui_view_label); if (v->state.hover && !ui_view.is_hidden(v)) { char ch = utf8[0]; // Copy to clipboard works for hover over text if ((ch == 3 || ch == 'c' || ch == 'C') && ui_app.ctrl) { rt_clipboard.put_text(ui_view.string(v)); // 3 is ASCII for Ctrl+C } } } void ui_view_init_label(ui_view_t* v) { rt_assert(v->type == ui_view_label); v->paint = ui_label_paint; v->character = ui_label_character; v->context_menu = ui_label_context_menu; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; v->text_align = ui.align.left; } void ui_label_init_va(ui_label_t* v, fp32_t min_w_em, const char* format, va_list va) { ui_view.set_text(v, format, va); v->min_w_em = min_w_em; v->type = ui_view_label; ui_view_init_label(v); } void ui_label_init(ui_label_t* v, fp32_t min_w_em, const char* format, ...) { va_list va; va_start(va, format); ui_label_init_va(v, min_w_em, format, va); va_end(va); } ================================================ FILE: src/ui/ui_mbx.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static void ui_mbx_button(ui_button_t* b) { ui_mbx_t* m = (ui_mbx_t*)b->parent; rt_assert(m->type == ui_view_mbx); m->option = -1; for (int32_t i = 0; i < rt_countof(m->button) && m->option < 0; i++) { if (b == &m->button[i]) { m->option = i; if (m->callback != null) { m->callback(&m->view); // need to disarm button because message box about to close b->state.pressed = false; b->state.armed = false; } } } ui_app.show_toast(null, 0); } static void ui_mbx_measured(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; int32_t n = 0; ui_view_for_each(v, c, { n++; }); n--; // number of buttons const int32_t em_x = m->label.fm->em.w; const int32_t em_y = m->label.fm->em.h; const int32_t tw = m->label.w; const int32_t th = m->label.h; if (n > 0) { int32_t bw = 0; for (int32_t i = 0; i < n; i++) { bw += m->button[i].w; } v->w = rt_max(tw, bw + em_x * 2); v->h = th + m->button[0].h + em_y + em_y / 2; } else { v->h = th + em_y / 2; v->w = tw; } } static void ui_mbx_layout(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; int32_t n = 0; ui_view_for_each(v, c, { n++; }); n--; // number of buttons const int32_t em_y = m->label.fm->em.h; m->label.x = v->x; m->label.y = v->y + em_y * 2 / 3; const int32_t tw = m->label.w; const int32_t th = m->label.h; if (n > 0) { int32_t bw = 0; for (int32_t i = 0; i < n; i++) { bw += m->button[i].w; } // center text: m->label.x = v->x + (v->w - tw) / 2; // spacing between buttons: int32_t sp = (v->w - bw) / (n + 1); int32_t x = sp; for (int32_t i = 0; i < n; i++) { m->button[i].x = v->x + x; m->button[i].y = v->y + th + em_y * 3 / 2; x += m->button[i].w + sp; } } } void ui_view_init_mbx(ui_view_t* v) { ui_mbx_t* m = (ui_mbx_t*)v; v->measured = ui_mbx_measured; v->layout = ui_mbx_layout; m->fm = &ui_app.fm.prop.normal; int32_t n = 0; while (m->options[n] != null && n < rt_countof(m->button) - 1) { m->button[n] = (ui_button_t)ui_button("", 6.0, ui_mbx_button); ui_view.set_text(&m->button[n], "%s", m->options[n]); n++; } rt_swear(n <= rt_countof(m->button), "inhumane: %d buttons is too many", n); if (n > rt_countof(m->button)) { n = rt_countof(m->button); } m->label = (ui_label_t)ui_label(0, ""); ui_view.set_text(&m->label, "%s", ui_view.string(&m->view)); ui_view.add_last(&m->view, &m->label); for (int32_t i = 0; i < n; i++) { ui_view.add_last(&m->view, &m->button[i]); m->button[i].fm = m->fm; } m->label.fm = m->fm; ui_view.set_text(&m->view, ""); m->option = -1; if (m->debug.id == null) { m->debug.id = "#mbx"; } } void ui_mbx_init(ui_mbx_t* m, const char* options[], const char* format, ...) { m->type = ui_view_mbx; m->measured = ui_mbx_measured; m->layout = ui_mbx_layout; m->color_id = ui_color_id_window; m->options = options; m->focusable = true; va_list va; va_start(va, format); ui_view.set_text_va(&m->view, format, va); ui_label_init(&m->label, 0.0, ui_view.string(&m->view)); va_end(va); ui_view_init_mbx(&m->view); } ================================================ FILE: src/ui/ui_midi.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "rt/rt_win32.h" #include "ui/ui.h" #include #pragma comment(lib, "winmm") typedef struct ui_midi_s_ { MCI_OPEN_PARMSA mop; // opaque ui_app_message_handler_t handler; char alias[32]; int64_t device_id; uintptr_t window; bool playing; } ui_midi_t_; rt_static_assertion(sizeof(ui_midi_t) >= sizeof(ui_midi_t_) + sizeof(void*)); rt_static_assertion(MMSYSERR_NOERROR == 0); static void ui_midi_error(errno_t r, char* text, int32_t count) { rt_fatal_win32err(mciGetErrorStringA(r, text, (UINT)count)); } static void ui_midi_warn_if_error_(int r, const char* call, const char* func, int line) { if (r != 0) { static char error[256]; ui_midi_error(r, error, rt_countof(error)); rt_println("%s:%d %s", func, line, call); rt_println("%d - MCIERR_BASE: %d %s", r, r - MCIERR_BASE, error); } } #define ui_midi_warn_if_error(r) do { \ ui_midi_warn_if_error_(r, #r, __func__, __LINE__); \ } while (0) #define ui_midi_fatal_if_error(call) do { \ int _r_ = call; ui_midi_warn_if_error_(r, #call, __func__, __LINE__); \ rt_fatal_if_error(r); \ } while (0) static bool ui_midi_message_callback(ui_app_message_handler_t* h, int32_t m, int64_t wp, int64_t lp, int64_t* rt) { if (m == MM_MCINOTIFY) { #ifdef UI_MIDI_DEBUG rt_println("device_id: %lld", lp); if (wp & MCI_NOTIFY_SUCCESSFUL) { rt_println("SUCCESSFUL"); } if (wp & MCI_NOTIFY_SUPERSEDED) { rt_println("SUPERSEDED"); } if (wp & MCI_NOTIFY_ABORTED) { rt_println("ABORTED"); } if (wp & MCI_NOTIFY_FAILURE) { rt_println("FAILURE"); } #endif ui_midi_t* midi = (ui_midi_t*)h->that; ui_midi_t_* mi = (ui_midi_t_*)midi; if (mi->device_id == lp) { if (midi->notify != null) { *rt = midi->notify(midi, wp); } else { *rt = 0; } return true; } } return false; } static void ui_midi_remove_handler(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; ui_app_message_handler_t* h = ui_app.handlers; if (h == &mi->handler) { ui_app.handlers = h->next; } else { while (h->next != null && h->next != &mi->handler) { h = h->next; } rt_swear(h->next == &mi->handler); if (h->next == &mi->handler) { h->next = h->next->next; } } mi->handler.callback = null; mi->handler.that = null; mi->handler.next = null; } static errno_t ui_midi_open(ui_midi_t* m, const char* filename) { rt_swear(rt_thread.id() == ui_app.tid); ui_midi_t_* mi = (ui_midi_t_*)m; mi->handler.that = mi; mi->handler.next = ui_app.handlers; ui_app.handlers = &mi->handler; mi->window = (uintptr_t)ui_app.window; mi->playing = false; mi->mop.dwCallback = mi->window; mi->mop.wDeviceID = (WORD)-1; mi->mop.lpstrDeviceType = (const char*)MCI_DEVTYPE_SEQUENCER; mi->mop.lpstrElementName = filename; mi->mop.lpstrAlias = mi->alias; rt_str_printf(mi->alias, "%p", m); const DWORD_PTR flags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT | MCI_OPEN_ALIAS; errno_t r = mciSendCommandA(0, MCI_OPEN, flags, (uintptr_t)&mi->mop); ui_midi_warn_if_error(r); rt_assert(mi->mop.wDeviceID != -1); mi->handler.callback = ui_midi_message_callback, mi->device_id = mi->mop.wDeviceID; if (r != 0) { ui_midi_remove_handler(m); memset(&mi->mop, 0x00, sizeof(mi->mop)); mi->window = 0; } return r; } static errno_t ui_midi_play(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); ui_midi_t_* mi = (ui_midi_t_*)m; rt_swear(ui_midi.is_open(m)); MCI_PLAY_PARMS pp = { .dwCallback = (uintptr_t)mi->window }; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_PLAY, MCI_NOTIFY, (uintptr_t)&pp); ui_midi_warn_if_error(r); if (r == 0) { mi->playing = true; } return r; } static errno_t ui_midi_rewind(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m)); ui_midi_t_* mi = (ui_midi_t_*)m; MCI_SEEK_PARMS p = { .dwCallback = (uintptr_t)mi->window, .dwTo = 0 }; const DWORD f = MCI_WAIT|MCI_SEEK_TO_START; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_SEEK, f, (DWORD_PTR)&p); ui_midi_warn_if_error(r); return r; } static errno_t ui_midi_get_volume(ui_midi_t* m, fp64_t* volume) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); DWORD v = 0; errno_t r = midiOutGetVolume((HMIDIOUT)0, &v); ui_midi_warn_if_error(r); *volume = (fp64_t)v / (fp64_t)0xFFFFFFFFU; return 0; } static errno_t ui_midi_set_volume(ui_midi_t* m, fp64_t volume) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); DWORD v = (DWORD)(volume * (fp64_t)0xFFFFFFFFU); const UINT n = midiOutGetNumDevs(); // Handle to a MIDI Output Device HMIDIOUT h = (HMIDIOUT)(uintptr_t)(n - 1); errno_t r = n == 0 ? MCIERR_DEVICE_NOT_INSTALLED : midiOutSetVolume(h, v); ui_midi_warn_if_error(r); rt_fatal_if_error(r); return r; } static errno_t ui_midi_stop(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && ui_midi.is_playing(m)); ui_midi_t_* mi = (ui_midi_t_*)m; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_STOP, 0, 0); ui_midi_warn_if_error(r); if (r == 0) { mi->playing = false; } return r; } static void ui_midi_close(ui_midi_t* m) { rt_swear(rt_thread.id() == ui_app.tid); rt_swear(ui_midi.is_open(m) && !ui_midi.is_playing(m)); ui_midi_t_* mi = (ui_midi_t_*)m; errno_t r = mciSendCommandA(mi->mop.wDeviceID, MCI_CLOSE, MCI_WAIT, 0); ui_midi_warn_if_error(r); r = mciSendCommandA(MCI_ALL_DEVICE_ID, MCI_CLOSE, MCI_WAIT, 0); ui_midi_warn_if_error(r); rt_fatal_if_error(r, "sound card is unplugged on the fly?"); memset(&mi->mop, 0x00, sizeof(mi->mop)); mi->window = 0; ui_midi_remove_handler(m); } static bool ui_midi_is_open(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; return mi->window != 0; } static bool ui_midi_is_playing(ui_midi_t* m) { ui_midi_t_* mi = (ui_midi_t_*)m; return mi->playing; } ui_midi_if ui_midi = { .success = MCI_NOTIFY_SUCCESSFUL, .failure = MCI_NOTIFY_FAILURE, .aborted = MCI_NOTIFY_ABORTED, .superseded = MCI_NOTIFY_SUPERSEDED, .error = ui_midi_error, .open = ui_midi_open, .play = ui_midi_play, .rewind = ui_midi_rewind, .get_volume = ui_midi_get_volume, .set_volume = ui_midi_set_volume, .stop = ui_midi_stop, .is_open = ui_midi_is_open, .is_playing = ui_midi_is_playing, .close = ui_midi_close }; ================================================ FILE: src/ui/ui_slider.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static void ui_slider_invalidate(const ui_slider_t* s) { const ui_view_t* v = &s->view; ui_view.invalidate(v, null); if (!s->dec.state.hidden) { ui_view.invalidate(&s->dec, null); } if (!s->inc.state.hidden) { ui_view.invalidate(&s->dec, null); } } static int32_t ui_slider_width(const ui_slider_t* s) { const ui_ltrb_t i = ui_view.margins(&s->view, &s->insets); int32_t w = s->w - i.left - i.right; if (!s->dec.state.hidden) { const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const ui_ltrb_t inc_p = ui_view.margins(&s->inc, &s->inc.padding); w -= s->dec.w + s->inc.w + dec_p.right + inc_p.left; } return w; } static ui_wh_t measure_text(const ui_fm_t* fm, const char* format, ...) { va_list va; va_start(va, format); const ui_gdi_ta_t ta = { .fm = fm, .color = ui_colors.white, .measure = true }; ui_wh_t wh = ui_gdi.text_va(&ta, 0, 0, format, va); va_end(va); return wh; } static ui_wh_t ui_slider_measure_text(ui_slider_t* s) { char formatted[rt_countof(s->p.text)]; const ui_fm_t* fm = s->fm; const char* text = ui_view.string(&s->view); const ui_ltrb_t i = ui_view.margins(&s->view, &s->insets); ui_wh_t wh = s->fm->em; if (s->debug.trace.mt) { const ui_ltrb_t p = ui_view.margins(&s->view, &s->padding); rt_println(">%dx%d em: %dx%d min: %.1fx%.1f " "i: %d %d %d %d p: %d %d %d %d \"%.*s\"", s->w, s->h, fm->em.w, fm->em.h, s->min_w_em, s->min_h_em, i.left, i.top, i.right, i.bottom, p.left, p.top, p.right, p.bottom, rt_min(64, strlen(text)), text); const ui_margins_t in = s->insets; const ui_margins_t pd = s->padding; rt_println(" i: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f" " p: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f", in.left, in.top, in.right, in.bottom, in.left + in.right, in.top + in.bottom, pd.left, pd.top, pd.right, pd.bottom, pd.left + pd.right, pd.top + pd.bottom); } if (s->format != null) { s->format(&s->view); rt_str_printf(formatted, "%s", text); wh = measure_text(s->fm, "%s", formatted); // TODO: format string 0x08X? } else if (text != null && (strstr(text, "%d") != null || strstr(text, "%u") != null)) { ui_wh_t mt_min = measure_text(s->fm, text, s->value_min); ui_wh_t mt_max = measure_text(s->fm, text, s->value_max); ui_wh_t mt_val = measure_text(s->fm, text, s->value); wh.h = rt_max(mt_val.h, rt_max(mt_min.h, mt_max.h)); wh.w = rt_max(mt_val.w, rt_max(mt_min.w, mt_max.w)); } else if (text != null && text[0] != 0) { wh = measure_text(s->fm, "%s", text); } if (s->debug.trace.mt) { rt_println(" mt: %dx%d", wh.w, wh.h); } return wh; } static void ui_slider_measure(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); // slider cannot be smaller than 2*em const fp32_t min_w_em = rt_max(2.0f, v->min_w_em); v->w = (int32_t)((fp64_t)fm->em.w * (fp64_t) min_w_em + 0.5); v->h = (int32_t)((fp64_t)fm->em.h * (fp64_t)v->min_h_em + 0.5); // dec and inc have same font metrics as a slider: s->dec.fm = fm; s->inc.fm = fm; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "not the same"); ui_view.measure_control(v); // s->text.mt = ui_slider_measure_text(s); if (s->dec.state.hidden) { v->w = rt_max(v->w, i.left + s->wh.w + i.right); } else { ui_view.measure(&s->dec); // remeasure with inherited metrics ui_view.measure(&s->inc); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const ui_ltrb_t inc_p = ui_view.margins(&s->inc, &s->inc.padding); v->w = rt_max(v->w, s->dec.w + dec_p.right + s->wh.w + inc_p.left + s->inc.w); } v->h = rt_max(v->h, i.top + fm->em.h + i.bottom); if (s->debug.trace.mt) { rt_println("<%dx%d", s->w, s->h); } } static void ui_slider_layout(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; // disregard inc/dec .state.hidden bit for layout: const ui_ltrb_t i = ui_view.margins(v, &v->insets); s->dec.x = v->x + i.left; s->dec.y = v->y; s->inc.x = v->x + v->w - i.right - s->inc.w; s->inc.y = v->y; } static void ui_slider_paint(ui_view_t* v) { rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); // dec button is sticking to the left into slider padding const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "hidden or not together"); const int32_t dx = s->dec.state.hidden ? 0 : dec_w; const int32_t x = v->x + dx + i.left; const int32_t w = ui_slider_width(s); // draw background: fp32_t d = ui_theme.is_app_dark() ? 0.50f : 0.25f; ui_color_t d0 = ui_colors.darken(v->background, d); d /= 4; ui_color_t d1 = ui_colors.darken(v->background, d); ui_gdi.gradient(x, v->y, w, v->h, d1, d0, true); // draw value: ui_color_t c = ui_theme.is_app_dark() ? ui_colors.darken(ui_colors.green, 1.0f / 128.0f) : ui_colors.jungle_green; d1 = c; d0 = ui_colors.darken(c, 1.0f / 64.0f); const fp64_t range = (fp64_t)s->value_max - (fp64_t)s->value_min; rt_assert(range > 0, "range: %.6f", range); const fp64_t vw = (fp64_t)w * (s->value - s->value_min) / range; const int32_t wi = (int32_t)(vw + 0.5); ui_gdi.gradient(x, v->y, wi, v->h, d1, d0, true); if (!v->flat) { ui_color_t color = v->state.hover ? ui_colors.get_color(ui_color_id_hot_tracking) : ui_colors.get_color(ui_color_id_gray_text); if (ui_view.is_disabled(v)) { color = ui_color_rgb(30, 30, 30); } // TODO: hardcoded ui_gdi.frame(x, v->y, w, v->h, color); } // text: const char* text = ui_view.string(v); char formatted[rt_countof(v->p.text)]; if (s->format != null) { s->format(v); s->p.strid = 0; // nls again text = ui_view.string(v); } else if (text != null && (strstr(text, "%d") != null || strstr(text, "%u") != null)) { rt_str.format(formatted, rt_countof(formatted), text, s->value); s->p.strid = 0; // nls again text = rt_nls.str(formatted); } // because current value was formatted into `text` need to // remeasure and align text again: ui_view.text_measure(v, text, &v->text); ui_view.text_align(v, &v->text); const ui_color_t text_color = !v->state.hover ? v->color : (ui_theme.is_app_dark() ? ui_colors.white : ui_colors.black); const ui_gdi_ta_t ta = { .fm = fm, .color = text_color }; ui_gdi.text(&ta, v->x + v->text.xy.x, v->y + v->text.xy.y, "%s", text); } static bool ui_slider_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { if (pressed) { ui_slider_t* s = (ui_slider_t*)v; const ui_ltrb_t i = ui_view.margins(v, &v->insets); const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, "hidden or not together"); const int32_t sw = ui_slider_width(s); // slider width const int32_t dx = s->dec.state.hidden ? 0 : dec_w + dec_p.right; const int32_t vx = v->x + i.left + dx; const int32_t x = ui_app.mouse.x - vx; const int32_t y = ui_app.mouse.y - (v->y + i.top); if (0 <= x && x < sw && 0 <= y && y < v->h) { const fp64_t range = (fp64_t)s->value_max - (fp64_t)s->value_min; fp64_t val = (fp64_t)x * range / (fp64_t)(sw - 1); int32_t vw = (int32_t)(val + s->value_min + 0.5); s->value = rt_min(rt_max(vw, s->value_min), s->value_max); if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } return pressed && inside; // swallow inside clicks } static void ui_slider_mouse_move(ui_view_t* v) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { const ui_ltrb_t i = ui_view.margins(v, &v->insets); ui_slider_t* s = (ui_slider_t*)v; bool drag = ui_app.mouse_left || ui_app.mouse_right; if (drag) { const ui_ltrb_t dec_p = ui_view.margins(&s->dec, &s->dec.padding); const int32_t dec_w = s->dec.w + dec_p.right; rt_assert(s->dec.state.hidden == s->inc.state.hidden, ".dec .inc must be .hidden in sync"); const int32_t sw = ui_slider_width(s); // slider width const int32_t dx = s->dec.state.hidden ? 0 : dec_w + dec_p.right; const int32_t vx = v->x + i.left + dx; const int32_t x = ui_app.mouse.x - vx; const int32_t y = ui_app.mouse.y - (v->y + i.top); if (0 <= x && x < sw && 0 <= y && y < v->h) { const fp64_t fmax = (fp64_t)s->value_max; const fp64_t fmin = (fp64_t)s->value_min; const fp64_t range = fmax - fmin; fp64_t val = (fp64_t)x * range / (fp64_t)(sw - 1); int32_t vw = (int32_t)(val + s->value_min + 0.5); s->value = rt_min(rt_max(vw, s->value_min), s->value_max); if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } } static void ui_slider_inc_dec_value(ui_slider_t* s, int32_t sign, int32_t mul) { if (!ui_view.is_hidden(&s->view) && !ui_view.is_disabled(&s->view)) { // full 0x80000000..0x7FFFFFFF (-2147483648..2147483647) range int32_t v = s->value; if (v > s->value_min && sign < 0) { mul = rt_min(v - s->value_min, mul); v += mul * sign; } else if (v < s->value_max && sign > 0) { mul = rt_min(s->value_max - v, mul); v += mul * sign; } if (s->value != v) { s->value = v; if (s->callback != null) { s->callback(&s->view); } ui_slider_invalidate(s); } } } static void ui_slider_inc_dec(ui_button_t* b) { ui_slider_t* s = (ui_slider_t*)b->parent; if (!ui_view.is_hidden(&s->view) && !ui_view.is_disabled(&s->view)) { int32_t sign = b == &s->inc ? +1 : -1; int32_t mul = ui_app.shift && ui_app.ctrl ? 1000 : ui_app.shift ? 100 : ui_app.ctrl ? 10 : 1; ui_slider_inc_dec_value(s, sign, mul); } } static void ui_slider_every_100ms(ui_view_t* v) { // 100ms rt_assert(v->type == ui_view_slider); ui_slider_t* s = (ui_slider_t*)v; if (ui_view.is_hidden(v) || ui_view.is_disabled(v)) { s->time = 0; } else if (!s->dec.state.armed && !s->inc.state.armed) { s->time = 0; } else { if (s->time == 0) { s->time = ui_app.now; } else if (ui_app.now - s->time > 1.0) { const int32_t sign = s->dec.state.armed ? -1 : +1; const int32_t sec = (int32_t)(ui_app.now - s->time + 0.5); int32_t initial = ui_app.shift && ui_app.ctrl ? 1000 : ui_app.shift ? 100 : ui_app.ctrl ? 10 : 1; int32_t mul = sec >= 1 ? initial << (sec - 1) : initial; const int64_t range = (int64_t)s->value_max - (int64_t)s->value_min; if (mul > range / 8) { mul = (int32_t)(range / 8); } ui_slider_inc_dec_value(s, sign, rt_max(mul, 1)); } } } void ui_view_init_slider(ui_view_t* v) { rt_assert(v->type == ui_view_slider); v->measure = ui_slider_measure; v->layout = ui_slider_layout; v->paint = ui_slider_paint; v->tap = ui_slider_tap; v->mouse_move = ui_slider_mouse_move; v->every_100ms = ui_slider_every_100ms; v->color_id = ui_color_id_window_text; v->background_id = ui_color_id_button_face; ui_slider_t* s = (ui_slider_t*)v; static const char* accel = " Hold key while clicking\n" " Ctrl: x 10 Shift: x 100 \n" " Ctrl+Shift: x 1000 \n for step multiplier."; s->dec = (ui_button_t)ui_button(rt_glyph_fullwidth_hyphen_minus, 0, // rt_glyph_heavy_minus_sign ui_slider_inc_dec); s->dec.fm = v->fm; rt_str_printf(s->dec.hint, "%s", accel); s->inc = (ui_button_t)ui_button(rt_glyph_fullwidth_plus_sign, 0, // rt_glyph_heavy_plus_sign ui_slider_inc_dec); s->inc.fm = v->fm; ui_view.add(&s->view, &s->dec, &s->inc, null); // single glyph buttons less insets look better: ui_view_for_each(&s->view, it, { it->insets.left = 0.125f; it->insets.right = 0.125f; }); // inherit initial padding and insets from buttons. // caller may change those later and it should be accounted to // in measure() and layout() v->insets = s->dec.insets; v->padding = s->dec.padding; s->dec.padding.right = 0; s->dec.padding.left = 0; s->inc.padding.left = 0; s->inc.padding.right = 0; s->dec.flat = true; s->inc.flat = true; s->dec.min_h_em = 1.0f + ui_view_i_tb * 2; s->dec.min_w_em = 1.0f + ui_view_i_tb * 2; s->inc.min_h_em = 1.0f + ui_view_i_tb * 2; s->inc.min_w_em = 1.0f + ui_view_i_tb * 2; rt_str_printf(s->inc.hint, "%s", accel); v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; if (v->debug.id == null) { v->debug.id = "#slider"; } } void ui_slider_init(ui_slider_t* s, const char* label, fp32_t min_w_em, int32_t value_min, int32_t value_max, void (*callback)(ui_view_t* r)) { static_assert(offsetof(ui_slider_t, view) == 0, "offsetof(.view)"); if (min_w_em < 6.0) { rt_println("6.0 em minimum"); } s->type = ui_view_slider; ui_view.set_text(&s->view, "%s", label); s->callback = callback; s->min_w_em = rt_max(6.0f, min_w_em); s->value_min = value_min; s->value_max = value_max; s->value = value_min; ui_view_init_slider(&s->view); } ================================================ FILE: src/ui/ui_theme.c ================================================ /* Copyright (c) Dmitry "Leo" Kuznetsov 2021-24 see LICENSE for details */ #include "rt/rt.h" #include "ui/ui.h" #include "ui/rt_win32.h" static int32_t ui_theme_dark = -1; // -1 unknown static errno_t ui_theme_reg_get_uint32(HKEY root, const char* path, const char* key, DWORD *v) { *v = 0; DWORD type = REG_DWORD; DWORD light_theme = 0; DWORD bytes = sizeof(light_theme); errno_t r = RegGetValueA(root, path, key, RRF_RT_DWORD, &type, v, &bytes); if (r != 0) { rt_println("RegGetValueA(%s\\%s) failed %s", path, key, rt_strerr(r)); } return r; } #pragma push_macro("ux_theme_reg_cv") #pragma push_macro("ux_theme_reg_default_colors") #define ux_theme_reg_cv "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\" #define ux_theme_reg_default_colors ux_theme_reg_cv "Themes\\DefaultColors\\" static bool ui_theme_use_light_theme(const char* key) { if ((!ui_app.dark_mode && !ui_app.light_mode) || ( ui_app.dark_mode && ui_app.light_mode)) { const char* personalize = ux_theme_reg_cv "Themes\\Personalize"; DWORD light_theme = 0; ui_theme_reg_get_uint32(HKEY_CURRENT_USER, personalize, key, &light_theme); return light_theme != 0; } else if (ui_app.light_mode) { return true; } else { rt_assert(ui_app.dark_mode); return false; } } #pragma pop_macro("ux_theme_reg_cv") #pragma pop_macro("ux_theme_reg_default_colors") static HMODULE ui_theme_uxtheme(void) { static HMODULE uxtheme; if (uxtheme == null) { uxtheme = GetModuleHandleA("uxtheme.dll"); if (uxtheme == null) { uxtheme = LoadLibraryA("uxtheme.dll"); } } rt_not_null(uxtheme); return uxtheme; } static void* ui_theme_uxtheme_func(uint16_t ordinal) { HMODULE uxtheme = ui_theme_uxtheme(); void* proc = (void*)GetProcAddress(uxtheme, MAKEINTRESOURCEA(ordinal)); rt_not_null(proc); return proc; } static void ui_theme_set_preferred_app_mode(int32_t mode) { typedef BOOL (__stdcall *SetPreferredAppMode_t)(int32_t mode); SetPreferredAppMode_t SetPreferredAppMode = (SetPreferredAppMode_t) (SetPreferredAppMode_t)ui_theme_uxtheme_func(135); errno_t r = rt_b2e(SetPreferredAppMode(mode)); // On Win11: 10.0.22631 // SetPreferredAppMode(true) failed 0x0000047E(1150) ERROR_OLD_WIN_VERSION // "The specified program requires a newer version of Windows." if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("SetPreferredAppMode(AllowDark) failed %s", rt_strerr(r)); } } // https://stackoverflow.com/questions/75835069/dark-system-contextmenu-in-window static void ui_theme_flush_menu_themes(void) { typedef BOOL (__stdcall *FlushMenuThemes_t)(void); FlushMenuThemes_t FlushMenuThemes = (FlushMenuThemes_t) (FlushMenuThemes_t)ui_theme_uxtheme_func(136); errno_t r = rt_b2e(FlushMenuThemes()); // FlushMenuThemes() works but returns ERROR_OLD_WIN_VERSION // on newest Windows 11 but it is not documented thus no complains. if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("FlushMenuThemes(AllowDark) failed %s", rt_strerr(r)); } } static void ui_theme_allow_dark_mode_for_app(bool allow) { // https://github.com/rizonesoft/Notepad3/tree/96a48bd829a3f3192bbc93cd6944cafb3228b96d/src/DarkMode typedef BOOL (__stdcall *AllowDarkModeForApp_t)(bool allow); AllowDarkModeForApp_t AllowDarkModeForApp = (AllowDarkModeForApp_t)ui_theme_uxtheme_func(132); if (AllowDarkModeForApp != null) { errno_t r = rt_b2e(AllowDarkModeForApp(allow)); if (r != 0 && r != ERROR_PROC_NOT_FOUND) { rt_println("AllowDarkModeForApp(true) failed %s", rt_strerr(r)); } } } static void ui_theme_allow_dark_mode_for_window(bool allow) { typedef BOOL (__stdcall *AllowDarkModeForWindow_t)(HWND hWnd, bool allow); AllowDarkModeForWindow_t AllowDarkModeForWindow = (AllowDarkModeForWindow_t)ui_theme_uxtheme_func(133); if (AllowDarkModeForWindow != null) { int r = rt_b2e(AllowDarkModeForWindow((HWND)ui_app.window, allow)); // On Win11: 10.0.22631 // AllowDarkModeForWindow(true) failed 0x0000047E(1150) ERROR_OLD_WIN_VERSION // "The specified program requires a newer version of Windows." if (r != 0 && r != ERROR_PROC_NOT_FOUND && r != ERROR_OLD_WIN_VERSION) { rt_println("AllowDarkModeForWindow(true) failed %s", rt_strerr(r)); } } } static bool ui_theme_are_apps_dark(void) { return !ui_theme_use_light_theme("AppsUseLightTheme"); } static bool ui_theme_is_system_dark(void) { return !ui_theme_use_light_theme("SystemUsesLightTheme"); } static bool ui_theme_is_app_dark(void) { if (ui_theme_dark < 0) { ui_theme_dark = ui_theme.are_apps_dark(); } return ui_theme_dark; } static void ui_theme_refresh(void) { rt_swear(ui_app.window != null); ui_theme_dark = -1; BOOL dark_mode = ui_theme_is_app_dark(); // must be 32-bit "BOOL" static const DWORD DWMWA_USE_IMMERSIVE_DARK_MODE = 20; /* 20 == DWMWA_USE_IMMERSIVE_DARK_MODE in Windows 11 SDK. This value was undocumented for Windows 10 versions 2004 and later, supported for Windows 11 Build 22000 and later. */ errno_t r = DwmSetWindowAttribute((HWND)ui_app.window, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark_mode, sizeof(dark_mode)); if (r != 0) { rt_println("DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) " "failed %s", rt_strerr(r)); } ui_theme.allow_dark_mode_for_app(dark_mode); ui_theme.allow_dark_mode_for_window(dark_mode); ui_theme.set_preferred_app_mode(dark_mode ? ui_theme_app_mode_force_dark : ui_theme_app_mode_force_light); ui_theme.flush_menu_themes(); ui_app.request_layout(); } ui_theme_if ui_theme = { .is_app_dark = ui_theme_is_app_dark, .is_system_dark = ui_theme_is_system_dark, .are_apps_dark = ui_theme_are_apps_dark, .set_preferred_app_mode = ui_theme_set_preferred_app_mode, .flush_menu_themes = ui_theme_flush_menu_themes, .allow_dark_mode_for_app = ui_theme_allow_dark_mode_for_app, .allow_dark_mode_for_window = ui_theme_allow_dark_mode_for_window, .refresh = ui_theme_refresh, }; ================================================ FILE: src/ui/ui_toggle.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static void ui_toggle_paint_on_off(ui_view_t* v) { const ui_ltrb_t i = ui_view.margins(v, &v->insets); int32_t x = v->x; int32_t y = v->y + i.top; ui_color_t c = ui_colors.darken(v->background, !ui_theme.is_app_dark() ? 0.125f : 0.5f); ui_color_t b = v->state.pressed ? ui_colors.tone_green : c; const int32_t a = v->fm->ascent; const int32_t d = v->fm->descent; const int32_t w = v->fm->em.w; int32_t r = ((a + d + 1) / 2) | 0x1; // radius must be odd int32_t h = r * 2 + 1; y += (v->h - i.top - i.bottom - h + 1) / 2; y += r + 1; // because radius is odd x += r; ui_color_t border = ui_theme.is_app_dark() ? ui_colors.darken(v->color, 0.5) : ui_colors.lighten(v->color, 0.5); if (v->state.hover) { border = ui_colors.get_color(ui_color_id_hot_tracking); } ui_gdi.circle(x, y, r, border, b); ui_gdi.circle(x + w - r, y, r, border, b); ui_gdi.fill(x, y - r, w - r + 1, h, b); ui_gdi.line(x, y - r, x + w - r + 1, y - r, border); ui_gdi.line(x, y + r, x + w - r + 1, y + r, border); int32_t x1 = v->state.pressed ? x + w - r : x; // circle is too bold in control color - water it down ui_color_t fill = ui_theme.is_app_dark() ? ui_colors.darken(v->color, 0.5f) : ui_colors.lighten(v->color, 0.5f); border = ui_theme.is_app_dark() ? ui_colors.darken(fill, 0.0625f) : ui_colors.lighten(fill, 0.0625f); ui_gdi.circle(x1, y, r - 2, border, fill); } static const char* ui_toggle_on_off_label(ui_view_t* v, char* label, int32_t count) { rt_str.format(label, count, "%s", ui_view.string(v)); char* s = strstr(label, "___"); if (s != null) { memcpy(s, v->state.pressed ? "On " : "Off", 3); } return rt_nls.str(label); } static void ui_toggle_measure(ui_view_t* v) { if (v->min_w_em < 3.0f) { rt_println("3.0f em minimum width"); v->min_w_em = 4.0f; } ui_view.measure_control(v); rt_assert(v->type == ui_view_toggle); } static void ui_toggle_paint(ui_view_t* v) { rt_assert(v->type == ui_view_toggle); char txt[rt_countof(v->p.text)]; const char* label = ui_toggle_on_off_label(v, txt, rt_countof(txt)); const char* text = rt_nls.str(label); ui_view.text_measure(v, text, &v->text); ui_view.text_align(v, &v->text); ui_toggle_paint_on_off(v); const ui_color_t text_color = !v->state.hover ? v->color : (ui_theme.is_app_dark() ? ui_colors.white : ui_colors.black); const ui_gdi_ta_t ta = { .fm = v->fm, .color = text_color }; ui_gdi.text(&ta, v->x + v->text.xy.x, v->y + v->text.xy.y, "%s", text); } static void ui_toggle_flip(ui_toggle_t* t) { ui_view.invalidate((ui_view_t*)t, null); t->state.pressed = !t->state.pressed; if (t->callback != null) { t->callback(t); } } static void ui_toggle_character(ui_view_t* v, const char* utf8) { char ch = utf8[0]; if (ui_view.is_shortcut_key(v, ch)) { ui_toggle_flip((ui_toggle_t*)v); } } static bool ui_toggle_key_pressed(ui_view_t* v, int64_t key) { const bool trigger = ui_app.alt && ui_view.is_shortcut_key(v, key); if (trigger) { ui_toggle_flip((ui_toggle_t*)v); } return trigger; // swallow if true } static bool ui_toggle_tap(ui_view_t* v, int32_t rt_unused(ix), bool pressed) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (pressed && inside) { ui_toggle_flip((ui_toggle_t*)v); } return pressed && inside; } void ui_view_init_toggle(ui_view_t* v) { rt_assert(v->type == ui_view_toggle); v->tap = ui_toggle_tap; v->paint = ui_toggle_paint; v->measure = ui_toggle_measure; v->character = ui_toggle_character; v->key_pressed = ui_toggle_key_pressed; v->color_id = ui_color_id_button_text; v->background_id = ui_color_id_button_face; v->text_align = ui.align.left; if (v->debug.id == null) { v->debug.id = "#toggle"; } } void ui_toggle_init(ui_toggle_t* t, const char* label, fp32_t ems, void (*callback)(ui_toggle_t* b)) { ui_view.set_text(t, "%s", label); t->min_w_em = ems; t->callback = callback; t->type = ui_view_toggle; ui_view_init_toggle(t); } ================================================ FILE: src/ui/ui_view.c ================================================ #include "rt/rt.h" #include "ui/ui.h" static const fp64_t ui_view_hover_delay = 1.5; // seconds #pragma push_macro("ui_view_for_each") static void ui_view_update_shortcut(ui_view_t* v); // adding and removing views is not expected to be frequent // actions by application code (human factor - UI design) // thus extra checks and verifications are there even in // release code because C is not type safety champion language. static inline void ui_view_check_type(ui_view_t* v) { // little endian: rt_static_assertion(('vwXX' & 0xFFFF0000U) == ('vwZZ' & 0xFFFF0000U)); rt_static_assertion((ui_view_stack & 0xFFFF0000U) == ('vwXX' & 0xFFFF0000U)); rt_swear(((uint32_t)v->type & 0xFFFF0000U) == ('vwXX' & 0xFFFF0000U), "not a view: %4.4s 0x%08X (forgotten &static_view?)", &v->type, v->type); } static void ui_view_verify(ui_view_t* p) { ui_view_check_type(p); ui_view_for_each(p, c, { ui_view_check_type(c); ui_view_update_shortcut(c); rt_swear(c->parent == p); rt_swear(c == c->next->prev); rt_swear(c == c->prev->next); }); } static ui_view_t* ui_view_add(ui_view_t* p, ...) { va_list va; va_start(va, p); ui_view_t* c = va_arg(va, ui_view_t*); while (c != null) { rt_swear(c->parent == null && c->prev == null && c->next == null); ui_view.add_last(p, c); c = va_arg(va, ui_view_t*); } va_end(va); ui_view_call_init(p); ui_app.request_layout(); return p; } static void ui_view_add_first(ui_view_t* p, ui_view_t* c) { rt_swear(c->parent == null && c->prev == null && c->next == null); c->parent = p; if (p->child == null) { c->prev = c; c->next = c; } else { c->prev = p->child->prev; c->next = p->child; c->prev->next = c; c->next->prev = c; } p->child = c; ui_view_call_init(c); ui_app.request_layout(); } static void ui_view_add_last(ui_view_t* p, ui_view_t* c) { rt_swear(c->parent == null && c->prev == null && c->next == null); c->parent = p; if (p->child == null) { c->prev = c; c->next = c; p->child = c; } else { c->prev = p->child->prev; c->next = p->child; c->prev->next = c; c->next->prev = c; } ui_view_call_init(c); ui_view_verify(p); ui_app.request_layout(); } static void ui_view_add_after(ui_view_t* c, ui_view_t* a) { rt_swear(c->parent == null && c->prev == null && c->next == null); rt_not_null(a->parent); c->parent = a->parent; c->next = a->next; c->prev = a; a->next = c; c->prev->next = c; c->next->prev = c; ui_view_call_init(c); ui_view_verify(c->parent); ui_app.request_layout(); } static void ui_view_add_before(ui_view_t* c, ui_view_t* b) { rt_swear(c->parent == null && c->prev == null && c->next == null); rt_not_null(b->parent); c->parent = b->parent; c->prev = b->prev; c->next = b; b->prev = c; c->prev->next = c; c->next->prev = c; ui_view_call_init(c); ui_view_verify(c->parent); ui_app.request_layout(); } static void ui_view_remove(ui_view_t* c) { rt_not_null(c->parent); rt_not_null(c->parent->child); // if a view that has focus is removed from parent: if (c == ui_app.focus) { ui_view.set_focus(null); } if (c->prev == c) { rt_swear(c->next == c); c->parent->child = null; } else { c->prev->next = c->next; c->next->prev = c->prev; if (c->parent->child == c) { c->parent->child = c->next; } } c->prev = null; c->next = null; ui_view_verify(c->parent); c->parent = null; ui_app.request_layout(); } static void ui_view_remove_all(ui_view_t* p) { while (p->child != null) { ui_view.remove(p->child); } ui_app.request_layout(); } static void ui_view_disband(ui_view_t* p) { // do not disband composite controls if (p->type != ui_view_mbx && p->type != ui_view_slider) { while (p->child != null) { ui_view_disband(p->child); ui_view.remove(p->child); } } ui_app.request_layout(); } static void ui_view_invalidate(const ui_view_t* v, const ui_rect_t* r) { if (ui_view.is_hidden(v)) { rt_println("hidden: %s", ui_view_debug_id(v)); } else { ui_rect_t rc = {0}; if (r != null) { rc = (ui_rect_t){ .x = v->x + r->x, .y = v->y + r->y, .w = r->w, .h = r->h }; } else { rc = (ui_rect_t){ v->x, v->y, v->w, v->h}; // expand view rectangle by padding const ui_ltrb_t p = ui_view.margins(v, &v->padding); rc.x -= p.left; rc.y -= p.top; rc.w += p.left + p.right; rc.h += p.top + p.bottom; } if (v->debug.trace.prc) { rt_println("%d,%d %dx%d", rc.x, rc.y, rc.w, rc.h); } ui_app.invalidate(&rc); } } static const char* ui_view_string(ui_view_t* v) { if (v->p.strid == 0) { int32_t id = rt_nls.strid(v->p.text); v->p.strid = id > 0 ? id : -1; } return v->p.strid < 0 ? v->p.text : // not localized rt_nls.string(v->p.strid, v->p.text); } static ui_wh_t ui_view_text_metrics_va(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, va_list va) { const ui_gdi_ta_t ta = { .fm = fm, .color = ui_colors.transparent, .measure = true }; return multiline ? ui_gdi.multiline_va(&ta, x, y, w, format, va) : ui_gdi.text_va(&ta, x, y, format, va); } static ui_wh_t ui_view_text_metrics(int32_t x, int32_t y, bool multiline, int32_t w, const ui_fm_t* fm, const char* format, ...) { va_list va; va_start(va, format); ui_wh_t wh = ui_view_text_metrics_va(x, y, multiline, w, fm, format, va); va_end(va); return wh; } static void ui_view_text_measure(ui_view_t* v, const char* s, ui_view_text_metrics_t* tm) { const ui_fm_t* fm = v->fm; tm->wh = (ui_wh_t){ .w = 0, .h = fm->height }; if (s[0] == 0) { tm->multiline = false; } else { tm->multiline = strchr(s, '\n') != null; if (v->type == ui_view_label && tm->multiline) { int32_t w = (int32_t)((fp64_t)v->min_w_em * (fp64_t)fm->em.w + 0.5); tm->wh = ui_view.text_metrics(v->x, v->y, true, w, fm, "%s", s); } else { tm->wh = ui_view.text_metrics(v->x, v->y, false, 0, fm, "%s", s); } } } static void ui_view_text_align(ui_view_t* v, ui_view_text_metrics_t* tm) { tm->xy = (ui_point_t){ .x = -1, .y = -1 }; const ui_ltrb_t i = ui_view.margins(v, &v->insets); // i_wh the inside insets w x h: const ui_wh_t i_wh = { .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom }; const int32_t h_align = v->text_align & ~(ui.align.top|ui.align.bottom); const int32_t v_align = v->text_align & ~(ui.align.left|ui.align.right); tm->xy.x = i.left + (i_wh.w - tm->wh.w + 1) / 2; if (h_align & ui.align.left) { tm->xy.x = i.left; } else if (h_align & ui.align.right) { tm->xy.x = i_wh.w - tm->wh.w - i.right; } // vertical centering is trickier. // mt.h is height of all measured lines of text tm->xy.y = i.top + (i_wh.h - tm->wh.h + 1) / 2; if (v_align & ui.align.top) { tm->xy.y = i.top; } else if (v_align & ui.align.bottom) { tm->xy.y = i_wh.h - tm->wh.h - i.bottom; } else if (!tm->multiline) { #if 0 // TODO: doesn't look good or right: // UI controls should have x-height line in the dead center // of the control to be visually balanced. // y offset of "x-line" of the glyph: const ui_fm_t* fm = v->fm; const int32_t y_of_x_line = fm->baseline - fm->x_height; // `dy` offset of the center to x-line (middle of glyph cell) const int32_t dy = tm->wh.h / 2 - y_of_x_line; tm->xy.y += dy / 2; if (v->debug.trace.mt) { rt_println(" x-line: %d mt.h: %d mt.h / 2 - x_line: %d", y_of_x_line, tm->wh.h, dy); } #endif } } static void ui_view_measure_control(ui_view_t* v) { v->p.strid = 0; const char* s = ui_view.string(v); const ui_fm_t* fm = v->fm; const ui_ltrb_t i = ui_view.margins(v, &v->insets); v->w = (int32_t)((fp64_t)fm->em.w * (fp64_t)v->min_w_em + 0.5); v->h = (int32_t)((fp64_t)fm->em.h * (fp64_t)v->min_h_em + 0.5); if (v->debug.trace.mt) { const ui_ltrb_t p = ui_view.margins(v, &v->padding); rt_println(">%dx%d em: %dx%d min: %.3fx%.3f " "i: %d %d %d %d p: %d %d %d %d %s \"%.*s\"", v->w, v->h, fm->em.w, fm->em.h, v->min_w_em, v->min_h_em, i.left, i.top, i.right, i.bottom, p.left, p.top, p.right, p.bottom, ui_view_debug_id(v), rt_min(64, strlen(s)), s); const ui_margins_t in = v->insets; const ui_margins_t pd = v->padding; rt_println(" i: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f" " p: %.3f %.3f %.3f %.3f l+r: %.3f t+b: %.3f", in.left, in.top, in.right, in.bottom, in.left + in.right, in.top + in.bottom, pd.left, pd.top, pd.right, pd.bottom, pd.left + pd.right, pd.top + pd.bottom); } ui_view_text_measure(v, s, &v->text); if (v->debug.trace.mt) { rt_println(" mt: %d %d", v->text.wh.w, v->text.wh.h); } v->w = rt_max(v->w, i.left + v->text.wh.w + i.right); v->h = rt_max(v->h, i.top + v->text.wh.h + i.bottom); ui_view_text_align(v, &v->text); if (v->debug.trace.mt) { rt_println("<%dx%d text_align x,y: %d,%d %s", v->w, v->h, v->text.xy.x, v->text.xy.y, ui_view_debug_id(v)); } } static void ui_view_measure_children(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_for_each(v, c, { ui_view.measure(c); }); } } static void ui_view_measure(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_measure_children(v); if (v->prepare != null) { v->prepare(v); } if (v->measure != null && v->measure != ui_view_measure) { v->measure(v); } else { ui_view.measure_control(v); } if (v->measured != null) { v->measured(v); } } } static void ui_layout_view(ui_view_t* rt_unused(v)) { // ui_ltrb_t i = ui_view.margins(v, &v->insets); // ui_ltrb_t p = ui_view.margins(v, &v->padding); // rt_println(">%s %d,%d %dx%d p: %d %d %d %d i: %d %d %d %d", // v->p.text, v->x, v->y, v->w, v->h, // p.left, p.top, p.right, p.bottom, // i.left, i.top, i.right, i.bottom); // rt_println("<%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); } static void ui_view_layout_children(ui_view_t* v) { if (!ui_view.is_hidden(v)) { ui_view_for_each(v, c, { ui_view.layout(c); }); } } static void ui_view_layout(ui_view_t* v) { // rt_println(">%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); if (!ui_view.is_hidden(v)) { if (v->layout != null && v->layout != ui_view_layout) { v->layout(v); } else { ui_layout_view(v); } if (v->composed != null) { v->composed(v); } ui_view_layout_children(v); } // rt_println("<%s %d,%d %dx%d", v->p.text, v->x, v->y, v->w, v->h); } static bool ui_view_inside(const ui_view_t* v, const ui_point_t* pt) { const int32_t x = pt->x - v->x; const int32_t y = pt->y - v->y; return 0 <= x && x < v->w && 0 <= y && y < v->h; } static bool ui_view_is_parent_of(const ui_view_t* parent, const ui_view_t* child) { rt_swear(parent != null && child != null); const ui_view_t* p = child->parent; while (p != null) { if (parent == p) { return true; } p = p->parent; } return false; } static ui_ltrb_t ui_view_margins(const ui_view_t* v, const ui_margins_t* m) { const fp64_t gw = (fp64_t)m->left + (fp64_t)m->right; const fp64_t gh = (fp64_t)m->top + (fp64_t)m->bottom; const ui_wh_t* em = &v->fm->em; const int32_t em_w = (int32_t)(em->w * gw + 0.5); const int32_t em_h = (int32_t)(em->h * gh + 0.5); const int32_t left = (int32_t)((fp64_t)em->w * (fp64_t)m->left + 0.5); const int32_t top = (int32_t)((fp64_t)em->h * (fp64_t)m->top + 0.5); return (ui_ltrb_t) { .left = left, .top = top, .right = em_w - left, .bottom = em_h - top }; } static void ui_view_inbox(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* insets) { rt_swear(r != null || insets != null); rt_swear(v->max_w >= 0 && v->max_h >= 0); const ui_ltrb_t i = ui_view_margins(v, &v->insets); if (insets != null) { *insets = i; } if (r != null) { *r = (ui_rect_t) { .x = v->x + i.left, .y = v->y + i.top, .w = v->w - i.left - i.right, .h = v->h - i.top - i.bottom, }; } } static void ui_view_outbox(const ui_view_t* v, ui_rect_t* r, ui_ltrb_t* padding) { rt_swear(r != null || padding != null); rt_swear(v->max_w >= 0 && v->max_h >= 0); const ui_ltrb_t p = ui_view_margins(v, &v->padding); if (padding != null) { *padding = p; } if (r != null) { // rt_println("%s %d,%d %dx%d %.1f %.1f %.1f %.1f", v->p.text, // v->x, v->y, v->w, v->h, // v->padding.left, v->padding.top, v->padding.right, v->padding.bottom); *r = (ui_rect_t) { .x = v->x - p.left, .y = v->y - p.top, .w = v->w + p.left + p.right, .h = v->h + p.top + p.bottom, }; // rt_println("%s %d,%d %dx%d", v->p.text, // r->x, r->y, r->w, r->h); } } static void ui_view_update_shortcut(ui_view_t* v) { if (ui_view.is_control(v) && v->type != ui_view_text && v->shortcut == 0x00) { const char* s = ui_view.string(v); const char* a = strchr(s, '&'); if (a != null && a[1] != 0 && a[1] != '&') { // TODO: utf-8 shortcuts? possible v->shortcut = a[1]; } } } static void ui_view_set_text_va(ui_view_t* v, const char* format, va_list va) { char t[rt_countof(v->p.text)]; rt_str.format_va(t, rt_countof(t), format, va); char* s = v->p.text; if (strcmp(s, t) != 0) { int32_t n = (int32_t)strlen(t); memcpy(s, t, (size_t)n + 1); v->p.strid = 0; // next call to nls() will localize it ui_view_update_shortcut(v); ui_app.request_layout(); } } static void ui_view_set_text(ui_view_t* v, const char* format, ...) { va_list va; va_start(va, format); ui_view.set_text_va(v, format, va); va_end(va); } static void ui_view_show_hint(ui_view_t* v, ui_view_t* hint) { ui_view_call_init(hint); ui_view.set_text(hint, v->hint); ui_view.measure(hint); int32_t x = v->x + v->w / 2 - hint->w / 2 + hint->fm->em.w / 4; int32_t y = v->y + v->h + hint->fm->em.h / 4; if (x + hint->w > ui_app.crc.w) { x = ui_app.crc.w - hint->w - hint->fm->em.w / 2; } if (x < 0) { x = hint->fm->em.w / 2; } if (y + hint->h > ui_app.crc.h) { y = ui_app.crc.h - hint->h - hint->fm->em.h / 2; } if (y < 0) { y = hint->fm->em.h / 2; } // show_tooltip will center horizontally ui_app.show_hint(hint, x + hint->w / 2, y, 0); } static void ui_view_hovering(ui_view_t* v, bool start) { static ui_label_t hint = ui_label(0.0, ""); if (start && ui_app.animating.view == null && v->hint[0] != 0 && !ui_view.is_hidden(v)) { hint.padding = (ui_margins_t){0, 0, 0, 0}; hint.parent = ui_app.content; hint.state.hidden = false; ui_view_show_hint(v, &hint); } else if (!start && ui_app.animating.view == &hint) { ui_app.show_hint(null, -1, -1, 0); } } static bool ui_view_is_shortcut_key(ui_view_t* v, int64_t key) { // Supported keyboard shortcuts are ASCII characters only for now // If there is not focused UI control in Alt+key [Alt] is optional. // If there is focused control only Alt+Key is accepted as shortcut char ch = 0x20 <= key && key <= 0x7F ? (char)toupper((char)key) : 0x00; bool needs_alt = ui_app.focus != null && ui_app.focus != v && !ui_view.is_parent_of(ui_app.focus, v); bool keyboard_shortcut = ch != 0x00 && v->shortcut != 0x00 && (ui_app.alt || ui_app.ctrl || !needs_alt) && toupper(v->shortcut) == ch; return keyboard_shortcut; } static bool ui_view_is_orphan(const ui_view_t* v) { while (v != ui_app.root && v != null) { v = v->parent; } return v == null; } static bool ui_view_is_hidden(const ui_view_t* v) { bool hidden = v->state.hidden || ui_view.is_orphan(v); while (!hidden && v->parent != null) { v = v->parent; hidden = v->state.hidden; } return hidden; } static bool ui_view_is_disabled(const ui_view_t* v) { bool disabled = v->state.disabled; while (!disabled && v->parent != null) { v = v->parent; disabled = v->state.disabled; } return disabled; } static void ui_view_timer(ui_view_t* v, ui_timer_t id) { if (v->timer != null) { v->timer(v, id); } // timers are delivered even to hidden and disabled views: ui_view_for_each(v, c, { ui_view_timer(c, id); }); } static void ui_view_every_sec(ui_view_t* v) { if (v->every_sec != null) { v->every_sec(v); } ui_view_for_each(v, c, { ui_view_every_sec(c); }); } static void ui_view_every_100ms(ui_view_t* v) { if (v->every_100ms != null) { v->every_100ms(v); } ui_view_for_each(v, c, { ui_view_every_100ms(c); }); } static bool ui_view_key_pressed(ui_view_t* v, int64_t k) { bool done = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->key_pressed != null) { ui_view_update_shortcut(v); done = v->key_pressed(v, k); } if (!done) { ui_view_for_each(v, c, { done = ui_view_key_pressed(c, k); if (done) { break; } }); } } return done; } static bool ui_view_key_released(ui_view_t* v, int64_t k) { bool done = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->key_released != null) { done = v->key_released(v, k); } if (!done) { ui_view_for_each(v, c, { done = ui_view_key_released(c, k); if (done) { break; } }); } } return done; } static void ui_view_character(ui_view_t* v, const char* utf8) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->character != null) { ui_view_update_shortcut(v); v->character(v, utf8); } ui_view_for_each(v, c, { ui_view_character(c, utf8); }); } } static void ui_view_resolve_color_ids(ui_view_t* v) { if (v->color_id > 0) { v->color = ui_colors.get_color(v->color_id); } if (v->background_id > 0) { v->background = ui_colors.get_color(v->background_id); } } static void ui_view_paint(ui_view_t* v) { rt_assert(ui_app.crc.w > 0 && ui_app.crc.h > 0); ui_view_resolve_color_ids(v); if (v->debug.trace.prc) { const char* s = ui_view.string(v); rt_println("%d,%d %dx%d prc: %d,%d %dx%d \"%.*s\"", v->x, v->y, v->w, v->h, ui_app.prc.x, ui_app.prc.y, ui_app.prc.w, ui_app.prc.h, rt_min(64, strlen(s)), s); } if (!v->state.hidden && ui_app.crc.w > 0 && ui_app.crc.h > 0) { if (v->erase != null) { v->erase(v); } if (v->paint != null) { v->paint(v); } if (v->painted != null) { v->painted(v); } if (v->debug.paint.margins) { ui_view.debug_paint_margins(v); } if (v->debug.paint.fm) { ui_view.debug_paint_fm(v); } if (v->debug.paint.call && v->debug_paint != null) { v->debug_paint(v); } ui_view_for_each(v, c, { ui_view_paint(c); }); } } static bool ui_view_has_focus(const ui_view_t* v) { return ui_app.focused() && ui_app.focus == v; } static void ui_view_set_focus(ui_view_t* v) { if (ui_app.focus != v) { ui_view_t* loosing = ui_app.focus; ui_view_t* gaining = v; if (gaining != null) { rt_swear(gaining->focusable && !ui_view.is_hidden(gaining) && !ui_view.is_disabled(gaining)); } if (loosing != null) { rt_swear(loosing->focusable); } ui_app.focus = v; if (loosing != null && loosing->focus_lost != null) { loosing->focus_lost(loosing); } if (gaining != null && gaining->focus_gained != null) { gaining->focus_gained(gaining); } } } static int64_t ui_view_hit_test(const ui_view_t* v, ui_point_t pt) { int64_t ht = ui.hit_test.nowhere; if (!ui_view.is_hidden(v) && v->hit_test != null) { ht = v->hit_test(v, pt); } if (ht == ui.hit_test.nowhere) { ui_view_for_each(v, c, { if (!c->state.hidden && ui_view.inside(c, &pt)) { ht = ui_view_hit_test(c, pt); if (ht != ui.hit_test.nowhere) { break; } } }); } return ht; } static void ui_view_update_hover(ui_view_t* v, bool hidden) { const bool hover = v->state.hover; const bool inside = ui_view.inside(v, &ui_app.mouse); v->state.hover = !ui_view.is_hidden(v) && inside; if (hover != v->state.hover) { // rt_println("hover := %d %p %s", v->state.hover, v, ui_view_debug_id(v)); ui_view.hover_changed(v); // even for hidden if (!hidden) { ui_view.invalidate(v, null); } } } static void ui_view_mouse_hover(ui_view_t* v) { // rt_println("%d,%d %s", ui_app.mouse.x, ui_app.mouse.y, // ui_app.mouse_left ? "L" : "_", // ui_app.mouse_right ? "R" : "_"); // mouse hover over is dispatched even to disabled views const bool hidden = ui_view.is_hidden(v); ui_view_update_hover(v, hidden); if (!hidden && v->mouse_hover != null) { v->mouse_hover(v); } ui_view_for_each(v, c, { ui_view_mouse_hover(c); }); } static void ui_view_mouse_move(ui_view_t* v) { // rt_println("%d,%d %s", ui_app.mouse.x, ui_app.mouse.y, // ui_app.mouse_left ? "L" : "_", // ui_app.mouse_right ? "R" : "_"); // mouse move is dispatched even to disabled views const bool hidden = ui_view.is_hidden(v); ui_view_update_hover(v, hidden); if (!hidden && v->mouse_move != null) { v->mouse_move(v); } ui_view_for_each(v, c, { ui_view_mouse_move(c); }); } static void ui_view_double_click(ui_view_t* v, int32_t ix) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { const bool inside = ui_view.inside(v, &ui_app.mouse); if (inside) { if (v->focusable) { ui_view.set_focus(v); } if (v->double_click != null) { v->double_click(v, ix); } } ui_view_for_each(v, c, { ui_view_double_click(c, ix); }); } } static void ui_view_mouse_scroll(ui_view_t* v, ui_point_t dx_dy) { if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { if (v->mouse_scroll != null) { v->mouse_scroll(v, dx_dy); } ui_view_for_each(v, c, { ui_view_mouse_scroll(c, dx_dy); }); } } static void ui_view_hover_changed(ui_view_t* v) { if (!v->state.hidden) { if (!v->state.hover) { v->p.hover_when = 0; ui_view.hovering(v, false); // cancel hover } else { rt_swear(ui_view_hover_delay >= 0); if (v->p.hover_when >= 0) { v->p.hover_when = ui_app.now + ui_view_hover_delay; } } } } static void ui_view_lose_hidden_focus(ui_view_t* v) { // removes focus from hidden or disabled ui controls if (ui_app.focus != null) { if (ui_app.focus == v && (v->state.disabled || v->state.hidden)) { ui_view.set_focus(null); } else { ui_view_for_each(v, c, { if (ui_app.focus != null) { ui_view_lose_hidden_focus(c); } }); } } } static bool ui_view_tap(ui_view_t* v, int32_t ix, bool pressed) { bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_tap(c, ix, pressed); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && pressed && inside) { if (v->focusable) { ui_view.set_focus(v); } if (v->tap != null) { swallow = v->tap(v, ix, pressed); } } if (!swallow && !pressed) { // mouse click release is never swallowed because a lot // of controls want to hear it: if (v->tap != null) { (void)v->tap(v, ix, pressed); } } } return swallow; } static bool ui_view_long_press(ui_view_t* v, int32_t ix) { bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_long_press(c, ix); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->long_press != null) { swallow = v->long_press(v, ix); } } return swallow; } static bool ui_view_double_tap(ui_view_t* v, int32_t ix) { // 0: left 1: middle 2: right bool swallow = false; // consumed if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_double_tap(c, ix); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->double_tap != null) { swallow = v->double_tap(v, ix); } } return swallow; } static bool ui_view_context_menu(ui_view_t* v) { bool swallow = false; if (!ui_view.is_hidden(v) && !ui_view.is_disabled(v)) { ui_view_for_each(v, c, { swallow = ui_view_context_menu(c); if (swallow) { break; } }); const bool inside = ui_view.inside(v, &ui_app.mouse); if (!swallow && inside && v->context_menu != null) { swallow = v->context_menu(v); } } return swallow; } static bool ui_view_message(ui_view_t* view, int32_t m, int64_t wp, int64_t lp, int64_t* ret) { if (!view->state.hidden) { if (view->p.hover_when > 0 && ui_app.now > view->p.hover_when) { view->p.hover_when = -1; // "already called" ui_view.hovering(view, true); } } // message() callback is called even for hidden and disabled views // could be useful for enabling conditions of post() messages from // background rt_thread. if (view->message != null) { if (view->message(view, m, wp, lp, ret)) { return true; } } ui_view_for_each(view, c, { if (ui_view_message(c, m, wp, lp, ret)) { return true; } }); return false; } static bool ui_view_is_container(const ui_view_t* v) { return v->type == ui_view_stack || v->type == ui_view_span || v->type == ui_view_list; } static bool ui_view_is_spacer(const ui_view_t* v) { return v->type == ui_view_spacer; } static bool ui_view_is_control(const ui_view_t* v) { return v->type == ui_view_text || v->type == ui_view_label || v->type == ui_view_toggle || v->type == ui_view_button || v->type == ui_view_slider || v->type == ui_view_mbx; } static void ui_view_debug_paint_margins(ui_view_t* v) { if (v->debug.paint.margins) { if (v->type == ui_view_spacer) { ui_gdi.fill(v->x, v->y, v->w, v->h, ui_color_rgb(128, 128, 128)); } const ui_ltrb_t p = ui_view.margins(v, &v->padding); const ui_ltrb_t i = ui_view.margins(v, &v->insets); ui_color_t c = ui_colors.green; const int32_t pl = p.left; const int32_t pr = p.right; const int32_t pt = p.top; const int32_t pb = p.bottom; if (pl > 0) { ui_gdi.frame(v->x - pl, v->y, pl, v->h, c); } if (pr > 0) { ui_gdi.frame(v->x + v->w, v->y, pr, v->h, c); } if (pt > 0) { ui_gdi.frame(v->x, v->y - pt, v->w, pt, c); } if (p.bottom > 0) { ui_gdi.frame(v->x, v->y + v->h, v->w, pb, c); } c = ui_colors.orange; const int32_t il = i.left; const int32_t ir = i.right; const int32_t it = i.top; const int32_t ib = i.bottom; if (il > 0) { ui_gdi.frame(v->x, v->y, il, v->h, c); } if (ir > 0) { ui_gdi.frame(v->x + v->w - ir, v->y, ir, v->h, c); } if (it > 0) { ui_gdi.frame(v->x, v->y, v->w, it, c); } if (ib > 0) { ui_gdi.frame(v->x, v->y + v->h - ib, v->w, ib, c); } if ((ui_view.is_container(v) || ui_view.is_spacer(v)) && v->w > 0 && v->h > 0) { ui_wh_t wh = ui_view_text_metrics(v->x, v->y, false, 0, v->fm, "%s", ui_view.string(v)); const int32_t tx = v->x; const int32_t ty = v->y + v->h - wh.h; const ui_gdi_ta_t ta = { .fm = v->fm, .color = ui_colors.red }; ui_gdi.text(&ta, tx, ty, "%s %d,%d %dx%d", ui_view_debug_id(v), v->x, v->y, v->w, v->h); } } } static void ui_view_debug_paint_fm(ui_view_t* v) { if (v->debug.paint.fm && v->p.text[0] != 0 && !ui_view_is_container(v) && !ui_view_is_spacer(v)) { const ui_point_t t = v->text.xy; const int32_t x = v->x; const int32_t y = v->y; const int32_t w = v->w; const int32_t y_0 = y + t.y; const int32_t y_b = y_0 + v->fm->baseline; const int32_t y_a = y_b - v->fm->ascent; const int32_t y_h = y_0 + v->fm->height; const int32_t y_x = y_b - v->fm->x_height; const int32_t y_d = y_b + v->fm->descent; // fm.height y == 0 line is painted one pixel higher: ui_gdi.line(x, y_0 - 1, x + w, y_0 - 1, ui_colors.red); ui_gdi.line(x, y_a, x + w, y_a, ui_colors.green); ui_gdi.line(x, y_x, x + w, y_x, ui_colors.orange); ui_gdi.line(x, y_b, x + w, y_b, ui_colors.red); ui_gdi.line(x, y_d, x + w, y_d, ui_colors.green); if (y_h != y_d) { ui_gdi.line(x, y_d, x + w, y_d, ui_colors.green); ui_gdi.line(x, y_h, x + w, y_h, ui_colors.red); } else { ui_gdi.line(x, y_h, x + w, y_h, ui_colors.orange); } // fm.height line painted _under_ the actual height } } #pragma push_macro("ui_view_no_siblings") #define ui_view_no_siblings(v) do { \ rt_swear((v)->parent == null && (v)->child == null && \ (v)->prev == null && (v)->next == null); \ } while (0) static void ui_view_test(void) { ui_view_t p0 = ui_view(stack); ui_view_t c1 = ui_view(stack); ui_view_t c2 = ui_view(stack); ui_view_t c3 = ui_view(stack); ui_view_t c4 = ui_view(stack); ui_view_t g1 = ui_view(stack); ui_view_t g2 = ui_view(stack); ui_view_t g3 = ui_view(stack); ui_view_t g4 = ui_view(stack); // add grand children to children: ui_view.add(&c2, &g1, &g2, null); ui_view_verify(&c2); ui_view.add(&c3, &g3, &g4, null); ui_view_verify(&c3); // single child ui_view.add(&p0, &c1, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); // two children ui_view.add(&p0, &c1, &c2, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); // three children ui_view.add(&p0, &c1, &c2, &c3, null); ui_view_verify(&p0); ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); ui_view.remove(&c3); ui_view_verify(&p0); // add_first, add_last, add_before, add_after ui_view.add_first(&p0, &c1); ui_view_verify(&p0); rt_swear(p0.child == &c1); ui_view.add_last(&p0, &c4); ui_view_verify(&p0); rt_swear(p0.child == &c1 && p0.child->prev == &c4); ui_view.add_after(&c2, &c1); ui_view_verify(&p0); rt_swear(p0.child == &c1); rt_swear(c1.next == &c2); ui_view.add_before(&c3, &c4); ui_view_verify(&p0); rt_swear(p0.child == &c1); rt_swear(c4.prev == &c3); // removing all ui_view.remove(&c1); ui_view_verify(&p0); ui_view.remove(&c2); ui_view_verify(&p0); ui_view.remove(&c3); ui_view_verify(&p0); ui_view.remove(&c4); ui_view_verify(&p0); ui_view_no_siblings(&p0); ui_view_no_siblings(&c1); ui_view_no_siblings(&c4); ui_view.remove(&g1); ui_view_verify(&c2); ui_view.remove(&g2); ui_view_verify(&c2); ui_view.remove(&g3); ui_view_verify(&c3); ui_view.remove(&g4); ui_view_verify(&c3); ui_view_no_siblings(&c2); ui_view_no_siblings(&c3); ui_view_no_siblings(&g1); ui_view_no_siblings(&g2); ui_view_no_siblings(&g3); ui_view_no_siblings(&g4); // a bit more intuitive (for a human) nested way to initialize tree: ui_view.add(&p0, &c1, ui_view.add(&c2, &g1, &g2, null), ui_view.add(&c3, &g3, &g4, null), &c4); ui_view_verify(&p0); ui_view_disband(&p0); ui_view_no_siblings(&p0); ui_view_no_siblings(&c1); ui_view_no_siblings(&c2); ui_view_no_siblings(&c3); ui_view_no_siblings(&c4); ui_view_no_siblings(&g1); ui_view_no_siblings(&g2); ui_view_no_siblings(&g3); ui_view_no_siblings(&g4); if (rt_debug.verbosity.level > rt_debug.verbosity.quiet) { rt_println("done"); } } #pragma pop_macro("ui_view_no_siblings") ui_view_if ui_view = { .add = ui_view_add, .add_first = ui_view_add_first, .add_last = ui_view_add_last, .add_after = ui_view_add_after, .add_before = ui_view_add_before, .remove = ui_view_remove, .remove_all = ui_view_remove_all, .disband = ui_view_disband, .inside = ui_view_inside, .is_parent_of = ui_view_is_parent_of, .margins = ui_view_margins, .inbox = ui_view_inbox, .outbox = ui_view_outbox, .set_text = ui_view_set_text, .set_text_va = ui_view_set_text_va, .invalidate = ui_view_invalidate, .text_metrics_va = ui_view_text_metrics_va, .text_metrics = ui_view_text_metrics, .text_measure = ui_view_text_measure, .text_align = ui_view_text_align, .measure_control = ui_view_measure_control, .measure_children = ui_view_measure_children, .layout_children = ui_view_layout_children, .measure = ui_view_measure, .layout = ui_view_layout, .string = ui_view_string, .is_orphan = ui_view_is_orphan, .is_hidden = ui_view_is_hidden, .is_disabled = ui_view_is_disabled, .is_control = ui_view_is_control, .is_container = ui_view_is_container, .is_spacer = ui_view_is_spacer, .timer = ui_view_timer, .every_sec = ui_view_every_sec, .every_100ms = ui_view_every_100ms, .hit_test = ui_view_hit_test, .key_pressed = ui_view_key_pressed, .key_released = ui_view_key_released, .character = ui_view_character, .paint = ui_view_paint, .has_focus = ui_view_has_focus, .set_focus = ui_view_set_focus, .lose_hidden_focus = ui_view_lose_hidden_focus, .mouse_hover = ui_view_mouse_hover, .mouse_move = ui_view_mouse_move, .mouse_scroll = ui_view_mouse_scroll, .hovering = ui_view_hovering, .hover_changed = ui_view_hover_changed, .is_shortcut_key = ui_view_is_shortcut_key, .context_menu = ui_view_context_menu, .tap = ui_view_tap, .long_press = ui_view_long_press, .double_tap = ui_view_double_tap, .message = ui_view_message, .debug_paint_margins = ui_view_debug_paint_margins, .debug_paint_fm = ui_view_debug_paint_fm, .test = ui_view_test }; #ifdef UI_VIEW_TEST rt_static_init(ui_view) { ui_view.test(); } #endif #pragma pop_macro("ui_view_for_each") ================================================ FILE: test/test1.c ================================================ #include "rt/rt.h" #include static int usage(void) { fprintf(stderr, "Usage: %s [options]\n", rt_args.basename()); fprintf(stderr, "Options:\n"); fprintf(stderr, " --help, -h - this help\n"); fprintf(stderr, " --verbosity - set verbosity level " "(quiet, info, verbose, debug, trace)\n"); fprintf(stderr, " --verbose, -v - set verbosity level to verbose\n"); return 0; } static int run(void) { if (rt_args.option_bool("--help") || rt_args.option_bool("-?")) { return usage(); } const char* v = rt_args.option_str("--verbosity"); if (v != null) { rt_debug.verbosity.level = rt_debug.verbosity_from_string(v); } else if (rt_args.option_bool("-v") || rt_args.option_bool("--verbose")) { rt_debug.verbosity.level = rt_debug.verbosity.verbose; } rt_core.test(); rt_println("all tests passed\n\n"); // rt_println("rt_args.basename(): %s", rt_args.basename()); // rt_println("rt_args.v[0]: %s", rt_args.v[0]); // for (int i = 1; i < rt_args.c; i++) { // rt_println("rt_args.v[%d]: %s", i, rt_args.v[i]); // } // $ .\bin\debug\test1.exe "Hello World" Hello World // rt_args.v[0]: .\bin\debug\test1.exe // rt_args.basename(): test1 // rt_args.v[1]: Hello World // rt_args.v[2]: Hello // rt_args.v[3]: World return 0; } // both main() wand WinMain() can be present and compiled. // Runtime does something along the lines: // #include // #include // IMAGE_NT_HEADERS32* h = ImageNtHeader(GetModuleHandle(null)); // h->OptionalHeader.Subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI // h->OptionalHeader.Subsystem == IMAGE_SUBSYSTEM_WINDOWS_GUI // to select and call appropriate function: int main(int argc, const char* argv[], const char *envp[]) { rt_args.main(argc, argv, envp); int r = run(); rt_args.fini(); return r; } #include "rt/rt_win32.h" #pragma warning(suppress: 28251) // no annotations int APIENTRY WinMain(HINSTANCE rt_unused(inst), HINSTANCE rt_unused(prev), char* rt_unused(command), int rt_unused(show)) { rt_args.WinMain(); // Uses GetCommandLineW() which has full pathname int r = run(); rt_args.fini(); return r; } ================================================ FILE: test/test1.rc ================================================ #include #define company_name "" #define copyright "Copyright (C) 2024 `Leo` Dmitry Kuznetsov. All rights reserved." #define file_description "https://github.com/leok7v/ut" #define original_file_name "test1.exe" #define product_name "test1" #include "..\inc\rt\version.rc.in" ================================================ FILE: test/test2.c ================================================ #define rt_implementation #include "single_file_lib/rt/rt.h" int main(int argc, char* argv[], char *envp[]) { rt_args.main(argc, argv, envp); const char* v = rt_args.option_str("--verbosity"); if (v != null) { rt_debug.verbosity.level = rt_debug.verbosity_from_string(v); } else if (rt_args.option_bool("-v") || rt_args.option_bool("--verbose")) { rt_debug.verbosity.level = rt_debug.verbosity.info; } rt_core.test(); rt_println("all tests passed\n"); rt_args.fini(); return 0; }