From 94b57a95df71a6d46f8f2a5b7e7fda1864706ac4 Mon Sep 17 00:00:00 2001 From: Jaren Glenn Date: Sun, 8 Mar 2026 19:01:07 -0600 Subject: [PATCH 01/17] fix: only access _bongocat config when module is enabled Fixes error when module is imported but not enabled - was trying to inherit from config._bongocat unconditionally, which is empty {} when programs.wayland-bongocat.enable = false. --- nix/home-module.nix | 8 +++++--- nix/nixos-module.nix | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/nix/home-module.nix b/nix/home-module.nix index 7c83edc9..f1e94495 100644 --- a/nix/home-module.nix +++ b/nix/home-module.nix @@ -4,10 +4,12 @@ pkgs, ... }: let - inherit (config._bongocat) cfg configFile; + cfg = config.programs.wayland-bongocat; in { imports = [./common.nix]; - config = lib.mkIf cfg.enable { + config = lib.mkIf cfg.enable (let + configFile = config._bongocat.configFile; + in { home.packages = [ cfg.package @@ -38,5 +40,5 @@ in { RestartSec = "5s"; }; }; - }; + }); } diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index 0fa1d02a..72e519f4 100644 --- a/nix/nixos-module.nix +++ b/nix/nixos-module.nix @@ -6,10 +6,12 @@ ... }: with lib; let - inherit (config._bongocat) cfg configFile; + cfg = config.programs.wayland-bongocat; in { imports = [./common.nix]; - config = lib.mkIf cfg.enable { + config = lib.mkIf cfg.enable (let + configFile = config._bongocat.configFile; + in { environment.systemPackages = [ cfg.package @@ -35,5 +37,5 @@ in { RestartSec = "5s"; }; }; - }; + }); } From 04b43ce8cf2b21c39b179e9fc0a606133031d532 Mon Sep 17 00:00:00 2001 From: furudbat Date: Tue, 31 Mar 2026 17:03:41 +0200 Subject: [PATCH 02/17] wip: merge upstream 1.4.0 * add AllocatedString * update find_input_devices script --- CHANGELOG.md | 8 + Makefile | 3 - Makefile.old | 3 - README.md | 12 +- docs/fragments/examples-basic.md | 2 +- docs/fragments/options-all.md | 5 +- docs/fragments/options-dm-classic.md | 5 +- docs/fragments/options-dm.md | 5 +- docs/fragments/options-ms-agent.md | 5 +- docs/fragments/options-pkmn.md | 5 +- docs/fragments/options.md | 5 +- examples/test2.bongocat.conf | 179 +++++++++ flake.lock | 2 +- include/config/config.h | 158 +++++--- include/config/config_watcher.h | 7 +- include/core/bongocat.h | 2 + include/image_loader/custom/load_custom.h | 11 +- include/platform/input_context.h | 23 +- include/platform/wayland.h | 2 +- include/utils/memory.h | 184 ++++++++++ scripts/find_input_devices.sh | 25 +- scripts/test_bongocat.sh | 13 + scripts/test_bongocat_10.sh | 2 + scripts/test_bongocat_11.sh | 423 ++++++++++++++++++++++ scripts/test_bongocat_2.sh | 1 + scripts/test_bongocat_4.sh | 1 + scripts/test_bongocat_7.sh | 3 + scripts/test_bongocat_8.sh | 37 ++ src/config/config.cpp | 329 +++++++++++++---- src/config/config_watcher.cpp | 66 +++- src/core/main.cpp | 161 ++------ src/graphics/animation_init.cpp | 12 +- src/graphics/bar.cpp | 2 +- src/image_loader/custom/load_custom.cpp | 9 +- src/platform/input.cpp | 101 +++--- src/platform/wayland.cpp | 16 +- src/platform/wayland_setups.cpp | 8 +- src/utils/error.cpp | 1 + 38 files changed, 1462 insertions(+), 374 deletions(-) create mode 100644 examples/test2.bongocat.conf create mode 100755 scripts/test_bongocat_11.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 839ad9a3..568beceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +### [3.6.2] - 2026-03-30 + +- pull from upstream [1.4.0](https://github.com/saatvik333/wayland-bongocat/releases/tag/v1.4.0) + - ~~**Multi-monitor CSV**~~ + - **NO** comma-separated output names for now; don't have much time to do bigger merges/refactors + - adapt `--monitor` argument + - Input hotplug robustness + ### [3.6.1] - 2025-12-13 ### Improved diff --git a/Makefile b/Makefile index c08c9681..14ed4148 100644 --- a/Makefile +++ b/Makefile @@ -52,9 +52,6 @@ doc: release cmake --build build --target manpages SOURCES = $(shell find $(SRCDIR) -name '*.cpp' ! -path '*/embedded_assets/**/*.c??' ! -path '*/image_loader/*.c' ! -path '*/image_loader/**/*.cpp') -# Static analysis -analyze: - clang-tidy -p build $(SOURCES) # Memory check (requires valgrind) memcheck: debug diff --git a/Makefile.old b/Makefile.old index 464226f0..cdd7091e 100644 --- a/Makefile.old +++ b/Makefile.old @@ -251,9 +251,6 @@ lint: protocols @clang-tidy $(PROJECT_SOURCES) -- $(CFLAGS) 2>/dev/null || true @echo "Static analysis complete." -# Alias for lint -analyze: lint - # Generate compile_commands.json for IDE support (requires bear) # Run: make compiledb compiledb: clean diff --git a/README.md b/README.md index 9d92a813..12b3f9e5 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ wpets-find-devices # or ./scripts/find_input_devices.sh ```bash wpets-all --watch-config +# Optional: force one monitor from CLI +wpets-all --watch-config --monitor DP-1 ``` ## Configuration @@ -231,7 +233,6 @@ _If you build with ALL assets included you can void naming conflicts by using th | `custom_end_moving_row` | 1-15 | -1 (auto) | Row nr for end moving animation | See [examples](examples/custom-sprite-sheets) for more details. - @@ -246,7 +247,7 @@ Options: -c, --config Specify config file (default: ~/.config/bongocat.conf) -w, --watch-config Watch config file for changes and reload automatically -t, --toggle Toggle bongocat on/off (start if not running, stop if running) - -o, --output-name NAME Specify output name (overwrite output_name from config) + -m, --monitor NAME Force specific monitor output --random Randomize animation_name at start up --strict Only start up with a valid config and valid parameter --nr NR Specify Nr. for PID file to avoid conflicting ruinning instances @@ -291,8 +292,8 @@ For [hyprland](https://hypr.land/) users, you can autostart `wpets` in your `hyp ```ini # Auto start -exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen1.bongocat.conf -o HDMI-A-1 -exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen2.bongocat.conf -o DP-1 +exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen1.bongocat.conf -m HDMI-A-1 +exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen2.bongocat.conf -m DP-1 exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen3.bongocat.conf exec-once = wpets-all --watch-config --config ~/.config/bongocat/screen4.bongocat.conf --random ``` @@ -338,6 +339,9 @@ cmake --build build **Requirements:** wayland-client, wayland-protocols, gcc/clang, make, cmake +> [!CAUTION] +> **Privacy Notice**: When building in `DEBUG` mode and by enabling `enable_debug=1`, all keystrokes are logged to stdout/stderr. Ensure this is disabled (default: 0) for normal usage. + ## License MIT License - see [LICENSE](LICENSE) \ No newline at end of file diff --git a/docs/fragments/examples-basic.md b/docs/fragments/examples-basic.md index 28447573..6e5f7965 100644 --- a/docs/fragments/examples-basic.md +++ b/docs/fragments/examples-basic.md @@ -36,7 +36,7 @@ cat ~/.config/base.bongocat.conf ~/.config/devices.bongocat.conf | bongocat --co ## More ### Overwrite output -Custom config with hot-reload and custom `output_name` +Custom config with hot-reload and custom `monitor` ```bash bongocat --watch-config --output-name DP-2 --config ~/.config/bongocat.conf diff --git a/docs/fragments/options-all.md b/docs/fragments/options-all.md index 68569ec1..31b582ab 100644 --- a/docs/fragments/options-all.md +++ b/docs/fragments/options-all.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/docs/fragments/options-dm-classic.md b/docs/fragments/options-dm-classic.md index cf72eee8..2ae557fd 100644 --- a/docs/fragments/options-dm-classic.md +++ b/docs/fragments/options-dm-classic.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/docs/fragments/options-dm.md b/docs/fragments/options-dm.md index cf72eee8..2ae557fd 100644 --- a/docs/fragments/options-dm.md +++ b/docs/fragments/options-dm.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/docs/fragments/options-ms-agent.md b/docs/fragments/options-ms-agent.md index bf92060f..66a20327 100644 --- a/docs/fragments/options-ms-agent.md +++ b/docs/fragments/options-ms-agent.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/docs/fragments/options-pkmn.md b/docs/fragments/options-pkmn.md index ffd46d05..de759655 100644 --- a/docs/fragments/options-pkmn.md +++ b/docs/fragments/options-pkmn.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/docs/fragments/options.md b/docs/fragments/options.md index 710c5113..bed3eb52 100644 --- a/docs/fragments/options.md +++ b/docs/fragments/options.md @@ -15,7 +15,10 @@ Toggle bongocat on/off (start if not running, stop if running) -o, --output-name NAME - Specify output name (overwrite output_name from config) + Specify output name (overwrite monitor from config) + + -m, --monitor NAME + Bind to a specific monitor output (same as --output-name) --random Enable random animation_index, at start (overwrite random_index from config) diff --git a/examples/test2.bongocat.conf b/examples/test2.bongocat.conf new file mode 100644 index 00000000..e153f84b --- /dev/null +++ b/examples/test2.bongocat.conf @@ -0,0 +1,179 @@ +# Bongo Cat Configuration File +# Edit these values to customize your bongo cat overlay + +# Position settings (in pixels) +# cat_x_offset: Horizontal offset from center position +# When cat_align is "center": Positive values move right, negative values move left (default) +# When cat_align is "left": Positive values move right, negative values move left +# When cat_align is "right": Positive values move left, negative values move right +cat_x_offset=155 + +# cat_y_offset: Vertical offset from default position +# Positive values move down, negative values move up +cat_y_offset=15 + +# cat_align: Horizontal alignment in the bar (default: "center") +# Options: "center", "left" or "right" +cat_align=right + +# Mirroring options +# mirror_x: Flip cat horizontally (mirror across Y axis) (0 = off, 1 = on) +mirror_x=0 +# mirror_y: Flip cat vertically (mirror across X axis) (0 = off, 1 = on) +mirror_y=0 + +# Anti-aliasing settings +# enable_antialiasing: Use bilinear interpolation for smooth scaling ()0 = off, 1 = on) +# When enabled, provides smoother edges when scaling the cat image +enable_antialiasing=1 + +# Size settings +# cat_height: Height of the bongo cat in pixels +# Width is automatically calculated to maintain aspect ratio +cat_height=64 + +# NOTE: OVERLAY SETTINGS DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART +# DRAWN LAYERS GETS GLITCHY SOMETIMES, BETTER TO RESTART WHEN THESE CHANGE +# Overlay settings +# overlay_height: Height of the entire overlay bar +overlay_height=128 +# overlay_position: Position of the overlay on screen +# Options: "top" or "bottom" +overlay_position=bottom + +# NOTE: ANIMATION FROM DIFFERENT TYPE DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART OR BUILD WITH LAZY LOAD (e.g. "bongocat" -> "clippy") +# animation_name: Animation index +# Default Options: "bongocat" +# dm Options: "Agumon", "Greymon", "metal_greymon", ... +# MS Agent Options: "Clippy" +# pkmn Options: "Bulbasaur", "Pikachu", ... +animation_name=bongocat + +# NOTE: SPRITE LOADING SETTINGS DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART +# invert_color: Invert color (0 = off, 1 = on) +# (non-colored) dm sprites are black by default, reverting colors can be used for dark mode +invert_color=0 + +# NOTE: Only affect on start up +# random: random animation_index (0 = off, 1 = on) +# animation_name still needs to be set to determine the animation set option (bongocat, dm or MS agent) +random=0 + +# NOTE: SPRITE LOADING SETTINGS DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART +# Padding for sprite frame (experimental) +# (ignored for bongocat) +padding_x=0 +padding_y=0 + +# Animation settings +# idle_frame: Which frame to use when idle (0, 1, or 2) +# Options for bongocat: 0 = both paws up, 1 = left paw down, 2 = right paw down +# Options for dm: 0 = idle 1, 1 = idle 2 +idle_frame=0 + +# Sleep Mode settings +# enable_scheduled_sleep: When set to 1, the system will pause animations and display the sleep screen +# Requires both sleep_start_time and sleep_end_time to be defined +enable_scheduled_sleep=0 +# Start time for scheduled sleep mode (24-hour format: hh:mm) +sleep_begin=21:00 +# End time for scheduled sleep mode (24-hour format: hh:mm) +sleep_end=06:00 + +# Duration of user inactivity before entering sleep mode (in seconds) +# Set to 0 to disable idle-based sleep. +idle_sleep_timeout=1080 + +# Happy Animation +# happy_kpm: Minimum keystrokes per minute (KPM) required to trigger the happy animation (0 = disabled) +happy_kpm=400 + +# Work Animation +# update_rate: Check states rate (in milliseconds) (0 = disabled) +update_rate=0 +# cpu_threshold: Threshold of avg. CPU usage for triggering work animation (0 = disabled) +cpu_threshold=90 + +# Animation timing (in milliseconds) +# keypress_duration: How long to show animation after keypress +keypress_duration=500 + + +# idle_animation: Enable idle animation (0 = off, 1 = on) +idle_animation=0 +# animation_speed: How long to show frame, until next frame (in milliseconds) +# Optional, use fps as fallback +animation_speed=1000 + +movement_radius=960 +enable_movement_debug=0 +movement_speed=0 + +# test_animation_duration: How long to show test animation (in milliseconds) +#test_animation_duration=200 +#DEPRECATED: use animation_speed (for non-bongocat) + +# test_animation_interval: How often to trigger test animation (in seconds) +# Set to 0 to disable test animations +#test_animation_interval=0 +#DEPRECATED: use idle_animation and animation_speed (for non-bongocat) + +# Frame rate settings +# fps: Animation frame rate (frames per second) +fps=15 +# input_fps: Input thread frame rate (if not set, same as fps) +# This is just a timeout for the input waiting +input_fps=60 + +# Transparency settings +# overlay_opacity: Opacity of the overlay background (0-255) +# 0 = fully transparent, 255 = fully opaque +overlay_opacity=0 + +# Debug settings +# enable_debug: Show debug messages (0 = off, 1 = on) +enable_debug=1 + + + +# Input devices (you can specify multiple devices) +# Use keyboard_device for each device you want to monitor +# Examples: +# keyboard_device=/dev/input/event4 +# keyboard_device=/dev/input/event20 # External bluetooth keyboard (commented out - doesn't exist) +# keyboard_device=/dev/input/event5 # Another input device + +# Input devices +keyboard_device=/dev/input/event3 +keyboard_device=/dev/input/event8 +keyboard_device=/dev/input/event16 +keyboard_device=/dev/input/event19 +keyboard_device=/dev/input/event4 +keyboard_device=/dev/input/event7 +keyboard_device=/dev/input/event17 +#keyboard_device=/dev/input/event0 +#keyboard_device=/dev/input/event1 +keyboard_device=/dev/input/event3 +#keyboard_device=/dev/input/event6 +keyboard_device=/dev/input/event7 +keyboard_device=/dev/input/event8 +keyboard_device=/dev/input/event9 +keyboard_device=/dev/input/event10 +keyboard_device=/dev/input/event12 +keyboard_device=/dev/input/event13 +keyboard_device=/dev/input/event16 +keyboard_device=/dev/input/event17 +keyboard_device=/dev/input/event18 +keyboard_device=/dev/input/event21 + +keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-event-kbd +keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-event-if01 +keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-if01-event-kbd + +hotplug_scan_interval=300 + +# Multi-monitor support +# Specify which monitor to display bongocat on (optional) +# Use wlr-randr or swaymsg -t get_outputs to find monitor names +# If not specified or monitor not found, uses first available monitor +monitor=DP-1 \ No newline at end of file diff --git a/flake.lock b/flake.lock index dec5ee92..1c1909e5 100644 --- a/flake.lock +++ b/flake.lock @@ -58,4 +58,4 @@ }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/include/config/config.h b/include/config/config.h index f5fdca79..69cdc7d0 100644 --- a/include/config/config.h +++ b/include/config/config.h @@ -50,6 +50,8 @@ inline static constexpr const char *ALIGN_RIGHT_STR = "right"; struct config_t; void config_free_keyboard_devices(config_t& config); void config_copy_keyboard_devices_from(config_t& config, const config_t& other); +void config_free_keyboard_names(config_t& config); +void config_copy_keyboard_names_from(config_t& config, const config_t& other); void cleanup(config_t& config); // ============================================================================= @@ -87,8 +89,8 @@ enum class config_animation_custom_set_t : uint8_t { }; struct config_t { - char *output_name{BONGOCAT_NULLPTR}; - char *keyboard_devices[input::MAX_INPUT_DEVICES]{}; + AllocatedString output_name{BONGOCAT_NULLPTR}; + AllocatedString keyboard_devices[input::MAX_INPUT_DEVICES]; int32_t num_keyboard_devices{0}; int32_t cat_x_offset{0}; int32_t cat_y_offset{0}; @@ -141,17 +143,26 @@ struct config_t { int32_t screen_width{0}; - char *custom_sprite_sheet_filename{BONGOCAT_NULLPTR}; // must be png file + AllocatedString custom_sprite_sheet_filename{BONGOCAT_NULLPTR}; // must be png file assets::custom_animation_settings_t custom_sprite_sheet_settings{}; int32_t enable_hand_mapping{0}; + int32_t hotplug_scan_interval_ms{0}; + + // Fullscreen behavior + int32_t disable_fullscreen_hide{0}; + + // Device matching by name (for hotplug/auto-detection) + AllocatedString _keyboard_names[input::MAX_INPUT_DEVICES]; + int32_t _num_keyboard_names{0}; + // for keep old index when reload config bool _keep_old_animation_index{false}; bool _strict{false}; bool _custom{false}; // is custom sprite sheet - char *_animation_name{BONGOCAT_NULLPTR}; // original animation_anim from parsing config - char *_loaded_animation_fqname{BONGOCAT_NULLPTR}; + AllocatedString _animation_name{BONGOCAT_NULLPTR}; // original animation_anim from parsing config + AllocatedString _loaded_animation_fqname{BONGOCAT_NULLPTR}; // Make Config movable and copyable config_t() { @@ -208,17 +219,17 @@ struct config_t { , screen_width(other.screen_width) , custom_sprite_sheet_settings(other.custom_sprite_sheet_settings) , enable_hand_mapping(other.enable_hand_mapping) + , hotplug_scan_interval_ms(other.hotplug_scan_interval_ms) + , disable_fullscreen_hide(other.disable_fullscreen_hide) , _keep_old_animation_index(other._keep_old_animation_index) , _strict(other._strict) , _custom(other._custom) { - output_name = other.output_name != BONGOCAT_NULLPTR ? strdup(other.output_name) : BONGOCAT_NULLPTR; + output_name = duplicate_string(other.output_name); config_copy_keyboard_devices_from(*this, other); - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename != BONGOCAT_NULLPTR - ? strdup(other.custom_sprite_sheet_filename) - : BONGOCAT_NULLPTR; - _animation_name = other._animation_name != BONGOCAT_NULLPTR ? strdup(other._animation_name) : BONGOCAT_NULLPTR; - _loaded_animation_fqname = - other._loaded_animation_fqname != BONGOCAT_NULLPTR ? strdup(other._loaded_animation_fqname) : BONGOCAT_NULLPTR; + config_copy_keyboard_names_from(*this, other); + custom_sprite_sheet_filename = duplicate_string(other.custom_sprite_sheet_filename); + _animation_name = duplicate_string(other._animation_name); + _loaded_animation_fqname = duplicate_string(other._loaded_animation_fqname); } config_t& operator=(const config_t& other) { @@ -269,26 +280,24 @@ struct config_t { screen_width = other.screen_width; custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; enable_hand_mapping = other.enable_hand_mapping; + hotplug_scan_interval_ms = other.hotplug_scan_interval_ms; + disable_fullscreen_hide = other.disable_fullscreen_hide; _keep_old_animation_index = other._keep_old_animation_index; _strict = other._strict; _custom = other._custom; - output_name = other.output_name != BONGOCAT_NULLPTR ? strdup(other.output_name) : BONGOCAT_NULLPTR; + output_name = duplicate_string(other.output_name); config_copy_keyboard_devices_from(*this, other); - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename != BONGOCAT_NULLPTR - ? strdup(other.custom_sprite_sheet_filename) - : BONGOCAT_NULLPTR; - _animation_name = other._animation_name != BONGOCAT_NULLPTR ? strdup(other._animation_name) : BONGOCAT_NULLPTR; - _loaded_animation_fqname = other._loaded_animation_fqname != BONGOCAT_NULLPTR - ? strdup(other._loaded_animation_fqname) - : BONGOCAT_NULLPTR; + config_copy_keyboard_names_from(*this, other); + custom_sprite_sheet_filename = duplicate_string(other.custom_sprite_sheet_filename); + _animation_name = duplicate_string(other._animation_name); + _loaded_animation_fqname = duplicate_string(other._loaded_animation_fqname); } return *this; } config_t(config_t&& other) noexcept - : output_name(other.output_name) - , num_keyboard_devices(other.num_keyboard_devices) + : output_name(bongocat::move(other.output_name)) , cat_x_offset(other.cat_x_offset) , cat_y_offset(other.cat_y_offset) , cat_height(other.cat_height) @@ -331,19 +340,30 @@ struct config_t { , movement_speed(other.movement_speed) , movement_wait_factor(other.movement_wait_factor) , screen_width(other.screen_width) - , custom_sprite_sheet_filename(other.custom_sprite_sheet_filename) - , custom_sprite_sheet_settings(other.custom_sprite_sheet_settings) + , custom_sprite_sheet_filename(bongocat::move(other.custom_sprite_sheet_filename)) + , custom_sprite_sheet_settings(bongocat::move(other.custom_sprite_sheet_settings)) , enable_hand_mapping(other.enable_hand_mapping) + , hotplug_scan_interval_ms(other.hotplug_scan_interval_ms) + , disable_fullscreen_hide(other.disable_fullscreen_hide) , _keep_old_animation_index(other._keep_old_animation_index) , _strict(other._strict) , _custom(other._custom) - , _animation_name(other._animation_name) - , _loaded_animation_fqname(other._loaded_animation_fqname) { - for (int i = 0; i < num_keyboard_devices; ++i) { - keyboard_devices[i] = other.keyboard_devices[i]; + , _animation_name(bongocat::move(other._animation_name)) + , _loaded_animation_fqname(bongocat::move(other._loaded_animation_fqname)) { + for (int i = 0; i < other.num_keyboard_devices; ++i) { + keyboard_devices[i] = bongocat::move(other.keyboard_devices[i]); other.keyboard_devices[i] = BONGOCAT_NULLPTR; } + num_keyboard_devices = other._num_keyboard_names; + + for (int i = 0; i < other._num_keyboard_names; ++i) { + _keyboard_names[i] = bongocat::move(other._keyboard_names[i]); + other._keyboard_names[i] = BONGOCAT_NULLPTR; + } + _num_keyboard_names = other._num_keyboard_names; + other.num_keyboard_devices = 0; + other._num_keyboard_names = 0; other.output_name = BONGOCAT_NULLPTR; other.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; other._animation_name = BONGOCAT_NULLPTR; @@ -354,8 +374,7 @@ struct config_t { if (this != &other) { cleanup(*this); - output_name = other.output_name; - num_keyboard_devices = other.num_keyboard_devices; + output_name = bongocat::move(other.output_name); cat_x_offset = other.cat_x_offset; cat_y_offset = other.cat_y_offset; cat_height = other.cat_height; @@ -397,21 +416,34 @@ struct config_t { enable_movement_debug = other.enable_movement_debug; movement_speed = other.movement_speed; movement_wait_factor = other.movement_wait_factor; - custom_sprite_sheet_filename = other.custom_sprite_sheet_filename; + custom_sprite_sheet_filename = bongocat::move(other.custom_sprite_sheet_filename); screen_width = other.screen_width; - custom_sprite_sheet_settings = other.custom_sprite_sheet_settings; + custom_sprite_sheet_settings = bongocat::move(other.custom_sprite_sheet_settings); enable_hand_mapping = other.enable_hand_mapping; + hotplug_scan_interval_ms = other.hotplug_scan_interval_ms; + disable_fullscreen_hide = other.disable_fullscreen_hide; _keep_old_animation_index = other._keep_old_animation_index; _strict = other._strict; _custom = other._custom; - _animation_name = other._animation_name; - _loaded_animation_fqname = other._loaded_animation_fqname; + _animation_name = bongocat::move(other._animation_name); + _loaded_animation_fqname = bongocat::move(other._loaded_animation_fqname); - for (int i = 0; i < num_keyboard_devices; ++i) { + for (int i = 0; i < other.num_keyboard_devices; ++i) { keyboard_devices[i] = other.keyboard_devices[i]; - other.keyboard_devices[i] = BONGOCAT_NULLPTR; + release_allocated_string(other.keyboard_devices[i]); + //other.keyboard_devices[i] = BONGOCAT_NULLPTR; + } + num_keyboard_devices = other.num_keyboard_devices; + + for (int i = 0; i < other._num_keyboard_names; ++i) { + _keyboard_names[i] = other._keyboard_names[i]; + release_allocated_string(other._keyboard_names[i]); + //other._keyboard_names[i] = BONGOCAT_NULLPTR; } + _num_keyboard_names = other._num_keyboard_names; + other.num_keyboard_devices = 0; + other._num_keyboard_names = 0; other.output_name = BONGOCAT_NULLPTR; other.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; other._animation_name = BONGOCAT_NULLPTR; @@ -421,42 +453,54 @@ struct config_t { } }; inline void cleanup(config_t& config) { - if (config.output_name != BONGOCAT_NULLPTR) { - ::free(config.output_name); - config.output_name = BONGOCAT_NULLPTR; - } + release_allocated_string(config.output_name); config_free_keyboard_devices(config); - if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR) { - ::free(config.custom_sprite_sheet_filename); - config.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; - } - if (config._animation_name != BONGOCAT_NULLPTR) { - ::free(config._animation_name); - config._animation_name = BONGOCAT_NULLPTR; - } - if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { - ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = BONGOCAT_NULLPTR; - } + config_free_keyboard_names(config); + release_allocated_string(config.custom_sprite_sheet_filename); + release_allocated_string(config._animation_name); + release_allocated_string(config._loaded_animation_fqname); } inline void config_free_keyboard_devices(config_t& config) { assert(config.num_keyboard_devices >= 0); for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { + /* if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { ::free(config.keyboard_devices[i]); config.keyboard_devices[i] = BONGOCAT_NULLPTR; } + */ + release_allocated_string(config.keyboard_devices[i]); } config.num_keyboard_devices = 0; } +inline void config_free_keyboard_names(config_t& config) { + assert(config._num_keyboard_names >= 0); + for (size_t i = 0; i < static_cast(config._num_keyboard_names) && i < input::MAX_INPUT_DEVICES; i++) { + /* + if (config._keyboard_names[i] != BONGOCAT_NULLPTR) { + ::free(config._keyboard_names[i]); + config._keyboard_names[i] = BONGOCAT_NULLPTR; + } + */ + release_allocated_string(config._keyboard_names[i]); + } + config._num_keyboard_names = 0; +} inline void config_copy_keyboard_devices_from(config_t& config, const config_t& other) { config_free_keyboard_devices(config); config.num_keyboard_devices = other.num_keyboard_devices; assert(config.num_keyboard_devices >= 0); for (size_t i = 0; i < static_cast(config.num_keyboard_devices) && i < input::MAX_INPUT_DEVICES; i++) { - config.keyboard_devices[i] = - other.keyboard_devices[i] != BONGOCAT_NULLPTR ? strdup(other.keyboard_devices[i]) : BONGOCAT_NULLPTR; + config.keyboard_devices[i] = duplicate_string(other.keyboard_devices[i]); + } +} +inline void config_copy_keyboard_names_from(config_t& config, const config_t& other) { + config_free_keyboard_names(config); + config._num_keyboard_names = other._num_keyboard_names; + assert(config._num_keyboard_names >= 0); + for (size_t i = 0; i < static_cast(config._num_keyboard_names) && i < input::MAX_INPUT_DEVICES; i++) { + config._keyboard_names[i] = duplicate_string(other._keyboard_names[i]); } } @@ -475,6 +519,12 @@ BONGOCAT_NODISCARD created_result_t load(const char *config_file_path, void reset(config_t& config); void set_defaults(config_t& config); + + +// Resolve config file path with XDG fallback +// Returns a static/allocated path, or NULL if none found. +AllocatedString resolve_path(const char *explicit_path); + } // namespace bongocat::config #endif // BONGOCAT_CONFIG_H \ No newline at end of file diff --git a/include/config/config_watcher.h b/include/config/config_watcher.h index 57de6d62..f9888601 100644 --- a/include/config/config_watcher.h +++ b/include/config/config_watcher.h @@ -29,7 +29,7 @@ struct config_watcher_t { platform::FileDescriptor inotify_fd; platform::FileDescriptor wd_file; platform::FileDescriptor wd_dir; - char *config_path{BONGOCAT_NULLPTR}; + AllocatedString config_path{BONGOCAT_NULLPTR}; platform::FileDescriptor reload_efd; @@ -62,10 +62,7 @@ inline void cleanup_watcher(config_watcher_t& watcher) { close_fd(watcher.inotify_fd); close_fd(watcher.reload_efd); - if (watcher.config_path != BONGOCAT_NULLPTR) { - ::free(watcher.config_path); - watcher.config_path = BONGOCAT_NULLPTR; - } + release_allocated_string(watcher.config_path); } // ============================================================================= diff --git a/include/core/bongocat.h b/include/core/bongocat.h index b077ed84..5fe4ff53 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -1,8 +1,10 @@ #ifndef BONGOCAT_BONGOCAT_H #define BONGOCAT_BONGOCAT_H +#ifndef _POSIX_C_SOURCE // POSIX feature test macro - must be before includes #define _POSIX_C_SOURCE 200809L +#endif #include "utils/error.h" #include "utils/memory.h" diff --git a/include/image_loader/custom/load_custom.h b/include/image_loader/custom/load_custom.h index b36c2f7a..cdb5acce 100644 --- a/include/image_loader/custom/load_custom.h +++ b/include/image_loader/custom/load_custom.h @@ -16,11 +16,11 @@ void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept; namespace bongocat::assets { struct custom_image_t { platform::MMapFileContent data; - char *name{BONGOCAT_NULLPTR}; + AllocatedString name{BONGOCAT_NULLPTR}; custom_image_t() = default; custom_image_t(const custom_image_t& other) = delete; - custom_image_t(custom_image_t&& other) noexcept : data(bongocat::move(other.data)), name(other.name) { + custom_image_t(custom_image_t&& other) noexcept : data(bongocat::move(other.data)), name(bongocat::move(other.name)) { other.name = BONGOCAT_NULLPTR; } ~custom_image_t() { @@ -34,11 +34,8 @@ struct custom_image_t { data = bongocat::move(other.data); - if (name != BONGOCAT_NULLPTR) { - ::free(name); - name = BONGOCAT_NULLPTR; - } - name = other.name; + release_allocated_string(name); + name = bongocat::move(other.name); other.name = BONGOCAT_NULLPTR; } diff --git a/include/platform/input_context.h b/include/platform/input_context.h index 84ee0d4e..262834a2 100644 --- a/include/platform/input_context.h +++ b/include/platform/input_context.h @@ -9,8 +9,13 @@ #include #include #include +#include namespace bongocat::platform::input { + +inline static constexpr size_t MAX_ACTIVE_DEVICES {32}; +inline static constexpr size_t input_hotplug_events {64}; + enum class input_unique_file_type_t : uint8_t { NONE, File, @@ -20,7 +25,7 @@ struct input_unique_file_t; void cleanup(input_unique_file_t& file); struct input_unique_file_t { const char *_device_path{BONGOCAT_NULLPTR}; // original string from config (ref to input_context_t._device_paths[i]) - char *canonical_path{BONGOCAT_NULLPTR}; // resolved real path (malloc'd) + AllocatedString canonical_path{BONGOCAT_NULLPTR}; // resolved real path FileDescriptor fd; input_unique_file_type_t type{input_unique_file_type_t::NONE}; @@ -34,7 +39,7 @@ struct input_unique_file_t { input_unique_file_t(input_unique_file_t&& other) noexcept : _device_path(other._device_path) - , canonical_path(other.canonical_path) + , canonical_path(bongocat::move(other.canonical_path)) , fd(bongocat::move(other.fd)) , type(other.type) { other._device_path = BONGOCAT_NULLPTR; @@ -46,7 +51,7 @@ struct input_unique_file_t { cleanup(*this); _device_path = other._device_path; - canonical_path = other.canonical_path; + canonical_path = bongocat::move(other.canonical_path); fd = bongocat::move(other.fd); type = other.type; @@ -60,10 +65,7 @@ struct input_unique_file_t { inline void cleanup(input_unique_file_t& file) { close_fd(file.fd); file._device_path = BONGOCAT_NULLPTR; - if (file.canonical_path != BONGOCAT_NULLPTR) { - ::free(file.canonical_path); - file.canonical_path = BONGOCAT_NULLPTR; - } + release_allocated_string(file.canonical_path); file.type = input_unique_file_type_t::NONE; } @@ -89,7 +91,7 @@ struct input_context_t { Mutex input_lock; // thread context - AllocatedArray _device_paths; // local copy of devices (from config) + AllocatedArray _device_paths; // local copy of devices (from config) AllocatedArray _unique_paths_indices; size_t _unique_paths_indices_capacity{0}; // keep real _unique_paths_indices count here, shrink // _unique_paths_indices.count to used unique_paths_indices @@ -133,10 +135,7 @@ inline void cleanup(input_context_t& ctx) { ctx._unique_paths_indices_capacity = 0; release_allocated_array(ctx._unique_paths_indices); for (size_t i = 0; i < ctx._device_paths.count; i++) { - if (ctx._device_paths[i] != BONGOCAT_NULLPTR) { - ::free(ctx._device_paths[i]); - ctx._device_paths[i] = BONGOCAT_NULLPTR; - } + release_allocated_string(ctx._device_paths[i]); } release_allocated_array(ctx._device_paths); diff --git a/include/platform/wayland.h b/include/platform/wayland.h index 2127b68a..bfd2ff27 100644 --- a/include/platform/wayland.h +++ b/include/platform/wayland.h @@ -38,7 +38,7 @@ BONGOCAT_NODISCARD int get_screen_width(const wayland_context_t& ctx); BONGOCAT_NODISCARD const char *get_output_name(const wayland_context_t& ctx); // Get current layer name for logging -BONGOCAT_NODISCARD const char *get_current_layer_name(); +BONGOCAT_NODISCARD const char *get_current_layer_name(wayland_context_t& ctx); bongocat_error_t request_render(animation::animation_context_t& animation_ctx); } // namespace bongocat::platform::wayland diff --git a/include/utils/memory.h b/include/utils/memory.h index 11aa552b..45ba1cff 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -5,6 +5,7 @@ #include "utils/error.h" #include +#include #include #include #if defined(__GNUC__) || defined(__GNUG__) @@ -305,6 +306,189 @@ BONGOCAT_NODISCARD inline static AllocatedMemory make_allocated_memory() { return ret; } +class AllocatedString; +void release_allocated_string(AllocatedString& memory) noexcept; + +class AllocatedString { +public: + char *ptr{BONGOCAT_NULLPTR}; + size_t _capacity{0}; + size_t _length{0}; + + constexpr AllocatedString() = default; + ~AllocatedString() noexcept { + release_allocated_string(*this); + } + + explicit AllocatedString(decltype(BONGOCAT_NULLPTR)) noexcept {} + AllocatedString& operator=(decltype(BONGOCAT_NULLPTR)) noexcept { + release_allocated_string(*this); + return *this; + } + + AllocatedString(const AllocatedString& other) { + //_length = 0; + //_capacity = 0; + if (other.ptr != BONGOCAT_NULLPTR && other._length > 0) { + /// @TODO: reuse string with _capacity + ptr = strdup(other.ptr); + if (ptr != BONGOCAT_NULLPTR) { + _length = strlen(ptr); + _capacity = _length + 1; + } else { + _length = 0; + _capacity = 0; + ptr = BONGOCAT_NULLPTR; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } else { + _length = 0; + _capacity = 0; + } + } + AllocatedString& operator=(const AllocatedString& other) { + if (this != &other) { + /// @TODO: reuse string with _capacity + release_allocated_string(*this); + _length = 0; + _capacity = 0; + if (other.ptr != BONGOCAT_NULLPTR && other._length > 0) { + ptr = strdup(other.ptr); + if (ptr != BONGOCAT_NULLPTR) { + _length = strlen(ptr); + _capacity = _length + 1; + } else { + _length = 0; + _capacity = 0; + ptr = BONGOCAT_NULLPTR; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } else { + _length = 0; + } + } + return *this; + } + + AllocatedString(AllocatedString&& other) noexcept : ptr(other.ptr), _capacity(other._capacity), _length(other._length) { + other.ptr = BONGOCAT_NULLPTR; + other._capacity = 0; + other._length = 0; + } + AllocatedString& operator=(AllocatedString&& other) noexcept { + if (this != &other) { + release_allocated_string(*this); + ptr = other.ptr; + _capacity = other._capacity; + _length = other._length; + other.ptr = BONGOCAT_NULLPTR; + other._capacity = 0; + other._length = 0; + } + return *this; + } + + constexpr operator bool() const noexcept { + return ptr != BONGOCAT_NULLPTR; + } + + char& operator*() { + assert(ptr != nullptr); + return *ptr; + } + constexpr const char& operator*() const { + assert(ptr != nullptr); + return *ptr; + } + char *operator->() { + assert(ptr); + return ptr; + } + constexpr const char *operator->() const { + assert(ptr); + return ptr; + } + /* + explicit operator char *() noexcept { + return ptr; + } + */ + constexpr explicit operator const char *() const noexcept { + return ptr; + } + + constexpr size_t length() const noexcept { + assert(!ptr || _length == strlen(ptr)); + return _length; + } + + constexpr size_t capacity() const noexcept { + assert(_capacity >= _length); + return _capacity; + } + + constexpr const char * c_str() const noexcept { + return ptr; + } +}; +inline void release_allocated_string(AllocatedString& memory) noexcept { + if (memory.ptr != BONGOCAT_NULLPTR) { + ::free(memory.ptr); + memory.ptr = BONGOCAT_NULLPTR; + memory._capacity = 0; + memory._length = 0; + } +} +BONGOCAT_NODISCARD inline static AllocatedString make_null_string() noexcept { + return AllocatedString(); +} + +BONGOCAT_NODISCARD inline static AllocatedString make_allocated_string(size_t length) { + AllocatedString ret; + //ret._length = 0; + if (length > 0) { + ret._capacity = length + 1; + ret.ptr = static_cast(BONGOCAT_MALLOC(ret._capacity)); + if (ret.ptr != BONGOCAT_NULLPTR) { + memset(ret.ptr, 0, ret._capacity); + ret._length = strlen(ret.ptr); + return ret; + } else { + ret._capacity = 0; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } + ret._capacity = 0; + ret._length = 0; + ret.ptr = BONGOCAT_NULLPTR; + return ret; +} + +BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char* src) noexcept(true) { + AllocatedString ret; + //ret._length = 0; + if (src != nullptr) { + ret.ptr = strdup(src); + if (ret.ptr != BONGOCAT_NULLPTR) { + ret._length = strlen(ret.ptr); + ret._capacity = ret._length + 1; + return ret; + } else { + ret._capacity = 0; + ret._length = 0; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } + ret._capacity = 0; + ret._length = 0; + ret.ptr = BONGOCAT_NULLPTR; + return ret; +} +BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const AllocatedString& other) noexcept(true) { + AllocatedString ret (other); + return other; +} + template class AllocatedArray; template diff --git a/scripts/find_input_devices.sh b/scripts/find_input_devices.sh index b7c36b9b..e5af6b3c 100755 --- a/scripts/find_input_devices.sh +++ b/scripts/find_input_devices.sh @@ -244,11 +244,26 @@ interactive_detect() { header "Add to Config" echo -e " ${BOLD}~/.config/bongocat/bongocat.conf:${NC}" echo + echo -e " ${DIM}# Option 1: By device path (may change on reboot)${NC}" for entry in "${detected_keyboards[@]}"; do IFS='|' read -r event name device_path <<< "$entry" - echo -e " ${CYAN}keyboard_device=$device_path${NC} ${BOLD}# $name${NC}" + + if [[ "$device_path" != /dev/input/by-id/* ]]; then + echo -e " ${CYAN}keyboard_device=$device_path${NC} ${BOLD}# $name${NC}" + fi done + echo + echo -e " ${DIM}# Option 2: By device name (persistent, recommended)${NC}" + for entry in "${detected_keyboards[@]}"; do + IFS='|' read -r event name device_path <<< "$entry" + if [[ "$device_path" == /dev/input/by-id/* ]]; then + echo -e " ${CYAN}keyboard_name=$device_path${NC} ${BOLD}# $name${NC}" + fi + done + + echo + echo -e " ${DIM}Not accurate? Use: $SCRIPT_NAME --interactive${NC}" echo } @@ -305,7 +320,13 @@ generate_config() { for entry in "${devices[@]}"; do IFS='|' read -r event name device_path <<< "$entry" #echo -e " ${CYAN}keyboard_device=$device_path${NC} ${BOLD}# $name${NC}" - printf "${CYAN}keyboard_device=%-${maxlen}s${NC} ${BOLD}# %s ${NC}\n" "$device_path" "$name" + + if [[ "$device_path" == /dev/input/by-id/* ]]; then + printf "${CYAN}keyboard_name=%-${maxlen}s${NC} ${BOLD}# %s ${NC}\n" "$device_path" "$name" + fi + if [[ "$device_path" != /dev/input/by-id/* ]]; then + printf "${CYAN}keyboard_device=%-${maxlen}s${NC} ${BOLD}# %s ${NC}\n" "$device_path" "$name" + fi done } diff --git a/scripts/test_bongocat.sh b/scripts/test_bongocat.sh index 88571a3f..387c849c 100755 --- a/scripts/test_bongocat.sh +++ b/scripts/test_bongocat.sh @@ -154,6 +154,7 @@ if [[ -f "/proc/$PID/fd/0" ]]; then fi echo "[INFO] Disable sleep" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" echo "[TEST] Sending SIGUSR2..." @@ -212,60 +213,70 @@ sleep 5 echo "[TEST] Load biggest assets" echo "[INFO] Set Sprite Sheet: Links" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 5 echo "[INFO] Set Sprite Sheet: pkmn:dialga" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: dmx:Hexeblaumon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dmx:Hexeblaumon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: dm20:Omegamon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dm20:Omegamon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: pen20:Megalo Growmon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=pen20:Megalo Growmon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: dmc:Omegamon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dmc:Omegamon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: dm:Coronamon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dm:Coronamon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: Metal Greymon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=Metal Greymon/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: neko" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=neko/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 5 echo "[INFO] Set Sprite Sheet: pmd:volcanion" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=pmd:volcanion/' "$CONFIG" echo "[INFO] Send SIGUSR2" @@ -275,6 +286,7 @@ sleep 2 echo "[TEST] CPU threshold" echo "[INFO] Enable CPU threshold" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" sed -i -E 's/^update_rate=[0-9]+/update_rate=1000/' "$CONFIG" @@ -302,6 +314,7 @@ sleep 5 # Test Sleep + reload in middle +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" echo "[TEST] Trigger Sleep" diff --git a/scripts/test_bongocat_10.sh b/scripts/test_bongocat_10.sh index 11292c51..394b1027 100755 --- a/scripts/test_bongocat_10.sh +++ b/scripts/test_bongocat_10.sh @@ -249,6 +249,7 @@ for group in relwithdebinfo-tsan debug-all-assets-preload debug relwithdebinfo; sleep 10 echo "[TEST] Load biggest assets" echo "[INFO] Set Sprite Sheet: Links" + sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" sleep 2 @@ -256,6 +257,7 @@ for group in relwithdebinfo-tsan debug-all-assets-preload debug relwithdebinfo; kill -USR2 "$PID" # Reload config sleep 5 echo "[INFO] Set Sprite Sheet: pkmn:dialga" + sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" sleep 2 diff --git a/scripts/test_bongocat_11.sh b/scripts/test_bongocat_11.sh new file mode 100755 index 00000000..d5d73658 --- /dev/null +++ b/scripts/test_bongocat_11.sh @@ -0,0 +1,423 @@ +#!/usr/bin/env bash + +set -euo pipefail + +#make debug +#PROGRAM="./cmake-build-debug-all-assets-colored-preload/bongocat" +PROGRAM="./cmake-build-debug/bongocat-all" +#PROGRAM="./build/bongocat-all" + +WORKDIR=$(mktemp -d) +CONFIG="$WORKDIR/test2.bongocat.conf" # config file to modify +OG_CONFIG=./examples/test.bongocat.conf +cp $OG_CONFIG $CONFIG + +if [[ $# -ge 1 ]]; then + PID="$1" + CONFIG="$2" + cp $CONFIG "${CONFIG}.bak" + OG_CONFIG="${CONFIG}.bak" + echo "[TEST] Using provided PID = $PID" +else + echo "[TEST] Starting program..." + "$PROGRAM" --config "$CONFIG" --ignore-running --strict & + PID=$! + echo "[TEST] Program PID = $PID" + sleep 5 +fi + +# --- trap cleanup --- +cleanup() { + echo "[TEST] Cleaning up..." + kill -9 "$PID" 2>/dev/null || true + cp $OG_CONFIG $CONFIG + rm -rf "$WORKDIR" +} +trap cleanup EXIT + +echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" + +echo "[TEST] Sending SIGUSR2..." +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 3 +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 5 +echo "[INFO] Spam SIGUSR2" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +sleep 7 + +# --- function to toggle idle_sleep_timeout --- +toggle_config() { + if grep -q '^idle_sleep_timeout=10' "$CONFIG"; then + new=3600 + else + new=10 + fi + sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=$new/" "$CONFIG" + echo "[TEST] Setting idle_sleep_timeout=$new" +} + +# Test Sleep +sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=0/' "$CONFIG" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +sleep 3 +toggle_config +sleep 10 +toggle_config +sleep 10 +echo "[TEST] Trigger Sleep" +echo "[INFO] Enable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" +sleep 5 +sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 20 +echo "[TEST] Wake up Sleep" +if [[ -f "/proc/$PID/fd/0" ]]; then + printf '\e' > /proc/$PID/fd/0 + sleep 5 +fi +echo "[INFO] Disable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[TEST] Change animation sprite" +echo "[INFO] Set animation_name..." +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Set animation_name..." +sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 1 + +echo "[TEST] move and delete config..." +echo "[INFO] Move Config: $CONFIG > ${CONFIG}.del" +mv $CONFIG "${CONFIG}.del" +sleep 5 +rm "${CONFIG}.del" +sleep 5 +echo "[INFO] Recreate Config: $CONFIG" +cp ./examples/digimon.bongocat.conf $CONFIG +sleep 5 +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +sleep 5 +echo "[INFO] Delete Config: $CONFIG" +rm $CONFIG +sleep 5 +echo "[INFO] Recreate Config: $CONFIG" +cp ./examples/digimon.bongocat.conf $CONFIG +sleep 3 + +echo "[INFO] Disable sleep" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sleep 10 +if [[ -f "/proc/$PID/fd/0" ]]; then + # --- simulate pressing ESC --- + echo "[TEST] Sending ESC key..." + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + # a bit of a spam + echo "[INFO] Spam stdin" + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Spam stdin slower" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 3 +fi + +echo "[INFO] Disable sleep" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +echo "[TEST] Sending SIGUSR2..." +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 3 +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 5 +echo "[INFO] Spam SIGUSR2" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +sleep 10 +echo "[INFO] Spam SIGUSR2 slower" +kill -USR2 "$PID" +sleep 5 +kill -USR2 "$PID" +sleep 3 +kill -USR2 "$PID" +sleep 2 +kill -USR2 "$PID" +sleep 15 + +echo "[TEST] Sending SIGUSR1..." +echo "[INFO] Send SIGUSR1" +kill -USR1 "$PID" +sleep 2 +echo "[INFO] Send SIGUSR1" +kill -USR1 "$PID" +sleep 2 + +echo "[TEST] replace config..." +echo "[INFO] Replace Config: $CONFIG > ${CONFIG}.del" +cp ./examples/pokemon.bongocat.conf $CONFIG +sleep 5 +echo "[TEST] Sending ESC key..." +if [[ -f "/proc/$PID/fd/0" ]]; then + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 2 + printf '\e' > /proc/$PID/fd/0 + sleep 2 + printf '\e' > /proc/$PID/fd/0 + sleep 5 +fi +echo "[INFO] Restore old config" +cp $OG_CONFIG $CONFIG +sleep 5 + +echo "[TEST] Change Config by other config (Digimon -> Clippy): $CONFIG" +cp ./examples/clippy.bongocat.conf $CONFIG +sleep 5 + + +echo "[TEST] Load biggest assets" +echo "[INFO] Set Sprite Sheet: Links" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Set Sprite Sheet: pkmn:dialga" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: dmx:Hexeblaumon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dmx:Hexeblaumon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: dm20:Omegamon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dm20:Omegamon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: pen20:Megalo Growmon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=pen20:Megalo Growmon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: dmc:Omegamon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dmc:Omegamon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: dm:Coronamon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dm:Coronamon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +echo "[INFO] Set Sprite Sheet: Metal Greymon" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=Metal Greymon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +echo "[INFO] Set Sprite Sheet: neko" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=neko/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Set Sprite Sheet: pmd:volcanion" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=pmd:volcanion/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 + + +echo "[TEST] CPU threshold" +echo "[INFO] Enable CPU threshold" +sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" +sed -i -E 's/^update_rate=[0-9]+/update_rate=1000/' "$CONFIG" +sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=30/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 2 +if command -v stress-ng >/dev/null 2>&1; then + echo "[INFO] Running stress-ng to generate load" + stress-ng --cpu 0 --timeout 15s --metrics-brief & + sleep 20 +elif command -v stress >/dev/null 2>&1; then + echo "[INFO] Running stress to generate load" + stress --cpu "$(nproc)" --timeout 15s & + sleep 20 +else + echo "[WARN] No stress tool found, skipping load generation" +fi +echo "[INFO] Disable CPU threshold" +sed -i -E 's/^update_rate=[0-9]+/update_rate=0/' "$CONFIG" +sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=90/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 + + +# Test Sleep + reload in middle +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=dm20:Agumon/' "$CONFIG" +echo "[TEST] Trigger Sleep" +echo "[INFO] Enable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" +sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 25 +echo "[TEST] Reload config while sleeping Sleep" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Disable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[TEST] Change animation sprite" +echo "[INFO] Set animation_name..." +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Set animation_name..." +sed -i -E 's/^animation_name=.*/animation_name=greymon/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 1 + + +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +# Restart - stdin config +echo "[INFO] Restart..." +echo "[INFO] Sending SIGTERM..." +kill -TERM "$PID" +sleep 15 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done +echo "[TEST] Start with stdin config..." +cat "$CONFIG" | "$PROGRAM" --ignore-running --strict --config - & +PID=$! +sleep 10 +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +sleep 5 +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +# Restart - stdin default config +echo "[INFO] Restart..." +echo "[INFO] Sending SIGTERM..." +kill -TERM "$PID" +sleep 15 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done +echo "[TEST] Start with stdin default config..." +cat bongocat.conf.example | "$PROGRAM" --ignore-running --config - & +PID=$! +sleep 10 +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + + +# --- send SIGTERM --- +echo "[TEST] Sending SIGTERM..." +kill -TERM "$PID" +sleep 10 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done + +# --- verify not running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[FAIL] Process $PID still running!" + kill -9 "$PID" 2>/dev/null + exit 1 +else + echo "[PASS] Process terminated successfully" +fi diff --git a/scripts/test_bongocat_2.sh b/scripts/test_bongocat_2.sh index d54b5adc..36b65e5c 100755 --- a/scripts/test_bongocat_2.sh +++ b/scripts/test_bongocat_2.sh @@ -37,6 +37,7 @@ trap cleanup EXIT echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" echo "[TEST] Sending SIGUSR2..." echo "[INFO] Send SIGUSR2" diff --git a/scripts/test_bongocat_4.sh b/scripts/test_bongocat_4.sh index 69379d54..68bebdf9 100755 --- a/scripts/test_bongocat_4.sh +++ b/scripts/test_bongocat_4.sh @@ -205,6 +205,7 @@ sleep 2 echo "[TEST] Load assets" echo "[INFO] Set Sprite Sheet: dmx:Hexeblaumon" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=1/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=dmx:Hexeblaumon/' "$CONFIG" echo "[INFO] Send SIGUSR2" diff --git a/scripts/test_bongocat_7.sh b/scripts/test_bongocat_7.sh index fca4f0b8..7d7c6b6b 100755 --- a/scripts/test_bongocat_7.sh +++ b/scripts/test_bongocat_7.sh @@ -53,12 +53,14 @@ sleep 7 echo "[TEST] Load biggest assets" echo "[INFO] Set Sprite Sheet: Links" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 5 echo "[INFO] Set Sprite Sheet: pkmn:dialga" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" echo "[INFO] Send SIGUSR2" @@ -101,6 +103,7 @@ echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 2 echo "[INFO] Set Sprite Sheet: neko" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" sed -i -E 's/^animation_name=.*/animation_name=neko/' "$CONFIG" echo "[INFO] Send SIGUSR2" diff --git a/scripts/test_bongocat_8.sh b/scripts/test_bongocat_8.sh index 6b68425c..12d9135d 100755 --- a/scripts/test_bongocat_8.sh +++ b/scripts/test_bongocat_8.sh @@ -58,6 +58,8 @@ sleep 7 echo "[TEST] Change overlay settings" echo "[INFO] Set overlay_height" +sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" sed -i -E 's/^cat_height=[0-9]+/cat_height=96/' "$CONFIG" sed -i -E 's/^overlay_height=[0-9]+/overlay_height=100/' "$CONFIG" echo "[INFO] Send SIGUSR2" @@ -90,6 +92,41 @@ kill -USR2 "$PID" # Reload config sleep 10 echo "[INFO] Set max size" +sed -i -E 's/^overlay_position=.*/overlay_position=top/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=pkmn:dialga/' "$CONFIG" +sed -i -E 's/^cat_height=[0-9]+/cat_height=1024/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=2560/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 +echo "[INFO] Set min size" +sed -i -E 's/^cat_height=[0-9]+/cat_height=16/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=32/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 + +echo "[INFO] Set max size (antialiasing)" +sed -i -E 's/^overlay_position=.*/overlay_position=top/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" +sed -i -E 's/^cat_height=[0-9]+/cat_height=1024/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=2560/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 +echo "[INFO] Set min size" +sed -i -E 's/^cat_height=[0-9]+/cat_height=16/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=32/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 15 + +echo "[INFO] Set max size (no antialiasing)" +sed -i -E 's/^overlay_position=.*/overlay_position=top/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=Links/' "$CONFIG" sed -i -E 's/^cat_height=[0-9]+/cat_height=1024/' "$CONFIG" sed -i -E 's/^overlay_height=[0-9]+/overlay_height=2560/' "$CONFIG" echo "[INFO] Send SIGUSR2" diff --git a/src/config/config.cpp b/src/config/config.cpp index 2d5656c4..731c8acd 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -17,7 +17,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #ifdef FEATURE_DM_EMBEDDED_ASSETS # include "dm_config_parse_animation_name.h" @@ -73,6 +82,9 @@ static inline constexpr int MAX_OFFSET = 16000; static inline constexpr int MIN_MOVEMENT_RADIUS = 0; static inline constexpr int MAX_MOVEMENT_RADIUS = MAX_OFFSET / 2; +static inline constexpr int MIN_HOTPLUG_SCAN_INTERVAL_MS = 0; +static inline constexpr int MAX_HOTPLUG_SCAN_INTERVAL_MS = 3600 * 1000; + static inline constexpr int MIN_CUSTOM_FRAMES = 0; static inline constexpr int MAX_CUSTOM_FRAMES = 512; static inline constexpr int MIN_CUSTOM_ROWS = 0; @@ -103,6 +115,7 @@ static inline constexpr platform::time_sec_t DEFAULT_TEST_ANIMATION_INTERVAL_SEC static inline constexpr int32_t DEFAULT_ENABLE_ANTIALIASING = 1; static inline constexpr double DEFAULT_MOVEMENT_WAIT_FACTOR = 5.1; static inline constexpr int32_t DEFAULT_ENABLE_HAND_MAPPING = 1; +//static inline constexpr int32_t DEFAULT_HOTPLUG_SCAN_INTERVAL_MS = 300; // Debug-specific defaults #ifndef NDEBUG @@ -157,6 +170,9 @@ static inline constexpr auto SCREEN_WIDTH_KEY = "screen_width"; static inline constexpr auto MONITOR_KEY = "monitor"; static inline constexpr auto OUTPUT_NAME_KEY = "output_name"; // monitor alt key static inline constexpr auto ENABLE_HAND_MAPPING_KEY = "enable_hand_mapping"; +static inline constexpr auto HOTPLUG_SCAN_INTERVAL_KEY = "hotplug_scan_interval"; +static inline constexpr auto DISABLE_FULLSCREEN_HIDE_KEY = "disable_fullscreen_hide"; +static inline constexpr auto KEYBOARD_NAME_KEY = "keyboard_name"; static inline constexpr auto CUSTOM_SPRITE_SHEET_FILENAME_KEY = "custom_sprite_sheet_filename"; static inline constexpr auto CUSTOM_IDLE_FRAMES_KEY = "custom_idle_frames"; @@ -258,6 +274,7 @@ static uint64_t config_validate_timing(config_t& config) { ret |= config_clamp_int(config.idle_sleep_timeout_sec, 0, MAX_SLEEP_TIMEOUT_SEC, IDLE_SLEEP_TIMEOUT_KEY); ret |= config_clamp_int(config.input_fps, 0, MAX_FPS, INPUT_FPS_KEY); ret |= config_clamp_int(config.movement_speed, 0, MAX_DURATION_MS, MOVEMENT_SPEED_KEY); + ret |= config_clamp_int(config.hotplug_scan_interval_ms, MIN_HOTPLUG_SCAN_INTERVAL_MS, MAX_HOTPLUG_SCAN_INTERVAL_MS, HOTPLUG_SCAN_INTERVAL_KEY); // Validate interval (0 is allowed to disable) if (config.test_animation_interval_sec < 0 || config.test_animation_interval_sec > MAX_INTERVAL_SEC) { @@ -272,6 +289,7 @@ static uint64_t config_validate_timing(config_t& config) { config.animation_speed_ms = (config.animation_speed_ms < 0) ? 0 : MAX_INTERVAL_SEC * 1000; ret = (1u << 2); } + return ret; } @@ -562,27 +580,27 @@ static uint64_t config_validate_custom(config_t& config) { } // validate sprite sheet file - if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR && strlen(config.custom_sprite_sheet_filename) != 0) { + if (config.custom_sprite_sheet_filename) { constexpr size_t PNG_SIGNATURE_SIZE = 8; constexpr unsigned char PNG_SIGNATURE[PNG_SIGNATURE_SIZE] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}; // Try to open the file - platform::FileDescriptor fd(open(config.custom_sprite_sheet_filename, O_RDONLY)); + platform::FileDescriptor fd(open(config.custom_sprite_sheet_filename.c_str(), O_RDONLY)); if (fd._fd < 0) { BONGOCAT_LOG_ERROR("Custom Sprite Sheet doesn't exist or can't be opened: %s", - config.custom_sprite_sheet_filename); + config.custom_sprite_sheet_filename.c_str()); ret |= (1u << 6); return ret; } struct stat st; if (fstat(fd._fd, &st) < 0) { - BONGOCAT_LOG_ERROR("Custom Sprite Sheet can't be opened: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Custom Sprite Sheet can't be opened: %s", config.custom_sprite_sheet_filename.c_str()); ret |= (1u << 7); return ret; } if (st.st_size == 0) { - BONGOCAT_LOG_ERROR("Custom Sprite Sheet is an empty file: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Custom Sprite Sheet is an empty file: %s", config.custom_sprite_sheet_filename.c_str()); ret |= (1u << 8); return ret; } @@ -590,12 +608,12 @@ static uint64_t config_validate_custom(config_t& config) { unsigned char header[PNG_SIGNATURE_SIZE] = {0}; const ssize_t n = read(fd._fd, header, PNG_SIGNATURE_SIZE); if (n < static_cast(PNG_SIGNATURE_SIZE)) { - BONGOCAT_LOG_ERROR("Failed to read PNG header: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Failed to read PNG header: %s", config.custom_sprite_sheet_filename.c_str()); ret |= (1u << 9); return ret; } if (memcmp(header, PNG_SIGNATURE, PNG_SIGNATURE_SIZE) != 0) { - BONGOCAT_LOG_ERROR("Invalid PNG signature: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Invalid PNG signature: %s", config.custom_sprite_sheet_filename.c_str()); ret |= (1u << 10); return ret; } @@ -876,6 +894,7 @@ static bongocat_error_t config_validate(config_t& config) { config.enable_antialiasing = config.enable_antialiasing >= 1 ? 1 : 0; config.enable_movement_debug = config.enable_movement_debug >= 1 ? 1 : 0; config.enable_hand_mapping = config.enable_hand_mapping >= 1 ? 1 : 0; + config.disable_fullscreen_hide = config.disable_fullscreen_hide >= 1 ? 1 : 0; ret |= config_validate_dimensions(config); ret |= config_validate_timing(config); @@ -916,14 +935,17 @@ static bongocat_error_t config_add_keyboard_device(config_t& config, const char assert(static_cast(new_num_keyboard_devices) <= input::MAX_INPUT_DEVICES); // Add new device path - config.keyboard_devices[old_num_keyboard_devices] = strdup(device_path); - if (config.keyboard_devices[old_num_keyboard_devices] == BONGOCAT_NULLPTR) [[unlikely]] { + config.keyboard_devices[old_num_keyboard_devices] = duplicate_string(device_path); + if (!config.keyboard_devices[old_num_keyboard_devices]) [[unlikely]] { // free new copied strings for (int i = 0; i < old_num_keyboard_devices; i++) { + /* if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { ::free(config.keyboard_devices[i]); config.keyboard_devices[i] = BONGOCAT_NULLPTR; } + */ + release_allocated_string(config.keyboard_devices[i]); } config.num_keyboard_devices = old_num_keyboard_devices; BONGOCAT_LOG_ERROR("Failed to copy new keyboard device path"); @@ -936,17 +958,124 @@ static bongocat_error_t config_add_keyboard_device(config_t& config, const char return bongocat_error_t::BONGOCAT_SUCCESS; } +static bongocat_error_t config_add_keyboard_name(config_t& config, const char *device_path) { + BONGOCAT_CHECK_NULL(device_path, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + + assert(config._num_keyboard_names >= 0 && config._num_keyboard_names < INT_MAX - 1); + + const int old_num_keyboard_names = config._num_keyboard_names; + + static_assert(input::MAX_INPUT_DEVICES <= INT_MAX); + if (old_num_keyboard_names >= static_cast(input::MAX_INPUT_DEVICES)) { + BONGOCAT_LOG_WARNING("Can not add more keyboard_names from config, max. reach: %d", input::MAX_INPUT_DEVICES); + return config._strict ? bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM : bongocat_error_t::BONGOCAT_SUCCESS; + } + const int new_num_keyboard_names = old_num_keyboard_names + 1; + assert(new_num_keyboard_names >= 0); + assert(static_cast(new_num_keyboard_names) <= input::MAX_INPUT_DEVICES); + + // Add new device path + config._keyboard_names[old_num_keyboard_names] = duplicate_string(device_path); + if (!config._keyboard_names[old_num_keyboard_names]) [[unlikely]] { + // free new copied strings + for (int i = 0; i < old_num_keyboard_names; i++) { + /* + if (config._keyboard_names[i] != BONGOCAT_NULLPTR) { + ::free(config._keyboard_names[i]); + config._keyboard_names[i] = BONGOCAT_NULLPTR; + } + */ + release_allocated_string(config._keyboard_names[i]); + } + config._num_keyboard_names = old_num_keyboard_names; + BONGOCAT_LOG_ERROR("Failed to copy new keyboard name path"); + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + // update new size + config._num_keyboard_names = new_num_keyboard_names; + + return bongocat_error_t::BONGOCAT_SUCCESS; +} + static void config_cleanup_devices(config_t& config) { assert(config.num_keyboard_devices >= 0); for (size_t i = 0; i < input::MAX_INPUT_DEVICES; i++) { if (i < static_cast(config.num_keyboard_devices)) { + /* if (config.keyboard_devices[i] != BONGOCAT_NULLPTR) { ::free(config.keyboard_devices[i]); config.keyboard_devices[i] = BONGOCAT_NULLPTR; } + */ + release_allocated_string(config.keyboard_devices[i]); } } config.num_keyboard_devices = 0; + + assert(config._num_keyboard_names >= 0); + for (size_t i = 0; i < static_cast(config._num_keyboard_names); i++) { + if (i < static_cast(config._num_keyboard_names)) { + /* + if (config._keyboard_names[i] != BONGOCAT_NULLPTR) { + ::free(config._keyboard_names[i]); + config._keyboard_names[i] = BONGOCAT_NULLPTR; + } + */ + release_allocated_string(config._keyboard_names[i]); + } + } + config._num_keyboard_names = 0; +} + + +static bongocat_error_t config_resolve_devices(config_t& config) { + DIR *dir = opendir("/dev/input"); + if (dir == nullptr) { + BONGOCAT_LOG_WARNING("Failed to open /dev/input for scanning: %s", + strerror(errno)); + return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; + } + + struct dirent *entry; + char path[PATH_MAX]; + char name[256] = {0}; + + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "event", 5) != 0) { + continue; + } + + snprintf(path, sizeof(path), "/dev/input/%s", entry->d_name); + platform::FileDescriptor fd = platform::FileDescriptor(open(path, O_RDONLY)); + if (fd._fd < 0) { + continue; + } + + bool matched = false; + memset(name, 0, sizeof(name)); + if (ioctl(fd._fd, EVIOCGNAME(sizeof(name) - 1), name) >= 0) { + name[sizeof(name) - 1] = '\0'; + assert(config._num_keyboard_names >= 0); + for (size_t i = 0; i < static_cast(config._num_keyboard_names); i++) { + assert(config._keyboard_names[i]); + if (strstr(name, config._keyboard_names[i].c_str()) != nullptr) { + BONGOCAT_LOG_INFO("Found device matching name '%s' (Device: '%s'): %s", config._keyboard_names[i].c_str(), name, path); + matched = true; + break; + } + } + } + + if (matched) { + config_add_keyboard_device(config, path); + } + + //platform::close_fd(fd); + } + + closedir(dir); + return bongocat_error_t::BONGOCAT_SUCCESS; } // ============================================================================= @@ -1046,6 +1175,10 @@ static bongocat_error_t config_parse_integer_key(config_t& config, const char *k config.screen_width = int_value; } else if (strcmp(key, ENABLE_HAND_MAPPING_KEY) == 0) { config.enable_hand_mapping = int_value; + } else if (strcmp(key, HOTPLUG_SCAN_INTERVAL_KEY) == 0) { + config.hotplug_scan_interval_ms = int_value; + } else if (strcmp(key, DISABLE_FULLSCREEN_HIDE_KEY) == 0) { + config.disable_fullscreen_hide = int_value; } else if (strcmp(key, CUSTOM_IDLE_FRAMES_KEY) == 0) { config.custom_sprite_sheet_settings.idle_frames = int_value; } else if (strcmp(key, CUSTOM_BORING_FRAMES_KEY) == 0) { @@ -1221,13 +1354,10 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const load_config_overwrite_parameters_t& overwrite_parameters) { using namespace assets; if (strcmp(key, MONITOR_KEY) == 0 || strcmp(key, OUTPUT_NAME_KEY) == 0) { - if (config.output_name != BONGOCAT_NULLPTR) { - ::free(config.output_name); - config.output_name = BONGOCAT_NULLPTR; - } + release_allocated_string(config.output_name); if (value != BONGOCAT_NULLPTR && value[0] != '\0') { - config.output_name = strdup(value); - if (config.output_name == BONGOCAT_NULLPTR) { + config.output_name = duplicate_string(value); + if (!config.output_name) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to allocate memory for interface output"); return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } @@ -1235,13 +1365,10 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c config.output_name = BONGOCAT_NULLPTR; } } else if (strcmp(key, CUSTOM_SPRITE_SHEET_FILENAME_KEY) == 0) { - if (config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR) { - ::free(config.custom_sprite_sheet_filename); - config.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; - } + release_allocated_string(config.custom_sprite_sheet_filename); if (value != BONGOCAT_NULLPTR && value[0] != '\0') { - config.custom_sprite_sheet_filename = strdup(value); - if (config.custom_sprite_sheet_filename == BONGOCAT_NULLPTR) { + config.custom_sprite_sheet_filename = duplicate_string(value); + if (!config.custom_sprite_sheet_filename) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to allocate memory for custom sprite sheet filename"); return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } @@ -1289,16 +1416,10 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c } // set config._animation_name - if (config._animation_name != BONGOCAT_NULLPTR) { - ::free(config._animation_name); - config._animation_name = BONGOCAT_NULLPTR; - } - config._animation_name = value != BONGOCAT_NULLPTR ? strdup(value) : BONGOCAT_NULLPTR; + release_allocated_string(config._animation_name); + config._animation_name = value != BONGOCAT_NULLPTR ? duplicate_string(value) : make_null_string(); - if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { - ::free(config._loaded_animation_fqname); - config._loaded_animation_fqname = BONGOCAT_NULLPTR; - } + release_allocated_string(config._loaded_animation_fqname); // reset state config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::None; @@ -1316,7 +1437,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c strcmp(value, BONGOCAT_FQNAME) == 0) { config.animation_index = BONGOCAT_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; - config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); + config._loaded_animation_fqname = duplicate_string(BONGOCAT_FQNAME); } animation_found = config.animation_index >= 0; @@ -1349,12 +1470,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_dm(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_dm(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_dm(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1366,12 +1489,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_dm20(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_dm20(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_dm20(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1383,12 +1508,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_dmx(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_dmx(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_dmx(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1400,12 +1527,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_dmc(config, value); if (found_index >= 0) { assert(config.animation_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_dmc(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_dmc(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1417,12 +1546,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_pen(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_pen(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_pen(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1434,12 +1565,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_pen20(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_pen20(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_pen20(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1451,12 +1584,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_dmall(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_dmall(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_dmall(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1471,11 +1606,13 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c strcmp(value, CLIPPY_FQNAME) == 0) { config.animation_index = CLIPPY_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(CLIPPY_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(CLIPPY_FQNAME); } #ifdef FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS /// @NOTE(assets): 4. add more MS Agents here @@ -1484,33 +1621,39 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c strcmp(value, LINKS_FQNAME) == 0) { config.animation_index = LINKS_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(LINKS_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(LINKS_FQNAME); } // Rover if (strcmp(value, ROVER_NAME) == 0 || strcmp(value, ROVER_ID) == 0 || strcmp(value, ROVER_FQID) == 0 || strcmp(value, ROVER_FQNAME) == 0) { config.animation_index = ROVER_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(ROVER_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(ROVER_FQNAME); } // Merlin if (strcmp(value, MERLIN_NAME) == 0 || strcmp(value, MERLIN_ID) == 0 || strcmp(value, MERLIN_FQID) == 0 || strcmp(value, MERLIN_FQNAME) == 0) { config.animation_index = MERLIN_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::MsAgent; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(MERLIN_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(MERLIN_FQNAME); } #endif @@ -1525,12 +1668,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_pkmn(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_pkmn(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_pkmn(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1545,12 +1690,14 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c const int found_index = config_parse_animation_name_pmd(config, value); if (found_index >= 0) { assert(found_index >= 0); + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } + */ config._loaded_animation_fqname = - strdup(get_config_animation_name_pmd(static_cast(found_index)).fqname); + duplicate_string(get_config_animation_name_pmd(static_cast(found_index)).fqname); BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); } animation_found = config.animation_index >= 0; @@ -1566,11 +1713,13 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c config.animation_index = MISC_NEKO_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; config.animation_custom_set = config_animation_custom_set_t::misc; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(MISC_NEKO_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(MISC_NEKO_FQNAME); animation_found = config.animation_index >= 0; } } @@ -1583,18 +1732,17 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c config.animation_index = CUSTOM_ANIM_INDEX; config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Custom; config.animation_custom_set = config_animation_custom_set_t::custom; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = config.custom_sprite_sheet_filename != BONGOCAT_NULLPTR - ? strdup(config.custom_sprite_sheet_filename) - : BONGOCAT_NULLPTR; + */ + config._loaded_animation_fqname = duplicate_string(config.custom_sprite_sheet_filename); animation_found = config.animation_index >= 0; config._custom = config.animation_index == CUSTOM_ANIM_INDEX; - if (config.custom_sprite_sheet_filename == BONGOCAT_NULLPTR || - strlen(config.custom_sprite_sheet_filename) <= 0) { + if (!config.custom_sprite_sheet_filename) { BONGOCAT_LOG_WARNING("custom_sprite_sheet_filename required for custom sprite sheet"); animation_found = false; } @@ -1619,11 +1767,13 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c config.animation_sprite_sheet_layout = config_animation_sprite_sheet_layout_t::Bongocat; config.animation_dm_set = config_animation_dm_set_t::None; config.animation_custom_set = config_animation_custom_set_t::None; + /* if (config._loaded_animation_fqname != BONGOCAT_NULLPTR) { ::free(config._loaded_animation_fqname); config._loaded_animation_fqname = BONGOCAT_NULLPTR; } - config._loaded_animation_fqname = strdup(BONGOCAT_FQNAME); + */ + config._loaded_animation_fqname = duplicate_string(BONGOCAT_FQNAME); } } else { return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; // Unknown key @@ -1653,6 +1803,10 @@ static bongocat_error_t config_parse_key_value(config_t& config, const char *key return bongocat_error_t::BONGOCAT_SUCCESS; } + // Handle device keys + if (strcmp(key, KEYBOARD_NAME_KEY) == 0) { + return config_add_keyboard_name(config, value); + } // Handle device keys if (strcmp(key, KEYBOARD_DEVICE_KEY) == 0 || strcmp(key, KEYBOARD_DEVICES_KEY) == 0) { return config_add_keyboard_device(config, value); @@ -1806,6 +1960,13 @@ void set_defaults(config_t& config) { cfg.screen_width = 0; cfg.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; cfg.custom_sprite_sheet_settings = {}; + //cfg.hotplug_scan_interval_ms = DEFAULT_HOTPLUG_SCAN_INTERVAL_MS; + + for (int i = 0; i < static_cast(input::MAX_INPUT_DEVICES); i++) { + cfg._keyboard_names[i] = BONGOCAT_NULLPTR; + } + cfg._num_keyboard_names = 0; + cfg._keep_old_animation_index = false; cfg._strict = false; cfg._custom = false; @@ -1834,26 +1995,26 @@ static void config_log_summary(const config_t& config) { // when loaded by default, _loaded_animation_fqname is not set // assert(config._loaded_animation_fqname); BONGOCAT_LOG_DEBUG(" Cat: '%s' %dx%d at offset (%d,%d)", - config._loaded_animation_fqname != nullptr ? config._loaded_animation_fqname : BONGOCAT_FQNAME, + config._loaded_animation_fqname ? config._loaded_animation_fqname.c_str() : BONGOCAT_FQNAME, config.cat_height, (config.cat_height * BONGOCAT_FRAME_WIDTH) / BONGOCAT_FRAME_HEIGHT, config.cat_x_offset, config.cat_y_offset); break; case config_animation_sprite_sheet_layout_t::Dm: assert(config._loaded_animation_fqname); - BONGOCAT_LOG_DEBUG(" dm: '%s' %03d/%03d (set=%d) at offset (%d,%d)", config._loaded_animation_fqname, + BONGOCAT_LOG_DEBUG(" dm: '%s' %03d/%03d (set=%d) at offset (%d,%d)", config._loaded_animation_fqname.c_str(), config.animation_index, DM_ANIMATIONS_COUNT, config.animation_dm_set, config.cat_x_offset, config.cat_y_offset); break; case config_animation_sprite_sheet_layout_t::Pkmn: assert(config._loaded_animation_fqname); static_assert(PKMN_ANIMATIONS_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" pkmn: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, + BONGOCAT_LOG_DEBUG(" pkmn: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname.c_str(), config.animation_index, PKMN_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); break; case config_animation_sprite_sheet_layout_t::MsAgent: assert(config._loaded_animation_fqname); static_assert(MS_AGENTS_ANIMATIONS_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" MS Agent: '%s' %02d/%02d at offset (%d,%d)", config._loaded_animation_fqname, + BONGOCAT_LOG_DEBUG(" MS Agent: '%s' %02d/%02d at offset (%d,%d)", config._loaded_animation_fqname.c_str(), config.animation_index, MS_AGENTS_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); break; case config_animation_sprite_sheet_layout_t::Custom: @@ -1863,19 +2024,19 @@ static void config_log_summary(const config_t& config) { case config_animation_custom_set_t::misc: assert(config._loaded_animation_fqname); static_assert(MISC_ANIM_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" Misc: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname, + BONGOCAT_LOG_DEBUG(" Misc: '%s' %03d/%03d at offset (%d,%d)", config._loaded_animation_fqname.c_str(), config.animation_index, MISC_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); break; case config_animation_custom_set_t::pmd: assert(config._loaded_animation_fqname); static_assert(PMD_ANIM_COUNT <= INT32_MAX); - BONGOCAT_LOG_DEBUG(" pkmn pmd: '%s' %04d/%04d at offset (%d,%d)", config._loaded_animation_fqname, + BONGOCAT_LOG_DEBUG(" pkmn pmd: '%s' %04d/%04d at offset (%d,%d)", config._loaded_animation_fqname.c_str(), config.animation_index, PMD_ANIMATIONS_COUNT, config.cat_x_offset, config.cat_y_offset); break; case config_animation_custom_set_t::custom: assert(config.custom_sprite_sheet_filename); assert(config._custom); - BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename, config.cat_x_offset, + BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename.c_str(), config.cat_x_offset, config.cat_y_offset); break; } @@ -1888,7 +2049,7 @@ static void config_log_summary(const config_t& config) { BONGOCAT_LOG_DEBUG(" Alignment: %d", config.cat_align, config.cat_align == align_type_t::ALIGN_CENTER ? "(center)" : ""); BONGOCAT_LOG_DEBUG(" Layer: %s", config.layer == layer_type_t::LAYER_TOP ? "top" : "overlay"); - BONGOCAT_LOG_DEBUG(" Output Screen: %s", config.output_name); + BONGOCAT_LOG_DEBUG(" Output Screen: %s", config.output_name.c_str()); } // ============================================================================= @@ -1914,11 +2075,8 @@ created_result_t load(const char *config_file_path, load_config_overwr } if (overwrite_parameters.output_name != BONGOCAT_NULLPTR) { - if (ret.output_name != BONGOCAT_NULLPTR) { - ::free(ret.output_name); - ret.output_name = BONGOCAT_NULLPTR; - } - ret.output_name = strdup(overwrite_parameters.output_name); + //release_allocated_string(ret.output_name); + ret.output_name = duplicate_string(overwrite_parameters.output_name); } if (overwrite_parameters.randomize_index >= 0) { ret.randomize_index = overwrite_parameters.randomize_index >= 1 ? 1 : 0; @@ -1931,6 +2089,14 @@ created_result_t load(const char *config_file_path, load_config_overwr ret.input_fps = ret.fps; } + // Resolve keyboard_name entries to device paths. + // Continue on scan failure so static keyboard_device entries still work. + result = config_resolve_devices(ret); + if (result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_WARNING("Failed to resolve keyboard names, continuing: %s", + bongocat::error_string(result)); + } + // Set default keyboard device if none specified if (ret.num_keyboard_devices == 0) { if (!ret._strict) { @@ -1974,4 +2140,39 @@ void reset(config_t& config) { config_cleanup_devices(config); set_defaults(config); } + +AllocatedString resolve_path(const char *explicit_path) { + if (explicit_path != nullptr) { + return duplicate_string(explicit_path); + } + + char path[PATH_MAX]; + + // 1. $XDG_CONFIG_HOME/bongocat/bongocat.conf + const char *xdg_config = getenv("XDG_CONFIG_HOME"); + if (xdg_config != nullptr && xdg_config[0] != '\0') { + snprintf(path, sizeof(path), "%s/bongocat/bongocat.conf", xdg_config); + if (access(path, R_OK) == 0) { + return duplicate_string(path); + } + } + + // 2. ~/.config/bongocat/bongocat.conf + const char *home = getenv("HOME"); + if (home != nullptr && home[0] != '\0') { + snprintf(path, sizeof(path), "%s/.config/bongocat/bongocat.conf", home); + if (access(path, R_OK) == 0) { + return duplicate_string(path); + } + } + + // 3. ./bongocat.conf (CWD) + if (access("bongocat.conf", R_OK) == 0) { + return duplicate_string("bongocat.conf"); + } + + // No config found - will use defaults + return make_null_string(); +} + } // namespace bongocat::config \ No newline at end of file diff --git a/src/config/config_watcher.cpp b/src/config/config_watcher.cpp index 1062b3c1..2899231c 100644 --- a/src/config/config_watcher.cpp +++ b/src/config/config_watcher.cpp @@ -23,25 +23,27 @@ static inline constexpr platform::time_ms_t RELOAD_DELAY_MS = 100; static inline constexpr platform::time_ms_t RECREATE_SLEEP_ATTEMPT_MS = 100; static inline constexpr platform::time_ms_t TIMEOUT_MS = 100; -static constexpr uint32_t FILE_MASK = IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; +static constexpr uint32_t FILE_MASK = IN_CLOSE_WRITE | IN_MODIFY | IN_MOVED_TO | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; static constexpr uint32_t DIR_MASK = IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM; -static bongocat_error_t reinitialize_inotify(config_watcher_t& watcher) { +static bongocat_error_t config_watcher_add_watch(config_watcher_t& watcher) { if (watcher.inotify_fd._fd >= 0) { platform::close_fd(watcher.inotify_fd); } watcher.inotify_fd = platform::FileDescriptor(inotify_init1(IN_NONBLOCK | IN_CLOEXEC)); if (watcher.inotify_fd._fd < 0) { - BONGOCAT_LOG_ERROR("config_watcher: Failed to reinit inotify: %s", strerror(errno)); + if (features::Debug) { + BONGOCAT_LOG_ERROR("config_watcher: Failed to reinit inotify: %s", strerror(errno)); + } return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } watcher.wd_dir = - platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, dirname(watcher.config_path), DIR_MASK)); - watcher.wd_file = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK)); + platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, dirname(watcher.config_path.ptr), DIR_MASK)); + watcher.wd_file = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path.c_str(), FILE_MASK)); - if (watcher.wd_dir._fd < 0 || watcher.wd_file._fd < 0) { + if ((watcher.wd_dir._fd < 0 || watcher.wd_file._fd < 0) && features::Debug) { BONGOCAT_LOG_WARNING("config_watcher: partial reinit of inotify watches"); } @@ -62,11 +64,11 @@ static void *config_watcher_thread(void *arg) { }; constexpr int timeout_ms = TIMEOUT_MS; - BONGOCAT_LOG_INFO("config_watcher: Config watcher started for: %s", watcher.config_path); + BONGOCAT_LOG_INFO("config_watcher: Config watcher started for: %s", watcher.config_path.c_str()); atomic_store(&watcher._running, true); while (atomic_load(&watcher._running)) { - int ret = poll(fds, fds_count, timeout_ms); + const int ret = poll(fds, fds_count, timeout_ms); if (ret < 0) { if (errno == EINTR) { continue; @@ -74,6 +76,11 @@ static void *config_watcher_thread(void *arg) { BONGOCAT_LOG_ERROR("config_watcher: Config watcher poll failed: %s", strerror(errno)); break; } + if (watcher.inotify_fd._fd < 0) { + BONGOCAT_LOG_ERROR("inotify is not initialized"); + atomic_store(&watcher._running, false); + } + if (!atomic_load(&watcher._running)) { // draining pools for (size_t i = 0; i < fds_count; i++) { @@ -93,7 +100,7 @@ static void *config_watcher_thread(void *arg) { if (length <= 0) { if (errno == EINVAL || errno == EBADF) { BONGOCAT_LOG_WARNING("config_watcher: inotify fd invalidated (likely after suspend). Reinitializing..."); - reinitialize_inotify(watcher); + config_watcher_add_watch(watcher); continue; } BONGOCAT_LOG_ERROR("config_watcher: Config watcher read failed: %s", strerror(errno)); @@ -103,6 +110,7 @@ static void *config_watcher_thread(void *arg) { bool should_reload = false; bool file_went_away = false; bool file_recreated = false; + bool watch_invalidated = false; ssize_t i = 0; int attempts = 0; @@ -115,13 +123,14 @@ static void *config_watcher_thread(void *arg) { // File events if (event->wd == watcher.wd_file._fd) { - should_reload |= event->mask & (IN_MODIFY | IN_CLOSE_WRITE | IN_ATTRIB); + should_reload |= event->mask & (IN_CLOSE_WRITE | IN_MODIFY | IN_MOVED_TO | IN_ATTRIB); file_went_away |= event->mask & (IN_MOVE_SELF | IN_DELETE_SELF); + watch_invalidated |= event->mask & (IN_MOVE_SELF | IN_DELETE_SELF | IN_IGNORED); } // Directory events (watch for recreate) else if (event->wd == watcher.wd_dir._fd) { if ((event->mask & (IN_CREATE | IN_MOVED_TO)) && event->len > 0) { - file_recreated |= strcmp(event->name, basename(watcher.config_path)) == 0; + file_recreated |= strcmp(event->name, basename(watcher.config_path.ptr)) == 0; } } // Inotify queue overflow (critical) @@ -136,18 +145,38 @@ static void *config_watcher_thread(void *arg) { } // Handle file disappearance - if (file_went_away && watcher.wd_file._fd >= 0) { + if (file_went_away && watcher._running && watcher.wd_file._fd >= 0) { BONGOCAT_LOG_VERBOSE("config_watcher: Config file went away; removing file watch"); inotify_rm_watch(watcher.inotify_fd._fd, watcher.wd_file._fd); watcher.wd_file._fd = -1; // the file is gone, wait for recreation should_reload = false; } + // File can be replaced atomically; re-register watch on the new inode. + else if (watch_invalidated && watcher._running && watcher.wd_file._fd >= 0) { + watcher.wd_file._fd = -1; + bool rewatch_ok = false; + for (int retry = 0; retry < RECREATE_MAX_ATTEMPTS && watcher._running && watcher.wd_file._fd >= 0; retry++) { + const auto result = config_watcher_add_watch(watcher); + if (result == bongocat_error_t::BONGOCAT_SUCCESS) [[likely]] { + rewatch_ok = true; + BONGOCAT_LOG_VERBOSE("config_watcher: Re-armed config file watcher"); + break; + } + usleep(RECREATE_SLEEP_ATTEMPT_MS * 1000); + } + + if (!rewatch_ok) { + BONGOCAT_LOG_WARNING("config_watcher: Config watcher lost file watch; hot-reload may stop working"); + // the file is gone, wait for recreation + should_reload = false; + } + } // Handle recreation if (file_recreated) { BONGOCAT_LOG_VERBOSE("config_watcher: Config file recreated; re-adding file watch"); - int new_wd = inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path, FILE_MASK); + int new_wd = inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path.c_str(), FILE_MASK); if (new_wd >= 0) { watcher.wd_file = platform::FileDescriptor(new_wd); new_wd = -1; @@ -158,6 +187,7 @@ static void *config_watcher_thread(void *arg) { } } + // ensure file exists (from IN_CLOSE_WRITE/IN_MODIFY) if (should_reload) { // If we don't currently have a file watch (it was removed), try stat the file @@ -168,7 +198,7 @@ static void *config_watcher_thread(void *arg) { // small retry loop to handle race where create races with our handling struct stat st{}; for (int attempt = 0; attempt < RECREATE_MAX_ATTEMPTS; attempt++) { - if (stat(watcher.config_path, &st) == 0) { + if (stat(watcher.config_path.c_str(), &st) == 0) { file_exists = true; break; } @@ -218,7 +248,7 @@ created_result_t> create_watcher(const char *c } // Store config path - ret->config_path = strdup(config_path); + ret->config_path = duplicate_string(config_path); if (!ret->config_path) [[unlikely]] { return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } @@ -236,13 +266,13 @@ created_result_t> create_watcher(const char *c dirbuf[dirbuf_size - 1] = '\0'; const char *dir = dirname(dirbuf); if (!dir) [[unlikely]] { - BONGOCAT_LOG_ERROR("dirname() failed for path %s", ret->config_path); + BONGOCAT_LOG_ERROR("dirname() failed for path %s", ret->config_path.c_str()); return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } - ret->wd_file = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, ret->config_path, FILE_MASK)); + ret->wd_file = platform::FileDescriptor(inotify_add_watch(ret->inotify_fd._fd, ret->config_path.c_str(), FILE_MASK)); if (ret->wd_file._fd < 0) { - BONGOCAT_LOG_ERROR("Failed to add inotify watch for file %s: %s", ret->config_path, strerror(errno)); + BONGOCAT_LOG_ERROR("Failed to add inotify watch for file %s: %s", ret->config_path.c_str(), strerror(errno)); return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } diff --git a/src/core/main.cpp b/src/core/main.cpp index d3b989ff..29bdc2d2 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -55,13 +55,12 @@ struct main_context_t { AllocatedMemory animation; AllocatedMemory wayland; - const char *signal_watch_path{BONGOCAT_NULLPTR}; + AllocatedString signal_watch_path{BONGOCAT_NULLPTR}; atomic_uint64_t config_generation{0}; platform::CondVariable configs_reloaded_cond; platform::Mutex sync_configs; - char *pid_filename{BONGOCAT_NULLPTR}; - char *default_config_filename{BONGOCAT_NULLPTR}; + AllocatedString pid_filename{BONGOCAT_NULLPTR}; main_context_t() = default; ~main_context_t() { @@ -167,15 +166,7 @@ void cleanup(main_context_t& context) { // cleanup signals handler platform::close_fd(context.signal_fd); - if (context.pid_filename != BONGOCAT_NULLPTR) { - ::free(context.pid_filename); - context.pid_filename = BONGOCAT_NULLPTR; - } - - if (context.default_config_filename != BONGOCAT_NULLPTR) { - ::free(context.default_config_filename); - context.default_config_filename = BONGOCAT_NULLPTR; - } + release_allocated_string(context.pid_filename); } inline main_context_t& get_main_context() { @@ -371,7 +362,7 @@ static bool config_devices_changed(const config::config_t& old_config, const con for (int i = 0; i < new_config.num_keyboard_devices; i++) { bool found = false; for (int j = 0; j < old_config.num_keyboard_devices; j++) { - if (strcmp(new_config.keyboard_devices[i], old_config.keyboard_devices[j]) == 0) { + if (strcmp(new_config.keyboard_devices[i].c_str(), old_config.keyboard_devices[j].c_str()) == 0) { found = true; break; } @@ -387,13 +378,13 @@ static bool config_devices_changed(const config::config_t& old_config, const con static void config_reload_callback() { assert(get_main_context().input != BONGOCAT_NULLPTR); assert(get_main_context().animation != BONGOCAT_NULLPTR); - assert(get_main_context().signal_watch_path != BONGOCAT_NULLPTR); - BONGOCAT_LOG_INFO("Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path, - (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path : "OFF"); + assert(get_main_context().signal_watch_path); + BONGOCAT_LOG_INFO("Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path.c_str(), + (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path.c_str() : "OFF"); assert(get_main_context().config_watcher == BONGOCAT_NULLPTR || - strcmp(get_main_context().config_watcher->config_path, get_main_context().signal_watch_path) == 0); + strcmp(get_main_context().config_watcher->config_path.c_str(), get_main_context().signal_watch_path.c_str()) == 0); - if (strcmp(get_main_context().signal_watch_path, "-") == 0) { + if (strcmp(get_main_context().signal_watch_path.c_str(), "-") == 0) { BONGOCAT_LOG_WARNING("No reload config for stdin"); BONGOCAT_LOG_INFO("Keeping current configuration"); return; @@ -401,7 +392,7 @@ static void config_reload_callback() { // Create a temporary config to test loading auto [new_config, error] = - config::load(get_main_context().signal_watch_path, get_main_context().overwrite_config_parameters); + config::load(get_main_context().signal_watch_path.c_str(), get_main_context().overwrite_config_parameters); if (error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to reload config: %s", bongocat::error_string(error)); BONGOCAT_LOG_INFO("Keeping current configuration"); @@ -572,46 +563,6 @@ static bongocat_error_t start_config_watcher(main_context_t& ctx, const char *co return error; } -static char *default_config_file_path() { - const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); - const char *home = getenv("HOME"); - - if (xdg_config_home != BONGOCAT_NULLPTR) { - const size_t len = strlen(xdg_config_home) + 1 + strlen(DEFAULT_CONF_FILENAME) + 1; - char *path = static_cast(::malloc(len)); - if (path == BONGOCAT_NULLPTR) [[unlikely]] { - return BONGOCAT_NULLPTR; - } - snprintf(path, len, "%s/%s", xdg_config_home, DEFAULT_CONF_FILENAME); - - if (access(path, F_OK) == 0) { - return path; // file exists - } - - free(path); - path = BONGOCAT_NULLPTR; - } - - if (home != BONGOCAT_NULLPTR) { - const size_t len = strlen(home) + strlen("/.config/") + strlen(DEFAULT_CONF_FILENAME) + 1; - char *path = static_cast(::malloc(len)); - if (path == BONGOCAT_NULLPTR) [[unlikely]] { - return BONGOCAT_NULLPTR; - } - snprintf(path, len, "%s/.config/%s", home, DEFAULT_CONF_FILENAME); - - if (access(path, F_OK) == 0) { - return path; // file exists - } - - free(path); - path = BONGOCAT_NULLPTR; - } - - // If neither env var is set, fallback to just filename in current dir - return strdup(DEFAULT_CONF_FILENAME); -} - // ============================================================================= // SIGNAL HANDLING MODULE // ============================================================================= @@ -750,7 +701,7 @@ static bongocat_error_t system_initialize_components(main_context_t& ctx) { stop_threads(ctx); BONGOCAT_LOG_INFO("Performing cleanup..."); - process_remove_pid_file(ctx.pid_filename); + process_remove_pid_file(ctx.pid_filename.c_str()); // clean up context before global cleanup (log mutex, etc.) cleanup(ctx); @@ -779,8 +730,8 @@ static void cli_show_help(const char *program_name) { printf(" -w, --watch-config Watch config file for changes and reload automatically\n"); printf(" -t, --toggle Toggle bongocat on/off (start if not running, stop if running)\n"); printf(" -o, --output-name NAME Specify output name (overwrite output_name from config)\n"); - printf( - " --random Enable random animation_index, at start (overwrite random_index from config)\n"); + printf(" -m, --monitor NAME Bind to a specific monitor output (same as --output-name)\n"); + printf(" --random Enable random animation_index, at start (overwrite random_index from config)\n"); printf(" --strict Enable strict mode, only start up with a valid config and valid parameter\n"); printf(" --nr NR Specify Nr. for PID file to avoid conflicting ruinning instances\n"); printf(" --ignore-running Ignore current running instance\n"); @@ -871,7 +822,8 @@ static created_result_t cli_parse_arguments(int argc, char *argv[]) BONGOCAT_LOG_ERROR("--nr option requires a number"); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } - } else if (strcmp(argv[i], "--output-name") == 0 || strcmp(argv[i], "-o") == 0) { + } else if ((strcmp(argv[i], "--output-name") == 0 || strcmp(argv[i], "-o") == 0) || + strcmp(argv[i], "--monitor") == 0 || strcmp(argv[i], "-m") == 0) { args.output_name_set = true; if (i + 1 < argc) { args.output_name = argv[i + 1]; @@ -924,30 +876,19 @@ int main(int argc, char *argv[]) { .randomize_index = args.randomize_index, .strict = args.strict, }; - if (args.config_file == BONGOCAT_NULLPTR) { - /// @TODO: RAII default_config_filename string - get_main_context().default_config_filename = default_config_file_path(); - } - const char *config_file = - args.config_file == BONGOCAT_NULLPTR ? get_main_context().default_config_filename : args.config_file; + AllocatedString existing_config_file = config::resolve_path(args.config_file); + const char* config_file = existing_config_file ? existing_config_file.c_str() : ""; if (args.strict >= 1) { if (strcmp(config_file, "-") != 0 && access(config_file, F_OK) != 0) { BONGOCAT_LOG_ERROR("Configuration file required: %s", config_file); - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } return EXIT_FAILURE; } } + BONGOCAT_LOG_VERBOSE("Try to load Configuration file: %s", config_file); auto [config, config_error] = config::load(config_file, ctx.overwrite_config_parameters); if (config_error != bongocat_error_t::BONGOCAT_SUCCESS) { BONGOCAT_LOG_ERROR("Failed to load configuration: %s", bongocat::error_string(config_error)); - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } return EXIT_FAILURE; } ctx.config = bongocat::move(config); @@ -956,18 +897,10 @@ int main(int argc, char *argv[]) { // validate args if (config._strict) { if (args.nr_set && args.nr < 0) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } BONGOCAT_LOG_ERROR("--nr needs to be a positive number"); return EXIT_FAILURE; } if (args.output_name_set && (args.output_name == BONGOCAT_NULLPTR || strlen(args.output_name) <= 0)) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } BONGOCAT_LOG_ERROR("--output_name value is missing"); return EXIT_FAILURE; } @@ -977,84 +910,68 @@ int main(int argc, char *argv[]) { // set pid file, based on nr const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr) + 1; assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != BONGOCAT_NULLPTR) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr); + ctx.pid_filename = make_allocated_string(static_cast(needed_size)); + if (ctx.pid_filename) { + snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr); } - } else if (ctx.config.output_name != BONGOCAT_NULLPTR && ctx.config.output_name[0] != '\0') { + } else if (ctx.config.output_name.length() > 0) { // set pid file, based on output_name if (!args.ignore_running) { - const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name) + 1; + const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name.c_str()) + 1; assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != BONGOCAT_NULLPTR) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_TEMPLATE, - ctx.config.output_name); + ctx.pid_filename = make_allocated_string(static_cast(needed_size)); + if (ctx.pid_filename) { + snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_TEMPLATE, + ctx.config.output_name.c_str()); } } else { - const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name, + const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name.c_str(), platform::slow_rand()) + 1; assert(needed_size >= 0); - ctx.pid_filename = static_cast(::malloc(static_cast(needed_size))); - if (ctx.pid_filename != BONGOCAT_NULLPTR) { - snprintf(ctx.pid_filename, static_cast(needed_size), PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, - ctx.config.output_name, platform::slow_rand()); + ctx.pid_filename = make_allocated_string(static_cast(needed_size)); + if (ctx.pid_filename) { + snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, + ctx.config.output_name.c_str(), platform::slow_rand()); } } - if (ctx.pid_filename == BONGOCAT_NULLPTR) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } + if (!ctx.pid_filename) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to allocate PID filename"); return EXIT_FAILURE; } } else { - ctx.pid_filename = strdup(DEFAULT_PID_FILE); + ctx.pid_filename = duplicate_string(DEFAULT_PID_FILE); } // Handle toggle mode if (args.toggle_mode) { - BONGOCAT_LOG_INFO("Toggle... (pid=%s)", ctx.pid_filename); - if (const int toggle_result = process_handle_toggle(argv[0], ctx.pid_filename); toggle_result >= 0) { + BONGOCAT_LOG_INFO("Toggle... (pid=%s)", ctx.pid_filename.c_str()); + if (const int toggle_result = process_handle_toggle(argv[0], ctx.pid_filename.c_str()); toggle_result >= 0) { return toggle_result; // Either successfully toggled off or error } // toggle_result == -1 means continue with startup } // Create PID file to track this instance - const platform::FileDescriptor pid_fd = process_create_pid_file(ctx.pid_filename); + const platform::FileDescriptor pid_fd = process_create_pid_file(ctx.pid_filename.c_str()); if (pid_fd._fd < 0) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } BONGOCAT_LOG_ERROR("Failed to create PID file"); return EXIT_FAILURE; } if (!args.ignore_running) { if (pid_fd._fd == -2) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } BONGOCAT_LOG_ERROR("Another instance of bongocat is already running"); return EXIT_FAILURE; } } - BONGOCAT_LOG_INFO("PID file created: %s", ctx.pid_filename); + BONGOCAT_LOG_INFO("PID file created: %s", ctx.pid_filename.c_str()); BONGOCAT_LOG_INFO("bongocat PID: %d", getpid()); // Setup signal handlers - ctx.signal_watch_path = config_file; + ctx.signal_watch_path = duplicate_string(config_file); const bongocat_error_t signal_result = signal_setup_handlers(ctx); if (signal_result != bongocat_error_t::BONGOCAT_SUCCESS) { - if (args.config_file == BONGOCAT_NULLPTR && get_main_context().default_config_filename != BONGOCAT_NULLPTR) { - ::free(get_main_context().default_config_filename); - get_main_context().default_config_filename = BONGOCAT_NULLPTR; - } BONGOCAT_LOG_ERROR("Failed to setup signal handlers: %s", bongocat::error_string(signal_result)); return EXIT_FAILURE; } diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index db039968..3572fad1 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -82,25 +82,25 @@ namespace bongocat::animation { namespace details { created_result_t anim_load_custom_animation(animation_thread_context_t& ctx, const config::config_t& config) { - BONGOCAT_CHECK_NULL(config.custom_sprite_sheet_filename, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); - if (strlen(config.custom_sprite_sheet_filename) <= 0) { + BONGOCAT_CHECK_NULL(config.custom_sprite_sheet_filename.c_str(), bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + if (strlen(config.custom_sprite_sheet_filename.c_str()) <= 0) { return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } assert(ctx.shm.ptr); assert(ctx._local_copy_config.ptr); - BONGOCAT_LOG_VERBOSE("Load custom Animation: %s ...", config.custom_sprite_sheet_filename); - auto sprite_sheet_image_result = load_custom_sprite_sheet_file(config.custom_sprite_sheet_filename); + BONGOCAT_LOG_VERBOSE("Load custom Animation: %s ...", config.custom_sprite_sheet_filename.c_str()); + auto sprite_sheet_image_result = load_custom_sprite_sheet_file(config.custom_sprite_sheet_filename.c_str()); if (sprite_sheet_image_result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename.c_str()); return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; } auto result = load_custom_anim(ctx, sprite_sheet_image_result.result, config.custom_sprite_sheet_settings); free_custom_sprite_sheet_file(sprite_sheet_image_result.result); if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename); + BONGOCAT_LOG_ERROR("Load custom Animation failed: %s", config.custom_sprite_sheet_filename.c_str()); return bongocat_error_t::BONGOCAT_ERROR_ANIMATION; } diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index 96740c24..f6650e8e 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -127,7 +127,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w // Skip fullscreen hiding when layer is LAYER_OVERLAY (always visible) const bool is_overlay_layer = current_config.layer == config::layer_type_t::LAYER_OVERLAY; - const bool is_fullscreen = !is_overlay_layer && wayland_ctx._fullscreen_detected; + const bool is_fullscreen = !is_overlay_layer && (current_config.disable_fullscreen_hide <= 0 && wayland_ctx._fullscreen_detected); const bool bar_visible = !is_fullscreen && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; diff --git a/src/image_loader/custom/load_custom.cpp b/src/image_loader/custom/load_custom.cpp index 383e312d..ae82fa5e 100644 --- a/src/image_loader/custom/load_custom.cpp +++ b/src/image_loader/custom/load_custom.cpp @@ -16,15 +16,12 @@ created_result_t load_custom_sprite_sheet_file(const cha assets::custom_image_t ret; ret.data = bongocat::move(file_content_result.result); - ret.name = ::strdup(filename); + ret.name = duplicate_string(filename); return ret; } void free_custom_sprite_sheet_file(assets::custom_image_t& image) noexcept { platform::release_allocated_mmap_file_content(image.data); - if (image.name != BONGOCAT_NULLPTR) { - ::free(image.name); - image.name = BONGOCAT_NULLPTR; - } + release_allocated_string(image.name); } created_result_t @@ -34,7 +31,7 @@ load_custom_anim(const animation_thread_context_t& ctx, const assets::custom_ima return load_custom_anim(ctx, assets::embedded_image_t{.data = sprite_sheet_image.data.data, .size = static_cast(sprite_sheet_image.data._size_bytes), - .name = sprite_sheet_image.name != BONGOCAT_NULLPTR ? sprite_sheet_image.name : ""}, + .name = sprite_sheet_image.name ? sprite_sheet_image.name.c_str() : ""}, sprite_sheet_settings); } created_result_t diff --git a/src/platform/input.cpp b/src/platform/input.cpp index 3d0b7703..b94dc72b 100644 --- a/src/platform/input.cpp +++ b/src/platform/input.cpp @@ -21,6 +21,7 @@ namespace bongocat::platform::input { static inline constexpr size_t INPUT_EVENT_BUF = 128; static inline constexpr size_t MAX_DEVICE_FDS = 256; +static inline constexpr size_t MAX_ACTIVE_DEVICE_FDS = 32; inline static constexpr int MAX_ATTEMPTS = 2048; static inline constexpr auto INPUT_POOL_TIMEOUT_MS = 10; @@ -75,10 +76,7 @@ static input_hand_mapping_t get_hand_mapping_form_keycode([[maybe_unused]] const static void cleanup_input_devices_paths(input_context_t& input, size_t device_paths_count) { for (size_t i = 0; i < device_paths_count; i++) { - if (input._device_paths[i] != BONGOCAT_NULLPTR) { - ::free(input._device_paths[i]); - input._device_paths[i] = BONGOCAT_NULLPTR; - } + release_allocated_string(input._device_paths[i]); } release_allocated_array(input._device_paths); } @@ -237,7 +235,7 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { input._unique_paths_indices.count = input._unique_paths_indices_capacity; size_t num_unique_devices = 0; for (size_t i = 0; i < input._device_paths.count; i++) { - const char *device_path = input._device_paths[i]; + const auto& device_path = input._device_paths[i]; // Resolve to canonical path char resolved[PATH_MAX]; @@ -245,17 +243,17 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { input_unique_file_type_t new_type = input_unique_file_type_t::File; struct stat lst{}; - if (lstat(device_path, &lst) == 0 && S_ISLNK(lst.st_mode)) { + if (lstat(device_path.c_str(), &lst) == 0 && S_ISLNK(lst.st_mode)) { new_type = input_unique_file_type_t::Symlink; - if (realpath(device_path, resolved) != BONGOCAT_NULLPTR) { + if (realpath(device_path.c_str(), resolved) != BONGOCAT_NULLPTR) { candidate = resolved; } else { - BONGOCAT_LOG_WARNING("Broken symlink: %s", device_path); + BONGOCAT_LOG_WARNING("Broken symlink: %s", device_path.c_str()); broken_symlink++; continue; } } else { - candidate = device_path; + candidate = device_path.c_str(); } // Skip if non-existent @@ -269,7 +267,7 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { bool duplicate = false; for (size_t j = 0; j < num_unique_devices; j++) { const input_unique_file_t& prev = input._unique_devices[j]; - if (prev.canonical_path != BONGOCAT_NULLPTR && strcmp(prev.canonical_path, candidate) == 0) { + if (prev.canonical_path && strcmp(prev.canonical_path.c_str(), candidate) == 0) { duplicate = true; break; } @@ -284,11 +282,11 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { // Decide if we need to replace real_device_path bool need_reopen = false; bool need_replace = false; - if (cur.canonical_path != BONGOCAT_NULLPTR) { - need_replace = strcmp(cur.canonical_path, candidate) != 0; + if (cur.canonical_path) { + need_replace = strcmp(cur.canonical_path.c_str(), candidate) != 0; } if (static_cast(cur.canonical_path) != static_cast(candidate)) { - need_reopen = cur.canonical_path == BONGOCAT_NULLPTR && candidate != BONGOCAT_NULLPTR; + need_reopen = !cur.canonical_path && candidate != BONGOCAT_NULLPTR; need_replace = true; } if (!need_reopen && cur.type != new_type) { @@ -308,23 +306,19 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { struct stat st{}; if (stat(candidate, &st) == 0 && S_ISCHR(st.st_mode)) { if (need_reopen) { - int fd = open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC); - if (fd >= 0 && is_open_device_valid(fd)) { - cur.fd = FileDescriptor(fd); + FileDescriptor fd (open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC)); + if (fd._fd >= 0 && is_open_device_valid(fd._fd)) { + cur.fd = bongocat::move(fd); BONGOCAT_LOG_INFO("Opened input device: %s (fd=%d)", candidate, cur.fd._fd); } } if (cur.fd._fd >= 0 && is_open_device_valid(cur.fd._fd)) { // Replace canonical path if changed if (need_replace) { - if (cur.canonical_path != BONGOCAT_NULLPTR) { - ::free(cur.canonical_path); - cur.canonical_path = BONGOCAT_NULLPTR; - } - cur.canonical_path = ::strdup(candidate); + cur.canonical_path = duplicate_string(candidate); } cur.type = new_type; - cur._device_path = device_path; + cur._device_path = device_path.c_str(); valid_devices++; } else { cleanup(cur); @@ -406,6 +400,9 @@ static void *input_thread(void *arg) { assert(input._local_copy_config); assert(input.update_config_efd._fd >= 0); + bool disable_adaptive_check_interval = false; + time_ms_t adaptive_check_interval_ms = START_ADAPTIVE_CHECK_INTERVAL_SEC * 1000; + // keep local copies of device_paths { // read-only config @@ -414,12 +411,12 @@ static void *input_thread(void *arg) { assert(current_config.num_keyboard_devices >= 0); const int device_paths_count = current_config.num_keyboard_devices; - const char *const *device_paths = current_config.keyboard_devices; + const auto *device_paths = current_config.keyboard_devices; assert(device_paths_count >= 0); - input._device_paths = make_allocated_array(static_cast(device_paths_count)); + input._device_paths = make_allocated_array(static_cast(device_paths_count)); for (size_t i = 0; i < input._device_paths.count; i++) { - input._device_paths[i] = strdup(device_paths[i]); - if (input._device_paths[i] == BONGOCAT_NULLPTR) { + input._device_paths[i] = duplicate_string(device_paths[i]); + if (!input._device_paths[i]) [[unlikely]] { atomic_store(&input._capture_input_running, false); cleanup_input_devices_paths(input, i); cleanup_input_thread_context(input); @@ -427,6 +424,11 @@ static void *input_thread(void *arg) { return BONGOCAT_NULLPTR; } } + + if (current_config.hotplug_scan_interval_ms > 0) { + adaptive_check_interval_ms = current_config.hotplug_scan_interval_ms; + disable_adaptive_check_interval = true; + } } BONGOCAT_LOG_DEBUG("input: Starting input capture on %d devices", input._device_paths.count); @@ -467,13 +469,13 @@ static void *input_thread(void *arg) { // local thread context int check_counter = 0; // check is done periodically - time_sec_t adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; /// event poll // 0: reload config event // 1: udev monitor fd - // 2 - n: device events + // 2 - m: device events + // m+1 - n: device events (keyboard_names) // last: stdin (optional) - constexpr size_t MAX_PFDS = 2 + MAX_DEVICE_FDS + ((features::Debug) ? 1 : 0); + constexpr size_t MAX_PFDS = 2 + MAX_DEVICE_FDS + MAX_ACTIVE_DEVICE_FDS + ((features::Debug) ? 1 : 0); pollfd pfds[MAX_PFDS]; input_event ev[INPUT_EVENT_BUF]; @@ -689,15 +691,15 @@ static void *input_thread(void *arg) { break; } if (poll_result == 0) { - // Timeout — adaptive device checking + // @TODO: device checking with scan_interval check_counter++; - if (check_counter >= (adaptive_check_interval_sec * (1000 / 100))) { + if (check_counter >= (adaptive_check_interval_ms / timeout)) { check_counter = 0; bool found_new_device = false; for (size_t i = 0; i < input._unique_devices.count; i++) { - const char *device_path = input._unique_devices[i].canonical_path; + const auto& device_path = input._unique_devices[i].canonical_path; bool need_reopen = false; - if (device_path == BONGOCAT_NULLPTR) { + if (!device_path) { continue; } // If an fd is already open, check if it is still valid @@ -710,7 +712,7 @@ static void *input_thread(void *arg) { struct stat old_st{}; if (input._unique_devices[i].fd._fd >= 0 && fstat(input._unique_devices[i].fd._fd, &old_st) == 0) { struct stat new_st{}; - if (stat(device_path, &new_st) == 0) { + if (stat(device_path.c_str(), &new_st) == 0) { if (old_st.st_rdev != new_st.st_rdev) { need_reopen = true; } @@ -728,32 +730,34 @@ static void *input_thread(void *arg) { close_fd(input._unique_devices[i].fd); } - if (int new_fd = open(device_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); new_fd >= 0) { + if (int new_fd = open(device_path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC); new_fd >= 0) { if (is_open_device_valid(new_fd)) { input._unique_devices[i].fd = FileDescriptor(new_fd); new_fd = -1; found_new_device = true; - BONGOCAT_LOG_INFO("input: New input device detected and opened: %s (fd=%d)", device_path, + BONGOCAT_LOG_INFO("input: New input device detected and opened: %s (fd=%d)", device_path.c_str(), input._unique_devices[i].fd._fd); } else { - // Not a valid char device — close immediately + // Not a valid char device - close immediately close(new_fd); - BONGOCAT_LOG_VERBOSE("input: vFile opened but not a char device: %s", device_path); + BONGOCAT_LOG_VERBOSE("input: vFile opened but not a char device: %s", device_path.c_str()); } } else { - BONGOCAT_LOG_VERBOSE("input: Failed to open input device: %s (%s)", device_path, strerror(errno)); + BONGOCAT_LOG_VERBOSE("input: Failed to open input device: %s (%s)", device_path.c_str(), strerror(errno)); } } } - if (!found_new_device && adaptive_check_interval_sec < MAX_ADAPTIVE_CHECK_INTERVAL_SEC) { - adaptive_check_interval_sec = (adaptive_check_interval_sec < MID_ADAPTIVE_CHECK_INTERVAL_SEC) - ? MID_ADAPTIVE_CHECK_INTERVAL_SEC - : MAX_ADAPTIVE_CHECK_INTERVAL_SEC; - BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", adaptive_check_interval_sec); - } else if (found_new_device && adaptive_check_interval_sec > START_ADAPTIVE_CHECK_INTERVAL_SEC) { - adaptive_check_interval_sec = START_ADAPTIVE_CHECK_INTERVAL_SEC; - BONGOCAT_LOG_DEBUG("input: Reset device check interval to %d seconds", START_ADAPTIVE_CHECK_INTERVAL_SEC); + if (!disable_adaptive_check_interval) { + if (!found_new_device && adaptive_check_interval_ms < MAX_ADAPTIVE_CHECK_INTERVAL_SEC*1000) { + adaptive_check_interval_ms = (adaptive_check_interval_ms < MID_ADAPTIVE_CHECK_INTERVAL_SEC*1000) + ? MID_ADAPTIVE_CHECK_INTERVAL_SEC*1000 + : MAX_ADAPTIVE_CHECK_INTERVAL_SEC*1000; + BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", adaptive_check_interval_ms/1000); + } else if (found_new_device && adaptive_check_interval_ms > START_ADAPTIVE_CHECK_INTERVAL_SEC*1000) { + adaptive_check_interval_ms = START_ADAPTIVE_CHECK_INTERVAL_SEC*1000; + BONGOCAT_LOG_DEBUG("input: Reset device check interval to %d seconds", START_ADAPTIVE_CHECK_INTERVAL_SEC); + } } } continue; @@ -811,7 +815,7 @@ static void *input_thread(void *arg) { bool sync_devices_needed = false; bool reload_devices_needed = false; if (pfds[fds_udev_monitor_index].revents & POLLIN) { - BONGOCAT_LOG_DEBUG("input: Receive udev event"); + //BONGOCAT_LOG_VERBOSE("input: Receive udev event"); size_t attempts = 0; udev_device *dev = BONGOCAT_NULLPTR; while ((dev = udev_monitor_receive_device(input._udev_mon)) != BONGOCAT_NULLPTR && attempts < MAX_ATTEMPTS) { @@ -824,6 +828,7 @@ static void *input_thread(void *arg) { sync_devices_needed = true; reload_devices_needed = true; } + BONGOCAT_LOG_VERBOSE("input: Receive udev event (action=%s)", action); } udev_device_unref(dev); diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index d08f14da..3dfdb4eb 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -40,7 +40,8 @@ static_assert(POOL_MAX_TIMEOUT_MS >= POOL_MIN_TIMEOUT_MS); inline static constexpr time_ms_t COND_INIT_TIMEOUT_MS = 5000; -static inline constexpr auto WAYLAND_LAYER_NAME = "OVERLAY"; +static inline constexpr auto WAYLAND_LAYER_NAME_OVERLAY = "OVERLAY"; +static inline constexpr auto WAYLAND_LAYER_NAME_TOP = "TOP"; // ============================================================================= // MAIN WAYLAND INTERFACE IMPLEMENTATION @@ -448,9 +449,9 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, (ctx.thread_context._local_copy_config->screen_width > 0 && old_width != ctx.thread_context._local_copy_config->screen_width); const bool change_screen = - ctx.thread_context._local_copy_config->output_name != BONGOCAT_NULLPTR && - (strcmp(old_screen_name, ctx.thread_context._local_copy_config->output_name) != 0 || - strcmp(ctx.thread_context._output_name_str, ctx.thread_context._local_copy_config->output_name) != 0); + ctx.thread_context._local_copy_config->output_name && + (strcmp(old_screen_name, ctx.thread_context._local_copy_config->output_name.c_str()) != 0 || + strcmp(ctx.thread_context._output_name_str, ctx.thread_context._local_copy_config->output_name.c_str()) != 0); if (((dimensions_changed && old_height > 0 && old_width > 0) || change_screen) && ctx.thread_context.ctx_shm) { // ~~Lock animation mutex to prevent draw_bar() during config update @@ -538,8 +539,11 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, } } -const char *get_current_layer_name() { - return WAYLAND_LAYER_NAME; +const char *get_current_layer_name(wayland_context_t& ctx) { + if (ctx.thread_context.ctx_shm && (atomic_load(&ctx.thread_context.ctx_shm->configured) && ctx.thread_context._local_copy_config)) { + return ctx.thread_context._local_copy_config->layer == config::layer_type_t::LAYER_OVERLAY ? WAYLAND_LAYER_NAME_OVERLAY : WAYLAND_LAYER_NAME_TOP; + } + return WAYLAND_LAYER_NAME_TOP; } bongocat_error_t request_render(animation::animation_context_t& animation_ctx) { diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp index fdbbbe00..cb85b5f3 100644 --- a/src/platform/wayland_setups.cpp +++ b/src/platform/wayland_setups.cpp @@ -85,10 +85,10 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { wayland_ctx.output = BONGOCAT_NULLPTR; wayland_ctx.bound_output_name = 0; wayland_ctx.using_named_output = false; - if (current_config.output_name != BONGOCAT_NULLPTR) { + if (current_config.output_name) { for (size_t i = 0; i < ctx.output_count; ++i) { if (has_flag(ctx.outputs[i].received, output_ref_received_flags_t::Name) && - strcmp(ctx.outputs[i].name_str, current_config.output_name) == 0) { + strcmp(ctx.outputs[i].name_str, current_config.output_name.c_str()) == 0) { wayland_ctx.output = ctx.outputs[i].wl_output; wayland_ctx._output_name_str = ctx.outputs[i].name_str; wayland_ctx._screen_info = &ctx.screen_infos[i]; @@ -101,10 +101,10 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { if (wayland_ctx.output == BONGOCAT_NULLPTR) { if (current_config._strict) { - BONGOCAT_LOG_ERROR("Could not find output named '%s'", current_config.output_name); + BONGOCAT_LOG_ERROR("Could not find output named '%s'", current_config.output_name.c_str()); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } else { - BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", current_config.output_name); + BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", current_config.output_name.c_str()); } } } diff --git a/src/utils/error.cpp b/src/utils/error.cpp index b38e9a70..a40f5b9b 100644 --- a/src/utils/error.cpp +++ b/src/utils/error.cpp @@ -47,6 +47,7 @@ namespace details { assert(name_len > 0); platform::LockGuard guard(get_log_mutex()); + char message[1024]; log_timestamp(stdout); fprintf(stdout, "%.*s: ", name_len, name); vfprintf(stdout, format, args); From c04d5cb877e05df404bacb5c04dea62129307050 Mon Sep 17 00:00:00 2001 From: furudbat Date: Thu, 2 Apr 2026 12:17:58 +0200 Subject: [PATCH 03/17] fix: nixos build --- CMakeLists.txt | 103 +++++++++++++------------- Dockerfiles/nix-test | 44 ++++-------- README.md | 2 +- nix/common.nix | 2 +- nix/default.nix | 33 +++++---- nix/home-module.nix | 2 +- nix/nixos-module.nix | 2 +- nix/shell.nix | 6 +- scripts/test_nix_build.sh | 75 +++++++++---------- scripts/test_nix_build_docker.sh | 5 ++ scripts/test_toggle.sh | 119 +++++++++++++++++++------------ 11 files changed, 204 insertions(+), 189 deletions(-) create mode 100755 scripts/test_nix_build_docker.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index ff9a14de..ba35c849 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,7 @@ endif() project( bongocat LANGUAGES C CXX - VERSION 3.6.1) + VERSION 3.6.2) # Feature Flags include(CMakeDependentOption) @@ -354,26 +354,31 @@ if(FEATURE_MULTI_VERSIONS) # @NOTE(assets): 1.2. add exec for multi versions endif() -# Formatting -include(cmake/CPM.cmake) -cpmaddpackage( - NAME - Format.cmake - VERSION - 1.8.3 - GITHUB_REPOSITORY - TheLartians/Format.cmake - OPTIONS - # set to yes skip cmake formatting - "FORMAT_SKIP_CMAKE NO" - # set to yes skip clang formatting - "FORMAT_SKIP_CLANG NO" - # path to exclude (optional, supports regular expressions) - "CMAKE_FORMAT_EXCLUDE cmake/CPM.cmake" - # extra arguments for cmake_format (optional) - "CMAKE_FORMAT_EXTRA_ARGS -c ${PROJECT_SOURCE_DIR}/cmake-format.yaml" - # path to exclude (optional, supports regular expressions) - "CMAKE_FORMAT_EXCLUDE lib|assets|protocols|docs|examples|nix") +option(SKIP_CPM "Skip CPM-based downloads" OFF) +if(DEFINED ENV{NIX_BUILD_TOP} OR SKIP_CPM) + message(STATUS "Nix build detected: skipping CPM and formatting setup") +else() + # Formatting + include(cmake/CPM.cmake) + cpmaddpackage( + NAME + Format.cmake + VERSION + 1.8.3 + GITHUB_REPOSITORY + TheLartians/Format.cmake + OPTIONS + # set to yes skip cmake formatting + "FORMAT_SKIP_CMAKE NO" + # set to yes skip clang formatting + "FORMAT_SKIP_CLANG NO" + # path to exclude (optional, supports regular expressions) + "CMAKE_FORMAT_EXCLUDE cmake/CPM.cmake" + # extra arguments for cmake_format (optional) + "CMAKE_FORMAT_EXTRA_ARGS -c ${PROJECT_SOURCE_DIR}/cmake-format.yaml" + # path to exclude (optional, supports regular expressions) + "CMAKE_FORMAT_EXCLUDE lib|assets|protocols|docs|examples|nix") +endif() # Install include(GNUInstallDirs) @@ -459,33 +464,35 @@ if(PANDOC) endif() # Package -cpmaddpackage("gh:TheLartians/PackageProject.cmake@1.13.0") -set(CPACK_PACKAGE_NAME "bongocat") -set(CPACK_PACKAGE_VERSION "3.6.1") -set(CPACK_PACKAGE_CONTACT "hircreacc@gmail.com") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A delightful Wayland overlay that displays an animated V-Pet reacting to your keyboard input! ") -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") -set(CPACK_GENERATOR "TGZ;ZIP") -packageproject( - # the name of the target to export - NAME - ${PROJECT_NAME} - # the version of the target to export - VERSION - ${PROJECT_VERSION} - # a temporary directory to create the config files - BINARY_DIR - ${PROJECT_BINARY_DIR} - # should match the target's INSTALL_INTERFACE include directory - INCLUDE_DESTINATION - include/${PROJECT_NAME}-${PROJECT_VERSION} - # (optional) define the project's version compatibility, defaults to `AnyNewerVersion` supported values: - # `AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion` - COMPATIBILITY - SameMajorVersion - # (optional) option to generate CPack variables - CPACK - YES) +if(NOT DEFINED ENV{NIX_BUILD_TOP} AND NOT SKIP_CPM) + cpmaddpackage("gh:TheLartians/PackageProject.cmake@1.13.0") + set(CPACK_PACKAGE_NAME "bongocat") + set(CPACK_PACKAGE_VERSION "3.6.1") + set(CPACK_PACKAGE_CONTACT "hircreacc@gmail.com") + set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A delightful Wayland overlay that displays an animated V-Pet reacting to your keyboard input! ") + set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") + set(CPACK_GENERATOR "TGZ;ZIP") + packageproject( + # the name of the target to export + NAME + ${PROJECT_NAME} + # the version of the target to export + VERSION + ${PROJECT_VERSION} + # a temporary directory to create the config files + BINARY_DIR + ${PROJECT_BINARY_DIR} + # should match the target's INSTALL_INTERFACE include directory + INCLUDE_DESTINATION + include/${PROJECT_NAME}-${PROJECT_VERSION} + # (optional) define the project's version compatibility, defaults to `AnyNewerVersion` supported values: + # `AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion` + COMPATIBILITY + SameMajorVersion + # (optional) option to generate CPack variables + CPACK + YES) +endif() # If MSVC is being used, and ASAN is enabled, we need to set the debugger environment so that it behaves well with MSVC's debugger, and we # can run the target from visual studio if(MSVC) get_all_installable_targets(all_targets) message("all_targets=${all_targets}") diff --git a/Dockerfiles/nix-test b/Dockerfiles/nix-test index a6b2a600..563dda13 100644 --- a/Dockerfiles/nix-test +++ b/Dockerfiles/nix-test @@ -1,36 +1,18 @@ -# vim: ft=Dockerfile -FROM ubuntu:24.04 +FROM nixos/nix:latest -# Install dependencies -RUN apt-get update && apt-get install -y curl git sudo xz-utils bzip2 build-essential && \ - rm -rf /var/lib/apt/lists/* - -# Create non-root user first -RUN useradd -m nixuser - -# Create /nix owned by nixuser -RUN mkdir -m 0755 /nix && chown nixuser:nixuser /nix - -# Switch to nixuser -USER nixuser -WORKDIR /home/nixuser - -# Install Nix (single-user) -RUN curl -L https://nixos.org/nix/install | sh +# Enable flakes +RUN mkdir -p /etc/nix && \ + echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf +ENV NIX_CONFIG="experimental-features = nix-command flakes" -# Set environment -ENV HOME=/home/nixuser -ENV PATH=/home/nixuser/.nix-profile/bin:/home/nixuser/.nix-profile/sbin:/home/nixuser/.nix-profile/libexec:/home/nixuser/.nix-profile/share:$PATH -ENV NIX_PATH=/home/nixuser/.nix-defexpr/channels +# Create working directory +WORKDIR /app -# Enable flakes -RUN mkdir -p /home/nixuser/.config/nix \ - && echo "experimental-features = nix-command flakes" > /home/nixuser/.config/nix/nix.conf +# Copy your repo +COPY . . -# Source nix.sh to update PATH -SHELL ["/bin/bash", "-c"] -RUN . /home/nixuser/.nix-profile/etc/profile.d/nix.sh +# Make script executable +RUN chmod +x scripts/test_nix_build.sh -# Optional: verify -RUN nix --version -RUN nix flake show github:NixOS/nixpkgs/nixos-unstable \ No newline at end of file +# Run the test +CMD ["./scripts/test_nix_build.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 12b3f9e5..96544b9c 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ``` -**Requirements:** wayland-client, wayland-protocols, gcc/clang, make, cmake +**Requirements:** wayland-client, wayland-protocols, gcc15 or clang19, make, cmake > [!CAUTION] > **Privacy Notice**: When building in `DEBUG` mode and by enabling `enable_debug=1`, all keystrokes are logged to stdout/stderr. Ensure this is disabled (default: 0) for normal usage. diff --git a/nix/common.nix b/nix/common.nix index e2fe5710..fdd9daec 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -245,4 +245,4 @@ in { config._bongocat = lib.mkIf cfg.enable { inherit cfg configFile; }; -} +} \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index de364ee2..66bfe541 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,17 +1,20 @@ { lib, - stdenv, + gcc15Stdenv, pkg-config, wayland, wayland-protocols, wayland-scanner, cmake, pandoc, - systemd + libffi, + systemd, + libcap, + libxkbcommon }: -stdenv.mkDerivation (finalAttrs: { +gcc15Stdenv.mkDerivation (finalAttrs: { pname = "wayland-vpets"; - version = "3.6.1"; + version = "3.6.2"; src = ../.; # Build toolchain and dependencies @@ -20,7 +23,10 @@ stdenv.mkDerivation (finalAttrs: { buildInputs = [ wayland wayland-protocols + libffi systemd + libxkbcommon + libcap ]; # Build phases @@ -29,19 +35,18 @@ stdenv.mkDerivation (finalAttrs: { export WAYLAND_PROTOCOLS_DIR="${wayland-protocols}/share/wayland-protocols" ''; - makeFlags = ["release"]; - installPhase = '' - runHook preInstall - - # Install binaries - install -Dm755 build/bongocat $out/bin/${finalAttrs.meta.mainProgram} - install -Dm755 scripts/find_input_devices.sh $out/bin/bongocat-find-devices - - runHook postInstall + postPatch = '' + grep -rl "/usr/share/wayland-protocols" . | while read f; do + substituteInPlace "$f" \ + --replace "/usr/share/wayland-protocols" "${wayland-protocols}/share/wayland-protocols" + done ''; cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" + "-DSKIP_CPM=ON" + "-DWAYLAND_PROTOCOLS_DIR=${wayland-protocols}/share/wayland-protocols" + "-DCMAKE_INSTALL_PREFIX=$out" ]; # Package information @@ -53,4 +58,4 @@ stdenv.mkDerivation (finalAttrs: { mainProgram = "bongocat"; platforms = lib.platforms.linux; }; -}) +}) \ No newline at end of file diff --git a/nix/home-module.nix b/nix/home-module.nix index f1e94495..1feb503c 100644 --- a/nix/home-module.nix +++ b/nix/home-module.nix @@ -41,4 +41,4 @@ in { }; }; }); -} +} \ No newline at end of file diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix index 72e519f4..2bb96563 100644 --- a/nix/nixos-module.nix +++ b/nix/nixos-module.nix @@ -38,4 +38,4 @@ in { }; }; }); -} +} \ No newline at end of file diff --git a/nix/shell.nix b/nix/shell.nix index d55837d8..0f524ed2 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -15,6 +15,7 @@ pkgs.mkShellNoCC { gdb # Debugger valgrind # Memory debugger clang-tools # Useful tools for C/C++ including a formatter `clang-format` + #cmake-format # Optional tools for input device debugging evtest @@ -23,7 +24,6 @@ pkgs.mkShellNoCC { buildInputs = with pkgs; [ wayland wayland-protocols - systemd ]; shellHook = '' # Ensure that the Makefile can find and access the Wayland protocols @@ -41,7 +41,7 @@ pkgs.mkShellNoCC { echo "" echo "Helper scripts:" echo " ./scripts/find_input_devices.sh - Find input devices" - echo " ./scripts/test-nix-build.sh - Test Nix flake and package" + echo " ./scripts/test_nix_build.sh - Test Nix flake and package" echo " ./scripts/test_toggle.sh - Test Bongocat toggle functionality (Install Bongocat first)" ''; -} +} \ No newline at end of file diff --git a/scripts/test_nix_build.sh b/scripts/test_nix_build.sh index 9d3eca77..7cdf5486 100755 --- a/scripts/test_nix_build.sh +++ b/scripts/test_nix_build.sh @@ -1,50 +1,39 @@ #!/usr/bin/env bash +# Test script for Nix builds set -euo pipefail -# Repository name (local test images) -REPO="bongocat-test" - -# Check if Docker login is needed -if ! docker info >/dev/null 2>&1; then - echo "Docker does not seem to be running. Please start Docker." - exit 1 +echo "🧪 Testing Nix builds for wayland-wpets" +echo "==========================================" + +# Test flake build +echo "📦 Testing flake build..." +if nix flake check --no-build 2>&1; then + echo "✅ Flake check: SUCCESS" + + if nix build --no-link 2>&1; then + echo "✅ Flake build: SUCCESS" + else + echo "❌ Flake build: FAILED" + exit 1 + fi +else + echo "⚠️ Nix flakes not available or flake invalid, skipping" fi -echo "Docker is running" - -TMP_DIR=$(mktemp -d) -trap 'rm -rf "$TMP_DIR"' EXIT - -DOCKERFILE="Dockerfiles/nix-test" -IMAGE_TAG="${REPO}:nix-test" - -if [[ ! -f "$DOCKERFILE" ]]; then - echo "Skipping $OS (Dockerfile not found: $DOCKERFILE)" - continue +# Test development shell +echo "" +echo "🔧 Testing development shell..." +if command -v nix-shell >/dev/null 2>&1; then + if nix-shell nix/shell.nix --run "echo 'Shell works'" >/dev/null 2>&1; then + echo "✅ Development shell: SUCCESS" + else + echo "❌ Development shell: FAILED" + exit 1 + fi +else + echo "⚠️ nix-shell not available, skipping" fi -echo "Building image for nix..." -docker build --rm -t "$IMAGE_TAG" -f "$DOCKERFILE" . - -echo "Running test build for nix..." -CONTAINER_ID=$(docker run -d "$IMAGE_TAG" sleep infinity) - -# Create filtered tarball and copy -tar --exclude='build' --exclude='cmake-build-*' --exclude='bin' --exclude='*.o' -czf ${TMP_DIR}/src.tar.gz . -docker cp ${TMP_DIR}/src.tar.gz "$CONTAINER_ID":/tmp/ -docker exec "$CONTAINER_ID" sh -c "mkdir -p /home/nixuser/src && tar -xzf /tmp/src.tar.gz -C /home/nixuser/src" -rm ${TMP_DIR}/src.tar.gz - -# Run test build inside container -docker exec "$CONTAINER_ID" sh -c " - cd /home/nixuser/src/; - . /home/nixuser/.nix-profile/etc/profile.d/nix.sh; - nix flake show; - nix flake check --print-build-logs; - nix build --print-build-logs -" - -# Clean up -docker rm -f "$CONTAINER_ID" - -echo "Done!" +echo "" +echo "🎉 All available Nix builds completed successfully!" +echo "" \ No newline at end of file diff --git a/scripts/test_nix_build_docker.sh b/scripts/test_nix_build_docker.sh new file mode 100755 index 00000000..880b1bf1 --- /dev/null +++ b/scripts/test_nix_build_docker.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +docker build -f Dockerfiles/nix-test -t bongocat-nix-test . +docker run --rm bongocat-nix-test \ No newline at end of file diff --git a/scripts/test_toggle.sh b/scripts/test_toggle.sh index 1976c553..c482c23c 100755 --- a/scripts/test_toggle.sh +++ b/scripts/test_toggle.sh @@ -1,62 +1,89 @@ -echo "Testing bongocat toggle functionality..." -echo +#!/usr/bin/env bash +set -euo pipefail -#make #PROGRAM="./cmake-build-debug/bongocat" PROGRAM="./build/bongocat" -trap 'killall bongocat 2>/dev/null' EXIT +if [[ ! -x $PROGRAM ]]; then + echo "Error: ./build/bongocat not found. Build first with: make" + exit 1 +fi + +if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then + echo "Skipping toggle test: WAYLAND_DISPLAY is not set." + exit 0 +fi + +if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" ]]; then + echo "Skipping toggle test: Wayland socket is not available." + exit 0 +fi + +show_processes() { + if ! pgrep -x -a bongocat; then + echo "No bongocat processes found" + fi +} -echo "[TEST] --- Toggle Functionality ---" +is_running() { + pgrep -x bongocat >/dev/null 2>&1 +} + +echo "Testing bongocat toggle functionality..." +echo # Start initial instance if [[ $# -ge 1 ]]; then - TOGGLE_PID="$1" - echo "[TEST] Using provided PID = $TOGGLE_PID" + TOGGLE_PID="$1" + echo "Using provided PID = $TOGGLE_PID" else - echo "[TEST] Starting program..." - "$PROGRAM" --toggle --config bongocat.conf.example & - TOGGLE_PID=$! - echo "[TEST] Program PID = $TOGGLE_PID" - sleep 20 -fi -echo "[TEST] Initial bongocat PID = $TOGGLE_PID" -sleep 2 + if is_running; then + echo "Pre-clean: existing bongocat instance detected, toggling it off first." + $PROGRAM --toggle --config bongocat.conf.example || true + sleep 1 + fi + + echo "1. Starting bongocat with --toggle (should start since not running):" + "$PROGRAM" --toggle --config bongocat.conf.example & + TOGGLE_PID=$! + echo "Program PID = $TOGGLE_PID" + sleep 2 -# Toggle off -echo "[TEST] Sending --toggle to stop the instance..." -"$PROGRAM" --toggle --config bongocat.conf.example -# Wait for shutdown -for i in {1..10}; do - if ! kill -0 "$TOGGLE_PID" 2>/dev/null; then break; fi - sleep 0.5 -done - -if kill -0 "$TOGGLE_PID" 2>/dev/null; then - echo "[FAIL] Toggle failed: Process $TOGGLE_PID still running!" - kill -9 "$TOGGLE_PID" 2>/dev/null || true + if kill -0 "$NEW_PID" 2>/dev/null; then + echo "New process $NEW_PID started successfully via toggle" + else + echo "Skipping toggle test: bongocat could not start (Wayland unavailable)." exit 1 -else - echo "[PASS] Process terminated successfully via toggle" + fi + sleep 2 fi -# Toggle on (should start new instance) -echo "[TEST] Sending --toggle to start a new instance..." -"$PROGRAM" --toggle --config bongocat.conf.example & -NEW_PID=$! + +echo +echo "2. Checking if bongocat is running:" +show_processes + +echo +echo "3. Toggling bongocat off (should stop the running instance):" +$PROGRAM --toggle --config bongocat.conf.example +sleep 1 + +echo +echo "4. Checking if bongocat is still running:" +show_processes + +echo +echo "5. Toggling bongocat on again (should start since not running):" +$PROGRAM --toggle --config bongocat.conf.example sleep 2 -if kill -0 "$NEW_PID" 2>/dev/null; then - echo "[PASS] New process $NEW_PID started successfully via toggle" -else - echo "[FAIL] Toggle failed to start new instance!" - exit 1 -fi +echo +echo "6. Final check - bongocat should be running:" +show_processes -# Clean up new instance -kill -TERM "$NEW_PID" -for i in {1..10}; do - if ! kill -0 "$NEW_PID" 2>/dev/null; then break; fi - sleep 0.5 -done -echo "[TEST] Toggle functionality test completed!" +echo +echo "7. Cleaning up - stopping bongocat:" +$PROGRAM --toggle --config bongocat.conf.example || true + +echo +echo "Toggle functionality test completed!" \ No newline at end of file From 6f36ee8520ba849e916c175875a7d1216ef7f18b Mon Sep 17 00:00:00 2001 From: furudbat Date: Mon, 6 Apr 2026 23:17:37 +0200 Subject: [PATCH 04/17] wip: merge from upstream - Replace gettimeofday() with clock_gettime() - Fix comment typos - Reduce INOTIFY_BUF_LEN - Add generated Wayland protocol files --- .gitignore | 5 - CMakeLists.txt | 7 + CMakePresets.json | 3 +- include/config/config_watcher.h | 2 +- include/platform/wayland_context.h | 2 +- nix/NIXOS.md | 2 +- protocols/CMakeLists.txt | 47 +- ...oreign-toplevel-management-unstable-v1.xml | 4 +- ...n-toplevel-management-v1-client-protocol.h | 615 +++++ ...-foreign-toplevel-management-v1-protocol.c | 107 + protocols/wlr-layer-shell-unstable-v1.xml | 518 ++-- .../xdg-output-unstable-v1-client-protocol.h | 706 +++++ protocols/xdg-output-unstable-v1-protocol.c | 94 + protocols/xdg-output-unstable-v1.xml | 518 ++-- protocols/xdg-shell-client-protocol.h | 2384 +++++++++++++++++ protocols/xdg-shell-protocol.c | 184 ++ protocols/xdg-shell.xml | 1418 ++++++++++ .../zwlr-layer-shell-v1-client-protocol.h | 415 +++ protocols/zwlr-layer-shell-v1-protocol.c | 79 + src/platform/wayland.cpp | 19 +- src/platform/wayland_callbacks.cpp | 4 +- src/utils/error.cpp | 10 +- src/utils/time.cpp | 6 +- 23 files changed, 6585 insertions(+), 564 deletions(-) create mode 100644 protocols/wlr-foreign-toplevel-management-v1-client-protocol.h create mode 100644 protocols/wlr-foreign-toplevel-management-v1-protocol.c create mode 100644 protocols/xdg-output-unstable-v1-client-protocol.h create mode 100644 protocols/xdg-output-unstable-v1-protocol.c create mode 100644 protocols/xdg-shell-client-protocol.h create mode 100644 protocols/xdg-shell-protocol.c create mode 100644 protocols/xdg-shell.xml create mode 100644 protocols/zwlr-layer-shell-v1-client-protocol.h create mode 100644 protocols/zwlr-layer-shell-v1-protocol.c diff --git a/.gitignore b/.gitignore index 6ce52b41..67122960 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,6 @@ result *.idb *.pdb -# Generated protocol files -*-protocol.c -*-protocol.h -*-client-protocol.h - # IDE files .vscode/ *.swp diff --git a/CMakeLists.txt b/CMakeLists.txt index ba35c849..90aaea54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,13 @@ option(ENABLE_ASAN "Enable Address Sanitizer" OFF) option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF) option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) +find_program(WAYLAND_SCANNER_EXECUTABLE wayland-scanner) +if (WAYLAND_SCANNER_EXECUTABLE) + option(GENERATE_PROTOCOLS "Generate Wayland protocol files" ON) +else() + option(GENERATE_PROTOCOLS "Generate Wayland protocol files" OFF) +endif() + # project_options More Warnings set(CLANG_WARNINGS -Wall diff --git a/CMakePresets.json b/CMakePresets.json index d2ac3690..1ee73ab4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -25,7 +25,8 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/cmake-build-release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "GENERATE_PROTOCOLS": "OFF" } }, { diff --git a/include/config/config_watcher.h b/include/config/config_watcher.h index f9888601..09ffa0d3 100644 --- a/include/config/config_watcher.h +++ b/include/config/config_watcher.h @@ -11,7 +11,7 @@ namespace bongocat::config { // Inotify buffer sizing inline static constexpr size_t INOTIFY_EVENT_SIZE = sizeof(struct inotify_event); -inline static constexpr size_t INOTIFY_BUF_LEN = 1024 * (INOTIFY_EVENT_SIZE + 16); +inline static constexpr size_t INOTIFY_BUF_LEN = 16 * (INOTIFY_EVENT_SIZE + 16); // ============================================================================= // TYPE DEFINITIONS diff --git a/include/platform/wayland_context.h b/include/platform/wayland_context.h index 15e961b7..7dd1cee5 100644 --- a/include/platform/wayland_context.h +++ b/include/platform/wayland_context.h @@ -20,7 +20,7 @@ inline static constexpr size_t OUTPUT_NAME_SIZE = 128; struct fullscreen_detector_t { struct zwlr_foreign_toplevel_manager_v1 *manager{BONGOCAT_NULLPTR}; bool has_fullscreen_toplevel{false}; - timeval last_check{}; + timespec last_check{}; }; struct tracked_toplevel_t { struct zwlr_foreign_toplevel_handle_v1 *handle{BONGOCAT_NULLPTR}; diff --git a/nix/NIXOS.md b/nix/NIXOS.md index 0ff4ec50..d621e69a 100644 --- a/nix/NIXOS.md +++ b/nix/NIXOS.md @@ -189,7 +189,7 @@ Ensure your compositor supports the layer shell protocol: - ✅ **Hyprland** - Full support - ✅ **Sway** - Full support - ✅ **Wayfire** - Compatible -- ✅ **KDE Wayland** - Compatiable +- ✅ **KDE Wayland** - Compatible - ❌ **GNOME Wayland** - Support Unknown ## Advanced Usage diff --git a/protocols/CMakeLists.txt b/protocols/CMakeLists.txt index 3c68ab86..f2e8d2e1 100644 --- a/protocols/CMakeLists.txt +++ b/protocols/CMakeLists.txt @@ -1,12 +1,18 @@ if(DEFINED ENV{WAYLAND_PROTOCOLS_DIR}) set(WAYLAND_PROTOCOLS_DIR "$ENV{WAYLAND_PROTOCOLS_DIR}" CACHE PATH "Path to wayland-protocols") + set(PROTOCOL_XML_XDG_EXTERNAL ${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml) else() set(WAYLAND_PROTOCOLS_DIR "/usr/share/wayland-protocols" CACHE PATH "Path to wayland-protocols") + set(PROTOCOL_XML_XDG_EXTERNAL ${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml) endif() set(PROTOCOL_XML_WLR ${PROTOCOLS_DIR}/wlr-layer-shell-unstable-v1.xml) -set(PROTOCOL_XML_XDG ${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml) +if(EXISTS "${PROTOCOL_XML_XDG_EXTERNAL}") + set(PROTOCOL_XML_XDG ${PROTOCOL_XML_XDG_EXTERNAL}) +else() + set(PROTOCOL_XML_XDG ${PROTOCOLS_DIR}/xdg-shell.xml) +endif() set(PROTOCOL_XML_WLR_FOREIGN ${PROTOCOLS_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml) set(PROTOCOL_XML_XDG_OUTPUT ${PROTOCOLS_DIR}/xdg-output-unstable-v1.xml) set(GENERATED_PROTOCOLS_SOURCES @@ -25,21 +31,26 @@ set(GENERATED_PROTOCOLS ${GENERATED_PROTOCOLS_SOURCES} ${GENERATED_PROTOCOLS_HEADERS} ) -find_program(WAYLAND_SCANNER_EXECUTABLE wayland-scanner REQUIRED) -add_custom_command(OUTPUT ${GENERATED_PROTOCOLS} - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_XDG} ${PROTOCOLS_DIR}/xdg-shell-client-protocol.h - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_XDG} ${PROTOCOLS_DIR}/xdg-shell-protocol.c - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_WLR} ${PROTOCOLS_DIR}/zwlr-layer-shell-v1-client-protocol.h - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_WLR} ${PROTOCOLS_DIR}/zwlr-layer-shell-v1-protocol.c - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOLS_DIR}/wlr-foreign-toplevel-management-v1-protocol.c - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOLS_DIR}/wlr-foreign-toplevel-management-v1-client-protocol.h - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_XDG_OUTPUT} ${PROTOCOLS_DIR}/xdg-output-unstable-v1-client-protocol.h - COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_XDG_OUTPUT} ${PROTOCOLS_DIR}/xdg-output-unstable-v1-protocol.c - DEPENDS ${PROTOCOL_XML_WLR} ${PROTOCOL_XML_XDG} ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOL_XML_XDG_OUTPUT} - COMMENT "Generating Wayland protocol files..." -) -add_custom_target(gen_protocols DEPENDS ${GENERATED_PROTOCOLS}) -set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${GENERATED_PROTOCOLS}") +if (GENERATE_PROTOCOLS) + set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${GENERATED_PROTOCOLS}") +endif () + +if (WAYLAND_SCANNER_EXECUTABLE) + add_custom_command(OUTPUT ${GENERATED_PROTOCOLS} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_XDG} ${PROTOCOLS_DIR}/xdg-shell-client-protocol.h + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_XDG} ${PROTOCOLS_DIR}/xdg-shell-protocol.c + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_WLR} ${PROTOCOLS_DIR}/zwlr-layer-shell-v1-client-protocol.h + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_WLR} ${PROTOCOLS_DIR}/zwlr-layer-shell-v1-protocol.c + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOLS_DIR}/wlr-foreign-toplevel-management-v1-protocol.c + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOLS_DIR}/wlr-foreign-toplevel-management-v1-client-protocol.h + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header ${PROTOCOL_XML_XDG_OUTPUT} ${PROTOCOLS_DIR}/xdg-output-unstable-v1-client-protocol.h + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${PROTOCOL_XML_XDG_OUTPUT} ${PROTOCOLS_DIR}/xdg-output-unstable-v1-protocol.c + DEPENDS ${PROTOCOL_XML_WLR} ${PROTOCOL_XML_XDG} ${PROTOCOL_XML_WLR_FOREIGN} ${PROTOCOL_XML_XDG_OUTPUT} + COMMENT "Generating Wayland protocol files..." + ) + add_custom_target(gen_protocols DEPENDS ${GENERATED_PROTOCOLS}) +endif() + add_library(protocols STATIC ${GENERATED_PROTOCOLS_SOURCES}) target_sources(protocols PRIVATE ${GENERATED_PROTOCOLS_SOURCES}) target_sources(protocols @@ -48,5 +59,7 @@ target_sources(protocols BASE_DIRS ${PROTOCOLS_DIR} FILES ${GENERATED_PROTOCOLS_HEADERS} ) -add_dependencies(protocols gen_protocols) +if (GENERATE_PROTOCOLS) + add_dependencies(protocols gen_protocols) +endif() target_include_directories(protocols SYSTEM PUBLIC ${PROTOCOLS_DIR}) diff --git a/protocols/wlr-foreign-toplevel-management-unstable-v1.xml b/protocols/wlr-foreign-toplevel-management-unstable-v1.xml index 44505bbb..d1b5c8a5 100644 --- a/protocols/wlr-foreign-toplevel-management-unstable-v1.xml +++ b/protocols/wlr-foreign-toplevel-management-unstable-v1.xml @@ -211,7 +211,7 @@ + summary="the provided rectangle is invalid"/> @@ -267,4 +267,4 @@ - + \ No newline at end of file diff --git a/protocols/wlr-foreign-toplevel-management-v1-client-protocol.h b/protocols/wlr-foreign-toplevel-management-v1-client-protocol.h new file mode 100644 index 00000000..12bc2858 --- /dev/null +++ b/protocols/wlr-foreign-toplevel-management-v1-client-protocol.h @@ -0,0 +1,615 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_wlr_foreign_toplevel_management_unstable_v1 The wlr_foreign_toplevel_management_unstable_v1 protocol + * @section page_ifaces_wlr_foreign_toplevel_management_unstable_v1 Interfaces + * - @subpage page_iface_zwlr_foreign_toplevel_manager_v1 - list and control opened apps + * - @subpage page_iface_zwlr_foreign_toplevel_handle_v1 - an opened toplevel + * @section page_copyright_wlr_foreign_toplevel_management_unstable_v1 Copyright + *
+ *
+ * Copyright © 2018 Ilia Bozhinov
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct zwlr_foreign_toplevel_handle_v1; +struct zwlr_foreign_toplevel_manager_v1; + +#ifndef ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_INTERFACE +#define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_INTERFACE +/** + * @page page_iface_zwlr_foreign_toplevel_manager_v1 zwlr_foreign_toplevel_manager_v1 + * @section page_iface_zwlr_foreign_toplevel_manager_v1_desc Description + * + * The purpose of this protocol is to enable the creation of taskbars + * and docks by providing them with a list of opened applications and + * letting them request certain actions on them, like maximizing, etc. + * + * After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + * toplevel window will be sent via the toplevel event + * @section page_iface_zwlr_foreign_toplevel_manager_v1_api API + * See @ref iface_zwlr_foreign_toplevel_manager_v1. + */ +/** + * @defgroup iface_zwlr_foreign_toplevel_manager_v1 The zwlr_foreign_toplevel_manager_v1 interface + * + * The purpose of this protocol is to enable the creation of taskbars + * and docks by providing them with a list of opened applications and + * letting them request certain actions on them, like maximizing, etc. + * + * After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + * toplevel window will be sent via the toplevel event + */ +extern const struct wl_interface zwlr_foreign_toplevel_manager_v1_interface; +#endif +#ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_INTERFACE +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_INTERFACE +/** + * @page page_iface_zwlr_foreign_toplevel_handle_v1 zwlr_foreign_toplevel_handle_v1 + * @section page_iface_zwlr_foreign_toplevel_handle_v1_desc Description + * + * A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + * window. Each app may have multiple opened toplevels. + * + * Each toplevel has a list of outputs it is visible on, conveyed to the + * client with the output_enter and output_leave events. + * @section page_iface_zwlr_foreign_toplevel_handle_v1_api API + * See @ref iface_zwlr_foreign_toplevel_handle_v1. + */ +/** + * @defgroup iface_zwlr_foreign_toplevel_handle_v1 The zwlr_foreign_toplevel_handle_v1 interface + * + * A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + * window. Each app may have multiple opened toplevels. + * + * Each toplevel has a list of outputs it is visible on, conveyed to the + * client with the output_enter and output_leave events. + */ +extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface; +#endif + +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + * @struct zwlr_foreign_toplevel_manager_v1_listener + */ +struct zwlr_foreign_toplevel_manager_v1_listener { + /** + * a toplevel has been created + * + * This event is emitted whenever a new toplevel window is + * created. It is emitted for all toplevels, regardless of the app + * that has created them. + * + * All initial details of the toplevel(title, app_id, states, etc.) + * will be sent immediately after this event via the corresponding + * events in zwlr_foreign_toplevel_handle_v1. + */ + void (*toplevel)(void *data, + struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, + struct zwlr_foreign_toplevel_handle_v1 *toplevel); + /** + * the compositor has finished with the toplevel manager + * + * This event indicates that the compositor is done sending + * events to the zwlr_foreign_toplevel_manager_v1. The server will + * destroy the object immediately after sending this request, so it + * will become invalid and the client should free any resources + * associated with it. + */ + void (*finished)(void *data, + struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1); +}; + +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + */ +static inline int +zwlr_foreign_toplevel_manager_v1_add_listener(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, + const struct zwlr_foreign_toplevel_manager_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, + (void (**)(void)) listener, data); +} + +#define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP 0 + +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_FINISHED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP_SINCE_VERSION 1 + +/** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ +static inline void +zwlr_foreign_toplevel_manager_v1_set_user_data(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, user_data); +} + +/** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ +static inline void * +zwlr_foreign_toplevel_manager_v1_get_user_data(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); +} + +static inline uint32_t +zwlr_foreign_toplevel_manager_v1_get_version(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); +} + +/** @ingroup iface_zwlr_foreign_toplevel_manager_v1 */ +static inline void +zwlr_foreign_toplevel_manager_v1_destroy(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) +{ + wl_proxy_destroy((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_manager_v1 + * + * Indicates the client no longer wishes to receive events for new toplevels. + * However the compositor may emit further toplevel_created events, until + * the finished event is emitted. + * + * The client must not send any more requests after this one. + */ +static inline void +zwlr_foreign_toplevel_manager_v1_stop(struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1, + ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_STOP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_manager_v1), 0); +} + +#ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * types of states on the toplevel + * + * The different states that a toplevel can have. These have the same meaning + * as the states with the same names defined in xdg-toplevel + */ +enum zwlr_foreign_toplevel_handle_v1_state { + /** + * the toplevel is maximized + */ + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED = 0, + /** + * the toplevel is minimized + */ + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED = 1, + /** + * the toplevel is active + */ + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED = 2, + /** + * the toplevel is fullscreen + * @since 2 + */ + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN = 3, +}; +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN_SINCE_VERSION 2 +#endif /* ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ENUM */ + +#ifndef ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM +enum zwlr_foreign_toplevel_handle_v1_error { + /** + * the provided rectangle is invalid + */ + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_INVALID_RECTANGLE = 0, +}; +#endif /* ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ERROR_ENUM */ + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * @struct zwlr_foreign_toplevel_handle_v1_listener + */ +struct zwlr_foreign_toplevel_handle_v1_listener { + /** + * title change + * + * This event is emitted whenever the title of the toplevel + * changes. + */ + void (*title)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + const char *title); + /** + * app-id change + * + * This event is emitted whenever the app-id of the toplevel + * changes. + */ + void (*app_id)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + const char *app_id); + /** + * toplevel entered an output + * + * This event is emitted whenever the toplevel becomes visible on + * the given output. A toplevel may be visible on multiple outputs. + */ + void (*output_enter)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_output *output); + /** + * toplevel left an output + * + * This event is emitted whenever the toplevel stops being + * visible on the given output. It is guaranteed that an + * entered-output event with the same output has been emitted + * before this event. + */ + void (*output_leave)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_output *output); + /** + * the toplevel state changed + * + * This event is emitted immediately after the + * zlw_foreign_toplevel_handle_v1 is created and each time the + * toplevel state changes, either because of a compositor action or + * because of a request in this protocol. + */ + void (*state)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_array *state); + /** + * all information about the toplevel has been sent + * + * This event is sent after all changes in the toplevel state + * have been sent. + * + * This allows changes to the zwlr_foreign_toplevel_handle_v1 + * properties to be seen as atomic, even if they happen via + * multiple events. + */ + void (*done)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1); + /** + * this toplevel has been destroyed + * + * This event means the toplevel has been destroyed. It is + * guaranteed there won't be any more events for this + * zwlr_foreign_toplevel_handle_v1. The toplevel itself becomes + * inert so any requests will be ignored except the destroy + * request. + */ + void (*closed)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1); + /** + * parent change + * + * This event is emitted whenever the parent of the toplevel + * changes. + * + * No event is emitted when the parent handle is destroyed by the + * client. + * @since 3 + */ + void (*parent)(void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct zwlr_foreign_toplevel_handle_v1 *parent); +}; + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +static inline int +zwlr_foreign_toplevel_handle_v1_add_listener(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + const struct zwlr_foreign_toplevel_handle_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + (void (**)(void)) listener, data); +} + +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED 0 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED 1 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED 2 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED 3 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE 4 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE 5 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE 6 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY 7 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN 8 +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN 9 + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_OUTPUT_ENTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_OUTPUT_LEAVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_PARENT_SINCE_VERSION 3 + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN_SINCE_VERSION 2 +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + */ +#define ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN_SINCE_VERSION 2 + +/** @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ +static inline void +zwlr_foreign_toplevel_handle_v1_set_user_data(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, user_data); +} + +/** @ingroup iface_zwlr_foreign_toplevel_handle_v1 */ +static inline void * +zwlr_foreign_toplevel_handle_v1_get_user_data(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1); +} + +static inline uint32_t +zwlr_foreign_toplevel_handle_v1_get_version(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be maximized. If the maximized state actually + * changes, this will be indicated by the state event. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_set_maximized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be unmaximized. If the maximized state actually + * changes, this will be indicated by the state event. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_unset_maximized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be minimized. If the minimized state actually + * changes, this will be indicated by the state event. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_set_minimized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be unminimized. If the minimized state actually + * changes, this will be indicated by the state event. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_unset_minimized(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Request that this toplevel be activated on the given seat. + * There is no guarantee the toplevel will be actually activated. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_activate(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_seat *seat) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_ACTIVATE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, seat); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Send a request to the toplevel to close itself. The compositor would + * typically use a shell-specific method to carry out this request, for + * example by sending the xdg_toplevel.close event. However, this gives + * no guarantees the toplevel will actually be destroyed. If and when + * this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + * be emitted. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_close(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_CLOSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * The rectangle of the surface specified in this request corresponds to + * the place where the app using this protocol represents the given toplevel. + * It can be used by the compositor as a hint for some operations, e.g + * minimizing. The client is however not required to set this, in which + * case the compositor is free to decide some default value. + * + * If the client specifies more than one rectangle, only the last one is + * considered. + * + * The dimensions are given in surface-local coordinates. + * Setting width=height=0 removes the already-set rectangle. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_set_rectangle(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_surface *surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, surface, x, y, width, height); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Destroys the zwlr_foreign_toplevel_handle_v1 object. + * + * This request should be called either when the client does not want to + * use the toplevel anymore or after the closed event to finalize the + * destruction of the object. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_destroy(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be fullscreened on the given output. If the + * fullscreen state and/or the outputs the toplevel is visible on actually + * change, this will be indicated by the state and output_enter/leave + * events. + * + * The output parameter is only a hint to the compositor. Also, if output + * is NULL, the compositor should decide which output the toplevel will be + * fullscreened on, if at all. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_set_fullscreen(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0, output); +} + +/** + * @ingroup iface_zwlr_foreign_toplevel_handle_v1 + * + * Requests that the toplevel be unfullscreened. If the fullscreen state + * actually changes, this will be indicated by the state event. + */ +static inline void +zwlr_foreign_toplevel_handle_v1_unset_fullscreen(struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1, + ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_foreign_toplevel_handle_v1), 0); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/protocols/wlr-foreign-toplevel-management-v1-protocol.c b/protocols/wlr-foreign-toplevel-management-v1-protocol.c new file mode 100644 index 00000000..7f914538 --- /dev/null +++ b/protocols/wlr-foreign-toplevel-management-v1-protocol.c @@ -0,0 +1,107 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2018 Ilia Bozhinov + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface; + +static const struct wl_interface *wlr_foreign_toplevel_management_unstable_v1_types[] = { + NULL, + &zwlr_foreign_toplevel_handle_v1_interface, + &wl_seat_interface, + &wl_surface_interface, + NULL, + NULL, + NULL, + NULL, + &wl_output_interface, + &wl_output_interface, + &wl_output_interface, + &zwlr_foreign_toplevel_handle_v1_interface, +}; + +static const struct wl_message zwlr_foreign_toplevel_manager_v1_requests[] = { + { "stop", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_foreign_toplevel_manager_v1_events[] = { + { "toplevel", "n", wlr_foreign_toplevel_management_unstable_v1_types + 1 }, + { "finished", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_foreign_toplevel_manager_v1_interface = { + "zwlr_foreign_toplevel_manager_v1", 3, + 1, zwlr_foreign_toplevel_manager_v1_requests, + 2, zwlr_foreign_toplevel_manager_v1_events, +}; + +static const struct wl_message zwlr_foreign_toplevel_handle_v1_requests[] = { + { "set_maximized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "unset_maximized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "set_minimized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "unset_minimized", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "activate", "o", wlr_foreign_toplevel_management_unstable_v1_types + 2 }, + { "close", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "set_rectangle", "oiiii", wlr_foreign_toplevel_management_unstable_v1_types + 3 }, + { "destroy", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "set_fullscreen", "2?o", wlr_foreign_toplevel_management_unstable_v1_types + 8 }, + { "unset_fullscreen", "2", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_foreign_toplevel_handle_v1_events[] = { + { "title", "s", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "app_id", "s", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "output_enter", "o", wlr_foreign_toplevel_management_unstable_v1_types + 9 }, + { "output_leave", "o", wlr_foreign_toplevel_management_unstable_v1_types + 10 }, + { "state", "a", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "done", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "closed", "", wlr_foreign_toplevel_management_unstable_v1_types + 0 }, + { "parent", "3?o", wlr_foreign_toplevel_management_unstable_v1_types + 11 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface = { + "zwlr_foreign_toplevel_handle_v1", 3, + 10, zwlr_foreign_toplevel_handle_v1_requests, + 8, zwlr_foreign_toplevel_handle_v1_events, +}; + diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml index 21669ac7..a149b35e 100644 --- a/protocols/wlr-layer-shell-unstable-v1.xml +++ b/protocols/wlr-layer-shell-unstable-v1.xml @@ -1,390 +1,222 @@ - + + - Copyright © 2017 Drew DeVault - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. + Copyright © 2017 Red Hat Inc. + + 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 (including the next + paragraph) 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. - - - Clients can use this interface to assign the surface_layer role to - wl_surfaces. Such surfaces are assigned to a "layer" of the output and - rendered with a defined z-depth respective to each other. They may also be - anchored to the edges and corners of a screen and specify input handling - semantics. This interface should be suitable for the implementation of - many desktop shell components, and a broad number of other applications - that interact with the desktop. + + This protocol aims at describing outputs in a way which is more in line + with the concept of an output on desktop oriented systems. + + Some information are more specific to the concept of an output for + a desktop oriented system and may not make sense in other applications, + such as IVI systems for example. + + Typically, the global compositor space on a desktop system is made of + a contiguous or overlapping set of rectangular regions. + + The logical_position and logical_size events defined in this protocol + might provide information identical to their counterparts already + available from wl_output, in which case the information provided by this + protocol should be preferred to their equivalent in wl_output. The goal is + to move the desktop specific concepts (such as output location within the + global compositor space, etc.) out of the core wl_output protocol. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + A global factory interface for xdg_output objects. - - - Create a layer surface for an existing surface. This assigns the role of - layer_surface, or raises a protocol error if another role is already - assigned. - - Creating a layer surface from a wl_surface which has a buffer attached - or committed is a client error, and any attempts by a client to attach - or manipulate a buffer prior to the first layer_surface.configure call - must also be treated as errors. - - After creating a layer_surface object and setting it up, the client - must perform an initial commit without any buffer attached. - The compositor will reply with a layer_surface.configure event. - The client must acknowledge it and is then allowed to attach a buffer - to map the surface. - - You may pass NULL for output to allow the compositor to decide which - output to use. Generally this will be the one that the user most - recently interacted with. - - Clients can specify a namespace that defines the purpose of the layer - surface. - - - - - - - + + + Using this request a client can tell the server that it is not + going to use the xdg_output_manager object anymore. - - - - - - - - - These values indicate which layers a surface can be rendered in. They - are ordered by z depth, bottom-most first. Traditional shell surfaces - will typically be rendered between the bottom and top layers. - Fullscreen shell surfaces are typically rendered at the top layer. - Multiple surfaces can share a single layer, and ordering within a - single layer is undefined. + Any objects already created through this instance are not affected. + - - - - - - - - - - - This request indicates that the client will not use the layer_shell - object any more. Objects that have been created through this instance - are not affected. + + + This creates a new xdg_output object for the given wl_output. + + - - - An interface that may be implemented by a wl_surface, for surfaces that - are designed to be rendered as a layer of a stacked desktop-like - environment. - - Layer surface state (layer, size, anchor, exclusive zone, - margin, interactivity) is double-buffered, and will be applied at the - time wl_surface.commit of the corresponding wl_surface is called. + + + An xdg_output describes part of the compositor geometry. - Attaching a null buffer to a layer surface unmaps it. + This typically corresponds to a monitor that displays part of the + compositor space. - Unmapping a layer_surface means that the surface cannot be shown by the - compositor until it is explicitly mapped again. The layer_surface - returns to the state it had right after layer_shell.get_layer_surface. - The client can re-map the surface by performing a commit without any - buffer attached, waiting for a configure event and handling it as usual. + For objects version 3 onwards, after all xdg_output properties have been + sent (when the object is created and when properties are updated), a + wl_output.done event is sent. This allows changes to the output + properties to be seen as atomic, even if they happen via multiple events. - - - Sets the size of the surface in surface-local coordinates. The - compositor will display the surface centered with respect to its - anchors. - - If you pass 0 for either value, the compositor will assign it and - inform you of the assignment in the configure event. You must set your - anchor to opposite edges in the dimensions you omit; not doing so is a - protocol error. Both values are 0 by default. - - Size is double-buffered, see wl_surface.commit. - - - - - - - - Requests that the compositor anchor the surface to the specified edges - and corners. If two orthogonal edges are specified (e.g. 'top' and - 'left'), then the anchor point will be the intersection of the edges - (e.g. the top left corner of the output); otherwise the anchor point - will be centered on that edge, or in the center if none is specified. - - Anchor is double-buffered, see wl_surface.commit. - - - - - - - Requests that the compositor avoids occluding an area with other - surfaces. The compositor's use of this information is - implementation-dependent - do not assume that this region will not - actually be occluded. - - A positive value is only meaningful if the surface is anchored to one - edge or an edge and both perpendicular edges. If the surface is not - anchored, anchored to only two perpendicular edges (a corner), anchored - to only two parallel edges or anchored to all edges, a positive value - will be treated the same as zero. - - A positive zone is the distance from the edge in surface-local - coordinates to consider exclusive. - - Surfaces that do not wish to have an exclusive zone may instead specify - how they should interact with surfaces that do. If set to zero, the - surface indicates that it would like to be moved to avoid occluding - surfaces with a positive exclusive zone. If set to -1, the surface - indicates that it would not like to be moved to accommodate for other - surfaces, and the compositor should extend it all the way to the edges - it is anchored to. - - For example, a panel might set its exclusive zone to 10, so that - maximized shell surfaces are not shown on top of it. A notification - might set its exclusive zone to 0, so that it is moved to avoid - occluding the panel, but shell surfaces are shown underneath it. A - wallpaper or lock screen might set their exclusive zone to -1, so that - they stretch below or over the panel. - - The default value is 0. - - Exclusive zone is double-buffered, see wl_surface.commit. + + + Using this request a client can tell the server that it is not + going to use the xdg_output object anymore. - - - - Requests that the surface be placed some distance away from the anchor - point on the output, in surface-local coordinates. Setting this value - for edges you are not anchored to has no effect. + + + The position event describes the location of the wl_output within + the global compositor space. - The exclusive zone includes the margin. - - Margin is double-buffered, see wl_surface.commit. + The logical_position event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the location + of the output changes within the global compositor space. - - - - - + + + - - - Types of keyboard interaction possible for layer shell surfaces. The - rationale for this is twofold: (1) some applications are not interested - in keyboard events and not allowing them to be focused can improve the - desktop experience; (2) some applications will want to take exclusive - keyboard focus. - + + + The logical_size event describes the size of the output in the + global compositor space. - - - This value indicates that this surface is not interested in keyboard - events and the compositor should never assign it the keyboard focus. - - This is the default value, set for newly created layer shell surfaces. - - This is useful for e.g. desktop widgets that display information or - only have interaction with non-keyboard input devices. - - - - - Request exclusive keyboard focus if this surface is above the shell surface layer. - - For the top and overlay layers, the seat will always give - exclusive keyboard focus to the top-most layer which has keyboard - interactivity set to exclusive. If this layer contains multiple - surfaces with keyboard interactivity set to exclusive, the compositor - determines the one receiving keyboard events in an implementation- - defined manner. In this case, no guarantee is made when this surface - will receive keyboard focus (if ever). - - For the bottom and background layers, the compositor is allowed to use - normal focus semantics. - - This setting is mainly intended for applications that need to ensure - they receive all keyboard events, such as a lock screen or a password - prompt. - - - - - This requests the compositor to allow this surface to be focused and - unfocused by the user in an implementation-defined manner. The user - should be able to unfocus this surface even regardless of the layer - it is on. - - Typically, the compositor will want to use its normal mechanism to - manage keyboard focus between layer shell surfaces with this setting - and regular toplevels on the desktop layer (e.g. click to focus). - Nevertheless, it is possible for a compositor to require a special - interaction to focus or unfocus layer shell surfaces (e.g. requiring - a click even if focus follows the mouse normally, or providing a - keybinding to switch focus between layers). - - This setting is mainly intended for desktop shell components (e.g. - panels) that allow keyboard interaction. Using this option can allow - implementing a desktop shell that can be fully usable without the - mouse. - - - - - - - Set how keyboard events are delivered to this surface. By default, - layer shell surfaces do not receive keyboard events; this request can - be used to change this. - - This setting is inherited by child surfaces set by the get_popup - request. - - Layer surfaces receive pointer, touch, and tablet events normally. If - you do not want to receive them, set the input region on your surface - to an empty region. - - Keyboard interactivity is double-buffered, see wl_surface.commit. - - - + Most regular Wayland clients should not pay attention to the + logical size and would rather rely on xdg_shell interfaces. - - - This assigns an xdg_popup's parent to this layer_surface. This popup - should have been created via xdg_surface::get_popup with the parent set - to NULL, and this request must be invoked before committing the popup's - initial state. + Some clients such as Xwayland, however, need this to configure + their surfaces in the global compositor space as the compositor + may apply a different scale from what is advertised by the output + scaling property (to achieve fractional scaling, for example). - See the documentation of xdg_popup for more details about what an - xdg_popup is and how it is used. - - - + For example, for a wl_output mode 3840×2160 and a scale factor 2: - - - When a configure event is received, if a client commits the - surface in response to the configure event, then the client - must make an ack_configure request sometime before the commit - request, passing along the serial of the configure event. + - A compositor not scaling the monitor viewport in its compositing space + will advertise a logical size of 3840×2160, - If the client receives multiple configure events before it - can respond to one, it only has to ack the last configure event. + - A compositor scaling the monitor viewport with scale factor 2 will + advertise a logical size of 1920×1080, - A client is not required to commit immediately after sending - an ack_configure request - it may even ack_configure several times - before its next surface commit. + - A compositor scaling the monitor viewport using a fractional scale of + 1.5 will advertise a logical size of 2560×1440. - A client may send multiple ack_configure requests before committing, but - only the last request sent before a commit indicates which configure - event the client really is responding to. - - - + For example, for a wl_output mode 1920×1080 and a 90 degree rotation, + the compositor will advertise a logical size of 1080x1920. - - - This request destroys the layer surface. + The logical_size event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the logical + size of the output changes, either as a result of a change in the + applied scale or because of a change in the corresponding output + mode(see wl_output.mode) or transform (see wl_output.transform). - - - - - The configure event asks the client to resize its surface. - - Clients should arrange their surface for the new states, and then send - an ack_configure request with the serial sent in this configure event at - some point before committing the new surface. - - The client is free to dismiss all but the last configure event it - received. + + + - The width and height arguments specify the size of the window in - surface-local coordinates. + + + This event is sent after all other properties of an xdg_output + have been sent. - The size is a hint, in the sense that the client is free to ignore it if - it doesn't resize, pick a smaller size (to satisfy aspect ratio or - resize in steps of NxM pixels). If the client picks a smaller size and - is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the - surface will be centered on this axis. + This allows changes to the xdg_output properties to be seen as + atomic, even if they happen via multiple events. - If the width or height arguments are zero, it means the client should - decide its own window dimension. + For objects version 3 onwards, this event is deprecated. Compositors + are not required to send it anymore and must send wl_output.done + instead. - - - - - - The closed event is sent by the compositor when the surface will no - longer be shown. The output may have been destroyed or the user may - have asked for it to be removed. Further changes to the surface will be - ignored. The client should destroy the resource after receiving this - event, and create a new surface if they so choose. + + + + + Many compositors will assign names to their outputs, show them to the + user, allow them to be configured by name, etc. The client may wish to + know this name as well to offer the user similar behaviors. + + The naming convention is compositor defined, but limited to + alphanumeric characters and dashes (-). Each name is unique among all + wl_output globals, but if a wl_output global is destroyed the same name + may be reused later. The names will also remain consistent across + sessions with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + The name event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output). This event is only sent once per + xdg_output, and the name does not change over the lifetime of the + wl_output global. + + This event is deprecated, instead clients should use wl_output.name. + Compositors must still support this event. + - - - - - - + + + Many compositors can produce human-readable descriptions of their + outputs. The client may wish to know this description as well, to + communicate the user for various purposes. - - - - - - + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. - + The description event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output) and whenever the description + changes. The description is optional, and may not be sent at all. - - - Change the layer that the surface is rendered on. + For objects of version 2 and lower, this event is only sent once per + xdg_output, and the description does not change over the lifetime of + the wl_output global. - Layer is double-buffered, see wl_surface.commit. + This event is deprecated, instead clients should use + wl_output.description. Compositors must still support this event. - - + + + - + \ No newline at end of file diff --git a/protocols/xdg-output-unstable-v1-client-protocol.h b/protocols/xdg-output-unstable-v1-client-protocol.h new file mode 100644 index 00000000..e98adf49 --- /dev/null +++ b/protocols/xdg-output-unstable-v1-client-protocol.h @@ -0,0 +1,706 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_wlr_layer_shell_unstable_v1 The wlr_layer_shell_unstable_v1 protocol + * @section page_ifaces_wlr_layer_shell_unstable_v1 Interfaces + * - @subpage page_iface_zwlr_layer_shell_v1 - create surfaces that are layers of the desktop + * - @subpage page_iface_zwlr_layer_surface_v1 - layer metadata interface + * @section page_copyright_wlr_layer_shell_unstable_v1 Copyright + *
+ *
+ * Copyright © 2017 Drew DeVault
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_surface; +struct xdg_popup; +struct zwlr_layer_shell_v1; +struct zwlr_layer_surface_v1; + +#ifndef ZWLR_LAYER_SHELL_V1_INTERFACE +#define ZWLR_LAYER_SHELL_V1_INTERFACE +/** + * @page page_iface_zwlr_layer_shell_v1 zwlr_layer_shell_v1 + * @section page_iface_zwlr_layer_shell_v1_desc Description + * + * Clients can use this interface to assign the surface_layer role to + * wl_surfaces. Such surfaces are assigned to a "layer" of the output and + * rendered with a defined z-depth respective to each other. They may also be + * anchored to the edges and corners of a screen and specify input handling + * semantics. This interface should be suitable for the implementation of + * many desktop shell components, and a broad number of other applications + * that interact with the desktop. + * @section page_iface_zwlr_layer_shell_v1_api API + * See @ref iface_zwlr_layer_shell_v1. + */ +/** + * @defgroup iface_zwlr_layer_shell_v1 The zwlr_layer_shell_v1 interface + * + * Clients can use this interface to assign the surface_layer role to + * wl_surfaces. Such surfaces are assigned to a "layer" of the output and + * rendered with a defined z-depth respective to each other. They may also be + * anchored to the edges and corners of a screen and specify input handling + * semantics. This interface should be suitable for the implementation of + * many desktop shell components, and a broad number of other applications + * that interact with the desktop. + */ +extern const struct wl_interface zwlr_layer_shell_v1_interface; +#endif +#ifndef ZWLR_LAYER_SURFACE_V1_INTERFACE +#define ZWLR_LAYER_SURFACE_V1_INTERFACE +/** + * @page page_iface_zwlr_layer_surface_v1 zwlr_layer_surface_v1 + * @section page_iface_zwlr_layer_surface_v1_desc Description + * + * An interface that may be implemented by a wl_surface, for surfaces that + * are designed to be rendered as a layer of a stacked desktop-like + * environment. + * + * Layer surface state (layer, size, anchor, exclusive zone, + * margin, interactivity) is double-buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * Attaching a null buffer to a layer surface unmaps it. + * + * Unmapping a layer_surface means that the surface cannot be shown by the + * compositor until it is explicitly mapped again. The layer_surface + * returns to the state it had right after layer_shell.get_layer_surface. + * The client can re-map the surface by performing a commit without any + * buffer attached, waiting for a configure event and handling it as usual. + * @section page_iface_zwlr_layer_surface_v1_api API + * See @ref iface_zwlr_layer_surface_v1. + */ +/** + * @defgroup iface_zwlr_layer_surface_v1 The zwlr_layer_surface_v1 interface + * + * An interface that may be implemented by a wl_surface, for surfaces that + * are designed to be rendered as a layer of a stacked desktop-like + * environment. + * + * Layer surface state (layer, size, anchor, exclusive zone, + * margin, interactivity) is double-buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * Attaching a null buffer to a layer surface unmaps it. + * + * Unmapping a layer_surface means that the surface cannot be shown by the + * compositor until it is explicitly mapped again. The layer_surface + * returns to the state it had right after layer_shell.get_layer_surface. + * The client can re-map the surface by performing a commit without any + * buffer attached, waiting for a configure event and handling it as usual. + */ +extern const struct wl_interface zwlr_layer_surface_v1_interface; +#endif + +#ifndef ZWLR_LAYER_SHELL_V1_ERROR_ENUM +#define ZWLR_LAYER_SHELL_V1_ERROR_ENUM +enum zwlr_layer_shell_v1_error { + /** + * wl_surface has another role + */ + ZWLR_LAYER_SHELL_V1_ERROR_ROLE = 0, + /** + * layer value is invalid + */ + ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER = 1, + /** + * wl_surface has a buffer attached or committed + */ + ZWLR_LAYER_SHELL_V1_ERROR_ALREADY_CONSTRUCTED = 2, +}; +#endif /* ZWLR_LAYER_SHELL_V1_ERROR_ENUM */ + +#ifndef ZWLR_LAYER_SHELL_V1_LAYER_ENUM +#define ZWLR_LAYER_SHELL_V1_LAYER_ENUM +/** + * @ingroup iface_zwlr_layer_shell_v1 + * available layers for surfaces + * + * These values indicate which layers a surface can be rendered in. They + * are ordered by z depth, bottom-most first. Traditional shell surfaces + * will typically be rendered between the bottom and top layers. + * Fullscreen shell surfaces are typically rendered at the top layer. + * Multiple surfaces can share a single layer, and ordering within a + * single layer is undefined. + */ +enum zwlr_layer_shell_v1_layer { + ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0, + ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM = 1, + ZWLR_LAYER_SHELL_V1_LAYER_TOP = 2, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY = 3, +}; +#endif /* ZWLR_LAYER_SHELL_V1_LAYER_ENUM */ + +#define ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE 0 +#define ZWLR_LAYER_SHELL_V1_DESTROY 1 + + +/** + * @ingroup iface_zwlr_layer_shell_v1 + */ +#define ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_shell_v1 + */ +#define ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION 3 + +/** @ingroup iface_zwlr_layer_shell_v1 */ +static inline void +zwlr_layer_shell_v1_set_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_shell_v1, user_data); +} + +/** @ingroup iface_zwlr_layer_shell_v1 */ +static inline void * +zwlr_layer_shell_v1_get_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_shell_v1); +} + +static inline uint32_t +zwlr_layer_shell_v1_get_version(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1); +} + +/** + * @ingroup iface_zwlr_layer_shell_v1 + * + * Create a layer surface for an existing surface. This assigns the role of + * layer_surface, or raises a protocol error if another role is already + * assigned. + * + * Creating a layer surface from a wl_surface which has a buffer attached + * or committed is a client error, and any attempts by a client to attach + * or manipulate a buffer prior to the first layer_surface.configure call + * must also be treated as errors. + * + * After creating a layer_surface object and setting it up, the client + * must perform an initial commit without any buffer attached. + * The compositor will reply with a layer_surface.configure event. + * The client must acknowledge it and is then allowed to attach a buffer + * to map the surface. + * + * You may pass NULL for output to allow the compositor to decide which + * output to use. Generally this will be the one that the user most + * recently interacted with. + * + * Clients can specify a namespace that defines the purpose of the layer + * surface. + */ +static inline struct zwlr_layer_surface_v1 * +zwlr_layer_shell_v1_get_layer_surface(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, struct wl_surface *surface, struct wl_output *output, uint32_t layer, const char *ns) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1, + ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE, &zwlr_layer_surface_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), 0, NULL, surface, output, layer, ns); + + return (struct zwlr_layer_surface_v1 *) id; +} + +/** + * @ingroup iface_zwlr_layer_shell_v1 + * + * This request indicates that the client will not use the layer_shell + * object any more. Objects that have been created through this instance + * are not affected. + */ +static inline void +zwlr_layer_shell_v1_destroy(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1, + ZWLR_LAYER_SHELL_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), WL_MARSHAL_FLAG_DESTROY); +} + +#ifndef ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM +#define ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM +/** + * @ingroup iface_zwlr_layer_surface_v1 + * types of keyboard interaction possible for a layer shell surface + * + * Types of keyboard interaction possible for layer shell surfaces. The + * rationale for this is twofold: (1) some applications are not interested + * in keyboard events and not allowing them to be focused can improve the + * desktop experience; (2) some applications will want to take exclusive + * keyboard focus. + */ +enum zwlr_layer_surface_v1_keyboard_interactivity { + /** + * no keyboard focus is possible + * + * This value indicates that this surface is not interested in + * keyboard events and the compositor should never assign it the + * keyboard focus. + * + * This is the default value, set for newly created layer shell + * surfaces. + * + * This is useful for e.g. desktop widgets that display information + * or only have interaction with non-keyboard input devices. + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE = 0, + /** + * request exclusive keyboard focus + * + * Request exclusive keyboard focus if this surface is above the + * shell surface layer. + * + * For the top and overlay layers, the seat will always give + * exclusive keyboard focus to the top-most layer which has + * keyboard interactivity set to exclusive. If this layer contains + * multiple surfaces with keyboard interactivity set to exclusive, + * the compositor determines the one receiving keyboard events in + * an implementation- defined manner. In this case, no guarantee is + * made when this surface will receive keyboard focus (if ever). + * + * For the bottom and background layers, the compositor is allowed + * to use normal focus semantics. + * + * This setting is mainly intended for applications that need to + * ensure they receive all keyboard events, such as a lock screen + * or a password prompt. + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE = 1, + /** + * request regular keyboard focus semantics + * + * This requests the compositor to allow this surface to be + * focused and unfocused by the user in an implementation-defined + * manner. The user should be able to unfocus this surface even + * regardless of the layer it is on. + * + * Typically, the compositor will want to use its normal mechanism + * to manage keyboard focus between layer shell surfaces with this + * setting and regular toplevels on the desktop layer (e.g. click + * to focus). Nevertheless, it is possible for a compositor to + * require a special interaction to focus or unfocus layer shell + * surfaces (e.g. requiring a click even if focus follows the mouse + * normally, or providing a keybinding to switch focus between + * layers). + * + * This setting is mainly intended for desktop shell components + * (e.g. panels) that allow keyboard interaction. Using this option + * can allow implementing a desktop shell that can be fully usable + * without the mouse. + * @since 4 + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND = 2, +}; +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION 4 +#endif /* ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM */ + +#ifndef ZWLR_LAYER_SURFACE_V1_ERROR_ENUM +#define ZWLR_LAYER_SURFACE_V1_ERROR_ENUM +enum zwlr_layer_surface_v1_error { + /** + * provided surface state is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE = 0, + /** + * size is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE = 1, + /** + * anchor bitfield is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_ANCHOR = 2, + /** + * keyboard interactivity is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_KEYBOARD_INTERACTIVITY = 3, +}; +#endif /* ZWLR_LAYER_SURFACE_V1_ERROR_ENUM */ + +#ifndef ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM +#define ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM +enum zwlr_layer_surface_v1_anchor { + /** + * the top edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP = 1, + /** + * the bottom edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM = 2, + /** + * the left edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT = 4, + /** + * the right edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT = 8, +}; +#endif /* ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM */ + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * @struct zwlr_layer_surface_v1_listener + */ +struct zwlr_layer_surface_v1_listener { + /** + * suggest a surface change + * + * The configure event asks the client to resize its surface. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * The client is free to dismiss all but the last configure event + * it received. + * + * The width and height arguments specify the size of the window in + * surface-local coordinates. + * + * The size is a hint, in the sense that the client is free to + * ignore it if it doesn't resize, pick a smaller size (to satisfy + * aspect ratio or resize in steps of NxM pixels). If the client + * picks a smaller size and is anchored to two opposite anchors + * (e.g. 'top' and 'bottom'), the surface will be centered on this + * axis. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. + */ + void (*configure)(void *data, + struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, + uint32_t serial, + uint32_t width, + uint32_t height); + /** + * surface should be closed + * + * The closed event is sent by the compositor when the surface + * will no longer be shown. The output may have been destroyed or + * the user may have asked for it to be removed. Further changes to + * the surface will be ignored. The client should destroy the + * resource after receiving this event, and create a new surface if + * they so choose. + */ + void (*closed)(void *data, + struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1); +}; + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +static inline int +zwlr_layer_surface_v1_add_listener(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, + const struct zwlr_layer_surface_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwlr_layer_surface_v1, + (void (**)(void)) listener, data); +} + +#define ZWLR_LAYER_SURFACE_V1_SET_SIZE 0 +#define ZWLR_LAYER_SURFACE_V1_SET_ANCHOR 1 +#define ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE 2 +#define ZWLR_LAYER_SURFACE_V1_SET_MARGIN 3 +#define ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY 4 +#define ZWLR_LAYER_SURFACE_V1_GET_POPUP 5 +#define ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE 6 +#define ZWLR_LAYER_SURFACE_V1_DESTROY 7 +#define ZWLR_LAYER_SURFACE_V1_SET_LAYER 8 + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_CLOSED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_MARGIN_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION 2 + +/** @ingroup iface_zwlr_layer_surface_v1 */ +static inline void +zwlr_layer_surface_v1_set_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_surface_v1, user_data); +} + +/** @ingroup iface_zwlr_layer_surface_v1 */ +static inline void * +zwlr_layer_surface_v1_get_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_surface_v1); +} + +static inline uint32_t +zwlr_layer_surface_v1_get_version(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Sets the size of the surface in surface-local coordinates. The + * compositor will display the surface centered with respect to its + * anchors. + * + * If you pass 0 for either value, the compositor will assign it and + * inform you of the assignment in the configure event. You must set your + * anchor to opposite edges in the dimensions you omit; not doing so is a + * protocol error. Both values are 0 by default. + * + * Size is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_size(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t width, uint32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, width, height); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the compositor anchor the surface to the specified edges + * and corners. If two orthogonal edges are specified (e.g. 'top' and + * 'left'), then the anchor point will be the intersection of the edges + * (e.g. the top left corner of the output); otherwise the anchor point + * will be centered on that edge, or in the center if none is specified. + * + * Anchor is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_anchor(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, anchor); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the compositor avoids occluding an area with other + * surfaces. The compositor's use of this information is + * implementation-dependent - do not assume that this region will not + * actually be occluded. + * + * A positive value is only meaningful if the surface is anchored to one + * edge or an edge and both perpendicular edges. If the surface is not + * anchored, anchored to only two perpendicular edges (a corner), anchored + * to only two parallel edges or anchored to all edges, a positive value + * will be treated the same as zero. + * + * A positive zone is the distance from the edge in surface-local + * coordinates to consider exclusive. + * + * Surfaces that do not wish to have an exclusive zone may instead specify + * how they should interact with surfaces that do. If set to zero, the + * surface indicates that it would like to be moved to avoid occluding + * surfaces with a positive exclusive zone. If set to -1, the surface + * indicates that it would not like to be moved to accommodate for other + * surfaces, and the compositor should extend it all the way to the edges + * it is anchored to. + * + * For example, a panel might set its exclusive zone to 10, so that + * maximized shell surfaces are not shown on top of it. A notification + * might set its exclusive zone to 0, so that it is moved to avoid + * occluding the panel, but shell surfaces are shown underneath it. A + * wallpaper or lock screen might set their exclusive zone to -1, so that + * they stretch below or over the panel. + * + * The default value is 0. + * + * Exclusive zone is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_exclusive_zone(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t zone) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, zone); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the surface be placed some distance away from the anchor + * point on the output, in surface-local coordinates. Setting this value + * for edges you are not anchored to has no effect. + * + * The exclusive zone includes the margin. + * + * Margin is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_margin(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t top, int32_t right, int32_t bottom, int32_t left) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_MARGIN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, top, right, bottom, left); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Set how keyboard events are delivered to this surface. By default, + * layer shell surfaces do not receive keyboard events; this request can + * be used to change this. + * + * This setting is inherited by child surfaces set by the get_popup + * request. + * + * Layer surfaces receive pointer, touch, and tablet events normally. If + * you do not want to receive them, set the input region on your surface + * to an empty region. + * + * Keyboard interactivity is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_keyboard_interactivity(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t keyboard_interactivity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, keyboard_interactivity); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * This assigns an xdg_popup's parent to this layer_surface. This popup + * should have been created via xdg_surface::get_popup with the parent set + * to NULL, and this request must be invoked before committing the popup's + * initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline void +zwlr_layer_surface_v1_get_popup(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, struct xdg_popup *popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_GET_POPUP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, popup); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + */ +static inline void +zwlr_layer_surface_v1_ack_configure(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, serial); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * This request destroys the layer surface. + */ +static inline void +zwlr_layer_surface_v1_destroy(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Change the layer that the surface is rendered on. + * + * Layer is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_layer(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t layer) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_LAYER, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, layer); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/protocols/xdg-output-unstable-v1-protocol.c b/protocols/xdg-output-unstable-v1-protocol.c new file mode 100644 index 00000000..0d5fa313 --- /dev/null +++ b/protocols/xdg-output-unstable-v1-protocol.c @@ -0,0 +1,94 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2017 Drew DeVault + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface zwlr_layer_surface_v1_interface; + +static const struct wl_interface *wlr_layer_shell_unstable_v1_types[] = { + NULL, + NULL, + NULL, + NULL, + &zwlr_layer_surface_v1_interface, + &wl_surface_interface, + &wl_output_interface, + NULL, + NULL, + &xdg_popup_interface, +}; + +static const struct wl_message zwlr_layer_shell_v1_requests[] = { + { "get_layer_surface", "no?ous", wlr_layer_shell_unstable_v1_types + 4 }, + { "destroy", "3", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_layer_shell_v1_interface = { + "zwlr_layer_shell_v1", 4, + 2, zwlr_layer_shell_v1_requests, + 0, NULL, +}; + +static const struct wl_message zwlr_layer_surface_v1_requests[] = { + { "set_size", "uu", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_anchor", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_exclusive_zone", "i", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_margin", "iiii", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_keyboard_interactivity", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "get_popup", "o", wlr_layer_shell_unstable_v1_types + 9 }, + { "ack_configure", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "destroy", "", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_layer", "2u", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_layer_surface_v1_events[] = { + { "configure", "uuu", wlr_layer_shell_unstable_v1_types + 0 }, + { "closed", "", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_layer_surface_v1_interface = { + "zwlr_layer_surface_v1", 4, + 9, zwlr_layer_surface_v1_requests, + 2, zwlr_layer_surface_v1_events, +}; + diff --git a/protocols/xdg-output-unstable-v1.xml b/protocols/xdg-output-unstable-v1.xml index 3b1b794c..720b4068 100644 --- a/protocols/xdg-output-unstable-v1.xml +++ b/protocols/xdg-output-unstable-v1.xml @@ -1,222 +1,390 @@ - - + - Copyright © 2017 Red Hat Inc. - - 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 (including the next - paragraph) 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. + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. - - This protocol aims at describing outputs in a way which is more in line - with the concept of an output on desktop oriented systems. - - Some information are more specific to the concept of an output for - a desktop oriented system and may not make sense in other applications, - such as IVI systems for example. - - Typically, the global compositor space on a desktop system is made of - a contiguous or overlapping set of rectangular regions. - - The logical_position and logical_size events defined in this protocol - might provide information identical to their counterparts already - available from wl_output, in which case the information provided by this - protocol should be preferred to their equivalent in wl_output. The goal is - to move the desktop specific concepts (such as output location within the - global compositor space, etc.) out of the core wl_output protocol. - - Warning! The protocol described in this file is experimental and - backward incompatible changes may be made. Backward compatible - changes may be added together with the corresponding interface - version bump. - Backward incompatible changes are done by bumping the version - number in the protocol and interface names and resetting the - interface version. Once the protocol is to be declared stable, - the 'z' prefix and the version number in the protocol and - interface names are removed and the interface version number is - reset. - - - - - A global factory interface for xdg_output objects. + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. - - - Using this request a client can tell the server that it is not - going to use the xdg_output_manager object anymore. - - Any objects already created through this instance are not affected. + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + - - - This creates a new xdg_output object for the given wl_output. + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. - - - - - An xdg_output describes part of the compositor geometry. + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. - This typically corresponds to a monitor that displays part of the - compositor space. + Attaching a null buffer to a layer surface unmaps it. - For objects version 3 onwards, after all xdg_output properties have been - sent (when the object is created and when properties are updated), a - wl_output.done event is sent. This allows changes to the output - properties to be seen as atomic, even if they happen via multiple events. + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. - - - Using this request a client can tell the server that it is not - going to use the xdg_output object anymore. + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + - - - The position event describes the location of the wl_output within - the global compositor space. + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. - The logical_position event is sent after creating an xdg_output - (see xdg_output_manager.get_xdg_output) and whenever the location - of the output changes within the global compositor space. + Anchor is double-buffered, see wl_surface.commit. - - - + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + - - - The logical_size event describes the size of the output in the - global compositor space. + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + - Most regular Wayland clients should not pay attention to the - logical size and would rather rely on xdg_shell interfaces. + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + - Some clients such as Xwayland, however, need this to configure - their surfaces in the global compositor space as the compositor - may apply a different scale from what is advertised by the output - scaling property (to achieve fractional scaling, for example). + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. - For example, for a wl_output mode 3840×2160 and a scale factor 2: + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + - - A compositor not scaling the monitor viewport in its compositing space - will advertise a logical size of 3840×2160, + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. - - A compositor scaling the monitor viewport with scale factor 2 will - advertise a logical size of 1920×1080, + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. - - A compositor scaling the monitor viewport using a fractional scale of - 1.5 will advertise a logical size of 2560×1440. + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. - For example, for a wl_output mode 1920×1080 and a 90 degree rotation, - the compositor will advertise a logical size of 1080x1920. + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + - The logical_size event is sent after creating an xdg_output - (see xdg_output_manager.get_xdg_output) and whenever the logical - size of the output changes, either as a result of a change in the - applied scale or because of a change in the corresponding output - mode(see wl_output.mode) or transform (see wl_output.transform). + + + This request destroys the layer surface. - - - + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. - - - This event is sent after all other properties of an xdg_output - have been sent. + The client is free to dismiss all but the last configure event it + received. - This allows changes to the xdg_output properties to be seen as - atomic, even if they happen via multiple events. + The width and height arguments specify the size of the window in + surface-local coordinates. - For objects version 3 onwards, this event is deprecated. Compositors - are not required to send it anymore and must send wl_output.done - instead. + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + - - - - - Many compositors will assign names to their outputs, show them to the - user, allow them to be configured by name, etc. The client may wish to - know this name as well to offer the user similar behaviors. - - The naming convention is compositor defined, but limited to - alphanumeric characters and dashes (-). Each name is unique among all - wl_output globals, but if a wl_output global is destroyed the same name - may be reused later. The names will also remain consistent across - sessions with the same hardware and software configuration. - - Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do - not assume that the name is a reflection of an underlying DRM - connector, X11 connection, etc. - - The name event is sent after creating an xdg_output (see - xdg_output_manager.get_xdg_output). This event is only sent once per - xdg_output, and the name does not change over the lifetime of the - wl_output global. - - This event is deprecated, instead clients should use wl_output.name. - Compositors must still support this event. + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. - - - - Many compositors can produce human-readable descriptions of their - outputs. The client may wish to know this description as well, to - communicate the user for various purposes. + + + + + + - The description is a UTF-8 string with no convention defined for its - contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 - output via :1'. + + + + + + - The description event is sent after creating an xdg_output (see - xdg_output_manager.get_xdg_output) and whenever the description - changes. The description is optional, and may not be sent at all. + - For objects of version 2 and lower, this event is only sent once per - xdg_output, and the description does not change over the lifetime of - the wl_output global. + + + Change the layer that the surface is rendered on. - This event is deprecated, instead clients should use - wl_output.description. Compositors must still support this event. + Layer is double-buffered, see wl_surface.commit. - - - + + - + \ No newline at end of file diff --git a/protocols/xdg-shell-client-protocol.h b/protocols/xdg-shell-client-protocol.h new file mode 100644 index 00000000..5a2ed89f --- /dev/null +++ b/protocols/xdg-shell-client-protocol.h @@ -0,0 +1,2384 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright © 2008-2013 Kristian Høgsberg
+ * Copyright © 2013      Rafael Antognolli
+ * Copyright © 2013      Jasper St. Pierre
+ * Copyright © 2010-2013 Intel Corporation
+ * Copyright © 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright © 2015-2017 Red Hat Inc.
+ *
+ * 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 (including the next
+ * paragraph) 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.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +#ifndef XDG_WM_BASE_INTERFACE +#define XDG_WM_BASE_INTERFACE +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +#endif +#ifndef XDG_POSITIONER_INTERFACE +#define XDG_POSITIONER_INTERFACE +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + */ +extern const struct wl_interface xdg_positioner_interface; +#endif +#ifndef XDG_SURFACE_INTERFACE +#define XDG_SURFACE_INTERFACE +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + */ +extern const struct wl_interface xdg_surface_interface; +#endif +#ifndef XDG_TOPLEVEL_INTERFACE +#define XDG_TOPLEVEL_INTERFACE +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +#endif +#ifndef XDG_POPUP_INTERFACE +#define XDG_POPUP_INTERFACE +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; +#endif + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, + /** + * the client didn’t respond to a ping event in time + */ + XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.pong. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. The + * “unresponsive” error is provided for compositors that wish + * to disconnect unresponsive clients. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a defunct_surfaces error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + * illegal to create an xdg_surface for a wl_surface which already has an + * assigned role and this will result in a role error. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping + * and xdg_wm_base.error.unresponsive. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * constraint adjustments + * + * The constraint adjustment value define ways the compositor will adjust + * the position of the surface, if the unadjusted position would result + * in the surface being partly constrained. + * + * Whether a surface is considered 'constrained' is left to the compositor + * to determine. For example, the surface may be partly outside the + * compositor's defined 'work area', thus necessitating the child surface's + * position be adjusted until it is entirely inside the work area. + * + * The adjustments can be combined, according to a defined precedence: 1) + * Flip, 2) Slide, 3) Resize. + */ +enum xdg_positioner_constraint_adjustment { + /** + * don't move the child surface when constrained + * + * Don't alter the surface position even if it is constrained on + * some axis, for example partially outside the edge of an output. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + /** + * move along the x axis until unconstrained + * + * Slide the surface along the x axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the x + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the x axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + /** + * move along the y axis until unconstrained + * + * Slide the surface along the y axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the y + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the y axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + /** + * invert the anchor and gravity on the x axis + * + * Invert the anchor and gravity on the x axis if the surface is + * constrained on the x axis. For example, if the left edge of the + * surface is constrained, the gravity is 'left' and the anchor is + * 'left', change the gravity to 'right' and the anchor to 'right'. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_x adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + /** + * invert the anchor and gravity on the y axis + * + * Invert the anchor and gravity on the y axis if the surface is + * constrained on the y axis. For example, if the bottom edge of + * the surface is constrained, the gravity is 'bottom' and the + * anchor is 'bottom', change the gravity to 'top' and the anchor + * to 'top'. + * + * The adjusted position is calculated given the original anchor + * rectangle and offset, but with the new flipped anchor and + * gravity values. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_y adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + /** + * horizontally resize the surface + * + * Resize the surface horizontally so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + /** + * vertically resize the surface + * + * Resize the surface vertically so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 +#define XDG_POSITIONER_SET_REACTIVE 7 +#define XDG_POSITIONER_SET_PARENT_SIZE 8 +#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. If the gravity is not in the ‘gravity’ enum, an + * invalid_input error is raised. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); +} + +/** + * @ingroup iface_xdg_positioner + * + * When set reactive, the surface is reconstrained if the conditions used + * for constraining changed, e.g. the parent window moved. + * + * If the conditions changed and the popup was reconstrained, an + * xdg_popup.configure event is sent with updated geometry, followed by an + * xdg_surface.configure event. + */ +static inline void +xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the parent window geometry the compositor should use when + * positioning the popup. The compositor may use this information to + * determine the future state the popup should be constrained using. If + * this doesn't match the dimension of the parent the popup is eventually + * positioned against, the behavior is undefined. + * + * The arguments are given in the surface-local coordinate space. + */ +static inline void +xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the serial of an xdg_surface.configure event this positioner will be + * used in response to. The compositor may use this information together + * with set_parent_size to determine what future state the popup should be + * constrained using. + */ +static inline void +xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + /** + * Surface was not fully constructed + */ + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + /** + * Surface was already constructed + */ + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + /** + * Attaching a buffer to an unconfigured surface + */ + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, + /** + * Invalid serial number when acking a configure event + */ + XDG_SURFACE_ERROR_INVALID_SERIAL = 4, + /** + * Width or height was zero or negative + */ + XDG_SURFACE_ERROR_INVALID_SIZE = 5, + /** + * Surface was destroyed before its role object + */ + XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed, otherwise + * a defunct_role_object error is raised. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. Note that + * in some situations, compositors may clip rendering to the window + * geometry, so the client should avoid putting functional elements + * outside of it. + * + * The window geometry is double-buffered state, see wl_surface.commit. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface, and may extend outside + * of the wl_surface itself to mark parts of the subsurface tree as part of + * the window geometry. + * + * When applied, the effective window geometry will be the set window + * geometry clamped to the bounding rectangle of the combined + * geometry of the surface of the xdg_surface and the associated + * subsurfaces. + * + * The effective geometry will not be recalculated unless a new call to + * set_window_geometry is done and the new pending surface state is + * subsequently applied. + * + * The width and height of the effective window geometry must be + * greater than zero. Setting an invalid size will raise an + * invalid_size error. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * Acking a configure event that was never sent raises an invalid_serial + * error. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + * + * Sending an ack_configure request consumes the serial number sent with + * the request, as well as serial numbers sent by all configure events + * sent on this xdg_surface prior to the configure event referenced by + * the committed serial. + * + * It is an error to issue multiple ack_configure requests referencing a + * serial from the same configure event, or to issue an ack_configure + * request referencing a serial from a configure event issued before the + * event identified by the last ack_configure request for the same + * xdg_surface. Doing so will raise an invalid_serial error. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial); +} + +#ifndef XDG_TOPLEVEL_ERROR_ENUM +#define XDG_TOPLEVEL_ERROR_ENUM +enum xdg_toplevel_error { + /** + * provided value is not a valid variant of the resize_edge enum + */ + XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, + /** + * invalid parent toplevel + */ + XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, + /** + * client provided an invalid min or max size + */ + XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, +}; +#endif /* XDG_TOPLEVEL_ERROR_ENUM */ + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * types of state on the surface + * + * The different state values used on the surface. This is designed for + * state values like maximized, fullscreen. It is paired with the + * configure event to ensure that both the client and the compositor + * setting the state can be synchronized. + * + * States set in this way are double-buffered, see wl_surface.commit. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + * the surface is maximized + * + * The surface is maximized. The window geometry specified in the + * configure event must be obeyed by the client, or the + * xdg_wm_base.invalid_surface_state error is raised. + * + * The client should draw without shadow or other decoration + * outside of the window geometry. + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + * the surface is fullscreen + * + * The surface is fullscreen. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. For a surface to cover the whole fullscreened area, + * the geometry dimensions must be obeyed by the client. For more + * details, see xdg_toplevel.set_fullscreen. + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + * the surface is being resized + * + * The surface is being resized. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. Clients that have aspect ratio or cell sizing + * configuration can use a smaller size, however. + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + * the surface is now activated + * + * Client window decorations should be painted as if the window + * is active. Do not assume this means that the window actually has + * keyboard or pointer focus. + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * the surface’s left edge is tiled + * + * The window is currently in a tiled layout and the left edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the left edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * the surface’s right edge is tiled + * + * The window is currently in a tiled layout and the right edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the right edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * the surface’s top edge is tiled + * + * The window is currently in a tiled layout and the top edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the top edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * the surface’s bottom edge is tiled + * + * The window is currently in a tiled layout and the bottom edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the bottom edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, + /** + * surface repaint is suspended + * + * The surface is currently not ordinarily being repainted; for + * example because its content is occluded by another window, or + * its outputs are switched off due to screen locking. + * @since 6 + */ + XDG_TOPLEVEL_STATE_SUSPENDED = 9, + /** + * the surface’s left edge is constrained + * + * The left edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT = 10, + /** + * the surface’s right edge is constrained + * + * The right edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT = 11, + /** + * the surface’s top edge is constrained + * + * The top edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_TOP = 12, + /** + * the surface’s bottom edge is constrained + * + * The bottom edge of the window is currently constrained, + * meaning it shouldn't attempt to resize from that edge. It can + * for example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM = 13, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION 6 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_TOP_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM_SINCE_VERSION 7 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +enum xdg_toplevel_wm_capabilities { + /** + * show_window_menu is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, + /** + * set_maximized and unset_maximized are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, + /** + * set_fullscreen and unset_fullscreen are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, + /** + * set_minimized is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, +}; +#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); + /** + * recommended window geometry bounds + * + * The configure_bounds event may be sent prior to a + * xdg_toplevel.configure event to communicate the bounds a window + * geometry size is recommended to constrain to. + * + * The passed width and height are in surface coordinate space. If + * width and height are 0, it means bounds is unknown and + * equivalent to as if no configure_bounds event was ever sent for + * this surface. + * + * The bounds can for example correspond to the size of a monitor + * excluding any panels or other shell components, so that a + * surface isn't created in a way that it cannot fit. + * + * The bounds may change at any point, and in such a case, a new + * xdg_toplevel.configure_bounds will be sent, followed by + * xdg_toplevel.configure and xdg_surface.configure. + * @since 4 + */ + void (*configure_bounds)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height); + /** + * compositor capabilities + * + * This event advertises the capabilities supported by the + * compositor. If a capability isn't supported, clients should hide + * or disable the UI elements that expose this functionality. For + * instance, if the compositor doesn't advertise support for + * minimized toplevels, a button triggering the set_minimized + * request should not be displayed. + * + * The compositor will ignore requests it doesn't support. For + * instance, a compositor which doesn't advertise support for + * minimized will ignore set_minimized requests. + * + * Compositors must send this event once before the first + * xdg_surface.configure event. When the capabilities change, + * compositors must send this event again and then send an + * xdg_surface.configure event. + * + * The configured state should not be applied immediately. See + * xdg_surface.configure for details. + * + * The capabilities are sent as an array of 32-bit unsigned + * integers in native endianness. + * @param capabilities array of 32-bit capabilities + * @since 5 + */ + void (*wm_capabilities)(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent surfaces should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child surface unsets its parent. Setting + * a null parent for a surface which currently has no parent is a no-op. + * + * Only mapped surfaces can have child surfaces. Setting a parent which + * is not mapped is equivalent to setting a null parent. If a surface + * becomes unmapped, its children's parent is set to the parent of + * the now-unmapped surface. If the now-unmapped surface has no parent, + * its children's parent is unset. If the now-unmapped surface becomes + * mapped again, its parent-child relationship is not restored. + * + * The parent toplevel must not be one of the child toplevel's + * descendants, and the parent must be different from the child toplevel, + * otherwise the invalid_parent protocol error is raised. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * Like other properties, a set_app_id request can be sent after the + * xdg_toplevel has been mapped to update the property. + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] https://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains, or even if a window menu will be drawn + * at all. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, and + * is one of the values of the resize_edge enum. Values not matching + * a variant of the enum will cause the invalid_resize_edge protocol error. + * The compositor may use this information to update the surface position + * for example when dragging the top left corner. The compositor may also + * use this information to adapt its behavior, e.g. choose an appropriate + * cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width or height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * + * For version 2 or older, the configure event for an xdg_popup is + * only ever sent once for the initial configuration. Starting with + * version 3, it may be sent again if the popup is setup with an + * xdg_positioner with set_reactive requested, or in response to + * xdg_popup.reposition requests. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); + /** + * signal the completion of a repositioned request + * + * The repositioned event is sent as part of a popup + * configuration sequence, together with xdg_popup.configure and + * lastly xdg_surface.configure to notify the completion of a + * reposition request. + * + * The repositioned event is to notify about the completion of a + * xdg_popup.reposition request. The token argument is the token + * passed in the xdg_popup.reposition request. + * + * Immediately after this event is emitted, xdg_popup.configure and + * xdg_surface.configure will be sent with the updated size and + * position, as well as a new configure serial. + * + * The client should optionally update the content of the popup, + * but must acknowledge the new popup configuration for the new + * position to take effect. See xdg_surface.ack_configure for + * details. + * @param token reposition request token + * @since 3 + */ + void (*repositioned)(void *data, + struct xdg_popup *xdg_popup, + uint32_t token); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 +#define XDG_POPUP_REPOSITION 2 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, the + * xdg_wm_base.not_the_topmost_popup protocol error will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_popup + * + * Reposition an already-mapped popup. The popup will be placed given the + * details in the passed xdg_positioner object, and a + * xdg_popup.repositioned followed by xdg_popup.configure and + * xdg_surface.configure will be emitted in response. Any parameters set + * by the previous positioner will be discarded. + * + * The passed token will be sent in the corresponding + * xdg_popup.repositioned event. The new popup position will not take + * effect until the corresponding configure event is acknowledged by the + * client. See xdg_popup.repositioned for details. The token itself is + * opaque, and has no other special meaning. + * + * If multiple reposition requests are sent, the compositor may skip all + * but the last one. + * + * If the popup is repositioned in response to a configure event for its + * parent, the client should send an xdg_positioner.set_parent_configure + * and possibly an xdg_positioner.set_parent_size request to allow the + * compositor to properly constrain the popup. + * + * If the popup is repositioned together with a parent that is being + * resized, but not in response to a configure event, the client should + * send an xdg_positioner.set_parent_size request. + */ +static inline void +xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/protocols/xdg-shell-protocol.c b/protocols/xdg-shell-protocol.c new file mode 100644 index 00000000..5a50433b --- /dev/null +++ b/protocols/xdg-shell-protocol.c @@ -0,0 +1,184 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2008-2013 Kristian Høgsberg + * Copyright © 2013 Rafael Antognolli + * Copyright © 2013 Jasper St. Pierre + * Copyright © 2010-2013 Intel Corporation + * Copyright © 2015-2017 Samsung Electronics Co., Ltd + * Copyright © 2015-2017 Red Hat Inc. + * + * 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 (including the next + * paragraph) 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. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *xdg_shell_types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, + &xdg_positioner_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "create_positioner", "n", xdg_shell_types + 4 }, + { "get_xdg_surface", "no", xdg_shell_types + 5 }, + { "pong", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 7, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_size", "ii", xdg_shell_types + 0 }, + { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, + { "set_anchor", "u", xdg_shell_types + 0 }, + { "set_gravity", "u", xdg_shell_types + 0 }, + { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, + { "set_offset", "ii", xdg_shell_types + 0 }, + { "set_reactive", "3", xdg_shell_types + 0 }, + { "set_parent_size", "3ii", xdg_shell_types + 0 }, + { "set_parent_configure", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 7, + 10, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "get_toplevel", "n", xdg_shell_types + 7 }, + { "get_popup", "n?oo", xdg_shell_types + 8 }, + { "set_window_geometry", "iiii", xdg_shell_types + 0 }, + { "ack_configure", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 7, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_parent", "?o", xdg_shell_types + 11 }, + { "set_title", "s", xdg_shell_types + 0 }, + { "set_app_id", "s", xdg_shell_types + 0 }, + { "show_window_menu", "ouii", xdg_shell_types + 12 }, + { "move", "ou", xdg_shell_types + 16 }, + { "resize", "ouu", xdg_shell_types + 18 }, + { "set_max_size", "ii", xdg_shell_types + 0 }, + { "set_min_size", "ii", xdg_shell_types + 0 }, + { "set_maximized", "", xdg_shell_types + 0 }, + { "unset_maximized", "", xdg_shell_types + 0 }, + { "set_fullscreen", "?o", xdg_shell_types + 21 }, + { "unset_fullscreen", "", xdg_shell_types + 0 }, + { "set_minimized", "", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", xdg_shell_types + 0 }, + { "close", "", xdg_shell_types + 0 }, + { "configure_bounds", "4ii", xdg_shell_types + 0 }, + { "wm_capabilities", "5a", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 7, + 14, xdg_toplevel_requests, + 4, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "grab", "ou", xdg_shell_types + 22 }, + { "reposition", "3ou", xdg_shell_types + 24 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", xdg_shell_types + 0 }, + { "popup_done", "", xdg_shell_types + 0 }, + { "repositioned", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 7, + 3, xdg_popup_requests, + 3, xdg_popup_events, +}; + diff --git a/protocols/xdg-shell.xml b/protocols/xdg-shell.xml new file mode 100644 index 00000000..f9affb2d --- /dev/null +++ b/protocols/xdg-shell.xml @@ -0,0 +1,1418 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + 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 (including the next + paragraph) 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. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. While xdg_surface + itself is not a role, the corresponding surface may only be assigned + a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + illegal to create an xdg_surface for a wl_surface which already has an + assigned role and this will result in a role error. + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up (e.g. by sending + the title, app ID, size constraints, parent, etc), the client must + perform an initial commit without any buffer attached. The compositor + will reply with initial wl_surface state such as + wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + event. The client must acknowledge it and is then allowed to attach a + buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. Note that + in some situations, compositors may clip rendering to the window + geometry, so the client should avoid putting functional elements + outside of it. + + The window geometry is double-buffered state, see wl_surface.commit. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface, and may extend outside + of the wl_surface itself to mark parts of the subsurface tree as part of + the window geometry. + + When applied, the effective window geometry will be the set window + geometry clamped to the bounding rectangle of the combined + geometry of the surface of the xdg_surface and the associated + subsurfaces. + + The effective geometry will not be recalculated unless a new call to + set_window_geometry is done and the new pending surface state is + subsequently applied. + + The width and height of the effective window geometry must be + greater than zero. Setting an invalid size will raise an + invalid_size error. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + A xdg_toplevel by default is responsible for providing the full intended + visual representation of the toplevel, which depending on the window + state, may mean things like a title bar, window controls and drop shadow. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by performing a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause the invalid_resize_edge protocol error. + The compositor may use this information to update the surface position + for example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered, see wl_surface.commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client, or the xdg_wm_base.invalid_surface_state + error is raised. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the left edge. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the right edge. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the top edge. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the bottom edge. + + + + + The surface is currently not ordinarily being repainted; for + example because its content is occluded by another window, or its + outputs are switched off due to screen locking. + + + + + The left edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The right edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The top edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The bottom edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in a + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + with border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, the + xdg_wm_base.not_the_topmost_popup protocol error will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + \ No newline at end of file diff --git a/protocols/zwlr-layer-shell-v1-client-protocol.h b/protocols/zwlr-layer-shell-v1-client-protocol.h new file mode 100644 index 00000000..40e11ef4 --- /dev/null +++ b/protocols/zwlr-layer-shell-v1-client-protocol.h @@ -0,0 +1,415 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_output_unstable_v1 The xdg_output_unstable_v1 protocol + * Protocol to describe output regions + * + * @section page_desc_xdg_output_unstable_v1 Description + * + * This protocol aims at describing outputs in a way which is more in line + * with the concept of an output on desktop oriented systems. + * + * Some information are more specific to the concept of an output for + * a desktop oriented system and may not make sense in other applications, + * such as IVI systems for example. + * + * Typically, the global compositor space on a desktop system is made of + * a contiguous or overlapping set of rectangular regions. + * + * The logical_position and logical_size events defined in this protocol + * might provide information identical to their counterparts already + * available from wl_output, in which case the information provided by this + * protocol should be preferred to their equivalent in wl_output. The goal is + * to move the desktop specific concepts (such as output location within the + * global compositor space, etc.) out of the core wl_output protocol. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible + * changes may be added together with the corresponding interface + * version bump. + * Backward incompatible changes are done by bumping the version + * number in the protocol and interface names and resetting the + * interface version. Once the protocol is to be declared stable, + * the 'z' prefix and the version number in the protocol and + * interface names are removed and the interface version number is + * reset. + * + * @section page_ifaces_xdg_output_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_output_manager_v1 - manage xdg_output objects + * - @subpage page_iface_zxdg_output_v1 - compositor logical output region + * @section page_copyright_xdg_output_unstable_v1 Copyright + *
+ *
+ * Copyright © 2017 Red Hat Inc.
+ *
+ * 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 (including the next
+ * paragraph) 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.
+ * 
+ */ +struct wl_output; +struct zxdg_output_manager_v1; +struct zxdg_output_v1; + +#ifndef ZXDG_OUTPUT_MANAGER_V1_INTERFACE +#define ZXDG_OUTPUT_MANAGER_V1_INTERFACE +/** + * @page page_iface_zxdg_output_manager_v1 zxdg_output_manager_v1 + * @section page_iface_zxdg_output_manager_v1_desc Description + * + * A global factory interface for xdg_output objects. + * @section page_iface_zxdg_output_manager_v1_api API + * See @ref iface_zxdg_output_manager_v1. + */ +/** + * @defgroup iface_zxdg_output_manager_v1 The zxdg_output_manager_v1 interface + * + * A global factory interface for xdg_output objects. + */ +extern const struct wl_interface zxdg_output_manager_v1_interface; +#endif +#ifndef ZXDG_OUTPUT_V1_INTERFACE +#define ZXDG_OUTPUT_V1_INTERFACE +/** + * @page page_iface_zxdg_output_v1 zxdg_output_v1 + * @section page_iface_zxdg_output_v1_desc Description + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + * @section page_iface_zxdg_output_v1_api API + * See @ref iface_zxdg_output_v1. + */ +/** + * @defgroup iface_zxdg_output_v1 The zxdg_output_v1 interface + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + */ +extern const struct wl_interface zxdg_output_v1_interface; +#endif + +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY 0 +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT 1 + + +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_output_manager_v1 */ +static inline void +zxdg_output_manager_v1_set_user_data(struct zxdg_output_manager_v1 *zxdg_output_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_output_manager_v1, user_data); +} + +/** @ingroup iface_zxdg_output_manager_v1 */ +static inline void * +zxdg_output_manager_v1_get_user_data(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_output_manager_v1); +} + +static inline uint32_t +zxdg_output_manager_v1_get_version(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1); +} + +/** + * @ingroup iface_zxdg_output_manager_v1 + * + * Using this request a client can tell the server that it is not + * going to use the xdg_output_manager object anymore. + * + * Any objects already created through this instance are not affected. + */ +static inline void +zxdg_output_manager_v1_destroy(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_manager_v1, + ZXDG_OUTPUT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zxdg_output_manager_v1 + * + * This creates a new xdg_output object for the given wl_output. + */ +static inline struct zxdg_output_v1 * +zxdg_output_manager_v1_get_xdg_output(struct zxdg_output_manager_v1 *zxdg_output_manager_v1, struct wl_output *output) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_manager_v1, + ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT, &zxdg_output_v1_interface, wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1), 0, NULL, output); + + return (struct zxdg_output_v1 *) id; +} + +/** + * @ingroup iface_zxdg_output_v1 + * @struct zxdg_output_v1_listener + */ +struct zxdg_output_v1_listener { + /** + * position of the output within the global compositor space + * + * The position event describes the location of the wl_output + * within the global compositor space. + * + * The logical_position event is sent after creating an xdg_output + * (see xdg_output_manager.get_xdg_output) and whenever the + * location of the output changes within the global compositor + * space. + * @param x x position within the global compositor space + * @param y y position within the global compositor space + */ + void (*logical_position)(void *data, + struct zxdg_output_v1 *zxdg_output_v1, + int32_t x, + int32_t y); + /** + * size of the output in the global compositor space + * + * The logical_size event describes the size of the output in the + * global compositor space. + * + * Most regular Wayland clients should not pay attention to the + * logical size and would rather rely on xdg_shell interfaces. + * + * Some clients such as Xwayland, however, need this to configure + * their surfaces in the global compositor space as the compositor + * may apply a different scale from what is advertised by the + * output scaling property (to achieve fractional scaling, for + * example). + * + * For example, for a wl_output mode 3840×2160 and a scale factor + * 2: + * + * - A compositor not scaling the monitor viewport in its + * compositing space will advertise a logical size of 3840×2160, + * + * - A compositor scaling the monitor viewport with scale factor 2 + * will advertise a logical size of 1920×1080, + * + * - A compositor scaling the monitor viewport using a fractional + * scale of 1.5 will advertise a logical size of 2560×1440. + * + * For example, for a wl_output mode 1920×1080 and a 90 degree + * rotation, the compositor will advertise a logical size of + * 1080x1920. + * + * The logical_size event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the logical size + * of the output changes, either as a result of a change in the + * applied scale or because of a change in the corresponding output + * mode(see wl_output.mode) or transform (see wl_output.transform). + * @param width width in global compositor space + * @param height height in global compositor space + */ + void (*logical_size)(void *data, + struct zxdg_output_v1 *zxdg_output_v1, + int32_t width, + int32_t height); + /** + * all information about the output have been sent + * + * This event is sent after all other properties of an xdg_output + * have been sent. + * + * This allows changes to the xdg_output properties to be seen as + * atomic, even if they happen via multiple events. + * + * For objects version 3 onwards, this event is deprecated. + * Compositors are not required to send it anymore and must send + * wl_output.done instead. + * @deprecated Deprecated since version 3 + */ + void (*done)(void *data, + struct zxdg_output_v1 *zxdg_output_v1); + /** + * name of this output + * + * Many compositors will assign names to their outputs, show them + * to the user, allow them to be configured by name, etc. The + * client may wish to know this name as well to offer the user + * similar behaviors. + * + * The naming convention is compositor defined, but limited to + * alphanumeric characters and dashes (-). Each name is unique + * among all wl_output globals, but if a wl_output global is + * destroyed the same name may be reused later. The names will also + * remain consistent across sessions with the same hardware and + * software configuration. + * + * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. + * However, do not assume that the name is a reflection of an + * underlying DRM connector, X11 connection, etc. + * + * The name event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output). This event is only sent once + * per xdg_output, and the name does not change over the lifetime + * of the wl_output global. + * + * This event is deprecated, instead clients should use + * wl_output.name. Compositors must still support this event. + * @param name output name + * @since 2 + */ + void (*name)(void *data, + struct zxdg_output_v1 *zxdg_output_v1, + const char *name); + /** + * human-readable description of this output + * + * Many compositors can produce human-readable descriptions of + * their outputs. The client may wish to know this description as + * well, to communicate the user for various purposes. + * + * The description is a UTF-8 string with no convention defined for + * its contents. Examples might include 'Foocorp 11" Display' or + * 'Virtual X11 output via :1'. + * + * The description event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the description + * changes. The description is optional, and may not be sent at + * all. + * + * For objects of version 2 and lower, this event is only sent once + * per xdg_output, and the description does not change over the + * lifetime of the wl_output global. + * + * This event is deprecated, instead clients should use + * wl_output.description. Compositors must still support this + * event. + * @param description output description + * @since 2 + */ + void (*description)(void *data, + struct zxdg_output_v1 *zxdg_output_v1, + const char *description); +}; + +/** + * @ingroup iface_zxdg_output_v1 + */ +static inline int +zxdg_output_v1_add_listener(struct zxdg_output_v1 *zxdg_output_v1, + const struct zxdg_output_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zxdg_output_v1, + (void (**)(void)) listener, data); +} + +#define ZXDG_OUTPUT_V1_DESTROY 0 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_POSITION_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_NAME_SINCE_VERSION 2 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION 2 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_output_v1 */ +static inline void +zxdg_output_v1_set_user_data(struct zxdg_output_v1 *zxdg_output_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_output_v1, user_data); +} + +/** @ingroup iface_zxdg_output_v1 */ +static inline void * +zxdg_output_v1_get_user_data(struct zxdg_output_v1 *zxdg_output_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_output_v1); +} + +static inline uint32_t +zxdg_output_v1_get_version(struct zxdg_output_v1 *zxdg_output_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_output_v1); +} + +/** + * @ingroup iface_zxdg_output_v1 + * + * Using this request a client can tell the server that it is not + * going to use the xdg_output object anymore. + */ +static inline void +zxdg_output_v1_destroy(struct zxdg_output_v1 *zxdg_output_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_v1, + ZXDG_OUTPUT_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_v1), WL_MARSHAL_FLAG_DESTROY); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/protocols/zwlr-layer-shell-v1-protocol.c b/protocols/zwlr-layer-shell-v1-protocol.c new file mode 100644 index 00000000..ca28cfc4 --- /dev/null +++ b/protocols/zwlr-layer-shell-v1-protocol.c @@ -0,0 +1,79 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2017 Red Hat Inc. + * + * 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 (including the next + * paragraph) 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. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface zxdg_output_v1_interface; + +static const struct wl_interface *xdg_output_unstable_v1_types[] = { + NULL, + NULL, + &zxdg_output_v1_interface, + &wl_output_interface, +}; + +static const struct wl_message zxdg_output_manager_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types + 0 }, + { "get_xdg_output", "no", xdg_output_unstable_v1_types + 2 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_output_manager_v1_interface = { + "zxdg_output_manager_v1", 3, + 2, zxdg_output_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zxdg_output_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types + 0 }, +}; + +static const struct wl_message zxdg_output_v1_events[] = { + { "logical_position", "ii", xdg_output_unstable_v1_types + 0 }, + { "logical_size", "ii", xdg_output_unstable_v1_types + 0 }, + { "done", "", xdg_output_unstable_v1_types + 0 }, + { "name", "2s", xdg_output_unstable_v1_types + 0 }, + { "description", "2s", xdg_output_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_output_v1_interface = { + "zxdg_output_v1", 3, + 1, zxdg_output_v1_requests, + 5, zxdg_output_v1_events, +}; + diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index 3dfdb4eb..0f17e0dd 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -148,16 +148,19 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int running = 1; while (running >= 1 && wayland_ctx.display != BONGOCAT_NULLPTR) { const time_ms_t frame_based_timeout = config.fps > 0 ? 1000 / config.fps : 0; + // Periodic fullscreen check for fallback fullscreen detection - timeval now{}; - gettimeofday(&now, BONGOCAT_NULLPTR); - const time_ms_t elapsed_ms = ((now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L) + - ((now.tv_usec - ctx.fs_detector.last_check.tv_usec) / 1000L); - time_ms_t fullscreen_check_interval = frame_based_timeout; - if (fullscreen_check_interval < CHECK_INTERVAL_MS) { - fullscreen_check_interval = CHECK_INTERVAL_MS; + timespec now{}; + clock_gettime(CLOCK_MONOTONIC, &now); + const time_ms_t elapsed_ms = + ((now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L) + + ((now.tv_nsec - ctx.fs_detector.last_check.tv_nsec) / 1000000L); + time_ms_t fullscreen_check_interval_ms = frame_based_timeout; + + if (fullscreen_check_interval_ms < CHECK_INTERVAL_MS) { + fullscreen_check_interval_ms = CHECK_INTERVAL_MS; } - if (elapsed_ms >= fullscreen_check_interval) { + if (elapsed_ms >= fullscreen_check_interval_ms) { details::fs_update_state_fallback(ctx); ctx.fs_detector.last_check = now; } diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index d10c86a5..d4e20ba5 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -287,7 +287,7 @@ update_fullscreen_state_toplevel(wayland_context_t& ctx, tracked_toplevel_t& tra tracked.is_fullscreen = state.is_fullscreen; tracked.is_activated = state.is_activated; - /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert + /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggered // Only trigger overlay update if this fullscreen window is on our output if (tracked.output == ctx.thread_context.output && state_changed) { state_changed = fs_update_state(ctx, state); @@ -324,7 +324,7 @@ void fs_handle_toplevel_state(void *data, [[maybe_unused]] zwlr_foreign_toplevel } } - /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggert + /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggered for (size_t i = 0; i < ctx.num_toplevels; ++i) { if (ctx.tracked_toplevels[i].handle == handle) { auto [output_found, changed] = update_fullscreen_state_toplevel( diff --git a/src/utils/error.cpp b/src/utils/error.cpp index a40f5b9b..5f3bdd93 100644 --- a/src/utils/error.cpp +++ b/src/utils/error.cpp @@ -30,15 +30,15 @@ namespace details { } inline void log_timestamp(FILE *stream) { - timeval tv{}; + timespec ts{}; tm tm_info{}; char timestamp[64] = {0}; - gettimeofday(&tv, BONGOCAT_NULLPTR); - localtime_r(&tv.tv_sec, &tm_info); // Thread-safe version + clock_gettime(CLOCK_REALTIME, &ts); + localtime_r(&ts.tv_sec, &tm_info); // Thread-safe strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info); - fprintf(stream, "[%s.%03ld] ", timestamp, tv.tv_usec / 1000); + fprintf(stream, "[%s.%03ld] ", timestamp, ts.tv_nsec / 1000000); } // Core log function using va_list @@ -47,7 +47,7 @@ namespace details { assert(name_len > 0); platform::LockGuard guard(get_log_mutex()); - char message[1024]; + //char message[1024]; log_timestamp(stdout); fprintf(stdout, "%.*s: ", name_len, name); vfprintf(stdout, format, args); diff --git a/src/utils/time.cpp b/src/utils/time.cpp index 49d4ef11..af5249e7 100644 --- a/src/utils/time.cpp +++ b/src/utils/time.cpp @@ -5,9 +5,9 @@ namespace bongocat::platform { timestamp_us_t get_current_time_us() { - timeval now{}; - gettimeofday(&now, nullptr); - return (now.tv_sec * 1000000LL) + now.tv_usec; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (now.tv_sec * 1000000L) + (now.tv_nsec / 1000L); } timestamp_ms_t get_current_time_ms() { return get_current_time_us() / 1000L; From fb8220e390bd9191c239118d149eedc9810b37f9 Mon Sep 17 00:00:00 2001 From: furudbat Date: Tue, 7 Apr 2026 02:49:57 +0200 Subject: [PATCH 05/17] wip: merge from upstream - Move PID file to $XDG_RUNTIME_DIR with O_NOFOLLOW and mode 0600 - config: Validate keyboard_device paths require /dev/input/ - Replace popen("hyprctl") with fork/execvp to prevent command injection --- include/platform/wayland_context.h | 5 ++ include/platform/wayland_setups.h | 28 ++++++ src/config/config.cpp | 17 +++- src/core/main.cpp | 134 ++++++++++++++++++++--------- src/platform/wayland.cpp | 2 + src/platform/wayland_callbacks.cpp | 10 +-- src/platform/wayland_hyprland.cpp | 50 +++++++---- src/platform/wayland_hyprland.h | 5 +- src/platform/wayland_setups.cpp | 57 +++++++++++- src/platform/wayland_sway.cpp | 12 +-- src/platform/wayland_sway.h | 3 +- 11 files changed, 248 insertions(+), 75 deletions(-) diff --git a/include/platform/wayland_context.h b/include/platform/wayland_context.h index 7dd1cee5..77b9db3c 100644 --- a/include/platform/wayland_context.h +++ b/include/platform/wayland_context.h @@ -94,6 +94,9 @@ struct wayland_context_t { // Output reconnection handling atomic_bool _output_lost{false}; // Set when our output disconnects + // for safe_popen_read_spawn (pointer to global environ) + char **_environ{nullptr}; + wayland_context_t() { for (size_t i = 0; i < MAX_TOP_LEVELS; i++) { tracked_toplevels[i] = {}; @@ -164,6 +167,8 @@ inline void cleanup_wayland(wayland_context_t& ctx) { cleanup_wayland_context(ctx.thread_context); ctx.animation_context = BONGOCAT_NULLPTR; + + ctx._environ = BONGOCAT_NULLPTR; } } // namespace bongocat::platform::wayland diff --git a/include/platform/wayland_setups.h b/include/platform/wayland_setups.h index 30952adc..a1f1a874 100644 --- a/include/platform/wayland_setups.h +++ b/include/platform/wayland_setups.h @@ -4,6 +4,11 @@ #include "graphics/animation.h" #include "platform/wayland_context.h" #include "platform/wayland_shared_memory.h" +#include +#include +#include +#include +#include namespace bongocat::platform::wayland::details { /// @TODO: use created_result_t for shm @@ -15,6 +20,29 @@ BONGOCAT_NODISCARD bongocat_error_t wayland_update_screen_info(wayland_context_t BONGOCAT_NODISCARD bongocat_error_t wayland_setup_surface(wayland_context_t& ctx); BONGOCAT_NODISCARD bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, animation::animation_context_t& animation_ctx); + + +struct spawn_pipe_t; +int safe_pclose_spawn(spawn_pipe_t& sp); + +struct spawn_pipe_t { + FILE *fp{nullptr}; + pid_t pid{-1}; + + spawn_pipe_t() = default; + spawn_pipe_t(const spawn_pipe_t&) = delete; + spawn_pipe_t& operator=(const spawn_pipe_t&) = delete; + spawn_pipe_t(spawn_pipe_t&& other) noexcept : fp(other.fp), pid(other.pid) { + other.fp = nullptr; + other.pid = -1; + } + spawn_pipe_t& operator=(spawn_pipe_t&& other) noexcept = delete; + ~spawn_pipe_t() { + safe_pclose_spawn(*this); + } +}; +BONGOCAT_NODISCARD spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv); + } // namespace bongocat::platform::wayland::details #endif \ No newline at end of file diff --git a/src/config/config.cpp b/src/config/config.cpp index 731c8acd..a677c9ff 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1100,10 +1100,14 @@ static char *config_trim_str(char *key) { static bongocat_error_t config_parse_integer_key(config_t& config, const char *key, const char *value) { errno = 0; char *endptr_int = BONGOCAT_NULLPTR; - const int int_value = static_cast(strtol(value, &endptr_int, 10)); - if (errno != 0 || endptr_int == value || *endptr_int != '\0') { + const auto read_value = strtol(value, &endptr_int, 10); + if (errno != 0 || endptr_int == value || (*endptr_int != '\0' && *endptr_int != ' ' && *endptr_int != '\t')) { return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } + if (read_value < INT_MIN || read_value > INT_MAX) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + const int int_value = static_cast(read_value); if (strcmp(key, CAT_X_OFFSET_KEY) == 0) { config.cat_x_offset = int_value; @@ -1809,6 +1813,15 @@ static bongocat_error_t config_parse_key_value(config_t& config, const char *key } // Handle device keys if (strcmp(key, KEYBOARD_DEVICE_KEY) == 0 || strcmp(key, KEYBOARD_DEVICES_KEY) == 0) { + // Validate path starts with /dev/input/ and has no traversal + if (strncmp(value, "/dev/input/", 11) != 0) { + BONGOCAT_LOG_WARNING("keyboard_device path must start with /dev/input/: %s", value); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + if (strstr(value, "..") != nullptr) { + BONGOCAT_LOG_WARNING("Path traversal detected in device path: %s", value); + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } return config_add_keyboard_device(config, value); } diff --git a/src/core/main.cpp b/src/core/main.cpp index 29bdc2d2..11a49b93 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -201,15 +201,22 @@ struct cli_args_t { inline static constexpr size_t PID_STR_BUF = 64; +/* inline static constexpr auto DEFAULT_PID_FILE = "/tmp/bongocat.pid"; inline static constexpr auto PID_FILE_WITH_SUFFIX_TEMPLATE = "/tmp/bongocat-%s.pid"; inline static constexpr auto PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE = "/tmp/bongocat-%s.%" PRIu32 ".pid"; inline static constexpr auto PID_FILE_WITH_SUFFIX_NR_TEMPLATE = "/tmp/bongocat-%" PRId64 ".pid"; +*/ + +inline static constexpr auto DEFAULT_PID_FILE_PATH_TEMPLATE = "%s/bongocat.pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_PATH_TEMPLATE = "%s/bongocat-%s.pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_MULTI_PATH_TEMPLATE = "%s/bongocat-%s.%" PRIu32 ".pid"; +inline static constexpr auto PID_FILE_WITH_SUFFIX_NR_PATH_TEMPLATE = "%s/bongocat-%" PRId64 ".pid"; inline static constexpr auto DEFAULT_CONF_FILENAME = "bongocat.conf"; static platform::FileDescriptor process_create_pid_file(const char *pid_filename) { - platform::FileDescriptor fd = platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC, 0644)); + platform::FileDescriptor fd = platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0600)); if (fd._fd < 0) { BONGOCAT_LOG_ERROR("Failed to create PID file: %s", strerror(errno)); return platform::FileDescriptor(-1); @@ -278,14 +285,24 @@ static pid_t process_get_running_pid(const char *program_name, const char *pid_f char *endptr = BONGOCAT_NULLPTR; errno = 0; // Reset errno before call - const auto pid = static_cast(strtol(pid_str, &endptr, 10)); + const auto pid_read = strtol(pid_str, &endptr, 10); if (endptr == pid_str) { return -1; // no digits at all } - if ((errno == ERANGE) || pid < 0) { + if ((errno == ERANGE) || pid_read < 0) { BONGOCAT_LOG_ERROR("'%s' out of range for pid_t", pid_str); return -1; } + if (errno != 0 || endptr == pid_str || + (*endptr != '\n' && *endptr != '\0' && *endptr != '\r')) { + BONGOCAT_LOG_ERROR("Invalid PID in PID file"); + return -1; + } + if (pid_read <= 1 || pid_read > INT32_MAX) { + BONGOCAT_LOG_ERROR("PID value out of safe range: %ld", pid_read); + return -1; + } + const pid_t pid = static_cast(pid_read); char exe_path[PATH_MAX] = {0}; snprintf(exe_path, sizeof(exe_path), "/proc/%d/exe", pid); @@ -306,11 +323,76 @@ static pid_t process_get_running_pid(const char *program_name, const char *pid_f } // Check if process is actually running - if (kill(pid, 0) == 0) { - return pid; // Process is running + if (kill(pid, 0) != 0) { + // Process is not running, remove stale PID file + return -1; + } + + // Verify the running process is actually bongocat via /proc/PID/comm + char proc_path[64] = {0}; + snprintf(proc_path, sizeof(proc_path), "/proc/%d/comm", pid); + FILE *fp = fopen(proc_path, "r"); + if (fp != nullptr) { + char comm[64] = {0}; + if (fgets(comm, sizeof(comm), fp) != nullptr) { + comm[strcspn(comm, "\n")] = '\0'; + /// @TODO: better process name validation + if (strcmp(comm, "bongocat") != 0 || + strcmp(comm, "wpets") != 0 || + strcmp(comm, "wpets-all") != 0 || + strcmp(comm, "wpets-dm") != 0 || + strcmp(comm, "wpets-dm-classic") != 0 || + strcmp(comm, "wpets-ms-agent") != 0 || + strcmp(comm, "wpets-pkmn") != 0) { + fclose(fp); + BONGOCAT_LOG_INFO("PID %d is not bongocat (is %s), removing stale file", + pid, comm); + return -1; + } + } + fclose(fp); + } + + return pid; +} + +static AllocatedString get_pid_file_path(const cli_args_t& args, const config::config_t& config) { + constexpr size_t pid_path_size = PATH_MAX; + static_assert(pid_path_size > 1, "pid path length must be fot minimal string"); + AllocatedString pid_path = make_allocated_string(pid_path_size-1); + + constexpr const char* tmp_dir = "/tmp"; + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + const char* pid_dir = (runtime_dir != nullptr && runtime_dir[0] != '\0') ? runtime_dir : tmp_dir; + + BONGOCAT_LOG_VERBOSE("Use path for PID file: %s", pid_dir); + + if (args.nr >= 0) { + // set pid file, based on nr + if (pid_path) { + snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_NR_PATH_TEMPLATE, pid_dir, args.nr); + } + } else if (config.output_name.length() > 0) { + // set pid file, based on output_name + if (!args.ignore_running) { + if (pid_path) { + snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_PATH_TEMPLATE, + pid_dir, config.output_name.c_str()); + } + } else { + if (pid_path) { + snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_MULTI_PATH_TEMPLATE, + pid_dir, config.output_name.c_str(), platform::slow_rand()); + } + } + } else { + if (pid_path) { + snprintf(pid_path.ptr, pid_path.capacity(), DEFAULT_PID_FILE_PATH_TEMPLATE, + pid_dir); + } } - return -1; + return pid_path; } static int process_handle_toggle(const char *program_name, const char *pid_filename) { @@ -906,42 +988,10 @@ int main(int argc, char *argv[]) { } } - if (args.nr >= 0) { - // set pid file, based on nr - const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr) + 1; - assert(needed_size >= 0); - ctx.pid_filename = make_allocated_string(static_cast(needed_size)); - if (ctx.pid_filename) { - snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_NR_TEMPLATE, args.nr); - } - } else if (ctx.config.output_name.length() > 0) { - // set pid file, based on output_name - if (!args.ignore_running) { - const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_TEMPLATE, ctx.config.output_name.c_str()) + 1; - assert(needed_size >= 0); - ctx.pid_filename = make_allocated_string(static_cast(needed_size)); - if (ctx.pid_filename) { - snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_TEMPLATE, - ctx.config.output_name.c_str()); - } - } else { - const int needed_size = snprintf(BONGOCAT_NULLPTR, 0, PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, ctx.config.output_name.c_str(), - platform::slow_rand()) + - 1; - assert(needed_size >= 0); - ctx.pid_filename = make_allocated_string(static_cast(needed_size)); - if (ctx.pid_filename) { - snprintf(ctx.pid_filename.ptr, ctx.pid_filename.capacity(), PID_FILE_WITH_SUFFIX_MULTI_TEMPLATE, - ctx.config.output_name.c_str(), platform::slow_rand()); - } - } - - if (!ctx.pid_filename) [[unlikely]] { - BONGOCAT_LOG_ERROR("Failed to allocate PID filename"); - return EXIT_FAILURE; - } - } else { - ctx.pid_filename = duplicate_string(DEFAULT_PID_FILE); + ctx.pid_filename = get_pid_file_path(args, ctx.config); + if (!ctx.pid_filename) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to allocate PID filename"); + return EXIT_FAILURE; } // Handle toggle mode diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index 0f17e0dd..61946c88 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -82,6 +82,8 @@ created_result_t> create(animation::animation *ret->thread_context._local_copy_config = config; ret->thread_context._bar_height = config.overlay_height; + ret->_environ = ::environ; + return ret; } diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index d4e20ba5..172c9e4b 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -200,7 +200,7 @@ static bool fs_update_state(wayland_context_t& ctx, update_fullscreen_state_topl namespace hyprland { static int fs_update_state(wayland_context_t& ctx) { - if (wayland::hyprland::window_info_t win; wayland::hyprland::get_active_window(win)) { + if (wayland::hyprland::window_info_t win; wayland::hyprland::get_active_window(ctx, win)) { bool find_output = false; wl_output *found_wl_output = nullptr; for (size_t i = 0; i < ctx.output_count; i++) { @@ -224,16 +224,16 @@ namespace hyprland { } } // namespace hyprland -static bool fs_check_compositor_fallback() { +static bool fs_check_compositor_fallback(wayland_context_t& ctx) { // BONGOCAT_LOG_VERBOSE("Using compositor-specific fullscreen detection"); // Try Hyprland first - if (const int result = wayland::hyprland::fs_check_compositor_fallback(); result >= 0) { + if (const int result = wayland::hyprland::fs_check_compositor_fallback(ctx); result >= 0) { return result == 1; } // Try Sway as fallback - if (const int result = wayland::sway::fs_check_compositor_fallback(); result >= 0) { + if (const int result = wayland::sway::fs_check_compositor_fallback(ctx); result >= 0) { return result == 1; } @@ -251,7 +251,7 @@ static bool fs_check_status(wayland_context_t& ctx) { return ctx.fs_detector.has_fullscreen_toplevel; } - return fs_check_compositor_fallback(); + return fs_check_compositor_fallback(ctx); } void fs_update_state_fallback(wayland_context_t& ctx) { diff --git a/src/platform/wayland_hyprland.cpp b/src/platform/wayland_hyprland.cpp index e0e70fd5..f478645c 100644 --- a/src/platform/wayland_hyprland.cpp +++ b/src/platform/wayland_hyprland.cpp @@ -1,5 +1,6 @@ #include "wayland_hyprland.h" +#include "platform/wayland_setups.h" #include "utils/error.h" #include @@ -14,13 +15,15 @@ namespace bongocat::platform::wayland::hyprland { -int fs_check_compositor_fallback() { - FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); - if (fp != BONGOCAT_NULLPTR) { +int fs_check_compositor_fallback(wayland_context_t& ctx) { + constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", NULL}; + details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); + + if (sp.fp != BONGOCAT_NULLPTR) { bool is_fullscreen = false; char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { + while (fgets(line, LINE_BUF, sp.fp)) { const size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') { line[len - 1] = '\0'; @@ -34,7 +37,6 @@ int fs_check_compositor_fallback() { } } - pclose(fp); return is_fullscreen ? 1 : 0; } @@ -42,13 +44,14 @@ int fs_check_compositor_fallback() { } void update_outputs_with_monitor_ids(wayland_context_t& ctx) { - FILE *fp = popen("hyprctl monitors 2>/dev/null", "r"); - if (fp == BONGOCAT_NULLPTR) { + constexpr const char *argv[] = {HYPRCTL_COMMAND, "monitors", NULL}; + details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); + if (sp.fp == BONGOCAT_NULLPTR) { return; } char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { + while (fgets(line, LINE_BUF, sp.fp)) { int id = -1; char name[256] = {0}; int result = sscanf(line, "Monitor %d \"%255[^\"]\"", &id, name); @@ -68,12 +71,12 @@ void update_outputs_with_monitor_ids(wayland_context_t& ctx) { } } } - - pclose(fp); } -bool get_active_window(window_info_t& win) { - FILE *fp = popen("hyprctl activewindow 2>/dev/null", "r"); +bool get_active_window(wayland_context_t& ctx, window_info_t& win) { + constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", NULL}; + details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); + FILE *fp = sp.fp; if (fp == BONGOCAT_NULLPTR) { return false; } @@ -81,14 +84,26 @@ bool get_active_window(window_info_t& win) { bool has_window = false; win.monitor_id = -1; win.fullscreen = false; + win.x = 0; + win.y = 0; + win.width = 0; + win.height = 0; + + char line[4096]; + while (fgets(line, sizeof(line), fp) != nullptr) { + // remove trailing newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } - char line[LINE_BUF]; - while (fgets(line, LINE_BUF, fp)) { // monitor: 0 if (strstr(line, "monitor:") != BONGOCAT_NULLPTR) { - sscanf(line, "%*[\t ]monitor: %d", &win.monitor_id); - has_window = true; + if (sscanf(line, "%*[\t ]monitor: %d", &win.monitor_id) == 1) { + has_window = true; + } } + // fullscreen: 0/1/2 if (strstr(line, "fullscreen:") != BONGOCAT_NULLPTR) { int val; @@ -96,12 +111,14 @@ bool get_active_window(window_info_t& win) { win.fullscreen = (val != 0); } } + // at: X,Y if (strstr(line, "at:") != BONGOCAT_NULLPTR) { if (sscanf(line, "%*[\t ]at: [%d, %d]", &win.x, &win.y) < 2) { sscanf(line, "%*[\t ]at: %d,%d", &win.x, &win.y); } } + // size: W,H if (strstr(line, "size:") != BONGOCAT_NULLPTR) { if (sscanf(line, "%*[\t ]size: [%d, %d]", &win.width, &win.height) < 2) { @@ -110,7 +127,6 @@ bool get_active_window(window_info_t& win) { } } - pclose(fp); return has_window; } diff --git a/src/platform/wayland_hyprland.h b/src/platform/wayland_hyprland.h index 7667dbdc..866a6671 100644 --- a/src/platform/wayland_hyprland.h +++ b/src/platform/wayland_hyprland.h @@ -5,6 +5,7 @@ namespace bongocat::platform::wayland::hyprland { static inline constexpr size_t LINE_BUF = 512; +static inline constexpr const char* HYPRCTL_COMMAND = "/usr/bin/hyprctl"; struct window_info_t { int monitor_id{-1}; // monitor number in Hyprland @@ -15,9 +16,9 @@ struct window_info_t { bool fullscreen{false}; }; -extern int fs_check_compositor_fallback(); +extern int fs_check_compositor_fallback(wayland_context_t& ctx); extern void update_outputs_with_monitor_ids(wayland_context_t& ctx); -extern bool get_active_window(window_info_t& win); +extern bool get_active_window(wayland_context_t& ctx, window_info_t& win); } // namespace bongocat::platform::wayland::hyprland diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp index cb85b5f3..b1ea1f2a 100644 --- a/src/platform/wayland_setups.cpp +++ b/src/platform/wayland_setups.cpp @@ -134,7 +134,9 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { // auto-detect screen width if (wayland_ctx.output != BONGOCAT_NULLPTR) { wl_display_roundtrip(wayland_ctx.display); - if (wayland_ctx._screen_info != BONGOCAT_NULLPTR && wayland_ctx._screen_info->screen_width > 0) { + if (wayland_ctx._screen_info != BONGOCAT_NULLPTR && + wayland_ctx._screen_info->screen_width > 0 && + wayland_ctx._screen_info->screen_width < INT16_MAX) { BONGOCAT_LOG_INFO("Detected screen width: %d", wayland_ctx._screen_info->screen_width); screen_width = wayland_ctx._screen_info->screen_width; } else { @@ -398,4 +400,57 @@ bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, return bongocat_error_t::BONGOCAT_SUCCESS; } + + +spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv) { + int pipefd[2] = {0}; + spawn_pipe_t result; + + if (pipe(pipefd) != 0) { + return result; + } + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + + // Redirect stdout -> pipe + posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO); + + // Redirect stderr -> /dev/null + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + posix_spawn_file_actions_adddup2(&actions, devnull, STDERR_FILENO); + } + + posix_spawn_file_actions_addclose(&actions, pipefd[0]); + + pid_t pid{-1}; + // @NOTE: safe-const cast for argv + if (posix_spawn(&pid, path, &actions, nullptr, const_cast(argv), ctx._environ) != 0) { + close(pipefd[0]); + close(pipefd[1]); + posix_spawn_file_actions_destroy(&actions); + return result; + } + + posix_spawn_file_actions_destroy(&actions); + close(pipefd[1]); + + result.fp = fdopen(pipefd[0], "r"); + result.pid = pid; + return result; +} +int safe_pclose_spawn(spawn_pipe_t& sp) { + int status = 0; + if (sp.fp) { + fclose(sp.fp); + } + + if (sp.pid > 0) { + waitpid(sp.pid, &status, 0); + } + + return status; +} + } // namespace bongocat::platform::wayland::details diff --git a/src/platform/wayland_sway.cpp b/src/platform/wayland_sway.cpp index fb386562..5a8bea11 100644 --- a/src/platform/wayland_sway.cpp +++ b/src/platform/wayland_sway.cpp @@ -1,5 +1,6 @@ #include "wayland_sway.h" +#include "platform/wayland_setups.h" #include "utils/error.h" #include @@ -14,13 +15,15 @@ namespace bongocat::platform::wayland::sway { -int fs_check_compositor_fallback() { - FILE *fp = popen("swaymsg -t get_tree 2>/dev/null", "r"); - if (fp != BONGOCAT_NULLPTR) { +int fs_check_compositor_fallback(wayland_context_t& ctx) { + constexpr const char *argv[] = {SWAYMSG_COMMAND, "-t", "get_tree", NULL}; + details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, SWAYMSG_COMMAND, argv); + + if (sp.fp != BONGOCAT_NULLPTR) { bool is_fullscreen = false; char sway_buffer[SWAY_BUF] = {0}; - while (fgets(sway_buffer, SWAY_BUF, fp)) { + while (fgets(sway_buffer, SWAY_BUF, sp.fp)) { if (strstr(sway_buffer, "\"fullscreen_mode\":1")) { is_fullscreen = true; BONGOCAT_LOG_DEBUG("Fullscreen detected in Sway"); @@ -28,7 +31,6 @@ int fs_check_compositor_fallback() { } } - pclose(fp); return is_fullscreen ? 1 : 0; } diff --git a/src/platform/wayland_sway.h b/src/platform/wayland_sway.h index 3169d37b..23207ebf 100644 --- a/src/platform/wayland_sway.h +++ b/src/platform/wayland_sway.h @@ -5,7 +5,8 @@ namespace bongocat::platform::wayland::sway { static inline constexpr size_t SWAY_BUF = 4096; +static inline constexpr const char * const SWAYMSG_COMMAND = "/usr/bin/swaymsg"; -extern int fs_check_compositor_fallback(); +extern int fs_check_compositor_fallback(wayland_context_t& ctx); } // namespace bongocat::platform::wayland::sway From 3079b94cb05da10fcfd31a5a02204e083735517d Mon Sep 17 00:00:00 2001 From: furudbat Date: Thu, 9 Apr 2026 18:01:12 +0200 Subject: [PATCH 06/17] wip: merge from upstream - wip: frame caching - wip: thread-safe error log --- include/config/config.h | 7 +-- include/core/bongocat.h | 2 +- include/platform/input_context.h | 8 +-- include/platform/wayland_context.h | 5 ++ include/platform/wayland_setups.h | 9 +-- include/platform/wayland_shared_memory.h | 19 ++++++ include/utils/error.h | 14 ----- include/utils/memory.h | 17 +++--- include/utils/system_error.h | 73 ++++++++++++++++++++++++ src/config/config.cpp | 63 ++++++++++---------- src/config/config_watcher.cpp | 7 ++- src/core/main.cpp | 67 +++++++++++----------- src/graphics/animation.cpp | 5 +- src/graphics/animation_init.cpp | 1 + src/graphics/bar.cpp | 30 +++++++++- src/graphics/bar.h | 1 + src/platform/input.cpp | 19 +++--- src/platform/wayland.cpp | 17 +++--- src/platform/wayland_callbacks.cpp | 2 +- src/platform/wayland_hyprland.cpp | 8 +-- src/platform/wayland_hyprland.h | 2 +- src/platform/wayland_setups.cpp | 20 +++---- src/platform/wayland_sway.cpp | 6 +- src/platform/wayland_sway.h | 2 +- src/utils/error.cpp | 4 +- src/utils/memory.cpp | 2 +- 26 files changed, 265 insertions(+), 145 deletions(-) create mode 100644 include/utils/system_error.h diff --git a/include/config/config.h b/include/config/config.h index 69cdc7d0..e56f97b0 100644 --- a/include/config/config.h +++ b/include/config/config.h @@ -160,7 +160,7 @@ struct config_t { // for keep old index when reload config bool _keep_old_animation_index{false}; bool _strict{false}; - bool _custom{false}; // is custom sprite sheet + bool _custom{false}; // is custom sprite sheet AllocatedString _animation_name{BONGOCAT_NULLPTR}; // original animation_anim from parsing config AllocatedString _loaded_animation_fqname{BONGOCAT_NULLPTR}; @@ -431,14 +431,14 @@ struct config_t { for (int i = 0; i < other.num_keyboard_devices; ++i) { keyboard_devices[i] = other.keyboard_devices[i]; release_allocated_string(other.keyboard_devices[i]); - //other.keyboard_devices[i] = BONGOCAT_NULLPTR; + // other.keyboard_devices[i] = BONGOCAT_NULLPTR; } num_keyboard_devices = other.num_keyboard_devices; for (int i = 0; i < other._num_keyboard_names; ++i) { _keyboard_names[i] = other._keyboard_names[i]; release_allocated_string(other._keyboard_names[i]); - //other._keyboard_names[i] = BONGOCAT_NULLPTR; + // other._keyboard_names[i] = BONGOCAT_NULLPTR; } _num_keyboard_names = other._num_keyboard_names; @@ -520,7 +520,6 @@ void reset(config_t& config); void set_defaults(config_t& config); - // Resolve config file path with XDG fallback // Returns a static/allocated path, or NULL if none found. AllocatedString resolve_path(const char *explicit_path); diff --git a/include/core/bongocat.h b/include/core/bongocat.h index 5fe4ff53..b07dd890 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -3,7 +3,7 @@ #ifndef _POSIX_C_SOURCE // POSIX feature test macro - must be before includes -#define _POSIX_C_SOURCE 200809L +# define _POSIX_C_SOURCE 200809L #endif #include "utils/error.h" diff --git a/include/platform/input_context.h b/include/platform/input_context.h index 262834a2..aa7ef496 100644 --- a/include/platform/input_context.h +++ b/include/platform/input_context.h @@ -7,14 +7,14 @@ #include "utils/time.h" #include +#include #include #include -#include namespace bongocat::platform::input { -inline static constexpr size_t MAX_ACTIVE_DEVICES {32}; -inline static constexpr size_t input_hotplug_events {64}; +inline static constexpr size_t MAX_ACTIVE_DEVICES{32}; +inline static constexpr size_t input_hotplug_events{64}; enum class input_unique_file_type_t : uint8_t { NONE, @@ -25,7 +25,7 @@ struct input_unique_file_t; void cleanup(input_unique_file_t& file); struct input_unique_file_t { const char *_device_path{BONGOCAT_NULLPTR}; // original string from config (ref to input_context_t._device_paths[i]) - AllocatedString canonical_path{BONGOCAT_NULLPTR}; // resolved real path + AllocatedString canonical_path{BONGOCAT_NULLPTR}; // resolved real path FileDescriptor fd; input_unique_file_type_t type{input_unique_file_type_t::NONE}; diff --git a/include/platform/wayland_context.h b/include/platform/wayland_context.h index 77b9db3c..f4bf55db 100644 --- a/include/platform/wayland_context.h +++ b/include/platform/wayland_context.h @@ -172,4 +172,9 @@ inline void cleanup_wayland(wayland_context_t& ctx) { } } // namespace bongocat::platform::wayland +namespace bongocat::animation { +void cache_frames(platform::wayland::wayland_context_t& ctx, int target_w, int target_h, int mirror_x, int mirror_y, + int enable_aa); +} // namespace bongocat::animation + #endif diff --git a/include/platform/wayland_setups.h b/include/platform/wayland_setups.h index a1f1a874..a57f53ab 100644 --- a/include/platform/wayland_setups.h +++ b/include/platform/wayland_setups.h @@ -4,6 +4,7 @@ #include "graphics/animation.h" #include "platform/wayland_context.h" #include "platform/wayland_shared_memory.h" + #include #include #include @@ -21,19 +22,18 @@ BONGOCAT_NODISCARD bongocat_error_t wayland_setup_surface(wayland_context_t& ctx BONGOCAT_NODISCARD bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, animation::animation_context_t& animation_ctx); - struct spawn_pipe_t; int safe_pclose_spawn(spawn_pipe_t& sp); struct spawn_pipe_t { - FILE *fp{nullptr}; + FILE *fp{BONGOCAT_NULLPTR}; pid_t pid{-1}; spawn_pipe_t() = default; spawn_pipe_t(const spawn_pipe_t&) = delete; spawn_pipe_t& operator=(const spawn_pipe_t&) = delete; spawn_pipe_t(spawn_pipe_t&& other) noexcept : fp(other.fp), pid(other.pid) { - other.fp = nullptr; + other.fp = BONGOCAT_NULLPTR; other.pid = -1; } spawn_pipe_t& operator=(spawn_pipe_t&& other) noexcept = delete; @@ -41,7 +41,8 @@ struct spawn_pipe_t { safe_pclose_spawn(*this); } }; -BONGOCAT_NODISCARD spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv); +BONGOCAT_NODISCARD spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, + const char *const *argv); } // namespace bongocat::platform::wayland::details diff --git a/include/platform/wayland_shared_memory.h b/include/platform/wayland_shared_memory.h index 7cd33594..938dfccf 100644 --- a/include/platform/wayland_shared_memory.h +++ b/include/platform/wayland_shared_memory.h @@ -16,6 +16,22 @@ void cleanup_shm_buffer(wayland_shm_buffer_t& buffer); struct wayland_thread_context; +// Pre-scaled frame cache (avoids repeated scaling of constant source images) +struct cached_frame_t { + MMapArray data; + int width; + int height; +}; + +struct wayland_context_t; +} // namespace bongocat::platform::wayland + +namespace bongocat::animation { +void invalidate_cache_frames(platform::wayland::wayland_context_t& ctx); +} // namespace bongocat::animation + +namespace bongocat::platform::wayland { + struct wayland_shm_buffer_t { wl_buffer *buffer{BONGOCAT_NULLPTR}; MMapFileBuffer pixels; @@ -27,6 +43,9 @@ struct wayland_shm_buffer_t { animation::animation_context_t *_animation_context{BONGOCAT_NULLPTR}; wayland_thread_context *_wayland_thread_context{BONGOCAT_NULLPTR}; // parent ref. for buffer_release + /// @TODO: add caching for frame buffer + // cached_frame_t _cached_frames[animation::MAX_NUM_FRAMES]; + wayland_shm_buffer_t() = default; ~wayland_shm_buffer_t() { cleanup_shm_buffer(*this); diff --git a/include/utils/error.h b/include/utils/error.h index b65ce86b..ee80de53 100644 --- a/include/utils/error.h +++ b/include/utils/error.h @@ -155,20 +155,6 @@ namespace features { # define BONGOCAT_LOG_VERBOSE(format, ...) #endif -inline int check_errno([[maybe_unused]] const char *fd_name) { - const int err = errno; - // supress compiler warning -#if EAGAIN == EWOULDBLOCK - if (err != EAGAIN && err != -1) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); - } -#else - if (err != EAGAIN && err != EWOULDBLOCK && err != -1) { - BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, strerror(err)); - } -#endif - return err; -} } // namespace bongocat #endif // ERROR_H \ No newline at end of file diff --git a/include/utils/memory.h b/include/utils/memory.h index 45ba1cff..59c4cadf 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -370,7 +370,10 @@ class AllocatedString { return *this; } - AllocatedString(AllocatedString&& other) noexcept : ptr(other.ptr), _capacity(other._capacity), _length(other._length) { + AllocatedString(AllocatedString&& other) noexcept + : ptr(other.ptr) + , _capacity(other._capacity) + , _length(other._length) { other.ptr = BONGOCAT_NULLPTR; other._capacity = 0; other._length = 0; @@ -427,7 +430,7 @@ class AllocatedString { return _capacity; } - constexpr const char * c_str() const noexcept { + constexpr const char *c_str() const noexcept { return ptr; } }; @@ -445,10 +448,10 @@ BONGOCAT_NODISCARD inline static AllocatedString make_null_string() noexcept { BONGOCAT_NODISCARD inline static AllocatedString make_allocated_string(size_t length) { AllocatedString ret; - //ret._length = 0; + // ret._length = 0; if (length > 0) { ret._capacity = length + 1; - ret.ptr = static_cast(BONGOCAT_MALLOC(ret._capacity)); + ret.ptr = static_cast(BONGOCAT_MALLOC(ret._capacity)); if (ret.ptr != BONGOCAT_NULLPTR) { memset(ret.ptr, 0, ret._capacity); ret._length = strlen(ret.ptr); @@ -464,9 +467,9 @@ BONGOCAT_NODISCARD inline static AllocatedString make_allocated_string(size_t le return ret; } -BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char* src) noexcept(true) { +BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char *src) noexcept(true) { AllocatedString ret; - //ret._length = 0; + // ret._length = 0; if (src != nullptr) { ret.ptr = strdup(src); if (ret.ptr != BONGOCAT_NULLPTR) { @@ -485,7 +488,7 @@ BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char* sr return ret; } BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const AllocatedString& other) noexcept(true) { - AllocatedString ret (other); + AllocatedString ret(other); return other; } diff --git a/include/utils/system_error.h b/include/utils/system_error.h new file mode 100644 index 00000000..940bad8a --- /dev/null +++ b/include/utils/system_error.h @@ -0,0 +1,73 @@ +#ifndef BONGOCAT_SYSTEM_ERROR_H +#define BONGOCAT_SYSTEM_ERROR_H + +#include "error.h" +#include "memory.h" + +#include +#include +#include + +namespace bongocat { + +inline static constexpr size_t STRERROR_BUFLEN = 256; + +inline AllocatedString get_strerror(int errnum) { + auto ret = make_allocated_string(STRERROR_BUFLEN); + + if (ret.ptr) { + assert(ret.capacity() >= 2); +#if defined(__GLIBC__) && defined(_GNU_SOURCE) + // GNU version: returns char* + char *msg = strerror_r(errnum, ret.ptr, ret.capacity()); + if (msg != ret.ptr) { + strncpy(ret.ptr, msg, ret.capacity() - 1); + ret.ptr[ret.capacity() - 1] = '\0'; + } +#else + // POSIX version: returns int + if (strerror_r(errnum, ret.ptr, ret.capacity()) != BONGOCAT_NULLPTR) { + snprintf(ret.ptr, ret.capacity(), "Unknown error %d", errnum); + } +#endif + } + + return ret; +} + +inline int check_errno([[maybe_unused]] const char *fd_name) { + const int err = errno; + // supress compiler warning +#if EAGAIN == EWOULDBLOCK + if (err != EAGAIN && err != -1) { + auto errstr = get_strerror(err); + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, errstr.c_str()); + } +#else + if (err != EAGAIN && err != EWOULDBLOCK && err != -1) { + auto errstr = get_strerror(err); + BONGOCAT_LOG_ERROR("Error reading %s: %s", fd_name, errstr.c_str()); + } +#endif + return err; +} + +#if (!defined(BONGOCAT_DISABLE_LOGGER) || defined(BONGOCAT_ENABLE_LOGGER)) && BONGOCAT_LOG_LEVEL >= 1 +# define BONGOCAT_LOG_ERROR_STRERROR_FIRST(format, ...) \ + do { \ + auto _err = ::bongocat::get_strerror(errnum); \ + ::bongocat::details::log_error(format, _err.c_str() __VA_OPT__(, ) __VA_ARGS__) \ + } while (0) +# define BONGOCAT_LOG_ERROR_STRERROR_LAST(format, ...) \ + do { \ + auto _err = ::bongocat::get_strerror(errnum); \ + ::bongocat::details::log_error(format, __VA_OPT__(, ) __VA_ARGS__ __VA_OPT__(, ) _err.c_str()) \ + } while (0) +#else +# define BONGOCAT_LOG_ERROR_STRERROR_FIRST(format, ...) +# define BONGOCAT_LOG_ERROR_STRERROR_LAST(format, ...) +#endif + +} // namespace bongocat + +#endif // ERROR_H \ No newline at end of file diff --git a/src/config/config.cpp b/src/config/config.cpp index a677c9ff..e36ebc50 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -16,15 +16,15 @@ #include #include +#include #include +#include +#include #include #include #include #include -#include #include -#include -#include #include #include @@ -115,7 +115,7 @@ static inline constexpr platform::time_sec_t DEFAULT_TEST_ANIMATION_INTERVAL_SEC static inline constexpr int32_t DEFAULT_ENABLE_ANTIALIASING = 1; static inline constexpr double DEFAULT_MOVEMENT_WAIT_FACTOR = 5.1; static inline constexpr int32_t DEFAULT_ENABLE_HAND_MAPPING = 1; -//static inline constexpr int32_t DEFAULT_HOTPLUG_SCAN_INTERVAL_MS = 300; +// static inline constexpr int32_t DEFAULT_HOTPLUG_SCAN_INTERVAL_MS = 300; // Debug-specific defaults #ifndef NDEBUG @@ -274,7 +274,8 @@ static uint64_t config_validate_timing(config_t& config) { ret |= config_clamp_int(config.idle_sleep_timeout_sec, 0, MAX_SLEEP_TIMEOUT_SEC, IDLE_SLEEP_TIMEOUT_KEY); ret |= config_clamp_int(config.input_fps, 0, MAX_FPS, INPUT_FPS_KEY); ret |= config_clamp_int(config.movement_speed, 0, MAX_DURATION_MS, MOVEMENT_SPEED_KEY); - ret |= config_clamp_int(config.hotplug_scan_interval_ms, MIN_HOTPLUG_SCAN_INTERVAL_MS, MAX_HOTPLUG_SCAN_INTERVAL_MS, HOTPLUG_SCAN_INTERVAL_KEY); + ret |= config_clamp_int(config.hotplug_scan_interval_ms, MIN_HOTPLUG_SCAN_INTERVAL_MS, MAX_HOTPLUG_SCAN_INTERVAL_MS, + HOTPLUG_SCAN_INTERVAL_KEY); // Validate interval (0 is allowed to disable) if (config.test_animation_interval_sec < 0 || config.test_animation_interval_sec > MAX_INTERVAL_SEC) { @@ -1028,12 +1029,10 @@ static void config_cleanup_devices(config_t& config) { config._num_keyboard_names = 0; } - static bongocat_error_t config_resolve_devices(config_t& config) { DIR *dir = opendir("/dev/input"); - if (dir == nullptr) { - BONGOCAT_LOG_WARNING("Failed to open /dev/input for scanning: %s", - strerror(errno)); + if (dir == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_WARNING("Failed to open /dev/input for scanning: %s", strerror(errno)); return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } @@ -1041,7 +1040,7 @@ static bongocat_error_t config_resolve_devices(config_t& config) { char path[PATH_MAX]; char name[256] = {0}; - while ((entry = readdir(dir)) != NULL) { + while ((entry = readdir(dir)) != BONGOCAT_NULLPTR) { if (strncmp(entry->d_name, "event", 5) != 0) { continue; } @@ -1059,8 +1058,9 @@ static bongocat_error_t config_resolve_devices(config_t& config) { assert(config._num_keyboard_names >= 0); for (size_t i = 0; i < static_cast(config._num_keyboard_names); i++) { assert(config._keyboard_names[i]); - if (strstr(name, config._keyboard_names[i].c_str()) != nullptr) { - BONGOCAT_LOG_INFO("Found device matching name '%s' (Device: '%s'): %s", config._keyboard_names[i].c_str(), name, path); + if (strstr(name, config._keyboard_names[i].c_str()) != BONGOCAT_NULLPTR) { + BONGOCAT_LOG_INFO("Found device matching name '%s' (Device: '%s'): %s", config._keyboard_names[i].c_str(), + name, path); matched = true; break; } @@ -1071,7 +1071,7 @@ static bongocat_error_t config_resolve_devices(config_t& config) { config_add_keyboard_device(config, path); } - //platform::close_fd(fd); + // platform::close_fd(fd); } closedir(dir); @@ -1482,7 +1482,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_dm(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1501,7 +1501,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_dm20(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1520,7 +1520,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_dmx(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1539,7 +1539,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_dmc(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1558,7 +1558,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_pen(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1577,7 +1577,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_pen20(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1596,7 +1596,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_dmall(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1680,7 +1680,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_pkmn(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1702,7 +1702,7 @@ static bongocat_error_t config_parse_string(config_t& config, const char *key, c */ config._loaded_animation_fqname = duplicate_string(get_config_animation_name_pmd(static_cast(found_index)).fqname); - BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname); + BONGOCAT_LOG_DEBUG("Animation found for %s: %s", value, config._loaded_animation_fqname.c_str()); } animation_found = config.animation_index >= 0; } @@ -1818,7 +1818,7 @@ static bongocat_error_t config_parse_key_value(config_t& config, const char *key BONGOCAT_LOG_WARNING("keyboard_device path must start with /dev/input/: %s", value); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } - if (strstr(value, "..") != nullptr) { + if (strstr(value, "..") != BONGOCAT_NULLPTR) { BONGOCAT_LOG_WARNING("Path traversal detected in device path: %s", value); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } @@ -1973,7 +1973,7 @@ void set_defaults(config_t& config) { cfg.screen_width = 0; cfg.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; cfg.custom_sprite_sheet_settings = {}; - //cfg.hotplug_scan_interval_ms = DEFAULT_HOTPLUG_SCAN_INTERVAL_MS; + // cfg.hotplug_scan_interval_ms = DEFAULT_HOTPLUG_SCAN_INTERVAL_MS; for (int i = 0; i < static_cast(input::MAX_INPUT_DEVICES); i++) { cfg._keyboard_names[i] = BONGOCAT_NULLPTR; @@ -2049,8 +2049,8 @@ static void config_log_summary(const config_t& config) { case config_animation_custom_set_t::custom: assert(config.custom_sprite_sheet_filename); assert(config._custom); - BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename.c_str(), config.cat_x_offset, - config.cat_y_offset); + BONGOCAT_LOG_DEBUG(" Custom: %s at offset (%d,%d)", config.custom_sprite_sheet_filename.c_str(), + config.cat_x_offset, config.cat_y_offset); break; } break; @@ -2088,7 +2088,7 @@ created_result_t load(const char *config_file_path, load_config_overwr } if (overwrite_parameters.output_name != BONGOCAT_NULLPTR) { - //release_allocated_string(ret.output_name); + // release_allocated_string(ret.output_name); ret.output_name = duplicate_string(overwrite_parameters.output_name); } if (overwrite_parameters.randomize_index >= 0) { @@ -2106,8 +2106,7 @@ created_result_t load(const char *config_file_path, load_config_overwr // Continue on scan failure so static keyboard_device entries still work. result = config_resolve_devices(ret); if (result != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - BONGOCAT_LOG_WARNING("Failed to resolve keyboard names, continuing: %s", - bongocat::error_string(result)); + BONGOCAT_LOG_WARNING("Failed to resolve keyboard names, continuing: %s", bongocat::error_string(result)); } // Set default keyboard device if none specified @@ -2155,7 +2154,7 @@ void reset(config_t& config) { } AllocatedString resolve_path(const char *explicit_path) { - if (explicit_path != nullptr) { + if (explicit_path != BONGOCAT_NULLPTR) { return duplicate_string(explicit_path); } @@ -2163,7 +2162,7 @@ AllocatedString resolve_path(const char *explicit_path) { // 1. $XDG_CONFIG_HOME/bongocat/bongocat.conf const char *xdg_config = getenv("XDG_CONFIG_HOME"); - if (xdg_config != nullptr && xdg_config[0] != '\0') { + if (xdg_config != BONGOCAT_NULLPTR && xdg_config[0] != '\0') { snprintf(path, sizeof(path), "%s/bongocat/bongocat.conf", xdg_config); if (access(path, R_OK) == 0) { return duplicate_string(path); @@ -2172,7 +2171,7 @@ AllocatedString resolve_path(const char *explicit_path) { // 2. ~/.config/bongocat/bongocat.conf const char *home = getenv("HOME"); - if (home != nullptr && home[0] != '\0') { + if (home != BONGOCAT_NULLPTR && home[0] != '\0') { snprintf(path, sizeof(path), "%s/.config/bongocat/bongocat.conf", home); if (access(path, R_OK) == 0) { return duplicate_string(path); diff --git a/src/config/config_watcher.cpp b/src/config/config_watcher.cpp index 2899231c..5424fa91 100644 --- a/src/config/config_watcher.cpp +++ b/src/config/config_watcher.cpp @@ -23,7 +23,8 @@ static inline constexpr platform::time_ms_t RELOAD_DELAY_MS = 100; static inline constexpr platform::time_ms_t RECREATE_SLEEP_ATTEMPT_MS = 100; static inline constexpr platform::time_ms_t TIMEOUT_MS = 100; -static constexpr uint32_t FILE_MASK = IN_CLOSE_WRITE | IN_MODIFY | IN_MOVED_TO | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; +static constexpr uint32_t FILE_MASK = + IN_CLOSE_WRITE | IN_MODIFY | IN_MOVED_TO | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; static constexpr uint32_t DIR_MASK = IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM; static bongocat_error_t config_watcher_add_watch(config_watcher_t& watcher) { @@ -41,7 +42,8 @@ static bongocat_error_t config_watcher_add_watch(config_watcher_t& watcher) { watcher.wd_dir = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, dirname(watcher.config_path.ptr), DIR_MASK)); - watcher.wd_file = platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path.c_str(), FILE_MASK)); + watcher.wd_file = + platform::FileDescriptor(inotify_add_watch(watcher.inotify_fd._fd, watcher.config_path.c_str(), FILE_MASK)); if ((watcher.wd_dir._fd < 0 || watcher.wd_file._fd < 0) && features::Debug) { BONGOCAT_LOG_WARNING("config_watcher: partial reinit of inotify watches"); @@ -187,7 +189,6 @@ static void *config_watcher_thread(void *arg) { } } - // ensure file exists (from IN_CLOSE_WRITE/IN_MODIFY) if (should_reload) { // If we don't currently have a file watch (it was removed), try stat the file diff --git a/src/core/main.cpp b/src/core/main.cpp index 11a49b93..ebd92116 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -7,6 +7,7 @@ #include "platform/wayland.h" #include "utils/error.h" #include "utils/memory.h" +#include "utils/system_error.h" #include #include @@ -216,7 +217,8 @@ inline static constexpr auto PID_FILE_WITH_SUFFIX_NR_PATH_TEMPLATE = "%s/bongoca inline static constexpr auto DEFAULT_CONF_FILENAME = "bongocat.conf"; static platform::FileDescriptor process_create_pid_file(const char *pid_filename) { - platform::FileDescriptor fd = platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0600)); + platform::FileDescriptor fd = + platform::FileDescriptor(open(pid_filename, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0600)); if (fd._fd < 0) { BONGOCAT_LOG_ERROR("Failed to create PID file: %s", strerror(errno)); return platform::FileDescriptor(-1); @@ -293,8 +295,7 @@ static pid_t process_get_running_pid(const char *program_name, const char *pid_f BONGOCAT_LOG_ERROR("'%s' out of range for pid_t", pid_str); return -1; } - if (errno != 0 || endptr == pid_str || - (*endptr != '\n' && *endptr != '\0' && *endptr != '\r')) { + if (errno != 0 || endptr == pid_str || (*endptr != '\n' && *endptr != '\0' && *endptr != '\r')) { BONGOCAT_LOG_ERROR("Invalid PID in PID file"); return -1; } @@ -332,21 +333,16 @@ static pid_t process_get_running_pid(const char *program_name, const char *pid_f char proc_path[64] = {0}; snprintf(proc_path, sizeof(proc_path), "/proc/%d/comm", pid); FILE *fp = fopen(proc_path, "r"); - if (fp != nullptr) { + if (fp != BONGOCAT_NULLPTR) { char comm[64] = {0}; - if (fgets(comm, sizeof(comm), fp) != nullptr) { + if (fgets(comm, sizeof(comm), fp) != BONGOCAT_NULLPTR) { comm[strcspn(comm, "\n")] = '\0'; /// @TODO: better process name validation - if (strcmp(comm, "bongocat") != 0 || - strcmp(comm, "wpets") != 0 || - strcmp(comm, "wpets-all") != 0 || - strcmp(comm, "wpets-dm") != 0 || - strcmp(comm, "wpets-dm-classic") != 0 || - strcmp(comm, "wpets-ms-agent") != 0 || - strcmp(comm, "wpets-pkmn") != 0) { + if (strcmp(comm, "bongocat") != 0 || strcmp(comm, "wpets") != 0 || strcmp(comm, "wpets-all") != 0 || + strcmp(comm, "wpets-dm") != 0 || strcmp(comm, "wpets-dm-classic") != 0 || + strcmp(comm, "wpets-ms-agent") != 0 || strcmp(comm, "wpets-pkmn") != 0) { fclose(fp); - BONGOCAT_LOG_INFO("PID %d is not bongocat (is %s), removing stale file", - pid, comm); + BONGOCAT_LOG_INFO("PID %d is not bongocat (is %s), removing stale file", pid, comm); return -1; } } @@ -359,11 +355,11 @@ static pid_t process_get_running_pid(const char *program_name, const char *pid_f static AllocatedString get_pid_file_path(const cli_args_t& args, const config::config_t& config) { constexpr size_t pid_path_size = PATH_MAX; static_assert(pid_path_size > 1, "pid path length must be fot minimal string"); - AllocatedString pid_path = make_allocated_string(pid_path_size-1); + AllocatedString pid_path = make_allocated_string(pid_path_size - 1); - constexpr const char* tmp_dir = "/tmp"; + constexpr const char *tmp_dir = "/tmp"; const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); - const char* pid_dir = (runtime_dir != nullptr && runtime_dir[0] != '\0') ? runtime_dir : tmp_dir; + const char *pid_dir = (runtime_dir != BONGOCAT_NULLPTR && runtime_dir[0] != '\0') ? runtime_dir : tmp_dir; BONGOCAT_LOG_VERBOSE("Use path for PID file: %s", pid_dir); @@ -376,19 +372,18 @@ static AllocatedString get_pid_file_path(const cli_args_t& args, const config::c // set pid file, based on output_name if (!args.ignore_running) { if (pid_path) { - snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_PATH_TEMPLATE, - pid_dir, config.output_name.c_str()); + snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_PATH_TEMPLATE, pid_dir, + config.output_name.c_str()); } } else { if (pid_path) { - snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_MULTI_PATH_TEMPLATE, - pid_dir, config.output_name.c_str(), platform::slow_rand()); + snprintf(pid_path.ptr, pid_path.capacity(), PID_FILE_WITH_SUFFIX_MULTI_PATH_TEMPLATE, pid_dir, + config.output_name.c_str(), platform::slow_rand()); } } } else { if (pid_path) { - snprintf(pid_path.ptr, pid_path.capacity(), DEFAULT_PID_FILE_PATH_TEMPLATE, - pid_dir); + snprintf(pid_path.ptr, pid_path.capacity(), DEFAULT_PID_FILE_PATH_TEMPLATE, pid_dir); } } @@ -461,10 +456,12 @@ static void config_reload_callback() { assert(get_main_context().input != BONGOCAT_NULLPTR); assert(get_main_context().animation != BONGOCAT_NULLPTR); assert(get_main_context().signal_watch_path); - BONGOCAT_LOG_INFO("Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path.c_str(), - (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path.c_str() : "OFF"); + BONGOCAT_LOG_INFO( + "Reloading configuration from: %s (config_watcher=%s)", get_main_context().signal_watch_path.c_str(), + (get_main_context().config_watcher) ? get_main_context().config_watcher->config_path.c_str() : "OFF"); assert(get_main_context().config_watcher == BONGOCAT_NULLPTR || - strcmp(get_main_context().config_watcher->config_path.c_str(), get_main_context().signal_watch_path.c_str()) == 0); + strcmp(get_main_context().config_watcher->config_path.c_str(), get_main_context().signal_watch_path.c_str()) == + 0); if (strcmp(get_main_context().signal_watch_path.c_str(), "-") == 0) { BONGOCAT_LOG_WARNING("No reload config for stdin"); @@ -797,14 +794,14 @@ static bongocat_error_t system_initialize_components(main_context_t& ctx) { // ============================================================================= static void cli_show_help(const char *program_name) { - char *base_program_name = strdup(program_name); - if (base_program_name == BONGOCAT_NULLPTR) [[unlikely]] { + auto base_program_name = duplicate_string(program_name); + if (!base_program_name) [[unlikely]] { perror("strdup"); return; } printf("Bongo Cat Wayland Overlay.\n\n"); - printf("Usage: %s [OPTIONS]\n\n", basename(base_program_name)); + printf("Usage: %s [OPTIONS]\n\n", basename(base_program_name.ptr)); printf("Options:\n"); printf(" -h, --help Show this help message\n"); printf(" -v, --version Show version information\n"); @@ -813,7 +810,8 @@ static void cli_show_help(const char *program_name) { printf(" -t, --toggle Toggle bongocat on/off (start if not running, stop if running)\n"); printf(" -o, --output-name NAME Specify output name (overwrite output_name from config)\n"); printf(" -m, --monitor NAME Bind to a specific monitor output (same as --output-name)\n"); - printf(" --random Enable random animation_index, at start (overwrite random_index from config)\n"); + printf( + " --random Enable random animation_index, at start (overwrite random_index from config)\n"); printf(" --strict Enable strict mode, only start up with a valid config and valid parameter\n"); printf(" --nr NR Specify Nr. for PID file to avoid conflicting ruinning instances\n"); printf(" --ignore-running Ignore current running instance\n"); @@ -855,8 +853,6 @@ static void cli_show_help(const char *program_name) { printf(" %8s - MS Agent\n", "ms_agent"); } printf("\n"); - - ::free(base_program_name); } static void cli_show_version() { @@ -931,6 +927,11 @@ int main(int argc, char *argv[]) { using namespace bongocat; // Initialize error system early bongocat::error_init(true); // Enable debug initially + { + // Lifetime extension of temporary objects + assert(get_strerror(1).c_str()); + BONGOCAT_LOG_VERBOSE("Test: Error string: %s", get_strerror(1).c_str()); + } // Parse command line arguments const auto [args, args_result] = cli_parse_arguments(argc, argv); @@ -959,7 +960,7 @@ int main(int argc, char *argv[]) { .strict = args.strict, }; AllocatedString existing_config_file = config::resolve_path(args.config_file); - const char* config_file = existing_config_file ? existing_config_file.c_str() : ""; + const char *config_file = existing_config_file ? existing_config_file.c_str() : ""; if (args.strict >= 1) { if (strcmp(config_file, "-") != 0 && access(config_file, F_OK) != 0) { BONGOCAT_LOG_ERROR("Configuration file required: %s", config_file); diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp index 980f7a40..3c03926a 100644 --- a/src/graphics/animation.cpp +++ b/src/graphics/animation.cpp @@ -14,6 +14,7 @@ #include "graphics/embedded_assets_pkmn.h" #include "image_loader/bongocat/load_images_bongocat.h" #include "platform/wayland.h" +#include "utils/system_error.h" #include "utils/time.h" #include @@ -2586,7 +2587,7 @@ anim_ms_agent_process_animation(animation_player_result_t& new_animation_result, anim_ms_agent_process_animation_result_t ret{.row_state = new_state.row_state, .status = anim_ms_agent_process_animation_result_status_t::None}; - if (section != nullptr && section->valid) { + if (section != BONGOCAT_NULLPTR && section->valid) { new_animation_result.sprite_sheet_row = section->row; new_animation_result.sprite_sheet_col = new_animation_result.sprite_sheet_col + 1; ret.status = anim_ms_agent_process_animation_result_status_t::Updated; @@ -2695,7 +2696,7 @@ anim_ms_agent_restart_animation([[maybe_unused]] animation_thread_context_t& ctx anim_ms_agent_process_animation_result_t ret{.row_state = new_state.row_state, .status = anim_ms_agent_process_animation_result_status_t::None}; - if (section != nullptr && section->valid) { + if (section != BONGOCAT_NULLPTR && section->valid) { new_state.row_state = new_row_state; new_animation_result.sprite_sheet_row = section->row; new_animation_result.sprite_sheet_col = section->start_col; diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index 3572fad1..9c31a3a2 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -42,6 +42,7 @@ #include "image_loader/pen20/load_images_pen20.h" #include "image_loader/pkmn/load_images_pkmn.h" #include "image_loader/pmd/load_images_pmd.h" +#include "utils/system_error.h" namespace bongocat::animation { [[maybe_unused]] static constexpr bool should_load_bongocat([[maybe_unused]] const config::config_t& config) { diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index f6650e8e..42f5295e 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -127,7 +127,8 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w // Skip fullscreen hiding when layer is LAYER_OVERLAY (always visible) const bool is_overlay_layer = current_config.layer == config::layer_type_t::LAYER_OVERLAY; - const bool is_fullscreen = !is_overlay_layer && (current_config.disable_fullscreen_hide <= 0 && wayland_ctx._fullscreen_detected); + const bool is_fullscreen = + !is_overlay_layer && (current_config.disable_fullscreen_hide <= 0 && wayland_ctx._fullscreen_detected); const bool bar_visible = !is_fullscreen && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; @@ -283,6 +284,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } + // draw debug bar const bool bar_visible = !wayland_ctx._fullscreen_detected && wayland_ctx.bar_visibility == platform::wayland::bar_visibility_t::Show; const int effective_opacity = bar_visible ? current_config.overlay_opacity : 0; @@ -926,4 +928,30 @@ draw_bar_result_t draw_bar(platform::wayland::wayland_context_t& ctx) { return draw_bar_result_t::NoFlushNeeded; } + +void invalidate_cache_frames(platform::wayland::wayland_context_t& /*ctx*/) { + for (size_t i = 0; i < animation::MAX_NUM_FRAMES; i++) { + /* + platform::release_allocated_mmap_array(ctx._cached_frames[i].data); + ctx._cached_frames[i].data = BONGOCAT_NULLPTR; + ctx._cached_frames[i].width = 0; + ctx._cached_frames[i].height = 0; + */ + } +} +void cache_frames(platform::wayland::wayland_context_t& ctx, int target_w, int target_h, int /*mirror_x*/, + int /*mirror_y*/, int /*enable_aa*/) { + if (ctx.thread_context.ctx_shm) { + invalidate_cache_frames(ctx); + + for (size_t i = 0; i < animation::MAX_NUM_FRAMES; i++) { + if (target_w <= 0 || target_h <= 0) { + continue; + } + + /// @TODO: fill cache + } + } +} + } // namespace bongocat::animation diff --git a/src/graphics/bar.h b/src/graphics/bar.h index f8f43bfc..3b093b0d 100644 --- a/src/graphics/bar.h +++ b/src/graphics/bar.h @@ -13,6 +13,7 @@ enum class draw_bar_result_t : uint8_t { // Draw the overlay bar draw_bar_result_t draw_bar(platform::wayland::wayland_context_t& ctx); + } // namespace bongocat::animation #endif // BONGOCAT_ANIMATION_BAR_H \ No newline at end of file diff --git a/src/platform/input.cpp b/src/platform/input.cpp index b94dc72b..ce0adaf6 100644 --- a/src/platform/input.cpp +++ b/src/platform/input.cpp @@ -306,7 +306,7 @@ sync_devices(input_context_t& input, sync_devices_options_t options = {}) { struct stat st{}; if (stat(candidate, &st) == 0 && S_ISCHR(st.st_mode)) { if (need_reopen) { - FileDescriptor fd (open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC)); + FileDescriptor fd(open(candidate, O_RDONLY | O_NONBLOCK | O_CLOEXEC)); if (fd._fd >= 0 && is_open_device_valid(fd._fd)) { cur.fd = bongocat::move(fd); BONGOCAT_LOG_INFO("Opened input device: %s (fd=%d)", candidate, cur.fd._fd); @@ -749,13 +749,14 @@ static void *input_thread(void *arg) { } if (!disable_adaptive_check_interval) { - if (!found_new_device && adaptive_check_interval_ms < MAX_ADAPTIVE_CHECK_INTERVAL_SEC*1000) { - adaptive_check_interval_ms = (adaptive_check_interval_ms < MID_ADAPTIVE_CHECK_INTERVAL_SEC*1000) - ? MID_ADAPTIVE_CHECK_INTERVAL_SEC*1000 - : MAX_ADAPTIVE_CHECK_INTERVAL_SEC*1000; - BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", adaptive_check_interval_ms/1000); - } else if (found_new_device && adaptive_check_interval_ms > START_ADAPTIVE_CHECK_INTERVAL_SEC*1000) { - adaptive_check_interval_ms = START_ADAPTIVE_CHECK_INTERVAL_SEC*1000; + if (!found_new_device && adaptive_check_interval_ms < MAX_ADAPTIVE_CHECK_INTERVAL_SEC * 1000) { + adaptive_check_interval_ms = (adaptive_check_interval_ms < MID_ADAPTIVE_CHECK_INTERVAL_SEC * 1000) + ? MID_ADAPTIVE_CHECK_INTERVAL_SEC * 1000 + : MAX_ADAPTIVE_CHECK_INTERVAL_SEC * 1000; + BONGOCAT_LOG_DEBUG("input: Increased device check interval to %d seconds", + adaptive_check_interval_ms / 1000); + } else if (found_new_device && adaptive_check_interval_ms > START_ADAPTIVE_CHECK_INTERVAL_SEC * 1000) { + adaptive_check_interval_ms = START_ADAPTIVE_CHECK_INTERVAL_SEC * 1000; BONGOCAT_LOG_DEBUG("input: Reset device check interval to %d seconds", START_ADAPTIVE_CHECK_INTERVAL_SEC); } } @@ -815,7 +816,7 @@ static void *input_thread(void *arg) { bool sync_devices_needed = false; bool reload_devices_needed = false; if (pfds[fds_udev_monitor_index].revents & POLLIN) { - //BONGOCAT_LOG_VERBOSE("input: Receive udev event"); + // BONGOCAT_LOG_VERBOSE("input: Receive udev event"); size_t attempts = 0; udev_device *dev = BONGOCAT_NULLPTR; while ((dev = udev_monitor_receive_device(input._udev_mon)) != BONGOCAT_NULLPTR && attempts < MAX_ATTEMPTS) { diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index 61946c88..ca51687b 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -27,6 +27,7 @@ // #include "wayland_sway.h" #include "platform/wayland_callbacks.h" #include "platform/wayland_setups.h" +#include "utils/system_error.h" namespace bongocat::platform::wayland { // ============================================================================= @@ -154,9 +155,8 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int // Periodic fullscreen check for fallback fullscreen detection timespec now{}; clock_gettime(CLOCK_MONOTONIC, &now); - const time_ms_t elapsed_ms = - ((now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L) + - ((now.tv_nsec - ctx.fs_detector.last_check.tv_nsec) / 1000000L); + const time_ms_t elapsed_ms = ((now.tv_sec - ctx.fs_detector.last_check.tv_sec) * 1000L) + + ((now.tv_nsec - ctx.fs_detector.last_check.tv_nsec) / 1000000L); time_ms_t fullscreen_check_interval_ms = frame_based_timeout; if (fullscreen_check_interval_ms < CHECK_INTERVAL_MS) { @@ -532,9 +532,9 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, } } - if (old_screen_name != nullptr) { + if (old_screen_name != BONGOCAT_NULLPTR) { ::free(old_screen_name); - old_screen_name = nullptr; + old_screen_name = BONGOCAT_NULLPTR; } /// @NOTE: assume animation has the same local copy as wayland config @@ -545,8 +545,11 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, } const char *get_current_layer_name(wayland_context_t& ctx) { - if (ctx.thread_context.ctx_shm && (atomic_load(&ctx.thread_context.ctx_shm->configured) && ctx.thread_context._local_copy_config)) { - return ctx.thread_context._local_copy_config->layer == config::layer_type_t::LAYER_OVERLAY ? WAYLAND_LAYER_NAME_OVERLAY : WAYLAND_LAYER_NAME_TOP; + if (ctx.thread_context.ctx_shm && + (atomic_load(&ctx.thread_context.ctx_shm->configured) && ctx.thread_context._local_copy_config)) { + return ctx.thread_context._local_copy_config->layer == config::layer_type_t::LAYER_OVERLAY + ? WAYLAND_LAYER_NAME_OVERLAY + : WAYLAND_LAYER_NAME_TOP; } return WAYLAND_LAYER_NAME_TOP; } diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index 172c9e4b..ee832669 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -202,7 +202,7 @@ namespace hyprland { static int fs_update_state(wayland_context_t& ctx) { if (wayland::hyprland::window_info_t win; wayland::hyprland::get_active_window(ctx, win)) { bool find_output = false; - wl_output *found_wl_output = nullptr; + wl_output *found_wl_output = BONGOCAT_NULLPTR; for (size_t i = 0; i < ctx.output_count; i++) { if (ctx.outputs[i].hypr_id == win.monitor_id) { if (ctx.thread_context.output == ctx.outputs[i].wl_output) { diff --git a/src/platform/wayland_hyprland.cpp b/src/platform/wayland_hyprland.cpp index f478645c..24e36e46 100644 --- a/src/platform/wayland_hyprland.cpp +++ b/src/platform/wayland_hyprland.cpp @@ -16,7 +16,7 @@ namespace bongocat::platform::wayland::hyprland { int fs_check_compositor_fallback(wayland_context_t& ctx) { - constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", NULL}; + constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", BONGOCAT_NULLPTR}; details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); if (sp.fp != BONGOCAT_NULLPTR) { @@ -44,7 +44,7 @@ int fs_check_compositor_fallback(wayland_context_t& ctx) { } void update_outputs_with_monitor_ids(wayland_context_t& ctx) { - constexpr const char *argv[] = {HYPRCTL_COMMAND, "monitors", NULL}; + constexpr const char *argv[] = {HYPRCTL_COMMAND, "monitors", BONGOCAT_NULLPTR}; details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); if (sp.fp == BONGOCAT_NULLPTR) { return; @@ -74,7 +74,7 @@ void update_outputs_with_monitor_ids(wayland_context_t& ctx) { } bool get_active_window(wayland_context_t& ctx, window_info_t& win) { - constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", NULL}; + constexpr const char *argv[] = {HYPRCTL_COMMAND, "activewindow", BONGOCAT_NULLPTR}; details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, HYPRCTL_COMMAND, argv); FILE *fp = sp.fp; if (fp == BONGOCAT_NULLPTR) { @@ -90,7 +90,7 @@ bool get_active_window(wayland_context_t& ctx, window_info_t& win) { win.height = 0; char line[4096]; - while (fgets(line, sizeof(line), fp) != nullptr) { + while (fgets(line, sizeof(line), fp) != BONGOCAT_NULLPTR) { // remove trailing newline size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') { diff --git a/src/platform/wayland_hyprland.h b/src/platform/wayland_hyprland.h index 866a6671..7b8fbb4d 100644 --- a/src/platform/wayland_hyprland.h +++ b/src/platform/wayland_hyprland.h @@ -5,7 +5,7 @@ namespace bongocat::platform::wayland::hyprland { static inline constexpr size_t LINE_BUF = 512; -static inline constexpr const char* HYPRCTL_COMMAND = "/usr/bin/hyprctl"; +static inline constexpr const char *HYPRCTL_COMMAND = "/usr/bin/hyprctl"; struct window_info_t { int monitor_id{-1}; // monitor number in Hyprland diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp index b1ea1f2a..8f6fb4e8 100644 --- a/src/platform/wayland_setups.cpp +++ b/src/platform/wayland_setups.cpp @@ -42,7 +42,7 @@ static_assert((CREATE_SHM_NAME_PREFIX_LEN + CREATE_SHM_NAME_SUFFIX_LEN) == LEN_A // ============================================================================= created_result_t create_shm(off_t size) { - char *name = strdup(CREATE_SHM_NAME_TEMPLATE); + auto name = duplicate_string(CREATE_SHM_NAME_TEMPLATE); constexpr char charset_arr[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; constexpr size_t charset_len = sizeof(charset_arr) - 1; int fd = -1; @@ -51,23 +51,22 @@ created_result_t create_shm(off_t size) { for (int i = 0; i < CREATE_SHM_MAX_ATTEMPTS; i++) { for (size_t j = 0; j < CREATE_SHM_NAME_SUFFIX_LEN; j++) { static_assert(sizeof(charset_arr) - 1 > 0); - name[CREATE_SHM_NAME_PREFIX_LEN + j] = charset_arr[rng.range(0, charset_len - 1)]; + assert(CREATE_SHM_NAME_PREFIX_LEN + j < name.capacity()); + name.ptr[CREATE_SHM_NAME_PREFIX_LEN + j] = charset_arr[rng.range(0, charset_len - 1)]; } - fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); if (fd >= 0) { - shm_unlink(name); + shm_unlink(name.c_str()); break; } } if (fd < 0 || ftruncate(fd, size) < 0) { - ::free(name); close(fd); fd = -1; return bongocat_error_t::BONGOCAT_ERROR_FILE_IO; } - ::free(name); return FileDescriptor(fd); } @@ -104,7 +103,8 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { BONGOCAT_LOG_ERROR("Could not find output named '%s'", current_config.output_name.c_str()); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } else { - BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", current_config.output_name.c_str()); + BONGOCAT_LOG_ERROR("Could not find output named '%s', defaulting to first output", + current_config.output_name.c_str()); } } } @@ -134,8 +134,7 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { // auto-detect screen width if (wayland_ctx.output != BONGOCAT_NULLPTR) { wl_display_roundtrip(wayland_ctx.display); - if (wayland_ctx._screen_info != BONGOCAT_NULLPTR && - wayland_ctx._screen_info->screen_width > 0 && + if (wayland_ctx._screen_info != BONGOCAT_NULLPTR && wayland_ctx._screen_info->screen_width > 0 && wayland_ctx._screen_info->screen_width < INT16_MAX) { BONGOCAT_LOG_INFO("Detected screen width: %d", wayland_ctx._screen_info->screen_width); screen_width = wayland_ctx._screen_info->screen_width; @@ -401,7 +400,6 @@ bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, return bongocat_error_t::BONGOCAT_SUCCESS; } - spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv) { int pipefd[2] = {0}; spawn_pipe_t result; @@ -426,7 +424,7 @@ spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, con pid_t pid{-1}; // @NOTE: safe-const cast for argv - if (posix_spawn(&pid, path, &actions, nullptr, const_cast(argv), ctx._environ) != 0) { + if (posix_spawn(&pid, path, &actions, BONGOCAT_NULLPTR, const_cast(argv), ctx._environ) != 0) { close(pipefd[0]); close(pipefd[1]); posix_spawn_file_actions_destroy(&actions); diff --git a/src/platform/wayland_sway.cpp b/src/platform/wayland_sway.cpp index 5a8bea11..7e066625 100644 --- a/src/platform/wayland_sway.cpp +++ b/src/platform/wayland_sway.cpp @@ -16,15 +16,15 @@ namespace bongocat::platform::wayland::sway { int fs_check_compositor_fallback(wayland_context_t& ctx) { - constexpr const char *argv[] = {SWAYMSG_COMMAND, "-t", "get_tree", NULL}; + constexpr const char *argv[] = {SWAYMSG_COMMAND, "-t", "get_tree", BONGOCAT_NULLPTR}; details::spawn_pipe_t sp = details::safe_popen_read_spawn(ctx, SWAYMSG_COMMAND, argv); if (sp.fp != BONGOCAT_NULLPTR) { bool is_fullscreen = false; char sway_buffer[SWAY_BUF] = {0}; - while (fgets(sway_buffer, SWAY_BUF, sp.fp)) { - if (strstr(sway_buffer, "\"fullscreen_mode\":1")) { + while (fgets(sway_buffer, SWAY_BUF, sp.fp) != BONGOCAT_NULLPTR) { + if (strstr(sway_buffer, "\"fullscreen_mode\":1") != BONGOCAT_NULLPTR) { is_fullscreen = true; BONGOCAT_LOG_DEBUG("Fullscreen detected in Sway"); break; diff --git a/src/platform/wayland_sway.h b/src/platform/wayland_sway.h index 23207ebf..087237a1 100644 --- a/src/platform/wayland_sway.h +++ b/src/platform/wayland_sway.h @@ -5,7 +5,7 @@ namespace bongocat::platform::wayland::sway { static inline constexpr size_t SWAY_BUF = 4096; -static inline constexpr const char * const SWAYMSG_COMMAND = "/usr/bin/swaymsg"; +static inline constexpr const char *const SWAYMSG_COMMAND = "/usr/bin/swaymsg"; extern int fs_check_compositor_fallback(wayland_context_t& ctx); diff --git a/src/utils/error.cpp b/src/utils/error.cpp index 5f3bdd93..ba11954e 100644 --- a/src/utils/error.cpp +++ b/src/utils/error.cpp @@ -38,7 +38,7 @@ namespace details { localtime_r(&ts.tv_sec, &tm_info); // Thread-safe strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info); - fprintf(stream, "[%s.%03ld] ", timestamp, ts.tv_nsec / 1000000); + fprintf(stream, "[%s.%03ld] ", timestamp, ts.tv_nsec / 1000000L); } // Core log function using va_list @@ -47,7 +47,7 @@ namespace details { assert(name_len > 0); platform::LockGuard guard(get_log_mutex()); - //char message[1024]; + // char message[1024]; log_timestamp(stdout); fprintf(stdout, "%.*s: ", name_len, name); vfprintf(stdout, format, args); diff --git a/src/utils/memory.cpp b/src/utils/memory.cpp index cc83a0e9..09916d43 100644 --- a/src/utils/memory.cpp +++ b/src/utils/memory.cpp @@ -151,7 +151,7 @@ memory_pool_t *memory_pool_create(size_t size, size_t alignment) { // Validate alignment is a power of 2 if ((alignment & (alignment - 1)) != 0) { BONGOCAT_LOG_ERROR("Memory pool alignment must be a power of 2, got %zu", alignment); - return NULL; + return BONGOCAT_NULLPTR; } auto *pool = static_cast(bongocat::malloc(sizeof(memory_pool_t))); From ffec78640526a63329f9483f64be58a7ce76f4ef Mon Sep 17 00:00:00 2001 From: furudbat Date: Tue, 14 Apr 2026 17:58:30 +0200 Subject: [PATCH 07/17] wip: merge from upstream - wip: replace png bongocat with svg --- CMakeLists.txt | 4 +- CMakePresets.json | 75 +- assets/new/bongo-both-down.svg | 42 + assets/new/bongo-both-up.svg | 21 + assets/new/bongo-left-down.svg | 24 + assets/new/bongo-right-down.svg | 25 + assets/new/bongo-sleeping.svg | 37 + bongocat.conf.example | 3 +- docs/begin.base.bongocat.conf.md | 2 + include/core/bongocat.h | 6 + include/embedded_assets/bongocat/bongocat.h | 10 +- .../bongocat/bongocat_images.h | 18 + .../bongocat/load_images_bongocat.h | 12 +- include/image_loader/load_images.h | 54 +- include/image_loader/load_svgs.h | 128 + include/utils/error.h | 1 + include/utils/memory.h | 24 + lib/nanosvg.h | 3133 +++++++++++++++++ lib/nanosvgrast.h | 1472 ++++++++ src/embedded_assets/bongocat/CMakeLists.txt | 3 +- .../bongocat/bongocat_get_sprite_sheet.cpp | 2 + .../bongocat_get_sprite_sheet_svg.cpp | 24 + .../bongocat/bongocat_images.c | 29 + src/graphics/animation_init.cpp | 12 +- src/image_loader/CMakeLists.txt | 15 + .../bongocat/load_images_bongocat.cpp | 85 +- src/image_loader/load_images.cpp | 37 +- src/image_loader/load_svgs.cpp | 213 ++ src/image_loader/nanosvg.c | 28 + src/platform/input.cpp | 38 +- 30 files changed, 5508 insertions(+), 69 deletions(-) create mode 100644 assets/new/bongo-both-down.svg create mode 100644 assets/new/bongo-both-up.svg create mode 100644 assets/new/bongo-left-down.svg create mode 100644 assets/new/bongo-right-down.svg create mode 100644 assets/new/bongo-sleeping.svg create mode 100644 include/image_loader/load_svgs.h create mode 100644 lib/nanosvg.h create mode 100644 lib/nanosvgrast.h create mode 100644 src/embedded_assets/bongocat/bongocat_get_sprite_sheet_svg.cpp create mode 100644 src/image_loader/load_svgs.cpp create mode 100644 src/image_loader/nanosvg.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 90aaea54..64d9e23e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ project( # Feature Flags include(CMakeDependentOption) option(FEATURE_BONGOCAT_EMBEDDED_ASSETS "Include bongocat assets (default)" ON) +cmake_dependent_option(FEATURE_USE_BONGOCAT_SVG "Use bongocat SVG embedded assets" ON FEATURE_BONGOCAT_EMBEDDED_ASSETS OFF) option(FEATURE_ENABLE_DM_EMBEDDED_ASSETS "Enable include dm embedded assets" OFF) cmake_dependent_option(FEATURE_DM_EMBEDDED_ASSETS "Include dm embedded assets" OFF FEATURE_ENABLE_DM_EMBEDDED_ASSETS OFF) cmake_dependent_option(FEATURE_DM20_EMBEDDED_ASSETS "Include dm20 embedded assets (replaces original dm)" OFF @@ -317,8 +318,7 @@ if(FEATURE_MULTI_VERSIONS) target_link_libraries(bongocat-pkmn PRIVATE bongocat_base bongocat_options bongocat_libs) add_executable(bongocat-all) - target_link_libraries(bongocat-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface - )# include bongocat as fallback + target_link_libraries(bongocat-all PRIVATE assets_bongocat_loader assets_bongocat assets_bongocat_feature assets_bongocat_interface) target_link_libraries(bongocat-all PRIVATE assets_dm_loader assets_dm assets_dm_feature assets_dm_interface) target_link_libraries(bongocat-all PRIVATE assets_dm20_loader assets_dm20 assets_dm20_feature assets_dm20_interface) target_link_libraries(bongocat-all PRIVATE assets_dmx_loader assets_dmx assets_dmx_feature assets_dmx_interface) diff --git a/CMakePresets.json b/CMakePresets.json index 1ee73ab4..9788f240 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -89,6 +89,24 @@ "FEATURE_LAZY_LOAD_ASSETS": "OFF" } }, + { + "name": "release_preload_assets_svg", + "displayName": "Release (preload assets + svg)", + "description": "Release with assets preloaded at the start", + "generator": "Ninja", + "binaryDir": "${sourceDir}/cmake-build-release-preload-assets", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "FEATURE_BONGOCAT_EMBEDDED_ASSETS": "ON", + "FEATURE_ENABLE_DM_EMBEDDED_ASSETS": "ON", + "FEATURE_MS_AGENT_EMBEDDED_ASSETS": "ON", + "FEATURE_PKMN_EMBEDDED_ASSETS": "ON", + "FEATURE_MISC_EMBEDDED_ASSETS": "ON", + "FEATURE_PRELOAD_ASSETS": "ON", + "FEATURE_LAZY_LOAD_ASSETS": "OFF", + "FEATURE_USE_BONGOCAT_SVG": "ON" + } + }, { "name": "release_only_bongocat", "displayName": "Release (Only Bongocat)", @@ -101,7 +119,8 @@ "FEATURE_MS_AGENT_EMBEDDED_ASSETS": "OFF", "FEATURE_ENABLE_DM_EMBEDDED_ASSETS": "OFF", "FEATURE_PKMN_EMBEDDED_ASSETS": "OFF", - "FEATURE_MISC_EMBEDDED_ASSETS": "OFF" + "FEATURE_MISC_EMBEDDED_ASSETS": "OFF", + "FEATURE_USE_BONGOCAT_SVG": "ON" } }, { @@ -255,6 +274,60 @@ "FEATURE_PRELOAD_ASSETS": "OFF", "FEATURE_LAZY_LOAD_ASSETS": "ON" } + }, + { + "name": "debug_all_assets_lazy_load_svg", + "displayName": "Debug (All Assets + colored sprites + svg, assets lazy load)", + "description": "Debug build with ALL assets (full dm versions + colored sprites), lazy load assets on demand", + "generator": "Ninja", + "binaryDir": "${sourceDir}/cmake-build-debug-all-assets-lazy-load", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ENABLE_ASAN": "ON", + "ENABLE_UBSAN": "ON", + "FEATURE_BONGOCAT_EMBEDDED_ASSETS": "ON", + "FEATURE_MS_AGENT_EMBEDDED_ASSETS": "ON", + "FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS": "ON", + "FEATURE_ENABLE_DM_EMBEDDED_ASSETS": "ON", + "FEATURE_DM_EMBEDDED_ASSETS": "ON", + "FEATURE_DM20_EMBEDDED_ASSETS": "ON", + "FEATURE_DMX_EMBEDDED_ASSETS": "ON", + "FEATURE_PEN_EMBEDDED_ASSETS": "ON", + "FEATURE_PEN20_EMBEDDED_ASSETS": "ON", + "FEATURE_DMC_EMBEDDED_ASSETS": "ON", + "FEATURE_DMALL_EMBEDDED_ASSETS": "ON", + "FEATURE_PKMN_EMBEDDED_ASSETS": "ON", + "FEATURE_MISC_EMBEDDED_ASSETS": "ON", + "FEATURE_PRELOAD_ASSETS": "OFF", + "FEATURE_LAZY_LOAD_ASSETS": "ON", + "FEATURE_USE_BONGOCAT_SVG": "ON" + } + }, + { + "name": "debug_svg", + "displayName": "Debug Build", + "description": "Debug build with sanitizers", + "generator": "Ninja", + "binaryDir": "${sourceDir}/cmake-build-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ENABLE_ASAN": "ON", + "ENABLE_UBSAN": "ON", + "FEATURE_USE_BONGOCAT_SVG": "ON" + } + }, + { + "name": "debug_no_svg", + "displayName": "Debug Build", + "description": "Debug build with sanitizers", + "generator": "Ninja", + "binaryDir": "${sourceDir}/cmake-build-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ENABLE_ASAN": "ON", + "ENABLE_UBSAN": "ON", + "FEATURE_USE_BONGOCAT_SVG": "OFF" + } } ], "buildPresets": [ diff --git a/assets/new/bongo-both-down.svg b/assets/new/bongo-both-down.svg new file mode 100644 index 00000000..da59b611 --- /dev/null +++ b/assets/new/bongo-both-down.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + bongo-cat-left-down + + + + + + + + + + + + bongo-cat-right-down + + + + + + \ No newline at end of file diff --git a/assets/new/bongo-both-up.svg b/assets/new/bongo-both-up.svg new file mode 100644 index 00000000..32e7f20b --- /dev/null +++ b/assets/new/bongo-both-up.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/new/bongo-left-down.svg b/assets/new/bongo-left-down.svg new file mode 100644 index 00000000..198efd6d --- /dev/null +++ b/assets/new/bongo-left-down.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + bongo-cat-left-down + + + + + + \ No newline at end of file diff --git a/assets/new/bongo-right-down.svg b/assets/new/bongo-right-down.svg new file mode 100644 index 00000000..b661ec9f --- /dev/null +++ b/assets/new/bongo-right-down.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + bongo-cat-right-down + + + + + + \ No newline at end of file diff --git a/assets/new/bongo-sleeping.svg b/assets/new/bongo-sleeping.svg new file mode 100644 index 00000000..1671e8b8 --- /dev/null +++ b/assets/new/bongo-sleeping.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + bongo-cat-left-down + + + + + + + + bongo-cat-right-down + + + + bongo-cat-both-down + + + + \ No newline at end of file diff --git a/bongocat.conf.example b/bongocat.conf.example index d199dad2..2c2a11f5 100644 --- a/bongocat.conf.example +++ b/bongocat.conf.example @@ -45,7 +45,8 @@ mirror_y=0 # Anti-aliasing settings # enable_antialiasing: Use bilinear interpolation for smooth scaling (0 = off, 1 = on) -# When enabled, provides smoother edges when scaling the cat image +# When enabled, provides smoother edges when scaling sprites +# Recommended for pixel sprites to be off. enable_antialiasing=1 # Size settings diff --git a/docs/begin.base.bongocat.conf.md b/docs/begin.base.bongocat.conf.md index c9fc6c78..b3304322 100644 --- a/docs/begin.base.bongocat.conf.md +++ b/docs/begin.base.bongocat.conf.md @@ -79,6 +79,8 @@ _**overlay_height** should work on config reload, it may take a second to reappe # ANTI-ALIASING - **enable_antialiasing**: Smooth scaling using bilinear interpolation (0 = off, 1 = on). + - Recommended for pixel sprites to be off (Digimon, Pokemon) + - Recommended for MS Agent to be on # SPRITE diff --git a/include/core/bongocat.h b/include/core/bongocat.h index b07dd890..4d111163 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -55,8 +55,14 @@ namespace features { #ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS inline static constexpr bool EnableBongocatEmbeddedAssets = true; +# ifdef FEATURE_USE_BONGOCAT_SVG + inline static constexpr bool EnableBongocatSvg = true; +# else + inline static constexpr bool EnableBongocatSvg = false; +# endif #else inline static constexpr bool EnableBongocatEmbeddedAssets = false; + inline static constexpr bool EnableBongocatSvg = false; #endif #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS diff --git a/include/embedded_assets/bongocat/bongocat.h b/include/embedded_assets/bongocat/bongocat.h index 7258d1dd..487520e2 100644 --- a/include/embedded_assets/bongocat/bongocat.h +++ b/include/embedded_assets/bongocat/bongocat.h @@ -17,8 +17,9 @@ inline static constexpr int BONGOCAT_FRAME_BOTH_UP = 0; inline static constexpr int BONGOCAT_FRAME_LEFT_DOWN = 1; inline static constexpr int BONGOCAT_FRAME_RIGHT_DOWN = 2; inline static constexpr int BONGOCAT_FRAME_BOTH_DOWN = 3; +inline static constexpr int BONGOCAT_FRAME_SLEEPING = 4; -inline static constexpr size_t BONGOCAT_SPRITE_SHEET_COLS = 4; +inline static constexpr size_t BONGOCAT_SPRITE_SHEET_COLS = 5; inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROWS = 1; inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROW = 0; @@ -26,12 +27,19 @@ inline static constexpr size_t BONGOCAT_SPRITE_SHEET_ROW = 0; inline static constexpr int BONGOCAT_FRAME_WIDTH = 864; inline static constexpr int BONGOCAT_FRAME_HEIGHT = 360; +inline static constexpr int BONGOCAT_SVG_FRAME_WIDTH = 500; +inline static constexpr int BONGOCAT_SVG_FRAME_HEIGHT = 277; + inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; BONGOCAT_NODISCARD extern embedded_image_t get_bongocat_sprite(size_t i); BONGOCAT_NODISCARD extern created_result_t get_bongocat_sprite_sheet(const animation::animation_thread_context_t& ctx, int index); + +#ifdef FEATURE_USE_BONGOCAT_SVG +BONGOCAT_NODISCARD extern embedded_image_t get_bongocat_sprite_svg(size_t i); +#endif } // namespace bongocat::assets #endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H diff --git a/include/embedded_assets/bongocat/bongocat_images.h b/include/embedded_assets/bongocat/bongocat_images.h index e8a459d0..7f038f18 100644 --- a/include/embedded_assets/bongocat/bongocat_images.h +++ b/include/embedded_assets/bongocat/bongocat_images.h @@ -16,4 +16,22 @@ extern const size_t bongo_cat_right_down_png_size; extern const unsigned char bongo_cat_both_down_png[]; extern const size_t bongo_cat_both_down_png_size; +#ifdef FEATURE_USE_BONGOCAT_SVG +// Embedded asset data (new svg files) +extern const unsigned char bongo_cat_both_up_svg[]; +extern const size_t bongo_cat_both_up_svg_size; + +extern const unsigned char bongo_cat_left_down_svg[]; +extern const size_t bongo_cat_left_down_svg_size; + +extern const unsigned char bongo_cat_right_down_svg[]; +extern const size_t bongo_cat_right_down_svg_size; + +extern const unsigned char bongo_cat_both_down_svg[]; +extern const size_t bongo_cat_both_down_svg_size; + +extern const unsigned char bongo_cat_sleeping_svg[]; +extern const size_t bongo_cat_sleeping_svg_size; +#endif + #endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_IMAGES_H diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index d0fdcb83..7d03d741 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -6,11 +6,17 @@ namespace bongocat::animation { struct animation_thread_context_t; + +enum class load_bongocat_anim_type_t : uint8_t { + PNG, + SVG, +}; BONGOCAT_NODISCARD created_result_t -load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count); +load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height); + bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, - size_t embedded_images_count); + size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height); BONGOCAT_NODISCARD created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, - int index); + int index); } // namespace bongocat::animation diff --git a/include/image_loader/load_images.h b/include/image_loader/load_images.h index bdc7fcf1..90619ffc 100644 --- a/include/image_loader/load_images.h +++ b/include/image_loader/load_images.h @@ -6,7 +6,6 @@ #include "embedded_assets/embedded_image.h" #include "graphics/sprite_sheet.h" #include "utils/memory.h" - #include namespace bongocat::animation { @@ -15,7 +14,7 @@ namespace bongocat::animation { // ============================================================================= class Image; -created_result_t load_image(const unsigned char *data, size_t size, int desired_channels = RGBA_CHANNELS); +BONGOCAT_NODISCARD created_result_t load_image(const unsigned char *data, size_t size, int desired_channels = RGBA_CHANNELS); void cleanup_image(Image& image); void init_image_loader(); @@ -35,8 +34,25 @@ class Image { assert(width >= 0); assert(height >= 0); assert(channels >= 0); - const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); - pixels = static_cast(::malloc(data_size)); + const size_t data_size = static_cast(width) * + static_cast(height) * + static_cast(channels); + if (data_size == 0 || other.pixels == nullptr) { + width = 0; + height = 0; + channels = 0; + pixels = nullptr; + return; + } + + pixels = static_cast(::malloc(data_size)); + if (pixels == BONGOCAT_NULLPTR) { + width = 0; + height = 0; + channels = 0; + return; + } + ::memcpy(pixels, other.pixels, data_size); } Image& operator=(const Image& other) { @@ -44,18 +60,30 @@ class Image { return *this; } + assert(other.width >= 0); + assert(other.height >= 0); + assert(other.channels >= 0); + const size_t data_size = static_cast(other.width) * + static_cast(other.height) * + static_cast(other.channels); + + unsigned char* new_pixels = nullptr; + if (data_size != 0 && other.pixels != nullptr) { + new_pixels = static_cast(::malloc(data_size)); + if (new_pixels == BONGOCAT_NULLPTR) { + return *this; + } + + ::memcpy(new_pixels, other.pixels, data_size); + } + cleanup_image(*this); width = other.width; height = other.height; channels = other.channels; - - assert(width >= 0); - assert(height >= 0); - assert(channels >= 0); - const size_t data_size = static_cast(width) * static_cast(height) * static_cast(channels); - pixels = static_cast(::malloc(data_size)); - ::memcpy(pixels, other.pixels, data_size); + pixels = new_pixels; + new_pixels = BONGOCAT_NULLPTR; return *this; } @@ -88,6 +116,9 @@ class Image { } }; +BONGOCAT_NODISCARD created_result_t make_image(int width, int height, int desired_channels = RGBA_CHANNELS); + + using get_sprite_callback_t = assets::embedded_image_t (*)(size_t); struct animation_thread_context_t; @@ -97,6 +128,7 @@ anim_sprite_sheet_from_embedded_images(get_sprite_callback_t get_sprite, size_t BONGOCAT_NODISCARD created_result_t load_sprite_sheet_anim(const config::config_t& config, const assets::embedded_image_t& sprite_sheet_image, int sprite_sheet_cols, int sprite_sheet_rows); + } // namespace bongocat::animation #endif \ No newline at end of file diff --git a/include/image_loader/load_svgs.h b/include/image_loader/load_svgs.h new file mode 100644 index 00000000..c463d1e8 --- /dev/null +++ b/include/image_loader/load_svgs.h @@ -0,0 +1,128 @@ +#ifndef BONGOCAT_EMBEDDED_LOAD_SVGS_H +#define BONGOCAT_EMBEDDED_LOAD_SVGS_H + +#include "load_images.h" +#include "config/config.h" +#include "core/bongocat.h" +#include "embedded_assets/embedded_image.h" +#include "graphics/sprite_sheet.h" +#include "utils/memory.h" +#ifdef FEATURE_USE_RASTER_IMAGE_LOADER +#include "load_images.h" +#include +#endif +#include +#include + +namespace bongocat::animation { +// ============================================================================= +// SVG LOADING MODULE +// ============================================================================= + +constexpr static inline const char* SVG_UNITS = "px"; +constexpr static inline float SVG_DPI = 96.0; + +class SvgImage; +BONGOCAT_NODISCARD created_result_t load_svg(char *data, const char* units, float dpi); +void cleanup_svg(SvgImage& image); +void init_svg_loader(); + +#ifdef FEATURE_USE_RASTER_IMAGE_LOADER +class SvgRasterImage; +void cleanup_svg_raster(SvgRasterImage& image); +BONGOCAT_NODISCARD created_result_t create_svg_rasterizer(); + +struct LoadSvgImageParams { + float tx; + float ty; + float scale; + int w; + int h; + int stride; +}; +BONGOCAT_NODISCARD created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params); +#endif + +class SvgImage { +public: + NSVGimage *image{BONGOCAT_NULLPTR}; + AllocatedString _units; + float _dpi; + + SvgImage() = default; + ~SvgImage() { + cleanup_svg(*this); + } + + /// @TODO: ctor svg image + SvgImage(const SvgImage& other) = delete; + SvgImage& operator=(const SvgImage& other) = delete; + + SvgImage(SvgImage&& other) noexcept + : image(other.image), _units(bongocat::move(other._units)), _dpi(other._dpi) { + other.image = BONGOCAT_NULLPTR; + other._units = BONGOCAT_NULLPTR; + other._dpi = 0; + } + SvgImage& operator=(SvgImage&& other) noexcept { + if (this == &other) { + return *this; + } + + cleanup_svg(*this); + + image = other.image; + _units = other._units; + _dpi = other._dpi; + + other.image = BONGOCAT_NULLPTR; + other._units = BONGOCAT_NULLPTR; + other._dpi = 0; + + return *this; + } +}; + +#ifdef FEATURE_USE_RASTER_IMAGE_LOADER +class SvgRasterImage { +public: + NSVGrasterizer *image{BONGOCAT_NULLPTR}; + + SvgRasterImage() { + image = nsvgCreateRasterizer(); + } + ~SvgRasterImage() { + cleanup_svg_raster(*this); + } + + /// @TODO: ctor svg image + SvgRasterImage(const SvgRasterImage& other) = delete; + SvgRasterImage& operator=(const SvgRasterImage& other) = delete; + + SvgRasterImage(SvgRasterImage&& other) noexcept + : image(other.image) { + other.image = BONGOCAT_NULLPTR; + } + SvgRasterImage& operator=(SvgRasterImage&& other) noexcept { + if (this == &other) { + return *this; + } + + cleanup_svg_raster(*this); + + image = other.image; + + other.image = BONGOCAT_NULLPTR; + + return *this; + } +}; + +BONGOCAT_NODISCARD created_result_t +anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, int target_w, int target_h); + +#endif + +} // namespace bongocat::animation + +#endif \ No newline at end of file diff --git a/include/utils/error.h b/include/utils/error.h index ee80de53..507e071e 100644 --- a/include/utils/error.h +++ b/include/utils/error.h @@ -56,6 +56,7 @@ enum class bongocat_error_t : uint8_t { BONGOCAT_ERROR_THREAD, BONGOCAT_ERROR_INVALID_PARAM, BONGOCAT_ERROR_IMAGE, + BONGOCAT_ERROR_SVG, }; // ============================================================================= diff --git a/include/utils/memory.h b/include/utils/memory.h index 59c4cadf..5480a138 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -487,6 +487,30 @@ BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char *sr ret.ptr = BONGOCAT_NULLPTR; return ret; } +BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const char *src, size_t length) noexcept(true) { + AllocatedString ret; + // ret._length = 0; + if (src != nullptr && length > 0) { + ret.ptr = static_cast(::malloc(length + 1)); + if (ret.ptr != BONGOCAT_NULLPTR) { + const size_t len = strnlen(src, length); + ::memcpy(ret.ptr, src, len); + ret.ptr[len] = '\0'; + + ret._length = strlen(ret.ptr); + ret._capacity = length + 1; + return ret; + } else { + ret._capacity = 0; + ret._length = 0; + BONGOCAT_LOG_ERROR("memory allocation failed"); + } + } + ret._capacity = 0; + ret._length = 0; + ret.ptr = BONGOCAT_NULLPTR; + return ret; +} BONGOCAT_NODISCARD inline static AllocatedString duplicate_string(const AllocatedString& other) noexcept(true) { AllocatedString ret(other); return other; diff --git a/lib/nanosvg.h b/lib/nanosvg.h new file mode 100644 index 00000000..a4fbe470 --- /dev/null +++ b/lib/nanosvg.h @@ -0,0 +1,3133 @@ + +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example + * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) + * + * Arc calculation code based on canvg (https://code.google.com/p/canvg/) + * + * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + * + */ + +#ifndef NANOSVG_H +#define NANOSVG_H + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +// NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. +// +// The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. +// +// NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! +// +// The shapes in the SVG images are transformed by the viewBox and converted to specified units. +// That is, you should get the same looking data as your designed in your favorite app. +// +// NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose +// to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. +// +// The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. +// DPI (dots-per-inch) controls how the unit conversion is done. +// +// If you don't know or care about the units stuff, "px" and 96 should get you going. + + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + printf("size: %f x %f\n", image->width, image->height); + // Use... + for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + for (int i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); + } + } + } + // Delete + nsvgDelete(image); +*/ + +enum NSVGpaintType { + NSVG_PAINT_UNDEF = -1, + NSVG_PAINT_NONE = 0, + NSVG_PAINT_COLOR = 1, + NSVG_PAINT_LINEAR_GRADIENT = 2, + NSVG_PAINT_RADIAL_GRADIENT = 3 +}; + +enum NSVGspreadType { + NSVG_SPREAD_PAD = 0, + NSVG_SPREAD_REFLECT = 1, + NSVG_SPREAD_REPEAT = 2 +}; + +enum NSVGlineJoin { + NSVG_JOIN_MITER = 0, + NSVG_JOIN_ROUND = 1, + NSVG_JOIN_BEVEL = 2 +}; + +enum NSVGlineCap { + NSVG_CAP_BUTT = 0, + NSVG_CAP_ROUND = 1, + NSVG_CAP_SQUARE = 2 +}; + +enum NSVGfillRule { + NSVG_FILLRULE_NONZERO = 0, + NSVG_FILLRULE_EVENODD = 1 +}; + +enum NSVGflags { + NSVG_FLAGS_VISIBLE = 0x01 +}; + +enum NSVGpaintOrder { + NSVG_PAINT_FILL = 0x00, + NSVG_PAINT_MARKERS = 0x01, + NSVG_PAINT_STROKE = 0x02, +}; + +typedef struct NSVGgradientStop { + unsigned int color; + float offset; +} NSVGgradientStop; + +typedef struct NSVGgradient { + float xform[6]; + char spread; + float fx, fy; + int nstops; + NSVGgradientStop stops[1]; +} NSVGgradient; + +typedef struct NSVGpaint { + signed char type; + union { + unsigned int color; + NSVGgradient* gradient; + }; +} NSVGpaint; + +typedef struct NSVGpath +{ + float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... + int npts; // Total number of bezier points. + char closed; // Flag indicating if shapes should be treated as closed. + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + struct NSVGpath* next; // Pointer to next path, or NULL if last element. +} NSVGpath; + +typedef struct NSVGshape +{ + char id[64]; // Optional 'id' attr of the shape or its group + NSVGpaint fill; // Fill paint + NSVGpaint stroke; // Stroke paint + float opacity; // Opacity of the shape. + float strokeWidth; // Stroke width (scaled). + float strokeDashOffset; // Stroke dash offset (scaled). + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. + char strokeLineJoin; // Stroke join type. + char strokeLineCap; // Stroke cap type. + float miterLimit; // Miter limit + char fillRule; // Fill rule, see NSVGfillRule. + unsigned char paintOrder; // Encoded paint order (3×2-bit fields) see NSVGpaintOrder + unsigned char flags; // Logical or of NSVG_FLAGS_* flags + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + char fillGradient[64]; // Optional 'id' of fill gradient + char strokeGradient[64]; // Optional 'id' of stroke gradient + float xform[6]; // Root transformation for fill/stroke gradient + NSVGpath* paths; // Linked list of paths in the image. + struct NSVGshape* next; // Pointer to next shape, or NULL if last element. +} NSVGshape; + +typedef struct NSVGimage +{ + float width; // Width of the image. + float height; // Height of the image. + NSVGshape* shapes; // Linked list of shapes in the image. +} NSVGimage; + +// Parses SVG file from a file, returns SVG image as paths. +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); + +// Parses SVG file from a null terminated string, returns SVG image as paths. +// Important note: changes the string. +NSVGimage* nsvgParse(char* input, const char* units, float dpi); + +// Duplicates a path. +NSVGpath* nsvgDuplicatePath(NSVGpath* p); + +// Deletes an image. +void nsvgDelete(NSVGimage* image); + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVG_IMPLEMENTATION + +#include +#include +#include +#include + +#define NSVG_PI (3.14159265358979323846264338327f) +#define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. + +#define NSVG_ALIGN_MIN 0 +#define NSVG_ALIGN_MID 1 +#define NSVG_ALIGN_MAX 2 +#define NSVG_ALIGN_NONE 0 +#define NSVG_ALIGN_MEET 1 +#define NSVG_ALIGN_SLICE 2 + +#define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) +#define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) + +#ifdef _MSC_VER + #pragma warning (disable: 4996) // Switch off security warnings + #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings + #ifdef __cplusplus + #define NSVG_INLINE inline + #else + #define NSVG_INLINE + #endif +#else + #define NSVG_INLINE inline +#endif + + +static int nsvg__isspace(char c) +{ + return strchr(" \t\n\v\f\r", c) != 0; +} + +static int nsvg__isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } +static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } + + +// Simple XML parser + +#define NSVG_XML_TAG 1 +#define NSVG_XML_CONTENT 2 +#define NSVG_XML_MAX_ATTRIBS 256 + +static void nsvg__parseContent(char* s, + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + // Trim start white spaces + while (*s && nsvg__isspace(*s)) s++; + if (!*s) return; + + if (contentCb) + (*contentCb)(ud, s); +} + +static void nsvg__parseElement(char* s, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void* ud) +{ + const char* attr[NSVG_XML_MAX_ATTRIBS]; + int nattr = 0; + char* name; + int start = 0; + int end = 0; + char quote; + + // Skip white space after the '<' + while (*s && nsvg__isspace(*s)) s++; + + // Check if the tag is end tag + if (*s == '/') { + s++; + end = 1; + } else { + start = 1; + } + + // Skip comments, data and preprocessor stuff. + if (!*s || *s == '?' || *s == '!') + return; + + // Get tag name + name = s; + while (*s && !nsvg__isspace(*s)) s++; + if (*s) { *s++ = '\0'; } + + // Get attribs + while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { + char* name = NULL; + char* value = NULL; + + // Skip white space before the attrib name + while (*s && nsvg__isspace(*s)) s++; + if (!*s) break; + if (*s == '/') { + end = 1; + break; + } + name = s; + // Find end of the attrib name. + while (*s && !nsvg__isspace(*s) && *s != '=') s++; + if (*s) { *s++ = '\0'; } + // Skip until the beginning of the value. + while (*s && *s != '\"' && *s != '\'') s++; + if (!*s) break; + quote = *s; + s++; + // Store value and find the end of it. + value = s; + while (*s && *s != quote) s++; + if (*s) { *s++ = '\0'; } + + // Store only well formed attributes + if (name && value) { + attr[nattr++] = name; + attr[nattr++] = value; + } + } + + // List terminator + attr[nattr++] = 0; + attr[nattr++] = 0; + + // Call callbacks. + if (start && startelCb) + (*startelCb)(ud, name, attr); + if (end && endelCb) + (*endelCb)(ud, name); +} + +int nsvg__parseXML(char* input, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + char* s = input; + char* mark = s; + int state = NSVG_XML_CONTENT; + while (*s) { + if (*s == '<' && state == NSVG_XML_CONTENT) { + // Start of a tag + *s++ = '\0'; + nsvg__parseContent(mark, contentCb, ud); + mark = s; + state = NSVG_XML_TAG; + } else if (*s == '>' && state == NSVG_XML_TAG) { + // Start of a content or new tag. + *s++ = '\0'; + nsvg__parseElement(mark, startelCb, endelCb, ud); + mark = s; + state = NSVG_XML_CONTENT; + } else { + s++; + } + } + + return 1; +} + + +/* Simple SVG parser. */ + +#define NSVG_MAX_ATTR 128 + +enum NSVGgradientUnits { + NSVG_USER_SPACE = 0, + NSVG_OBJECT_SPACE = 1 +}; + +#define NSVG_MAX_DASHES 8 + +enum NSVGunits { + NSVG_UNITS_USER, + NSVG_UNITS_PX, + NSVG_UNITS_PT, + NSVG_UNITS_PC, + NSVG_UNITS_MM, + NSVG_UNITS_CM, + NSVG_UNITS_IN, + NSVG_UNITS_PERCENT, + NSVG_UNITS_EM, + NSVG_UNITS_EX +}; + +typedef struct NSVGcoordinate { + float value; + int units; +} NSVGcoordinate; + +typedef struct NSVGlinearData { + NSVGcoordinate x1, y1, x2, y2; +} NSVGlinearData; + +typedef struct NSVGradialData { + NSVGcoordinate cx, cy, r, fx, fy; +} NSVGradialData; + +typedef struct NSVGgradientData +{ + char id[64]; + char ref[64]; + signed char type; + union { + NSVGlinearData linear; + NSVGradialData radial; + }; + char spread; + char units; + float xform[6]; + int nstops; + NSVGgradientStop* stops; + struct NSVGgradientData* next; +} NSVGgradientData; + +typedef struct NSVGattrib +{ + char id[64]; + float xform[6]; + unsigned int fillColor; + unsigned int strokeColor; + float opacity; + float fillOpacity; + float strokeOpacity; + char fillGradient[64]; + char strokeGradient[64]; + float strokeWidth; + float strokeDashOffset; + float strokeDashArray[NSVG_MAX_DASHES]; + int strokeDashCount; + char strokeLineJoin; + char strokeLineCap; + float miterLimit; + char fillRule; + float fontSize; + unsigned int stopColor; + float stopOpacity; + float stopOffset; + char hasFill; + char hasStroke; + char visible; + unsigned char paintOrder; +} NSVGattrib; + +typedef struct NSVGparser +{ + NSVGattrib attr[NSVG_MAX_ATTR]; + int attrHead; + float* pts; + int npts; + int cpts; + NSVGpath* plist; + NSVGimage* image; + NSVGgradientData* gradients; + NSVGshape* shapesTail; + float viewMinx, viewMiny, viewWidth, viewHeight; + int alignX, alignY, alignType; + float dpi; + char pathFlag; + char defsFlag; +} NSVGparser; + +static void nsvg__xformIdentity(float* t) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetTranslation(float* t, float tx, float ty) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = tx; t[5] = ty; +} + +static void nsvg__xformSetScale(float* t, float sx, float sy) +{ + t[0] = sx; t[1] = 0.0f; + t[2] = 0.0f; t[3] = sy; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewX(float* t, float a) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = tanf(a); t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewY(float* t, float a) +{ + t[0] = 1.0f; t[1] = tanf(a); + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetRotation(float* t, float a) +{ + float cs = cosf(a), sn = sinf(a); + t[0] = cs; t[1] = sn; + t[2] = -sn; t[3] = cs; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformMultiply(float* t, float* s) +{ + float t0 = t[0] * s[0] + t[1] * s[2]; + float t2 = t[2] * s[0] + t[3] * s[2]; + float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; + t[1] = t[0] * s[1] + t[1] * s[3]; + t[3] = t[2] * s[1] + t[3] * s[3]; + t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; + t[0] = t0; + t[2] = t2; + t[4] = t4; +} + +static void nsvg__xformInverse(float* inv, float* t) +{ + double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; + if (det > -1e-6 && det < 1e-6) { + nsvg__xformIdentity(t); + return; + } + invdet = 1.0 / det; + inv[0] = (float)(t[3] * invdet); + inv[2] = (float)(-t[2] * invdet); + inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); + inv[1] = (float)(-t[1] * invdet); + inv[3] = (float)(t[0] * invdet); + inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); +} + +static void nsvg__xformPremultiply(float* t, float* s) +{ + float s2[6]; + memcpy(s2, s, sizeof(float)*6); + nsvg__xformMultiply(s2, t); + memcpy(t, s2, sizeof(float)*6); +} + +static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2] + t[4]; + *dy = x*t[1] + y*t[3] + t[5]; +} + +static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2]; + *dy = x*t[1] + y*t[3]; +} + +#define NSVG_EPSILON (1e-12) + +static int nsvg__ptInBounds(float* pt, float* bounds) +{ + return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; +} + + +static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) +{ + double it = 1.0-t; + return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; +} + +static void nsvg__curveBounds(float* bounds, float* curve) +{ + int i, j, count; + double roots[2], a, b, c, b2ac, t, v; + float* v0 = &curve[0]; + float* v1 = &curve[2]; + float* v2 = &curve[4]; + float* v3 = &curve[6]; + + // Start the bounding box by end points + bounds[0] = nsvg__minf(v0[0], v3[0]); + bounds[1] = nsvg__minf(v0[1], v3[1]); + bounds[2] = nsvg__maxf(v0[0], v3[0]); + bounds[3] = nsvg__maxf(v0[1], v3[1]); + + // Bezier curve fits inside the convex hull of it's control points. + // If control points are inside the bounds, we're done. + if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) + return; + + // Add bezier curve inflection points in X and Y. + for (i = 0; i < 2; i++) { + a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; + b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; + c = 3.0 * v1[i] - 3.0 * v0[i]; + count = 0; + if (fabs(a) < NSVG_EPSILON) { + if (fabs(b) > NSVG_EPSILON) { + t = -c / b; + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } else { + b2ac = b*b - 4.0*c*a; + if (b2ac > NSVG_EPSILON) { + t = (-b + sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + t = (-b - sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } + for (j = 0; j < count; j++) { + v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); + bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); + bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); + } + } +} + +static unsigned char nsvg__encodePaintOrder(enum NSVGpaintOrder a, enum NSVGpaintOrder b, enum NSVGpaintOrder c) { + return (a & 0x03) | ((b & 0x03) << 2) | ((c & 0x03) << 4); +} + +static NSVGparser* nsvg__createParser(void) +{ + NSVGparser* p; + p = (NSVGparser*)malloc(sizeof(NSVGparser)); + if (p == NULL) goto error; + memset(p, 0, sizeof(NSVGparser)); + + p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); + if (p->image == NULL) goto error; + memset(p->image, 0, sizeof(NSVGimage)); + + // Init style + nsvg__xformIdentity(p->attr[0].xform); + memset(p->attr[0].id, 0, sizeof p->attr[0].id); + p->attr[0].fillColor = NSVG_RGB(0,0,0); + p->attr[0].strokeColor = NSVG_RGB(0,0,0); + p->attr[0].opacity = 1; + p->attr[0].fillOpacity = 1; + p->attr[0].strokeOpacity = 1; + p->attr[0].stopOpacity = 1; + p->attr[0].strokeWidth = 1; + p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; + p->attr[0].strokeLineCap = NSVG_CAP_BUTT; + p->attr[0].miterLimit = 4; + p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; + p->attr[0].hasFill = 1; + p->attr[0].visible = 1; + p->attr[0].paintOrder = nsvg__encodePaintOrder(NSVG_PAINT_FILL, NSVG_PAINT_STROKE, NSVG_PAINT_MARKERS); + + return p; + +error: + if (p) { + if (p->image) free(p->image); + free(p); + } + return NULL; +} + +static void nsvg__deletePaths(NSVGpath* path) +{ + while (path) { + NSVGpath *next = path->next; + if (path->pts != NULL) + free(path->pts); + free(path); + path = next; + } +} + +static void nsvg__deletePaint(NSVGpaint* paint) +{ + if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) + free(paint->gradient); +} + +static void nsvg__deleteGradientData(NSVGgradientData* grad) +{ + NSVGgradientData* next; + while (grad != NULL) { + next = grad->next; + free(grad->stops); + free(grad); + grad = next; + } +} + +static void nsvg__deleteParser(NSVGparser* p) +{ + if (p != NULL) { + nsvg__deletePaths(p->plist); + nsvg__deleteGradientData(p->gradients); + nsvgDelete(p->image); + free(p->pts); + free(p); + } +} + +static void nsvg__resetPath(NSVGparser* p) +{ + p->npts = 0; +} + +static void nsvg__addPoint(NSVGparser* p, float x, float y) +{ + if (p->npts+1 > p->cpts) { + p->cpts = p->cpts ? p->cpts*2 : 8; + p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); + if (!p->pts) return; + } + p->pts[p->npts*2+0] = x; + p->pts[p->npts*2+1] = y; + p->npts++; +} + +static void nsvg__moveTo(NSVGparser* p, float x, float y) +{ + if (p->npts > 0) { + p->pts[(p->npts-1)*2+0] = x; + p->pts[(p->npts-1)*2+1] = y; + } else { + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__lineTo(NSVGparser* p, float x, float y) +{ + float px,py, dx,dy; + if (p->npts > 0) { + px = p->pts[(p->npts-1)*2+0]; + py = p->pts[(p->npts-1)*2+1]; + dx = x - px; + dy = y - py; + nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); + nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) +{ + if (p->npts > 0) { + nsvg__addPoint(p, cpx1, cpy1); + nsvg__addPoint(p, cpx2, cpy2); + nsvg__addPoint(p, x, y); + } +} + +static NSVGattrib* nsvg__getAttr(NSVGparser* p) +{ + return &p->attr[p->attrHead]; +} + +static void nsvg__pushAttr(NSVGparser* p) +{ + if (p->attrHead < NSVG_MAX_ATTR-1) { + p->attrHead++; + memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); + } +} + +static void nsvg__popAttr(NSVGparser* p) +{ + if (p->attrHead > 0) + p->attrHead--; +} + +static float nsvg__actualOrigX(NSVGparser* p) +{ + return p->viewMinx; +} + +static float nsvg__actualOrigY(NSVGparser* p) +{ + return p->viewMiny; +} + +static float nsvg__actualWidth(NSVGparser* p) +{ + return p->viewWidth; +} + +static float nsvg__actualHeight(NSVGparser* p) +{ + return p->viewHeight; +} + +static float nsvg__actualLength(NSVGparser* p) +{ + float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); + return sqrtf(w*w + h*h) / sqrtf(2.0f); +} + +static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) +{ + NSVGattrib* attr = nsvg__getAttr(p); + switch (c.units) { + case NSVG_UNITS_USER: return c.value; + case NSVG_UNITS_PX: return c.value; + case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; + case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; + case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; + case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; + case NSVG_UNITS_IN: return c.value * p->dpi; + case NSVG_UNITS_EM: return c.value * attr->fontSize; + case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. + case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; + default: return c.value; + } + return c.value; +} + +static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) +{ + NSVGgradientData* grad = p->gradients; + if (id == NULL || *id == '\0') + return NULL; + while (grad != NULL) { + if (strcmp(grad->id, id) == 0) + return grad; + grad = grad->next; + } + return NULL; +} + +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) +{ + NSVGgradientData* data = NULL; + NSVGgradientData* ref = NULL; + NSVGgradientStop* stops = NULL; + NSVGgradient* grad; + float ox, oy, sw, sh, sl; + int nstops = 0; + int refIter; + + data = nsvg__findGradientData(p, id); + if (data == NULL) return NULL; + + // TODO: use ref to fill in all unset values too. + ref = data; + refIter = 0; + while (ref != NULL) { + NSVGgradientData* nextRef = NULL; + if (stops == NULL && ref->stops != NULL) { + stops = ref->stops; + nstops = ref->nstops; + break; + } + nextRef = nsvg__findGradientData(p, ref->ref); + if (nextRef == ref) break; // prevent infite loops on malformed data + ref = nextRef; + refIter++; + if (refIter > 32) break; // prevent infite loops on malformed data + } + if (stops == NULL) return NULL; + + grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); + if (grad == NULL) return NULL; + + // The shape width and height. + if (data->units == NSVG_OBJECT_SPACE) { + ox = localBounds[0]; + oy = localBounds[1]; + sw = localBounds[2] - localBounds[0]; + sh = localBounds[3] - localBounds[1]; + } else { + ox = nsvg__actualOrigX(p); + oy = nsvg__actualOrigY(p); + sw = nsvg__actualWidth(p); + sh = nsvg__actualHeight(p); + } + sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); + + if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { + float x1, y1, x2, y2, dx, dy; + x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); + y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); + x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); + y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); + // Calculate transform aligned to the line + dx = x2 - x1; + dy = y2 - y1; + grad->xform[0] = dy; grad->xform[1] = -dx; + grad->xform[2] = dx; grad->xform[3] = dy; + grad->xform[4] = x1; grad->xform[5] = y1; + } else { + float cx, cy, fx, fy, r; + cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); + cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); + fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); + fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); + r = nsvg__convertToPixels(p, data->radial.r, 0, sl); + // Calculate transform aligned to the circle + grad->xform[0] = r; grad->xform[1] = 0; + grad->xform[2] = 0; grad->xform[3] = r; + grad->xform[4] = cx; grad->xform[5] = cy; + grad->fx = fx / r; + grad->fy = fy / r; + } + + nsvg__xformMultiply(grad->xform, data->xform); + nsvg__xformMultiply(grad->xform, xform); + + grad->spread = data->spread; + memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); + grad->nstops = nstops; + + *paintType = data->type; + + return grad; +} + +static float nsvg__getAverageScale(float* t) +{ + float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); + float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); + return (sx + sy) * 0.5f; +} + +static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) +{ + NSVGpath* path; + float curve[4*2], curveBounds[4]; + int i, first = 1; + for (path = shape->paths; path != NULL; path = path->next) { + nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); + for (i = 0; i < path->npts-1; i += 3) { + nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); + nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); + nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); + nsvg__curveBounds(curveBounds, curve); + if (first) { + bounds[0] = curveBounds[0]; + bounds[1] = curveBounds[1]; + bounds[2] = curveBounds[2]; + bounds[3] = curveBounds[3]; + first = 0; + } else { + bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); + bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); + bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); + bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); + } + curve[0] = curve[6]; + curve[1] = curve[7]; + } + } +} + +static void nsvg__addShape(NSVGparser* p) +{ + NSVGattrib* attr = nsvg__getAttr(p); + float scale = 1.0f; + NSVGshape* shape; + NSVGpath* path; + int i; + + if (p->plist == NULL) + return; + + shape = (NSVGshape*)malloc(sizeof(NSVGshape)); + if (shape == NULL) goto error; + memset(shape, 0, sizeof(NSVGshape)); + + memcpy(shape->id, attr->id, sizeof shape->id); + memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); + memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); + memcpy(shape->xform, attr->xform, sizeof shape->xform); + scale = nsvg__getAverageScale(attr->xform); + shape->strokeWidth = attr->strokeWidth * scale; + shape->strokeDashOffset = attr->strokeDashOffset * scale; + shape->strokeDashCount = (char)attr->strokeDashCount; + for (i = 0; i < attr->strokeDashCount; i++) + shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; + shape->strokeLineJoin = attr->strokeLineJoin; + shape->strokeLineCap = attr->strokeLineCap; + shape->miterLimit = attr->miterLimit; + shape->fillRule = attr->fillRule; + shape->opacity = attr->opacity; + shape->paintOrder = attr->paintOrder; + + shape->paths = p->plist; + p->plist = NULL; + + // Calculate shape bounds + shape->bounds[0] = shape->paths->bounds[0]; + shape->bounds[1] = shape->paths->bounds[1]; + shape->bounds[2] = shape->paths->bounds[2]; + shape->bounds[3] = shape->paths->bounds[3]; + for (path = shape->paths->next; path != NULL; path = path->next) { + shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); + shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); + shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); + shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); + } + + // Set fill + if (attr->hasFill == 0) { + shape->fill.type = NSVG_PAINT_NONE; + } else if (attr->hasFill == 1) { + shape->fill.type = NSVG_PAINT_COLOR; + shape->fill.color = attr->fillColor; + shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; + } else if (attr->hasFill == 2) { + shape->fill.type = NSVG_PAINT_UNDEF; + } + + // Set stroke + if (attr->hasStroke == 0) { + shape->stroke.type = NSVG_PAINT_NONE; + } else if (attr->hasStroke == 1) { + shape->stroke.type = NSVG_PAINT_COLOR; + shape->stroke.color = attr->strokeColor; + shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; + } else if (attr->hasStroke == 2) { + shape->stroke.type = NSVG_PAINT_UNDEF; + } + + // Set flags + shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); + + // Add to tail + if (p->image->shapes == NULL) + p->image->shapes = shape; + else + p->shapesTail->next = shape; + p->shapesTail = shape; + + return; + +error: + if (shape) free(shape); +} + +static void nsvg__addPath(NSVGparser* p, char closed) +{ + NSVGattrib* attr = nsvg__getAttr(p); + NSVGpath* path = NULL; + float bounds[4]; + float* curve; + int i; + + if (p->npts < 4) + return; + + if (closed) + nsvg__lineTo(p, p->pts[0], p->pts[1]); + + // Expect 1 + N*3 points (N = number of cubic bezier segments). + if ((p->npts % 3) != 1) + return; + + path = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (path == NULL) goto error; + memset(path, 0, sizeof(NSVGpath)); + + path->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (path->pts == NULL) goto error; + path->closed = closed; + path->npts = p->npts; + + // Transform path. + for (i = 0; i < p->npts; ++i) + nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); + + // Find bounds + for (i = 0; i < path->npts-1; i += 3) { + curve = &path->pts[i*2]; + nsvg__curveBounds(bounds, curve); + if (i == 0) { + path->bounds[0] = bounds[0]; + path->bounds[1] = bounds[1]; + path->bounds[2] = bounds[2]; + path->bounds[3] = bounds[3]; + } else { + path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); + path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); + path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); + path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); + } + } + + path->next = p->plist; + p->plist = path; + + return; + +error: + if (path != NULL) { + if (path->pts != NULL) free(path->pts); + free(path); + } +} + +// We roll our own string to float because the std library one uses locale and messes things up. +static double nsvg__atof(const char* s) +{ + char* cur = (char*)s; + char* end = NULL; + double res = 0.0, sign = 1.0; + long long intPart = 0, fracPart = 0; + char hasIntPart = 0, hasFracPart = 0; + + // Parse optional sign + if (*cur == '+') { + cur++; + } else if (*cur == '-') { + sign = -1; + cur++; + } + + // Parse integer part + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + intPart = strtoll(cur, &end, 10); + if (cur != end) { + res = (double)intPart; + hasIntPart = 1; + cur = end; + } + } + + // Parse fractional part. + if (*cur == '.') { + cur++; // Skip '.' + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + fracPart = strtoll(cur, &end, 10); + if (cur != end) { + res += (double)fracPart / pow(10.0, (double)(end - cur)); + hasFracPart = 1; + cur = end; + } + } + } + + // A valid number should have integer or fractional part. + if (!hasIntPart && !hasFracPart) + return 0.0; + + // Parse optional exponent + if (*cur == 'e' || *cur == 'E') { + long expPart = 0; + cur++; // skip 'E' + expPart = strtol(cur, &end, 10); // Parse digit sequence with sign + if (cur != end) { + res *= pow(10.0, (double)expPart); + } + } + + return res * sign; +} + + +static const char* nsvg__parseNumber(const char* s, char* it, const int size) +{ + const int last = size-1; + int i = 0; + + // sign + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + // integer part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + if (*s == '.') { + // decimal point + if (i < last) it[i++] = *s; + s++; + // fraction part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + // exponent + if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { + if (i < last) it[i++] = *s; + s++; + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + it[i] = '\0'; + + return s; +} + +static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) +{ + it[0] = '\0'; + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '0' || *s == '1') { + it[0] = *s++; + it[1] = '\0'; + return s; + } + return s; +} + +static const char* nsvg__getNextPathItem(const char* s, char* it) +{ + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { + s = nsvg__parseNumber(s, it, 64); + } else { + // Parse command + it[0] = *s++; + it[1] = '\0'; + return s; + } + + return s; +} + +static unsigned int nsvg__parseColorHex(const char* str) +{ + unsigned int r=0, g=0, b=0; + if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex + return NSVG_RGB(r, g, b); + if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa + return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. + return NSVG_RGB(128, 128, 128); +} + +// Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). +// This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors +// for backwards compatibility. Note: other image viewers return black instead. + +static unsigned int nsvg__parseColorRGB(const char* str) +{ + int i; + unsigned int rgbi[3]; + float rgbf[3]; + // try decimal integers first + if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { + // integers failed, try percent values (float, locale independent) + const char delimiter[3] = {',', ',', ')'}; + str += 4; // skip "rgb(" + for (i = 0; i < 3; i++) { + while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces + if (*str == '+') str++; // skip '+' (don't allow '-') + if (!*str) break; + rgbf[i] = nsvg__atof(str); + + // Note 1: it would be great if nsvg__atof() returned how many + // bytes it consumed but it doesn't. We need to skip the number, + // the '%' character, spaces, and the delimiter ',' or ')'. + + // Note 2: The following code does not allow values like "33.%", + // i.e. a decimal point w/o fractional part, but this is consistent + // with other image viewers, e.g. firefox, chrome, eog, gimp. + + while (*str && nsvg__isdigit(*str)) str++; // skip integer part + if (*str == '.') { + str++; + if (!nsvg__isdigit(*str)) break; // error: no digit after '.' + while (*str && nsvg__isdigit(*str)) str++; // skip fractional part + } + if (*str == '%') str++; else break; + while (*str && nsvg__isspace(*str)) str++; + if (*str == delimiter[i]) str++; + else break; + } + if (i == 3) { + rgbi[0] = roundf(rgbf[0] * 2.55f); + rgbi[1] = roundf(rgbf[1] * 2.55f); + rgbi[2] = roundf(rgbf[2] * 2.55f); + } else { + rgbi[0] = rgbi[1] = rgbi[2] = 128; + } + } + // clip values as the CSS spec requires + for (i = 0; i < 3; i++) { + if (rgbi[i] > 255) rgbi[i] = 255; + } + return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); +} + +typedef struct NSVGNamedColor { + const char* name; + unsigned int color; +} NSVGNamedColor; + +NSVGNamedColor nsvg__colors[] = { + + { "red", NSVG_RGB(255, 0, 0) }, + { "green", NSVG_RGB( 0, 128, 0) }, + { "blue", NSVG_RGB( 0, 0, 255) }, + { "yellow", NSVG_RGB(255, 255, 0) }, + { "cyan", NSVG_RGB( 0, 255, 255) }, + { "magenta", NSVG_RGB(255, 0, 255) }, + { "black", NSVG_RGB( 0, 0, 0) }, + { "grey", NSVG_RGB(128, 128, 128) }, + { "gray", NSVG_RGB(128, 128, 128) }, + { "white", NSVG_RGB(255, 255, 255) }, + +#ifdef NANOSVG_ALL_COLOR_KEYWORDS + { "aliceblue", NSVG_RGB(240, 248, 255) }, + { "antiquewhite", NSVG_RGB(250, 235, 215) }, + { "aqua", NSVG_RGB( 0, 255, 255) }, + { "aquamarine", NSVG_RGB(127, 255, 212) }, + { "azure", NSVG_RGB(240, 255, 255) }, + { "beige", NSVG_RGB(245, 245, 220) }, + { "bisque", NSVG_RGB(255, 228, 196) }, + { "blanchedalmond", NSVG_RGB(255, 235, 205) }, + { "blueviolet", NSVG_RGB(138, 43, 226) }, + { "brown", NSVG_RGB(165, 42, 42) }, + { "burlywood", NSVG_RGB(222, 184, 135) }, + { "cadetblue", NSVG_RGB( 95, 158, 160) }, + { "chartreuse", NSVG_RGB(127, 255, 0) }, + { "chocolate", NSVG_RGB(210, 105, 30) }, + { "coral", NSVG_RGB(255, 127, 80) }, + { "cornflowerblue", NSVG_RGB(100, 149, 237) }, + { "cornsilk", NSVG_RGB(255, 248, 220) }, + { "crimson", NSVG_RGB(220, 20, 60) }, + { "darkblue", NSVG_RGB( 0, 0, 139) }, + { "darkcyan", NSVG_RGB( 0, 139, 139) }, + { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, + { "darkgray", NSVG_RGB(169, 169, 169) }, + { "darkgreen", NSVG_RGB( 0, 100, 0) }, + { "darkgrey", NSVG_RGB(169, 169, 169) }, + { "darkkhaki", NSVG_RGB(189, 183, 107) }, + { "darkmagenta", NSVG_RGB(139, 0, 139) }, + { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, + { "darkorange", NSVG_RGB(255, 140, 0) }, + { "darkorchid", NSVG_RGB(153, 50, 204) }, + { "darkred", NSVG_RGB(139, 0, 0) }, + { "darksalmon", NSVG_RGB(233, 150, 122) }, + { "darkseagreen", NSVG_RGB(143, 188, 143) }, + { "darkslateblue", NSVG_RGB( 72, 61, 139) }, + { "darkslategray", NSVG_RGB( 47, 79, 79) }, + { "darkslategrey", NSVG_RGB( 47, 79, 79) }, + { "darkturquoise", NSVG_RGB( 0, 206, 209) }, + { "darkviolet", NSVG_RGB(148, 0, 211) }, + { "deeppink", NSVG_RGB(255, 20, 147) }, + { "deepskyblue", NSVG_RGB( 0, 191, 255) }, + { "dimgray", NSVG_RGB(105, 105, 105) }, + { "dimgrey", NSVG_RGB(105, 105, 105) }, + { "dodgerblue", NSVG_RGB( 30, 144, 255) }, + { "firebrick", NSVG_RGB(178, 34, 34) }, + { "floralwhite", NSVG_RGB(255, 250, 240) }, + { "forestgreen", NSVG_RGB( 34, 139, 34) }, + { "fuchsia", NSVG_RGB(255, 0, 255) }, + { "gainsboro", NSVG_RGB(220, 220, 220) }, + { "ghostwhite", NSVG_RGB(248, 248, 255) }, + { "gold", NSVG_RGB(255, 215, 0) }, + { "goldenrod", NSVG_RGB(218, 165, 32) }, + { "greenyellow", NSVG_RGB(173, 255, 47) }, + { "honeydew", NSVG_RGB(240, 255, 240) }, + { "hotpink", NSVG_RGB(255, 105, 180) }, + { "indianred", NSVG_RGB(205, 92, 92) }, + { "indigo", NSVG_RGB( 75, 0, 130) }, + { "ivory", NSVG_RGB(255, 255, 240) }, + { "khaki", NSVG_RGB(240, 230, 140) }, + { "lavender", NSVG_RGB(230, 230, 250) }, + { "lavenderblush", NSVG_RGB(255, 240, 245) }, + { "lawngreen", NSVG_RGB(124, 252, 0) }, + { "lemonchiffon", NSVG_RGB(255, 250, 205) }, + { "lightblue", NSVG_RGB(173, 216, 230) }, + { "lightcoral", NSVG_RGB(240, 128, 128) }, + { "lightcyan", NSVG_RGB(224, 255, 255) }, + { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, + { "lightgray", NSVG_RGB(211, 211, 211) }, + { "lightgreen", NSVG_RGB(144, 238, 144) }, + { "lightgrey", NSVG_RGB(211, 211, 211) }, + { "lightpink", NSVG_RGB(255, 182, 193) }, + { "lightsalmon", NSVG_RGB(255, 160, 122) }, + { "lightseagreen", NSVG_RGB( 32, 178, 170) }, + { "lightskyblue", NSVG_RGB(135, 206, 250) }, + { "lightslategray", NSVG_RGB(119, 136, 153) }, + { "lightslategrey", NSVG_RGB(119, 136, 153) }, + { "lightsteelblue", NSVG_RGB(176, 196, 222) }, + { "lightyellow", NSVG_RGB(255, 255, 224) }, + { "lime", NSVG_RGB( 0, 255, 0) }, + { "limegreen", NSVG_RGB( 50, 205, 50) }, + { "linen", NSVG_RGB(250, 240, 230) }, + { "maroon", NSVG_RGB(128, 0, 0) }, + { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, + { "mediumblue", NSVG_RGB( 0, 0, 205) }, + { "mediumorchid", NSVG_RGB(186, 85, 211) }, + { "mediumpurple", NSVG_RGB(147, 112, 219) }, + { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, + { "mediumslateblue", NSVG_RGB(123, 104, 238) }, + { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, + { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, + { "mediumvioletred", NSVG_RGB(199, 21, 133) }, + { "midnightblue", NSVG_RGB( 25, 25, 112) }, + { "mintcream", NSVG_RGB(245, 255, 250) }, + { "mistyrose", NSVG_RGB(255, 228, 225) }, + { "moccasin", NSVG_RGB(255, 228, 181) }, + { "navajowhite", NSVG_RGB(255, 222, 173) }, + { "navy", NSVG_RGB( 0, 0, 128) }, + { "oldlace", NSVG_RGB(253, 245, 230) }, + { "olive", NSVG_RGB(128, 128, 0) }, + { "olivedrab", NSVG_RGB(107, 142, 35) }, + { "orange", NSVG_RGB(255, 165, 0) }, + { "orangered", NSVG_RGB(255, 69, 0) }, + { "orchid", NSVG_RGB(218, 112, 214) }, + { "palegoldenrod", NSVG_RGB(238, 232, 170) }, + { "palegreen", NSVG_RGB(152, 251, 152) }, + { "paleturquoise", NSVG_RGB(175, 238, 238) }, + { "palevioletred", NSVG_RGB(219, 112, 147) }, + { "papayawhip", NSVG_RGB(255, 239, 213) }, + { "peachpuff", NSVG_RGB(255, 218, 185) }, + { "peru", NSVG_RGB(205, 133, 63) }, + { "pink", NSVG_RGB(255, 192, 203) }, + { "plum", NSVG_RGB(221, 160, 221) }, + { "powderblue", NSVG_RGB(176, 224, 230) }, + { "purple", NSVG_RGB(128, 0, 128) }, + { "rosybrown", NSVG_RGB(188, 143, 143) }, + { "royalblue", NSVG_RGB( 65, 105, 225) }, + { "saddlebrown", NSVG_RGB(139, 69, 19) }, + { "salmon", NSVG_RGB(250, 128, 114) }, + { "sandybrown", NSVG_RGB(244, 164, 96) }, + { "seagreen", NSVG_RGB( 46, 139, 87) }, + { "seashell", NSVG_RGB(255, 245, 238) }, + { "sienna", NSVG_RGB(160, 82, 45) }, + { "silver", NSVG_RGB(192, 192, 192) }, + { "skyblue", NSVG_RGB(135, 206, 235) }, + { "slateblue", NSVG_RGB(106, 90, 205) }, + { "slategray", NSVG_RGB(112, 128, 144) }, + { "slategrey", NSVG_RGB(112, 128, 144) }, + { "snow", NSVG_RGB(255, 250, 250) }, + { "springgreen", NSVG_RGB( 0, 255, 127) }, + { "steelblue", NSVG_RGB( 70, 130, 180) }, + { "tan", NSVG_RGB(210, 180, 140) }, + { "teal", NSVG_RGB( 0, 128, 128) }, + { "thistle", NSVG_RGB(216, 191, 216) }, + { "tomato", NSVG_RGB(255, 99, 71) }, + { "turquoise", NSVG_RGB( 64, 224, 208) }, + { "violet", NSVG_RGB(238, 130, 238) }, + { "wheat", NSVG_RGB(245, 222, 179) }, + { "whitesmoke", NSVG_RGB(245, 245, 245) }, + { "yellowgreen", NSVG_RGB(154, 205, 50) }, +#endif +}; + +static unsigned int nsvg__parseColorName(const char* str) +{ + int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); + + for (i = 0; i < ncolors; i++) { + if (strcmp(nsvg__colors[i].name, str) == 0) { + return nsvg__colors[i].color; + } + } + + return NSVG_RGB(128, 128, 128); +} + +static unsigned int nsvg__parseColor(const char* str) +{ + size_t len = 0; + while(*str == ' ') ++str; + len = strlen(str); + if (len >= 1 && *str == '#') + return nsvg__parseColorHex(str); + else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') + return nsvg__parseColorRGB(str); + return nsvg__parseColorName(str); +} + +static float nsvg__parseOpacity(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + return val; +} + +static float nsvg__parseMiterLimit(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + return val; +} + +static int nsvg__parseUnits(const char* units) +{ + if (units[0] == 'p' && units[1] == 'x') + return NSVG_UNITS_PX; + else if (units[0] == 'p' && units[1] == 't') + return NSVG_UNITS_PT; + else if (units[0] == 'p' && units[1] == 'c') + return NSVG_UNITS_PC; + else if (units[0] == 'm' && units[1] == 'm') + return NSVG_UNITS_MM; + else if (units[0] == 'c' && units[1] == 'm') + return NSVG_UNITS_CM; + else if (units[0] == 'i' && units[1] == 'n') + return NSVG_UNITS_IN; + else if (units[0] == '%') + return NSVG_UNITS_PERCENT; + else if (units[0] == 'e' && units[1] == 'm') + return NSVG_UNITS_EM; + else if (units[0] == 'e' && units[1] == 'x') + return NSVG_UNITS_EX; + return NSVG_UNITS_USER; +} + +static int nsvg__isCoordinate(const char* s) +{ + // optional sign + if (*s == '-' || *s == '+') + s++; + // must have at least one digit, or start by a dot + return (nsvg__isdigit(*s) || *s == '.'); +} + +static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) +{ + NSVGcoordinate coord = {0, NSVG_UNITS_USER}; + char buf[64]; + coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); + coord.value = nsvg__atof(buf); + return coord; +} + +static NSVGcoordinate nsvg__coord(float v, int units) +{ + NSVGcoordinate coord = {v, units}; + return coord; +} + +static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) +{ + NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); + return nsvg__convertToPixels(p, coord, orig, length); +} + +static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) +{ + const char* end; + const char* ptr; + char it[64]; + + *na = 0; + ptr = str; + while (*ptr && *ptr != '(') ++ptr; + if (*ptr == 0) + return 1; + end = ptr; + while (*end && *end != ')') ++end; + if (*end == 0) + return 1; + + while (ptr < end) { + if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { + if (*na >= maxNa) return 0; + ptr = nsvg__parseNumber(ptr, it, 64); + args[(*na)++] = (float)nsvg__atof(it); + } else { + ++ptr; + } + } + return (int)(end - str); +} + + +static int nsvg__parseMatrix(float* xform, const char* str) +{ + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, t, 6, &na); + if (na != 6) return len; + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseTranslate(float* xform, const char* str) +{ + float args[2]; + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = 0.0; + + nsvg__xformSetTranslation(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseScale(float* xform, const char* str) +{ + float args[2]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = args[0]; + nsvg__xformSetScale(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewX(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewY(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseRotate(float* xform, const char* str) +{ + float args[3]; + int na = 0; + float m[6]; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 3, &na); + if (na == 1) + args[1] = args[2] = 0.0f; + nsvg__xformIdentity(m); + + if (na > 1) { + nsvg__xformSetTranslation(t, -args[1], -args[2]); + nsvg__xformMultiply(m, t); + } + + nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); + nsvg__xformMultiply(m, t); + + if (na > 1) { + nsvg__xformSetTranslation(t, args[1], args[2]); + nsvg__xformMultiply(m, t); + } + + memcpy(xform, m, sizeof(float)*6); + + return len; +} + +static void nsvg__parseTransform(float* xform, const char* str) +{ + float t[6]; + int len; + nsvg__xformIdentity(xform); + while (*str) + { + if (strncmp(str, "matrix", 6) == 0) + len = nsvg__parseMatrix(t, str); + else if (strncmp(str, "translate", 9) == 0) + len = nsvg__parseTranslate(t, str); + else if (strncmp(str, "scale", 5) == 0) + len = nsvg__parseScale(t, str); + else if (strncmp(str, "rotate", 6) == 0) + len = nsvg__parseRotate(t, str); + else if (strncmp(str, "skewX", 5) == 0) + len = nsvg__parseSkewX(t, str); + else if (strncmp(str, "skewY", 5) == 0) + len = nsvg__parseSkewY(t, str); + else{ + ++str; + continue; + } + if (len != 0) { + str += len; + } else { + ++str; + continue; + } + + nsvg__xformPremultiply(xform, t); + } +} + +static void nsvg__parseUrl(char* id, const char* str) +{ + int i = 0; + str += 4; // "url("; + if (*str && *str == '#') + str++; + while (i < 63 && *str && *str != ')') { + id[i] = *str++; + i++; + } + id[i] = '\0'; +} + +static char nsvg__parseLineCap(const char* str) +{ + if (strcmp(str, "butt") == 0) + return NSVG_CAP_BUTT; + else if (strcmp(str, "round") == 0) + return NSVG_CAP_ROUND; + else if (strcmp(str, "square") == 0) + return NSVG_CAP_SQUARE; + // TODO: handle inherit. + return NSVG_CAP_BUTT; +} + +static char nsvg__parseLineJoin(const char* str) +{ + if (strcmp(str, "miter") == 0) + return NSVG_JOIN_MITER; + else if (strcmp(str, "round") == 0) + return NSVG_JOIN_ROUND; + else if (strcmp(str, "bevel") == 0) + return NSVG_JOIN_BEVEL; + // TODO: handle inherit. + return NSVG_JOIN_MITER; +} + +static char nsvg__parseFillRule(const char* str) +{ + if (strcmp(str, "nonzero") == 0) + return NSVG_FILLRULE_NONZERO; + else if (strcmp(str, "evenodd") == 0) + return NSVG_FILLRULE_EVENODD; + // TODO: handle inherit. + return NSVG_FILLRULE_NONZERO; +} + +static unsigned char nsvg__parsePaintOrder(const char* str) +{ + if (strcmp(str, "normal") == 0 || strcmp(str, "fill stroke markers") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_FILL, NSVG_PAINT_STROKE, NSVG_PAINT_MARKERS); + else if (strcmp(str, "fill markers stroke") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_FILL, NSVG_PAINT_MARKERS, NSVG_PAINT_STROKE); + else if (strcmp(str, "markers fill stroke") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_MARKERS, NSVG_PAINT_FILL, NSVG_PAINT_STROKE); + else if (strcmp(str, "markers stroke fill") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_MARKERS, NSVG_PAINT_STROKE, NSVG_PAINT_FILL); + else if (strcmp(str, "stroke fill markers") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_STROKE, NSVG_PAINT_FILL, NSVG_PAINT_MARKERS); + else if (strcmp(str, "stroke markers fill") == 0) + return nsvg__encodePaintOrder(NSVG_PAINT_STROKE, NSVG_PAINT_MARKERS, NSVG_PAINT_FILL); + // TODO: handle inherit. + return nsvg__encodePaintOrder(NSVG_PAINT_FILL, NSVG_PAINT_STROKE, NSVG_PAINT_MARKERS); +} + +static const char* nsvg__getNextDashItem(const char* s, char* it) +{ + int n = 0; + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + // Advance until whitespace, comma or end. + while (*s && (!nsvg__isspace(*s) && *s != ',')) { + if (n < 63) + it[n++] = *s; + s++; + } + it[n++] = '\0'; + return s; +} + +static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) +{ + char item[64]; + int count = 0, i; + float sum = 0.0f; + + // Handle "none" + if (str[0] == 'n') + return 0; + + // Parse dashes + while (*str) { + str = nsvg__getNextDashItem(str, item); + if (!*item) break; + if (count < NSVG_MAX_DASHES) + strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); + } + + for (i = 0; i < count; i++) + sum += strokeDashArray[i]; + if (sum <= 1e-6f) + count = 0; + + return count; +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str); + +static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) +{ + float xform[6]; + NSVGattrib* attr = nsvg__getAttr(p); + if (!attr) return 0; + + if (strcmp(name, "style") == 0) { + nsvg__parseStyle(p, value); + } else if (strcmp(name, "display") == 0) { + if (strcmp(value, "none") == 0) + attr->visible = 0; + // Don't reset ->visible on display:inline, one display:none hides the whole subtree + + } else if (strcmp(name, "fill") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasFill = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasFill = 2; + nsvg__parseUrl(attr->fillGradient, value); + } else { + attr->hasFill = 1; + attr->fillColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "opacity") == 0) { + attr->opacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "fill-opacity") == 0) { + attr->fillOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasStroke = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasStroke = 2; + nsvg__parseUrl(attr->strokeGradient, value); + } else { + attr->hasStroke = 1; + attr->strokeColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "stroke-width") == 0) { + attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-dasharray") == 0) { + attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); + } else if (strcmp(name, "stroke-dashoffset") == 0) { + attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-opacity") == 0) { + attr->strokeOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke-linecap") == 0) { + attr->strokeLineCap = nsvg__parseLineCap(value); + } else if (strcmp(name, "stroke-linejoin") == 0) { + attr->strokeLineJoin = nsvg__parseLineJoin(value); + } else if (strcmp(name, "stroke-miterlimit") == 0) { + attr->miterLimit = nsvg__parseMiterLimit(value); + } else if (strcmp(name, "fill-rule") == 0) { + attr->fillRule = nsvg__parseFillRule(value); + } else if (strcmp(name, "font-size") == 0) { + attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "transform") == 0) { + nsvg__parseTransform(xform, value); + nsvg__xformPremultiply(attr->xform, xform); + } else if (strcmp(name, "stop-color") == 0) { + attr->stopColor = nsvg__parseColor(value); + } else if (strcmp(name, "stop-opacity") == 0) { + attr->stopOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "offset") == 0) { + attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); + } else if (strcmp(name, "paint-order") == 0) { + attr->paintOrder = nsvg__parsePaintOrder(value); + } else if (strcmp(name, "id") == 0) { + strncpy(attr->id, value, 63); + attr->id[63] = '\0'; + } else { + return 0; + } + return 1; +} + +static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) +{ + const char* str; + const char* val; + char name[512]; + char value[512]; + int n; + + str = start; + while (str < end && *str != ':') ++str; + + val = str; + + // Right Trim + while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; + ++str; + + n = (int)(str - start); + if (n > 511) n = 511; + if (n) memcpy(name, start, n); + name[n] = 0; + + while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; + + n = (int)(end - val); + if (n > 511) n = 511; + if (n) memcpy(value, val, n); + value[n] = 0; + + return nsvg__parseAttr(p, name, value); +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str) +{ + const char* start; + const char* end; + + while (*str) { + // Left Trim + while(*str && nsvg__isspace(*str)) ++str; + start = str; + while(*str && *str != ';') ++str; + end = str; + + // Right Trim + while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; + ++end; + + nsvg__parseNameValue(p, start, end); + if (*str) ++str; + } +} + +static void nsvg__parseAttribs(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) + { + if (strcmp(attr[i], "style") == 0) + nsvg__parseStyle(p, attr[i + 1]); + else + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } +} + +static int nsvg__getArgsPerElement(char cmd) +{ + switch (cmd) { + case 'v': + case 'V': + case 'h': + case 'H': + return 1; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + return 2; + case 'q': + case 'Q': + case 's': + case 'S': + return 4; + case 'c': + case 'C': + return 6; + case 'a': + case 'A': + return 7; + case 'z': + case 'Z': + return 0; + } + return -1; +} + +static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__moveTo(p, *cpx, *cpy); +} + +static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpx += args[0]; + else + *cpx = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpy += args[0]; + else + *cpy = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x2, y2, cx1, cy1, cx2, cy2; + + if (rel) { + cx1 = *cpx + args[0]; + cy1 = *cpy + args[1]; + cx2 = *cpx + args[2]; + cy2 = *cpy + args[3]; + x2 = *cpx + args[4]; + y2 = *cpy + args[5]; + } else { + cx1 = args[0]; + cy1 = args[1]; + cx2 = args[2]; + cy2 = args[3]; + x2 = args[4]; + y2 = args[5]; + } + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx2 = *cpx + args[0]; + cy2 = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx2 = args[0]; + cy2 = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + cx1 = 2*x1 - *cpx2; + cy1 = 2*y1 - *cpy2; + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx = *cpx + args[0]; + cy = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx = args[0]; + cy = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + // Convert to cubic bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + x2 = *cpx + args[0]; + y2 = *cpy + args[1]; + } else { + x2 = args[0]; + y2 = args[1]; + } + + cx = 2*x1 - *cpx2; + cy = 2*y1 - *cpy2; + + // Convert to cubix bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static float nsvg__sqr(float x) { return x*x; } +static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } + +static float nsvg__vecrat(float ux, float uy, float vx, float vy) +{ + return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); +} + +static float nsvg__vecang(float ux, float uy, float vx, float vy) +{ + float r = nsvg__vecrat(ux,uy, vx,vy); + if (r < -1.0f) r = -1.0f; + if (r > 1.0f) r = 1.0f; + return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); +} + +static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + // Ported from canvg (https://code.google.com/p/canvg/) + float rx, ry, rotx; + float x1, y1, x2, y2, cx, cy, dx, dy, d; + float x1p, y1p, cxp, cyp, s, sa, sb; + float ux, uy, vx, vy, a1, da; + float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; + float sinrx, cosrx; + int fa, fs; + int i, ndivs; + float hda, kappa; + + rx = fabsf(args[0]); // y radius + ry = fabsf(args[1]); // x radius + rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle + fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc + fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction + x1 = *cpx; // start point + y1 = *cpy; + if (rel) { // end point + x2 = *cpx + args[5]; + y2 = *cpy + args[6]; + } else { + x2 = args[5]; + y2 = args[6]; + } + + dx = x1 - x2; + dy = y1 - y2; + d = sqrtf(dx*dx + dy*dy); + if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { + // The arc degenerates to a line + nsvg__lineTo(p, x2, y2); + *cpx = x2; + *cpy = y2; + return; + } + + sinrx = sinf(rotx); + cosrx = cosf(rotx); + + // Convert to center point parameterization. + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // 1) Compute x1', y1' + x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; + y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; + d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); + if (d > 1) { + d = sqrtf(d); + rx *= d; + ry *= d; + } + // 2) Compute cx', cy' + s = 0.0f; + sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); + sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); + if (sa < 0.0f) sa = 0.0f; + if (sb > 0.0f) + s = sqrtf(sa / sb); + if (fa == fs) + s = -s; + cxp = s * rx * y1p / ry; + cyp = s * -ry * x1p / rx; + + // 3) Compute cx,cy from cx',cy' + cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; + cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; + + // 4) Calculate theta1, and delta theta. + ux = (x1p - cxp) / rx; + uy = (y1p - cyp) / ry; + vx = (-x1p - cxp) / rx; + vy = (-y1p - cyp) / ry; + a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle + da = nsvg__vecang(ux,uy, vx,vy); // Delta angle + +// if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; +// if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; + + if (fs == 0 && da > 0) + da -= 2 * NSVG_PI; + else if (fs == 1 && da < 0) + da += 2 * NSVG_PI; + + // Approximate the arc using cubic spline segments. + t[0] = cosrx; t[1] = sinrx; + t[2] = -sinrx; t[3] = cosrx; + t[4] = cx; t[5] = cy; + + // Split arc into max 90 degree segments. + // The loop assumes an iteration per end point (including start and end), this +1. + ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); + hda = (da / (float)ndivs) / 2.0f; + // Fix for ticket #179: division by 0: avoid cotangens around 0 (infinite) + if ((hda < 1e-3f) && (hda > -1e-3f)) + hda *= 0.5f; + else + hda = (1.0f - cosf(hda)) / sinf(hda); + kappa = fabsf(4.0f / 3.0f * hda); + if (da < 0.0f) + kappa = -kappa; + + for (i = 0; i <= ndivs; i++) { + a = a1 + da * ((float)i/(float)ndivs); + dx = cosf(a); + dy = sinf(a); + nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position + nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent + if (i > 0) + nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); + px = x; + py = y; + ptanx = tanx; + ptany = tany; + } + + *cpx = x2; + *cpy = y2; +} + +static void nsvg__parsePath(NSVGparser* p, const char** attr) +{ + const char* s = NULL; + char cmd = '\0'; + float args[10]; + int nargs; + int rargs = 0; + char initPoint; + float cpx, cpy, cpx2, cpy2; + const char* tmp[4]; + char closedFlag; + int i; + char item[64]; + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "d") == 0) { + s = attr[i + 1]; + } else { + tmp[0] = attr[i]; + tmp[1] = attr[i + 1]; + tmp[2] = 0; + tmp[3] = 0; + nsvg__parseAttribs(p, tmp); + } + } + + if (s) { + nsvg__resetPath(p); + cpx = 0; cpy = 0; + cpx2 = 0; cpy2 = 0; + initPoint = 0; + closedFlag = 0; + nargs = 0; + + while (*s) { + item[0] = '\0'; + if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) + s = nsvg__getNextPathItemWhenArcFlag(s, item); + if (!*item) + s = nsvg__getNextPathItem(s, item); + if (!*item) break; + if (cmd != '\0' && nsvg__isCoordinate(item)) { + if (nargs < 10) + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= rargs) { + switch (cmd) { + case 'm': + case 'M': + nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); + // Moveto can be followed by multiple coordinate pairs, + // which should be treated as linetos. + cmd = (cmd == 'm') ? 'l' : 'L'; + rargs = nsvg__getArgsPerElement(cmd); + cpx2 = cpx; cpy2 = cpy; + initPoint = 1; + break; + case 'l': + case 'L': + nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'H': + case 'h': + nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'V': + case 'v': + nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'C': + case 'c': + nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); + break; + case 'S': + case 's': + nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); + break; + case 'Q': + case 'q': + nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); + break; + case 'T': + case 't': + nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); + break; + case 'A': + case 'a': + nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + default: + if (nargs >= 2) { + cpx = args[nargs-2]; + cpy = args[nargs-1]; + cpx2 = cpx; cpy2 = cpy; + } + break; + } + nargs = 0; + } + } else { + cmd = item[0]; + if (cmd == 'M' || cmd == 'm') { + // Commit path. + if (p->npts > 0) + nsvg__addPath(p, closedFlag); + // Start new subpath. + nsvg__resetPath(p); + closedFlag = 0; + nargs = 0; + } else if (initPoint == 0) { + // Do not allow other commands until initial point has been set (moveTo called once). + cmd = '\0'; + } + if (cmd == 'Z' || cmd == 'z') { + closedFlag = 1; + // Commit path. + if (p->npts > 0) { + // Move current point to first point + cpx = p->pts[0]; + cpy = p->pts[1]; + cpx2 = cpx; cpy2 = cpy; + nsvg__addPath(p, closedFlag); + } + // Start new subpath. + nsvg__resetPath(p); + nsvg__moveTo(p, cpx, cpy); + closedFlag = 0; + nargs = 0; + } + rargs = nsvg__getArgsPerElement(cmd); + if (rargs == -1) { + // Command not recognized + cmd = '\0'; + rargs = 0; + } + } + } + // Commit path. + if (p->npts) + nsvg__addPath(p, closedFlag); + } + + nsvg__addShape(p); +} + +static void nsvg__parseRect(NSVGparser* p, const char** attr) +{ + float x = 0.0f; + float y = 0.0f; + float w = 0.0f; + float h = 0.0f; + float rx = -1.0f; // marks not set + float ry = -1.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); + if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx < 0.0f && ry > 0.0f) rx = ry; + if (ry < 0.0f && rx > 0.0f) ry = rx; + if (rx < 0.0f) rx = 0.0f; + if (ry < 0.0f) ry = 0.0f; + if (rx > w/2.0f) rx = w/2.0f; + if (ry > h/2.0f) ry = h/2.0f; + + if (w != 0.0f && h != 0.0f) { + nsvg__resetPath(p); + + if (rx < 0.00001f || ry < 0.0001f) { + nsvg__moveTo(p, x, y); + nsvg__lineTo(p, x+w, y); + nsvg__lineTo(p, x+w, y+h); + nsvg__lineTo(p, x, y+h); + } else { + // Rounded rectangle + nsvg__moveTo(p, x+rx, y); + nsvg__lineTo(p, x+w-rx, y); + nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); + nsvg__lineTo(p, x+w, y+h-ry); + nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); + nsvg__lineTo(p, x+rx, y+h); + nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); + nsvg__lineTo(p, x, y+ry); + nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); + } + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseCircle(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float r = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); + } + } + + if (r > 0.0f) { + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+r, cy); + nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); + nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); + nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); + nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseEllipse(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float rx = 0.0f; + float ry = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx > 0.0f && ry > 0.0f) { + + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+rx, cy); + nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); + nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); + nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); + nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseLine(NSVGparser* p, const char** attr) +{ + float x1 = 0.0; + float y1 = 0.0; + float x2 = 0.0; + float y2 = 0.0; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + } + } + + nsvg__resetPath(p); + + nsvg__moveTo(p, x1, y1); + nsvg__lineTo(p, x2, y2); + + nsvg__addPath(p, 0); + + nsvg__addShape(p); +} + +static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) +{ + int i; + const char* s; + float args[2]; + int nargs, npts = 0; + char item[64]; + + nsvg__resetPath(p); + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "points") == 0) { + s = attr[i + 1]; + nargs = 0; + while (*s) { + s = nsvg__getNextPathItem(s, item); + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= 2) { + if (npts == 0) + nsvg__moveTo(p, args[0], args[1]); + else + nsvg__lineTo(p, args[0], args[1]); + nargs = 0; + npts++; + } + } + } + } + } + + nsvg__addPath(p, (char)closeFlag); + + nsvg__addShape(p); +} + +static void nsvg__parseSVG(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "width") == 0) { + p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "height") == 0) { + p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "viewBox") == 0) { + const char *s = attr[i + 1]; + char buf[64]; + s = nsvg__parseNumber(s, buf, 64); + p->viewMinx = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewMiny = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewWidth = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewHeight = nsvg__atof(buf); + } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { + if (strstr(attr[i + 1], "none") != 0) { + // No uniform scaling + p->alignType = NSVG_ALIGN_NONE; + } else { + // Parse X align + if (strstr(attr[i + 1], "xMin") != 0) + p->alignX = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "xMid") != 0) + p->alignX = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "xMax") != 0) + p->alignX = NSVG_ALIGN_MAX; + // Parse X align + if (strstr(attr[i + 1], "yMin") != 0) + p->alignY = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "yMid") != 0) + p->alignY = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "yMax") != 0) + p->alignY = NSVG_ALIGN_MAX; + // Parse meet/slice + p->alignType = NSVG_ALIGN_MEET; + if (strstr(attr[i + 1], "slice") != 0) + p->alignType = NSVG_ALIGN_SLICE; + } + } + } + } +} + +static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) +{ + int i; + NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); + if (grad == NULL) return; + memset(grad, 0, sizeof(NSVGgradientData)); + grad->units = NSVG_OBJECT_SPACE; + grad->type = type; + if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { + grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); + grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { + grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + } + + nsvg__xformIdentity(grad->xform); + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "id") == 0) { + strncpy(grad->id, attr[i+1], 63); + grad->id[63] = '\0'; + } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "gradientUnits") == 0) { + if (strcmp(attr[i+1], "objectBoundingBox") == 0) + grad->units = NSVG_OBJECT_SPACE; + else + grad->units = NSVG_USER_SPACE; + } else if (strcmp(attr[i], "gradientTransform") == 0) { + nsvg__parseTransform(grad->xform, attr[i + 1]); + } else if (strcmp(attr[i], "cx") == 0) { + grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "cy") == 0) { + grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "r") == 0) { + grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fx") == 0) { + grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fy") == 0) { + grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x1") == 0) { + grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y1") == 0) { + grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x2") == 0) { + grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y2") == 0) { + grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "spreadMethod") == 0) { + if (strcmp(attr[i+1], "pad") == 0) + grad->spread = NSVG_SPREAD_PAD; + else if (strcmp(attr[i+1], "reflect") == 0) + grad->spread = NSVG_SPREAD_REFLECT; + else if (strcmp(attr[i+1], "repeat") == 0) + grad->spread = NSVG_SPREAD_REPEAT; + } else if (strcmp(attr[i], "xlink:href") == 0) { + const char *href = attr[i+1]; + strncpy(grad->ref, href+1, 62); + grad->ref[62] = '\0'; + } + } + } + + grad->next = p->gradients; + p->gradients = grad; +} + +static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) +{ + NSVGattrib* curAttr = nsvg__getAttr(p); + NSVGgradientData* grad; + NSVGgradientStop* stop; + int i, idx; + + curAttr->stopOffset = 0; + curAttr->stopColor = 0; + curAttr->stopOpacity = 1.0f; + + for (i = 0; attr[i]; i += 2) { + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } + + // Add stop to the last gradient. + grad = p->gradients; + if (grad == NULL) return; + + grad->nstops++; + grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); + if (grad->stops == NULL) return; + + // Insert + idx = grad->nstops-1; + for (i = 0; i < grad->nstops-1; i++) { + if (curAttr->stopOffset < grad->stops[i].offset) { + idx = i; + break; + } + } + if (idx != grad->nstops-1) { + for (i = grad->nstops-1; i > idx; i--) + grad->stops[i] = grad->stops[i-1]; + } + + stop = &grad->stops[idx]; + stop->color = curAttr->stopColor; + stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; + stop->offset = curAttr->stopOffset; +} + +static void nsvg__startElement(void* ud, const char* el, const char** attr) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (p->defsFlag) { + // Skip everything but gradients in defs + if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } + return; + } + + if (strcmp(el, "g") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + } else if (strcmp(el, "path") == 0) { + if (p->pathFlag) // Do not allow nested paths. + return; + nsvg__pushAttr(p); + nsvg__parsePath(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "rect") == 0) { + nsvg__pushAttr(p); + nsvg__parseRect(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "circle") == 0) { + nsvg__pushAttr(p); + nsvg__parseCircle(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "ellipse") == 0) { + nsvg__pushAttr(p); + nsvg__parseEllipse(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "line") == 0) { + nsvg__pushAttr(p); + nsvg__parseLine(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "polyline") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 0); + nsvg__popAttr(p); + } else if (strcmp(el, "polygon") == 0) { + nsvg__pushAttr(p); + nsvg__parsePoly(p, attr, 1); + nsvg__popAttr(p); + } else if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 1; + } else if (strcmp(el, "svg") == 0) { + nsvg__parseSVG(p, attr); + } +} + +static void nsvg__endElement(void* ud, const char* el) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (strcmp(el, "g") == 0) { + nsvg__popAttr(p); + } else if (strcmp(el, "path") == 0) { + p->pathFlag = 0; + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 0; + } +} + +static void nsvg__content(void* ud, const char* s) +{ + NSVG_NOTUSED(ud); + NSVG_NOTUSED(s); + // empty +} + +static void nsvg__imageBounds(NSVGparser* p, float* bounds) +{ + NSVGshape* shape; + shape = p->image->shapes; + if (shape == NULL) { + bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; + return; + } + bounds[0] = shape->bounds[0]; + bounds[1] = shape->bounds[1]; + bounds[2] = shape->bounds[2]; + bounds[3] = shape->bounds[3]; + for (shape = shape->next; shape != NULL; shape = shape->next) { + bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); + bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); + bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); + bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); + } +} + +static float nsvg__viewAlign(float content, float container, int type) +{ + if (type == NSVG_ALIGN_MIN) + return 0; + else if (type == NSVG_ALIGN_MAX) + return container - content; + // mid + return (container - content) * 0.5f; +} + +static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) +{ + float t[6]; + nsvg__xformSetTranslation(t, tx, ty); + nsvg__xformMultiply (grad->xform, t); + + nsvg__xformSetScale(t, sx, sy); + nsvg__xformMultiply (grad->xform, t); +} + +static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) +{ + NSVGshape* shape; + NSVGpath* path; + float tx, ty, sx, sy, us, bounds[4], t[6], avgs; + int i; + float* pt; + + // Guess image size if not set completely. + nsvg__imageBounds(p, bounds); + + if (p->viewWidth == 0) { + if (p->image->width > 0) { + p->viewWidth = p->image->width; + } else { + p->viewMinx = bounds[0]; + p->viewWidth = bounds[2] - bounds[0]; + } + } + if (p->viewHeight == 0) { + if (p->image->height > 0) { + p->viewHeight = p->image->height; + } else { + p->viewMiny = bounds[1]; + p->viewHeight = bounds[3] - bounds[1]; + } + } + if (p->image->width == 0) + p->image->width = p->viewWidth; + if (p->image->height == 0) + p->image->height = p->viewHeight; + + tx = -p->viewMinx; + ty = -p->viewMiny; + sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; + sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; + // Unit scaling + us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); + + // Fix aspect ratio + if (p->alignType == NSVG_ALIGN_MEET) { + // fit whole image into viewbox + sx = sy = nsvg__minf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } else if (p->alignType == NSVG_ALIGN_SLICE) { + // fill whole viewbox with image + sx = sy = nsvg__maxf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } + + // Transform + sx *= us; + sy *= us; + avgs = (sx+sy) / 2.0f; + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + shape->bounds[0] = (shape->bounds[0] + tx) * sx; + shape->bounds[1] = (shape->bounds[1] + ty) * sy; + shape->bounds[2] = (shape->bounds[2] + tx) * sx; + shape->bounds[3] = (shape->bounds[3] + ty) * sy; + for (path = shape->paths; path != NULL; path = path->next) { + path->bounds[0] = (path->bounds[0] + tx) * sx; + path->bounds[1] = (path->bounds[1] + ty) * sy; + path->bounds[2] = (path->bounds[2] + tx) * sx; + path->bounds[3] = (path->bounds[3] + ty) * sy; + for (i =0; i < path->npts; i++) { + pt = &path->pts[i*2]; + pt[0] = (pt[0] + tx) * sx; + pt[1] = (pt[1] + ty) * sy; + } + } + + if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); + memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->fill.gradient->xform, t); + } + if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); + memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->stroke.gradient->xform, t); + } + + shape->strokeWidth *= avgs; + shape->strokeDashOffset *= avgs; + for (i = 0; i < shape->strokeDashCount; i++) + shape->strokeDashArray[i] *= avgs; + } +} + +static void nsvg__createGradients(NSVGparser* p) +{ + NSVGshape* shape; + + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + if (shape->fill.type == NSVG_PAINT_UNDEF) { + if (shape->fillGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); + } + if (shape->fill.type == NSVG_PAINT_UNDEF) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + if (shape->strokeGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + shape->stroke.type = NSVG_PAINT_NONE; + } + } + } +} + +NSVGimage* nsvgParse(char* input, const char* units, float dpi) +{ + NSVGparser* p; + NSVGimage* ret = 0; + + p = nsvg__createParser(); + if (p == NULL) { + return NULL; + } + p->dpi = dpi; + + nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + + // Create gradients after all definitions have been parsed + nsvg__createGradients(p); + + // Scale to viewBox + nsvg__scaleToViewbox(p, units); + + ret = p->image; + p->image = NULL; + + nsvg__deleteParser(p); + + return ret; +} + +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) +{ + FILE* fp = NULL; + size_t size; + char* data = NULL; + NSVGimage* image = NULL; + + fp = fopen(filename, "rb"); + if (!fp) goto error; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + data = (char*)malloc(size+1); + if (data == NULL) goto error; + if (fread(data, 1, size, fp) != size) goto error; + data[size] = '\0'; // Must be null terminated. + fclose(fp); + image = nsvgParse(data, units, dpi); + free(data); + + return image; + +error: + if (fp) fclose(fp); + if (data) free(data); + if (image) nsvgDelete(image); + return NULL; +} + +NSVGpath* nsvgDuplicatePath(NSVGpath* p) +{ + NSVGpath* res = NULL; + + if (p == NULL) + return NULL; + + res = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (res == NULL) goto error; + memset(res, 0, sizeof(NSVGpath)); + + res->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (res->pts == NULL) goto error; + memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); + res->npts = p->npts; + + memcpy(res->bounds, p->bounds, sizeof(p->bounds)); + + res->closed = p->closed; + + return res; + +error: + if (res != NULL) { + free(res->pts); + free(res); + } + return NULL; +} + +void nsvgDelete(NSVGimage* image) +{ + NSVGshape *snext, *shape; + if (image == NULL) return; + shape = image->shapes; + while (shape != NULL) { + snext = shape->next; + nsvg__deletePaths(shape->paths); + nsvg__deletePaint(&shape->fill); + nsvg__deletePaint(&shape->stroke); + free(shape); + shape = snext; + } + free(image); +} + +#endif // NANOSVG_IMPLEMENTATION + +#endif // NANOSVG_H \ No newline at end of file diff --git a/lib/nanosvgrast.h b/lib/nanosvgrast.h new file mode 100644 index 00000000..5742ee02 --- /dev/null +++ b/lib/nanosvgrast.h @@ -0,0 +1,1472 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The polygon rasterization is heavily based on stb_truetype rasterizer + * by Sean Barrett - http://nothings.org/ + * + */ + +#ifndef NANOSVGRAST_H +#define NANOSVGRAST_H + +#include "nanosvg.h" + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +typedef struct NSVGrasterizer NSVGrasterizer; + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + + // Create rasterizer (can be used to render multiple images). + struct NSVGrasterizer* rast = nsvgCreateRasterizer(); + // Allocate memory for image + unsigned char* img = malloc(w*h*4); + // Rasterize + nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); +*/ + +// Allocated rasterizer context. +NSVGrasterizer* nsvgCreateRasterizer(void); + +// Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) +// r - pointer to rasterizer context +// image - pointer to image to rasterize +// tx,ty - image offset (applied after scaling) +// scale - image scale +// dst - pointer to destination image data, 4 bytes per pixel (RGBA) +// w - width of the image to render +// h - height of the image to render +// stride - number of bytes per scaleline in the destination buffer +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride); + +// Deletes rasterizer context. +void nsvgDeleteRasterizer(NSVGrasterizer*); + + +#ifndef NANOSVGRAST_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#ifdef NANOSVGRAST_IMPLEMENTATION + +#include +#include +#include + +#define NSVG__SUBSAMPLES 5 +#define NSVG__FIXSHIFT 10 +#define NSVG__FIX (1 << NSVG__FIXSHIFT) +#define NSVG__FIXMASK (NSVG__FIX-1) +#define NSVG__MEMPAGE_SIZE 1024 + +typedef struct NSVGedge { + float x0,y0, x1,y1; + int dir; + struct NSVGedge* next; +} NSVGedge; + +typedef struct NSVGpoint { + float x, y; + float dx, dy; + float len; + float dmx, dmy; + unsigned char flags; +} NSVGpoint; + +typedef struct NSVGactiveEdge { + int x,dx; + float ey; + int dir; + struct NSVGactiveEdge *next; +} NSVGactiveEdge; + +typedef struct NSVGmemPage { + unsigned char mem[NSVG__MEMPAGE_SIZE]; + int size; + struct NSVGmemPage* next; +} NSVGmemPage; + +typedef struct NSVGcachedPaint { + signed char type; + char spread; + float xform[6]; + unsigned int colors[256]; +} NSVGcachedPaint; + +struct NSVGrasterizer +{ + float px, py; + + float tessTol; + float distTol; + + NSVGedge* edges; + int nedges; + int cedges; + + NSVGpoint* points; + int npoints; + int cpoints; + + NSVGpoint* points2; + int npoints2; + int cpoints2; + + NSVGactiveEdge* freelist; + NSVGmemPage* pages; + NSVGmemPage* curpage; + + unsigned char* scanline; + int cscanline; + + unsigned char* bitmap; + int width, height, stride; +}; + +NSVGrasterizer* nsvgCreateRasterizer(void) +{ + NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); + if (r == NULL) goto error; + memset(r, 0, sizeof(NSVGrasterizer)); + + r->tessTol = 0.25f; + r->distTol = 0.01f; + + return r; + +error: + nsvgDeleteRasterizer(r); + return NULL; +} + +void nsvgDeleteRasterizer(NSVGrasterizer* r) +{ + NSVGmemPage* p; + + if (r == NULL) return; + + p = r->pages; + while (p != NULL) { + NSVGmemPage* next = p->next; + free(p); + p = next; + } + + if (r->edges) free(r->edges); + if (r->points) free(r->points); + if (r->points2) free(r->points2); + if (r->scanline) free(r->scanline); + + free(r); +} + +static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) +{ + NSVGmemPage *newp; + + // If using existing chain, return the next page in chain + if (cur != NULL && cur->next != NULL) { + return cur->next; + } + + // Alloc new page + newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); + if (newp == NULL) return NULL; + memset(newp, 0, sizeof(NSVGmemPage)); + + // Add to linked list + if (cur != NULL) + cur->next = newp; + else + r->pages = newp; + + return newp; +} + +static void nsvg__resetPool(NSVGrasterizer* r) +{ + NSVGmemPage* p = r->pages; + while (p != NULL) { + p->size = 0; + p = p->next; + } + r->curpage = r->pages; +} + +static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) +{ + unsigned char* buf; + if (size > NSVG__MEMPAGE_SIZE) return NULL; + if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { + r->curpage = nsvg__nextPage(r, r->curpage); + } + buf = &r->curpage->mem[r->curpage->size]; + r->curpage->size += size; + return buf; +} + +static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) +{ + float dx = x2 - x1; + float dy = y2 - y1; + return dx*dx + dy*dy < tol*tol; +} + +static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) +{ + NSVGpoint* pt; + + if (r->npoints > 0) { + pt = &r->points[r->npoints-1]; + if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { + pt->flags = (unsigned char)(pt->flags | flags); + return; + } + } + + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + + pt = &r->points[r->npoints]; + pt->x = x; + pt->y = y; + pt->flags = (unsigned char)flags; + r->npoints++; +} + +static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) +{ + if (r->npoints+1 > r->cpoints) { + r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; + r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); + if (r->points == NULL) return; + } + r->points[r->npoints] = pt; + r->npoints++; +} + +static void nsvg__duplicatePoints(NSVGrasterizer* r) +{ + if (r->npoints > r->cpoints2) { + r->cpoints2 = r->npoints; + r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); + if (r->points2 == NULL) return; + } + + memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); + r->npoints2 = r->npoints; +} + +static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) +{ + NSVGedge* e; + + // Skip horizontal edges + if (y0 == y1) + return; + + if (r->nedges+1 > r->cedges) { + r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; + r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); + if (r->edges == NULL) return; + } + + e = &r->edges[r->nedges]; + r->nedges++; + + if (y0 < y1) { + e->x0 = x0; + e->y0 = y0; + e->x1 = x1; + e->y1 = y1; + e->dir = 1; + } else { + e->x0 = x1; + e->y0 = y1; + e->x1 = x0; + e->y1 = y0; + e->dir = -1; + } +} + +static float nsvg__normalize(float *x, float* y) +{ + float d = sqrtf((*x)*(*x) + (*y)*(*y)); + if (d > 1e-6f) { + float id = 1.0f / d; + *x *= id; + *y *= id; + } + return d; +} + +static float nsvg__absf(float x) { return x < 0 ? -x : x; } +static float nsvg__roundf(float x) { return (x >= 0) ? floorf(x + 0.5) : ceilf(x - 0.5); } + +static void nsvg__flattenCubicBez(NSVGrasterizer* r, + float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4, + int level, int type) +{ + float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; + float dx,dy,d2,d3; + + if (level > 10) return; + + x12 = (x1+x2)*0.5f; + y12 = (y1+y2)*0.5f; + x23 = (x2+x3)*0.5f; + y23 = (y2+y3)*0.5f; + x34 = (x3+x4)*0.5f; + y34 = (y3+y4)*0.5f; + x123 = (x12+x23)*0.5f; + y123 = (y12+y23)*0.5f; + + dx = x4 - x1; + dy = y4 - y1; + d2 = nsvg__absf((x2 - x4) * dy - (y2 - y4) * dx); + d3 = nsvg__absf((x3 - x4) * dy - (y3 - y4) * dx); + + if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { + nsvg__addPathPoint(r, x4, y4, type); + return; + } + + x234 = (x23+x34)*0.5f; + y234 = (y23+y34)*0.5f; + x1234 = (x123+x234)*0.5f; + y1234 = (y123+y234)*0.5f; + + nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); + nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); +} + +static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j; + NSVGpath* path; + + for (path = shape->paths; path != NULL; path = path->next) { + r->npoints = 0; + // Flatten path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); + } + // Close path + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + // Build edges + for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) + nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); + } +} + +enum NSVGpointFlags +{ + NSVG_PT_CORNER = 0x01, + NSVG_PT_BEVEL = 0x02, + NSVG_PT_LEFT = 0x04 +}; + +static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + float len = nsvg__normalize(&dx, &dy); + float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) +{ + float w = lineWidth * 0.5f; + float px = p->x - dx*w, py = p->y - dy*w; + float dlx = dy, dly = -dx; + float lx = px - dlx*w, ly = py - dly*w; + float rx = px + dlx*w, ry = py + dly*w; + + nsvg__addEdge(r, lx, ly, rx, ry); + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +#ifndef NSVG_PI +#define NSVG_PI (3.14159265358979323846264338327f) +#endif + +static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) +{ + int i; + float w = lineWidth * 0.5f; + float px = p->x, py = p->y; + float dlx = dy, dly = -dx; + float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; + + for (i = 0; i < ncap; i++) { + float a = (float)i/(float)(ncap-1)*NSVG_PI; + float ax = cosf(a) * w, ay = sinf(a) * w; + float x = px - dlx*ax - dx*ay; + float y = py - dly*ax - dy*ay; + + if (i > 0) + nsvg__addEdge(r, prevx, prevy, x, y); + + prevx = x; + prevy = y; + + if (i == 0) { + lx = x; ly = y; + } else if (i == ncap-1) { + rx = x; ry = y; + } + } + + if (connect) { + nsvg__addEdge(r, left->x, left->y, lx, ly); + nsvg__addEdge(r, rx, ry, right->x, right->y); + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); + float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); + float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); + float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); + + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float lx0, rx0, lx1, rx1; + float ly0, ry0, ly1, ry1; + + if (p1->flags & NSVG_PT_LEFT) { + lx0 = lx1 = p1->x - p1->dmx * w; + ly0 = ly1 = p1->y - p1->dmy * w; + nsvg__addEdge(r, lx1, ly1, left->x, left->y); + + rx0 = p1->x + (dlx0 * w); + ry0 = p1->y + (dly0 * w); + rx1 = p1->x + (dlx1 * w); + ry1 = p1->y + (dly1 * w); + nsvg__addEdge(r, right->x, right->y, rx0, ry0); + nsvg__addEdge(r, rx0, ry0, rx1, ry1); + } else { + lx0 = p1->x - (dlx0 * w); + ly0 = p1->y - (dly0 * w); + lx1 = p1->x - (dlx1 * w); + ly1 = p1->y - (dly1 * w); + nsvg__addEdge(r, lx0, ly0, left->x, left->y); + nsvg__addEdge(r, lx1, ly1, lx0, ly0); + + rx0 = rx1 = p1->x + p1->dmx * w; + ry0 = ry1 = p1->y + p1->dmy * w; + nsvg__addEdge(r, right->x, right->y, rx1, ry1); + } + + left->x = lx1; left->y = ly1; + right->x = rx1; right->y = ry1; +} + +static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) +{ + int i, n; + float w = lineWidth * 0.5f; + float dlx0 = p0->dy, dly0 = -p0->dx; + float dlx1 = p1->dy, dly1 = -p1->dx; + float a0 = atan2f(dly0, dlx0); + float a1 = atan2f(dly1, dlx1); + float da = a1 - a0; + float lx, ly, rx, ry; + + if (da < NSVG_PI) da += NSVG_PI*2; + if (da > NSVG_PI) da -= NSVG_PI*2; + + n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); + if (n < 2) n = 2; + if (n > ncap) n = ncap; + + lx = left->x; + ly = left->y; + rx = right->x; + ry = right->y; + + for (i = 0; i < n; i++) { + float u = (float)i/(float)(n-1); + float a = a0 + u*da; + float ax = cosf(a) * w, ay = sinf(a) * w; + float lx1 = p1->x - ax, ly1 = p1->y - ay; + float rx1 = p1->x + ax, ry1 = p1->y + ay; + + nsvg__addEdge(r, lx1, ly1, lx, ly); + nsvg__addEdge(r, rx, ry, rx1, ry1); + + lx = lx1; ly = ly1; + rx = rx1; ry = ry1; + } + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) +{ + float w = lineWidth * 0.5f; + float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); + float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); + + nsvg__addEdge(r, lx, ly, left->x, left->y); + nsvg__addEdge(r, right->x, right->y, rx, ry); + + left->x = lx; left->y = ly; + right->x = rx; right->y = ry; +} + +static int nsvg__curveDivs(float r, float arc, float tol) +{ + float da = acosf(r / (r + tol)) * 2.0f; + int divs = (int)ceilf(arc / da); + if (divs < 2) divs = 2; + return divs; +} + +static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) +{ + int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. + NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; + NSVGpoint* p0, *p1; + int j, s, e; + + // Build stroke edges + if (closed) { + // Looping + p0 = &points[npoints-1]; + p1 = &points[0]; + s = 0; + e = npoints; + } else { + // Add cap + p0 = &points[0]; + p1 = &points[1]; + s = 1; + e = npoints-1; + } + + if (closed) { + nsvg__initClosed(&left, &right, p0, p1, lineWidth); + firstLeft = left; + firstRight = right; + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); + } + + for (j = s; j < e; ++j) { + if (p1->flags & NSVG_PT_CORNER) { + if (lineJoin == NSVG_JOIN_ROUND) + nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); + else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) + nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); + else + nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); + } else { + nsvg__straightJoin(r, &left, &right, p1, lineWidth); + } + p0 = p1++; + } + + if (closed) { + // Loop it + nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); + nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); + } else { + // Add cap + float dx = p1->x - p0->x; + float dy = p1->y - p0->y; + nsvg__normalize(&dx, &dy); + if (lineCap == NSVG_CAP_BUTT) + nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_SQUARE) + nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); + else if (lineCap == NSVG_CAP_ROUND) + nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); + } +} + +static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) +{ + int i, j; + NSVGpoint* p0, *p1; + + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (i = 0; i < r->npoints; i++) { + // Calculate segment direction and length + p0->dx = p1->x - p0->x; + p0->dy = p1->y - p0->y; + p0->len = nsvg__normalize(&p0->dx, &p0->dy); + // Advance + p0 = p1++; + } + + // calculate joins + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + for (j = 0; j < r->npoints; j++) { + float dlx0, dly0, dlx1, dly1, dmr2, cross; + dlx0 = p0->dy; + dly0 = -p0->dx; + dlx1 = p1->dy; + dly1 = -p1->dx; + // Calculate extrusions + p1->dmx = (dlx0 + dlx1) * 0.5f; + p1->dmy = (dly0 + dly1) * 0.5f; + dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; + if (dmr2 > 0.000001f) { + float s2 = 1.0f / dmr2; + if (s2 > 600.0f) { + s2 = 600.0f; + } + p1->dmx *= s2; + p1->dmy *= s2; + } + + // Clear flags, but keep the corner. + p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; + + // Keep track of left turns. + cross = p1->dx * p0->dy - p0->dx * p1->dy; + if (cross > 0.0f) + p1->flags |= NSVG_PT_LEFT; + + // Check to see if the corner needs to be beveled. + if (p1->flags & NSVG_PT_CORNER) { + if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { + p1->flags |= NSVG_PT_BEVEL; + } + } + + p0 = p1++; + } +} + +static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) +{ + int i, j, closed; + NSVGpath* path; + NSVGpoint* p0, *p1; + float miterLimit = shape->miterLimit; + int lineJoin = shape->strokeLineJoin; + int lineCap = shape->strokeLineCap; + float lineWidth = shape->strokeWidth * scale; + + for (path = shape->paths; path != NULL; path = path->next) { + // Flatten path + r->npoints = 0; + nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); + for (i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); + } + if (r->npoints < 2) + continue; + + closed = path->closed; + + // If the first and last points are the same, remove the last, mark as closed path. + p0 = &r->points[r->npoints-1]; + p1 = &r->points[0]; + if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { + r->npoints--; + p0 = &r->points[r->npoints-1]; + closed = 1; + } + + if (shape->strokeDashCount > 0) { + int idash = 0, dashState = 1; + float totalDist = 0, dashLen, allDashLen, dashOffset; + NSVGpoint cur; + + if (closed) + nsvg__appendPathPoint(r, r->points[0]); + + // Duplicate points -> points2. + nsvg__duplicatePoints(r); + + r->npoints = 0; + cur = r->points2[0]; + nsvg__appendPathPoint(r, cur); + + // Figure out dash offset. + allDashLen = 0; + for (j = 0; j < shape->strokeDashCount; j++) + allDashLen += shape->strokeDashArray[j]; + if (shape->strokeDashCount & 1) + allDashLen *= 2.0f; + // Find location inside pattern + dashOffset = fmodf(shape->strokeDashOffset, allDashLen); + if (dashOffset < 0.0f) + dashOffset += allDashLen; + + while (dashOffset > shape->strokeDashArray[idash]) { + dashOffset -= shape->strokeDashArray[idash]; + idash = (idash + 1) % shape->strokeDashCount; + } + dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; + + for (j = 1; j < r->npoints2; ) { + float dx = r->points2[j].x - cur.x; + float dy = r->points2[j].y - cur.y; + float dist = sqrtf(dx*dx + dy*dy); + + if ((totalDist + dist) > dashLen) { + // Calculate intermediate point + float d = (dashLen - totalDist) / dist; + float x = cur.x + dx * d; + float y = cur.y + dy * d; + nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); + + // Stroke + if (r->npoints > 1 && dashState) { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } + // Advance dash pattern + dashState = !dashState; + idash = (idash+1) % shape->strokeDashCount; + dashLen = shape->strokeDashArray[idash] * scale; + // Restart + cur.x = x; + cur.y = y; + cur.flags = NSVG_PT_CORNER; + totalDist = 0.0f; + r->npoints = 0; + nsvg__appendPathPoint(r, cur); + } else { + totalDist += dist; + cur = r->points2[j]; + nsvg__appendPathPoint(r, cur); + j++; + } + } + // Stroke any leftover path + if (r->npoints > 1 && dashState) { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); + } + } else { + nsvg__prepareStroke(r, miterLimit, lineJoin); + nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); + } + } +} + +static int nsvg__cmpEdge(const void *p, const void *q) +{ + const NSVGedge* a = (const NSVGedge*)p; + const NSVGedge* b = (const NSVGedge*)q; + + if (a->y0 < b->y0) return -1; + if (a->y0 > b->y0) return 1; + return 0; +} + + +static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) +{ + NSVGactiveEdge* z; + + if (r->freelist != NULL) { + // Restore from freelist. + z = r->freelist; + r->freelist = z->next; + } else { + // Alloc new edge. + z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); + if (z == NULL) return NULL; + } + + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); +// STBTT_assert(e->y0 <= start_point); + // round dx down to avoid going too far + if (dxdy < 0) + z->dx = (int)(-nsvg__roundf(NSVG__FIX * -dxdy)); + else + z->dx = (int)nsvg__roundf(NSVG__FIX * dxdy); + z->x = (int)nsvg__roundf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); +// z->x -= off_x * FIX; + z->ey = e->y1; + z->next = 0; + z->dir = e->dir; + + return z; +} + +static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) +{ + z->next = r->freelist; + r->freelist = z; +} + +static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) +{ + int i = x0 >> NSVG__FIXSHIFT; + int j = x1 >> NSVG__FIXSHIFT; + if (i < *xmin) *xmin = i; + if (j > *xmax) *xmax = j; + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = (unsigned char)(scanline[i] + maxWeight); + } + } +} + +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) +{ + // non-zero winding fill + int x0 = 0, w = 0; + + if (fillRule == NSVG_FILLRULE_NONZERO) { + // Non-zero + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->dir; + } else { + int x1 = e->x; w += e->dir; + // if we went to zero, we need to draw + if (w == 0) + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } else if (fillRule == NSVG_FILLRULE_EVENODD) { + // Even-odd + while (e != NULL) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w = 1; + } else { + int x1 = e->x; w = 0; + nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); + } + e = e->next; + } + } +} + +static float nsvg__clampf(float a, float mn, float mx) { + if (isnan(a)) + return mn; + return a < mn ? mn : (a > mx ? mx : a); +} + +static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) +{ + return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); +} + +static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; + int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; + int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; + int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static unsigned int nsvg__applyOpacity(unsigned int c, float u) +{ + int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); + int r = (c) & 0xff; + int g = (c>>8) & 0xff; + int b = (c>>16) & 0xff; + int a = (((c>>24) & 0xff)*iu) >> 8; + return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); +} + +static inline int nsvg__div255(int x) +{ + return ((x+1) * 257) >> 16; +} + +static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, + float tx, float ty, float scale, NSVGcachedPaint* cache) +{ + + if (cache->type == NSVG_PAINT_COLOR) { + int i, cr, cg, cb, ca; + cr = cache->colors[0] & 0xff; + cg = (cache->colors[0] >> 8) & 0xff; + cb = (cache->colors[0] >> 16) & 0xff; + ca = (cache->colors[0] >> 24) & 0xff; + + for (i = 0; i < count; i++) { + int r,g,b; + int a = nsvg__div255((int)cover[0] * ca); + int ia = 255 - a; + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + } + } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + float fx, fy, dx, gy; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gy = fx*t[1] + fy*t[3] + t[5]; + c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { + // TODO: spread modes. + // TODO: plenty of opportunities to optimize. + // TODO: focus (fx,fy) + float fx, fy, dx, gx, gy, gd; + float* t = cache->xform; + int i, cr, cg, cb, ca; + unsigned int c; + + fx = ((float)x - tx) / scale; + fy = ((float)y - ty) / scale; + dx = 1.0f / scale; + + for (i = 0; i < count; i++) { + int r,g,b,a,ia; + gx = fx*t[0] + fy*t[2] + t[4]; + gy = fx*t[1] + fy*t[3] + t[5]; + gd = sqrtf(gx*gx + gy*gy); + c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; + cr = (c) & 0xff; + cg = (c >> 8) & 0xff; + cb = (c >> 16) & 0xff; + ca = (c >> 24) & 0xff; + + a = nsvg__div255((int)cover[0] * ca); + ia = 255 - a; + + // Premultiply + r = nsvg__div255(cr * a); + g = nsvg__div255(cg * a); + b = nsvg__div255(cb * a); + + // Blend over + r += nsvg__div255(ia * (int)dst[0]); + g += nsvg__div255(ia * (int)dst[1]); + b += nsvg__div255(ia * (int)dst[2]); + a += nsvg__div255(ia * (int)dst[3]); + + dst[0] = (unsigned char)r; + dst[1] = (unsigned char)g; + dst[2] = (unsigned char)b; + dst[3] = (unsigned char)a; + + cover++; + dst += 4; + fx += dx; + } + } +} + +static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) +{ + NSVGactiveEdge *active = NULL; + int y, s; + int e = 0; + int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline + int xmin, xmax; + + for (y = 0; y < r->height; y++) { + memset(r->scanline, 0, r->width); + xmin = r->width; + xmax = 0; + for (s = 0; s < NSVG__SUBSAMPLES; ++s) { + // find center of pixel for this scanline + float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; + NSVGactiveEdge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + NSVGactiveEdge *z = *step; + if (z->ey <= scany) { + *step = z->next; // delete from list +// NSVG__assert(z->valid); + nsvg__freeActive(r, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for (;;) { + int changed = 0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + NSVGactiveEdge* t = *step; + NSVGactiveEdge* q = t->next; + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e < r->nedges && r->edges[e].y0 <= scany) { + if (r->edges[e].y1 > scany) { + NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); + if (z == NULL) break; + // find insertion point + if (active == NULL) { + active = z; + } else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + NSVGactiveEdge* p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + e++; + } + + // now process all active edges in non-zero fashion + if (active != NULL) + nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); + } + // Blit + if (xmin < 0) xmin = 0; + if (xmax > r->width-1) xmax = r->width-1; + if (xmin <= xmax) { + nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); + } + } + +} + +static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) +{ + int x,y; + + // Unpremultiply + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = row[0], g = row[1], b = row[2], a = row[3]; + if (a != 0) { + row[0] = (unsigned char)(r*255/a); + row[1] = (unsigned char)(g*255/a); + row[2] = (unsigned char)(b*255/a); + } + row += 4; + } + } + + // Defringe + for (y = 0; y < h; y++) { + unsigned char *row = &image[y*stride]; + for (x = 0; x < w; x++) { + int r = 0, g = 0, b = 0, a = row[3], n = 0; + if (a == 0) { + if (x-1 > 0 && row[-1] != 0) { + r += row[-4]; + g += row[-3]; + b += row[-2]; + n++; + } + if (x+1 < w && row[7] != 0) { + r += row[4]; + g += row[5]; + b += row[6]; + n++; + } + if (y-1 > 0 && row[-stride+3] != 0) { + r += row[-stride]; + g += row[-stride+1]; + b += row[-stride+2]; + n++; + } + if (y+1 < h && row[stride+3] != 0) { + r += row[stride]; + g += row[stride+1]; + b += row[stride+2]; + n++; + } + if (n > 0) { + row[0] = (unsigned char)(r/n); + row[1] = (unsigned char)(g/n); + row[2] = (unsigned char)(b/n); + } + } + row += 4; + } + } +} + + +static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) +{ + int i, j; + NSVGgradient* grad; + + cache->type = paint->type; + + if (paint->type == NSVG_PAINT_COLOR) { + cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); + return; + } + + grad = paint->gradient; + + cache->spread = grad->spread; + memcpy(cache->xform, grad->xform, sizeof(float)*6); + + if (grad->nstops == 0) { + for (i = 0; i < 256; i++) + cache->colors[i] = 0; + } else if (grad->nstops == 1) { + unsigned int color = nsvg__applyOpacity(grad->stops[0].color, opacity); + for (i = 0; i < 256; i++) + cache->colors[i] = color; + } else { + unsigned int ca, cb = 0; + float ua, ub, du, u; + int ia, ib, count; + + ca = nsvg__applyOpacity(grad->stops[0].color, opacity); + ua = nsvg__clampf(grad->stops[0].offset, 0, 1); + ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + for (i = 0; i < ia; i++) { + cache->colors[i] = ca; + } + + for (i = 0; i < grad->nstops-1; i++) { + ca = nsvg__applyOpacity(grad->stops[i].color, opacity); + cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); + ua = nsvg__clampf(grad->stops[i].offset, 0, 1); + ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); + ia = (int)(ua * 255.0f); + ib = (int)(ub * 255.0f); + count = ib - ia; + if (count <= 0) continue; + u = 0; + du = 1.0f / (float)count; + for (j = 0; j < count; j++) { + cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); + u += du; + } + } + + for (i = ib; i < 256; i++) + cache->colors[i] = cb; + } + +} + +/* +static void dumpEdges(NSVGrasterizer* r, const char* name) +{ + float xmin = 0, xmax = 0, ymin = 0, ymax = 0; + NSVGedge *e = NULL; + int i; + if (r->nedges == 0) return; + FILE* fp = fopen(name, "w"); + if (fp == NULL) return; + + xmin = xmax = r->edges[0].x0; + ymin = ymax = r->edges[0].y0; + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + xmin = nsvg__minf(xmin, e->x0); + xmin = nsvg__minf(xmin, e->x1); + xmax = nsvg__maxf(xmax, e->x0); + xmax = nsvg__maxf(xmax, e->x1); + ymin = nsvg__minf(ymin, e->y0); + ymin = nsvg__minf(ymin, e->y1); + ymax = nsvg__maxf(ymax, e->y0); + ymax = nsvg__maxf(ymax, e->y1); + } + + fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); + + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); + } + + for (i = 0; i < r->npoints; i++) { + if (i+1 < r->npoints) + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); + fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); + } + + fprintf(fp, ""); + fclose(fp); +} +*/ + +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride) +{ + NSVGshape *shape = NULL; + NSVGedge *e = NULL; + NSVGcachedPaint cache; + int i; + int j; + unsigned char paintOrder; + + r->bitmap = dst; + r->width = w; + r->height = h; + r->stride = stride; + + if (w > r->cscanline) { + r->cscanline = w; + r->scanline = (unsigned char*)realloc(r->scanline, w); + if (r->scanline == NULL) return; + } + + for (i = 0; i < h; i++) + memset(&dst[i*stride], 0, w*4); + + for (shape = image->shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + + for (j = 0; j < 3; j++) { + paintOrder = (shape->paintOrder >> (2 * j)) & 0x03; + + if (paintOrder == NSVG_PAINT_FILL && shape->fill.type != NSVG_PAINT_NONE) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShape(r, shape, scale); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->fill, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); + } + if (paintOrder == NSVG_PAINT_STROKE && shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { + nsvg__resetPool(r); + r->freelist = NULL; + r->nedges = 0; + + nsvg__flattenShapeStroke(r, shape, scale); + + // dumpEdges(r, "edge.svg"); + + // Scale and translate edges + for (i = 0; i < r->nedges; i++) { + e = &r->edges[i]; + e->x0 = tx + e->x0; + e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; + e->x1 = tx + e->x1; + e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; + } + + // Rasterize edges + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + + // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule + nsvg__initPaint(&cache, &shape->stroke, shape->opacity); + + nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); + } + } + } + + nsvg__unpremultiplyAlpha(dst, w, h, stride); + + r->bitmap = NULL; + r->width = 0; + r->height = 0; + r->stride = 0; +} + +#endif // NANOSVGRAST_IMPLEMENTATION + +#endif // NANOSVGRAST_H \ No newline at end of file diff --git a/src/embedded_assets/bongocat/CMakeLists.txt b/src/embedded_assets/bongocat/CMakeLists.txt index 7cf44ee6..4e2c0427 100644 --- a/src/embedded_assets/bongocat/CMakeLists.txt +++ b/src/embedded_assets/bongocat/CMakeLists.txt @@ -2,9 +2,10 @@ add_library(assets_bongocat_interface INTERFACE) add_library(assets_bongocat_feature INTERFACE) target_compile_definitions(assets_bongocat_feature INTERFACE FEATURE_BONGOCAT_EMBEDDED_ASSETS) +target_compile_definitions(assets_bongocat_feature INTERFACE $<$:FEATURE_USE_BONGOCAT_SVG>) add_library(assets_bongocat STATIC) -target_sources(assets_bongocat PRIVATE bongocat_get_sprite_sheet.cpp bongocat_images.c) +target_sources(assets_bongocat PRIVATE bongocat_get_sprite_sheet.cpp bongocat_images.c $<$:bongocat_get_sprite_sheet_svg.cpp>) target_compile_options(assets_bongocat PRIVATE -ffunction-sections -fdata-sections) target_include_directories(assets_bongocat PUBLIC ${INCLUDE_DIR} diff --git a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp index 5aff3b8c..0f55fad8 100644 --- a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp +++ b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp @@ -14,6 +14,8 @@ embedded_image_t get_bongocat_sprite(size_t i) { return {bongo_cat_right_down_png, bongo_cat_right_down_png_size, "bongo-cat-right-down"}; case BONGOCAT_FRAME_BOTH_DOWN: return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; + case BONGOCAT_FRAME_SLEEPING: + return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; default: return {}; } diff --git a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet_svg.cpp b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet_svg.cpp new file mode 100644 index 00000000..28022a65 --- /dev/null +++ b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet_svg.cpp @@ -0,0 +1,24 @@ +#include "embedded_assets/bongocat/bongocat.h" +#include "embedded_assets/bongocat/bongocat_images.h" +#include "embedded_assets/embedded_image.h" + +namespace bongocat::assets { +embedded_image_t get_bongocat_sprite_svg(size_t i) { + using namespace assets; + switch (i) { + case BONGOCAT_FRAME_BOTH_UP: + return {bongo_cat_both_up_svg, bongo_cat_both_up_svg_size, "bongo-cat-both-up"}; + case BONGOCAT_FRAME_LEFT_DOWN: + return {bongo_cat_left_down_svg, bongo_cat_left_down_svg_size, "bongo-cat-left-down"}; + case BONGOCAT_FRAME_RIGHT_DOWN: + return {bongo_cat_right_down_svg, bongo_cat_right_down_svg_size, "bongo-cat-right-down"}; + case BONGOCAT_FRAME_BOTH_DOWN: + return {bongo_cat_both_down_svg, bongo_cat_both_down_svg_size, "bongo-cat-both-down"}; + case BONGOCAT_FRAME_SLEEPING: + return {bongo_cat_sleeping_svg, bongo_cat_sleeping_svg_size, "bongo-cat-sleeping"}; + default: + return {}; + } + return {}; +} +} // namespace bongocat::assets \ No newline at end of file diff --git a/src/embedded_assets/bongocat/bongocat_images.c b/src/embedded_assets/bongocat/bongocat_images.c index a5768bb6..6b5727cf 100644 --- a/src/embedded_assets/bongocat/bongocat_images.c +++ b/src/embedded_assets/bongocat/bongocat_images.c @@ -22,3 +22,32 @@ const unsigned char bongo_cat_both_down_png[] = { #embed "../../../assets/bongo-cat-both-down.png" }; const size_t bongo_cat_both_down_png_size = sizeof(bongo_cat_both_down_png); + + +#ifdef FEATURE_USE_BONGOCAT_SVG +// Embedded asset data (svg) +const unsigned char bongo_cat_both_up_svg[] = { +#embed "../../../assets/new/bongo-both-up.svg" +}; +const size_t bongo_cat_both_up_svg_size = sizeof(bongo_cat_both_up_svg); + +const unsigned char bongo_cat_left_down_svg[] = { +#embed "../../../assets/new/bongo-left-down.svg" +}; +const size_t bongo_cat_left_down_svg_size = sizeof(bongo_cat_left_down_svg); + +const unsigned char bongo_cat_right_down_svg[] = { +#embed "../../../assets/new/bongo-right-down.svg" +}; +const size_t bongo_cat_right_down_svg_size = sizeof(bongo_cat_right_down_svg); + +const unsigned char bongo_cat_both_down_svg[] = { +#embed "../../../assets/new/bongo-both-down.svg" +}; +const size_t bongo_cat_both_down_svg_size = sizeof(bongo_cat_both_down_svg); + +const unsigned char bongo_cat_sleeping_svg[] = { +#embed "../../../assets/new/bongo-sleeping.svg" +}; +const size_t bongo_cat_sleeping_svg_size = sizeof(bongo_cat_sleeping_svg); +#endif \ No newline at end of file diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index 9c31a3a2..bca154fd 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -499,7 +499,17 @@ created_result_t> create(const config::conf ctx.shm->bongocat_anims = platform::make_allocated_mmap_array(BONGOCAT_ANIM_COUNT); - init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); + if constexpr (features::EnableBongocatSvg) { + static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_FRAME_HEIGHT > 0); + const int cat_h = ret->thread_context._local_copy_config->cat_height; + const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; + init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, + load_bongocat_anim_type_t::SVG, cat_w, cat_h); + } else { + init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, + load_bongocat_anim_type_t::PNG, 0, 0); + } } } diff --git a/src/image_loader/CMakeLists.txt b/src/image_loader/CMakeLists.txt index 3b829a7f..4acfeead 100644 --- a/src/image_loader/CMakeLists.txt +++ b/src/image_loader/CMakeLists.txt @@ -22,6 +22,7 @@ if(FEATURE_USE_HYBRID_IMAGE_BACKEND) target_sources(assets_image_loader PRIVATE load_images.cpp load_images_hybrid.cpp) target_link_libraries(assets_image_loader PRIVATE pngle stb_image) target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_HYBRID_IMAGE_BACKEND) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_RASTER_IMAGE_LOADER) message(STATUS "Use hybrid image backend (stb_image + pngle)") else() if(FEATURE_USE_PNGLE) @@ -33,6 +34,7 @@ else() target_sources(assets_image_loader PRIVATE load_images.cpp load_images_pngle.cpp) target_link_libraries(assets_image_loader PRIVATE pngle) target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_PNGLE) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_RASTER_IMAGE_LOADER) message(STATUS "Use pngle image backend") else() add_library(stb_image STATIC) @@ -44,10 +46,23 @@ else() target_sources(assets_image_loader PRIVATE load_images.cpp load_images_stb_image.cpp) target_link_libraries(assets_image_loader PRIVATE stb_image) target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_STB_IMAGE) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_RASTER_IMAGE_LOADER) message(STATUS "Use stb_image backend") endif() endif() +if (FEATURE_USE_BONGOCAT_SVG) + add_library(nanosvg STATIC) + target_include_directories(nanosvg SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) + target_sources(nanosvg PRIVATE nanosvg.c) + + target_sources(assets_image_loader PRIVATE load_svgs.cpp) + target_include_directories(assets_image_loader SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/lib) + target_link_libraries(assets_image_loader PRIVATE nanosvg) + target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_NANOSVG) + message(STATUS "Use nanosvg backend") +endif() + # @NOTE(assets): 3. add image_loader sub directory add_subdirectory(bongocat) add_subdirectory(base_dm) diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index 1f82c8c7..67c177ff 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -1,31 +1,47 @@ +#include "load_images_bongocat.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" #include "graphics/animation.h" #include "graphics/animation_thread_context.h" #include "graphics/drawing.h" #include "image_loader/load_images.h" +#include "image_loader/load_svgs.h" #include "utils/memory.h" #include namespace bongocat::animation { + created_result_t -load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count) { +load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height) { BONGOCAT_LOG_VERBOSE("Load bongocat Animation(index=%d) ...", anim_index); - auto result = anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { - return result.error; + + auto [sprite_sheet, sprite_sheet_error] = [&]() { + switch (type) { + case load_bongocat_anim_type_t::SVG: + assert(target_frame_width >= 0); + assert(target_frame_height >= 0); + return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, target_frame_width, target_frame_height); + case load_bongocat_anim_type_t::PNG: + return anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); + } + + return anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); + }(); + if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return sprite_sheet_error; } bongocat_sprite_sheet_t ret; - ret.image = bongocat::move(result.result.image); - ret.frame_width = bongocat::move(result.result.frame_width); - ret.frame_height = bongocat::move(result.result.frame_height); - ret.total_frames = bongocat::move(result.result.total_frames); - ret.both_up = bongocat::move(result.result.frames[0]); - ret.left_down = bongocat::move(result.result.frames[1]); - ret.right_down = bongocat::move(result.result.frames[2]); - ret.both_down = bongocat::move(result.result.frames[3]); + ret.image = bongocat::move(sprite_sheet.image); + ret.frame_width = bongocat::move(sprite_sheet.frame_width); + ret.frame_height = bongocat::move(sprite_sheet.frame_height); + ret.total_frames = bongocat::move(sprite_sheet.total_frames); + ret.both_up = bongocat::move(sprite_sheet.frames[0]); + ret.left_down = bongocat::move(sprite_sheet.frames[1]); + ret.right_down = bongocat::move(sprite_sheet.frames[2]); + ret.both_down = bongocat::move(sprite_sheet.frames[3]); + sprite_sheet = {}; // generic sprite has been moved // setup animations (cache) using namespace assets; @@ -41,7 +57,7 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp ret.animations.idle[2] = BONGOCAT_FRAME_BOTH_UP; ret.animations.idle[3] = BONGOCAT_FRAME_BOTH_UP; - ret.animations.boring[0] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.boring[0] = BONGOCAT_FRAME_SLEEPING; ret.animations.boring[1] = BONGOCAT_FRAME_BOTH_UP; ret.animations.boring[2] = BONGOCAT_FRAME_BOTH_UP; ret.animations.boring[3] = BONGOCAT_FRAME_BOTH_UP; @@ -51,10 +67,10 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp ret.animations.writing[2] = BONGOCAT_FRAME_LEFT_DOWN; ret.animations.writing[3] = BONGOCAT_FRAME_RIGHT_DOWN; - ret.animations.sleep[0] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[1] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[2] = BONGOCAT_FRAME_BOTH_DOWN; - ret.animations.sleep[3] = BONGOCAT_FRAME_BOTH_DOWN; + ret.animations.sleep[0] = BONGOCAT_FRAME_SLEEPING; + ret.animations.sleep[1] = BONGOCAT_FRAME_SLEEPING; + ret.animations.sleep[2] = BONGOCAT_FRAME_SLEEPING; + ret.animations.sleep[3] = BONGOCAT_FRAME_SLEEPING; ret.animations.wake_up[0] = BONGOCAT_FRAME_BOTH_UP; ret.animations.wake_up[1] = BONGOCAT_FRAME_BOTH_UP; @@ -95,7 +111,7 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp } bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, - size_t embedded_images_count) { + size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -103,33 +119,46 @@ bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_in assert(anim_index >= 0 && static_cast(anim_index) < BONGOCAT_ANIM_COUNT); BONGOCAT_LOG_VERBOSE("Load bongocat Animation (%d/%d): %s ...", anim_index, BONGOCAT_ANIM_COUNT, get_sprite(embedded_images_count).name); - auto result = load_bongocat_anim(anim_index, get_sprite, embedded_images_count); - if (result.error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + + auto [sprite_sheet, sprite_sheet_error] = load_bongocat_anim(anim_index, get_sprite, embedded_images_count, type, target_frame_width, target_frame_height); + if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Load bongocat Animation failed: index: %d", anim_index); - return result.error; + return sprite_sheet_error; } - assert(result.result.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image + assert(sprite_sheet.total_frames > 0); ///< this SHOULD always work if it's an valid EMBEDDED image assert(MAX_NUM_FRAMES <= INT_MAX); - assert(result.result.total_frames <= static_cast(MAX_NUM_FRAMES)); - if (result.result.total_frames > static_cast(MAX_NUM_FRAMES)) { + assert(sprite_sheet.total_frames <= static_cast(MAX_NUM_FRAMES)); + if (sprite_sheet.total_frames > static_cast(MAX_NUM_FRAMES)) { BONGOCAT_LOG_ERROR("Sprite Sheet does not fit in out_frames: %d, total_frames: %d", MAX_NUM_FRAMES, - result.result.total_frames); + sprite_sheet.total_frames); return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } assert(anim_index >= 0); - ctx.shm->bongocat_anims[static_cast(anim_index)] = bongocat::move(result.result); + ctx.shm->bongocat_anims[static_cast(anim_index)] = bongocat::move(sprite_sheet); assert(ctx.shm->bongocat_anims[static_cast(anim_index)].type == animation_t::type_t::Bongocat); + sprite_sheet = {}; ///< sprite sheet has been moved return bongocat_error_t::BONGOCAT_SUCCESS; } -created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, int index) { +created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& ctx, int index) { using namespace assets; using namespace animation; + BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); + switch (index) { case BONGOCAT_ANIM_INDEX: - return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT); + if constexpr (features::EnableBongocatSvg) { + static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_FRAME_HEIGHT > 0); + const int cat_h = ctx._local_copy_config->cat_height; + const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, cat_w, cat_h); + } else { + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, 0, 0); + } default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } diff --git a/src/image_loader/load_images.cpp b/src/image_loader/load_images.cpp index 1cf6f2a0..1fd8c868 100644 --- a/src/image_loader/load_images.cpp +++ b/src/image_loader/load_images.cpp @@ -1,10 +1,12 @@ #include "image_loader/load_images.h" +#ifdef FEATURE_USE_BONGOCAT_SVG +#include "image_loader/load_svgs.h" +#endif #include "graphics/animation.h" #include "graphics/animation_thread_context.h" #include "graphics/drawing.h" #include "utils/memory.h" - #include namespace bongocat::animation { @@ -252,4 +254,37 @@ created_result_t load_sprite_sheet_anim(const config::co return result; } + +BONGOCAT_NODISCARD created_result_t make_image(int width, int height, int desired_channels) { + BONGOCAT_CHECK_ERROR(width < 0, bongocat_error_t::BONGOCAT_ERROR_IMAGE, "image width can not be negative"); + BONGOCAT_CHECK_ERROR(height < 0, bongocat_error_t::BONGOCAT_ERROR_IMAGE, "image height can not be negative"); + BONGOCAT_CHECK_ERROR(desired_channels < 0, bongocat_error_t::BONGOCAT_ERROR_IMAGE, "image channels can not be negative"); + + Image ret; + if (width == 0 || height == 0 || desired_channels == 0) { + return ret; + } + + assert(width > 0); + assert(height > 0); + assert(desired_channels > 0); + const size_t data_size = static_cast(width) * + static_cast(height) * + static_cast(desired_channels); + assert(data_size > 0); + + + unsigned char* new_pixels = static_cast(::malloc(data_size)); + if (new_pixels == BONGOCAT_NULLPTR) { + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + + ret.width = width; + ret.height = height; + ret.channels = desired_channels; + ret.pixels = new_pixels; + new_pixels = BONGOCAT_NULLPTR; + + return ret; +} } // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/load_svgs.cpp b/src/image_loader/load_svgs.cpp new file mode 100644 index 00000000..65e264a7 --- /dev/null +++ b/src/image_loader/load_svgs.cpp @@ -0,0 +1,213 @@ +#include "image_loader/load_svgs.h" +#ifdef FEATURE_USE_RASTER_IMAGE_LOADER +#include "image_loader/load_images.h" +#endif +#include +#include + +#include "graphics/animation.h" +#include "graphics/animation_thread_context.h" +#include "graphics/drawing.h" +#include "utils/memory.h" +#include + +namespace bongocat::animation { +// ============================================================================= +// SVG LOADING MODULE +// ============================================================================= + +void init_svg_loader() {} + +void cleanup_svg(SvgImage& image) { + nsvgDelete(image.image); + image.image = BONGOCAT_NULLPTR; + image._units = BONGOCAT_NULLPTR; + image._dpi = 0; +} + +created_result_t load_svg(char *data, const char* units, float dpi) { + SvgImage ret; + ret.image = ::nsvgParse(data, units, dpi); + if (ret.image != BONGOCAT_NULLPTR) { + ret._units = duplicate_string(units); + ret._dpi = dpi; + + return ret; + } + + return bongocat_error_t::BONGOCAT_ERROR_SVG; +} + +#ifdef FEATURE_USE_RASTER_IMAGE_LOADER +void cleanup_svg_raster(SvgRasterImage& image) { + nsvgDeleteRasterizer(image.image); + image.image = BONGOCAT_NULLPTR; +} + + +BONGOCAT_NODISCARD created_result_t create_svg_rasterizer() { + SvgRasterImage ret; + if (ret.image == BONGOCAT_NULLPTR) [[unlikely]] { + return bongocat_error_t::BONGOCAT_ERROR_SVG; + } + + return ret; +} + +created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) { + BONGOCAT_CHECK_ERROR(params.w < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image width can not be zero nor negative"); + BONGOCAT_CHECK_ERROR(params.h < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image height can not be zero nor negative"); + BONGOCAT_CHECK_ERROR(params.stride < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image stride can not be zero nor negative"); + + auto [image, image_error] = make_image(params.w, params.h, RGBA_CHANNELS); + if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return image_error; + } + + auto [svg_raster, svg_raster_error] = create_svg_rasterizer(); + if (svg_raster_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + return svg_raster_error; + } + assert(svg_raster.image != BONGOCAT_NULLPTR); + ::nsvgRasterize(svg_raster.image, svg.image, params.tx, params.ty, params.scale, image.pixels, params.w, params.h, params.stride); + + return bongocat::move(image); +} + +created_result_t anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, + size_t embedded_images_count, + int target_w, int target_h) { + generic_sprite_sheet_t ret; + + int total_frames = 0; + int max_frame_width = 0; + int max_frame_height = 0; + int max_channels = 0; + auto loaded_images = make_allocated_array(embedded_images_count); + for (size_t i = 0; i < embedded_images_count && i < loaded_images.count; i++) { + const assets::embedded_image_t img = get_sprite(i); + AllocatedString svg_data = duplicate_string(reinterpret_cast(img.data), img.size); + + BONGOCAT_LOG_DEBUG("Loading embedded svg: %s", img.name); + auto [loaded_svg, svg_error] = load_svg(svg_data.ptr, SVG_UNITS, SVG_DPI); + if (svg_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load embedded svg: %s (%d)", img.name, svg_error); + continue; + } + bongocat::release_allocated_string(svg_data); + svg_data = BONGOCAT_NULLPTR; + assert(loaded_svg.image != nullptr); + float svg_w = loaded_svg.image->width; + float svg_h = loaded_svg.image->height; + if (svg_w <= 0.0f || svg_h <= 0.0f) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load embedded svg: width/height can not be zero (%fx%f)", static_cast(svg_w), static_cast(svg_h)); + continue; + } + + assert(svg_w > 0); + assert(svg_h > 0); + const float scale = static_cast(target_w) / svg_w; + auto [loaded_image, image_error] = load_svg_image(loaded_svg, { + .tx = 0, + .ty = 0, + .scale = scale, + .w = target_w, + .h = target_h, + .stride = target_w * 4, + }); + if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { + BONGOCAT_LOG_ERROR("Failed to load embedded svg: %s (%d)", img.name, image_error); + continue; + } + loaded_images[i] = bongocat::move(loaded_image); + assert(loaded_images[i].width >= 0); + assert(loaded_images[i].height >= 0); + assert(loaded_images[i].channels >= 0); + + // update image properties + max_frame_width = loaded_images[i].width > max_frame_width ? loaded_images[i].width : max_frame_width; + max_frame_height = loaded_images[i].height > max_frame_height ? loaded_images[i].height : max_frame_height; + max_channels = loaded_images[i].channels > max_channels ? loaded_images[i].channels : max_channels; + + BONGOCAT_LOG_DEBUG("Loaded %dx%d embedded svg", loaded_images[i].width, loaded_images[i].height); + total_frames++; + } + + ret.frame_width = max_frame_width; + ret.frame_height = max_frame_height; + ret.total_frames = total_frames; + ret.image.sprite_sheet_width = max_frame_width * total_frames; + ret.image.sprite_sheet_height = max_frame_height; + ret.image.channels = max_channels; + // create sprite sheet + assert(ret.image.sprite_sheet_width >= 0); + assert(ret.image.sprite_sheet_height >= 0); + assert(ret.image.channels >= 0); + ret.image.pixels = make_allocated_array(static_cast(ret.image.sprite_sheet_width) * + static_cast(ret.image.sprite_sheet_height) * + static_cast(ret.image.channels)); + if (!ret.image.pixels) { + ret.frame_width = 0; + ret.frame_height = 0; + ret.total_frames = 0; + ret.image.sprite_sheet_width = 0; + ret.image.sprite_sheet_height = 0; + ret.image.channels = 0; + + for (size_t i = 0; i < loaded_images.count; i++) { + if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { + ::free(loaded_images[i].pixels); + loaded_images[i].pixels = BONGOCAT_NULLPTR; + } + } + return bongocat_error_t::BONGOCAT_ERROR_MEMORY; + } + // reset frames + // memset(anim->pixels.data, 0, anim->pixels.count * sizeof(uint8_t)); + for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { + ret.frames[i] = {}; + } + // append images into one sprite sheet + assert(max_frame_width >= 0); + assert(max_channels >= 0); + assert(ret.image.sprite_sheet_width >= 0); + for (size_t frame = 0; frame < loaded_images.count; frame++) { + const auto& src = loaded_images[frame]; + assert(src.channels >= 0); + assert(src.width >= 0); + assert(src.height >= 0); + if (src.pixels != BONGOCAT_NULLPTR && src.height > 0) { + // copy pixel data of sub-region + assert(src.height >= 0); + for (size_t y = 0; y < static_cast(src.height); y++) { + unsigned char *dest_row = ret.image.pixels.data + (((y * static_cast(ret.image.sprite_sheet_width)) + + (frame * static_cast(max_frame_width))) * + static_cast(max_channels)); + const unsigned char *src_row = + src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); + memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); + } + + // update sub-region + if (frame < MAX_NUM_FRAMES) { + ret.frames[frame] = {.valid = true, .col = static_cast(frame), .row = 0}; + } + } else { + if (frame < MAX_NUM_FRAMES) { + ret.frames[frame] = {.valid = false, .col = static_cast(frame), .row = 0}; + } + } + } + + for (size_t i = 0; i < loaded_images.count; i++) { + if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { + ::free(loaded_images[i].pixels); + loaded_images[i].pixels = BONGOCAT_NULLPTR; + } + } + return ret; +} + +#endif + +} // namespace bongocat::animation \ No newline at end of file diff --git a/src/image_loader/nanosvg.c b/src/image_loader/nanosvg.c new file mode 100644 index 00000000..c489695d --- /dev/null +++ b/src/image_loader/nanosvg.c @@ -0,0 +1,28 @@ +#define NANOSVG_IMPLEMENTATION +#define NANOSVGRAST_IMPLEMENTATION +// include stb_image +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wdouble-promotion" +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#pragma GCC diagnostic ignored "-Wstrict-prototypes" +#pragma GCC diagnostic ignored "-Wold-style-definition" +#pragma GCC diagnostic ignored "-Wdouble-promotion" +#pragma GCC diagnostic ignored "-Wsign-compare" +#pragma GCC diagnostic ignored "-Wunused-function" +//#pragma GCC diagnostic ignored "-Wold-style-cast" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wcast-align" +#pragma GCC diagnostic ignored "-Wconversion" +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wduplicated-branches" +#pragma GCC diagnostic ignored "-Wuseless-cast" +//#pragma GCC diagnostic ignored "-Wimplicit-int-conversion" +#endif +#endif +#include "../lib/nanosvg.h" +#include "../lib/nanosvgrast.h" +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif diff --git a/src/platform/input.cpp b/src/platform/input.cpp index ce0adaf6..488a66b1 100644 --- a/src/platform/input.cpp +++ b/src/platform/input.cpp @@ -40,25 +40,25 @@ inline static constexpr size_t TEST_STDIN_BUF_LEN = 256; // Uses Linux input keycodes from // Left-hand keys on QWERTY keyboard // clang-format off - inline static constexpr int INPUT_LEFT_KEYS[] = { - // Number row left half (1-6) - 2, 3, 4, 5, 6, 7, // KEY_1 to KEY_6 - // QWERTY row left half - 16, 17, 18, 19, 20, // KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T - // Home row left half - 30, 31, 32, 33, 34, // KEY_A, KEY_S, KEY_D, KEY_F, KEY_G - // Bottom row left half - 44, 45, 46, 47, 48, // KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B - // Modifiers and special keys (left side) - 1, // KEY_ESC - 15, // KEY_TAB - 58, // KEY_CAPSLOCK - 42, // KEY_LEFTSHIFT - 29, // KEY_LEFTCTRL - 56, // KEY_LEFTALT - 41, // KEY_GRAVE (backtick) - 125, // KEY_LEFTMETA (super) - }; +inline static constexpr int INPUT_LEFT_KEYS[] = { + // Number row left half (1-6) + 2, 3, 4, 5, 6, 7, // KEY_1 to KEY_6 + // QWERTY row left half + 16, 17, 18, 19, 20, // KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T + // Home row left half + 30, 31, 32, 33, 34, // KEY_A, KEY_S, KEY_D, KEY_F, KEY_G + // Bottom row left half + 44, 45, 46, 47, 48, // KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B + // Modifiers and special keys (left side) + 1, // KEY_ESC + 15, // KEY_TAB + 58, // KEY_CAPSLOCK + 42, // KEY_LEFTSHIFT + 29, // KEY_LEFTCTRL + 56, // KEY_LEFTALT + 41, // KEY_GRAVE (backtick) + 125, // KEY_LEFTMETA (super) +}; // clang-format on static input_hand_mapping_t get_hand_mapping_form_keycode([[maybe_unused]] const input_context_t& input, int keycode) { From 8deed6fd77b48bed055e720af5262e81c47322c9 Mon Sep 17 00:00:00 2001 From: furudbat Date: Wed, 15 Apr 2026 11:45:24 +0200 Subject: [PATCH 08/17] wip: merge from upstream - wip: replace png bongocat with svg - wip: fix SVG alpha masking (gray) --- assets/new/bongo-both-down.svg | 8 ---- assets/new/bongo-both-up.svg | 1 - assets/new/bongo-left-down.svg | 4 -- assets/new/bongo-right-down.svg | 4 -- assets/new/bongo-sleeping.svg | 11 ------ include/embedded_assets/bongocat/bongocat.h | 5 ++- .../bongocat/load_images_bongocat.h | 23 +++++++++++- include/image_loader/load_svgs.h | 11 +++++- include/utils/memory.h | 8 ++-- include/utils/system_memory.h | 16 ++++---- lib/nanosvgrast.h | 20 +++++++--- src/graphics/animation_init.cpp | 10 ++--- .../bongocat/load_images_bongocat.cpp | 21 +++++------ src/image_loader/load_images.cpp | 6 +-- src/image_loader/load_images_hybrid.cpp | 1 - src/image_loader/load_images_pngle.cpp | 1 - src/image_loader/load_images_stb_image.cpp | 1 - src/image_loader/load_svgs.cpp | 37 ++++++++++--------- 18 files changed, 96 insertions(+), 92 deletions(-) diff --git a/assets/new/bongo-both-down.svg b/assets/new/bongo-both-down.svg index da59b611..2157e35c 100644 --- a/assets/new/bongo-both-down.svg +++ b/assets/new/bongo-both-down.svg @@ -14,27 +14,19 @@ - - - bongo-cat-left-down - - - - bongo-cat-right-down - diff --git a/assets/new/bongo-both-up.svg b/assets/new/bongo-both-up.svg index 32e7f20b..8ab11451 100644 --- a/assets/new/bongo-both-up.svg +++ b/assets/new/bongo-both-up.svg @@ -11,7 +11,6 @@ - diff --git a/assets/new/bongo-left-down.svg b/assets/new/bongo-left-down.svg index 198efd6d..be94ecb9 100644 --- a/assets/new/bongo-left-down.svg +++ b/assets/new/bongo-left-down.svg @@ -8,15 +8,11 @@ - - - bongo-cat-left-down - diff --git a/assets/new/bongo-right-down.svg b/assets/new/bongo-right-down.svg index b661ec9f..3dee1ae3 100644 --- a/assets/new/bongo-right-down.svg +++ b/assets/new/bongo-right-down.svg @@ -9,15 +9,11 @@ - - - bongo-cat-right-down - diff --git a/assets/new/bongo-sleeping.svg b/assets/new/bongo-sleeping.svg index 1671e8b8..34b4f043 100644 --- a/assets/new/bongo-sleeping.svg +++ b/assets/new/bongo-sleeping.svg @@ -12,26 +12,15 @@ - - - bongo-cat-left-down - - - - bongo-cat-right-down - - - bongo-cat-both-down - \ No newline at end of file diff --git a/include/embedded_assets/bongocat/bongocat.h b/include/embedded_assets/bongocat/bongocat.h index 487520e2..d679c5d9 100644 --- a/include/embedded_assets/bongocat/bongocat.h +++ b/include/embedded_assets/bongocat/bongocat.h @@ -28,7 +28,10 @@ inline static constexpr int BONGOCAT_FRAME_WIDTH = 864; inline static constexpr int BONGOCAT_FRAME_HEIGHT = 360; inline static constexpr int BONGOCAT_SVG_FRAME_WIDTH = 500; -inline static constexpr int BONGOCAT_SVG_FRAME_HEIGHT = 277; +inline static constexpr int BONGOCAT_SVG_FRAME_HEIGHT = 500; +inline static constexpr int BONGOCAT_SVG_FRAME_TX = 0; +inline static constexpr int BONGOCAT_SVG_FRAME_TY = -101; +inline static constexpr int BONGOCAT_SVG_ALPHA_THRESHOLD = 127; inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index 7d03d741..fca1be4e 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -1,8 +1,10 @@ #pragma once +#include "embedded_assets/bongocat/bongocat.h" #include "core/bongocat.h" #include "graphics/sprite_sheet.h" #include "image_loader/load_images.h" +#include "image_loader/load_svgs.h" namespace bongocat::animation { struct animation_thread_context_t; @@ -12,10 +14,27 @@ enum class load_bongocat_anim_type_t : uint8_t { SVG, }; BONGOCAT_NODISCARD created_result_t -load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height); +load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params); bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, - size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height); + size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params); +inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int cat_height) { + using namespace assets; + static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_FRAME_HEIGHT > 0); + const int cat_h = cat_height; + const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; + assert(cat_h > 0); + const float scale = static_cast(cat_h) / static_cast(BONGOCAT_SVG_FRAME_HEIGHT); + + return { + .target_w = cat_w, + .target_h = cat_h, + .tx = static_cast(BONGOCAT_SVG_FRAME_TX) * scale, + .ty = static_cast(BONGOCAT_SVG_FRAME_TY) * scale, + .alpha_threshold = BONGOCAT_SVG_ALPHA_THRESHOLD, + }; +} BONGOCAT_NODISCARD created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, int index); diff --git a/include/image_loader/load_svgs.h b/include/image_loader/load_svgs.h index c463d1e8..ee4dad77 100644 --- a/include/image_loader/load_svgs.h +++ b/include/image_loader/load_svgs.h @@ -38,7 +38,7 @@ struct LoadSvgImageParams { float scale; int w; int h; - int stride; + int alpha_threshold; }; BONGOCAT_NODISCARD created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params); #endif @@ -118,8 +118,15 @@ class SvgRasterImage { } }; +struct anim_sprite_sheet_from_embedded_svgs_t { + int target_w; + int target_h; + float tx{0.0f}; + float ty{0.0f}; + int alpha_threshold{0}; +}; BONGOCAT_NODISCARD created_result_t -anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, int target_w, int target_h); +anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, anim_sprite_sheet_from_embedded_svgs_t svg_params); #endif diff --git a/include/utils/memory.h b/include/utils/memory.h index 5480a138..d5722176 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -186,7 +186,7 @@ class AllocatedMemory { ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); if (ptr != BONGOCAT_NULLPTR) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { new (ptr) T(*other.ptr); } @@ -207,7 +207,7 @@ class AllocatedMemory { ptr = static_cast(BONGOCAT_MALLOC(_size_bytes)); if (ptr) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { new (ptr) T(*other.ptr); } @@ -557,7 +557,7 @@ class AllocatedArray { data = static_cast(BONGOCAT_MALLOC(_size_bytes)); if (data) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { *data[i] = *other.data[i]; @@ -582,7 +582,7 @@ class AllocatedArray { data = static_cast(BONGOCAT_MALLOC(_size_bytes)); if (data) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { *data[i] = *other.data[i]; diff --git a/include/utils/system_memory.h b/include/utils/system_memory.h index 17731290..df4ce111 100644 --- a/include/utils/system_memory.h +++ b/include/utils/system_memory.h @@ -289,7 +289,7 @@ class MMapMemory { mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); if (ptr != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { // assign/copy new (ptr) T(*other.ptr); @@ -311,7 +311,7 @@ class MMapMemory { mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); if (ptr != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { // assign/copy new (ptr) T(*other.ptr); @@ -465,7 +465,7 @@ class MMapArray { mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); if (data != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { *data[i] = *other.data[i]; @@ -490,7 +490,7 @@ class MMapArray { mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)); if (data != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { data[i] = other.data[i]; @@ -619,7 +619,7 @@ class MMapFile { ptr = mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset); if (ptr != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { new (ptr) T(*other.ptr); } @@ -641,7 +641,7 @@ class MMapFile { ptr = mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset); if (ptr) { if constexpr (is_trivially_copyable::value) { - memcpy(ptr, other.ptr, _size_bytes); + ::memcpy(ptr, other.ptr, _size_bytes); } else { new (ptr) T(*other.ptr); } @@ -792,7 +792,7 @@ class MMapFileBuffer { data = static_cast(mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset)); if (data != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { *data[i] = *other.data[i]; @@ -818,7 +818,7 @@ class MMapFileBuffer { data = static_cast(mmap(BONGOCAT_NULLPTR, _size_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, _offset)); if (data != MAP_FAILED) { if constexpr (is_trivially_copyable::value) { - memcpy(data, other.data, _size_bytes); + ::memcpy(data, other.data, _size_bytes); } else { for (size_t i = 0; i < other.count; i++) { *data[i] = *other.data[i]; diff --git a/lib/nanosvgrast.h b/lib/nanosvgrast.h index 5742ee02..c3304bba 100644 --- a/lib/nanosvgrast.h +++ b/lib/nanosvgrast.h @@ -64,6 +64,10 @@ void nsvgRasterize(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, unsigned char* dst, int w, int h, int stride); +void nsvgRasterizeA(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride, int alpha_threshold); + // Deletes rasterizer context. void nsvgDeleteRasterizer(NSVGrasterizer*); @@ -1209,7 +1213,7 @@ static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, fl } -static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) +static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride, int alpha_threshold) { int x,y; @@ -1218,7 +1222,7 @@ static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int str unsigned char *row = &image[y*stride]; for (x = 0; x < w; x++) { int r = row[0], g = row[1], b = row[2], a = row[3]; - if (a != 0) { + if (a > alpha_threshold) { row[0] = (unsigned char)(r*255/a); row[1] = (unsigned char)(g*255/a); row[2] = (unsigned char)(b*255/a); @@ -1372,9 +1376,9 @@ static void dumpEdges(NSVGrasterizer* r, const char* name) } */ -void nsvgRasterize(NSVGrasterizer* r, +void nsvgRasterizeA(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, - unsigned char* dst, int w, int h, int stride) + unsigned char* dst, int w, int h, int stride, int alpha_threshold) { NSVGshape *shape = NULL; NSVGedge *e = NULL; @@ -1459,7 +1463,7 @@ void nsvgRasterize(NSVGrasterizer* r, } } - nsvg__unpremultiplyAlpha(dst, w, h, stride); + nsvg__unpremultiplyAlpha(dst, w, h, stride, alpha_threshold); r->bitmap = NULL; r->width = 0; @@ -1467,6 +1471,12 @@ void nsvgRasterize(NSVGrasterizer* r, r->stride = 0; } +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride) { + return nsvgRasterizeA(r, image, tx, ty, scale, dst, w, h, stride, 0); +} + #endif // NANOSVGRAST_IMPLEMENTATION #endif // NANOSVGRAST_H \ No newline at end of file diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index bca154fd..34bd13b3 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -488,7 +488,7 @@ created_result_t> create(const config::conf /// @TODO: async assets load // Initialize embedded images/animations if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { - assert(ret->thread_context._local_copy_config.ptr); + assert(ret->thread_context._local_copy_config); // preload assets if constexpr (features::EnableBongocatEmbeddedAssets) { // Load Bongocat @@ -500,15 +500,11 @@ created_result_t> create(const config::conf ctx.shm->bongocat_anims = platform::make_allocated_mmap_array(BONGOCAT_ANIM_COUNT); if constexpr (features::EnableBongocatSvg) { - static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); - static_assert(BONGOCAT_FRAME_HEIGHT > 0); - const int cat_h = ret->thread_context._local_copy_config->cat_height; - const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::SVG, cat_w, cat_h); + load_bongocat_anim_type_t::SVG, anim_bongocat_get_svg_params(ret->thread_context._local_copy_config->cat_height)); } else { init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::PNG, 0, 0); + load_bongocat_anim_type_t::PNG, {0,0,0,0}); } } } diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index 67c177ff..f5c3b9b7 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -13,15 +13,15 @@ namespace bongocat::animation { created_result_t -load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height) { +load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params) { BONGOCAT_LOG_VERBOSE("Load bongocat Animation(index=%d) ...", anim_index); auto [sprite_sheet, sprite_sheet_error] = [&]() { switch (type) { case load_bongocat_anim_type_t::SVG: - assert(target_frame_width >= 0); - assert(target_frame_height >= 0); - return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, target_frame_width, target_frame_height); + assert(svg_params.target_w >= 0); + assert(svg_params.target_h >= 0); + return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, svg_params); case load_bongocat_anim_type_t::PNG: return anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); } @@ -111,7 +111,8 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp } bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, - size_t embedded_images_count, load_bongocat_anim_type_t type, int target_frame_width, int target_frame_height) { + size_t embedded_images_count, load_bongocat_anim_type_t type, + anim_sprite_sheet_from_embedded_svgs_t svg_params) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -120,7 +121,7 @@ bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_in BONGOCAT_LOG_VERBOSE("Load bongocat Animation (%d/%d): %s ...", anim_index, BONGOCAT_ANIM_COUNT, get_sprite(embedded_images_count).name); - auto [sprite_sheet, sprite_sheet_error] = load_bongocat_anim(anim_index, get_sprite, embedded_images_count, type, target_frame_width, target_frame_height); + auto [sprite_sheet, sprite_sheet_error] = load_bongocat_anim(anim_index, get_sprite, embedded_images_count, type, svg_params); if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Load bongocat Animation failed: index: %d", anim_index); return sprite_sheet_error; @@ -151,13 +152,9 @@ created_result_t load_bongocat_sprite_sheet(const anima switch (index) { case BONGOCAT_ANIM_INDEX: if constexpr (features::EnableBongocatSvg) { - static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); - static_assert(BONGOCAT_FRAME_HEIGHT > 0); - const int cat_h = ctx._local_copy_config->cat_height; - const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; - return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, cat_w, cat_h); + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, anim_bongocat_get_svg_params(ctx._local_copy_config->cat_height)); } else { - return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, 0, 0); + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}); } default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; diff --git a/src/image_loader/load_images.cpp b/src/image_loader/load_images.cpp index 1fd8c868..421213dd 100644 --- a/src/image_loader/load_images.cpp +++ b/src/image_loader/load_images.cpp @@ -2,12 +2,12 @@ #ifdef FEATURE_USE_BONGOCAT_SVG #include "image_loader/load_svgs.h" #endif - #include "graphics/animation.h" #include "graphics/animation_thread_context.h" #include "graphics/drawing.h" #include "utils/memory.h" #include +#include namespace bongocat::animation { // ============================================================================= @@ -206,7 +206,7 @@ created_result_t anim_sprite_sheet_from_embedded_images( static_cast(max_channels)); const unsigned char *src_row = src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); - memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); + ::memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); } // update sub-region @@ -273,11 +273,11 @@ BONGOCAT_NODISCARD created_result_t make_image(int width, int height, int static_cast(desired_channels); assert(data_size > 0); - unsigned char* new_pixels = static_cast(::malloc(data_size)); if (new_pixels == BONGOCAT_NULLPTR) { return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } + ::memset(new_pixels, 0, data_size); ret.width = width; ret.height = height; diff --git a/src/image_loader/load_images_hybrid.cpp b/src/image_loader/load_images_hybrid.cpp index a5fe4d3b..1d0e53a4 100644 --- a/src/image_loader/load_images_hybrid.cpp +++ b/src/image_loader/load_images_hybrid.cpp @@ -1,5 +1,4 @@ #include "image_loader/load_images.h" - #include #include #include diff --git a/src/image_loader/load_images_pngle.cpp b/src/image_loader/load_images_pngle.cpp index c2711d6a..c7b8af2d 100644 --- a/src/image_loader/load_images_pngle.cpp +++ b/src/image_loader/load_images_pngle.cpp @@ -1,5 +1,4 @@ #include "image_loader/load_images.h" - #include #include #include diff --git a/src/image_loader/load_images_stb_image.cpp b/src/image_loader/load_images_stb_image.cpp index 6c775363..2c151b62 100644 --- a/src/image_loader/load_images_stb_image.cpp +++ b/src/image_loader/load_images_stb_image.cpp @@ -1,5 +1,4 @@ #include "image_loader/load_images.h" - // include stb_image #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push diff --git a/src/image_loader/load_svgs.cpp b/src/image_loader/load_svgs.cpp index 65e264a7..d7bbb182 100644 --- a/src/image_loader/load_svgs.cpp +++ b/src/image_loader/load_svgs.cpp @@ -2,14 +2,14 @@ #ifdef FEATURE_USE_RASTER_IMAGE_LOADER #include "image_loader/load_images.h" #endif -#include -#include - #include "graphics/animation.h" -#include "graphics/animation_thread_context.h" #include "graphics/drawing.h" #include "utils/memory.h" #include +#include + +#include +#include namespace bongocat::animation { // ============================================================================= @@ -57,7 +57,6 @@ BONGOCAT_NODISCARD created_result_t create_svg_rasterizer() { created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) { BONGOCAT_CHECK_ERROR(params.w < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image width can not be zero nor negative"); BONGOCAT_CHECK_ERROR(params.h < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image height can not be zero nor negative"); - BONGOCAT_CHECK_ERROR(params.stride < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image stride can not be zero nor negative"); auto [image, image_error] = make_image(params.w, params.h, RGBA_CHANNELS); if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { @@ -69,14 +68,18 @@ created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) return svg_raster_error; } assert(svg_raster.image != BONGOCAT_NULLPTR); - ::nsvgRasterize(svg_raster.image, svg.image, params.tx, params.ty, params.scale, image.pixels, params.w, params.h, params.stride); + //assert(image.width >= 0); + //assert(image.height >= 0); + //assert(image.channels >= 0); + //::memset(image.pixels, 0, static_cast(image.width) * static_cast(image.height) * static_cast(image.channels)); + ::nsvgRasterizeA(svg_raster.image, svg.image, params.tx, params.ty, params.scale, image.pixels, params.w, params.h, image.width * image.channels, params.alpha_threshold); return bongocat::move(image); } created_result_t anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, - int target_w, int target_h) { + anim_sprite_sheet_from_embedded_svgs_t svg_params) { generic_sprite_sheet_t ret; int total_frames = 0; @@ -97,8 +100,8 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge bongocat::release_allocated_string(svg_data); svg_data = BONGOCAT_NULLPTR; assert(loaded_svg.image != nullptr); - float svg_w = loaded_svg.image->width; - float svg_h = loaded_svg.image->height; + const float svg_w = loaded_svg.image->width; + const float svg_h = loaded_svg.image->height; if (svg_w <= 0.0f || svg_h <= 0.0f) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to load embedded svg: width/height can not be zero (%fx%f)", static_cast(svg_w), static_cast(svg_h)); continue; @@ -106,14 +109,14 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge assert(svg_w > 0); assert(svg_h > 0); - const float scale = static_cast(target_w) / svg_w; + const float scale = static_cast(svg_params.target_w) / svg_w; auto [loaded_image, image_error] = load_svg_image(loaded_svg, { - .tx = 0, - .ty = 0, + .tx = svg_params.tx, + .ty = svg_params.ty, .scale = scale, - .w = target_w, - .h = target_h, - .stride = target_w * 4, + .w = svg_params.target_w, + .h = svg_params.target_h, + .alpha_threshold = svg_params.alpha_threshold, }); if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to load embedded svg: %s (%d)", img.name, image_error); @@ -163,7 +166,7 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } // reset frames - // memset(anim->pixels.data, 0, anim->pixels.count * sizeof(uint8_t)); + //::memset(ret.image.pixels.data, 0, ret.image.pixels.count * sizeof(uint8_t)); for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { ret.frames[i] = {}; } @@ -185,7 +188,7 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge static_cast(max_channels)); const unsigned char *src_row = src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); - memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); + ::memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); } // update sub-region From 1d62c2d7ee03b6a31e4ac83d2e18888bb5bdc350 Mon Sep 17 00:00:00 2001 From: furudbat Date: Wed, 15 Apr 2026 12:12:00 +0200 Subject: [PATCH 09/17] wip: merge from upstream - replace png bongocat with svg - fix SVG alpha masking (gray) --- include/embedded_assets/bongocat/bongocat.h | 4 +-- .../bongocat/load_images_bongocat.h | 2 +- include/image_loader/load_svgs.h | 5 +-- lib/nanosvgrast.h | 35 ++++++++++++++++--- src/image_loader/load_svgs.cpp | 4 +-- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/include/embedded_assets/bongocat/bongocat.h b/include/embedded_assets/bongocat/bongocat.h index d679c5d9..4d435276 100644 --- a/include/embedded_assets/bongocat/bongocat.h +++ b/include/embedded_assets/bongocat/bongocat.h @@ -4,8 +4,8 @@ #include "core/bongocat.h" #include "embedded_assets/embedded_image.h" #include "graphics/sprite_sheet.h" - #include +#include namespace bongocat::animation { struct animation_thread_context_t; @@ -31,7 +31,7 @@ inline static constexpr int BONGOCAT_SVG_FRAME_WIDTH = 500; inline static constexpr int BONGOCAT_SVG_FRAME_HEIGHT = 500; inline static constexpr int BONGOCAT_SVG_FRAME_TX = 0; inline static constexpr int BONGOCAT_SVG_FRAME_TY = -101; -inline static constexpr int BONGOCAT_SVG_ALPHA_THRESHOLD = 127; +inline static constexpr uint32_t BONGOCAT_SVG_ALPHA_MASK = 0x808080FF; // rgba(128,128,128,255) inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index fca1be4e..509626f0 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -32,7 +32,7 @@ inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int c .target_h = cat_h, .tx = static_cast(BONGOCAT_SVG_FRAME_TX) * scale, .ty = static_cast(BONGOCAT_SVG_FRAME_TY) * scale, - .alpha_threshold = BONGOCAT_SVG_ALPHA_THRESHOLD, + .alpha_mask = BONGOCAT_SVG_ALPHA_MASK, }; } diff --git a/include/image_loader/load_svgs.h b/include/image_loader/load_svgs.h index ee4dad77..86988480 100644 --- a/include/image_loader/load_svgs.h +++ b/include/image_loader/load_svgs.h @@ -13,6 +13,7 @@ #endif #include #include +#include namespace bongocat::animation { // ============================================================================= @@ -38,7 +39,7 @@ struct LoadSvgImageParams { float scale; int w; int h; - int alpha_threshold; + uint32_t alpha_mask{0}; }; BONGOCAT_NODISCARD created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params); #endif @@ -123,7 +124,7 @@ struct anim_sprite_sheet_from_embedded_svgs_t { int target_h; float tx{0.0f}; float ty{0.0f}; - int alpha_threshold{0}; + uint32_t alpha_mask{0}; }; BONGOCAT_NODISCARD created_result_t anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, anim_sprite_sheet_from_embedded_svgs_t svg_params); diff --git a/lib/nanosvgrast.h b/lib/nanosvgrast.h index c3304bba..aa716526 100644 --- a/lib/nanosvgrast.h +++ b/lib/nanosvgrast.h @@ -26,6 +26,7 @@ #define NANOSVGRAST_H #include "nanosvg.h" +#include #ifndef NANOSVGRAST_CPLUSPLUS #ifdef __cplusplus @@ -66,7 +67,7 @@ void nsvgRasterize(NSVGrasterizer* r, void nsvgRasterizeA(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, - unsigned char* dst, int w, int h, int stride, int alpha_threshold); + unsigned char* dst, int w, int h, int stride, uint32_t alpha_threshold); // Deletes rasterizer context. void nsvgDeleteRasterizer(NSVGrasterizer*); @@ -1213,7 +1214,29 @@ static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, fl } -static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride, int alpha_threshold) +static void nsvg__applyAlphaMask(unsigned char* image, int w, int h, int stride, uint32_t mask_color) +{ + // Extract mask RGBA + unsigned char mr = (mask_color >> 24) & 0xFF; + unsigned char mg = (mask_color >> 16) & 0xFF; + unsigned char mb = (mask_color >> 8) & 0xFF; + unsigned char ma = (mask_color) & 0xFF; + + for (int y = 0; y < h; y++) { + unsigned char* row = &image[y * stride]; + for (int x = 0; x < w; x++) { + if (row[0] == mr && row[1] == mg && + row[2] == mb && row[3] == ma) + { + row[3] = 0; + } + + row += 4; + } + } +} + +static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) { int x,y; @@ -1222,7 +1245,7 @@ static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int str unsigned char *row = &image[y*stride]; for (x = 0; x < w; x++) { int r = row[0], g = row[1], b = row[2], a = row[3]; - if (a > alpha_threshold) { + if (a != 0) { row[0] = (unsigned char)(r*255/a); row[1] = (unsigned char)(g*255/a); row[2] = (unsigned char)(b*255/a); @@ -1378,7 +1401,7 @@ static void dumpEdges(NSVGrasterizer* r, const char* name) void nsvgRasterizeA(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, - unsigned char* dst, int w, int h, int stride, int alpha_threshold) + unsigned char* dst, int w, int h, int stride, uint32_t alpha_mask_color) { NSVGshape *shape = NULL; NSVGedge *e = NULL; @@ -1463,7 +1486,9 @@ void nsvgRasterizeA(NSVGrasterizer* r, } } - nsvg__unpremultiplyAlpha(dst, w, h, stride, alpha_threshold); + nsvg__applyAlphaMask(dst, w, h, stride, alpha_mask_color); + + nsvg__unpremultiplyAlpha(dst, w, h, stride); r->bitmap = NULL; r->width = 0; diff --git a/src/image_loader/load_svgs.cpp b/src/image_loader/load_svgs.cpp index d7bbb182..0c4bb4b5 100644 --- a/src/image_loader/load_svgs.cpp +++ b/src/image_loader/load_svgs.cpp @@ -72,7 +72,7 @@ created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) //assert(image.height >= 0); //assert(image.channels >= 0); //::memset(image.pixels, 0, static_cast(image.width) * static_cast(image.height) * static_cast(image.channels)); - ::nsvgRasterizeA(svg_raster.image, svg.image, params.tx, params.ty, params.scale, image.pixels, params.w, params.h, image.width * image.channels, params.alpha_threshold); + ::nsvgRasterizeA(svg_raster.image, svg.image, params.tx, params.ty, params.scale, image.pixels, params.w, params.h, image.width * image.channels, params.alpha_mask); return bongocat::move(image); } @@ -116,7 +116,7 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge .scale = scale, .w = svg_params.target_w, .h = svg_params.target_h, - .alpha_threshold = svg_params.alpha_threshold, + .alpha_mask = svg_params.alpha_mask, }); if (image_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to load embedded svg: %s (%d)", img.name, image_error); From 6d9e6d42ce604c3c4d11746dd29dd5c74179dc08 Mon Sep 17 00:00:00 2001 From: furudbat Date: Wed, 15 Apr 2026 15:03:42 +0200 Subject: [PATCH 10/17] fix: sleeping bongocat * fix build with and without svg --- CMakePresets.json | 8 +- assets/bongo-cat-both-down.png | Bin 18602 -> 18400 bytes assets/bongo-cat-sleeping.png | Bin 0 -> 18602 bytes include/embedded_assets/bongocat/bongocat.h | 2 - .../bongocat/bongocat_images.h | 7 +- include/graphics/sprite_sheet.h | 6 +- include/image_loader/load_svgs.h | 11 - scripts/test_bongocat.sh | 1 + scripts/test_bongocat_12.sh | 268 ++++++++++++++++++ src/embedded_assets/bongocat/CMakeLists.txt | 2 +- .../bongocat/bongocat_get_sprite_sheet.cpp | 2 +- .../bongocat/bongocat_images.c | 9 +- src/graphics/animation.cpp | 3 +- src/graphics/animation_init.cpp | 36 +-- src/graphics/bar.cpp | 3 + src/image_loader/CMakeLists.txt | 3 +- .../bongocat/load_images_bongocat.cpp | 10 +- src/image_loader/load_images.cpp | 2 - src/image_loader/load_svgs.cpp | 7 +- 19 files changed, 325 insertions(+), 55 deletions(-) create mode 100644 assets/bongo-cat-sleeping.png create mode 100755 scripts/test_bongocat_12.sh diff --git a/CMakePresets.json b/CMakePresets.json index 9788f240..ff60687e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -94,7 +94,7 @@ "displayName": "Release (preload assets + svg)", "description": "Release with assets preloaded at the start", "generator": "Ninja", - "binaryDir": "${sourceDir}/cmake-build-release-preload-assets", + "binaryDir": "${sourceDir}/cmake-build-release-preload-assets-svg", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FEATURE_BONGOCAT_EMBEDDED_ASSETS": "ON", @@ -280,7 +280,7 @@ "displayName": "Debug (All Assets + colored sprites + svg, assets lazy load)", "description": "Debug build with ALL assets (full dm versions + colored sprites), lazy load assets on demand", "generator": "Ninja", - "binaryDir": "${sourceDir}/cmake-build-debug-all-assets-lazy-load", + "binaryDir": "${sourceDir}/cmake-build-debug-all-assets-lazy-load-svg", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "ENABLE_ASAN": "ON", @@ -308,7 +308,7 @@ "displayName": "Debug Build", "description": "Debug build with sanitizers", "generator": "Ninja", - "binaryDir": "${sourceDir}/cmake-build-debug", + "binaryDir": "${sourceDir}/cmake-build-debug-svg", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "ENABLE_ASAN": "ON", @@ -321,7 +321,7 @@ "displayName": "Debug Build", "description": "Debug build with sanitizers", "generator": "Ninja", - "binaryDir": "${sourceDir}/cmake-build-debug", + "binaryDir": "${sourceDir}/cmake-build-debug-no-svg", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "ENABLE_ASAN": "ON", diff --git a/assets/bongo-cat-both-down.png b/assets/bongo-cat-both-down.png index 921f84c95ffe2c6337f3c1b7e92ad56485476232..57612cd36f6c337a8f4af3d5f3ad48419280aa3e 100644 GIT binary patch literal 18400 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 get_bongocat_sprite_sheet(const animation::animation_thread_context_t& ctx, int index); -#ifdef FEATURE_USE_BONGOCAT_SVG BONGOCAT_NODISCARD extern embedded_image_t get_bongocat_sprite_svg(size_t i); -#endif } // namespace bongocat::assets #endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_H diff --git a/include/embedded_assets/bongocat/bongocat_images.h b/include/embedded_assets/bongocat/bongocat_images.h index 7f038f18..9898360d 100644 --- a/include/embedded_assets/bongocat/bongocat_images.h +++ b/include/embedded_assets/bongocat/bongocat_images.h @@ -16,7 +16,10 @@ extern const size_t bongo_cat_right_down_png_size; extern const unsigned char bongo_cat_both_down_png[]; extern const size_t bongo_cat_both_down_png_size; -#ifdef FEATURE_USE_BONGOCAT_SVG +extern const unsigned char bongo_cat_sleeping_png[]; +extern const size_t bongo_cat_sleeping_png_size; + + // Embedded asset data (new svg files) extern const unsigned char bongo_cat_both_up_svg[]; extern const size_t bongo_cat_both_up_svg_size; @@ -33,5 +36,3 @@ extern const size_t bongo_cat_both_down_svg_size; extern const unsigned char bongo_cat_sleeping_svg[]; extern const size_t bongo_cat_sleeping_svg_size; #endif - -#endif // BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_IMAGES_H diff --git a/include/graphics/sprite_sheet.h b/include/graphics/sprite_sheet.h index 3323dd57..5c404701 100644 --- a/include/graphics/sprite_sheet.h +++ b/include/graphics/sprite_sheet.h @@ -7,7 +7,7 @@ namespace bongocat::animation { // bongocat: both-up, left-down, right-down, both-down -inline static constexpr size_t BONGOCAT_NUM_FRAMES = 4; +inline static constexpr size_t BONGOCAT_NUM_FRAMES = 5; // dm: Idle 1, Idle 2, Angry, Down1, Happy, Eat1, Sleep1, Refuse, Down2 ~~, Eat2, Sleep2, Attack~~ inline static constexpr size_t MAX_DIGIMON_FRAMES = 16; // pkmn: Idle 1, Idle 2 @@ -17,7 +17,8 @@ inline static constexpr size_t MAX_PKMN_FRAMES = 2; inline static constexpr size_t MAX_NUM_FRAMES = 16; inline static constexpr size_t MAX_ANIMATION_FRAMES = 4; -// @NOTE: MS agents can have more frames per row and are more custom +// @NOTE: MS agents and custom sprite sheets can have more frames per row, this is only the default for embedded sprite +// sheets struct sprite_sheet_animation_frame_t { bool valid{false}; @@ -119,6 +120,7 @@ struct bongocat_sprite_sheet_t { sprite_sheet_animation_frame_t left_down; sprite_sheet_animation_frame_t right_down; sprite_sheet_animation_frame_t both_down; + sprite_sheet_animation_frame_t sleeping; bongocat_sprite_sheet_animations_t animations; }; diff --git a/include/image_loader/load_svgs.h b/include/image_loader/load_svgs.h index 86988480..cfe087f7 100644 --- a/include/image_loader/load_svgs.h +++ b/include/image_loader/load_svgs.h @@ -7,10 +7,8 @@ #include "embedded_assets/embedded_image.h" #include "graphics/sprite_sheet.h" #include "utils/memory.h" -#ifdef FEATURE_USE_RASTER_IMAGE_LOADER #include "load_images.h" #include -#endif #include #include #include @@ -20,15 +18,11 @@ namespace bongocat::animation { // SVG LOADING MODULE // ============================================================================= -constexpr static inline const char* SVG_UNITS = "px"; -constexpr static inline float SVG_DPI = 96.0; - class SvgImage; BONGOCAT_NODISCARD created_result_t load_svg(char *data, const char* units, float dpi); void cleanup_svg(SvgImage& image); void init_svg_loader(); -#ifdef FEATURE_USE_RASTER_IMAGE_LOADER class SvgRasterImage; void cleanup_svg_raster(SvgRasterImage& image); BONGOCAT_NODISCARD created_result_t create_svg_rasterizer(); @@ -42,7 +36,6 @@ struct LoadSvgImageParams { uint32_t alpha_mask{0}; }; BONGOCAT_NODISCARD created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params); -#endif class SvgImage { public: @@ -84,7 +77,6 @@ class SvgImage { } }; -#ifdef FEATURE_USE_RASTER_IMAGE_LOADER class SvgRasterImage { public: NSVGrasterizer *image{BONGOCAT_NULLPTR}; @@ -129,8 +121,5 @@ struct anim_sprite_sheet_from_embedded_svgs_t { BONGOCAT_NODISCARD created_result_t anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, anim_sprite_sheet_from_embedded_svgs_t svg_params); -#endif - } // namespace bongocat::animation - #endif \ No newline at end of file diff --git a/scripts/test_bongocat.sh b/scripts/test_bongocat.sh index 387c849c..0d751b6a 100755 --- a/scripts/test_bongocat.sh +++ b/scripts/test_bongocat.sh @@ -93,6 +93,7 @@ sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 5 + echo "[TEST] Change animation sprite" echo "[INFO] Set animation_name..." sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" diff --git a/scripts/test_bongocat_12.sh b/scripts/test_bongocat_12.sh new file mode 100755 index 00000000..4bae08de --- /dev/null +++ b/scripts/test_bongocat_12.sh @@ -0,0 +1,268 @@ +#!/usr/bin/env bash + +set -euo pipefail + +#make debug +#PROGRAM="./cmake-build-debug-all-assets-colored-preload/bongocat" +PROGRAM="./cmake-build-debug/bongocat-all" +#PROGRAM="./build/bongocat-all" + +WORKDIR=$(mktemp -d) +CONFIG="$WORKDIR/test.bongocat.conf" # config file to modify +OG_CONFIG=./examples/test.bongocat.conf +cp $OG_CONFIG $CONFIG + +if [[ $# -ge 1 ]]; then + PID="$1" + CONFIG="$2" + cp $CONFIG "${CONFIG}.bak" + OG_CONFIG="${CONFIG}.bak" + echo "[TEST] Using provided PID = $PID" +else + echo "[TEST] Starting program..." + "$PROGRAM" --config "$CONFIG" --ignore-running --strict & + PID=$! + echo "[TEST] Program PID = $PID" + sleep 5 +fi + +# --- trap cleanup --- +cleanup() { + echo "[TEST] Cleaning up..." + kill -9 "$PID" 2>/dev/null || true + cp $OG_CONFIG $CONFIG + rm -rf "$WORKDIR" +} +trap cleanup EXIT + +echo "[INFO] Test Program: ${PROGRAM} --config $CONFIG (pid=${PID})" + +echo "[TEST] Sending SIGUSR2..." +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 3 +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 5 +echo "[INFO] Spam SIGUSR2" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +sleep 7 + +# --- function to toggle idle_sleep_timeout --- +toggle_config() { + if grep -q '^idle_sleep_timeout=10' "$CONFIG"; then + new=3600 + else + new=10 + fi + sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=$new/" "$CONFIG" + echo "[TEST] Setting idle_sleep_timeout=$new" +} + +# Test Sleep +sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=0/' "$CONFIG" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=bongocat/' "$CONFIG" +sleep 3 +toggle_config +sleep 10 +toggle_config +sleep 10 +echo "[TEST] Trigger Sleep" +echo "[INFO] Enable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" +sleep 5 +sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 20 +echo "[TEST] Wake up Sleep" +if [[ -f "/proc/$PID/fd/0" ]]; then + printf '\e' > /proc/$PID/fd/0 + sleep 5 +fi +echo "[INFO] Disable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 + +echo "[INFO] Disable sleep" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sleep 10 +if [[ -f "/proc/$PID/fd/0" ]]; then + # --- simulate pressing ESC --- + echo "[TEST] Sending ESC key..." + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Send stdin" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + # a bit of a spam + echo "[INFO] Spam stdin" + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + printf '\e' > /proc/$PID/fd/0 + sleep 5 + echo "[INFO] Spam stdin slower" + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 1 + printf '\e' > /proc/$PID/fd/0 + sleep 3 +fi + +echo "[INFO] Disable sleep" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=bongocat/' "$CONFIG" +echo "[TEST] Sending SIGUSR2..." +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 3 +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" +sleep 5 +echo "[INFO] Spam SIGUSR2" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +kill -USR2 "$PID" +sleep 10 +echo "[INFO] Spam SIGUSR2 slower" +kill -USR2 "$PID" +sleep 5 +kill -USR2 "$PID" +sleep 3 +kill -USR2 "$PID" +sleep 2 +kill -USR2 "$PID" +sleep 15 + +echo "[TEST] Sending SIGUSR1..." +echo "[INFO] Send SIGUSR1" +kill -USR1 "$PID" +sleep 2 +echo "[INFO] Send SIGUSR1" +kill -USR1 "$PID" +sleep 2 + +# Test Sleep + reload in middle +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=0/' "$CONFIG" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=bongocat/' "$CONFIG" +echo "[TEST] Trigger Sleep" +echo "[INFO] Enable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=10/" "$CONFIG" +sed -i 's/^enable_scheduled_sleep=0/enable_scheduled_sleep=1/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 25 +echo "[TEST] Reload config while sleeping Sleep" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=bongocat/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Disable idle_sleep_timeout..." +sed -i -E "s/^idle_sleep_timeout=[0-9]+/idle_sleep_timeout=3600/" "$CONFIG" +sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 + + +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +# Restart - stdin config +echo "[INFO] Restart..." +echo "[INFO] Sending SIGTERM..." +kill -TERM "$PID" +sleep 15 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done +echo "[TEST] Start with stdin config..." +cat "$CONFIG" | "$PROGRAM" --ignore-running --strict --config - & +PID=$! +sleep 10 +sed -i -E 's/^animation_name=.*/animation_name=agumon/' "$CONFIG" +sleep 5 +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + +# Restart - stdin default config +echo "[INFO] Restart..." +echo "[INFO] Sending SIGTERM..." +kill -TERM "$PID" +sleep 15 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done +echo "[TEST] Start with stdin default config..." +cat bongocat.conf.example | "$PROGRAM" --ignore-running --config - & +PID=$! +sleep 10 +# --- verify running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[PASS] Process $PID still running!" +else + echo "[FAIL] Process terminated" + exit 1 +fi + + +# --- send SIGTERM --- +echo "[TEST] Sending SIGTERM..." +kill -TERM "$PID" +sleep 10 +echo "[INFO] Wait for TERM" +# wait up to 5 seconds +for i in {1..5}; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 +done + +# --- verify not running --- +if kill -0 "$PID" 2>/dev/null; then + echo "[FAIL] Process $PID still running!" + kill -9 "$PID" 2>/dev/null + exit 1 +else + echo "[PASS] Process terminated successfully" +fi diff --git a/src/embedded_assets/bongocat/CMakeLists.txt b/src/embedded_assets/bongocat/CMakeLists.txt index 4e2c0427..d374b4b9 100644 --- a/src/embedded_assets/bongocat/CMakeLists.txt +++ b/src/embedded_assets/bongocat/CMakeLists.txt @@ -5,7 +5,7 @@ target_compile_definitions(assets_bongocat_feature INTERFACE FEATURE_BONGOCAT_EM target_compile_definitions(assets_bongocat_feature INTERFACE $<$:FEATURE_USE_BONGOCAT_SVG>) add_library(assets_bongocat STATIC) -target_sources(assets_bongocat PRIVATE bongocat_get_sprite_sheet.cpp bongocat_images.c $<$:bongocat_get_sprite_sheet_svg.cpp>) +target_sources(assets_bongocat PRIVATE bongocat_get_sprite_sheet.cpp bongocat_images.c bongocat_get_sprite_sheet_svg.cpp) target_compile_options(assets_bongocat PRIVATE -ffunction-sections -fdata-sections) target_include_directories(assets_bongocat PUBLIC ${INCLUDE_DIR} diff --git a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp index 0f55fad8..119ebb5c 100644 --- a/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp +++ b/src/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp @@ -15,7 +15,7 @@ embedded_image_t get_bongocat_sprite(size_t i) { case BONGOCAT_FRAME_BOTH_DOWN: return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; case BONGOCAT_FRAME_SLEEPING: - return {bongo_cat_both_down_png, bongo_cat_both_down_png_size, "bongo-cat-both-down"}; + return {bongo_cat_sleeping_png, bongo_cat_sleeping_png_size, "bongo-cat-sleeping"}; default: return {}; } diff --git a/src/embedded_assets/bongocat/bongocat_images.c b/src/embedded_assets/bongocat/bongocat_images.c index 6b5727cf..7a7a817a 100644 --- a/src/embedded_assets/bongocat/bongocat_images.c +++ b/src/embedded_assets/bongocat/bongocat_images.c @@ -23,8 +23,12 @@ const unsigned char bongo_cat_both_down_png[] = { }; const size_t bongo_cat_both_down_png_size = sizeof(bongo_cat_both_down_png); +const unsigned char bongo_cat_sleeping_png[] = { +#embed "../../../assets/bongo-cat-sleeping.png" +}; +const size_t bongo_cat_sleeping_png_size = sizeof(bongo_cat_sleeping_png); + -#ifdef FEATURE_USE_BONGOCAT_SVG // Embedded asset data (svg) const unsigned char bongo_cat_both_up_svg[] = { #embed "../../../assets/new/bongo-both-up.svg" @@ -49,5 +53,4 @@ const size_t bongo_cat_both_down_svg_size = sizeof(bongo_cat_both_down_svg); const unsigned char bongo_cat_sleeping_svg[] = { #embed "../../../assets/new/bongo-sleeping.svg" }; -const size_t bongo_cat_sleeping_svg_size = sizeof(bongo_cat_sleeping_svg); -#endif \ No newline at end of file +const size_t bongo_cat_sleeping_svg_size = sizeof(bongo_cat_sleeping_svg); \ No newline at end of file diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp index 3c03926a..715876e2 100644 --- a/src/graphics/animation.cpp +++ b/src/graphics/animation.cpp @@ -769,7 +769,8 @@ anim_bongocat_idle_next_frame(animation_thread_context_t& ctx, const platform::i // Start Test animation if (!conditions.any_key_pressed && conditions.trigger_test_animation && - current_state.row_state == animation_state_row_t::Idle) { + (current_state.row_state == animation_state_row_t::Idle || + current_state.row_state == animation_state_row_t::IdleSleep)) { anim_bongocat_restart_animation(ctx, input, animation_state_row_t::Test, new_animation_result, new_state, current_state, current_frames); } else if (!conditions.any_key_pressed && conditions.trigger_test_animation && diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index 34bd13b3..d3272849 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -2,7 +2,7 @@ #include "graphics/animation_thread_context.h" #include "platform/wayland.h" #include "utils/memory.h" - +#include "utils/system_error.h" #include #include @@ -42,7 +42,6 @@ #include "image_loader/pen20/load_images_pen20.h" #include "image_loader/pkmn/load_images_pkmn.h" #include "image_loader/pmd/load_images_pmd.h" -#include "utils/system_error.h" namespace bongocat::animation { [[maybe_unused]] static constexpr bool should_load_bongocat([[maybe_unused]] const config::config_t& config) { @@ -285,7 +284,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { case config::config_animation_sprite_sheet_layout_t::None: return none_sprite_sheet; case config::config_animation_sprite_sheet_layout_t::Bongocat: { - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Bongocat); return anim_shm.anim; } @@ -299,7 +298,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { case config::config_animation_dm_set_t::None: return none_sprite_sheet; case config::config_animation_dm_set_t::min_dm: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -308,7 +307,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.min_dm_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::dm: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -316,7 +315,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.dm_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::dm20: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -325,7 +324,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.dm20_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::dmx: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -334,7 +333,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.dmx_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::pen: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -342,7 +341,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.pen_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::pen20: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -351,7 +350,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.pen20_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::dmc: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -360,7 +359,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.dmc_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_dm_set_t::dmall: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Dm); return anim_shm.anim; } @@ -371,7 +370,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { } } break; case config::config_animation_sprite_sheet_layout_t::Pkmn: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Pkmn); return anim_shm.anim; } @@ -380,7 +379,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { ? anim_shm.pkmn_anims[static_cast(anim_index)] : none_sprite_sheet; case config::config_animation_sprite_sheet_layout_t::MsAgent: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::MsAgent); return anim_shm.anim; } @@ -393,7 +392,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { case config::config_animation_custom_set_t::None: break; case config::config_animation_custom_set_t::misc: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Custom); return anim_shm.anim; } @@ -403,7 +402,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { : none_sprite_sheet; break; case config::config_animation_custom_set_t::pmd: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Custom); return anim_shm.anim; } @@ -413,7 +412,7 @@ animation_t& get_current_animation(animation_thread_context_t& ctx) { : none_sprite_sheet; break; case config::config_animation_custom_set_t::custom: - if (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets) { assert(anim_shm.anim.type == animation_t::type_t::Custom); return anim_shm.anim; } @@ -501,10 +500,11 @@ created_result_t> create(const config::conf if constexpr (features::EnableBongocatSvg) { init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::SVG, anim_bongocat_get_svg_params(ret->thread_context._local_copy_config->cat_height)); + load_bongocat_anim_type_t::SVG, + anim_bongocat_get_svg_params(ret->thread_context._local_copy_config->cat_height)); } else { init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::PNG, {0,0,0,0}); + load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}); } } } diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index 42f5295e..6144786b 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -93,6 +93,9 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w case BONGOCAT_FRAME_BOTH_DOWN: region = &sheet.both_down; break; + case BONGOCAT_FRAME_SLEEPING: + region = &sheet.sleeping; + break; default: assert(anim_shm.animation_player_result.sprite_sheet_col >= 0 && static_cast(anim_shm.animation_player_result.sprite_sheet_col) < BONGOCAT_SPRITE_SHEET_COLS); diff --git a/src/image_loader/CMakeLists.txt b/src/image_loader/CMakeLists.txt index 4acfeead..bad39241 100644 --- a/src/image_loader/CMakeLists.txt +++ b/src/image_loader/CMakeLists.txt @@ -4,7 +4,7 @@ target_include_directories( assets_image_loader PRIVATE ${INCLUDE_DIR}/image_loader PUBLIC ${INCLUDE_DIR}) -target_include_directories(assets_image_loader SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/lib) +target_include_directories(assets_image_loader SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/lib) target_link_libraries(assets_image_loader PRIVATE bongocat_options) if(FEATURE_USE_HYBRID_IMAGE_BACKEND) @@ -57,7 +57,6 @@ if (FEATURE_USE_BONGOCAT_SVG) target_sources(nanosvg PRIVATE nanosvg.c) target_sources(assets_image_loader PRIVATE load_svgs.cpp) - target_include_directories(assets_image_loader SYSTEM PUBLIC ${PROJECT_SOURCE_DIR}/lib) target_link_libraries(assets_image_loader PRIVATE nanosvg) target_compile_definitions(assets_image_loader PUBLIC FEATURE_USE_NANOSVG) message(STATUS "Use nanosvg backend") diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index f5c3b9b7..eb3b65da 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -7,7 +7,6 @@ #include "image_loader/load_images.h" #include "image_loader/load_svgs.h" #include "utils/memory.h" - #include namespace bongocat::animation { @@ -21,7 +20,12 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp case load_bongocat_anim_type_t::SVG: assert(svg_params.target_w >= 0); assert(svg_params.target_h >= 0); +#ifdef FEATURE_USE_BONGOCAT_SVG return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, svg_params); +#else + BONGOCAT_LOG_WARNING("load_bongocat_anim: SVG not supported"); + break; +#endif case load_bongocat_anim_type_t::PNG: return anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); } @@ -41,6 +45,7 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp ret.left_down = bongocat::move(sprite_sheet.frames[1]); ret.right_down = bongocat::move(sprite_sheet.frames[2]); ret.both_down = bongocat::move(sprite_sheet.frames[3]); + ret.sleeping = bongocat::move(sprite_sheet.frames[4]); sprite_sheet = {}; // generic sprite has been moved // setup animations (cache) @@ -50,8 +55,9 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp assert(ret.left_down.valid); assert(ret.right_down.valid); assert(ret.both_down.valid); + assert(ret.sleeping.valid); - assert(MAX_ANIMATION_FRAMES >= 4); + static_assert(MAX_ANIMATION_FRAMES >= 4); ret.animations.idle[0] = BONGOCAT_FRAME_BOTH_UP; ret.animations.idle[1] = BONGOCAT_FRAME_BOTH_UP; ret.animations.idle[2] = BONGOCAT_FRAME_BOTH_UP; diff --git a/src/image_loader/load_images.cpp b/src/image_loader/load_images.cpp index 421213dd..28013312 100644 --- a/src/image_loader/load_images.cpp +++ b/src/image_loader/load_images.cpp @@ -1,7 +1,5 @@ #include "image_loader/load_images.h" -#ifdef FEATURE_USE_BONGOCAT_SVG #include "image_loader/load_svgs.h" -#endif #include "graphics/animation.h" #include "graphics/animation_thread_context.h" #include "graphics/drawing.h" diff --git a/src/image_loader/load_svgs.cpp b/src/image_loader/load_svgs.cpp index 0c4bb4b5..309b3a0c 100644 --- a/src/image_loader/load_svgs.cpp +++ b/src/image_loader/load_svgs.cpp @@ -1,7 +1,5 @@ #include "image_loader/load_svgs.h" -#ifdef FEATURE_USE_RASTER_IMAGE_LOADER #include "image_loader/load_images.h" -#endif #include "graphics/animation.h" #include "graphics/drawing.h" #include "utils/memory.h" @@ -16,6 +14,9 @@ namespace bongocat::animation { // SVG LOADING MODULE // ============================================================================= +constexpr static inline const char* SVG_UNITS = "px"; +constexpr static inline float SVG_DPI = 96.0; + void init_svg_loader() {} void cleanup_svg(SvgImage& image) { @@ -44,7 +45,6 @@ void cleanup_svg_raster(SvgRasterImage& image) { image.image = BONGOCAT_NULLPTR; } - BONGOCAT_NODISCARD created_result_t create_svg_rasterizer() { SvgRasterImage ret; if (ret.image == BONGOCAT_NULLPTR) [[unlikely]] { @@ -54,6 +54,7 @@ BONGOCAT_NODISCARD created_result_t create_svg_rasterizer() { return ret; } + created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) { BONGOCAT_CHECK_ERROR(params.w < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image width can not be zero nor negative"); BONGOCAT_CHECK_ERROR(params.h < 0, bongocat_error_t::BONGOCAT_ERROR_SVG, "svg image height can not be zero nor negative"); From a7690534924e31c1d2ec95b3fcc3d1198502cc1d Mon Sep 17 00:00:00 2001 From: furudbat Date: Wed, 15 Apr 2026 18:59:13 +0200 Subject: [PATCH 11/17] fix: bongocat svg size * crop white space --- include/embedded_assets/bongocat/bongocat.h | 4 +- .../bongocat/load_images_bongocat.h | 41 ++++++- include/image_loader/load_svgs.h | 8 +- scripts/test_bongocat_12.sh | 32 ++++++ src/graphics/animation_init.cpp | 2 +- .../bongocat/load_images_bongocat.cpp | 24 ++-- src/image_loader/load_images.cpp | 19 +--- src/image_loader/load_svgs.cpp | 106 +++++++++++++----- 8 files changed, 179 insertions(+), 57 deletions(-) diff --git a/include/embedded_assets/bongocat/bongocat.h b/include/embedded_assets/bongocat/bongocat.h index fdb73467..9236ea5e 100644 --- a/include/embedded_assets/bongocat/bongocat.h +++ b/include/embedded_assets/bongocat/bongocat.h @@ -30,8 +30,10 @@ inline static constexpr int BONGOCAT_FRAME_HEIGHT = 360; inline static constexpr int BONGOCAT_SVG_FRAME_WIDTH = 500; inline static constexpr int BONGOCAT_SVG_FRAME_HEIGHT = 500; inline static constexpr int BONGOCAT_SVG_FRAME_TX = 0; -inline static constexpr int BONGOCAT_SVG_FRAME_TY = -101; +inline static constexpr int BONGOCAT_SVG_FRAME_TY = 0; inline static constexpr uint32_t BONGOCAT_SVG_ALPHA_MASK = 0x808080FF; // rgba(128,128,128,255) +inline static constexpr int BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH = 390; +inline static constexpr int BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT = 216; inline static constexpr size_t BONGOCAT_EMBEDDED_IMAGES_COUNT = animation::BONGOCAT_NUM_FRAMES; inline static constexpr size_t BONGOCAT_ANIMATIONS_COUNT = 1; diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index 509626f0..0d14c3fc 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -14,15 +14,28 @@ enum class load_bongocat_anim_type_t : uint8_t { SVG, }; BONGOCAT_NODISCARD created_result_t -load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params); +load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, + load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params, + anim_sprite_sheet_from_embedded_svgs_cropping_t cropping); bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params); + inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int cat_height) { using namespace assets; static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); static_assert(BONGOCAT_FRAME_HEIGHT > 0); - const int cat_h = cat_height; + + //constexpr auto pad_x = + // (BONGOCAT_SVG_FRAME_WIDTH - BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH) / 2; + constexpr auto pad_y = + (BONGOCAT_SVG_FRAME_HEIGHT - BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT) / 2; + constexpr auto top = (pad_y + BONGOCAT_SVG_FRAME_TY); + constexpr auto bottom = (pad_y - BONGOCAT_SVG_FRAME_TY); + + // target_width must have bottom so the end sprite will end up in the exact height of cat_height + // frame_height must be the same as cat_height, so the scaling didn't get messed up + const int cat_h = cat_height + bottom + top; const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; assert(cat_h > 0); const float scale = static_cast(cat_h) / static_cast(BONGOCAT_SVG_FRAME_HEIGHT); @@ -35,6 +48,30 @@ inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int c .alpha_mask = BONGOCAT_SVG_ALPHA_MASK, }; } +inline anim_sprite_sheet_from_embedded_svgs_cropping_t anim_bongocat_get_svg_cropping(int cat_height) { + using namespace assets; + static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_FRAME_HEIGHT > 0); + + constexpr auto pad_x = + (BONGOCAT_SVG_FRAME_WIDTH - BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH) / 2; + constexpr auto pad_y = + (BONGOCAT_SVG_FRAME_HEIGHT - BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT) / 2; + constexpr auto top = (pad_y + BONGOCAT_SVG_FRAME_TY); + constexpr auto bottom = (pad_y - BONGOCAT_SVG_FRAME_TY); + + const int cat_h = cat_height + bottom + top; + //const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; + assert(cat_h > 0); + const float scale = static_cast(cat_h) / static_cast(BONGOCAT_SVG_FRAME_HEIGHT); + + return { + .left = static_cast((pad_x - BONGOCAT_SVG_FRAME_TX) * scale), + .right = static_cast((pad_x + BONGOCAT_SVG_FRAME_TX) * scale), + .top = static_cast(top * scale), + .bottom = static_cast(bottom * scale), + }; +} BONGOCAT_NODISCARD created_result_t load_bongocat_sprite_sheet(const animation_thread_context_t& /*ctx*/, int index); diff --git a/include/image_loader/load_svgs.h b/include/image_loader/load_svgs.h index cfe087f7..e90e9a4a 100644 --- a/include/image_loader/load_svgs.h +++ b/include/image_loader/load_svgs.h @@ -118,8 +118,14 @@ struct anim_sprite_sheet_from_embedded_svgs_t { float ty{0.0f}; uint32_t alpha_mask{0}; }; +struct anim_sprite_sheet_from_embedded_svgs_cropping_t { + int left{0}; + int right{0}; + int top{0}; + int bottom{0}; +}; BONGOCAT_NODISCARD created_result_t -anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, anim_sprite_sheet_from_embedded_svgs_t svg_params); +anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, anim_sprite_sheet_from_embedded_svgs_t svg_params, anim_sprite_sheet_from_embedded_svgs_cropping_t cropping = {}); } // namespace bongocat::animation #endif \ No newline at end of file diff --git a/scripts/test_bongocat_12.sh b/scripts/test_bongocat_12.sh index 4bae08de..7f6fa89d 100755 --- a/scripts/test_bongocat_12.sh +++ b/scripts/test_bongocat_12.sh @@ -62,6 +62,15 @@ toggle_config() { echo "[TEST] Setting idle_sleep_timeout=$new" } +sed -i -E 's/^overlay_position=[a-zA-Z]+/overlay_position=top/' "$CONFIG" +sed -i -E 's/^overlay_opacity=[0-9]+/overlay_opacity=128/' "$CONFIG" +sed -i -E 's/^cat_x_offset=[0-9]+/cat_x_offset=0/' "$CONFIG" +sed -i -E 's/^cat_y_offset=[0-9]+/cat_y_offset=0/' "$CONFIG" +sed -i -E 's/^cat_align=[a-zA-Z]+/cat_align=center/' "$CONFIG" +sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" +sed -i -E 's/^cat_height=[0-9]+/cat_align=60/' "$CONFIG" +sed -i -E 's/^overlay_height=[0-9]+/overlay_height=80/' "$CONFIG" + # Test Sleep sed -i -E 's/^cpu_threshold=[0-9]+/cpu_threshold=0/' "$CONFIG" sed -i 's/^enable_scheduled_sleep=1/enable_scheduled_sleep=0/' "$CONFIG" @@ -183,6 +192,29 @@ echo "[INFO] Send SIGUSR2" kill -USR2 "$PID" # Reload config sleep 5 +echo "[TEST] Scale Height" +sed -i -E 's/^invert_color=[0-9]+/invert_color=0/' "$CONFIG" +sed -i -E 's/^animation_name=.*/animation_name=bongocat/' "$CONFIG" +echo "[INFO] Normal height..." +sed -i -E "s/^cat_height=[0-9]+/cat_height=500/" "$CONFIG" +sed -i -E "s/^overlay_height=[0-9]+/overlay_height=500/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Big height..." +sed -i -E "s/^cat_height=[0-9]+/cat_height=1080/" "$CONFIG" +sed -i -E "s/^overlay_height=[0-9]+/overlay_height=1080/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Small height..." +sed -i -E "s/^cat_height=[0-9]+/cat_height=32/" "$CONFIG" +sed -i -E "s/^overlay_height=[0-9]+/overlay_height=32/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 + + # --- verify running --- if kill -0 "$PID" 2>/dev/null; then diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index d3272849..30c3c766 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -480,7 +480,7 @@ created_result_t> create(const config::conf [[maybe_unused]] const auto t0 = platform::get_current_time_us(); // Load embedded images/animations - if constexpr (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets || features::EnableBongocatSvg) { hot_load_animation(ret->thread_context); } diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index eb3b65da..f4e69d8c 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -12,20 +12,23 @@ namespace bongocat::animation { created_result_t -load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params) { +load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, + load_bongocat_anim_type_t type, + anim_sprite_sheet_from_embedded_svgs_t svg_params, anim_sprite_sheet_from_embedded_svgs_cropping_t cropping) { BONGOCAT_LOG_VERBOSE("Load bongocat Animation(index=%d) ...", anim_index); auto [sprite_sheet, sprite_sheet_error] = [&]() { switch (type) { - case load_bongocat_anim_type_t::SVG: + case load_bongocat_anim_type_t::SVG:{ assert(svg_params.target_w >= 0); assert(svg_params.target_h >= 0); #ifdef FEATURE_USE_BONGOCAT_SVG - return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, svg_params); + return anim_sprite_sheet_from_embedded_svgs(get_sprite, embedded_images_count, svg_params, cropping); #else - BONGOCAT_LOG_WARNING("load_bongocat_anim: SVG not supported"); - break; + BONGOCAT_LOG_WARNING("load_bongocat_anim: SVG not supported"); + break; #endif + } case load_bongocat_anim_type_t::PNG: return anim_sprite_sheet_from_embedded_images(get_sprite, embedded_images_count); } @@ -118,7 +121,8 @@ load_bongocat_anim([[maybe_unused]] int anim_index, get_sprite_callback_t get_sp bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, size_t embedded_images_count, load_bongocat_anim_type_t type, - anim_sprite_sheet_from_embedded_svgs_t svg_params) { + anim_sprite_sheet_from_embedded_svgs_t svg_params, + anim_sprite_sheet_from_embedded_svgs_cropping_t cropping) { using namespace assets; BONGOCAT_CHECK_NULL(ctx.shm.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); BONGOCAT_CHECK_NULL(ctx._local_copy_config.ptr, bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM); @@ -127,7 +131,7 @@ bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_in BONGOCAT_LOG_VERBOSE("Load bongocat Animation (%d/%d): %s ...", anim_index, BONGOCAT_ANIM_COUNT, get_sprite(embedded_images_count).name); - auto [sprite_sheet, sprite_sheet_error] = load_bongocat_anim(anim_index, get_sprite, embedded_images_count, type, svg_params); + auto [sprite_sheet, sprite_sheet_error] = load_bongocat_anim(anim_index, get_sprite, embedded_images_count, type, svg_params, cropping); if (sprite_sheet_error != bongocat_error_t::BONGOCAT_SUCCESS) [[unlikely]] { BONGOCAT_LOG_ERROR("Load bongocat Animation failed: index: %d", anim_index); return sprite_sheet_error; @@ -158,9 +162,11 @@ created_result_t load_bongocat_sprite_sheet(const anima switch (index) { case BONGOCAT_ANIM_INDEX: if constexpr (features::EnableBongocatSvg) { - return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, anim_bongocat_get_svg_params(ctx._local_copy_config->cat_height)); + const auto svg_params = anim_bongocat_get_svg_params(ctx._local_copy_config->cat_height); + const auto svg_cropping = anim_bongocat_get_svg_cropping(ctx._local_copy_config->cat_height); + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, svg_params, svg_cropping); } else { - return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}); + return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}, {0, 0, 0, 0}); } default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; diff --git a/src/image_loader/load_images.cpp b/src/image_loader/load_images.cpp index 28013312..3b8bdc6b 100644 --- a/src/image_loader/load_images.cpp +++ b/src/image_loader/load_images.cpp @@ -6,6 +6,7 @@ #include "utils/memory.h" #include #include +#include namespace bongocat::animation { // ============================================================================= @@ -51,7 +52,7 @@ load_sprite_sheet_from_memory(const uint8_t *sprite_data, size_t sprite_data_siz BONGOCAT_LOG_ERROR("Failed to allocate memory for dest_pixels (%zu bytes)\n", dest_pixels_size); return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } - // memset(dest_pixels.data, 0, dest_pixels_size); + ::memset(dest_pixels.data, 0, dest_pixels_size); const auto src_frame_width = frame_width; const auto src_frame_height = frame_height; @@ -173,19 +174,15 @@ created_result_t anim_sprite_sheet_from_embedded_images( ret.image.sprite_sheet_height = 0; ret.image.channels = 0; - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { - ::free(loaded_images[i].pixels); - loaded_images[i].pixels = BONGOCAT_NULLPTR; - } - } return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } + // reset frames - // memset(anim->pixels.data, 0, anim->pixels.count * sizeof(uint8_t)); + ::memset(ret.image.pixels.data, 0, ret.image.pixels._size_bytes); for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { ret.frames[i] = {}; } + // append images into one sprite sheet assert(max_frame_width >= 0); assert(max_channels >= 0); @@ -218,12 +215,6 @@ created_result_t anim_sprite_sheet_from_embedded_images( } } - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { - ::free(loaded_images[i].pixels); - loaded_images[i].pixels = BONGOCAT_NULLPTR; - } - } return ret; } diff --git a/src/image_loader/load_svgs.cpp b/src/image_loader/load_svgs.cpp index 309b3a0c..a83d220b 100644 --- a/src/image_loader/load_svgs.cpp +++ b/src/image_loader/load_svgs.cpp @@ -4,7 +4,9 @@ #include "graphics/drawing.h" #include "utils/memory.h" #include +#include #include +#include #include #include @@ -80,7 +82,8 @@ created_result_t load_svg_image(SvgImage& svg, LoadSvgImageParams params) created_result_t anim_sprite_sheet_from_embedded_svgs(get_sprite_callback_t get_sprite, size_t embedded_images_count, - anim_sprite_sheet_from_embedded_svgs_t svg_params) { + anim_sprite_sheet_from_embedded_svgs_t svg_params, + anim_sprite_sheet_from_embedded_svgs_cropping_t cropping) { generic_sprite_sheet_t ret; int total_frames = 0; @@ -137,6 +140,15 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge total_frames++; } + // prepare sprite sheet size + int cropped_w = max_frame_width - cropping.left - cropping.right; + int cropped_h = max_frame_height - cropping.top - cropping.bottom; + if (cropped_w < 0) cropped_w = 0; + if (cropped_h < 0) cropped_h = 0; + max_frame_width = cropped_w; + max_frame_height = cropped_h; + assert(max_frame_width >= 0); + assert(max_frame_height >= 0); ret.frame_width = max_frame_width; ret.frame_height = max_frame_height; ret.total_frames = total_frames; @@ -147,6 +159,17 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge assert(ret.image.sprite_sheet_width >= 0); assert(ret.image.sprite_sheet_height >= 0); assert(ret.image.channels >= 0); + if (ret.image.sprite_sheet_width == 0 || ret.image.sprite_sheet_height == 0 || ret.image.channels == 0) { + ret.frame_width = 0; + ret.frame_height = 0; + ret.total_frames = 0; + ret.image.sprite_sheet_width = 0; + ret.image.sprite_sheet_height = 0; + ret.image.channels = 0; + + return bongocat_error_t::BONGOCAT_ERROR_SVG; + } + ret.image.pixels = make_allocated_array(static_cast(ret.image.sprite_sheet_width) * static_cast(ret.image.sprite_sheet_height) * static_cast(ret.image.channels)); @@ -158,57 +181,82 @@ created_result_t anim_sprite_sheet_from_embedded_svgs(ge ret.image.sprite_sheet_height = 0; ret.image.channels = 0; - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { - ::free(loaded_images[i].pixels); - loaded_images[i].pixels = BONGOCAT_NULLPTR; - } - } return bongocat_error_t::BONGOCAT_ERROR_MEMORY; } + // reset frames - //::memset(ret.image.pixels.data, 0, ret.image.pixels.count * sizeof(uint8_t)); + ::memset(ret.image.pixels.data, 0, ret.image.pixels._size_bytes); for (size_t i = 0; i < MAX_NUM_FRAMES; i++) { ret.frames[i] = {}; } + // append images into one sprite sheet assert(max_frame_width >= 0); assert(max_channels >= 0); assert(ret.image.sprite_sheet_width >= 0); for (size_t frame = 0; frame < loaded_images.count; frame++) { const auto& src = loaded_images[frame]; - assert(src.channels >= 0); - assert(src.width >= 0); - assert(src.height >= 0); + if (src.pixels != BONGOCAT_NULLPTR && src.height > 0) { - // copy pixel data of sub-region - assert(src.height >= 0); - for (size_t y = 0; y < static_cast(src.height); y++) { - unsigned char *dest_row = ret.image.pixels.data + (((y * static_cast(ret.image.sprite_sheet_width)) + - (frame * static_cast(max_frame_width))) * - static_cast(max_channels)); - const unsigned char *src_row = - src.pixels + (y * static_cast(src.width) * static_cast(src.channels)); - ::memcpy(dest_row, src_row, static_cast(src.width) * static_cast(max_channels)); + const int s_cropped_w = src.width - cropping.left - cropping.right; + const int s_cropped_h = src.height - cropping.top - cropping.bottom; + if (s_cropped_w <= 0 || s_cropped_h <= 0) { + continue; + } + + int dy = 0; + int end_y = cropping.top + s_cropped_h; + if (end_y > src.height) end_y = src.height; + for (int y = cropping.top; y < end_y; y++) { + assert(dy >= 0); + unsigned char* dest_row = + ret.image.pixels.data + + (((static_cast(dy) * static_cast(ret.image.sprite_sheet_width)) + + (frame * static_cast(max_frame_width))) * + static_cast(max_channels)); + + if (y < src.height) { + const unsigned char* src_row = + src.pixels + + static_cast(((y * src.width) + cropping.left) * src.channels); + + assert(ret.image.sprite_sheet_height >= 0); + if (dy < ret.image.sprite_sheet_height) { + assert(s_cropped_w >= 0); + int line_w = s_cropped_w; + if (line_w >= max_frame_width) line_w = max_frame_width; + if (line_w >= src.width) line_w = src.width; + if (line_w >= ret.image.sprite_sheet_width) line_w = ret.image.sprite_sheet_width; + + assert(max_channels == src.channels); + assert(max_channels == ret.image.channels); + ::memcpy(dest_row, + src_row, + static_cast(line_w) * static_cast(max_channels)); + } + } + + dy++; } - // update sub-region if (frame < MAX_NUM_FRAMES) { - ret.frames[frame] = {.valid = true, .col = static_cast(frame), .row = 0}; + ret.frames[frame] = { + .valid = true, + .col = static_cast(frame), + .row = 0 + }; } } else { if (frame < MAX_NUM_FRAMES) { - ret.frames[frame] = {.valid = false, .col = static_cast(frame), .row = 0}; + ret.frames[frame] = { + .valid = false, + .col = static_cast(frame), + .row = 0 + }; } } } - for (size_t i = 0; i < loaded_images.count; i++) { - if (loaded_images[i].pixels != BONGOCAT_NULLPTR) { - ::free(loaded_images[i].pixels); - loaded_images[i].pixels = BONGOCAT_NULLPTR; - } - } return ret; } From ac3a7643b54c2b58f97ddaf0ea8911d7c134c507 Mon Sep 17 00:00:00 2001 From: furudbat Date: Thu, 16 Apr 2026 12:30:38 +0200 Subject: [PATCH 12/17] fix: bongocat svg size * crop white space --- include/core/bongocat.h | 24 -------- .../bongocat/load_images_bongocat.h | 55 +++++++++---------- src/graphics/bar.cpp | 43 +++++++++++---- 3 files changed, 57 insertions(+), 65 deletions(-) diff --git a/include/core/bongocat.h b/include/core/bongocat.h index 4d111163..208bf2e7 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -53,18 +53,6 @@ namespace features { inline static constexpr bool Debug = false; #endif -#ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS - inline static constexpr bool EnableBongocatEmbeddedAssets = true; -# ifdef FEATURE_USE_BONGOCAT_SVG - inline static constexpr bool EnableBongocatSvg = true; -# else - inline static constexpr bool EnableBongocatSvg = false; -# endif -#else - inline static constexpr bool EnableBongocatEmbeddedAssets = false; - inline static constexpr bool EnableBongocatSvg = false; -#endif - #ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS inline static constexpr bool EnableDmEmbeddedAssets = true; # ifdef FEATURE_DM_EMBEDDED_ASSETS @@ -121,12 +109,6 @@ namespace features { inline static constexpr bool EnableDmAllEmbeddedAssets = false; #endif -#ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS - inline static constexpr bool EnableMsAgentEmbeddedAssets = true; -#else - inline static constexpr bool EnableMsAgentEmbeddedAssets = false; -#endif - #ifdef FEATURE_PKMN_EMBEDDED_ASSETS inline static constexpr bool EnablePkmnEmbeddedAssets = true; #else @@ -139,12 +121,6 @@ namespace features { inline static constexpr bool EnablePmdEmbeddedAssets = false; #endif -#ifdef FEATURE_MISC_EMBEDDED_ASSETS - inline static constexpr bool EnableMiscEmbeddedAssets = true; -#else - inline static constexpr bool EnableMiscEmbeddedAssets = false; -#endif - #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) inline static constexpr bool EnableMemoryStatistics = true; #else diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index 0d14c3fc..232b5c24 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -24,25 +24,22 @@ bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_in inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int cat_height) { using namespace assets; static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); - static_assert(BONGOCAT_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT > 0); - //constexpr auto pad_x = - // (BONGOCAT_SVG_FRAME_WIDTH - BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH) / 2; - constexpr auto pad_y = - (BONGOCAT_SVG_FRAME_HEIGHT - BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT) / 2; - constexpr auto top = (pad_y + BONGOCAT_SVG_FRAME_TY); - constexpr auto bottom = (pad_y - BONGOCAT_SVG_FRAME_TY); + // scale so that the _visible cat_ becomes cat_height + const float scale = + static_cast(cat_height) / + static_cast(BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT); - // target_width must have bottom so the end sprite will end up in the exact height of cat_height - // frame_height must be the same as cat_height, so the scaling didn't get messed up - const int cat_h = cat_height + bottom + top; - const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; - assert(cat_h > 0); - const float scale = static_cast(cat_h) / static_cast(BONGOCAT_SVG_FRAME_HEIGHT); + const int target_w = static_cast( + BONGOCAT_SVG_FRAME_WIDTH * scale); + + const int target_h = static_cast( + BONGOCAT_SVG_FRAME_HEIGHT * scale); return { - .target_w = cat_w, - .target_h = cat_h, + .target_w = target_w, + .target_h = target_h, .tx = static_cast(BONGOCAT_SVG_FRAME_TX) * scale, .ty = static_cast(BONGOCAT_SVG_FRAME_TY) * scale, .alpha_mask = BONGOCAT_SVG_ALPHA_MASK, @@ -51,25 +48,23 @@ inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int c inline anim_sprite_sheet_from_embedded_svgs_cropping_t anim_bongocat_get_svg_cropping(int cat_height) { using namespace assets; static_assert(BONGOCAT_SVG_FRAME_HEIGHT > 0); - static_assert(BONGOCAT_FRAME_HEIGHT > 0); + static_assert(BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT > 0); - constexpr auto pad_x = - (BONGOCAT_SVG_FRAME_WIDTH - BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH) / 2; - constexpr auto pad_y = - (BONGOCAT_SVG_FRAME_HEIGHT - BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT) / 2; - constexpr auto top = (pad_y + BONGOCAT_SVG_FRAME_TY); - constexpr auto bottom = (pad_y - BONGOCAT_SVG_FRAME_TY); + // cropping must scale with _visible cat_ + const float scale = + static_cast(cat_height) / + static_cast(BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT); - const int cat_h = cat_height + bottom + top; - //const int cat_w = (cat_h * BONGOCAT_SVG_FRAME_WIDTH) / BONGOCAT_SVG_FRAME_HEIGHT; - assert(cat_h > 0); - const float scale = static_cast(cat_h) / static_cast(BONGOCAT_SVG_FRAME_HEIGHT); + const float pad_x = + (BONGOCAT_SVG_FRAME_WIDTH - BONGOCAT_SVG_FRAME_REAL_CAT_WIDTH) / 2.0f; + const float pad_y = + (BONGOCAT_SVG_FRAME_HEIGHT - BONGOCAT_SVG_FRAME_REAL_CAT_HEIGHT) / 2.0f; return { - .left = static_cast((pad_x - BONGOCAT_SVG_FRAME_TX) * scale), - .right = static_cast((pad_x + BONGOCAT_SVG_FRAME_TX) * scale), - .top = static_cast(top * scale), - .bottom = static_cast(bottom * scale), + .left = static_cast(pad_x * scale), + .right = static_cast(pad_x * scale), + .top = static_cast(pad_y * scale), + .bottom = static_cast(pad_y * scale), }; } diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index 6144786b..5dc4bb37 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -29,13 +29,31 @@ struct cat_rect_t { int height; }; +enum class blit_image_sprite_option_flags_t : uint32_t { + None = (1u << 0), + IgnoreCatHeight = (1u << 1), // use frame_height +}; + template /// @TODO: required SpriteSheet must be _sprite_sheet_t cat_rect_t get_position(const platform::wayland::wayland_thread_context& wayland_ctx, const SpriteSheet& sheet, - const config::config_t& config) { - const int cat_height = config.cat_height; - const int cat_width = static_cast(static_cast(cat_height) * (static_cast(sheet.frame_width) / + const config::config_t& config, + blit_image_sprite_option_flags_t options) { + const int cat_height = [&]() { + if (has_flag(options, blit_image_sprite_option_flags_t::IgnoreCatHeight)) { + return sheet.frame_height; + } + + return config.cat_height; + }(); + const int cat_width = [&]() { + if (has_flag(options, blit_image_sprite_option_flags_t::IgnoreCatHeight)) { + return sheet.frame_width; + } + + return static_cast(static_cast(cat_height) * (static_cast(sheet.frame_width) / static_cast(sheet.frame_height))); + }(); int cat_x = 0; switch (config.cat_align) { @@ -60,7 +78,8 @@ cat_rect_t get_position(const platform::wayland::wayland_thread_context& wayland /// @TODO: make draw_sprite more generic (template?) void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const bongocat_sprite_sheet_t& sheet, - blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal) { + blit_image_color_option_flags_t extra_drawing_option = blit_image_color_option_flags_t::Normal, + blit_image_sprite_option_flags_t sprite_options = blit_image_sprite_option_flags_t::None) { using namespace assets; if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { return; @@ -102,7 +121,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, sprite_options); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); if (region != BONGOCAT_NULLPTR) { @@ -261,7 +280,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); if (region != BONGOCAT_NULLPTR) { @@ -375,7 +394,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); if (region != BONGOCAT_NULLPTR) { @@ -472,7 +491,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w uint8_t *pixels = shm_buffer.pixels.data; const size_t pixels_size = shm_buffer.pixels._size_bytes; - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; if (current_config.invert_color >= 1) { @@ -503,7 +522,8 @@ enum class draw_sprite_overwrite_option_t : uint32_t { }; void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::wayland_shm_buffer_t& shm_buffer, const custom_sprite_sheet_t& sheet, int col, int row, - draw_sprite_overwrite_option_t overwrite_option = draw_sprite_overwrite_option_t::None) { + draw_sprite_overwrite_option_t overwrite_option = draw_sprite_overwrite_option_t::None, + blit_image_sprite_option_flags_t sprite_options = blit_image_sprite_option_flags_t::None) { if (sheet.frame_width <= 0 || sheet.frame_height <= 0) { return; } @@ -521,7 +541,7 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w uint8_t *pixels = shm_buffer.pixels.data; const size_t pixels_size = shm_buffer.pixels._size_bytes; - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config); + auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, sprite_options); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); // draw debug rectangle @@ -671,7 +691,8 @@ static bool draw_bar_on_buffer(platform::wayland::wayland_context_t& ctx, const bongocat_sprite_sheet_t& sheet = cat_anim.bongocat; draw_sprite(ctx, shm_buffer, sheet, current_config.enable_antialiasing >= 1 ? blit_image_color_option_flags_t::BilinearInterpolation - : blit_image_color_option_flags_t::Normal); + : blit_image_color_option_flags_t::Normal, + features::EnableBongocatSvg ? blit_image_sprite_option_flags_t::IgnoreCatHeight : blit_image_sprite_option_flags_t::None); } break; case config::config_animation_sprite_sheet_layout_t::Dm: { if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { From 0895814d974488696efeb8961013d1f7bc255e99 Mon Sep 17 00:00:00 2001 From: furudbat Date: Thu, 16 Apr 2026 23:41:06 +0200 Subject: [PATCH 13/17] refactor: feature flags * fix modular builds * fix bongocat svg hot-reload --- CMakeLists.txt | 25 +++-- include/core/bongocat.h | 92 ------------------- .../bongocat/assets_bongocat_features.h | 21 +++++ .../embedded_assets/dm/assets_dm_features.h | 21 +++++ .../dm20/assets_dm20_features.h | 19 ++++ .../dmall/assets_dmall_features.h | 19 ++++ .../embedded_assets/dmc/assets_dmc_features.h | 19 ++++ .../embedded_assets/dmx/assets_dmx_features.h | 19 ++++ .../min_dm/assets_min_dm_features.h | 21 +++++ .../misc/assets_misc_features.h | 15 +++ .../ms_agent/assets_ms_agent_features.h | 15 +++ .../embedded_assets/pen/assets_pen_features.h | 19 ++++ .../pen20/assets_pen20_features.h | 19 ++++ .../pkmn/assets_pkmn_features.h | 15 +++ .../embedded_assets/pmd/assets_pmd_features.h | 15 +++ include/graphics/embedded_assets_dms.h | 24 +++++ include/graphics/embedded_assets_pkmn.h | 3 + .../bongocat/load_images_bongocat.h | 3 +- .../custom/load_custom_features.h | 10 ++ include/image_loader/load_images.h | 23 +++++ src/CMakeLists.txt | 24 ++--- src/config/config.cpp | 7 ++ src/core/main.cpp | 11 +++ src/embedded_assets/bongocat/CMakeLists.txt | 1 + src/embedded_assets/bongocat/include/.gitkeep | 0 src/embedded_assets/dm/include/.gitkeep | 0 src/embedded_assets/dm20/include/.gitkeep | 0 src/embedded_assets/dmall/include/.gitkeep | 0 src/embedded_assets/dmc/include/.gitkeep | 0 src/embedded_assets/dmx/include/.gitkeep | 0 src/embedded_assets/min_dm/include/.gitkeep | 0 src/embedded_assets/misc/CMakeLists.txt | 1 + src/embedded_assets/misc/include/.gitkeep | 0 src/embedded_assets/ms_agent/CMakeLists.txt | 2 + src/embedded_assets/ms_agent/include/.gitkeep | 0 src/embedded_assets/pen/include/.gitkeep | 0 src/embedded_assets/pen20/include/.gitkeep | 0 src/embedded_assets/pkmn/include/.gitkeep | 0 src/embedded_assets/pmd/include/.gitkeep | 0 src/graphics/animation.cpp | 5 +- src/graphics/animation_init.cpp | 23 ++++- src/graphics/bar.cpp | 23 +++-- .../bongocat/load_images_bongocat.cpp | 7 +- 43 files changed, 388 insertions(+), 133 deletions(-) create mode 100644 include/embedded_assets/bongocat/assets_bongocat_features.h create mode 100644 include/embedded_assets/dm/assets_dm_features.h create mode 100644 include/embedded_assets/dm20/assets_dm20_features.h create mode 100644 include/embedded_assets/dmall/assets_dmall_features.h create mode 100644 include/embedded_assets/dmc/assets_dmc_features.h create mode 100644 include/embedded_assets/dmx/assets_dmx_features.h create mode 100644 include/embedded_assets/min_dm/assets_min_dm_features.h create mode 100644 include/embedded_assets/misc/assets_misc_features.h create mode 100644 include/embedded_assets/ms_agent/assets_ms_agent_features.h create mode 100644 include/embedded_assets/pen/assets_pen_features.h create mode 100644 include/embedded_assets/pen20/assets_pen20_features.h create mode 100644 include/embedded_assets/pkmn/assets_pkmn_features.h create mode 100644 include/embedded_assets/pmd/assets_pmd_features.h create mode 100644 include/image_loader/custom/load_custom_features.h create mode 100644 src/embedded_assets/bongocat/include/.gitkeep create mode 100644 src/embedded_assets/dm/include/.gitkeep create mode 100644 src/embedded_assets/dm20/include/.gitkeep create mode 100644 src/embedded_assets/dmall/include/.gitkeep create mode 100644 src/embedded_assets/dmc/include/.gitkeep create mode 100644 src/embedded_assets/dmx/include/.gitkeep create mode 100644 src/embedded_assets/min_dm/include/.gitkeep create mode 100644 src/embedded_assets/misc/include/.gitkeep create mode 100644 src/embedded_assets/ms_agent/include/.gitkeep create mode 100644 src/embedded_assets/pen/include/.gitkeep create mode 100644 src/embedded_assets/pen20/include/.gitkeep create mode 100644 src/embedded_assets/pkmn/include/.gitkeep create mode 100644 src/embedded_assets/pmd/include/.gitkeep diff --git a/CMakeLists.txt b/CMakeLists.txt index 64d9e23e..8ef39e41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -334,21 +334,20 @@ if(FEATURE_MULTI_VERSIONS) bongocat-all PRIVATE # only include pmd for testing $<$:assets_pmd_loader - assets_pmd - assets_pmd_feature - assets_pmd_interface - assets_custom_loader> + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader> $<$:assets_pmd_loader - assets_pmd - assets_pmd_feature - assets_pmd_interface - assets_custom_loader> + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader> $<$:assets_pmd_loader - assets_pmd - assets_pmd_feature - assets_pmd_interface - assets_custom_loader>) - # target_link_libraries(bongocat-all PRIVATE assets_pmd_loader assets_pmd assets_pmd_feature assets_pmd_interface assets_custom_loader) + assets_pmd + assets_pmd_feature + assets_pmd_interface + assets_custom_loader>) target_link_libraries(bongocat-all PRIVATE assets_misc_loader assets_misc assets_misc_feature assets_misc_interface assets_custom_loader) target_link_libraries(bongocat-all PRIVATE assets_custom_sprite_sheet_feature assets_custom_loader) target_link_libraries(bongocat-all PRIVATE bongocat_base bongocat_options bongocat_libs) diff --git a/include/core/bongocat.h b/include/core/bongocat.h index 208bf2e7..af238c36 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -53,74 +53,6 @@ namespace features { inline static constexpr bool Debug = false; #endif -#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS - inline static constexpr bool EnableDmEmbeddedAssets = true; -# ifdef FEATURE_DM_EMBEDDED_ASSETS - inline static constexpr bool EnableFullDmEmbeddedAssets = true; -# else - inline static constexpr bool EnableFullDmEmbeddedAssets = false; -# endif -# ifdef FEATURE_DM20_EMBEDDED_ASSETS - inline static constexpr bool EnableDm20EmbeddedAssets = true; -# else - inline static constexpr bool EnableDm20EmbeddedAssets = false; -# endif -# ifdef FEATURE_DMC_EMBEDDED_ASSETS - inline static constexpr bool EnableDmcEmbeddedAssets = true; -# else - inline static constexpr bool EnableDmcEmbeddedAssets = false; -# endif -# ifdef FEATURE_DMX_EMBEDDED_ASSETS - inline static constexpr bool EnableDmxEmbeddedAssets = true; -# else - inline static constexpr bool EnableDmxEmbeddedAssets = false; -# endif -# ifdef FEATURE_PEN_EMBEDDED_ASSETS - inline static constexpr bool EnablePenEmbeddedAssets = true; -# else - inline static constexpr bool EnablePenEmbeddedAssets = false; -# endif -# ifdef FEATURE_PEN20_EMBEDDED_ASSETS - inline static constexpr bool EnablePen20EmbeddedAssets = true; -# else - inline static constexpr bool EnablePen20EmbeddedAssets = false; -# endif -# ifdef FEATURE_DMALL_EMBEDDED_ASSETS - inline static constexpr bool EnableDmAllEmbeddedAssets = true; -# else - inline static constexpr bool EnableDmAllEmbeddedAssets = false; -# endif -# if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ - !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ - !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) - inline static constexpr bool EnableMinDmEmbeddedAssets = true; -# else - inline static constexpr bool EnableMinDmEmbeddedAssets = false; -# endif -#else - inline static constexpr bool EnableDmEmbeddedAssets = false; - inline static constexpr bool EnableFullDmEmbeddedAssets = false; - inline static constexpr bool EnableDm20EmbeddedAssets = false; - inline static constexpr bool EnableDmcEmbeddedAssets = false; - inline static constexpr bool EnableDmxEmbeddedAssets = false; - inline static constexpr bool EnablePenEmbeddedAssets = false; - inline static constexpr bool EnablePen20EmbeddedAssets = false; - inline static constexpr bool EnableMinDmEmbeddedAssets = false; - inline static constexpr bool EnableDmAllEmbeddedAssets = false; -#endif - -#ifdef FEATURE_PKMN_EMBEDDED_ASSETS - inline static constexpr bool EnablePkmnEmbeddedAssets = true; -#else - inline static constexpr bool EnablePkmnEmbeddedAssets = false; -#endif - -#ifdef FEATURE_PMD_EMBEDDED_ASSETS - inline static constexpr bool EnablePmdEmbeddedAssets = true; -#else - inline static constexpr bool EnablePmdEmbeddedAssets = false; -#endif - #if !defined(BONGOCAT_DISABLE_MEMORY_STATISTICS) || defined(BONGOCAT_ENABLE_MEMORY_STATISTICS) inline static constexpr bool EnableMemoryStatistics = true; #else @@ -145,30 +77,6 @@ namespace features { inline static constexpr bool EnableLazyLoadAssets = false; #endif -#ifdef FEATURE_USE_HYBRID_IMAGE_BACKEND - inline static constexpr bool UseHybridImageBackend = true; -#else - inline static constexpr bool UseHybridImageBackend = false; - -# ifdef FEATURE_USE_PNGLE - inline static constexpr bool UsePngleImageBackend = true; -# else - inline static constexpr bool UsePngleImageBackend = false; -# endif - -# ifdef FEATURE_USE_STB_IMAGE - inline static constexpr bool UseStbImageBackend = true; -# else - inline static constexpr bool UseStbImageBackend = false; -# endif -#endif - -#ifdef FEATURE_CUSTOM_SPRITE_SHEETS - inline static constexpr bool EnableCustomSpriteSheetsAssets = true; -#else - inline static constexpr bool EnableCustomSpriteSheetsAssets = false; -#endif - } // namespace features // Global constants diff --git a/include/embedded_assets/bongocat/assets_bongocat_features.h b/include/embedded_assets/bongocat/assets_bongocat_features.h new file mode 100644 index 00000000..6db7835f --- /dev/null +++ b/include/embedded_assets/bongocat/assets_bongocat_features.h @@ -0,0 +1,21 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_BONGOCAT_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_BONGOCAT_EMBEDDED_ASSETS + inline static constexpr bool EnableBongocatEmbeddedAssets = true; +# ifdef FEATURE_USE_BONGOCAT_SVG + inline static constexpr bool EnableBongocatSvg = true; +# else + inline static constexpr bool EnableBongocatSvg = false; +# endif +#else + inline static constexpr bool EnableBongocatEmbeddedAssets = false; + inline static constexpr bool EnableBongocatSvg = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/dm/assets_dm_features.h b/include/embedded_assets/dm/assets_dm_features.h new file mode 100644 index 00000000..361862cd --- /dev/null +++ b/include/embedded_assets/dm/assets_dm_features.h @@ -0,0 +1,21 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_DM_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_DM_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +// inline static constexpr bool EnableDmEmbeddedAssets = true; +# ifdef FEATURE_DM_EMBEDDED_ASSETS + inline static constexpr bool EnableFullDmEmbeddedAssets = true; +# else + inline static constexpr bool EnableFullDmEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableFullDmEmbeddedAssets = false; +#endif + + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/dm20/assets_dm20_features.h b/include/embedded_assets/dm20/assets_dm20_features.h new file mode 100644 index 00000000..aa53efc5 --- /dev/null +++ b/include/embedded_assets/dm20/assets_dm20_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_DM20_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_DM20_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_DM20_EMBEDDED_ASSETS + inline static constexpr bool EnableDm20EmbeddedAssets = true; +# else + inline static constexpr bool EnableDm20EmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableDm20EmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/dmall/assets_dmall_features.h b/include/embedded_assets/dmall/assets_dmall_features.h new file mode 100644 index 00000000..b2ac3fcd --- /dev/null +++ b/include/embedded_assets/dmall/assets_dmall_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_DMALL_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_DMALL_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_DMALL_EMBEDDED_ASSETS + inline static constexpr bool EnableDmAllEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmAllEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableDmAllEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/dmc/assets_dmc_features.h b/include/embedded_assets/dmc/assets_dmc_features.h new file mode 100644 index 00000000..446b91b0 --- /dev/null +++ b/include/embedded_assets/dmc/assets_dmc_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_DMC_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_DMC_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_DMC_EMBEDDED_ASSETS + inline static constexpr bool EnableDmcEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmcEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableDmcEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/dmx/assets_dmx_features.h b/include/embedded_assets/dmx/assets_dmx_features.h new file mode 100644 index 00000000..b2abdce7 --- /dev/null +++ b/include/embedded_assets/dmx/assets_dmx_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_DMX_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_DMX_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_DMX_EMBEDDED_ASSETS + inline static constexpr bool EnableDmxEmbeddedAssets = true; +# else + inline static constexpr bool EnableDmxEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableDmxEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/min_dm/assets_min_dm_features.h b/include/embedded_assets/min_dm/assets_min_dm_features.h new file mode 100644 index 00000000..21be3f12 --- /dev/null +++ b/include/embedded_assets/min_dm/assets_min_dm_features.h @@ -0,0 +1,21 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_MIN_DM_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_MIN_DM_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS + inline static constexpr bool EnableDmEmbeddedAssets = true; +# if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ + !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ + !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) + inline static constexpr bool EnableMinDmEmbeddedAssets = true; +# else + inline static constexpr bool EnableMinDmEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnableMinDmEmbeddedAssets = false; +#endif +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/misc/assets_misc_features.h b/include/embedded_assets/misc/assets_misc_features.h new file mode 100644 index 00000000..1d6a8aa7 --- /dev/null +++ b/include/embedded_assets/misc/assets_misc_features.h @@ -0,0 +1,15 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_MISC_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_MISC_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_MISC_EMBEDDED_ASSETS +inline static constexpr bool EnableMiscEmbeddedAssets = true; +#else +inline static constexpr bool EnableMiscEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/ms_agent/assets_ms_agent_features.h b/include/embedded_assets/ms_agent/assets_ms_agent_features.h new file mode 100644 index 00000000..3d0cbc71 --- /dev/null +++ b/include/embedded_assets/ms_agent/assets_ms_agent_features.h @@ -0,0 +1,15 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_MS_AGENT_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_MS_AGENT_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_MS_AGENT_EMBEDDED_ASSETS +inline static constexpr bool EnableMsAgentEmbeddedAssets = true; +#else +inline static constexpr bool EnableMsAgentEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/pen/assets_pen_features.h b/include/embedded_assets/pen/assets_pen_features.h new file mode 100644 index 00000000..b9e638ee --- /dev/null +++ b/include/embedded_assets/pen/assets_pen_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_PEN_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_PEN_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_PEN_EMBEDDED_ASSETS + inline static constexpr bool EnablePenEmbeddedAssets = true; +# else + inline static constexpr bool EnablePenEmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnablePenEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/pen20/assets_pen20_features.h b/include/embedded_assets/pen20/assets_pen20_features.h new file mode 100644 index 00000000..3f545a2a --- /dev/null +++ b/include/embedded_assets/pen20/assets_pen20_features.h @@ -0,0 +1,19 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_PEN20_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_PEN20_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# ifdef FEATURE_PEN20_EMBEDDED_ASSETS + inline static constexpr bool EnablePen20EmbeddedAssets = true; +# else + inline static constexpr bool EnablePen20EmbeddedAssets = false; +# endif +#else + inline static constexpr bool EnablePen20EmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/pkmn/assets_pkmn_features.h b/include/embedded_assets/pkmn/assets_pkmn_features.h new file mode 100644 index 00000000..45d81dc5 --- /dev/null +++ b/include/embedded_assets/pkmn/assets_pkmn_features.h @@ -0,0 +1,15 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_PKMN_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_PKMN_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_PKMN_EMBEDDED_ASSETS +inline static constexpr bool EnablePkmnEmbeddedAssets = true; +#else +inline static constexpr bool EnablePkmnEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/embedded_assets/pmd/assets_pmd_features.h b/include/embedded_assets/pmd/assets_pmd_features.h new file mode 100644 index 00000000..e4e9cc93 --- /dev/null +++ b/include/embedded_assets/pmd/assets_pmd_features.h @@ -0,0 +1,15 @@ +#ifndef BONGOCAT_EMBEDDED_ASSETS_PMD_FEATURES_H +#define BONGOCAT_EMBEDDED_ASSETS_PMD_FEATURES_H + +// feature flags +namespace bongocat::features { + +#ifdef FEATURE_PMD_EMBEDDED_ASSETS +inline static constexpr bool EnablePmdEmbeddedAssets = true; +#else +inline static constexpr bool EnablePmdEmbeddedAssets = false; +#endif + +} // namespace features + +#endif \ No newline at end of file diff --git a/include/graphics/embedded_assets_dms.h b/include/graphics/embedded_assets_dms.h index 34ab43c8..3ec24468 100644 --- a/include/graphics/embedded_assets_dms.h +++ b/include/graphics/embedded_assets_dms.h @@ -1,6 +1,30 @@ #ifndef BONGOCAT_EMBEDDED_ASSETS_DMS_H #define BONGOCAT_EMBEDDED_ASSETS_DMS_H +#include +#include + +// feature flags +namespace bongocat::features { +#ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +inline static constexpr bool EnableDmEmbeddedAssets = true; +# ifdef FEATURE_ENABLE_DM_EMBEDDED_ASSETS +# if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ + !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ + !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && !defined(FEATURE_DMALL_EMBEDDED_ASSETS) +inline static constexpr bool EnableMinDmEmbeddedAssets = true; +# else +inline static constexpr bool EnableMinDmEmbeddedAssets = false; +# endif +# else +inline static constexpr bool EnableMinDmEmbeddedAssets = false; +# endif +#else +inline static constexpr bool EnableDmEmbeddedAssets = false; +inline static constexpr bool EnableMinDmEmbeddedAssets = false; +#endif +} // namespace bongocat::features + #if !defined(FEATURE_DM_EMBEDDED_ASSETS) && !defined(FEATURE_DM20_EMBEDDED_ASSETS) && \ !defined(FEATURE_DMC_EMBEDDED_ASSETS) && !defined(FEATURE_DMX_EMBEDDED_ASSETS) && \ !defined(FEATURE_PEN_EMBEDDED_ASSETS) && !defined(FEATURE_PEN20_EMBEDDED_ASSETS) && \ diff --git a/include/graphics/embedded_assets_pkmn.h b/include/graphics/embedded_assets_pkmn.h index 532d1dd8..cd01d9a3 100644 --- a/include/graphics/embedded_assets_pkmn.h +++ b/include/graphics/embedded_assets_pkmn.h @@ -1,6 +1,9 @@ #ifndef BONGOCAT_EMBEDDED_ASSETS_PKMN_INDEX_H #define BONGOCAT_EMBEDDED_ASSETS_PKMN_INDEX_H +#include "../embedded_assets/pkmn/assets_pkmn_features.h" +#include "../embedded_assets/pmd/assets_pmd_features.h" + /// pkmn #ifdef FEATURE_PKMN_EMBEDDED_ASSETS # include "embedded_assets/pkmn/pkmn.hpp" diff --git a/include/image_loader/bongocat/load_images_bongocat.h b/include/image_loader/bongocat/load_images_bongocat.h index 232b5c24..17be110a 100644 --- a/include/image_loader/bongocat/load_images_bongocat.h +++ b/include/image_loader/bongocat/load_images_bongocat.h @@ -19,7 +19,8 @@ load_bongocat_anim(int anim_index, get_sprite_callback_t get_sprite, size_t embe anim_sprite_sheet_from_embedded_svgs_cropping_t cropping); bongocat_error_t init_bongocat_anim(animation_thread_context_t& ctx, int anim_index, get_sprite_callback_t get_sprite, - size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params); + size_t embedded_images_count, load_bongocat_anim_type_t type, anim_sprite_sheet_from_embedded_svgs_t svg_params, + anim_sprite_sheet_from_embedded_svgs_cropping_t cropping); inline anim_sprite_sheet_from_embedded_svgs_t anim_bongocat_get_svg_params(int cat_height) { using namespace assets; diff --git a/include/image_loader/custom/load_custom_features.h b/include/image_loader/custom/load_custom_features.h new file mode 100644 index 00000000..a1b4c68c --- /dev/null +++ b/include/image_loader/custom/load_custom_features.h @@ -0,0 +1,10 @@ +#pragma once + +// feature flags +namespace bongocat::features { +#ifdef FEATURE_CUSTOM_SPRITE_SHEETS +inline static constexpr bool EnableCustomSpriteSheetsAssets = true; +#else +inline static constexpr bool EnableCustomSpriteSheetsAssets = false; +#endif +} // namespace features diff --git a/include/image_loader/load_images.h b/include/image_loader/load_images.h index 90619ffc..6142de39 100644 --- a/include/image_loader/load_images.h +++ b/include/image_loader/load_images.h @@ -8,6 +8,29 @@ #include "utils/memory.h" #include +// feature flags +namespace features { + +#ifdef FEATURE_USE_HYBRID_IMAGE_BACKEND +inline static constexpr bool UseHybridImageBackend = true; +#else +inline static constexpr bool UseHybridImageBackend = false; + +# ifdef FEATURE_USE_PNGLE +inline static constexpr bool UsePngleImageBackend = true; +# else +inline static constexpr bool UsePngleImageBackend = false; +# endif + +# ifdef FEATURE_USE_STB_IMAGE +inline static constexpr bool UseStbImageBackend = true; +# else +inline static constexpr bool UseStbImageBackend = false; +# endif +#endif + +} // namespace features + namespace bongocat::animation { // ============================================================================= // IMAGE LOADING MODULE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cd24e983..11e87079 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,24 +28,24 @@ target_compile_options( $<$,$>:-Wno-c23-extensions> $<$,$>:-Wno-c23-extensions>) # no exceptions -target_compile_options(bongocat_options INTERFACE $<$: -fno-exceptions -fno-unwind-tables -fno-rtti -nostdlib++ >) +target_compile_options(bongocat_options INTERFACE $<$:-fno-exceptions -fno-unwind-tables -fno-rtti -nostdlib++>) # optimization target_compile_options( bongocat_options INTERFACE $<$: - -march=native - -fomit-frame-pointer - -funroll-loops - -finline-functions + -march=native + -fomit-frame-pointer + -funroll-loops + -finline-functions > $<$: - -fno-inline-functions - -fomit-frame-pointer - -fno-asynchronous-unwind-tables - -fno-unroll-loops - -fmerge-all-constants - -fno-math-errno - -fno-stack-protector + -fno-inline-functions + -fomit-frame-pointer + -fno-asynchronous-unwind-tables + -fno-unroll-loops + -fmerge-all-constants + -fno-math-errno + -fno-stack-protector >) target_compile_definitions( bongocat_options diff --git a/src/config/config.cpp b/src/config/config.cpp index e36ebc50..fe3cbae7 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,17 +1,24 @@ #include "config/config.h" #include "core/bongocat.h" +#include "embedded_assets/bongocat/assets_bongocat_features.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" +#include "embedded_assets/misc/assets_misc_features.h" #include "embedded_assets/misc/misc.hpp" #include "embedded_assets/misc/misc_sprite.h" +#include "embedded_assets/ms_agent/assets_ms_agent_features.h" #include "embedded_assets/ms_agent/ms_agent.hpp" #include "embedded_assets/ms_agent/ms_agent_sprite.h" +#include "embedded_assets/pkmn/assets_pkmn_features.h" #include "embedded_assets/pkmn/pkmn_sprite.h" +#include "embedded_assets/pmd/assets_pmd_features.h" #include "embedded_assets/pmd/pmd_sprite.h" #include "graphics/animation_thread_context.h" #include "graphics/embedded_assets_dms.h" #include "graphics/embedded_assets_pkmn.h" +#include "image_loader/custom/load_custom.h" +#include "image_loader/custom/load_custom_features.h" #include "utils/error.h" #include diff --git a/src/core/main.cpp b/src/core/main.cpp index ebd92116..3e3f608a 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,6 +1,17 @@ #include "config/config.h" #include "core/bongocat.h" +#include "embedded_assets/bongocat/assets_bongocat_features.h" +#include "embedded_assets/dm20/assets_dm20_features.h" +#include "embedded_assets/dmall/assets_dmall_features.h" +#include "embedded_assets/dmc/assets_dmc_features.h" +#include "embedded_assets/dmx/assets_dmx_features.h" +#include "embedded_assets/ms_agent/assets_ms_agent_features.h" +#include "embedded_assets/pen/assets_pen_features.h" +#include "embedded_assets/pen20/assets_pen20_features.h" +#include "embedded_assets/pkmn/assets_pkmn_features.h" +#include "embedded_assets/pmd/assets_pmd_features.h" #include "graphics/animation.h" +#include "graphics/embedded_assets_dms.h" #include "image_loader/load_images.h" #include "platform/input.h" #include "platform/update.h" diff --git a/src/embedded_assets/bongocat/CMakeLists.txt b/src/embedded_assets/bongocat/CMakeLists.txt index d374b4b9..30804506 100644 --- a/src/embedded_assets/bongocat/CMakeLists.txt +++ b/src/embedded_assets/bongocat/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(assets_bongocat_interface INTERFACE) +target_include_directories(assets_bongocat_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_library(assets_bongocat_feature INTERFACE) target_compile_definitions(assets_bongocat_feature INTERFACE FEATURE_BONGOCAT_EMBEDDED_ASSETS) diff --git a/src/embedded_assets/bongocat/include/.gitkeep b/src/embedded_assets/bongocat/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/dm/include/.gitkeep b/src/embedded_assets/dm/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/dm20/include/.gitkeep b/src/embedded_assets/dm20/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/dmall/include/.gitkeep b/src/embedded_assets/dmall/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/dmc/include/.gitkeep b/src/embedded_assets/dmc/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/dmx/include/.gitkeep b/src/embedded_assets/dmx/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/min_dm/include/.gitkeep b/src/embedded_assets/min_dm/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/misc/CMakeLists.txt b/src/embedded_assets/misc/CMakeLists.txt index 5462aa4e..9e4893dd 100644 --- a/src/embedded_assets/misc/CMakeLists.txt +++ b/src/embedded_assets/misc/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(assets_misc_interface INTERFACE) +target_include_directories(assets_misc_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_library(assets_misc_feature INTERFACE) target_compile_definitions(assets_misc_feature INTERFACE FEATURE_MISC_EMBEDDED_ASSETS) diff --git a/src/embedded_assets/misc/include/.gitkeep b/src/embedded_assets/misc/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/ms_agent/CMakeLists.txt b/src/embedded_assets/ms_agent/CMakeLists.txt index ed93cf19..ffedc631 100644 --- a/src/embedded_assets/ms_agent/CMakeLists.txt +++ b/src/embedded_assets/ms_agent/CMakeLists.txt @@ -1,5 +1,6 @@ # MS Agent add_library(assets_ms_agent_interface INTERFACE) +target_include_directories(assets_ms_agent_interface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_library(assets_ms_agent_feature INTERFACE) target_compile_definitions(assets_ms_agent_feature INTERFACE FEATURE_MS_AGENT_EMBEDDED_ASSETS) @@ -21,6 +22,7 @@ add_library(assets_more_ms_agent_interface INTERFACE) add_library(assets_more_ms_agent_feature INTERFACE) target_compile_definitions(assets_more_ms_agent_feature INTERFACE FEATURE_MS_AGENT_EMBEDDED_ASSETS) target_compile_definitions(assets_more_ms_agent_feature INTERFACE FEATURE_MORE_MS_AGENT_EMBEDDED_ASSETS) +target_include_directories(assets_more_ms_agent_feature INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_library(assets_more_ms_agent STATIC) target_sources(assets_more_ms_agent PRIVATE embedded_assets_ms_agent.cpp ms_agent_images.c) diff --git a/src/embedded_assets/ms_agent/include/.gitkeep b/src/embedded_assets/ms_agent/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/pen/include/.gitkeep b/src/embedded_assets/pen/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/pen20/include/.gitkeep b/src/embedded_assets/pen20/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/pkmn/include/.gitkeep b/src/embedded_assets/pkmn/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/embedded_assets/pmd/include/.gitkeep b/src/embedded_assets/pmd/include/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp index 715876e2..40e7a924 100644 --- a/src/graphics/animation.cpp +++ b/src/graphics/animation.cpp @@ -1,5 +1,6 @@ #include "graphics/animation.h" +#include "embedded_assets/bongocat/assets_bongocat_features.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" #include "embedded_assets/custom/custom_sprite.h" @@ -13,6 +14,8 @@ #include "graphics/embedded_assets_dms.h" #include "graphics/embedded_assets_pkmn.h" #include "image_loader/bongocat/load_images_bongocat.h" +#include "image_loader/custom/load_custom.h" +#include "image_loader/custom/load_custom_features.h" #include "platform/wayland.h" #include "utils/system_error.h" #include "utils/time.h" @@ -5358,7 +5361,7 @@ static void update_config_reload_sprite_sheet(animation_thread_context_t& ctx) { : old_anim_index; [[maybe_unused]] const auto t0 = platform::get_current_time_us(); - if constexpr (features::EnableLazyLoadAssets) { + if constexpr (features::EnableLazyLoadAssets || (features::EnableBongocatSvg && ctx.shm->anim_type == config::config_animation_sprite_sheet_layout_t::Bongocat)) { auto [result, error] = hot_load_animation(ctx); if (error != bongocat_error_t::BONGOCAT_SUCCESS) { // rollback diff --git a/src/graphics/animation_init.cpp b/src/graphics/animation_init.cpp index 30c3c766..29638b95 100644 --- a/src/graphics/animation_init.cpp +++ b/src/graphics/animation_init.cpp @@ -3,25 +3,38 @@ #include "platform/wayland.h" #include "utils/memory.h" #include "utils/system_error.h" + #include #include // assets +#include "embedded_assets/bongocat/assets_bongocat_features.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" +#include "embedded_assets/dm/assets_dm_features.h" #include "embedded_assets/dm/dm_sprite.h" +#include "embedded_assets/dm20/assets_dm20_features.h" #include "embedded_assets/dm20/dm20_sprite.h" +#include "embedded_assets/dmall/assets_dmall_features.h" #include "embedded_assets/dmall/dmall_sprite.h" +#include "embedded_assets/dmc/assets_dmc_features.h" #include "embedded_assets/dmc/dmc_sprite.h" +#include "embedded_assets/dmx/assets_dmx_features.h" #include "embedded_assets/dmx/dmx_sprite.h" #include "embedded_assets/min_dm/min_dm_sprite.h" +#include "embedded_assets/misc/assets_misc_features.h" #include "embedded_assets/misc/misc.hpp" #include "embedded_assets/misc/misc_sprite.h" +#include "embedded_assets/ms_agent/assets_ms_agent_features.h" #include "embedded_assets/ms_agent/ms_agent.hpp" #include "embedded_assets/ms_agent/ms_agent_sprite.h" +#include "embedded_assets/pen/assets_pen_features.h" #include "embedded_assets/pen/pen_sprite.h" +#include "embedded_assets/pen20/assets_pen20_features.h" #include "embedded_assets/pen20/pen20_sprite.h" +#include "embedded_assets/pkmn/assets_pkmn_features.h" #include "embedded_assets/pkmn/pkmn_sprite.h" +#include "embedded_assets/pmd/assets_pmd_features.h" #include "embedded_assets/pmd/pmd_sprite.h" #include "graphics/embedded_assets_dms.h" #include "graphics/embedded_assets_pkmn.h" @@ -30,6 +43,7 @@ #include "image_loader/base_dm/load_dm.h" #include "image_loader/bongocat/load_images_bongocat.h" #include "image_loader/custom/load_custom.h" +#include "image_loader/custom/load_custom_features.h" #include "image_loader/dm/load_images_dm.h" #include "image_loader/dm20/load_images_dm20.h" #include "image_loader/dmall/load_images_dmall.h" @@ -480,7 +494,7 @@ created_result_t> create(const config::conf [[maybe_unused]] const auto t0 = platform::get_current_time_us(); // Load embedded images/animations - if constexpr (features::EnableLazyLoadAssets || features::EnableBongocatSvg) { + if constexpr (features::EnableLazyLoadAssets) { hot_load_animation(ret->thread_context); } @@ -499,12 +513,13 @@ created_result_t> create(const config::conf ctx.shm->bongocat_anims = platform::make_allocated_mmap_array(BONGOCAT_ANIM_COUNT); if constexpr (features::EnableBongocatSvg) { + const int cat_height = ret->thread_context._local_copy_config->cat_height; init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::SVG, - anim_bongocat_get_svg_params(ret->thread_context._local_copy_config->cat_height)); + load_bongocat_anim_type_t::SVG, anim_bongocat_get_svg_params(cat_height), + anim_bongocat_get_svg_cropping(cat_height)); } else { init_bongocat_anim(ctx, BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, - load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}); + load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}, {0, 0, 0, 0}); } } } diff --git a/src/graphics/bar.cpp b/src/graphics/bar.cpp index 5dc4bb37..13b2371e 100644 --- a/src/graphics/bar.cpp +++ b/src/graphics/bar.cpp @@ -1,12 +1,16 @@ #include "bar.h" +#include "embedded_assets/bongocat/assets_bongocat_features.h" #include "embedded_assets/bongocat/bongocat.h" +#include "embedded_assets/misc/assets_misc_features.h" #include "embedded_assets/misc/misc.hpp" #include "graphics/animation.h" #include "graphics/animation_thread_context.h" #include "graphics/drawing.h" #include "graphics/embedded_assets_dms.h" #include "graphics/embedded_assets_pkmn.h" +#include "image_loader/custom/load_custom.h" +#include "image_loader/custom/load_custom_features.h" #include "platform/wayland-protocols.hpp" #include "platform/wayland.h" #include "platform/wayland_callbacks.h" @@ -37,8 +41,7 @@ enum class blit_image_sprite_option_flags_t : uint32_t { template /// @TODO: required SpriteSheet must be _sprite_sheet_t cat_rect_t get_position(const platform::wayland::wayland_thread_context& wayland_ctx, const SpriteSheet& sheet, - const config::config_t& config, - blit_image_sprite_option_flags_t options) { + const config::config_t& config, blit_image_sprite_option_flags_t options) { const int cat_height = [&]() { if (has_flag(options, blit_image_sprite_option_flags_t::IgnoreCatHeight)) { return sheet.frame_height; @@ -51,8 +54,8 @@ cat_rect_t get_position(const platform::wayland::wayland_thread_context& wayland return sheet.frame_width; } - return static_cast(static_cast(cat_height) * (static_cast(sheet.frame_width) / - static_cast(sheet.frame_height))); + return static_cast(static_cast(cat_height) * + (static_cast(sheet.frame_width) / static_cast(sheet.frame_height))); }(); int cat_x = 0; @@ -280,7 +283,8 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); + auto [cat_x, cat_y, cat_width, cat_height] = + get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); if (region != BONGOCAT_NULLPTR) { @@ -394,7 +398,8 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w break; } - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); + auto [cat_x, cat_y, cat_width, cat_height] = + get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); auto cat_x_with_offset = cat_x + static_cast(anim_shm.movement_offset_x); if (region != BONGOCAT_NULLPTR) { @@ -491,7 +496,8 @@ void draw_sprite(platform::wayland::wayland_context_t& ctx, platform::wayland::w uint8_t *pixels = shm_buffer.pixels.data; const size_t pixels_size = shm_buffer.pixels._size_bytes; - auto [cat_x, cat_y, cat_width, cat_height] = get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); + auto [cat_x, cat_y, cat_width, cat_height] = + get_position(wayland_ctx, sheet, current_config, blit_image_sprite_option_flags_t::None); blit_image_color_option_flags_t drawing_option = blit_image_color_option_flags_t::Normal; if (current_config.invert_color >= 1) { @@ -692,7 +698,8 @@ static bool draw_bar_on_buffer(platform::wayland::wayland_context_t& ctx, draw_sprite(ctx, shm_buffer, sheet, current_config.enable_antialiasing >= 1 ? blit_image_color_option_flags_t::BilinearInterpolation : blit_image_color_option_flags_t::Normal, - features::EnableBongocatSvg ? blit_image_sprite_option_flags_t::IgnoreCatHeight : blit_image_sprite_option_flags_t::None); + features::EnableBongocatSvg ? blit_image_sprite_option_flags_t::IgnoreCatHeight + : blit_image_sprite_option_flags_t::None); } break; case config::config_animation_sprite_sheet_layout_t::Dm: { if constexpr (!features::EnableLazyLoadAssets || features::EnablePreloadAssets) { diff --git a/src/image_loader/bongocat/load_images_bongocat.cpp b/src/image_loader/bongocat/load_images_bongocat.cpp index f4e69d8c..f974a3f0 100644 --- a/src/image_loader/bongocat/load_images_bongocat.cpp +++ b/src/image_loader/bongocat/load_images_bongocat.cpp @@ -1,4 +1,5 @@ #include "load_images_bongocat.h" +#include "embedded_assets/bongocat/assets_bongocat_features.h" #include "embedded_assets/bongocat/bongocat.h" #include "embedded_assets/bongocat/bongocat.hpp" #include "graphics/animation.h" @@ -162,8 +163,9 @@ created_result_t load_bongocat_sprite_sheet(const anima switch (index) { case BONGOCAT_ANIM_INDEX: if constexpr (features::EnableBongocatSvg) { - const auto svg_params = anim_bongocat_get_svg_params(ctx._local_copy_config->cat_height); - const auto svg_cropping = anim_bongocat_get_svg_cropping(ctx._local_copy_config->cat_height); + const int cat_height = ctx._local_copy_config->cat_height; + const auto svg_params = anim_bongocat_get_svg_params(cat_height); + const auto svg_cropping = anim_bongocat_get_svg_cropping(cat_height); return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite_svg, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::SVG, svg_params, svg_cropping); } else { return load_bongocat_anim(BONGOCAT_ANIM_INDEX, get_bongocat_sprite, BONGOCAT_EMBEDDED_IMAGES_COUNT, load_bongocat_anim_type_t::PNG, {0, 0, 0, 0}, {0, 0, 0, 0}); @@ -171,6 +173,7 @@ created_result_t load_bongocat_sprite_sheet(const anima default: return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; } } // namespace bongocat::animation From a784b8de2aa922f8234a95854367003aa9bfd452 Mon Sep 17 00:00:00 2001 From: furudbat Date: Sat, 18 Apr 2026 15:00:12 +0200 Subject: [PATCH 14/17] wip: merge from upstream * fix KWin rendering & fullscreen --- include/platform/wayland_setups.h | 6 + include/platform/wayland_thread_context.h | 11 +- src/platform/wayland.cpp | 218 ++++++++++++++-------- src/platform/wayland_callbacks.cpp | 26 +-- src/platform/wayland_setups.cpp | 194 ++++++++++++++++--- 5 files changed, 340 insertions(+), 115 deletions(-) diff --git a/include/platform/wayland_setups.h b/include/platform/wayland_setups.h index a57f53ab..9fc4e7af 100644 --- a/include/platform/wayland_setups.h +++ b/include/platform/wayland_setups.h @@ -44,6 +44,12 @@ struct spawn_pipe_t { BONGOCAT_NODISCARD spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv); +created_result_t wayland_update_current_output_info(wayland_context_t& ctx); +bongocat_error_t wayland_update_output(wayland_context_t& ctx); +BONGOCAT_NODISCARD bool fs_detector_available(wayland_context_t& ctx); +zwlr_layer_shell_v1_layer wayland_apply_layer_properties(wayland_context_t& ctx); +uint32_t wayland_apply_anchor_properties(wayland_context_t& ctx); + } // namespace bongocat::platform::wayland::details #endif \ No newline at end of file diff --git a/include/platform/wayland_thread_context.h b/include/platform/wayland_thread_context.h index 9a9c3aaa..2b3a9b19 100644 --- a/include/platform/wayland_thread_context.h +++ b/include/platform/wayland_thread_context.h @@ -44,12 +44,15 @@ struct wayland_thread_context { MMapMemory ctx_shm; bar_visibility_t bar_visibility{bar_visibility_t::Show}; - int32_t _bar_height{0}; - int32_t _screen_width{0}; + int32_t _bar_height{0}; // applied_height + int32_t _screen_width{0}; // applied_width // ref to existing name in output, Will default to automatic one if kept null - char *_output_name_str{BONGOCAT_NULLPTR}; + char *_output_name_str{BONGOCAT_NULLPTR}; // bound_screen_name bool _fullscreen_detected{false}; screen_info_t *_screen_info{BONGOCAT_NULLPTR}; + config::layer_type_t _layer{config::layer_type_t::LAYER_TOP}; + config::overlay_position_t _overlay_position{config::overlay_position_t::POSITION_BOTTOM}; + AllocatedString _target_output_name; // applied_output_name // frame done callback data wl_callback *_frame_cb{BONGOCAT_NULLPTR}; @@ -191,6 +194,8 @@ inline void cleanup_wayland_context(wayland_thread_context& ctx) { ctx._screen_width = 0; ctx._fullscreen_detected = false; ctx._screen_info = BONGOCAT_NULLPTR; + ctx._layer = config::layer_type_t::LAYER_TOP; + ctx._overlay_position = config::overlay_position_t::POSITION_BOTTOM; } } // namespace bongocat::platform::wayland diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index ca51687b..e50f9bd8 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -443,100 +444,169 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, const auto old_height = ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_height : 0; const auto old_width = ctx.thread_context._screen_width; - char *old_screen_name = ctx.thread_context._output_name_str != BONGOCAT_NULLPTR - ? strdup(ctx.thread_context._output_name_str) - : BONGOCAT_NULLPTR; + const auto old_layer = + ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->layer : config::layer_type_t::LAYER_TOP; + //const auto old_overlay_position = + // ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_position : config::overlay_position_t::POSITION_BOTTOM; + const AllocatedString old_screen_name = ctx.thread_context._output_name_str != BONGOCAT_NULLPTR + ? duplicate_string(ctx.thread_context._output_name_str) + : make_null_string(); // update old config *ctx.thread_context._local_copy_config = config; + const config::config_t& current_config = *ctx.thread_context._local_copy_config; + + const bool layer_changed = (old_layer != current_config.layer); + const bool position_changed = (ctx.thread_context._overlay_position != current_config.overlay_position); + const bool dimensions_changed = (old_height != current_config.overlay_height) || + (current_config.screen_width > 0 && + old_width != ctx.thread_context._local_copy_config->screen_width); + const bool output_name_changed = + (static_cast(old_screen_name.c_str()) != static_cast(current_config.output_name.c_str())) || + (old_screen_name && current_config.output_name && + (strcmp(old_screen_name.c_str(), current_config.output_name.c_str()) != 0)); + const bool bound_output_changed = + (static_cast(ctx.thread_context._output_name_str) != static_cast(current_config.output_name.c_str())) || + (ctx.thread_context._output_name_str != BONGOCAT_NULLPTR && current_config.output_name && + (strcmp(ctx.thread_context._output_name_str, current_config.output_name.c_str()) != 0)); + const bool screen_changed = output_name_changed || bound_output_changed; + + // Determine which update path to use: + // - Full recreate: only for output (monitor) changes + // - Buffer recreate: for dimension changes (overlay_height, screen_width) + // - Property update: for position/layer changes (double-buffered, no recreate) + // - Cache only: for cat_height, mirror, etc. + const bool needs_full_recreate = screen_changed; + const bool needs_buffer_recreate = dimensions_changed && old_height > 0 && old_width > 0; + const bool needs_property_update = layer_changed || position_changed; + + if (ctx.thread_context.ctx_shm) { + if (needs_full_recreate) { + // PATH 3: Output changed - full surface recreation required + BONGOCAT_LOG_INFO("Output changed, recreating surface"); + + { + // ~~Lock animation mutex to prevent draw_bar() during config update + // This is critical - animation thread must not access buffer while we + // recreate it~~ + // not sure if this is needed, animation thread don't touch the bar directly, + // it just triggers a rerender event + platform::LockGuard anim_guard(animation_ctx.thread_context.anim_lock); + + BONGOCAT_LOG_INFO("Dimensions changed (%dx%d -> %dx%d), recreating buffer...", old_width, old_height, + current_config.screen_width, + current_config.overlay_height); + + // Mark as not configured first + assert(ctx.thread_context.ctx_shm); + atomic_store(&ctx.thread_context.ctx_shm->configured, false); + + if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to update width for bar"); + return; + } + // _screen_width updated in wayland_update_screen_info + ctx.thread_context._bar_height = current_config.overlay_height; - const bool dimensions_changed = (old_height != ctx.thread_context._local_copy_config->overlay_height) || - (ctx.thread_context._local_copy_config->screen_width > 0 && - old_width != ctx.thread_context._local_copy_config->screen_width); - const bool change_screen = - ctx.thread_context._local_copy_config->output_name && - (strcmp(old_screen_name, ctx.thread_context._local_copy_config->output_name.c_str()) != 0 || - strcmp(ctx.thread_context._output_name_str, ctx.thread_context._local_copy_config->output_name.c_str()) != 0); - - if (((dimensions_changed && old_height > 0 && old_width > 0) || change_screen) && ctx.thread_context.ctx_shm) { - // ~~Lock animation mutex to prevent draw_bar() during config update - // This is critical - animation thread must not access buffer while we - // recreate it~~ - // not sure if this is needed, animation thread don't touch the bar directly, - // it just triggers a rerender event - platform::LockGuard anim_guard(animation_ctx.thread_context.anim_lock); - - BONGOCAT_LOG_INFO("Dimensions changed (%dx%d -> %dx%d), recreating buffer...", old_width, old_height, - ctx.thread_context._local_copy_config->screen_width, - ctx.thread_context._local_copy_config->overlay_height); - - // Mark as not configured first - assert(ctx.thread_context.ctx_shm); - atomic_store(&ctx.thread_context.ctx_shm->configured, false); - - if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { - if (old_screen_name != BONGOCAT_NULLPTR) { - ::free(old_screen_name); - old_screen_name = BONGOCAT_NULLPTR; - } - BONGOCAT_LOG_ERROR("Failed to update width for bar"); - return; - } - ctx.thread_context._bar_height = config.overlay_height; + if (ctx.thread_context._screen_width > 0 && ctx.thread_context._bar_height > 0) { + // Cleanup old buffer + cleanup_wayland_context_buffer(ctx.thread_context); - if (ctx.thread_context._screen_width > 0 && ctx.thread_context._bar_height > 0) { - // Cleanup old buffer - cleanup_wayland_context_buffer(ctx.thread_context); + // Cleanup old surface + cleanup_wayland_context_surface(ctx.thread_context); - // Cleanup old surface - cleanup_wayland_context_surface(ctx.thread_context); + if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to update width for bar"); + return; + } + ctx.thread_context._bar_height = current_config.overlay_height; + + /// @TODO: reduce unnessery updates + details::wayland_update_output(ctx); + details::wayland_update_current_output_info(ctx); + + // Recreate surface and buffer with new dimensions + if (details::wayland_setup_surface(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to recreate surface after config change"); + return; + } - if (details::wayland_update_screen_info(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { - if (old_screen_name != BONGOCAT_NULLPTR) { - ::free(old_screen_name); - old_screen_name = BONGOCAT_NULLPTR; + /// @TODO: reduce unnessery updates + details::wayland_update_output(ctx); + details::wayland_update_current_output_info(ctx); + + if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); + return; + } + ctx.thread_context._layer = current_config.layer; + ctx.thread_context._overlay_position = current_config.overlay_position; + ctx.thread_context._target_output_name = duplicate_string(config.output_name); + + atomic_store(&ctx.thread_context.ctx_shm->configured, true); + + BONGOCAT_LOG_INFO("Buffer recreated successfully (%dx%d)", current_config.screen_width, + current_config.overlay_height); + } else { + BONGOCAT_LOG_ERROR("Buffer recreated failed (%dx%d)", current_config.screen_width, + current_config.overlay_height); } - BONGOCAT_LOG_ERROR("Failed to update width for bar"); - return; } - ctx.thread_context._bar_height = config.overlay_height; - // Recreate surface and buffer with new dimensions - if (details::wayland_setup_surface(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { - if (old_screen_name != BONGOCAT_NULLPTR) { - ::free(old_screen_name); - old_screen_name = BONGOCAT_NULLPTR; - } - BONGOCAT_LOG_ERROR("Failed to recreate surface after config change"); - return; + // Wait for new configure event + wl_display_roundtrip(ctx.thread_context.display); + details::wayland_update_current_output_info(ctx); + + BONGOCAT_LOG_INFO("Surface recreated successfully (%dx%d)", + current_config.screen_width, current_config.overlay_height); + } else if (needs_buffer_recreate) { + // PATH 2: Dimensions changed - update size property, recreate buffer only + BONGOCAT_LOG_INFO("Overlay dimensions changed (%dx%d -> %dx%d)", old_width, + old_height, current_config.screen_width, current_config.overlay_height); + + assert(current_config.overlay_height >= 0 && current_config.screen_width <= INT32_MAX); + // Update double-buffered properties on existing layer surface + zwlr_layer_surface_v1_set_size(ctx.thread_context.layer_surface, 0, static_cast(current_config.overlay_height)); + if (position_changed) { + details::wayland_apply_layer_properties(ctx); } - if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { - if (old_screen_name != BONGOCAT_NULLPTR) { - ::free(old_screen_name); - old_screen_name = BONGOCAT_NULLPTR; - } - BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); - return; + if (layer_changed) { + details::wayland_apply_anchor_properties(ctx); } + wl_surface_commit(ctx.thread_context.surface); + + platform::LockGuard anim_guard(animation_ctx.thread_context.anim_lock); + { + assert(ctx.thread_context.ctx_shm); + atomic_store(&ctx.thread_context.ctx_shm->configured, false); - atomic_store(&ctx.thread_context.ctx_shm->configured, true); + // Cleanup old buffer + cleanup_wayland_context_buffer(ctx.thread_context); + + if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { + BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); + return; + } + } // Wait for new configure event wl_display_roundtrip(ctx.thread_context.display); - BONGOCAT_LOG_INFO("Buffer recreated successfully (%dx%d)", ctx.thread_context._local_copy_config->screen_width, - ctx.thread_context._local_copy_config->overlay_height); - } else { - BONGOCAT_LOG_ERROR("Buffer recreated failed (%dx%d)", ctx.thread_context._local_copy_config->screen_width, - ctx.thread_context._local_copy_config->overlay_height); + BONGOCAT_LOG_INFO("Buffer resized successfully (%dx%d)", + current_config.screen_width, current_config.overlay_height); + } else if (needs_property_update) { + // PATH 1: Position/layer only - no buffer changes needed + if (position_changed) { + details::wayland_apply_layer_properties(ctx); + } + if (layer_changed) { + details::wayland_apply_anchor_properties(ctx); + } + wl_surface_commit(ctx.thread_context.surface); + wl_display_roundtrip(ctx.thread_context.display); } } - if (old_screen_name != BONGOCAT_NULLPTR) { - ::free(old_screen_name); - old_screen_name = BONGOCAT_NULLPTR; - } - /// @NOTE: assume animation has the same local copy as wayland config // animation_update_config(anim, config); if (atomic_load(&ctx.thread_context.ctx_shm->configured)) { diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index ee832669..0a40aaba 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -53,7 +53,8 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out /// Reconnection handling if (oref->wayland != BONGOCAT_NULLPTR) { - wayland_thread_context& wayland_ctx = oref->wayland->thread_context; + wayland_context_t& wayland_ctx = *oref->wayland; + wayland_thread_context& wayland_thread_ctx = oref->wayland->thread_context; // animation_context_t& anim = *ctx.animation_context; // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; @@ -62,17 +63,17 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out // const config::config_t& current_config = *wayland_ctx._local_copy_config; // Check if this is the output we're waiting for (reconnection case) - if (!atomic_load(&oref->wayland->_output_lost)) { + if (!atomic_load(&wayland_ctx._output_lost)) { return; } bool should_reconnect = false; // Case 1: User specified an output name - match exactly - if (wayland_ctx.using_named_output && wayland_ctx._output_name_str != BONGOCAT_NULLPTR) { - should_reconnect = (strcmp(name, wayland_ctx._output_name_str) == 0); + if (wayland_thread_ctx.using_named_output && wayland_thread_ctx._output_name_str != BONGOCAT_NULLPTR) { + should_reconnect = (strcmp(name, wayland_thread_ctx._output_name_str) == 0); } // Case 2: Using fallback (first output) - reconnect to any output - else if (!wayland_ctx.using_named_output) { + else if (!wayland_thread_ctx.using_named_output) { should_reconnect = true; BONGOCAT_LOG_DEBUG("Using fallback output, accepting '%s'", name); } @@ -81,11 +82,11 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out BONGOCAT_LOG_INFO("Target output '%s' reconnected!", name); // Clean up old surface if it exists - cleanup_wayland_context_surface(wayland_ctx); + cleanup_wayland_context_surface(wayland_thread_ctx); // Set new output - wayland_ctx.output = oref->wl_output; - wayland_ctx.bound_output_name = oref->name; + wayland_thread_ctx.output = oref->wl_output; + wayland_thread_ctx.bound_output_name = oref->name; atomic_store(&oref->wayland->_output_lost, false); // Recreate surface on new output @@ -93,13 +94,14 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out // event. The layer_surface_configure callback will ack and call draw_bar() // to render. if (wayland_setup_surface(*oref->wayland) == bongocat_error_t::BONGOCAT_SUCCESS) { - assert(wayland_ctx.ctx_shm); - if (wayland_ctx.ctx_shm) { - atomic_store(&wayland_ctx.ctx_shm->configured, true); + assert(wayland_thread_ctx.ctx_shm); + if (wayland_thread_ctx.ctx_shm) { + atomic_store(&wayland_thread_ctx.ctx_shm->configured, true); } if constexpr (WAYLAND_NUM_BUFFERS != 1) { // Wait for configure event to be processed - wl_display_roundtrip(wayland_ctx.display); + wl_display_roundtrip(wayland_thread_ctx.display); + wayland_update_current_output_info(wayland_ctx); } if (oref->wayland->animation_context != BONGOCAT_NULLPTR) { request_render(*oref->wayland->animation_context); diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp index 8f6fb4e8..dadbc6fb 100644 --- a/src/platform/wayland_setups.cpp +++ b/src/platform/wayland_setups.cpp @@ -74,7 +74,7 @@ created_result_t create_shm(off_t size) { // MAIN WAYLAND INTERFACE IMPLEMENTATION // ============================================================================= -bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { +bongocat_error_t wayland_update_output(wayland_context_t& ctx) { wayland_thread_context& wayland_ctx = ctx.thread_context; // read-only config @@ -119,6 +119,66 @@ bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { BONGOCAT_LOG_WARNING("Falling back to first output: %s", wayland_ctx._output_name_str); } + return bongocat_error_t::BONGOCAT_SUCCESS; +} +created_result_t wayland_update_current_output_info(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + + // auto-detect screen width + bool output_found = false; + int screen_width{DEFAULT_SCREEN_WIDTH}; + char *bound_screen_name{BONGOCAT_NULLPTR}; + screen_info_t *screen_info{BONGOCAT_NULLPTR}; + if (wayland_ctx.output != BONGOCAT_NULLPTR) { + wl_display_roundtrip(wayland_ctx.display); + // update screen_infos + + for (size_t i = 0; i < ctx.output_count && i < MAX_OUTPUTS; i++) { + if (ctx.screen_infos[i].wl_output == ctx.thread_context.output) { + BONGOCAT_LOG_INFO("Detected screen name: %s", ctx.outputs[i].name_str); + bound_screen_name = ctx.outputs[i].name_str; + + if (ctx.screen_infos[i].screen_width > 0 && ctx.screen_infos[i].screen_width <= INT16_MAX) { + BONGOCAT_LOG_INFO("Detected screen width: %d", + ctx.screen_infos[i].screen_width); + screen_info = &ctx.screen_infos[i]; + screen_width = ctx.screen_infos[i].screen_width; + output_found = true; + } + } + } + } else { + screen_width = DEFAULT_SCREEN_WIDTH; + if (current_config._strict) { + return bongocat_error_t::BONGOCAT_ERROR_INVALID_PARAM; + } + output_found = false; + } + + if (output_found) { + ctx.thread_context._output_name_str = bound_screen_name; + ctx.thread_context._screen_info = screen_info; + ctx.thread_context._screen_width = screen_width; + } else { + BONGOCAT_LOG_WARNING("No output found, using default screen width: %d", + DEFAULT_SCREEN_WIDTH); + } + + return screen_width; +} +bongocat_error_t wayland_update_screen_info(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + + wayland_update_output(ctx); + if (wayland_ctx.compositor == BONGOCAT_NULLPTR || wayland_ctx.shm == BONGOCAT_NULLPTR || wayland_ctx.layer_shell == BONGOCAT_NULLPTR) { BONGOCAT_LOG_ERROR("Missing required Wayland protocols"); @@ -193,21 +253,46 @@ bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { hyprland::update_outputs_with_monitor_ids(ctx); } + wayland_update_output(ctx); + if (wayland_ctx.compositor == BONGOCAT_NULLPTR || wayland_ctx.shm == BONGOCAT_NULLPTR || wayland_ctx.layer_shell == BONGOCAT_NULLPTR) { + if (wayland_ctx.compositor == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Missing protocol: wl_compositor"); + } + if (wayland_ctx.shm == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Missing protocol: wl_shm"); + } + if (wayland_ctx.layer_shell == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Missing protocol: wlr-layer-shell (required for " + "overlay rendering). Your compositor may not support " + "this protocol."); + } BONGOCAT_LOG_ERROR("Missing required Wayland protocols"); + wl_registry_destroy(registry); return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; } - const bongocat_error_t result_update_screen_width = wayland_update_screen_info(ctx); - if (result_update_screen_width != bongocat_error_t::BONGOCAT_SUCCESS) { + // Warn about optional protocols + if (!fs_detector_available(ctx)) { + BONGOCAT_LOG_WARNING("Foreign toplevel protocol not available - fullscreen " + "detection disabled. Overlay will not auto-hide when " + "apps go fullscreen."); + } + + // Configure screen dimensions + wayland_update_current_output_info(ctx); + auto [result_update_screen_width, result_update_screen_width_error] = wayland_update_current_output_info(ctx); // wayland_update_screen_info(ctx); + if (result_update_screen_width_error != bongocat_error_t::BONGOCAT_SUCCESS) { wl_registry_destroy(registry); - return result_update_screen_width; + return result_update_screen_width_error; } + // Keep registry alive for output reconnection handling // move new registry if (wayland_ctx.registry != BONGOCAT_NULLPTR) { + // destroy old registry wl_registry_destroy(wayland_ctx.registry); } wayland_ctx.registry = registry; @@ -220,7 +305,7 @@ bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { return bongocat_error_t::BONGOCAT_SUCCESS; } -bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { +zwlr_layer_shell_v1_layer wayland_apply_layer_properties(wayland_context_t& ctx) { wayland_thread_context& wayland_ctx = ctx.thread_context; // animation_context_t& anim = *ctx.animation_context; // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; @@ -229,12 +314,6 @@ bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { assert(wayland_ctx._local_copy_config); const config::config_t& current_config = *wayland_ctx._local_copy_config; - wayland_ctx.surface = wl_compositor_create_surface(wayland_ctx.compositor); - if (wayland_ctx.surface == BONGOCAT_NULLPTR) { - BONGOCAT_LOG_ERROR("Failed to create surface"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; - } - zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; switch (current_config.layer) { case config::layer_type_t::LAYER_BACKGROUND: @@ -250,14 +329,23 @@ bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; break; } - wayland_ctx.layer_surface = zwlr_layer_shell_v1_get_layer_surface(wayland_ctx.layer_shell, wayland_ctx.surface, - wayland_ctx.output, layer, WAYLAND_LAYER_NAMESPACE); - if (wayland_ctx.layer_surface == BONGOCAT_NULLPTR) { - BONGOCAT_LOG_ERROR("Failed to create layer surface"); - return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + + if (wayland_ctx.layer_surface != BONGOCAT_NULLPTR) { + zwlr_layer_surface_v1_set_layer(wayland_ctx.layer_surface, layer); + BONGOCAT_LOG_INFO("Layer changed to %x", layer); } - // Configure layer surface + return layer; +} +uint32_t wayland_apply_anchor_properties(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + uint32_t anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; switch (current_config.overlay_position) { case config::overlay_position_t::POSITION_TOP: @@ -272,6 +360,52 @@ bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { break; } + if (wayland_ctx.layer_surface != BONGOCAT_NULLPTR) { + zwlr_layer_surface_v1_set_anchor(wayland_ctx.layer_surface, anchor); + BONGOCAT_LOG_INFO("Overlay position changed to %x", anchor); + } + + return anchor; +} + +bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { + wayland_thread_context& wayland_ctx = ctx.thread_context; + // animation_context_t& anim = *ctx.animation_context; + // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; + + // read-only config + assert(wayland_ctx._local_copy_config); + const config::config_t& current_config = *wayland_ctx._local_copy_config; + + wayland_ctx.surface = wl_compositor_create_surface(wayland_ctx.compositor); + if (wayland_ctx.surface == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create surface"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + switch (current_config.layer) { + case config::layer_type_t::LAYER_BACKGROUND: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + break; + case config::layer_type_t::LAYER_BOTTOM: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + break; + case config::layer_type_t::LAYER_TOP: + layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + break; + case config::layer_type_t::LAYER_OVERLAY: + layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + break; + } + wayland_ctx.layer_surface = zwlr_layer_shell_v1_get_layer_surface(wayland_ctx.layer_shell, wayland_ctx.surface, + wayland_ctx.output, layer, WAYLAND_LAYER_NAMESPACE); + if (wayland_ctx.layer_surface == BONGOCAT_NULLPTR) { + BONGOCAT_LOG_ERROR("Failed to create layer surface"); + return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; + } + + // Configure layer surface assert(wayland_ctx._bar_height >= 0); // assert(current_config.bar_height <= UINT32_MAX); if (wayland_ctx._bar_height == 0) { @@ -279,7 +413,7 @@ bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { zwlr_layer_surface_v1_destroy(wayland_ctx.layer_surface); return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; } - zwlr_layer_surface_v1_set_anchor(wayland_ctx.layer_surface, anchor); + wayland_apply_anchor_properties(ctx); zwlr_layer_surface_v1_set_size(wayland_ctx.layer_surface, 0, static_cast(wayland_ctx._bar_height)); zwlr_layer_surface_v1_set_exclusive_zone(wayland_ctx.layer_surface, -1); zwlr_layer_surface_v1_set_keyboard_interactivity(wayland_ctx.layer_surface, @@ -333,11 +467,11 @@ bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, FileDescriptor fd; { assert(total_size <= INT32_MAX); - created_result_t fd_result = create_shm(static_cast(total_size)); - if (fd_result.error != bongocat_error_t::BONGOCAT_SUCCESS) { - return fd_result.error; + auto [fd_shm, fd_shm_error] = create_shm(static_cast(total_size)); + if (fd_shm_error != bongocat_error_t::BONGOCAT_SUCCESS) { + return fd_shm_error; } - fd = bongocat::move(fd_result.result); + fd = bongocat::move(fd_shm); } wl_shm_pool *pool = wl_shm_create_pool(wayland_context.shm, fd._fd, static_cast(total_size)); @@ -401,7 +535,7 @@ bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, } spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, const char *const *argv) { - int pipefd[2] = {0}; + int pipefd[2] = {-1, -1}; spawn_pipe_t result; if (pipe(pipefd) != 0) { @@ -415,9 +549,9 @@ spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, con posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO); // Redirect stderr -> /dev/null - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { - posix_spawn_file_actions_adddup2(&actions, devnull, STDERR_FILENO); + int devnull_fd = open("/dev/null", O_WRONLY); + if (devnull_fd >= 0) { + posix_spawn_file_actions_adddup2(&actions, devnull_fd, STDERR_FILENO); } posix_spawn_file_actions_addclose(&actions, pipefd[0]); @@ -427,12 +561,15 @@ spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, con if (posix_spawn(&pid, path, &actions, BONGOCAT_NULLPTR, const_cast(argv), ctx._environ) != 0) { close(pipefd[0]); close(pipefd[1]); + //pipefd[0] = -1; + //pipefd[1] = -1; posix_spawn_file_actions_destroy(&actions); return result; } posix_spawn_file_actions_destroy(&actions); close(pipefd[1]); + //pipefd[1] = -1; result.fp = fdopen(pipefd[0], "r"); result.pid = pid; @@ -442,6 +579,7 @@ int safe_pclose_spawn(spawn_pipe_t& sp) { int status = 0; if (sp.fp) { fclose(sp.fp); + sp.fp = BONGOCAT_NULLPTR; } if (sp.pid > 0) { @@ -451,4 +589,8 @@ int safe_pclose_spawn(spawn_pipe_t& sp) { return status; } +bool fs_detector_available(wayland_context_t& ctx) { + return ctx.fs_detector.manager != BONGOCAT_NULLPTR; +} + } // namespace bongocat::platform::wayland::details From 227610b6659b335e7427d4479bb993da300f5420 Mon Sep 17 00:00:00 2001 From: furudbat Date: Sat, 18 Apr 2026 16:25:44 +0200 Subject: [PATCH 15/17] wip: merge from upstream * fix KWin rendering & fullscreen * update doc * fix reconnect monitor --- CHANGELOG.md | 11 + bongocat.conf.example | 11 +- examples/test.bongocat.conf | 3 + examples/test2.bongocat.conf | 5 +- include/platform/wayland_context.h | 8 + include/platform/wayland_setups.h | 4 +- include/platform/wayland_thread_context.h | 10 +- scripts/test_bongocat_12.sh | 52 ++++- src/config/config.cpp | 6 +- src/graphics/animation.cpp | 3 +- src/platform/wayland.cpp | 128 +++++++---- src/platform/wayland_callbacks.cpp | 263 +++++++++++++++------- src/platform/wayland_setups.cpp | 46 ++-- 13 files changed, 398 insertions(+), 152 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568beceb..4709962d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +### [4.0.0] - 2026-04-18 + +- pull from upstream [2.0.0](https://github.com/saatvik333/wayland-bongocat/releases/tag/v2.0.0) + - replace png bongocat with svg bongocat + - 5 animation frames including a dedicated sleeping frame + - fix KWin input and fullscreen handling + - Generated Wayland protocol files are now committed to git. Building from source no longer requires `wayland-scanner` or `wayland-protocols`. + - Uses wlr-layer-shell double-buffered properties instead of destroying/recreating surfaces. + +BREAKING CHANGE: New bongocat replacement has different height and position needs to be reconfigured! + ### [3.6.2] - 2026-03-30 - pull from upstream [1.4.0](https://github.com/saatvik333/wayland-bongocat/releases/tag/v1.4.0) diff --git a/bongocat.conf.example b/bongocat.conf.example index 2c2a11f5..37169916 100644 --- a/bongocat.conf.example +++ b/bongocat.conf.example @@ -9,10 +9,10 @@ # DRAWN LAYERS GETS GLITCHY SOMETIMES, BETTER TO RESTART WHEN THESE CHANGE # Overlay settings # overlay_height: Height of the entire overlay bar -overlay_height=80 +overlay_height=120 # overlay_position: Position of the overlay on screen # Options: "top" or "bottom" -overlay_position=top +overlay_position=bottom # layer: layer for surface of overlay (default: "top") # Options: "overlay", "top", "bottom", "background"; top (above windows), overlay (always visible) #overlay_layer=top @@ -52,7 +52,7 @@ enable_antialiasing=1 # Size settings # cat_height: Height of the bongo cat in pixels # Width is automatically calculated to maintain aspect ratio -cat_height=80 +cat_height=110 # animation_name: Sprite name (CASE SENSITIVE) # Default Option: "bongocat" @@ -179,4 +179,7 @@ enable_debug=0 # Specify which monitor to display bongocat on (optional) # Use wlr-randr or swaymsg -t get_outputs to find monitor names # If not specified or monitor not found, uses first available monitor -#monitor=HDMI-A-1 \ No newline at end of file +#monitor=HDMI-A-1 + +# How often to rescan for new input devices, in seconds (0=scan once at startup) +# hotplug_scan_interval=30 \ No newline at end of file diff --git a/examples/test.bongocat.conf b/examples/test.bongocat.conf index 3785edf4..a98d192c 100644 --- a/examples/test.bongocat.conf +++ b/examples/test.bongocat.conf @@ -40,6 +40,9 @@ overlay_height=128 # overlay_position: Position of the overlay on screen # Options: "top" or "bottom" overlay_position=bottom +# layer: layer for surface of overlay (default: "top") +# Options: "overlay", "top", "bottom", "background"; top (above windows), overlay (always visible) +overlay_layer=top # NOTE: ANIMATION FROM DIFFERENT TYPE DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART OR BUILD WITH LAZY LOAD (e.g. "bongocat" -> "clippy") # animation_name: Animation index diff --git a/examples/test2.bongocat.conf b/examples/test2.bongocat.conf index e153f84b..9cb12662 100644 --- a/examples/test2.bongocat.conf +++ b/examples/test2.bongocat.conf @@ -40,6 +40,9 @@ overlay_height=128 # overlay_position: Position of the overlay on screen # Options: "top" or "bottom" overlay_position=bottom +# layer: layer for surface of overlay (default: "top") +# Options: "overlay", "top", "bottom", "background"; top (above windows), overlay (always visible) +overlay_layer=top # NOTE: ANIMATION FROM DIFFERENT TYPE DOESN'T WORK WITH HOT RELOAD, NEEDS BONGOCAT RESTART OR BUILD WITH LAZY LOAD (e.g. "bongocat" -> "clippy") # animation_name: Animation index @@ -170,7 +173,7 @@ keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-event-kbd keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-event-if01 keyboard_name=/dev/input/by-id/usb-SEMICO_DIERYA_61SE-if01-event-kbd -hotplug_scan_interval=300 +hotplug_scan_interval=30 # Multi-monitor support # Specify which monitor to display bongocat on (optional) diff --git a/include/platform/wayland_context.h b/include/platform/wayland_context.h index f4bf55db..254accbd 100644 --- a/include/platform/wayland_context.h +++ b/include/platform/wayland_context.h @@ -94,6 +94,10 @@ struct wayland_context_t { // Output reconnection handling atomic_bool _output_lost{false}; // Set when our output disconnects + // Fullscreen + atomic_bool _compositor_sends_output_events{false}; + atomic_bool _active_toplevel_fullscreen{false}; + // for safe_popen_read_spawn (pointer to global environ) char **_environ{nullptr}; @@ -169,6 +173,10 @@ inline void cleanup_wayland(wayland_context_t& ctx) { ctx.animation_context = BONGOCAT_NULLPTR; ctx._environ = BONGOCAT_NULLPTR; + + atomic_store(&ctx._output_lost, false); + atomic_store(&ctx._compositor_sends_output_events, false); + atomic_store(&ctx._active_toplevel_fullscreen, false); } } // namespace bongocat::platform::wayland diff --git a/include/platform/wayland_setups.h b/include/platform/wayland_setups.h index 9fc4e7af..005a682b 100644 --- a/include/platform/wayland_setups.h +++ b/include/platform/wayland_setups.h @@ -47,8 +47,8 @@ BONGOCAT_NODISCARD spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, co created_result_t wayland_update_current_output_info(wayland_context_t& ctx); bongocat_error_t wayland_update_output(wayland_context_t& ctx); BONGOCAT_NODISCARD bool fs_detector_available(wayland_context_t& ctx); -zwlr_layer_shell_v1_layer wayland_apply_layer_properties(wayland_context_t& ctx); -uint32_t wayland_apply_anchor_properties(wayland_context_t& ctx); +zwlr_layer_shell_v1_layer wayland_apply_layer_properties_v1(wayland_context_t& ctx); +uint32_t wayland_apply_anchor_properties_v1(wayland_context_t& ctx); } // namespace bongocat::platform::wayland::details diff --git a/include/platform/wayland_thread_context.h b/include/platform/wayland_thread_context.h index 2b3a9b19..1b0dbe9a 100644 --- a/include/platform/wayland_thread_context.h +++ b/include/platform/wayland_thread_context.h @@ -34,6 +34,7 @@ struct wayland_thread_context { wl_surface *surface{BONGOCAT_NULLPTR}; zwlr_layer_surface_v1 *layer_surface{BONGOCAT_NULLPTR}; struct wl_registry *registry{BONGOCAT_NULLPTR}; + uint32_t layer_shell_version{0}; // Output reconnection handling uint32_t bound_output_name{0}; // Registry name of our bound output @@ -44,15 +45,15 @@ struct wayland_thread_context { MMapMemory ctx_shm; bar_visibility_t bar_visibility{bar_visibility_t::Show}; - int32_t _bar_height{0}; // applied_height - int32_t _screen_width{0}; // applied_width + int32_t _bar_height{0}; // applied_height + int32_t _screen_width{0}; // applied_width // ref to existing name in output, Will default to automatic one if kept null - char *_output_name_str{BONGOCAT_NULLPTR}; // bound_screen_name + char *_output_name_str{BONGOCAT_NULLPTR}; // bound_screen_name bool _fullscreen_detected{false}; screen_info_t *_screen_info{BONGOCAT_NULLPTR}; config::layer_type_t _layer{config::layer_type_t::LAYER_TOP}; config::overlay_position_t _overlay_position{config::overlay_position_t::POSITION_BOTTOM}; - AllocatedString _target_output_name; // applied_output_name + AllocatedString _target_output_name; // applied_output_name // frame done callback data wl_callback *_frame_cb{BONGOCAT_NULLPTR}; @@ -148,6 +149,7 @@ inline void cleanup_wayland_context(wayland_thread_context& ctx) { if (ctx.layer_shell != BONGOCAT_NULLPTR) { zwlr_layer_shell_v1_destroy(ctx.layer_shell); ctx.layer_shell = BONGOCAT_NULLPTR; + ctx.layer_shell_version = 0; } if (ctx.xdg_wm_base != BONGOCAT_NULLPTR) { xdg_wm_base_destroy(ctx.xdg_wm_base); diff --git a/scripts/test_bongocat_12.sh b/scripts/test_bongocat_12.sh index 7f6fa89d..139cfc7c 100755 --- a/scripts/test_bongocat_12.sh +++ b/scripts/test_bongocat_12.sh @@ -68,7 +68,7 @@ sed -i -E 's/^cat_x_offset=[0-9]+/cat_x_offset=0/' "$CONFIG" sed -i -E 's/^cat_y_offset=[0-9]+/cat_y_offset=0/' "$CONFIG" sed -i -E 's/^cat_align=[a-zA-Z]+/cat_align=center/' "$CONFIG" sed -i -E 's/^enable_antialiasing=[0-9]+/enable_antialiasing=1/' "$CONFIG" -sed -i -E 's/^cat_height=[0-9]+/cat_align=60/' "$CONFIG" +sed -i -E 's/^cat_height=[0-9]+/cat_height=60/' "$CONFIG" sed -i -E 's/^overlay_height=[0-9]+/overlay_height=80/' "$CONFIG" # Test Sleep @@ -215,6 +215,56 @@ kill -USR2 "$PID" # Reload config sleep 5 +echo "[TEST] Set Position" +echo "[INFO] Top Position..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=top/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Bottom Position..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=bottom/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +echo "[INFO] Top Position..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=top/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 + +echo "[TEST] Set Layer" +echo "[INFO] Top Layer..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=bottom/" "$CONFIG" +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=top/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Bottom Layer..." +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=bottom/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Overlay Layer..." +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=overlay/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Overlay Layer..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=top/" "$CONFIG" +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=overlay/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 10 +echo "[INFO] Background Layer..." +sed -i -E "s/^overlay_position=[a-zA-Z]+/overlay_position=bottom/" "$CONFIG" +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=background/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 +echo "[INFO] Top Layer..." +sed -i -E "s/^overlay_layer=[a-zA-Z]+/overlay_layer=top/" "$CONFIG" +echo "[INFO] Send SIGUSR2" +kill -USR2 "$PID" # Reload config +sleep 5 # --- verify running --- if kill -0 "$PID" 2>/dev/null; then diff --git a/src/config/config.cpp b/src/config/config.cpp index fe3cbae7..bb23aacf 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -122,7 +122,7 @@ static inline constexpr platform::time_sec_t DEFAULT_TEST_ANIMATION_INTERVAL_SEC static inline constexpr int32_t DEFAULT_ENABLE_ANTIALIASING = 1; static inline constexpr double DEFAULT_MOVEMENT_WAIT_FACTOR = 5.1; static inline constexpr int32_t DEFAULT_ENABLE_HAND_MAPPING = 1; -// static inline constexpr int32_t DEFAULT_HOTPLUG_SCAN_INTERVAL_MS = 300; +static inline constexpr int32_t DEFAULT_HOTPLUG_SCAN_INTERVAL_MS = 30 * 1000; // Debug-specific defaults #ifndef NDEBUG @@ -1187,7 +1187,7 @@ static bongocat_error_t config_parse_integer_key(config_t& config, const char *k } else if (strcmp(key, ENABLE_HAND_MAPPING_KEY) == 0) { config.enable_hand_mapping = int_value; } else if (strcmp(key, HOTPLUG_SCAN_INTERVAL_KEY) == 0) { - config.hotplug_scan_interval_ms = int_value; + config.hotplug_scan_interval_ms = int_value * 1000; } else if (strcmp(key, DISABLE_FULLSCREEN_HIDE_KEY) == 0) { config.disable_fullscreen_hide = int_value; } else if (strcmp(key, CUSTOM_IDLE_FRAMES_KEY) == 0) { @@ -1980,7 +1980,7 @@ void set_defaults(config_t& config) { cfg.screen_width = 0; cfg.custom_sprite_sheet_filename = BONGOCAT_NULLPTR; cfg.custom_sprite_sheet_settings = {}; - // cfg.hotplug_scan_interval_ms = DEFAULT_HOTPLUG_SCAN_INTERVAL_MS; + cfg.hotplug_scan_interval_ms = DEFAULT_HOTPLUG_SCAN_INTERVAL_MS; for (int i = 0; i < static_cast(input::MAX_INPUT_DEVICES); i++) { cfg._keyboard_names[i] = BONGOCAT_NULLPTR; diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp index 40e7a924..2b1f055a 100644 --- a/src/graphics/animation.cpp +++ b/src/graphics/animation.cpp @@ -5361,7 +5361,8 @@ static void update_config_reload_sprite_sheet(animation_thread_context_t& ctx) { : old_anim_index; [[maybe_unused]] const auto t0 = platform::get_current_time_us(); - if constexpr (features::EnableLazyLoadAssets || (features::EnableBongocatSvg && ctx.shm->anim_type == config::config_animation_sprite_sheet_layout_t::Bongocat)) { + if (features::EnableLazyLoadAssets || + (features::EnableBongocatSvg && ctx.shm->anim_type == config::config_animation_sprite_sheet_layout_t::Bongocat)) { auto [result, error] = hot_load_animation(ctx); if (error != bongocat_error_t::BONGOCAT_SUCCESS) { // rollback diff --git a/src/platform/wayland.cpp b/src/platform/wayland.cpp index e50f9bd8..ae2e9286 100644 --- a/src/platform/wayland.cpp +++ b/src/platform/wayland.cpp @@ -65,8 +65,7 @@ created_result_t> create(animation::animation if (!ret->thread_context.ctx_shm) [[unlikely]] { BONGOCAT_LOG_ERROR("Failed to create shared memory for animation system: %s", strerror(errno)); return bongocat_error_t::BONGOCAT_ERROR_MEMORY; - } - if (ret->thread_context.ctx_shm) [[unlikely]] { + } else { static_assert(WAYLAND_NUM_BUFFERS <= INT_MAX); for (size_t i = 0; i < WAYLAND_NUM_BUFFERS; i++) { ret->thread_context.ctx_shm->buffers[i] = {}; @@ -138,10 +137,10 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int // from thread context wayland_thread_context& wayland_ctx = ctx.thread_context; // animation_context_t& anim = trigger_ctx.anim; - // wait for context + // wait for context ctx.animation_context->init_cond.timedwait([&]() { return atomic_load(&ctx.animation_context->ready); }, COND_INIT_TIMEOUT_MS); - input.init_cond.timedwait([&]() { return atomic_load(&ctx.animation_context->ready); }, COND_INIT_TIMEOUT_MS); + input.init_cond.timedwait([&]() { return atomic_load(&input.ready); }, COND_INIT_TIMEOUT_MS); animation::animation_context_t& animation_ctx = *ctx.animation_context; assert(animation_ctx._input != BONGOCAT_NULLPTR); assert(animation_ctx._input == &input); @@ -216,6 +215,7 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int if (flush_ret == -1 && errno == EAGAIN) { // send buffer full; need to make progress by reading pending events first wl_display_cancel_read(wayland_ctx.display); + prepared_read = false; ///< read slot released, don't call read_events if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); running = 0; @@ -223,6 +223,7 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int } else if (flush_ret == -1) { BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); wl_display_cancel_read(wayland_ctx.display); + prepared_read = false; if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { running = 0; } @@ -332,9 +333,9 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int } } - BONGOCAT_LOG_VERBOSE("Poll revents: poll_result=%d; signal=%x, reload=%x, render=%x, wayland=%x", poll_result, - fds[fds_signals_index].revents, fds[fds_config_reload_index].revents, - fds[fds_animation_render_index].revents, fds[fds_wayland_index].revents); + // BONGOCAT_LOG_VERBOSE("Poll revents: poll_result=%d; signal=%x, reload=%x, render=%x, wayland=%x", poll_result, + // fds[fds_signals_index].revents, fds[fds_config_reload_index].revents, + // fds[fds_animation_render_index].revents, fds[fds_wayland_index].revents); } else if (poll_result == 0) { if (prepared_read) { wl_display_cancel_read(wayland_ctx.display); @@ -403,15 +404,13 @@ bongocat_error_t run(wayland_context_t& ctx, volatile sig_atomic_t& running, int if (needs_flush) { const int flush_ret = wl_display_flush(wayland_ctx.display); if (flush_ret == -1 && errno == EAGAIN) { - // send buffer full; need to make progress by reading pending events first - wl_display_cancel_read(wayland_ctx.display); + // send buffer full; need to dispatch pending events to drain it if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { BONGOCAT_LOG_ERROR("wl_display_dispatch_pending failed after EAGAIN"); running = 0; } } else if (flush_ret == -1) { BONGOCAT_LOG_ERROR("wl_display_flush failed: %s", strerror(errno)); - wl_display_cancel_read(wayland_ctx.display); if (wl_display_dispatch_pending(wayland_ctx.display) == -1) { running = 0; } @@ -444,13 +443,14 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, const auto old_height = ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_height : 0; const auto old_width = ctx.thread_context._screen_width; - const auto old_layer = - ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->layer : config::layer_type_t::LAYER_TOP; - //const auto old_overlay_position = - // ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_position : config::overlay_position_t::POSITION_BOTTOM; + const auto old_layer = ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->layer + : config::layer_type_t::LAYER_TOP; + // const auto old_overlay_position = + // ctx.thread_context._local_copy_config ? ctx.thread_context._local_copy_config->overlay_position : + // config::overlay_position_t::POSITION_BOTTOM; const AllocatedString old_screen_name = ctx.thread_context._output_name_str != BONGOCAT_NULLPTR - ? duplicate_string(ctx.thread_context._output_name_str) - : make_null_string(); + ? duplicate_string(ctx.thread_context._output_name_str) + : make_null_string(); // update old config *ctx.thread_context._local_copy_config = config; @@ -459,14 +459,16 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, const bool layer_changed = (old_layer != current_config.layer); const bool position_changed = (ctx.thread_context._overlay_position != current_config.overlay_position); const bool dimensions_changed = (old_height != current_config.overlay_height) || - (current_config.screen_width > 0 && - old_width != ctx.thread_context._local_copy_config->screen_width); + (current_config.screen_width > 0 && old_width != current_config.screen_width); const bool output_name_changed = - (static_cast(old_screen_name.c_str()) != static_cast(current_config.output_name.c_str())) || + (static_cast(old_screen_name.c_str()) != static_cast(current_config.output_name.c_str()) && + static_cast(current_config.output_name.c_str())) || (old_screen_name && current_config.output_name && (strcmp(old_screen_name.c_str(), current_config.output_name.c_str()) != 0)); const bool bound_output_changed = - (static_cast(ctx.thread_context._output_name_str) != static_cast(current_config.output_name.c_str())) || + (static_cast(ctx.thread_context._output_name_str) != + static_cast(current_config.output_name.c_str()) && + static_cast(current_config.output_name.c_str())) || (ctx.thread_context._output_name_str != BONGOCAT_NULLPTR && current_config.output_name && (strcmp(ctx.thread_context._output_name_str, current_config.output_name.c_str()) != 0)); const bool screen_changed = output_name_changed || bound_output_changed; @@ -476,8 +478,11 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, // - Buffer recreate: for dimension changes (overlay_height, screen_width) // - Property update: for position/layer changes (double-buffered, no recreate) // - Cache only: for cat_height, mirror, etc. - const bool needs_full_recreate = screen_changed; - const bool needs_buffer_recreate = dimensions_changed && old_height > 0 && old_width > 0; + const bool needs_full_recreate = screen_changed || (ctx.thread_context.layer_shell_version <= 1 && layer_changed); + const bool needs_buffer_recreate = + (dimensions_changed && old_height > 0 && old_width > 0) || + (ctx.thread_context._screen_width <= 0 || ctx.thread_context._bar_height <= 0) || + (ctx.thread_context.layer_surface == BONGOCAT_NULLPTR || ctx.thread_context.surface == BONGOCAT_NULLPTR); const bool needs_property_update = layer_changed || position_changed; if (ctx.thread_context.ctx_shm) { @@ -494,8 +499,7 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, platform::LockGuard anim_guard(animation_ctx.thread_context.anim_lock); BONGOCAT_LOG_INFO("Dimensions changed (%dx%d -> %dx%d), recreating buffer...", old_width, old_height, - current_config.screen_width, - current_config.overlay_height); + current_config.screen_width, current_config.overlay_height); // Mark as not configured first assert(ctx.thread_context.ctx_shm); @@ -519,12 +523,18 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, BONGOCAT_LOG_ERROR("Failed to update width for bar"); return; } - ctx.thread_context._bar_height = current_config.overlay_height; /// @TODO: reduce unnessery updates details::wayland_update_output(ctx); details::wayland_update_current_output_info(ctx); + assert(ctx.thread_context._screen_width > 0); + if (ctx.thread_context._screen_width <= 0) [[unlikely]] { + // keep old screen_width + ctx.thread_context._screen_width = old_width; + } + ctx.thread_context._bar_height = current_config.overlay_height; + // Recreate surface and buffer with new dimensions if (details::wayland_setup_surface(ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { BONGOCAT_LOG_ERROR("Failed to recreate surface after config change"); @@ -534,6 +544,12 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, /// @TODO: reduce unnessery updates details::wayland_update_output(ctx); details::wayland_update_current_output_info(ctx); + assert(ctx.thread_context._screen_width > 0); + if (ctx.thread_context._screen_width <= 0) [[unlikely]] { + // keep old screen_width + ctx.thread_context._screen_width = old_width; + } + ctx.thread_context._bar_height = current_config.overlay_height; if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); @@ -543,10 +559,8 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, ctx.thread_context._overlay_position = current_config.overlay_position; ctx.thread_context._target_output_name = duplicate_string(config.output_name); - atomic_store(&ctx.thread_context.ctx_shm->configured, true); - - BONGOCAT_LOG_INFO("Buffer recreated successfully (%dx%d)", current_config.screen_width, - current_config.overlay_height); + BONGOCAT_LOG_INFO("Buffer recreated successfully (%dx%d)", ctx.thread_context._screen_width, + ctx.thread_context._bar_height); } else { BONGOCAT_LOG_ERROR("Buffer recreated failed (%dx%d)", current_config.screen_width, current_config.overlay_height); @@ -557,21 +571,33 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, wl_display_roundtrip(ctx.thread_context.display); details::wayland_update_current_output_info(ctx); - BONGOCAT_LOG_INFO("Surface recreated successfully (%dx%d)", - current_config.screen_width, current_config.overlay_height); + if (!atomic_load(&ctx.thread_context.ctx_shm->configured)) { + // Compositor did not send a configure event (permitted if logical size + // did not change from its perspective). Mark as configured so rendering + // is not permanently suppressed. + BONGOCAT_LOG_WARNING("No configure event after buffer resize - assuming configured"); + atomic_store(&ctx.thread_context.ctx_shm->configured, true); + } + assert(atomic_load(&ctx.thread_context.ctx_shm->configured)); + + BONGOCAT_LOG_INFO("Surface recreated successfully (%dx%d)", ctx.thread_context._screen_width, + ctx.thread_context._bar_height); } else if (needs_buffer_recreate) { // PATH 2: Dimensions changed - update size property, recreate buffer only - BONGOCAT_LOG_INFO("Overlay dimensions changed (%dx%d -> %dx%d)", old_width, - old_height, current_config.screen_width, current_config.overlay_height); + BONGOCAT_LOG_INFO("Overlay dimensions changed (%dx%d -> %dx%d)", old_width, old_height, + current_config.screen_width, current_config.overlay_height); assert(current_config.overlay_height >= 0 && current_config.screen_width <= INT32_MAX); // Update double-buffered properties on existing layer surface - zwlr_layer_surface_v1_set_size(ctx.thread_context.layer_surface, 0, static_cast(current_config.overlay_height)); + zwlr_layer_surface_v1_set_size(ctx.thread_context.layer_surface, 0, + static_cast(current_config.overlay_height)); if (position_changed) { - details::wayland_apply_layer_properties(ctx); + details::wayland_apply_anchor_properties_v1(ctx); } if (layer_changed) { - details::wayland_apply_anchor_properties(ctx); + /// @NOTE: when layer_shell_version is version, buffer needs to be recreated + assert(ctx.thread_context.layer_shell_version >= 2); + details::wayland_apply_layer_properties_v1(ctx); } wl_surface_commit(ctx.thread_context.surface); @@ -582,6 +608,11 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, // Cleanup old buffer cleanup_wayland_context_buffer(ctx.thread_context); + if (ctx.thread_context._screen_width <= 0) { + // keep old screen_width + ctx.thread_context._screen_width = old_width; + } + ctx.thread_context._bar_height = current_config.overlay_height; if (details::wayland_setup_buffer(ctx.thread_context, animation_ctx) != bongocat_error_t::BONGOCAT_SUCCESS) { BONGOCAT_LOG_ERROR("Failed to recreate buffer after config change"); @@ -592,15 +623,26 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, // Wait for new configure event wl_display_roundtrip(ctx.thread_context.display); - BONGOCAT_LOG_INFO("Buffer resized successfully (%dx%d)", - current_config.screen_width, current_config.overlay_height); + if (!atomic_load(&ctx.thread_context.ctx_shm->configured)) { + // Compositor did not send a configure event (permitted if logical size + // did not change from its perspective). Mark as configured so rendering + // is not permanently suppressed. + BONGOCAT_LOG_WARNING("No configure event after buffer resize - assuming configured"); + atomic_store(&ctx.thread_context.ctx_shm->configured, true); + } + assert(atomic_load(&ctx.thread_context.ctx_shm->configured)); + + BONGOCAT_LOG_INFO("Buffer resized successfully (%dx%d)", current_config.screen_width, + current_config.overlay_height); } else if (needs_property_update) { // PATH 1: Position/layer only - no buffer changes needed if (position_changed) { - details::wayland_apply_layer_properties(ctx); + details::wayland_apply_anchor_properties_v1(ctx); } if (layer_changed) { - details::wayland_apply_anchor_properties(ctx); + /// @NOTE: when layer_shell_version is version, buffer needs to be recreated + assert(ctx.thread_context.layer_shell_version >= 2); + details::wayland_apply_layer_properties_v1(ctx); } wl_surface_commit(ctx.thread_context.surface); wl_display_roundtrip(ctx.thread_context.display); @@ -610,6 +652,12 @@ void update_config(wayland_context_t& ctx, const config::config_t& config, /// @NOTE: assume animation has the same local copy as wayland config // animation_update_config(anim, config); if (atomic_load(&ctx.thread_context.ctx_shm->configured)) { + assert(ctx.thread_context._screen_width > 0); + ctx.thread_context._bar_height = current_config.overlay_height; + ctx.thread_context._layer = current_config.layer; + ctx.thread_context._overlay_position = current_config.overlay_position; + ctx.thread_context._target_output_name = duplicate_string(config.output_name); + request_render(animation_ctx); } } diff --git a/src/platform/wayland_callbacks.cpp b/src/platform/wayland_callbacks.cpp index 0a40aaba..be856786 100644 --- a/src/platform/wayland_callbacks.cpp +++ b/src/platform/wayland_callbacks.cpp @@ -59,8 +59,8 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; // read-only config - // assert(wayland_ctx._local_copy_config != nullptr); - // const config::config_t& current_config = *wayland_ctx._local_copy_config; + assert(wayland_ctx.thread_context._local_copy_config != nullptr); + const config::config_t& current_config = *wayland_ctx.thread_context._local_copy_config; // Check if this is the output we're waiting for (reconnection case) if (!atomic_load(&wayland_ctx._output_lost)) { @@ -69,25 +69,27 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out bool should_reconnect = false; // Case 1: User specified an output name - match exactly - if (wayland_thread_ctx.using_named_output && wayland_thread_ctx._output_name_str != BONGOCAT_NULLPTR) { - should_reconnect = (strcmp(name, wayland_thread_ctx._output_name_str) == 0); + if (wayland_thread_ctx.using_named_output && current_config.output_name) { + should_reconnect = (strcmp(name, current_config.output_name.c_str()) == 0); } // Case 2: Using fallback (first output) - reconnect to any output else if (!wayland_thread_ctx.using_named_output) { should_reconnect = true; - BONGOCAT_LOG_DEBUG("Using fallback output, accepting '%s'", name); + BONGOCAT_LOG_DEBUG("xdg_output.name: Using fallback output, accepting '%s'", name); } if (should_reconnect) { - BONGOCAT_LOG_INFO("Target output '%s' reconnected!", name); - - // Clean up old surface if it exists - cleanup_wayland_context_surface(wayland_thread_ctx); + BONGOCAT_LOG_INFO("xdg_output.name: Target output '%s' reconnected!", name); // Set new output wayland_thread_ctx.output = oref->wl_output; wayland_thread_ctx.bound_output_name = oref->name; atomic_store(&oref->wayland->_output_lost, false); + wl_display_roundtrip(wayland_thread_ctx.display); + wayland_update_output(wayland_ctx); + + // Clean up old surface if it exists + cleanup_wayland_context_surface(wayland_thread_ctx); // Recreate surface on new output // Note: wayland_setup_surface already commits, triggering a configure @@ -96,19 +98,20 @@ void handle_xdg_output_name(void *data, [[maybe_unused]] zxdg_output_v1 *xdg_out if (wayland_setup_surface(*oref->wayland) == bongocat_error_t::BONGOCAT_SUCCESS) { assert(wayland_thread_ctx.ctx_shm); if (wayland_thread_ctx.ctx_shm) { - atomic_store(&wayland_thread_ctx.ctx_shm->configured, true); - } - if constexpr (WAYLAND_NUM_BUFFERS != 1) { - // Wait for configure event to be processed - wl_display_roundtrip(wayland_thread_ctx.display); - wayland_update_current_output_info(wayland_ctx); + /// @TODO: premature configured ? + // atomic_store(&wayland_thread_ctx.ctx_shm->configured, true); + if (!atomic_load(&wayland_thread_ctx.ctx_shm->configured)) { + BONGOCAT_LOG_WARNING("xdg_output.name: assuming configured, yet"); + } } + // Wait for configure event to be processed + wl_display_roundtrip(wayland_thread_ctx.display); if (oref->wayland->animation_context != BONGOCAT_NULLPTR) { request_render(*oref->wayland->animation_context); } - BONGOCAT_LOG_INFO("Surface recreated, configure event processed"); + BONGOCAT_LOG_INFO("xdg_output.name: Surface recreated, configure event processed"); } else { - BONGOCAT_LOG_ERROR("Failed to recreate surface on reconnected output"); + BONGOCAT_LOG_ERROR("xdg_output.name: Failed to recreate surface on reconnected output"); } } } @@ -327,31 +330,71 @@ void fs_handle_toplevel_state(void *data, [[maybe_unused]] zwlr_foreign_toplevel } /// @NOTE: tracked.output can always be NULL when no output.enter/output.leave event were triggered + bool output_found = false; + bool handle_tracked = false; + bool handle_has_output = false; for (size_t i = 0; i < ctx.num_toplevels; ++i) { if (ctx.tracked_toplevels[i].handle == handle) { - auto [output_found, changed] = update_fullscreen_state_toplevel( + handle_tracked = true; + handle_has_output |= ctx.tracked_toplevels[i].output == BONGOCAT_NULLPTR; + auto [output_found_result, changed] = update_fullscreen_state_toplevel( ctx, ctx.tracked_toplevels[i], {.is_fullscreen = is_fullscreen, .is_activated = is_activated}); - if (output_found) { + output_found |= output_found_result; + + if (output_found_result) { if (changed) { BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: (fullscreen=%d;activated=%d)", is_fullscreen, is_activated); } - return; } } } - // fallback: check for hyprland active fullscreen windows - if (const int result = hyprland::fs_update_state(ctx); result >= 0) { - BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d (hyprland)", result); + // This toplevel is known to belong to a different output. Do not use + // compositor-global fallbacks, otherwise fullscreen on monitor A can hide + // overlay on monitor B. + if (handle_tracked && handle_has_output && !output_found) { return; } - // Fallback for when no toplevel was found - const bool changed = fs_update_state(ctx, {.is_fullscreen = is_fullscreen, .is_activated = is_activated}); - if (changed) { - BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: (fullscreen=%d;activated=%d)", - is_fullscreen, is_activated); + // fallback: check for hyprland active fullscreen windows + if (!output_found) { + if (const int result = hyprland::fs_update_state(ctx); result >= 0) { + output_found = true; + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: %d (hyprland)", result); + } + } + + // fallback: global fullscreen + // Safe when single-output, or when the compositor never sends output_enter + // events (e.g. KDE/KWin). In the latter case, per-output tracking is + // impossible, so we use the global activated+fullscreen state as a best + // effort. On multi-monitor setups without output events, fullscreen on + // any monitor will hide the overlay on all monitors - acceptable trade-off + // vs never hiding at all. + if (!output_found && (ctx.output_count <= 1 || !atomic_load(&ctx._compositor_sends_output_events))) { + // Case 1: Window becomes active - update state based on its fullscreen + // status + if (is_activated) { + atomic_store(&ctx._active_toplevel_fullscreen, is_fullscreen); + + const bool changed = fs_update_state(ctx, {.is_fullscreen = is_fullscreen, .is_activated = is_activated}); + if (changed) { + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: (fullscreen=%d;activated=%d)", + is_fullscreen, is_activated); + } + } + // Case 2: Previously active fullscreen window loses activation + // (e.g., switching to empty workspace) - show bongocat + else if (!is_activated) { + atomic_store(&ctx._active_toplevel_fullscreen, false); + + const bool changed = fs_update_state(ctx, {.is_fullscreen = false, .is_activated = is_activated}); + if (changed) { + BONGOCAT_LOG_VERBOSE("fs_handle_toplevel.state: Update fullscreen state: (fullscreen=%d;activated=%d)", + is_fullscreen, is_activated); + } + } } } @@ -366,6 +409,21 @@ void fs_handle_toplevel_closed(void *data, zwlr_foreign_toplevel_handle_v1 *hand return; } + bool closed_on_current_output = false; + bool closed_was_fullscreen = false; + for (size_t i = 0; i < ctx.num_toplevels; ++i) { + if (ctx.tracked_toplevels[i].handle == handle) { + closed_on_current_output = ctx.tracked_toplevels[i].output == ctx.thread_context.output; + closed_was_fullscreen = ctx.tracked_toplevels[i].is_fullscreen; + break; + } + } + + if (closed_on_current_output && closed_was_fullscreen) { + ctx._active_toplevel_fullscreen = false; + fs_update_state(ctx, {.is_fullscreen = false, .is_activated = true}); + } + // remove from tracked_toplevels if present for (size_t i = 0; i < ctx.num_toplevels; ++i) { if (ctx.tracked_toplevels[i].handle == handle) { @@ -417,6 +475,8 @@ void fs_handle_output_enter(void *data, [[maybe_unused]] zwlr_foreign_toplevel_h } wayland_context_t& ctx = *static_cast(data); + atomic_store(&ctx._compositor_sends_output_events, true); + for (size_t i = 0; i < ctx.num_toplevels; i++) { auto& tracked = ctx.tracked_toplevels[i]; if (tracked.handle == handle) { @@ -537,15 +597,14 @@ static void screen_calculate_dimensions(screen_info_t& screen_info) { if (screen_info.received == screen_info_received_flags_t::None || (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Geometry)) == 0 || - (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Mode)) == 0 || - screen_info.screen_width > 0) { + (static_cast(screen_info.received) & static_cast(screen_info_received_flags_t::Mode)) == 0) { return; } - const bool is_rotated = - (screen_info.transform == WL_OUTPUT_TRANSFORM_90 || screen_info.transform == WL_OUTPUT_TRANSFORM_270 || - screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 || - screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); + const bool is_rotated = screen_info.transform == WL_OUTPUT_TRANSFORM_90 || + screen_info.transform == WL_OUTPUT_TRANSFORM_270 || + screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 || + screen_info.transform == WL_OUTPUT_TRANSFORM_FLIPPED_270; if (is_rotated) { screen_info.screen_width = screen_info.raw_height; @@ -763,11 +822,15 @@ void frame_done(void *data, wl_callback *cb, [[maybe_unused]] uint32_t time) { // Fallback for missed buffer.release platform::wayland::wayland_shared_memory_t *shm_ctx = wayland_ctx.ctx_shm.ptr; assert(shm_ctx); - for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { - auto& buf = shm_ctx->buffers[i]; - if (atomic_load(&buf.busy)) { - atomic_store(&buf.busy, false); - BONGOCAT_LOG_WARNING("wl_callback.done: fallback released stuck buffer %zu (missed wl_buffer.release)", i); + /// @TODO: a second buffer may have been submitted after the one whose frame callback just fired. That buffer may + /// still be actively read by the compositor. track the submitted buffer pointer explicitly and only clear that one. + if constexpr (platform::wayland::WAYLAND_NUM_BUFFERS == 1) { + for (size_t i = 0; i < platform::wayland::WAYLAND_NUM_BUFFERS; i++) { + auto& buf = shm_ctx->buffers[i]; + if (atomic_load(&buf.busy)) { + atomic_store(&buf.busy, false); + BONGOCAT_LOG_WARNING("wl_callback.done: fallback released stuck buffer %zu (missed wl_buffer.release)", i); + } } } @@ -782,41 +845,58 @@ void frame_done(void *data, wl_callback *cb, [[maybe_unused]] uint32_t time) { // WAYLAND PROTOCOL REGISTRY // ============================================================================= -void registry_global(void *data, wl_registry *reg, uint32_t name, const char *iface, [[maybe_unused]] uint32_t ver) { +inline uint32_t bind_min_ver(uint32_t v, uint32_t desired) { + return v < desired ? v : desired; +} +void registry_global(void *data, wl_registry *reg, uint32_t name, const char *iface, uint32_t ver) { if (data == BONGOCAT_NULLPTR) { BONGOCAT_LOG_VERBOSE("Handler called with null data (ignored)"); return; } wayland_context_t& ctx = *static_cast(data); - BONGOCAT_LOG_VERBOSE("wl_registry.global: registry received: %s", iface); + BONGOCAT_LOG_VERBOSE("wl_registry.global: registry received: %s; version: %u", iface, ver); + + constexpr uint32_t wl_compositor_interface_version = 4; + constexpr uint32_t wl_shm_interface_version = 1; + constexpr uint32_t zwlr_layer_shell_v1_interface_version = 4; + constexpr uint32_t xdg_wm_base_interface_version = 1; + constexpr uint32_t zxdg_output_manager_v1_interface_version = 3; + constexpr uint32_t wl_output_interface_version = 2; + constexpr uint32_t zwlr_foreign_toplevel_manager_v1_interface_version = 3; if (strcmp(iface, wl_compositor_interface.name) == 0) { - ctx.thread_context.compositor = - static_cast(wl_registry_bind(reg, name, &wl_compositor_interface, 4)); + ctx.thread_context.compositor = static_cast( + wl_registry_bind(reg, name, &wl_compositor_interface, bind_min_ver(ver, wl_compositor_interface_version))); BONGOCAT_LOG_VERBOSE("wl_registry.global: compositor registry bind"); } else if (strcmp(iface, wl_shm_interface.name) == 0) { - ctx.thread_context.shm = static_cast(wl_registry_bind(reg, name, &wl_shm_interface, 1)); + ctx.thread_context.shm = static_cast( + wl_registry_bind(reg, name, &wl_shm_interface, bind_min_ver(ver, wl_shm_interface_version))); BONGOCAT_LOG_VERBOSE("wl_registry.global: shm registry bind"); } else if (strcmp(iface, zwlr_layer_shell_v1_interface.name) == 0) { + const uint32_t bind_version = bind_min_ver(ver, zwlr_layer_shell_v1_interface_version); ctx.thread_context.layer_shell = - static_cast(wl_registry_bind(reg, name, &zwlr_layer_shell_v1_interface, 1)); + static_cast(wl_registry_bind(reg, name, &zwlr_layer_shell_v1_interface, bind_version)); + ctx.thread_context.layer_shell_version = bind_version; BONGOCAT_LOG_VERBOSE("wl_registry.global: layer_shell registry bind"); + BONGOCAT_LOG_VERBOSE("wl_registry.global: layer_shell version (compositor): %u, bound: %u", + ctx.thread_context.layer_shell_version, bind_version); } else if (strcmp(iface, xdg_wm_base_interface.name) == 0) { - ctx.thread_context.xdg_wm_base = static_cast(wl_registry_bind(reg, name, &xdg_wm_base_interface, 1)); + ctx.thread_context.xdg_wm_base = static_cast( + wl_registry_bind(reg, name, &xdg_wm_base_interface, bind_min_ver(ver, xdg_wm_base_interface_version))); BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_wm_base registry bind"); if (ctx.thread_context.xdg_wm_base != BONGOCAT_NULLPTR) { xdg_wm_base_add_listener(ctx.thread_context.xdg_wm_base, &xdg_wm_base_listener, &ctx); } } else if (strcmp(iface, zxdg_output_manager_v1_interface.name) == 0) { - ctx.xdg_output_manager = - static_cast(wl_registry_bind(reg, name, &zxdg_output_manager_v1_interface, 3)); + ctx.xdg_output_manager = static_cast(wl_registry_bind( + reg, name, &zxdg_output_manager_v1_interface, bind_min_ver(ver, zxdg_output_manager_v1_interface_version))); BONGOCAT_LOG_VERBOSE("wl_registry.global: xdg_output_manager registry bind"); } else if (strcmp(iface, wl_output_interface.name) == 0) { if (ctx.output_count < MAX_OUTPUTS) { ctx.outputs[ctx.output_count].name = name; - ctx.outputs[ctx.output_count].wl_output = - static_cast(wl_registry_bind(reg, name, &wl_output_interface, 2)); + ctx.outputs[ctx.output_count].wl_output = static_cast( + wl_registry_bind(reg, name, &wl_output_interface, bind_min_ver(ver, wl_output_interface_version))); wl_output_add_listener(ctx.outputs[ctx.output_count].wl_output, &output_listener, &ctx); BONGOCAT_LOG_VERBOSE("wl_registry.global: wl_output registry bind: %i", ctx.output_count); @@ -837,7 +917,8 @@ void registry_global(void *data, wl_registry *reg, uint32_t name, const char *if } } else if (strcmp(iface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { ctx.fs_detector.manager = static_cast( - wl_registry_bind(reg, name, &zwlr_foreign_toplevel_manager_v1_interface, 3)); + wl_registry_bind(reg, name, &zwlr_foreign_toplevel_manager_v1_interface, + bind_min_ver(ver, zwlr_foreign_toplevel_manager_v1_interface_version))); BONGOCAT_LOG_VERBOSE("wl_registry.global: foreign_toplevel_manager (fs_detector.manager) registry bind"); if (ctx.fs_detector.manager != BONGOCAT_NULLPTR) { zwlr_foreign_toplevel_manager_v1_add_listener(ctx.fs_detector.manager, &fs_manager_listener, &ctx); @@ -866,34 +947,57 @@ void registry_remove(void *data, [[maybe_unused]] wl_registry *registry, [[maybe BONGOCAT_LOG_VERBOSE("wl_registry.global_remove: registry received"); + size_t removed_index = ctx.output_count; + for (size_t i = 0; i < ctx.output_count; ++i) { + if (ctx.outputs[i].name == name) { + removed_index = i; + break; + } + } + if (removed_index >= ctx.output_count) { + return; + } + // Check if the removed global is our bound output - if (name == ctx.thread_context.bound_output_name && ctx.thread_context.bound_output_name != 0) { - BONGOCAT_LOG_VERBOSE("Bound output disconnected (registry name %u)", name); + const bool removed_bound = name == ctx.thread_context.bound_output_name && ctx.thread_context.bound_output_name != 0; + if (removed_bound) { + BONGOCAT_LOG_VERBOSE("wl_registry.global_remove: Bound output disconnected (registry name %u)", name); atomic_store(&ctx._output_lost, true); atomic_store(&wayland_ctx_shm.configured, false); // Clean up the old output reference wayland_ctx.output = BONGOCAT_NULLPTR; + wayland_ctx.bound_output_name = 0; + wayland_ctx._output_name_str = BONGOCAT_NULLPTR; + } - // Remove from outputs array + // Remove from outputs array + for (size_t i = 0; i < ctx.output_count; ++i) { + if (ctx.outputs[i].name == name) { + if (ctx.outputs[i].xdg_output != BONGOCAT_NULLPTR) { + zxdg_output_v1_destroy(ctx.outputs[i].xdg_output); + ctx.outputs[i].xdg_output = BONGOCAT_NULLPTR; + } + if (ctx.outputs[i].wl_output != BONGOCAT_NULLPTR) { + wl_output_destroy(ctx.outputs[i].wl_output); + ctx.outputs[i].wl_output = BONGOCAT_NULLPTR; + } + // Shift remaining outputs + for (size_t j = i; j < ctx.output_count - 1; ++j) { + ctx.outputs[j] = ctx.outputs[j + 1]; + } + // Zero out the now-unused slot + ctx.outputs[ctx.output_count - 1] = {}; + ctx.output_count--; + break; + } + } + + if (!removed_bound && wayland_ctx.output != BONGOCAT_NULLPTR) { + wayland_ctx._output_name_str = BONGOCAT_NULLPTR; for (size_t i = 0; i < ctx.output_count; ++i) { - if (ctx.outputs[i].name == name) { - if (ctx.outputs[i].xdg_output != BONGOCAT_NULLPTR) { - zxdg_output_v1_destroy(ctx.outputs[i].xdg_output); - ctx.outputs[i].xdg_output = BONGOCAT_NULLPTR; - } - if (ctx.outputs[i].wl_output != BONGOCAT_NULLPTR) { - wl_output_destroy(ctx.outputs[i].wl_output); - ctx.outputs[i].wl_output = BONGOCAT_NULLPTR; - } - // Shift remaining outputs - for (size_t j = i; j < ctx.output_count - 1; ++j) { - ctx.outputs[j] = ctx.outputs[j + 1]; - } - // Zero out the now-unused slot - ctx.outputs[ctx.output_count - 1] = {}; - ctx.output_count--; - break; + if (ctx.outputs[i].wl_output == wayland_ctx.output) { + wayland_ctx._output_name_str = ctx.outputs[i].name_str; } } } @@ -912,7 +1016,8 @@ void wayland_handle_output_reconnect(output_ref_t *oref, struct wl_output *new_o // assert(wayland_ctx._local_copy_config != nullptr); // const config::config_t& current_config = *wayland_ctx._local_copy_config; - BONGOCAT_LOG_INFO("Output '%s' reconnected (registry name %u)", output_name, registry_name); + BONGOCAT_LOG_INFO("wayland_handle_output_reconnect: Output '%s' reconnected (registry name %u)", output_name, + registry_name); // Clean up old surface if it exists cleanup_wayland_context_surface(wayland_ctx); @@ -926,17 +1031,19 @@ void wayland_handle_output_reconnect(output_ref_t *oref, struct wl_output *new_o if (wayland_setup_surface(*oref->wayland) == bongocat_error_t::BONGOCAT_SUCCESS) { assert(wayland_ctx.ctx_shm.ptr && wayland_ctx.ctx_shm.ptr != MAP_FAILED); if (wayland_ctx.ctx_shm) { - atomic_store(&wayland_ctx.ctx_shm->configured, true); - } - BONGOCAT_LOG_INFO("Surface recreated on reconnected output"); - if constexpr (WAYLAND_NUM_BUFFERS != 1) { - wl_display_roundtrip(wayland_ctx.display); + /// @TODO: pre-matured configured ? + // atomic_store(&wayland_ctx.ctx_shm->configured, true); + if (!atomic_load(&wayland_ctx.ctx_shm->configured)) { + BONGOCAT_LOG_WARNING("wayland_handle_output_reconnect: assuming configured, yet"); + } } + BONGOCAT_LOG_INFO("wayland_handle_output_reconnect: Surface recreated on reconnected output"); + wl_display_roundtrip(wayland_ctx.display); if (oref->wayland->animation_context != BONGOCAT_NULLPTR) { request_render(*oref->wayland->animation_context); } } else { - BONGOCAT_LOG_ERROR("Failed to recreate surface on reconnected output"); + BONGOCAT_LOG_ERROR("wayland_handle_output_reconnect: Failed to recreate surface on reconnected output"); } } } // namespace bongocat::platform::wayland::details diff --git a/src/platform/wayland_setups.cpp b/src/platform/wayland_setups.cpp index dadbc6fb..f888da43 100644 --- a/src/platform/wayland_setups.cpp +++ b/src/platform/wayland_setups.cpp @@ -84,6 +84,8 @@ bongocat_error_t wayland_update_output(wayland_context_t& ctx) { wayland_ctx.output = BONGOCAT_NULLPTR; wayland_ctx.bound_output_name = 0; wayland_ctx.using_named_output = false; + wayland_ctx._output_name_str = BONGOCAT_NULLPTR; + if (current_config.output_name) { for (size_t i = 0; i < ctx.output_count; ++i) { if (has_flag(ctx.outputs[i].received, output_ref_received_flags_t::Name) && @@ -93,7 +95,9 @@ bongocat_error_t wayland_update_output(wayland_context_t& ctx) { wayland_ctx._screen_info = &ctx.screen_infos[i]; wayland_ctx.bound_output_name = ctx.outputs[i].name; // Store registry name for tracking wayland_ctx.using_named_output = true; // User specified this output - BONGOCAT_LOG_INFO("Matched output: %s", wayland_ctx._output_name_str); + + BONGOCAT_LOG_INFO("Matched output: %s (registry name %u, %s)", ctx.outputs[i].name_str, + wayland_ctx.bound_output_name, wayland_ctx._output_name_str); break; } } @@ -116,7 +120,9 @@ bongocat_error_t wayland_update_output(wayland_context_t& ctx) { wayland_ctx._screen_info = &ctx.screen_infos[0]; wayland_ctx.bound_output_name = ctx.outputs[0].name; wayland_ctx.using_named_output = false; // Using fallback, not a named output - BONGOCAT_LOG_WARNING("Falling back to first output: %s", wayland_ctx._output_name_str); + + BONGOCAT_LOG_WARNING("Falling back to first output (registry name %u, %s)", wayland_ctx.bound_output_name, + wayland_ctx._output_name_str); } return bongocat_error_t::BONGOCAT_SUCCESS; @@ -143,8 +149,7 @@ created_result_t wayland_update_current_output_info(wayland_context_t& ctx) bound_screen_name = ctx.outputs[i].name_str; if (ctx.screen_infos[i].screen_width > 0 && ctx.screen_infos[i].screen_width <= INT16_MAX) { - BONGOCAT_LOG_INFO("Detected screen width: %d", - ctx.screen_infos[i].screen_width); + BONGOCAT_LOG_INFO("Detected screen width: %d", ctx.screen_infos[i].screen_width); screen_info = &ctx.screen_infos[i]; screen_width = ctx.screen_infos[i].screen_width; output_found = true; @@ -160,12 +165,12 @@ created_result_t wayland_update_current_output_info(wayland_context_t& ctx) } if (output_found) { + // assert(screen_width > 0); ctx.thread_context._output_name_str = bound_screen_name; ctx.thread_context._screen_info = screen_info; ctx.thread_context._screen_width = screen_width; } else { - BONGOCAT_LOG_WARNING("No output found, using default screen width: %d", - DEFAULT_SCREEN_WIDTH); + BONGOCAT_LOG_WARNING("No output found, using default screen width: %d", DEFAULT_SCREEN_WIDTH); } return screen_width; @@ -238,8 +243,10 @@ bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { ctx.outputs[i].wayland = &ctx; ctx.outputs[i].xdg_output = zxdg_output_manager_v1_get_xdg_output(ctx.xdg_output_manager, ctx.outputs[i].wl_output); - zxdg_output_v1_add_listener(ctx.outputs[i].xdg_output, &details::xdg_output_listener, &ctx.outputs[i]); ctx.screen_infos[i] = {}; + ctx.screen_infos[i].wl_output = ctx.outputs[i].wl_output; + zxdg_output_v1_add_listener(ctx.outputs[i].xdg_output, &details::xdg_output_listener, &ctx.outputs[i]); + assert(ctx.outputs[i].wl_output); ctx.screen_infos[i].wl_output = ctx.outputs[i].wl_output; } @@ -283,7 +290,8 @@ bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { // Configure screen dimensions wayland_update_current_output_info(ctx); - auto [result_update_screen_width, result_update_screen_width_error] = wayland_update_current_output_info(ctx); // wayland_update_screen_info(ctx); + auto [result_update_screen_width, result_update_screen_width_error] = + wayland_update_current_output_info(ctx); // wayland_update_screen_info(ctx); if (result_update_screen_width_error != bongocat_error_t::BONGOCAT_SUCCESS) { wl_registry_destroy(registry); return result_update_screen_width_error; @@ -305,7 +313,7 @@ bongocat_error_t wayland_setup_protocols(wayland_context_t& ctx) { return bongocat_error_t::BONGOCAT_SUCCESS; } -zwlr_layer_shell_v1_layer wayland_apply_layer_properties(wayland_context_t& ctx) { +zwlr_layer_shell_v1_layer wayland_apply_layer_properties_v1(wayland_context_t& ctx) { wayland_thread_context& wayland_ctx = ctx.thread_context; // animation_context_t& anim = *ctx.animation_context; // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; @@ -331,13 +339,14 @@ zwlr_layer_shell_v1_layer wayland_apply_layer_properties(wayland_context_t& ctx) } if (wayland_ctx.layer_surface != BONGOCAT_NULLPTR) { + assert(wayland_ctx.layer_shell_version >= 2); zwlr_layer_surface_v1_set_layer(wayland_ctx.layer_surface, layer); - BONGOCAT_LOG_INFO("Layer changed to %x", layer); + BONGOCAT_LOG_INFO("Layer changed to %i", layer); } return layer; } -uint32_t wayland_apply_anchor_properties(wayland_context_t& ctx) { +uint32_t wayland_apply_anchor_properties_v1(wayland_context_t& ctx) { wayland_thread_context& wayland_ctx = ctx.thread_context; // animation_context_t& anim = *ctx.animation_context; // animation_trigger_context_t& trigger_ctx = *ctx.animation_trigger_context; @@ -355,14 +364,15 @@ uint32_t wayland_apply_anchor_properties(wayland_context_t& ctx) { anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; break; default: - BONGOCAT_LOG_ERROR("Invalid overlay_position %d for layer surface, set to top (default)"); + BONGOCAT_LOG_ERROR("Invalid overlay_position %d for layer surface, set to top (default)", + static_cast(current_config.overlay_position)); anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; break; } if (wayland_ctx.layer_surface != BONGOCAT_NULLPTR) { zwlr_layer_surface_v1_set_anchor(wayland_ctx.layer_surface, anchor); - BONGOCAT_LOG_INFO("Overlay position changed to %x", anchor); + BONGOCAT_LOG_INFO("Overlay position changed to %u", anchor); } return anchor; @@ -413,7 +423,7 @@ bongocat_error_t wayland_setup_surface(wayland_context_t& ctx) { zwlr_layer_surface_v1_destroy(wayland_ctx.layer_surface); return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; } - wayland_apply_anchor_properties(ctx); + wayland_apply_anchor_properties_v1(ctx); zwlr_layer_surface_v1_set_size(wayland_ctx.layer_surface, 0, static_cast(wayland_ctx._bar_height)); zwlr_layer_surface_v1_set_exclusive_zone(wayland_ctx.layer_surface, -1); zwlr_layer_surface_v1_set_keyboard_interactivity(wayland_ctx.layer_surface, @@ -451,7 +461,7 @@ bongocat_error_t wayland_setup_buffer(wayland_thread_context& wayland_context, static_assert(RGBA_CHANNELS >= 0); const size_t buffer_size = static_cast(buffer_width) * static_cast(buffer_height) * RGBA_CHANNELS; if (buffer_size <= 0) { - BONGOCAT_LOG_ERROR("Invalid buffer size: %d", buffer_size); + BONGOCAT_LOG_ERROR("Invalid buffer size: %zu", buffer_size); return bongocat_error_t::BONGOCAT_ERROR_WAYLAND; } @@ -561,15 +571,15 @@ spawn_pipe_t safe_popen_read_spawn(wayland_context_t& ctx, const char *path, con if (posix_spawn(&pid, path, &actions, BONGOCAT_NULLPTR, const_cast(argv), ctx._environ) != 0) { close(pipefd[0]); close(pipefd[1]); - //pipefd[0] = -1; - //pipefd[1] = -1; + // pipefd[0] = -1; + // pipefd[1] = -1; posix_spawn_file_actions_destroy(&actions); return result; } posix_spawn_file_actions_destroy(&actions); close(pipefd[1]); - //pipefd[1] = -1; + // pipefd[1] = -1; result.fp = fdopen(pipefd[0], "r"); result.pid = pid; From 677b27296a89ae2ae94c8a43f787b3503a8b6f71 Mon Sep 17 00:00:00 2001 From: furudbat Date: Sun, 19 Apr 2026 18:18:29 +0200 Subject: [PATCH 16/17] wip: merge from upstream * update nix stuff --- nix/README.md | 6 +++--- nix/default.nix | 24 ++++++------------------ nix/shell.nix | 3 --- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/nix/README.md b/nix/README.md index f0124c3b..09039d48 100644 --- a/nix/README.md +++ b/nix/README.md @@ -4,7 +4,7 @@ - **`default.nix`** - Main package derivation - **`nixos-module.nix`** - NixOS system module - **`shell.nix`** - Development environment -- **`scripts/test-nix-build.sh`** - Test script for validating builds +- **`scripts/test_nix_build.sh`** - Test script for validating builds ## Quick Usage Run these commands from the root directory of the project where `flake.nix` is. @@ -19,7 +19,7 @@ nix build nix run ./#default # Test all Nix builds -./scripts/test-nix-build.sh +./scripts/test_nix_build.sh ``` -See [NIXOS.md](NIXOS.md) for further information. +See [NIXOS.md](NIXOS.md) for further information. \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index 66bfe541..598c7040 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -4,22 +4,23 @@ pkg-config, wayland, wayland-protocols, - wayland-scanner, cmake, pandoc, libffi, systemd, libcap, - libxkbcommon + libxkbcommon, }: gcc15Stdenv.mkDerivation (finalAttrs: { pname = "wayland-vpets"; - version = "3.6.2"; + version = "4.0.0"; src = ../.; # Build toolchain and dependencies + # Protocol bindings are pre-generated and committed to git, so + # wayland-scanner and wayland-protocols are only needed for `make protocols`. strictDeps = true; - nativeBuildInputs = [pkg-config cmake wayland-scanner pandoc]; + nativeBuildInputs = [pkg-config cmake pandoc]; buildInputs = [ wayland wayland-protocols @@ -29,19 +30,6 @@ gcc15Stdenv.mkDerivation (finalAttrs: { libcap ]; - # Build phases - # Ensure that the Makefile has the correct directory with the Wayland protocols - preBuild = '' - export WAYLAND_PROTOCOLS_DIR="${wayland-protocols}/share/wayland-protocols" - ''; - - postPatch = '' - grep -rl "/usr/share/wayland-protocols" . | while read f; do - substituteInPlace "$f" \ - --replace "/usr/share/wayland-protocols" "${wayland-protocols}/share/wayland-protocols" - done - ''; - cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" "-DSKIP_CPM=ON" @@ -51,7 +39,7 @@ gcc15Stdenv.mkDerivation (finalAttrs: { # Package information meta = { - description = "Delightful Wayland overlay that displays an animated v-pet reacting to your keyboard input!"; + description = "Delightful Wayland overlay that displays an animated bongo cat and more vpets reacting to your keyboard input!"; homepage = "https://github.com/furudbat/wayland-vpets"; license = lib.licenses.mit; maintainers = with lib.maintainers; [voxi0 furudbat]; diff --git a/nix/shell.nix b/nix/shell.nix index 0f524ed2..8cf58f51 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -26,9 +26,6 @@ pkgs.mkShellNoCC { wayland-protocols ]; shellHook = '' - # Ensure that the Makefile can find and access the Wayland protocols - export WAYLAND_PROTOCOLS_DIR="${pkgs.wayland-protocols}/share/wayland-protocols" - # Simple TUI echo "🐱 Bongo Cat Development Environment" echo "Build output is stored in 'build' if you don't use 'nix build'" From c3122da0487cbdf7ef435ef40134951758419949 Mon Sep 17 00:00:00 2001 From: furudbat Date: Sun, 19 Apr 2026 22:51:45 +0200 Subject: [PATCH 17/17] chore!: dump version * update doc and README BREAKING CHANGE: remove Makefiles, use cmake --- CMakeLists.txt | 4 +- CONTRIBUTING.md | 16 ++- Makefile | 96 --------------- Makefile.old | 261 ---------------------------------------- README.md | 4 +- include/core/bongocat.h | 7 +- nix/shell.nix | 25 ++-- 7 files changed, 31 insertions(+), 382 deletions(-) delete mode 100644 Makefile delete mode 100644 Makefile.old diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ef39e41..5919eab4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_C_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON) include(FetchContent) -if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0.0") cmake_policy(SET CMP0135 NEW) endif() @@ -473,7 +473,7 @@ endif() if(NOT DEFINED ENV{NIX_BUILD_TOP} AND NOT SKIP_CPM) cpmaddpackage("gh:TheLartians/PackageProject.cmake@1.13.0") set(CPACK_PACKAGE_NAME "bongocat") - set(CPACK_PACKAGE_VERSION "3.6.1") + set(CPACK_PACKAGE_VERSION "4.0.0") set(CPACK_PACKAGE_CONTACT "hircreacc@gmail.com") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A delightful Wayland overlay that displays an animated V-Pet reacting to your keyboard input! ") set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3225778f..a23b58cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,12 +49,6 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build ```` -> Legacy `make debug` is supported for old Bongo Cat dev workflows. - -`make debug` provides a quick debug build. -You can inspect the old workflow in the old [`Makefile`](Makefile.old). -_**Note:** The binary name in AUR is `wpets`, but during development and `make install` it is still `bongocat`._ - ### Running ```bash @@ -74,6 +68,10 @@ Follow the project’s coding guidelines: * **Try to avoid STL & Minimal Templates, you can use C functions and Linux build-in functions** * **Assets**: Embed large assets in separate TUs * **Global State**: Avoid globals, pass context structs +* **Style**: Use clang-format and `cmake --build build --target fix-format` + * `lower_case` functions/variables, `UPPER_CASE` macros/constants, `_t` suffix on typedefs +* **Commit messages**: conventional commits (`feat:`, `fix:`, `docs:`, `refactor:`) +* **No `popen`**: use `spawn_pipe` for subprocess execution _(security requirement)_ _Run `make format` before committing_ @@ -223,3 +221,9 @@ See [COPYRIGHT](assets/COPYRIGHT.md) for more details. Thank you for helping make **Wayland Bongo Cat + V-Pets** a better, more delightful overlay! 💖 +## LLM Disclaimer + +I personally don't use much LLMs, only some Web based LLms like ChatGPT, for troubleshooting and helping me generate some Boilerplate-Code for some wayland functions, bash scripts, or rule-of-five C++ constructors. +Upstream uses [Claude Code](https://github.com/saatvik333/wayland-bongocat/blob/main/CLAUDE.md), I merge most code by hand and adapt fixes, improvments, step-by-step. + +This is a Project for fun and hand-writen code is fun for me. diff --git a/Makefile b/Makefile deleted file mode 100644 index 14ed4148..00000000 --- a/Makefile +++ /dev/null @@ -1,96 +0,0 @@ -# keep Makefile for older scripts, cmake still needed - -# Directories -SRCDIR = src -INCDIR = include -BUILDDIR = build -OBJDIR = $(BUILDDIR)/obj -PROTOCOLDIR = protocols -WAYLAND_PROTOCOLS_DIR ?= /usr/share/wayland-protocols - -ONLY_BONGOCAT ?= 1 - -ifeq ($(ONLY_BONGOCAT),1) - FEATURES = -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -else - FEATURES = -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS -endif - -# Target executable -TARGET = $(BUILDDIR)/bongocat - -.PHONY: all format format-check lint - -# Default target -default: build -all: build - -# Release build -build: - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - cmake --build build -release: - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DFEATURE_LAZY_LOAD_ASSETS=ON -DFEATURE_MULTI_VERSIONS=ON - cmake --build build - -# Debug build -build-debug: - cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug - cmake --build build -debug: - cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug - cmake --build build - -# Install target -install: build - cmake --install build - -pack: release doc - cd build && cpack - -doc: release - cmake --build build --target manpages - -SOURCES = $(shell find $(SRCDIR) -name '*.cpp' ! -path '*/embedded_assets/**/*.c??' ! -path '*/image_loader/*.c' ! -path '*/image_loader/**/*.cpp') - -# Memory check (requires valgrind) -memcheck: debug - valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./$(TARGET) - -# Performance profiling -profile: release - perf record -g ./$(TARGET) - perf report - -# ============================================================================= -# CODE QUALITY TARGETS -# ============================================================================= - -# Find all project source files (exclude lib/ and protocols/) -PROJECT_SOURCES = $(shell find $(SRCDIR) -name '*.cpp' ! -path '*/embedded_assets/**/*.c??' ! -path '*/image_loader/*.c' ! -path '*/image_loader/**/*.cpp') -PROJECT_HEADERS = $(shell find $(INCDIR) -name '*.h' ! -path '*/embedded_assets/**/*.h' ! -path '*/image_loader/**/*.h') -ALL_PROJECT_FILES = $(PROJECT_SOURCES) $(PROJECT_HEADERS) - -# Format all project source files -format: - cmake --build build --target fix-format - -# Check if formatting is correct (for CI) -format-check: - cmake --build build --target check-format - -# Static analysis with clang-tidy (uses .clang-tidy config) -lint: protocols - @echo "Running static analysis..." - @clang-tidy -p build $(PROJECT_SOURCES) 2>/dev/null || true - @echo "Static analysis complete." - -# Alias for lint -analyze: lint - -# Generate compile_commands.json for IDE support, or just use cmake and presets -# Run: make compiledb -compiledb: clean - cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -.PHONY: compiledb \ No newline at end of file diff --git a/Makefile.old b/Makefile.old deleted file mode 100644 index cdd7091e..00000000 --- a/Makefile.old +++ /dev/null @@ -1,261 +0,0 @@ -# Compiler -CC = gcc -CXX = g++ - -# Build type (debug or release) -BUILD_TYPE ?= release - -ONLY_BONGOCAT ?= 1 - -# Directories -SRCDIR = src -INCDIR = include -BUILDDIR = build -OBJDIR = $(BUILDDIR)/obj -PROTOCOLDIR = protocols -WAYLAND_PROTOCOLS_DIR ?= /usr/share/wayland-protocols - -# Base flags -BASE_CFLAGS = -std=c2x -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols -BASE_CFLAGS += -Wall -Wextra -Wpedantic -Wformat=2 -Wstrict-prototypes -BASE_CFLAGS += -Wmissing-prototypes -Wold-style-definition -Wredundant-decls -BASE_CFLAGS += -Wnested-externs -Wmissing-include-dirs -Wlogical-op -BASE_CFLAGS += -Wjump-misses-init -Wdouble-promotion -Wshadow -BASE_CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2 - -BASE_CXXFLAGS = -std=c++23 -I$(INCDIR) -I$(SRCDIR) -isystem lib -isystem protocols -BASE_CXXFLAGS += -Wall -Wextra -Wpedantic -Wformat=2 -BASE_CXXFLAGS += -Wredundant-decls -BASE_CXXFLAGS += -Wmissing-include-dirs -Wlogical-op -BASE_CXXFLAGS += -Wdouble-promotion -Wshadow -BASE_CXXFLAGS += -fstack-protector-strong - -ifeq ($(ONLY_BONGOCAT),1) - BASE_CFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS - BASE_CXXFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -else - BASE_CFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS - BASE_CXXFLAGS += -DFEATURE_BONGOCAT_EMBEDDED_ASSETS -DFEATURE_ENABLE_DM_EMBEDDED_ASSETS -DFEATURE_MS_AGENT_EMBEDDED_ASSETS -DFEATURE_PKMN_EMBEDDED_ASSETS - BASE_CFLAGS += -I$(INCDIR)/embedded_assets/min_dm -I$(INCDIR)/image_loader/min_dm -I$(SRCDIR)/embedded_assets/min_dm/include - BASE_CXXFLAGS += -I$(INCDIR)/embedded_assets/min_dm -I$(INCDIR)/image_loader/min_dm -I$(SRCDIR)/embedded_assets/min_dm/include -endif - -# Debug flags -DEBUG_CFLAGS = $(BASE_CFLAGS) -g3 -O0 -DDEBUG -fsanitize=address -fsanitize=undefined -DEBUG_CXXFLAGS = $(BASE_CXXFLAGS) -g3 -O0 -DDEBUG -fsanitize=address -fsanitize=undefined -DEBUG_LDFLAGS = -fsanitize=address -fsanitize=undefined - -# Release flags -RELEASE_CFLAGS = $(BASE_CFLAGS) -O3 -DNDEBUG -flto -march=native -D_FORTIFY_SOURCE=2 -RELEASE_CFLAGS += -fomit-frame-pointer -funroll-loops -finline-functions -D_FORTIFY_SOURCE=2 -RELEASE_CXXFLAGS = $(BASE_CXXFLAGS) -O3 -DNDEBUG -flto -march=native -D_FORTIFY_SOURCE=2 -RELEASE_CXXFLAGS += -fomit-frame-pointer -funroll-loops -finline-functions -D_FORTIFY_SOURCE=2 - -# Set flags based on build type -ifeq ($(BUILD_TYPE),debug) - CFLAGS = $(DEBUG_CFLAGS) - CXXFLAGS = $(DEBUG_CXXFLAGS) - LDFLAGS = -lwayland-client -lm -lpthread -lrt -ludev $(DEBUG_LDFLAGS) -else - CFLAGS = $(RELEASE_CFLAGS) - CXXFLAGS = $(RELEASE_CXXFLAGS) - LDFLAGS = -lwayland-client -lm -lpthread -lrt -ludev -flto -endif - -# Source files (including embedded assets which are now committed) -CXX_SRC = $(SRCDIR)/image_loader/load_images.cpp \ - $(SRCDIR)/image_loader/load_images_stb_image.cpp \ - $(SRCDIR)/utils/memory.cpp \ - $(SRCDIR)/utils/system_memory.cpp \ - $(SRCDIR)/utils/random.cpp \ - $(SRCDIR)/utils/error.cpp \ - $(SRCDIR)/utils/time.cpp \ - $(SRCDIR)/core/main.cpp \ - $(SRCDIR)/platform/input.cpp \ - $(SRCDIR)/platform/update.cpp \ - $(SRCDIR)/platform/wayland.cpp \ - $(SRCDIR)/platform/wayland_callbacks.cpp \ - $(SRCDIR)/platform/wayland_hyprland.cpp \ - $(SRCDIR)/platform/wayland_setups.cpp \ - $(SRCDIR)/platform/wayland_sway.cpp \ - $(SRCDIR)/graphics/bar.cpp \ - $(SRCDIR)/graphics/animation.cpp \ - $(SRCDIR)/graphics/animation_init.cpp \ - $(SRCDIR)/graphics/drawing_images.cpp \ - $(SRCDIR)/config/config_watcher.cpp \ - $(SRCDIR)/config/config.cpp - -C_SRC = $(SRCDIR)/image_loader/stb_image.c -ifeq ($(ONLY_BONGOCAT),1) - C_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_images.c - CXX_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp $(SRCDIR)/image_loader/bongocat/load_images_bongocat.cpp -else - C_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_images.c $(SRCDIR)/embedded_assets/min_dm/min_dm_images.c $(SRCDIR)/embedded_assets/ms_agent/ms_agent_images.c $(SRCDIR)/embedded_assets/pkmn/pkmn_images.c - CXX_SRC += $(SRCDIR)/embedded_assets/bongocat/bongocat_get_sprite_sheet.cpp \ - $(SRCDIR)/image_loader/bongocat/load_images_bongocat.cpp \ - $(SRCDIR)/image_loader/min_dm/load_images_min_dm.cpp \ - $(SRCDIR)/image_loader/min_dm/min_dm_load_sprite_sheet.cpp \ - $(SRCDIR)/image_loader/base_dm/load_dm.cpp \ - $(SRCDIR)/image_loader/pkmn/load_images_pkmn.cpp \ - $(SRCDIR)/image_loader/ms_agent/load_images_ms_agent.cpp \ - $(SRCDIR)/embedded_assets/pkmn/pkmn_get_sprite_sheet.cpp \ - $(SRCDIR)/embedded_assets/min_dm/min_dm_get_sprite_sheet.cpp \ - $(SRCDIR)/embedded_assets/ms_agent/embedded_assets_ms_agent.cpp -endif - -C_OBJECTS = $(C_SRC:$(SRCDIR)/%.c=$(OBJDIR)/%.o) -CXX_OBJECTS = $(CXX_SRC:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) - -# Protocol files -C_PROTOCOL_SRC = $(PROTOCOLDIR)/zwlr-layer-shell-v1-protocol.c $(PROTOCOLDIR)/xdg-shell-protocol.c $(PROTOCOLDIR)/wlr-foreign-toplevel-management-v1-protocol.c $(PROTOCOLDIR)/xdg-output-unstable-v1-protocol.c -H_PROTOCOL_HDR = $(PROTOCOLDIR)/zwlr-layer-shell-v1-client-protocol.h $(PROTOCOLDIR)/xdg-shell-client-protocol.h $(PROTOCOLDIR)/wlr-foreign-toplevel-management-v1-client-protocol.h $(PROTOCOLDIR)/xdg-output-unstable-v1-client-protocol.h -PROTOCOL_OBJECTS = $(C_PROTOCOL_SRC:$(PROTOCOLDIR)/%.c=$(OBJDIR)/%.o) - -# Target executable -TARGET = $(BUILDDIR)/bongocat - -.PHONY: all clean protocols embed-assets format format-check lint - -all: protocols $(TARGET) - -# Generate protocol files first -protocols: $(C_PROTOCOL_SRC) $(H_PROTOCOL_HDR) - -# Create build directories -$(OBJDIR): - mkdir -p $(OBJDIR) - mkdir -p $(OBJDIR)/config - mkdir -p $(OBJDIR)/core - mkdir -p $(OBJDIR)/embedded_assets - mkdir -p $(OBJDIR)/embedded_assets/bongocat - mkdir -p $(OBJDIR)/embedded_assets/dm - mkdir -p $(OBJDIR)/embedded_assets/dm20 - mkdir -p $(OBJDIR)/embedded_assets/dmc - mkdir -p $(OBJDIR)/embedded_assets/dmx - mkdir -p $(OBJDIR)/embedded_assets/pen - mkdir -p $(OBJDIR)/embedded_assets/pen20 - mkdir -p $(OBJDIR)/embedded_assets/dmall - mkdir -p $(OBJDIR)/embedded_assets/min_dm - mkdir -p $(OBJDIR)/embedded_assets/ms_agent - mkdir -p $(OBJDIR)/embedded_assets/misc - mkdir -p $(OBJDIR)/embedded_assets/pkmn - mkdir -p $(OBJDIR)/embedded_assets/pmd - mkdir -p $(OBJDIR)/graphics - mkdir -p $(OBJDIR)/image_loader - mkdir -p $(OBJDIR)/image_loader/base_dm - mkdir -p $(OBJDIR)/image_loader/bongocat - mkdir -p $(OBJDIR)/image_loader/custom - mkdir -p $(OBJDIR)/image_loader/dm - mkdir -p $(OBJDIR)/image_loader/dm20 - mkdir -p $(OBJDIR)/image_loader/dmc - mkdir -p $(OBJDIR)/image_loader/dmx - mkdir -p $(OBJDIR)/image_loader/pen - mkdir -p $(OBJDIR)/image_loader/pen20 - mkdir -p $(OBJDIR)/image_loader/dmall - mkdir -p $(OBJDIR)/image_loader/min_dm - mkdir -p $(OBJDIR)/image_loader/ms_agent - mkdir -p $(OBJDIR)/image_loader/misc - mkdir -p $(OBJDIR)/image_loader/pkmn - mkdir -p $(OBJDIR)/image_loader/pmd - mkdir -p $(OBJDIR)/platform - mkdir -p $(OBJDIR)/utils - mkdir -p $(BUILDDIR) - -# Compile source files (depends on protocol headers) -$(OBJDIR)/%.o: $(SRCDIR)/%.c $(H_PROTOCOL_HDR) | $(OBJDIR) - $(CC) $(CFLAGS) -c $< -o $@ -$(OBJDIR)/%.o: $(SRCDIR)/%.cpp | $(OBJDIR) - $(CXX) $(CXXFLAGS) -c $< -o $@ - -# Compile protocol files -$(OBJDIR)/%.o: $(PROTOCOLDIR)/%.c | $(OBJDIR) - $(CC) $(CFLAGS) -c $< -o $@ -$(OBJDIR)/%.o: $(PROTOCOLDIR)/%.cpp | $(OBJDIR) - $(CXX) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(C_OBJECTS) $(CXX_OBJECTS) $(PROTOCOL_OBJECTS) - $(CXX) $(C_OBJECTS) $(CXX_OBJECTS) $(PROTOCOL_OBJECTS) -o $(TARGET) $(LDFLAGS) - -# Rule to generate Wayland protocol files -$(C_PROTOCOL_SRC) $(H_PROTOCOL_HDR): $(PROTOCOLDIR)/wlr-layer-shell-unstable-v1.xml $(PROTOCOLDIR)/wlr-foreign-toplevel-management-unstable-v1.xml $(PROTOCOLDIR)/xdg-output-unstable-v1.xml - wayland-scanner client-header $(WAYLAND_PROTOCOLS_DIR)/stable/xdg-shell/xdg-shell.xml $(PROTOCOLDIR)/xdg-shell-client-protocol.h - wayland-scanner private-code $(WAYLAND_PROTOCOLS_DIR)/stable/xdg-shell/xdg-shell.xml $(PROTOCOLDIR)/xdg-shell-protocol.c - wayland-scanner private-code $(PROTOCOLDIR)/wlr-layer-shell-unstable-v1.xml $(PROTOCOLDIR)/zwlr-layer-shell-v1-protocol.c - wayland-scanner client-header $(PROTOCOLDIR)/wlr-layer-shell-unstable-v1.xml $(PROTOCOLDIR)/zwlr-layer-shell-v1-client-protocol.h - wayland-scanner private-code $(PROTOCOLDIR)/wlr-foreign-toplevel-management-unstable-v1.xml $(PROTOCOLDIR)/wlr-foreign-toplevel-management-v1-protocol.c - wayland-scanner client-header $(PROTOCOLDIR)/wlr-foreign-toplevel-management-unstable-v1.xml $(PROTOCOLDIR)/wlr-foreign-toplevel-management-v1-client-protocol.h - wayland-scanner client-header $(PROTOCOLDIR)/xdg-output-unstable-v1.xml $(PROTOCOLDIR)/xdg-output-unstable-v1-client-protocol.h - wayland-scanner private-code $(PROTOCOLDIR)/xdg-output-unstable-v1.xml $(PROTOCOLDIR)/xdg-output-unstable-v1-protocol.c - -clean: - rm -rf $(BUILDDIR) $(C_PROTOCOL_SRC) $(H_PROTOCOL_HDR) - -# Development targets -debug: - $(MAKE) BUILD_TYPE=debug - -release: - $(MAKE) BUILD_TYPE=release - -install: $(TARGET) - install -D $(TARGET) $(DESTDIR)/usr/local/bin/bongocat - install -D bongocat.conf.example $(DESTDIR)/usr/local/share/bongocat/bongocat.conf.example - install -D scripts/find_input_devices.sh $(DESTDIR)/usr/local/bin/bongocat-find-devices - -uninstall: - rm -f $(DESTDIR)/usr/local/bin/bongocat - rm -f $(DESTDIR)/usr/local/bin/bongocat-find-devices - rm -rf $(DESTDIR)/usr/local/share/bongocat - -SOURCES = $(CXX_SRC) $(C_SRC) -# Static analysis -analyze: - clang-tidy $(SOURCES) -- $(CFLAGS) $(CXXFLAGS) - -# Memory check (requires valgrind) -memcheck: debug - valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./$(TARGET) - -# Performance profiling -profile: release - perf record -g ./$(TARGET) - perf report - -.PHONY: debug release install uninstall analyze memcheck profile format format-check lint - - -# ============================================================================= -# CODE QUALITY TARGETS -# ============================================================================= - -# Find all project source files (exclude lib/ and protocols/) -PROJECT_SOURCES = $(CXX_SRC) $(C_SRC) -PROJECT_HEADERS = $(shell find $(INCDIR) -name '*.h' ! -path '*/embedded_assets/**/*.h' ! -path '*/image_loader/**/*.h') -ALL_PROJECT_FILES = $(PROJECT_SOURCES) $(PROJECT_HEADERS) - -# Format all project source files -format: - @echo "Formatting source files..." - @clang-format -i $(ALL_PROJECT_FILES) - @echo "Done! Formatted $(words $(ALL_PROJECT_FILES)) files." - -# Check if formatting is correct (for CI) -format-check: - @echo "Checking code formatting..." - @clang-format --dry-run --Werror $(ALL_PROJECT_FILES) - @echo "All files are properly formatted." - -# Static analysis with clang-tidy (uses .clang-tidy config) -lint: protocols - @echo "Running static analysis..." - @clang-tidy $(PROJECT_SOURCES) -- $(CFLAGS) 2>/dev/null || true - @echo "Static analysis complete." - -# Generate compile_commands.json for IDE support (requires bear) -# Run: make compiledb -compiledb: clean - @echo "Generating compile_commands.json..." - @bear -- $(MAKE) all 2>/dev/null || (echo "Note: 'bear' not installed. Install with: sudo pacman -S bear" && false) - @echo "compile_commands.json generated!" - -.PHONY: compiledb diff --git a/README.md b/README.md index 96544b9c..bb4ab6d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Bongo Cat + V-Pets Wayland Overlay [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Version](https://img.shields.io/badge/version-3.6.1-blue.svg)](https://github.com/furudbat/wayland-vpets/releases) +[![Version](https://img.shields.io/badge/version-4.0.0-blue.svg)](https://github.com/furudbat/wayland-vpets/releases) [![Release Build](https://github.com/furudbat/wayland-vpets/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/furudbat/wayland-vpets/actions/workflows/release.yml) A cute Wayland overlay that shows an animated pets reacting to your keyboard input. @@ -337,7 +337,7 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build ``` -**Requirements:** wayland-client, wayland-protocols, gcc15 or clang19, make, cmake +**Requirements:** wayland-client, gcc15 or clang19, make, cmake > [!CAUTION] > **Privacy Notice**: When building in `DEBUG` mode and by enabling `enable_debug=1`, all keystrokes are logged to stdout/stderr. Ensure this is disabled (default: 0) for normal usage. diff --git a/include/core/bongocat.h b/include/core/bongocat.h index af238c36..e05b6cd1 100644 --- a/include/core/bongocat.h +++ b/include/core/bongocat.h @@ -1,11 +1,6 @@ #ifndef BONGOCAT_BONGOCAT_H #define BONGOCAT_BONGOCAT_H -#ifndef _POSIX_C_SOURCE -// POSIX feature test macro - must be before includes -# define _POSIX_C_SOURCE 200809L -#endif - #include "utils/error.h" #include "utils/memory.h" @@ -17,7 +12,7 @@ // ============================================================================= // Version -inline static constexpr const char *BONGOCAT_VERSION = "3.6.1"; +inline static constexpr const char *BONGOCAT_VERSION = "4.0.0"; // ============================================================================= // COMPILE-TIME CONSTANTS diff --git a/nix/shell.nix b/nix/shell.nix index 8cf58f51..e36a337b 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -26,15 +26,22 @@ pkgs.mkShellNoCC { wayland-protocols ]; shellHook = '' - # Simple TUI - echo "🐱 Bongo Cat Development Environment" - echo "Build output is stored in 'build' if you don't use 'nix build'" - echo "Commands:" - echo " nix build - Build the Nix package (Build output is stored in 'result')" - echo " make - Build the project" - echo " make debug - Build with debug symbols" - echo " make release - Build in release mode with optimizations and such (Longer compile time)" - echo " make clean - Clean build artifacts" + export CC=clang + export CXX=clang++ + export CMAKE_GENERATOR=Ninja + + echo "🐱 Bongo Cat Dev Shell" + echo + echo "Toolchain:" + echo " CC=$CC" + echo " CXX=$CXX" + echo " CMake=$(cmake --version | head -n1)" + echo + echo "Quick commands:" + echo " nix build -> build via flake" + echo " cmake -B build -> configure" + echo " cmake --build build" + echo echo "" echo "Helper scripts:" echo " ./scripts/find_input_devices.sh - Find input devices"