Compare commits
1 Commits
31cd35bf84
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5d31cccdd
|
@@ -5,28 +5,12 @@ BasedOnStyle: Google
|
||||
IndentWidth: 2
|
||||
ConstructorInitializerIndentWidth: 2
|
||||
ContinuationIndentWidth: 2
|
||||
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: true
|
||||
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: true
|
||||
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: true
|
||||
|
||||
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
|
||||
|
||||
IndentCaseLabels: false
|
||||
|
||||
# 多行函数每个参数单独一行
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
|
||||
# 指针样式(右对齐)
|
||||
---
|
||||
Language: C
|
||||
DerivePointerAlignment: false
|
||||
PointerAlignment: Right
|
||||
---
|
||||
Language: Cpp
|
||||
DerivePointerAlignment: false
|
||||
PointerAlignment: Right
|
||||
---
|
||||
|
||||
30
.clang-tidy
30
.clang-tidy
@@ -1,30 +0,0 @@
|
||||
# -*- mode: yaml; -*-
|
||||
|
||||
Checks: >-
|
||||
-*,
|
||||
clang-analyzer-*,
|
||||
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
|
||||
cert-err34-c,
|
||||
bugprone-assert-side-effect,
|
||||
bugprone-assignment-in-if-condition,
|
||||
bugprone-branch-clone,
|
||||
bugprone-infinite-loop,
|
||||
bugprone-macro-parentheses,
|
||||
bugprone-macro-repeated-side-effects,
|
||||
bugprone-misplaced-operator-in-strlen-in-alloc,
|
||||
bugprone-misplaced-pointer-arithmetic-in-alloc,
|
||||
bugprone-not-null-terminated-result,
|
||||
bugprone-posix-return,
|
||||
bugprone-sizeof-expression,
|
||||
bugprone-suspicious-memory-comparison,
|
||||
bugprone-suspicious-memset-usage,
|
||||
bugprone-suspicious-missing-comma,
|
||||
bugprone-suspicious-semicolon,
|
||||
bugprone-swapped-arguments,
|
||||
bugprone-undefined-memory-manipulation,
|
||||
bugprone-unsafe-functions,
|
||||
bugprone-unused-return-value
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: '^(src|include|tests)/'
|
||||
FormatStyle: file
|
||||
SystemHeaders: false
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
cd "$repo_root"
|
||||
|
||||
if ! command -v clang-format >/dev/null 2>&1; then
|
||||
echo "pre-commit: clang-format not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git clang-format -h >/dev/null 2>&1; then
|
||||
echo "pre-commit: git clang-format is not available" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -d '' staged_files < <(
|
||||
git diff --cached --name-only --diff-filter=ACMR -z -- \
|
||||
'*.c' '*.h' '*.cc' '*.cpp' '*.cxx' '*.hh' '*.hpp' '*.hxx'
|
||||
)
|
||||
|
||||
if ((${#staged_files[@]} == 0)); then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||
base_commit=HEAD
|
||||
else
|
||||
empty_tree="$(git hash-object -t tree /dev/null)"
|
||||
base_commit="$(
|
||||
printf 'pre-commit empty base\n' | \
|
||||
GIT_AUTHOR_NAME=pre-commit \
|
||||
GIT_AUTHOR_EMAIL=pre-commit@example.invalid \
|
||||
GIT_COMMITTER_NAME=pre-commit \
|
||||
GIT_COMMITTER_EMAIL=pre-commit@example.invalid \
|
||||
git commit-tree "$empty_tree"
|
||||
)"
|
||||
fi
|
||||
|
||||
set +e
|
||||
git clang-format --staged --binary clang-format --quiet "$base_commit" -- \
|
||||
"${staged_files[@]}"
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -ne 0 && $status -ne 1 ]]; then
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
git add -- "${staged_files[@]}"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,4 +2,3 @@
|
||||
/build/
|
||||
/zdwm
|
||||
/.cache/
|
||||
/Testing/
|
||||
|
||||
138
CMakeLists.txt
138
CMakeLists.txt
@@ -7,9 +7,10 @@ set(CMAKE_C_EXTENSIONS ON)
|
||||
|
||||
set(APP_NAME "zdwm")
|
||||
set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
set(BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
add_compile_definitions(LOG_FILE="$ENV{HOME}/${APP_NAME}-log.txt")
|
||||
|
||||
# generate compile_commands.json for clangd
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
@@ -24,63 +25,38 @@ project(${APP_NAME}
|
||||
)
|
||||
|
||||
add_executable(${APP_NAME}
|
||||
${SOURCE_DIR}/backend/output_utils.c
|
||||
${SOURCE_DIR}/backend/x11/backend.c
|
||||
${SOURCE_DIR}/backend/x11/common.c
|
||||
${SOURCE_DIR}/backend/x11/cursor.c
|
||||
${SOURCE_DIR}/backend/x11/event.c
|
||||
${SOURCE_DIR}/backend/x11/tray.c
|
||||
${SOURCE_DIR}/backend/x11/window.c
|
||||
|
||||
${SOURCE_DIR}/bar/bar.c
|
||||
${SOURCE_DIR}/bar/binding.c
|
||||
${SOURCE_DIR}/bar/cell.c
|
||||
${SOURCE_DIR}/bar/text.c
|
||||
${SOURCE_DIR}/bar/tray.c
|
||||
${SOURCE_DIR}/bar/windows.c
|
||||
${SOURCE_DIR}/bar/workspaces.c
|
||||
|
||||
${SOURCE_DIR}/base/color.c
|
||||
${SOURCE_DIR}/base/log.c
|
||||
${SOURCE_DIR}/base/process.c
|
||||
${SOURCE_DIR}/base/time.c
|
||||
|
||||
${SOURCE_DIR}/config/defaults.c
|
||||
${SOURCE_DIR}/config/loader.c
|
||||
${SOURCE_DIR}/config/runtime_config.c
|
||||
|
||||
${SOURCE_DIR}/common/event.c
|
||||
${SOURCE_DIR}/common/listeners.c
|
||||
${SOURCE_DIR}/common/window.c
|
||||
${SOURCE_DIR}/common/workspace.c
|
||||
|
||||
${SOURCE_DIR}/core/binding.c
|
||||
${SOURCE_DIR}/core/command_buffer.c
|
||||
${SOURCE_DIR}/core/layer.c
|
||||
${SOURCE_DIR}/core/layout.c
|
||||
${SOURCE_DIR}/core/listeners.c
|
||||
${SOURCE_DIR}/core/plan.c
|
||||
${SOURCE_DIR}/core/policy.c
|
||||
${SOURCE_DIR}/core/rules.c
|
||||
${SOURCE_DIR}/core/state.c
|
||||
${SOURCE_DIR}/core/window.c
|
||||
|
||||
${SOURCE_DIR}/layouts/fair.c
|
||||
${SOURCE_DIR}/layouts/fullscreen.c
|
||||
${SOURCE_DIR}/layouts/maximize.c
|
||||
|
||||
${SOURCE_DIR}/runtime/runtime.c
|
||||
|
||||
${SOURCE_DIR}/main.c
|
||||
${SOURCE_DIR}/wm.c
|
||||
${SOURCE_DIR}/utils.c
|
||||
${SOURCE_DIR}/buffer.c
|
||||
${SOURCE_DIR}/backtrace.c
|
||||
${SOURCE_DIR}/monitor.c
|
||||
${SOURCE_DIR}/client.c
|
||||
${SOURCE_DIR}/text.c
|
||||
${SOURCE_DIR}/color.c
|
||||
${SOURCE_DIR}/xcursor.c
|
||||
${SOURCE_DIR}/xwindow.c
|
||||
${SOURCE_DIR}/event.c
|
||||
${SOURCE_DIR}/action.c
|
||||
${SOURCE_DIR}/xkb.c
|
||||
${SOURCE_DIR}/atoms.c
|
||||
${SOURCE_DIR}/layout.c
|
||||
${SOURCE_DIR}/xres.c
|
||||
${SOURCE_DIR}/status.c
|
||||
${SOURCE_DIR}/audio.c
|
||||
${SOURCE_DIR}/rect.c
|
||||
${SOURCE_DIR}/wallpaper.c
|
||||
${SOURCE_DIR}/config.c
|
||||
${SOURCE_DIR}/renderer.c
|
||||
${SOURCE_DIR}/image.c
|
||||
${SOURCE_DIR}/tray.c
|
||||
)
|
||||
|
||||
# target_compile_definitions(${APP_NAME}
|
||||
# PRIVATE LOG_FILE="$ENV{HOME}/${APP_NAME}-log.txt"
|
||||
# )
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
pkg_check_modules(deps REQUIRED
|
||||
glib-2.0
|
||||
|
||||
x11
|
||||
xcb
|
||||
xcb-aux
|
||||
xcb-cursor
|
||||
@@ -89,17 +65,21 @@ pkg_check_modules(deps REQUIRED
|
||||
xcb-randr
|
||||
xcb-xfixes
|
||||
xcb-xinerama
|
||||
xkbcommon
|
||||
xcb-xkb
|
||||
xcb-xrm
|
||||
xkbcommon-x11
|
||||
|
||||
cairo
|
||||
cairo-xcb
|
||||
pango
|
||||
pangocairo
|
||||
imlib2
|
||||
|
||||
libpulse-mainloop-glib
|
||||
)
|
||||
|
||||
target_include_directories(${APP_NAME} SYSTEM
|
||||
PRIVATE ${deps_INCLUDE_DIRS}
|
||||
PRIVATE ${INCLUDE_DIR}
|
||||
)
|
||||
target_include_directories(${APP_NAME}
|
||||
PRIVATE ${SOURCE_DIR}
|
||||
@@ -110,3 +90,51 @@ target_link_libraries(${APP_NAME}
|
||||
PRIVATE m
|
||||
PRIVATE ${deps_LIBRARIES}
|
||||
)
|
||||
|
||||
|
||||
## 配置项
|
||||
# Check for backtrace_symbols()
|
||||
include(CheckSymbolExists)
|
||||
check_symbol_exists(backtrace_symbols execinfo.h HAS_EXECINFO)
|
||||
if(NOT HAS_EXECINFO)
|
||||
find_library(LIB_EXECINFO execinfo)
|
||||
if(LIB_EXECINFO)
|
||||
set(HAS_EXECINFO TRUE)
|
||||
target_link_libraries(${APP_NAME} ${LIB_EXECINFO})
|
||||
endif()
|
||||
endif()
|
||||
if(HAS_EXECINFO)
|
||||
message(STATUS "checking for execinfo -- found")
|
||||
else()
|
||||
message(STATUS "checking for execinfo -- not found")
|
||||
endif()
|
||||
|
||||
configure_file("${SOURCE_DIR}/app.h.in"
|
||||
"${BUILD_DIR}/app.h"
|
||||
ESCAPE_QUOTES
|
||||
@ONLY
|
||||
)
|
||||
|
||||
set(ATOM_LIST_FILE "${SOURCE_DIR}/atoms-list.txt")
|
||||
add_custom_command(
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build-utils/atoms-extern.sh
|
||||
ARGS ${ATOM_LIST_FILE} > ${BUILD_DIR}/atoms-extern.h
|
||||
OUTPUT ${BUILD_DIR}/atoms-extern.h
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPENDS ${ATOM_LIST_FILE}
|
||||
COMMENT "Generating atoms-extern.h"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_command(
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build-utils/atoms-intern.sh
|
||||
ARGS ${ATOM_LIST_FILE} > ${BUILD_DIR}/atoms-intern.h
|
||||
OUTPUT ${BUILD_DIR}/atoms-intern.h
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPENDS ${ATOM_LIST_FILE}
|
||||
COMMENT "Generating atoms-intern.h"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(generate_atoms
|
||||
DEPENDS ${BUILD_DIR}/atoms-extern.h ${BUILD_DIR}/atoms-intern.h
|
||||
)
|
||||
add_dependencies(${APP_NAME} generate_atoms)
|
||||
|
||||
10
Makefile
10
Makefile
@@ -32,15 +32,11 @@ debug: $(TARGET_NAME)
|
||||
|
||||
prepare:
|
||||
@cmake -S $(SRC_DIR) -B $(BUILD_DIR)
|
||||
@cp -lf $(BUILD_DIR)/compile_commands.json $(MAKEFILE_DIR)
|
||||
@cp $(BUILD_DIR)/compile_commands.json $(MAKEFILE_DIR)
|
||||
|
||||
build: prepare
|
||||
${RM} $(TARGET)
|
||||
@cmake --build $(BUILD_DIR)
|
||||
@cp -lf $(BUILD_DIR)/$(TARGET_NAME) $(TARGET)
|
||||
@cp $(BUILD_DIR)/$(TARGET_NAME) $(TARGET)
|
||||
|
||||
install-hooks:
|
||||
chmod +x $(MAKEFILE_DIR).githooks/pre-commit
|
||||
git -C $(MAKEFILE_DIR) config core.hooksPath $(MAKEFILE_DIR).githooks
|
||||
|
||||
.PHONY: clean run wm prepare build install uninstall reinstall install-hooks
|
||||
.PHONY: clean run wm prepare build install uninstall reinstall
|
||||
|
||||
17
build-utils/atoms-extern.sh
Executable file
17
build-utils/atoms-extern.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "/* This file is autogenerated by $0 - do not edit */"
|
||||
echo
|
||||
echo "#include <xcb/xproto.h>"
|
||||
echo
|
||||
|
||||
while read atom
|
||||
do
|
||||
# 去掉首位空格
|
||||
atom_clean=$(echo "$atom" | sed 's/^[[:space:]]*//;s/[[:space:]]$//')
|
||||
|
||||
# 跳过空行
|
||||
if [[ -n "$atom_clean" ]]; then
|
||||
echo "extern xcb_atom_t ${atom_clean};"
|
||||
fi
|
||||
done < "$1"
|
||||
39
build-utils/atoms-intern.sh
Executable file
39
build-utils/atoms-intern.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "/* This file is autogenerated by $0 - do not edit */"
|
||||
echo
|
||||
echo "#include <xcb/xproto.h>"
|
||||
echo
|
||||
|
||||
while read atom
|
||||
do
|
||||
# 去掉首位空格
|
||||
atom_clean=$(echo "$atom" | sed 's/^[[:space:]]*//;s/[[:space:]]$//')
|
||||
|
||||
# 跳过空行
|
||||
if [[ -n "$atom_clean" ]]; then
|
||||
echo "xcb_atom_t ${atom_clean};"
|
||||
fi
|
||||
done < "$1"
|
||||
|
||||
echo
|
||||
echo "typedef struct atom_item_t {"
|
||||
echo " const char *name;"
|
||||
echo " size_t len;"
|
||||
echo " xcb_atom_t *atom;"
|
||||
echo "} atom_item_t;"
|
||||
echo
|
||||
echo "static atom_item_t ATOM_LIST[] = {"
|
||||
|
||||
while read atom
|
||||
do
|
||||
# 去掉首位空格
|
||||
atom_clean=$(echo "$atom" | sed 's/^[[:space:]]*//;s/[[:space:]]$//')
|
||||
|
||||
# 跳过空行
|
||||
if [[ -n "$atom_clean" ]]; then
|
||||
echo " {\"${atom_clean}\", sizeof(\"${atom_clean}\") - 1, &${atom_clean}},"
|
||||
fi
|
||||
done < "$1"
|
||||
|
||||
echo "};"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,650 +0,0 @@
|
||||
# ZDWM 最小核心 + 外围扩展设计
|
||||
|
||||
* 设计原则
|
||||
|
||||
1. *核心只做一件事*:管理窗口、显示器、工作区
|
||||
2. *所有扩展都通过插件*:状态栏、配置、快捷键、布局算法
|
||||
3. *核心提供稳定的钩子*:插件可以在关键点介入
|
||||
4. *零运行时开销*:扩展不增加核心复杂度
|
||||
|
||||
* 最小核心定义
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// src/core/zdwm.h
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
typedef struct {
|
||||
int x, y;
|
||||
unsigned int width, height;
|
||||
} rect_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *instance;
|
||||
} wm_class_t;
|
||||
|
||||
// ========== 窗口 ==========
|
||||
typedef uint64_t wm_window_id_t;
|
||||
|
||||
typedef struct {
|
||||
wm_window_id_t id;
|
||||
rect_t geometry;
|
||||
wm_class_t class;
|
||||
bool mapped;
|
||||
bool floating;
|
||||
bool fullscreen;
|
||||
bool focused;
|
||||
// ... 其他基本状态
|
||||
} wm_window_t;
|
||||
|
||||
// ========== 工作区 ==========
|
||||
typedef uint32_t wm_workspace_id_t;
|
||||
|
||||
typedef struct {
|
||||
wm_workspace_id_t id;
|
||||
char *name;
|
||||
wm_window_id_t *windows;
|
||||
size_t window_count;
|
||||
wm_window_id_t focused_window;
|
||||
} wm_workspace_t;
|
||||
|
||||
// ========== 显示器 ==========
|
||||
typedef uint32_t wm_output_id_t;
|
||||
|
||||
typedef struct {
|
||||
wm_output_id_t id;
|
||||
rect_t geometry;
|
||||
rect_t workarea;
|
||||
wm_workspace_id_t current_workspace;
|
||||
} wm_output_t;
|
||||
|
||||
// ========== 核心状态 ==========
|
||||
typedef struct {
|
||||
wm_window_t *windows;
|
||||
size_t window_count;
|
||||
|
||||
wm_workspace_t *workspaces;
|
||||
size_t workspace_count;
|
||||
wm_workspace_id_t current_workspace;
|
||||
|
||||
wm_output_t *outputs;
|
||||
size_t output_count;
|
||||
} wm_state_t;
|
||||
|
||||
// ========== 核心 API ==========
|
||||
// 初始化和清理
|
||||
bool wm_init(void);
|
||||
void wm_shutdown(void);
|
||||
|
||||
// 主循环
|
||||
void wm_run(void);
|
||||
|
||||
// 查询接口
|
||||
wm_window_t* wm_find_window(wm_window_id_t id);
|
||||
wm_workspace_t* wm_find_workspace(wm_workspace_id_t id);
|
||||
wm_output_t* wm_find_output(wm_output_id_t id);
|
||||
|
||||
// 窗口操作
|
||||
void wm_manage_window(wm_window_id_t id);
|
||||
void wm_unmanage_window(wm_window_id_t id);
|
||||
void wm_focus_window(wm_window_id_t id);
|
||||
void wm_set_window_floating(wm_window_id_t id, bool floating);
|
||||
|
||||
// 工作区操作
|
||||
void wm_switch_workspace(wm_workspace_id_t id);
|
||||
void wm_send_window_to_workspace(wm_window_id_t window, wm_workspace_id_t workspace);
|
||||
|
||||
// 显示器操作
|
||||
void wm_switch_output(wm_output_id_t id);
|
||||
#+END_SRC
|
||||
|
||||
* 扩展接口设计
|
||||
|
||||
**1. 事件钩子系统**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// src/core/hooks.h
|
||||
|
||||
// 钩子类型(关键事件点)
|
||||
typedef enum wm_hook_type_t {
|
||||
HOOK_WINDOW_MANAGE, // 窗口被管理时
|
||||
HOOK_WINDOW_UNMANAGE, // 窗口取消管理时
|
||||
HOOK_WINDOW_FOCUS, // 窗口焦点变化时
|
||||
HOOK_WORKSPACE_SWITCH, // 工作区切换时
|
||||
HOOK_LAYOUT_CALCULATE, // 布局计算时
|
||||
HOOK_CONFIG_RELOAD, // 配置重载时
|
||||
HOOK_KEY_PRESS, // 键盘按下时
|
||||
HOOK_COUNT
|
||||
} wm_hook_type_t;
|
||||
|
||||
// 钩子回调函数类型
|
||||
typedef void (*wm_hook_window_manage_t)(wm_window_t *window, void *user_data);
|
||||
typedef void (*wm_hook_window_focus_t)(wm_window_t *old_focus, wm_window_t *new_focus, void *user_data);
|
||||
typedef void (*wm_hook_layout_calculate_t)(wm_workspace_t *workspace, rect_t *geometries, size_t count, void *user_data);
|
||||
typedef bool (*wm_hook_key_press_t)(uint32_t keycode, uint32_t modifiers, void *user_data);
|
||||
|
||||
// 钩子注册
|
||||
typedef struct {
|
||||
wm_hook_type_t type;
|
||||
void *callback;
|
||||
void *user_data;
|
||||
int priority; // 优先级:数字越小越先执行
|
||||
} wm_hook_t;
|
||||
|
||||
// 核心 API:注册钩子
|
||||
void wm_hook_register(wm_hook_type_t type, void *callback, void *user_data, int priority);
|
||||
void wm_hook_unregister(wm_hook_type_t type, void *callback);
|
||||
|
||||
// 核心 API:触发钩子
|
||||
void wm_hook_trigger_window_manage(wm_window_t *window);
|
||||
void wm_hook_trigger_window_focus(wm_window_t *old_focus, wm_window_t *new_focus);
|
||||
void wm_hook_trigger_layout_calculate(wm_workspace_t *workspace, rect_t *geometries, size_t count);
|
||||
bool wm_hook_trigger_key_press(uint32_t keycode, uint32_t modifiers);
|
||||
#+END_SRC
|
||||
|
||||
**2. 插件系统**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// src/core/plugin.h
|
||||
|
||||
// 插件接口
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *version;
|
||||
|
||||
// 必须实现
|
||||
bool (*init)(void);
|
||||
void (*shutdown)(void);
|
||||
|
||||
// 可选实现
|
||||
void (*on_event)(wm_hook_type_t event, void *event_data);
|
||||
} wm_plugin_t;
|
||||
|
||||
// 核心 API:插件加载
|
||||
bool wm_plugin_load(wm_plugin_t *plugin);
|
||||
bool wm_plugin_unload(const char *name);
|
||||
#+END_SRC
|
||||
|
||||
* 扩展编写指南
|
||||
|
||||
**扩展1:布局算法(插件)**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// plugins/layout_tile.c
|
||||
|
||||
#include "plugins/layout.h"
|
||||
#include "core/hooks.h"
|
||||
|
||||
// 布局算法实现
|
||||
static void tile_calculate_layout(wm_workspace_t *workspace,
|
||||
rect_t *geometries,
|
||||
size_t count,
|
||||
void *user_data) {
|
||||
// 布局参数(可以通过配置获取)
|
||||
int gap = 10;
|
||||
int master_count = 1;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (i < master_count) {
|
||||
// 主窗口:左侧,占 60%
|
||||
geometries[i].x = workspace->outputs[0].x;
|
||||
geometries[i].y = workspace->outputs[0].y;
|
||||
geometries[i].width = workspace->outputs[0].width * 0.6 - gap;
|
||||
geometries[i].height = workspace->outputs[0].height;
|
||||
} else {
|
||||
// 堆叠窗口:右侧,占 40%
|
||||
geometries[i].x = workspace->outputs[0].x + workspace->outputs[0].width * 0.6 + gap;
|
||||
geometries[i].y = workspace->outputs[0].y + (i - master_count) * 20;
|
||||
geometries[i].width = workspace->outputs[0].width * 0.4 - gap;
|
||||
geometries[i].height = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 插件初始化
|
||||
static bool layout_tile_init(void) {
|
||||
// 注册布局钩子
|
||||
wm_hook_register(HOOK_LAYOUT_CALCULATE, tile_calculate_layout, NULL, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void layout_tile_shutdown(void) {
|
||||
// 自动取消注册
|
||||
}
|
||||
|
||||
// 插件定义
|
||||
wm_plugin_t plugin_tile = {
|
||||
.name = "tile",
|
||||
.version = "1.0",
|
||||
.init = layout_tile_init,
|
||||
.shutdown = layout_tile_shutdown
|
||||
};
|
||||
#+END_SRC
|
||||
|
||||
**扩展2:状态栏(插件)**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// plugins/statusbar.c
|
||||
|
||||
#include "plugins/statusbar.h"
|
||||
#include "core/hooks.h"
|
||||
|
||||
static cairo_surface_t *g_bar_surface = NULL;
|
||||
|
||||
// 在工作区切换时更新状态栏
|
||||
static void on_workspace_switch(void *event_data) {
|
||||
wm_workspace_t *workspace = event_data;
|
||||
|
||||
// 绘制状态栏
|
||||
draw_statusbar(workspace);
|
||||
}
|
||||
|
||||
// 在布局计算后绘制到状态栏
|
||||
static void on_layout_calculate(wm_workspace_t *workspace,
|
||||
rect_t *geometries,
|
||||
size_t count,
|
||||
void *user_data) {
|
||||
// 可以在这里显示布局信息
|
||||
draw_layout_info(workspace);
|
||||
}
|
||||
|
||||
// 绘制状态栏
|
||||
static void draw_statusbar(wm_workspace_t *workspace) {
|
||||
// 使用 cairo 绘制
|
||||
// ...
|
||||
}
|
||||
|
||||
static bool statusbar_init(void) {
|
||||
// 注册钩子
|
||||
wm_hook_register(HOOK_WORKSPACE_SWITCH, on_workspace_switch, NULL, 0);
|
||||
wm_hook_register(HOOK_LAYOUT_CALCULATE, on_layout_calculate, NULL, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void statusbar_shutdown(void) {
|
||||
if (g_bar_surface) {
|
||||
cairo_surface_destroy(g_bar_surface);
|
||||
}
|
||||
}
|
||||
|
||||
wm_plugin_t plugin_statusbar = {
|
||||
.name = "statusbar",
|
||||
.version = "1.0",
|
||||
.init = statusbar_init,
|
||||
.shutdown = statusbar_shutdown
|
||||
};
|
||||
#+END_SRC
|
||||
|
||||
**扩展3:快捷键绑定(插件)**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// plugins/keybindings.c
|
||||
|
||||
#include "plugins/keybindings.h"
|
||||
#include "core/hooks.h"
|
||||
|
||||
// 快捷键配置
|
||||
typedef struct {
|
||||
uint32_t keycode;
|
||||
uint32_t modifiers;
|
||||
void (*action)(void);
|
||||
} keybinding_t;
|
||||
|
||||
static keybinding_t g_bindings[] = {
|
||||
{ .keycode = XK_Return, .modifiers = Mod4Mask, .action = action_launch_terminal },
|
||||
{ .keycode = XK_q, .modifiers = Mod4Mask | ShiftMask, .action = action_close_window },
|
||||
{ .keycode = XK_space, .modifiers = Mod4Mask, .action = action_next_layout },
|
||||
};
|
||||
|
||||
static bool handle_key_press(uint32_t keycode, uint32_t modifiers, void *user_data) {
|
||||
for (size_t i = 0; i < sizeof(g_bindings)/sizeof(g_bindings[0]); i++) {
|
||||
if (g_bindings[i].keycode == keycode &&
|
||||
g_bindings[i].modifiers == modifiers) {
|
||||
g_bindings[i].action();
|
||||
return true; // 事件被消费
|
||||
}
|
||||
}
|
||||
return false; // 继续传递
|
||||
}
|
||||
|
||||
static bool keybindings_init(void) {
|
||||
wm_hook_register(HOOK_KEY_PRESS, handle_key_press, NULL, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void keybindings_shutdown(void) {
|
||||
// 自动取消注册
|
||||
}
|
||||
|
||||
wm_plugin_t plugin_keybindings = {
|
||||
.name = "keybindings",
|
||||
.version = "1.0",
|
||||
.init = keybindings_init,
|
||||
.shutdown = keybindings_shutdown
|
||||
};
|
||||
#+END_SRC
|
||||
|
||||
**扩展4:窗口规则(插件)**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// plugins/rules.c
|
||||
|
||||
#include "plugins/rules.h"
|
||||
#include "core/hooks.h"
|
||||
|
||||
typedef struct {
|
||||
char *class_name;
|
||||
bool floating;
|
||||
bool sticky;
|
||||
wm_workspace_id_t target_workspace;
|
||||
} rule_t;
|
||||
|
||||
static rule_t g_rules[] = {
|
||||
{ .class_name = "floating_window", .floating = true },
|
||||
{ .class_name = "sticky_terminal", .sticky = true },
|
||||
};
|
||||
|
||||
static void on_window_manage(wm_window_t *window, void *user_data) {
|
||||
// 应用规则
|
||||
for (size_t i = 0; i < sizeof(g_rules)/sizeof(g_rules[0]); i++) {
|
||||
if (strcmp(window->class.name, g_rules[i].class_name) == 0) {
|
||||
if (g_rules[i].floating) {
|
||||
wm_set_window_floating(window->id, true);
|
||||
}
|
||||
// ... 应用其他规则
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool rules_init(void) {
|
||||
wm_hook_register(HOOK_WINDOW_MANAGE, on_window_manage, NULL, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void rules_shutdown(void) {
|
||||
// 自动取消注册
|
||||
}
|
||||
|
||||
wm_plugin_t plugin_rules = {
|
||||
.name = "rules",
|
||||
.version = "1.0",
|
||||
.init = rules_init,
|
||||
.shutdown = rules_shutdown
|
||||
};
|
||||
#+END_SRC
|
||||
|
||||
**扩展5:配置系统(插件)**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// plugins/config.c
|
||||
|
||||
#include "plugins/config.h"
|
||||
#include "core/hooks.h"
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
char *value;
|
||||
} config_item_t;
|
||||
|
||||
static config_item_t g_config[] = {
|
||||
{ .key = "terminal", .value = "st" },
|
||||
{ .key = "browser", .value = "firefox" },
|
||||
};
|
||||
|
||||
static const char* config_get(const char *key) {
|
||||
for (size_t i = 0; i < sizeof(g_config)/sizeof(g_config[0]); i++) {
|
||||
if (strcmp(g_config[i].key, key) == 0) {
|
||||
return g_config[i].value;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void on_config_reload(void *event_data) {
|
||||
// 重新加载配置文件
|
||||
load_config_file("~/.config/zdwm/config");
|
||||
}
|
||||
|
||||
static bool config_init(void) {
|
||||
// 加载初始配置
|
||||
load_config_file("~/.config/zdwm/config");
|
||||
|
||||
// 监听配置重载信号
|
||||
wm_hook_register(HOOK_CONFIG_RELOAD, on_config_reload, NULL, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void config_shutdown(void) {
|
||||
// 清理配置资源
|
||||
}
|
||||
|
||||
wm_plugin_t plugin_config = {
|
||||
.name = "config",
|
||||
.version = "1.0",
|
||||
.init = config_init,
|
||||
.shutdown = config_shutdown
|
||||
};
|
||||
#+END_SRC
|
||||
|
||||
* 核心实现(简化版)
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// src/core/zdwm.c
|
||||
|
||||
// 核心 API:触发钩子
|
||||
void wm_hook_trigger_layout_calculate(wm_workspace_t *workspace, rect_t *geometries, size_t count) {
|
||||
wm_hook_t *hook = g_hooks[HOOK_LAYOUT_CALCULATE];
|
||||
while (hook) {
|
||||
wm_hook_layout_calculate_t callback = (wm_hook_layout_calculate_t)hook->callback;
|
||||
callback(workspace, geometries, count, hook->user_data);
|
||||
hook = hook->next;
|
||||
}
|
||||
}
|
||||
|
||||
bool wm_hook_trigger_key_press(uint32_t keycode, uint32_t modifiers) {
|
||||
wm_hook_t *hook = g_hooks[HOOK_KEY_PRESS];
|
||||
while (hook) {
|
||||
wm_hook_key_press_t callback = (wm_hook_key_press_t)hook->callback;
|
||||
if (callback(keycode, modifiers, hook->user_data)) {
|
||||
return true; // 事件被消费
|
||||
}
|
||||
hook = hook->next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 主循环(简化版)
|
||||
void wm_run(void) {
|
||||
xcb_generic_event_t *event;
|
||||
|
||||
while (g_running) {
|
||||
// 1. 获取事件
|
||||
event = xcb_wait_for_event(g_conn);
|
||||
|
||||
// 2. 处理事件
|
||||
wm_handle_event(event);
|
||||
|
||||
// 3. 更新布局(如果需要)
|
||||
if (g_dirty_layout) {
|
||||
wm_update_layout();
|
||||
}
|
||||
|
||||
// 4. 渲染(如果有状态栏插件)
|
||||
// (通过钩子让状态栏插件自己绘制)
|
||||
}
|
||||
}
|
||||
|
||||
// 布局更新
|
||||
void wm_update_layout(void) {
|
||||
for (size_t i = 0; i < g_state.workspace_count; i++) {
|
||||
wm_workspace_t *ws = &g_state.workspaces[i];
|
||||
|
||||
// 分配几何数组
|
||||
rect_t geometries[ws->window_count];
|
||||
|
||||
// 触发布局钩子(布局插件会计算几何)
|
||||
wm_hook_trigger_layout_calculate(ws, geometries, ws->window_count);
|
||||
|
||||
// 应用几何到窗口
|
||||
for (size_t j = 0; j < ws->window_count; j++) {
|
||||
wm_window_t *win = wm_find_window(ws->windows[j]);
|
||||
win->geometry = geometries[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
* 扩展交互示例
|
||||
|
||||
**场景1:状态栏需要显示布局信息**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// 状态栏插件订阅布局钩子
|
||||
static void on_layout_calculate(wm_workspace_t *workspace,
|
||||
rect_t *geometries,
|
||||
size_t count,
|
||||
void *user_data) {
|
||||
// 更新状态栏显示的布局信息
|
||||
update_layout_indicator(workspace, geometries, count);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
**场景2:多个布局算法共存**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// 插件1:tile 布局
|
||||
wm_plugin_t plugin_tile = { .name = "tile", ... };
|
||||
|
||||
// 插件2:monocle 布局
|
||||
wm_plugin_t plugin_monocle = { .name = "monocle", ... };
|
||||
|
||||
// 运行时切换
|
||||
void switch_layout(const char *layout_name) {
|
||||
// 卸载当前布局插件
|
||||
wm_plugin_unload("tile");
|
||||
|
||||
// 加载新布局插件
|
||||
wm_plugin_load(find_plugin(layout_name));
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
**场景3:插件间通信**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// 场景:网络速度插件需要通知状态栏插件更新
|
||||
|
||||
// 方案1:通过核心状态
|
||||
// network 插件更新核心状态中的某个字段
|
||||
// statusbar 插件在 HOOK_LAYOUT_CALCULATE 中读取该字段
|
||||
|
||||
// 方案2:直接函数调用(如果两个插件在同一进程中)
|
||||
extern void statusbar_update_network_speed(uint64_t speed);
|
||||
|
||||
void network_on_speed_update(uint64_t speed) {
|
||||
statusbar_update_network_speed(speed);
|
||||
}
|
||||
|
||||
// 方案3:自定义事件(如果有事件总线插件)
|
||||
event_bus_publish("network.speed_changed", &speed);
|
||||
#+END_SRC
|
||||
|
||||
* 扩展最佳实践
|
||||
|
||||
**✅ 推荐做法**
|
||||
|
||||
1. *插件职责单一*
|
||||
- 一个插件做一件事
|
||||
- 例如:tile 布局插件只负责布局计算
|
||||
|
||||
2. *使用钩子优先级*
|
||||
- 核心功能(如窗口管理)优先级 = 0
|
||||
- 扩展功能(如状态栏)优先级 = 10
|
||||
- 调试功能优先级 = 100
|
||||
|
||||
3. *插件间依赖最小化*
|
||||
- 尽量避免插件间直接调用
|
||||
- 通过核心状态中转
|
||||
|
||||
4. *错误处理*
|
||||
- 插件 init 失败应返回 false
|
||||
- 核心会跳过该插件,继续运行
|
||||
|
||||
**❌ 避免的做法**
|
||||
|
||||
1. *插件直接修改核心结构*
|
||||
#+BEGIN_SRC c
|
||||
// ❌ 不好:插件直接修改核心
|
||||
extern wm_state_t *g_state;
|
||||
g_state->current_workspace = 1;
|
||||
|
||||
// ✅ 好:通过核心 API
|
||||
wm_switch_workspace(1);
|
||||
#+END_SRC
|
||||
|
||||
2. *插件阻塞执行*
|
||||
#+BEGIN_SRC c
|
||||
// ❌ 不好:插件中有阻塞操作
|
||||
void statusbar_init(void) {
|
||||
sleep(1); // 阻塞主循环!
|
||||
}
|
||||
|
||||
// ✅ 好:异步操作
|
||||
void statusbar_init(void) {
|
||||
pthread_create(&thread, NULL, async_init, NULL);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
3. *插件循环依赖*
|
||||
- 插件A依赖插件B
|
||||
- 插件B依赖插件A
|
||||
- 解决方案:通过核心状态解耦
|
||||
|
||||
* 与 mini_core_draft 对比
|
||||
|
||||
| 特性 | mini_core_draft | 最小核心 + 插件 |
|
||||
|--------------+-----------------+-----------------|
|
||||
| 核心代码行数 | ~3000行 | ~1500行 |
|
||||
| 抽象层 | 3-4层 | 1层(钩子) |
|
||||
| 扩展方式 | 服务注册 | 插件+钩子 |
|
||||
| 学习曲线 | 陡峭 | 平缓 |
|
||||
| 性能 | 中等 | 最好 |
|
||||
| 可扩展性 | 很高 | 高 |
|
||||
| 复杂度 | 高 | 低 |
|
||||
|
||||
* 核心代码量估算
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
核心:
|
||||
- zdwm.h (核心 API): 200 行
|
||||
- zdwm.c (核心实现): 500 行
|
||||
- hooks.c (钩子系统): 200 行
|
||||
- plugin.c (插件加载): 150 行
|
||||
------------------------------------
|
||||
核心总计: ~1050 行
|
||||
|
||||
插件示例:
|
||||
- layout_tile.c: 100 行
|
||||
- statusbar.c: 300 行
|
||||
- keybindings.c: 150 行
|
||||
- rules.c: 200 行
|
||||
------------------------------------
|
||||
插件总计: ~750 行
|
||||
|
||||
总计: ~1800 行完整功能的窗口管理器
|
||||
#+END_EXAMPLE
|
||||
|
||||
* 总结
|
||||
|
||||
*最小核心 + 插件*方案的优势:
|
||||
|
||||
1. *核心真正最小* - 只做窗口管理,1500行代码
|
||||
2. *扩展性有保证* - 钩子系统允许任何扩展
|
||||
3. *性能最优* - 一层间接,零开销抽象
|
||||
4. *易于理解* - 新手也能看懂核心代码
|
||||
5. *渐进式开发* - 先核心,后扩展
|
||||
|
||||
这个设计比 ~mini_core_draft~ 简单很多,但扩展性并不差。关键是*钩子系统*设计得当,可以实现大部分扩展需求。
|
||||
@@ -1,407 +0,0 @@
|
||||
# Zdwm 配置系统设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
三层配置系统,按优先级从低到高加载:
|
||||
|
||||
```
|
||||
内置默认配置 → XResources → 动态库配置
|
||||
(后者覆盖前者)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置层次
|
||||
|
||||
### 第一层:内置默认配置
|
||||
- **位置**: 编译到二进制中
|
||||
- **作用**: 提供完整的默认配置,确保无外部配置时也能正常运行
|
||||
- **范围**: 所有配置项
|
||||
|
||||
### 第二层:XResources 配置
|
||||
- **位置**: `~/.Xresources` 或 `~/.config/zdwm/Xresources`
|
||||
- **作用**: 覆盖外观配置(颜色、字体、边框等)
|
||||
- **特点**: 无需重新编译,`xrdb` 后重启生效
|
||||
- **范围**: 仅外观配置
|
||||
|
||||
### 第三层:动态库配置
|
||||
- **位置**: `~/.config/zdwm/config.c` → `config.so`
|
||||
- **作用**: 高级配置(快捷键、规则、自定义布局等)
|
||||
- **特点**: 最大灵活性,需编译 C 代码
|
||||
- **范围**: 所有配置项
|
||||
|
||||
---
|
||||
|
||||
## 配置加载流程
|
||||
|
||||
```
|
||||
1. 加载内置默认配置
|
||||
↓
|
||||
2. 尝试加载 XResources
|
||||
├─ 成功 → 覆盖默认配置中的外观项
|
||||
└─ 失败 → 跳过,保持默认值
|
||||
↓
|
||||
3. 尝试加载动态库
|
||||
├─ 存在 → 调用 zdwm_config_init(),增量修改配置
|
||||
└─ 不存在 → 跳过
|
||||
↓
|
||||
4. 应用最终配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 配置优先级标记
|
||||
|
||||
```c
|
||||
typedef struct config_source_t {
|
||||
bool has_xres; // 是否加载了 XResources
|
||||
bool has_user_lib; // 是否加载了用户动态库
|
||||
} config_source_t;
|
||||
```
|
||||
|
||||
### 完整配置结构
|
||||
|
||||
```c
|
||||
typedef struct config_t {
|
||||
// 外观
|
||||
char *font_family;
|
||||
uint32_t font_size;
|
||||
uint32_t dpi;
|
||||
uint16_t border_width;
|
||||
uint16_t bar_y_padding;
|
||||
uint16_t tag_x_padding;
|
||||
color_set_t colors;
|
||||
|
||||
// 快捷键
|
||||
const keyboard_t *key_list;
|
||||
size_t key_count;
|
||||
|
||||
// 布局
|
||||
const layout_t *layout_list;
|
||||
size_t layout_count;
|
||||
|
||||
// 标签
|
||||
const char *const *tags;
|
||||
|
||||
// 规则
|
||||
const rule_t *rules;
|
||||
size_t rules_count;
|
||||
|
||||
// 自动启动
|
||||
const char *const *autostart_list;
|
||||
|
||||
// 钩子
|
||||
void (*hook_manage_new)(client_t *client);
|
||||
void (*hook_manage_unmanage)(client_t *client);
|
||||
|
||||
// 元数据
|
||||
config_source_t source;
|
||||
} config_t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口(动态库)
|
||||
|
||||
### zdwm_api_t
|
||||
|
||||
```c
|
||||
typedef struct zdwm_api_t {
|
||||
uint32_t version;
|
||||
|
||||
// 外观(覆盖 XResources)
|
||||
void (*set_font)(const char *family, uint32_t size);
|
||||
void (*set_dpi)(uint32_t dpi);
|
||||
void (*set_color)(const char *name, const char *hex);
|
||||
void (*set_border_width)(uint16_t width);
|
||||
void (*set_padding)(uint16_t bar_y, uint16_t tag_x);
|
||||
|
||||
// 快捷键(增量式)
|
||||
void (*add_keybinding)(uint32_t modifiers, xcb_keysym_t keysym,
|
||||
void (*action)(const void *),
|
||||
const void *arg);
|
||||
void (*remove_keybinding)(uint32_t modifiers, xcb_keysym_t keysym);
|
||||
void (*clear_keybindings)(void);
|
||||
|
||||
// 规则
|
||||
void (*add_rule)(const char *role, const char *class,
|
||||
int32_t tag_index, bool floating,
|
||||
bool fullscreen, bool maximize,
|
||||
bool switch_to_tag);
|
||||
|
||||
// 自动启动
|
||||
void (*add_autostart)(const char *command);
|
||||
|
||||
// 布局
|
||||
void (*add_layout)(const char *symbol, void (*arrange)(tag_t *tag));
|
||||
|
||||
// 钩子
|
||||
void (*hook_manage_new)(void (*hook)(client_t *client));
|
||||
void (*hook_manage_unmanage)(void (*hook)(client_t *client));
|
||||
} zdwm_api_t;
|
||||
```
|
||||
|
||||
### 用户配置初始化函数
|
||||
|
||||
```c
|
||||
typedef struct zdwm_user_config_t {
|
||||
void (*cleanup)(void);
|
||||
} zdwm_user_config_t;
|
||||
|
||||
// 用户在 config.c 中实现此函数(可选)
|
||||
zdwm_user_config_t *zdwm_config_init(zdwm_api_t *api);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## XResources 支持的配置项
|
||||
|
||||
### 配置项命名规范
|
||||
|
||||
```
|
||||
Zdwm.font.family # 字体族
|
||||
Zdwm.font.size # 字体大小
|
||||
Zdwm.border.width # 边框宽度
|
||||
Zdwm.padding.bar_y # Bar 上下内边距
|
||||
Zdwm.padding.tag_x # Tag 左右内边距
|
||||
Zdwm.color.bar # Bar 背景色
|
||||
Zdwm.color.tag # Tag 背景色
|
||||
Zdwm.color.tagActive # 活动 Tag 背景色
|
||||
Zdwm.color.tagText # Tag 文字颜色
|
||||
Zdwm.color.tagActiveText # 活动 Tag 文字颜色
|
||||
Zdwm.color.border # 边框颜色
|
||||
Zdwm.color.borderActive # 活动窗口边框颜色
|
||||
```
|
||||
|
||||
### 数据类型映射
|
||||
|
||||
```
|
||||
XResources 类型 → C 类型
|
||||
--------------------------→-------------------
|
||||
字符串 → char *
|
||||
整数 → uint32_t
|
||||
颜色 (#RRGGBB) → color_t (uint32_t argb)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现要点
|
||||
|
||||
### 1. 配置加载函数
|
||||
|
||||
```c
|
||||
// src/config.h
|
||||
config_t *config_load_full(void);
|
||||
void config_free(config_t *config);
|
||||
config_t *config_reload(void);
|
||||
```
|
||||
|
||||
### 2. 加载顺序
|
||||
|
||||
```c
|
||||
// src/config.c
|
||||
config_t *config_load_full(void) {
|
||||
config_t *config = calloc(1, sizeof(config_t));
|
||||
|
||||
// 第一步:默认配置
|
||||
config_apply_defaults(config);
|
||||
|
||||
// 第二步:XResources(覆盖外观)
|
||||
config_load_xres(config);
|
||||
|
||||
// 第三步:动态库(覆盖所有)
|
||||
config_load_user_lib(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. XResources 解析
|
||||
|
||||
```c
|
||||
// src/config_xres.c
|
||||
bool config_load_xres(config_t *config) {
|
||||
if (!xres_init_xrm_db()) return false;
|
||||
|
||||
// 覆盖外观配置
|
||||
xres_get_string("Zdwm.font.family", &config->font_family, default_font);
|
||||
xres_get_uint32("Zdwm.font.size", &config->font_size);
|
||||
// ...
|
||||
|
||||
xres_clean();
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 动态库加载
|
||||
|
||||
```c
|
||||
// src/config_lib.c
|
||||
bool config_load_user_lib(config_t *config) {
|
||||
const char *path = "~/.config/zdwm/config.so";
|
||||
|
||||
void *handle = dlopen(path, RTLD_LAZY);
|
||||
if (!handle) return false;
|
||||
|
||||
zdwm_user_config_t *(*init)(zdwm_api_t *) = dlsym(handle, "zdwm_config_init");
|
||||
if (!init) {
|
||||
dlclose(handle);
|
||||
return false; // 没有配置函数是正常的
|
||||
}
|
||||
|
||||
zdwm_api_t api = create_api(config);
|
||||
init(&api);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置热重载
|
||||
|
||||
### XResources 热重载
|
||||
|
||||
编辑 XResources 后的操作流程:
|
||||
|
||||
```bash
|
||||
# 1. 编辑配置文件
|
||||
vim ~/.Xresources
|
||||
|
||||
# 2. 加载到 X Server
|
||||
xrdb -merge ~/.Xresources
|
||||
|
||||
# 3. 重载 zdwm 配置
|
||||
killall -HUP zdwm
|
||||
```
|
||||
|
||||
### 实现要点
|
||||
|
||||
```c
|
||||
// src/wm.c
|
||||
|
||||
// 重新应用配置
|
||||
static void wm_reapply_config(void) {
|
||||
// 重新初始化字体
|
||||
text_reinit_pango_layout(wm.font_family, wm.font_size, wm.dpi);
|
||||
|
||||
// 重新计算 bar 高度
|
||||
wm.bar_height = text_get_height() + 2 * wm.padding.bar_y;
|
||||
|
||||
// 刷新所有 monitor 的 bar
|
||||
for (monitor_t *m = wm.monitor_list; m; m = m->next) {
|
||||
monitor_draw_bar(m);
|
||||
}
|
||||
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
// SIGHUP 信号处理器
|
||||
static void reload_config_handler(int sig) {
|
||||
log("Received SIGHUP, reloading configuration...");
|
||||
|
||||
// 清理旧的 XResources 数据库
|
||||
xres_clean();
|
||||
|
||||
// 重新加载 XResources
|
||||
wm_get_xres_config();
|
||||
|
||||
// 重新应用配置
|
||||
wm_reapply_config();
|
||||
|
||||
log("Configuration reloaded successfully");
|
||||
}
|
||||
|
||||
// 在 wm_setup_signal() 中注册
|
||||
void wm_setup_signal(void) {
|
||||
// ... 现有信号处理 ...
|
||||
|
||||
signal(SIGHUP, reload_config_handler);
|
||||
signal(SIGUSR1, reload_config_handler); // 备用信号
|
||||
}
|
||||
```
|
||||
|
||||
### xres 模块需要添加
|
||||
|
||||
```c
|
||||
// src/xres.h
|
||||
void xres_reload(void);
|
||||
|
||||
// src/xres.c
|
||||
void xres_reload(void) {
|
||||
if (wm.xrm) {
|
||||
xcb_xrm_database_free(wm.xrm);
|
||||
wm.xrm = nullptr;
|
||||
}
|
||||
xres_init_xrm_db();
|
||||
}
|
||||
```
|
||||
|
||||
### 完整重载流程
|
||||
|
||||
```
|
||||
用户编辑 XResources
|
||||
↓
|
||||
xrdb -merge ~/.Xresources (加载到 X Server)
|
||||
↓
|
||||
killall -HUP zdwm (发送重载信号)
|
||||
↓
|
||||
zdwm 收到 SIGHUP
|
||||
↓
|
||||
xres_clean() (清理旧数据库)
|
||||
↓
|
||||
wm_get_xres_config() (重新读取 XResources)
|
||||
↓
|
||||
wm_reapply_config() (应用新配置)
|
||||
↓
|
||||
monitor_draw_bar() (刷新显示)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── config.h # 配置系统头文件
|
||||
├── config.c # 配置加载主逻辑
|
||||
├── config_default.c # 内置默认配置
|
||||
├── config_xres.c # XResources 解析
|
||||
├── config_lib.c # 动态库加载
|
||||
└── config_api.c # API 实现(供动态库调用)
|
||||
|
||||
docs/
|
||||
└── config_system.md # 本文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
config.c
|
||||
├── config_default.c (内置默认值)
|
||||
├── config_xres.c (XResources 解析)
|
||||
│ └── xres.c (已有)
|
||||
└── config_lib.c (动态库加载)
|
||||
└── dlopen()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现优先级
|
||||
|
||||
| 优先级 | 模块 | 说明 |
|
||||
|--------|------------------|--------------------------------|
|
||||
| P0 | config_default.c | 定义默认配置值 |
|
||||
| P0 | config.c | 主加载逻辑 |
|
||||
| P1 | config_xres.c | XResources 支持(已有基础) |
|
||||
| P1 | 热重载 | SIGHUP 支持(配合 XResources) |
|
||||
| P1 | config_lib.c | 动态库加载 |
|
||||
| P2 | config_api.c | API 实现 |
|
||||
|
||||
**P0**: 核心功能,必须实现
|
||||
**P1**: 重要功能,提升用户体验
|
||||
**P2**: 增强功能,可后续添加
|
||||
@@ -1,412 +0,0 @@
|
||||
* ZDWM 配置系统设计
|
||||
|
||||
** 概述
|
||||
|
||||
配置系统现在只保留两类正式输入:
|
||||
|
||||
1. 编译进二进制的默认配置
|
||||
2. 用户动态库配置(=~/.config/zdwm/config.c= 编译得到 =config.so=)
|
||||
|
||||
不再支持 XResources,也不再保留 =Xft.dpi= 作为兼容输入。字体、DPI、颜色、边框、布局、规则、快捷键和服务设置都只来自默认配置和动态库配置。
|
||||
|
||||
配置系统的职责也收紧为:
|
||||
|
||||
1. 生成 core-native 配置快照。
|
||||
2. 为 runtime adapter、service 层和输入绑定层提供稳定输入。
|
||||
3. 不直接操作 =wm_state_t= 。
|
||||
4. 不再暴露 =client_t= 、=tag_t= 、=monitor_t= 之类的旧对象。
|
||||
|
||||
** 设计目标
|
||||
|
||||
1. 配置层继续放在核心之外。
|
||||
2. 配置结果必须能无损对接新的 =runtime/policy/layout/service= 边界。
|
||||
3. 配置重载应以“重建快照并显式应用”为单位,而不是任意时刻写全局内存。
|
||||
4. 彻底移除 XResources 路径,避免维护两套并行语义。
|
||||
|
||||
** 配置来源与优先级
|
||||
|
||||
最终有效配置按下面顺序构建:
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
默认配置
|
||||
↓
|
||||
动态库配置 config.so
|
||||
↓
|
||||
冻结为 zdwm_config_t
|
||||
#+END_EXAMPLE
|
||||
|
||||
动态库配置优先级高于默认配置。
|
||||
|
||||
** 非目标
|
||||
|
||||
下面这些能力不再属于配置系统设计范围:
|
||||
|
||||
1. =Zdwm.*= 风格的 XResources 覆盖。
|
||||
2. =Xft.dpi= 的额外兼容读取。
|
||||
3. =hook_manage_new(client_t *)= 和 =hook_manage_unmanage(client_t *)= 这类直接对象钩子。
|
||||
4. =arrange(tag_t *)= 这类依赖旧 tag 模型的布局接口。
|
||||
5. 从配置层直接访问或修改 =wm= 全局对象。
|
||||
|
||||
** 配置输出模型
|
||||
|
||||
配置系统输出一份冻结后的配置快照,而不是一组可随时调用的副作用回调。
|
||||
|
||||
*** 外观配置
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_appearance_config_t {
|
||||
char *font_family;
|
||||
uint32_t font_size;
|
||||
uint32_t dpi;
|
||||
uint16_t border_width;
|
||||
uint16_t bar_y_padding;
|
||||
uint16_t tag_x_padding;
|
||||
color_set_t colors;
|
||||
} zdwm_appearance_config_t;
|
||||
#+END_SRC
|
||||
|
||||
这部分不进入最小核心状态,而是供 render/bar/text 这类核心外服务使用。
|
||||
|
||||
*** workspace 配置
|
||||
|
||||
=wm_workspace_t= 是运行时状态结构,里面包含 =focused_window_id= 这类不属于静态配置的数据。因此配置层应单独定义 workspace 配置描述:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_workspace_config_t {
|
||||
wm_workspace_id_t id;
|
||||
wm_layout_id_t initial_layout_id;
|
||||
|
||||
const char *name;
|
||||
const char *symbol_text;
|
||||
const char *symbol_icon_path;
|
||||
} zdwm_workspace_config_t;
|
||||
#+END_SRC
|
||||
|
||||
runtime adapter 会把它拆成:
|
||||
|
||||
1. =wm_workspace_t= 的初始数组
|
||||
2. =wm_workspace_desc_t= 描述表
|
||||
|
||||
*** 管理规则
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_manage_rule_t {
|
||||
const char *title;
|
||||
const char *app_id;
|
||||
const char *class_name;
|
||||
const char *instance_name;
|
||||
|
||||
wm_workspace_id_t workspace_id;
|
||||
wm_manage_window_init_t initial_state;
|
||||
bool switch_to_workspace;
|
||||
} zdwm_manage_rule_t;
|
||||
#+END_SRC
|
||||
|
||||
规则层的职责仅限于:
|
||||
|
||||
1. 匹配窗口元数据。
|
||||
2. 决定 =MANAGE_WINDOW= 的 =workspace_id= 。
|
||||
3. 决定 =MANAGE_WINDOW= 的 =initial_state= 。
|
||||
4. 可选地决定是否切到目标 workspace。
|
||||
|
||||
规则层不直接管理窗口对象,也不直接写 =wm_state_t= 。
|
||||
|
||||
*** 快捷键绑定
|
||||
|
||||
快捷键也需要从旧的“直接 action 回调”收敛到新边界。
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef enum zdwm_binding_target_t {
|
||||
ZDWM_BINDING_CORE_COMMAND,
|
||||
ZDWM_BINDING_SERVICE_ACTION,
|
||||
} zdwm_binding_target_t;
|
||||
|
||||
typedef struct zdwm_service_action_t {
|
||||
const char *service_name;
|
||||
const char *action_name;
|
||||
const char *string_arg;
|
||||
int32_t int_arg;
|
||||
} zdwm_service_action_t;
|
||||
|
||||
typedef struct zdwm_keybinding_t {
|
||||
uint32_t modifiers;
|
||||
xkb_keysym_t keysym;
|
||||
zdwm_binding_target_t target;
|
||||
union {
|
||||
wm_command_t command;
|
||||
zdwm_service_action_t service_action;
|
||||
} as;
|
||||
} zdwm_keybinding_t;
|
||||
#+END_SRC
|
||||
|
||||
=core command= 用于工作区切换、布局切换、浮动切换、焦点切换等窗口管理行为。
|
||||
|
||||
=service action= 用于启动外部程序、刷新状态服务、触发核心外功能。
|
||||
|
||||
*** 服务设置
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_service_setting_t {
|
||||
const char *service_name;
|
||||
const char *key;
|
||||
const char *value;
|
||||
} zdwm_service_setting_t;
|
||||
#+END_SRC
|
||||
|
||||
服务配置由配置层生成,但仍由 service 层解释和持有。
|
||||
|
||||
*** 总配置快照
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_config_source_t {
|
||||
bool has_user_lib;
|
||||
} zdwm_config_source_t;
|
||||
|
||||
typedef struct zdwm_runtime_config_t {
|
||||
wm_policy_config_t policy;
|
||||
|
||||
zdwm_workspace_config_t *workspaces;
|
||||
size_t workspace_count;
|
||||
|
||||
wm_layout_slot_t *layouts;
|
||||
size_t layout_count;
|
||||
} zdwm_runtime_config_t;
|
||||
|
||||
typedef struct zdwm_config_t {
|
||||
zdwm_runtime_config_t runtime;
|
||||
zdwm_appearance_config_t appearance;
|
||||
|
||||
zdwm_manage_rule_t *rules;
|
||||
size_t rule_count;
|
||||
|
||||
zdwm_keybinding_t *keybindings;
|
||||
size_t keybinding_count;
|
||||
|
||||
const char *const *autostart_list;
|
||||
|
||||
zdwm_service_setting_t *service_settings;
|
||||
size_t service_setting_count;
|
||||
|
||||
zdwm_config_source_t source;
|
||||
} zdwm_config_t;
|
||||
#+END_SRC
|
||||
|
||||
注意这里故意不把 =outputs= 和启动时已有窗口塞进配置结果里。它们属于 backend 发现结果,不属于静态配置。
|
||||
|
||||
** 与最小核心的集成方式
|
||||
|
||||
配置层与 runtime 的拼装分为两步:
|
||||
|
||||
*** 第一步:构建配置快照
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
默认配置
|
||||
↓
|
||||
config.so 覆盖与增量注册
|
||||
↓
|
||||
zdwm_config_t
|
||||
#+END_EXAMPLE
|
||||
|
||||
*** 第二步:由 adapter 组装 runtime bootstrap
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
zdwm_config_t
|
||||
+
|
||||
backend 发现的 outputs
|
||||
+
|
||||
backend 扫描得到的初始 MANAGE_WINDOW 命令
|
||||
↓
|
||||
wm_runtime_bootstrap_t
|
||||
#+END_EXAMPLE
|
||||
|
||||
组装规则应为:
|
||||
|
||||
1. =policy= 来自 =config.runtime.policy= 。
|
||||
2. =workspaces= 来自 =config.runtime.workspaces[]= 的静态定义。
|
||||
3. =workspace_descs= 来自 =config.runtime.workspaces[]= 中的展示字段。
|
||||
4. =outputs= 来自 backend。
|
||||
5. =initial_commands= 来自 backend 对已有窗口的扫描和规则匹配。
|
||||
|
||||
这样配置层不需要知道当前有哪些物理输出,也不需要自己扫描窗口。
|
||||
|
||||
** 动态库 API
|
||||
|
||||
动态库 API 由“builder 风格”的配置写入接口组成:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_api_t {
|
||||
uint32_t version;
|
||||
|
||||
// appearance
|
||||
void (*set_font)(const char *family, uint32_t size);
|
||||
void (*set_dpi)(uint32_t dpi);
|
||||
void (*set_color)(const char *name, const char *hex);
|
||||
void (*set_border_width)(uint16_t width);
|
||||
void (*set_padding)(uint16_t bar_y, uint16_t tag_x);
|
||||
|
||||
// runtime config
|
||||
void (*set_policy)(wm_policy_config_t policy);
|
||||
void (*define_workspace)(zdwm_workspace_config_t workspace);
|
||||
void (*register_layout)(wm_layout_slot_t layout);
|
||||
|
||||
// rules
|
||||
void (*add_rule)(zdwm_manage_rule_t rule);
|
||||
|
||||
// bindings
|
||||
void (*add_keybinding)(zdwm_keybinding_t binding);
|
||||
void (*clear_keybindings)(void);
|
||||
|
||||
// autostart and services
|
||||
void (*add_autostart)(const char *command);
|
||||
void (*set_service_option)(const char *service_name,
|
||||
const char *key,
|
||||
const char *value);
|
||||
} zdwm_api_t;
|
||||
#+END_SRC
|
||||
|
||||
用户动态库入口保持简单:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct zdwm_user_config_t {
|
||||
void (*cleanup)(void);
|
||||
} zdwm_user_config_t;
|
||||
|
||||
zdwm_user_config_t *zdwm_config_init(zdwm_api_t *api);
|
||||
#+END_SRC
|
||||
|
||||
=cleanup()= 只负责动态库自身资源,不负责 runtime 状态回收。
|
||||
|
||||
** 加载流程
|
||||
|
||||
#+BEGIN_SRC c
|
||||
zdwm_config_t *config_load(void) {
|
||||
zdwm_config_t *config = calloc(1, sizeof(*config));
|
||||
|
||||
config_apply_defaults(config);
|
||||
config_load_user_lib(config);
|
||||
config_finalize(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
这里的 =config_finalize()= 建议至少做这些事情:
|
||||
|
||||
1. 检查 workspace id 是否唯一。
|
||||
2. 检查 layout id 是否唯一。
|
||||
3. 检查规则和快捷键数组是否合法。
|
||||
4. 补齐未设置的默认值。
|
||||
5. 冻结动态数组,避免运行时再被随意写入。
|
||||
|
||||
** 管理规则接入链路
|
||||
|
||||
管理规则不直接操作窗口,推荐链路如下:
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
backend 发现新窗口
|
||||
↓
|
||||
同步窗口 metadata
|
||||
↓
|
||||
在 rules 中匹配
|
||||
↓
|
||||
生成 WM_COMMAND_MANAGE_WINDOW
|
||||
- workspace_id
|
||||
- initial_state
|
||||
↓
|
||||
runtime / policy apply_command()
|
||||
#+END_EXAMPLE
|
||||
|
||||
这样规则层只负责“把 metadata 翻译成命令参数”。
|
||||
|
||||
** 快捷键接入链路
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
backend 输入事件
|
||||
↓
|
||||
keybinding table 匹配
|
||||
↓
|
||||
若 target == ZDWM_BINDING_CORE_COMMAND:
|
||||
生成 wm_command_t
|
||||
若 target == ZDWM_BINDING_SERVICE_ACTION:
|
||||
发送给 service 层
|
||||
#+END_EXAMPLE
|
||||
|
||||
配置层不再保存 =void (*action)(const void *)= 这类旧式回调。
|
||||
|
||||
** 热重载
|
||||
|
||||
配置热重载仍然可以保留,但边界必须清晰。
|
||||
|
||||
推荐流程:
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
收到 SIGHUP
|
||||
↓
|
||||
重建 zdwm_config_t
|
||||
↓
|
||||
原子替换:
|
||||
- appearance
|
||||
- policy
|
||||
- keybindings
|
||||
- workspace descriptors
|
||||
- layout registry
|
||||
- service settings
|
||||
↓
|
||||
通知相关 service 刷新
|
||||
#+END_EXAMPLE
|
||||
|
||||
热重载约束:
|
||||
|
||||
1. 不直接改 =wm_state_t= 。
|
||||
2. 规则变更只影响后续新管理的窗口,不回溯重算现有窗口。
|
||||
3. workspace 描述、颜色、字体、DPI、bindings、policy 适合热重载。
|
||||
4. 大规模布局或 service 拓扑变化,必要时可以走受控重启。
|
||||
|
||||
** 文件结构
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
src/
|
||||
├── config.h # 配置系统头文件
|
||||
├── config.c # 配置加载主逻辑
|
||||
├── config_defaults.c # 默认配置
|
||||
├── config_lib.c # 动态库加载
|
||||
├── config_api.c # builder API
|
||||
└── config_validate.c # 配置收尾校验(可选)
|
||||
|
||||
docs/
|
||||
└── config_system.org # 本文档
|
||||
#+END_EXAMPLE
|
||||
|
||||
这里不再有 =config_xres.c= 或任何通用 XResources 解析模块。
|
||||
|
||||
** 迁移说明
|
||||
|
||||
下面这些旧设计应视为废弃:
|
||||
|
||||
1. =默认配置 -> XResources -> 动态库配置= 三层覆盖模型。
|
||||
2. =Xft.dpi= 的兼容读取。
|
||||
3. =hook_manage_new(client_t *)= 和 =hook_manage_unmanage(client_t *)= 。
|
||||
4. =add_layout(... arrange(tag_t *tag))= 。
|
||||
5. 任何直接依赖 =client_t= 、=tag_t= 、=monitor_t= 的配置接口。
|
||||
|
||||
迁移后的对应关系:
|
||||
|
||||
1. tag 名称与符号 → =zdwm_workspace_config_t=
|
||||
2. 布局注册 → =wm_layout_slot_t=
|
||||
3. 窗口规则 → =zdwm_manage_rule_t=
|
||||
4. 快捷键动作 → =wm_command_t= 或 =zdwm_service_action_t=
|
||||
5. 外观设置 → =zdwm_appearance_config_t=
|
||||
|
||||
** 实施优先级
|
||||
|
||||
1. =config_defaults.c=
|
||||
先保证“无用户配置”也能跑。
|
||||
|
||||
2. =config.c + config_lib.c=
|
||||
打通默认配置和动态库配置装配。
|
||||
|
||||
3. =config_api.c=
|
||||
落地新的 builder API,替代旧 action/hook 模型。
|
||||
|
||||
4. runtime adapter 对接
|
||||
把 =zdwm_config_t= 装配成 =wm_runtime_bootstrap_t= 、规则表、service 设置和 keybinding table。
|
||||
@@ -1,462 +0,0 @@
|
||||
* ZDWM 最小核心草案
|
||||
|
||||
这个目录放的是一套头文件级别的最小核心 API 草案,目标是先把窗口管理器最核心的对象关系、子系统边界和主执行流程固定下来,再决定具体实现。
|
||||
|
||||
* 目标
|
||||
|
||||
1. 保持核心技术栈简洁,不引入脚本运行时、序列化协议和多进程插件宿主。
|
||||
2. 先把最小核心收敛到可以管理窗口、计算布局、处理输入、提交后端副作用。
|
||||
3. 将渲染、状态栏、配置、插件都放在核心之外,作为后续服务挂接。
|
||||
|
||||
* 最小核心子系统
|
||||
|
||||
1. =runtime=
|
||||
负责主循环、模块装配和固定执行顺序。
|
||||
|
||||
2. =backend=
|
||||
负责平台交互。它把平台原始事件转成 =wm_event_t= ,执行 =wm_effect_t= ,
|
||||
并在需要时根据固定的键盘/鼠标绑定表安装平台侧被动抓键 / 抓按钮。
|
||||
像 X11 的 =_NET_ACTIVE_WINDOW= 这类 =ClientMessage= 也应在这里先归一化成
|
||||
request event,而不是直接改 core 状态。
|
||||
|
||||
3. =state=
|
||||
负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。
|
||||
当前头文件级草案先直接展示结构体字段,便于讨论;
|
||||
实现阶段再收敛为不透明结构体也不迟。
|
||||
窗口元数据(title/app_id/class/instance)直接存储在 =wm_window_t= 中。
|
||||
窗口 hint / capability(如 =urgent= / =fixed_size= / =skip_taskbar=)也直接存储在
|
||||
=wm_window_t= 中。
|
||||
工作区名称存储在 =wm_workspace_t= 中。
|
||||
|
||||
4. =policy=
|
||||
负责把事件路由为命令,并把命令应用到状态。
|
||||
=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 的路由都使用 bootstrap
|
||||
固定下来的输入绑定表;像 =WM_EVENT_POINTER_ENTER= 这种不经绑定的输入事实,
|
||||
则由 policy 配置决定是否转换为 =FOCUS_WINDOW=。
|
||||
|
||||
5. =layout=
|
||||
负责为某个输出上当前显示的工作区中的平铺窗口计算外框几何。
|
||||
|
||||
* ID 分配策略
|
||||
|
||||
最小核心采用"运行时固定,启动时配置"的 ID 策略:
|
||||
|
||||
**半静态实体(启动时分配,运行时固定)**:
|
||||
- =workspace= : 启动时根据配置分配 N 个(如 10 个),ID = 数组索引(0-N-1)
|
||||
- =output= : 启动时根据扫描到的显示器数量分配,ID = 数组索引
|
||||
- =layout= : 启动时根据注册的布局算法分配,ID = 数组索引
|
||||
|
||||
这些实体在启动时一次性分配,运行时不再增删。
|
||||
访问时直接用索引,零开销查找,无需映射表。
|
||||
|
||||
```c
|
||||
// 半静态实体:直接访问(零开销)
|
||||
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
|
||||
wm_output_t *output = &state->outputs[output_id]; // O(1)
|
||||
```
|
||||
|
||||
**动态实体(运行时增删)**:
|
||||
- =window= : 动态数组,ID 由后端生成(如 X11 的 xcb_window_t)
|
||||
查找时线性搜索 O(n),对于几十到几百个窗口性能足够。
|
||||
(后续如成为性能热点,可改用 hashmap 优化到 O(1))
|
||||
|
||||
```c
|
||||
// 半静态实体:直接访问(零开销)
|
||||
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
|
||||
wm_output_t *output = &state->outputs[output_id]; // O(1)
|
||||
|
||||
// 动态实体:线性查找(简单且足够)
|
||||
const wm_window_t *win = wm_state_window_get(state, window_id); // O(n)
|
||||
```
|
||||
|
||||
**设计优势**:
|
||||
- 灵活性:workspace 数量由配置决定,不硬编码
|
||||
- 性能:半静态实体零开销访问,动态实体简单线性查找
|
||||
- 简单性:无需复杂的映射表,代码更清晰
|
||||
|
||||
* 固定集合约束
|
||||
|
||||
以下集合一经 bootstrap 确定,运行时不再增删或重建:
|
||||
|
||||
1. =outputs=
|
||||
2. =workspaces=
|
||||
3. =layout registry=
|
||||
4. 每个 workspace 的 =available_layouts=
|
||||
5. =key binding table=
|
||||
6. =pointer binding table=
|
||||
|
||||
运行时允许变化的只是:
|
||||
|
||||
1. 某个 output 当前显示的 =current_workspace_id=
|
||||
2. 某个 workspace 当前选中的 =layout_id=
|
||||
3. 窗口集合及窗口运行态
|
||||
|
||||
* 非核心但常见的服务
|
||||
|
||||
这些服务故意不放进最小核心:
|
||||
|
||||
1. =render=
|
||||
状态栏、标题和其他装饰绘制。窗口边框不是独立服务,而是
|
||||
=WM_EFFECT_CONFIGURE_WINDOW= 的一部分。
|
||||
|
||||
2. =status=
|
||||
网速、内存、CPU、音量、时间等信息采集与聚合。
|
||||
|
||||
3. =config=
|
||||
默认配置和用户覆盖。
|
||||
|
||||
4. =plugin=
|
||||
动态扩展加载。
|
||||
|
||||
* 运行时装配与所有权
|
||||
|
||||
为了避免再次出现当前全局 =wm= 那样的大对象耦合,最小核心里的所有权再收紧一层:
|
||||
|
||||
1. =runtime= 拥有 =state= 、=plan= 、=command_buffer= 和 =layout_registry= 。
|
||||
其中 workspace/output/layout registry 集合在 init 完成后保持固定。
|
||||
2. =runtime= 也拥有 =policy config= 、键盘/鼠标绑定表视图、全局边框配置、
|
||||
交互态和服务注册表。
|
||||
3. =backend= 只负责平台事件翻译和平台副作用,不拥有控制状态,不直接拼 bar。
|
||||
4. 窗口元数据(title/app_id/class/instance)直接存储在 =wm_window_t= 中,与控制状态一同管理。
|
||||
5. 工作区名称存储在 =wm_workspace_t= 中,与控制状态一同管理。
|
||||
|
||||
* 启动与初始化约定
|
||||
|
||||
最小核心的初始化流程:
|
||||
|
||||
1. =wm_runtime_init()= 接收一份 bootstrap 描述,包含:
|
||||
- 工作区数量和初始配置(由用户配置决定)
|
||||
- 布局算法注册(由代码或配置决定)
|
||||
- 策略配置(focus_raises 等)
|
||||
- 键盘绑定表(规范化的 =keysym + modifiers -> wm_command_t= 模板)
|
||||
- 鼠标按键绑定表(规范化的 =button + modifiers + target -> wm_command_t= 模板)
|
||||
- 全局统一边框宽度
|
||||
- 全局边框颜色集(普通/焦点)
|
||||
- 初始命令(如启动时扫描到的窗口)
|
||||
|
||||
2. 启动时扫描到的已有窗口,不直接塞进 =wm_state_t= ,而是翻译成一批 =MANAGE_WINDOW= 命令进入 runtime。
|
||||
|
||||
3. 若 backend 提供 =set_keybindings()= / =set_pointer_bindings()= ,runtime
|
||||
在 init 时把 bootstrap 里的绑定表同步给 backend,供平台侧建立被动抓键 /
|
||||
抓按钮。
|
||||
|
||||
4. 工作区在启动时根据配置分配 N 个,运行时不再增删。
|
||||
|
||||
5. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。
|
||||
|
||||
6. layout registry 在启动时注册完成,运行时不再增删。
|
||||
|
||||
* 显示器热插拔策略
|
||||
|
||||
当检测到显示器配置变化(新增/删除/几何改变)时:
|
||||
|
||||
1. 后端检测到 RandR/Output 变化,使 =backend.next_event()= 返回 =WM_BACKEND_NEXT_RESTART_REQUIRED=
|
||||
2. Runtime 退出主循环,清理资源
|
||||
3. 由启动脚本(systemd/xinitrc)自动重启 WM
|
||||
4. WM 重启后重新扫描显示器配置,从干净状态启动
|
||||
|
||||
注意:
|
||||
- core 不处理 =output changed/removed= 这类增量事件
|
||||
- core 内不存在运行时增删 output/workspace/layout 集合的命令
|
||||
|
||||
**优势**:
|
||||
- ✅ 逻辑简单,无需复杂的运行时重配置
|
||||
- ✅ 状态一致,避免增量更新的边界情况
|
||||
- ✅ 实现可靠,从干净状态启动
|
||||
|
||||
**代价**:
|
||||
- 窗口会短暂闪烁(~1-2 秒)
|
||||
- 但相比复杂的热插拔逻辑,这是合理的工程权衡
|
||||
|
||||
* Workspace-Output 配置
|
||||
|
||||
工作区配置可以在启动阶段根据输出数量生成;一旦 bootstrap 完成,归属关系保持固定:
|
||||
|
||||
**配置方式 1:手动指定**
|
||||
```lua
|
||||
workspaces = {
|
||||
{ id = 1, name = "web", output_id = 0 }, -- DP-1 的 workspace
|
||||
{ id = 2, name = "code", output_id = 0 },
|
||||
{ id = 3, name = "chat", output_id = 1 }, -- DP-2 的 workspace
|
||||
{ id = 4, name = "media", output_id = 1 },
|
||||
}
|
||||
```
|
||||
|
||||
**配置方式 2:规则式**
|
||||
```lua
|
||||
-- 每个 output 自动分配 N 个 workspace
|
||||
workspace_distribution = {
|
||||
{ output_id = 0, count = 5 }, -- DP-1: workspace 1-5
|
||||
{ output_id = 1, count = 5 }, -- DP-2: workspace 6-10
|
||||
}
|
||||
```
|
||||
|
||||
**配置方式 3:启动阶段脚本生成**
|
||||
```lua
|
||||
function configure_workspaces(outputs)
|
||||
local workspaces = {}
|
||||
local ws_id = 1
|
||||
|
||||
for _, output in ipairs(outputs) do
|
||||
for i = 1, 3 do -- 每个 output 3 个 workspace
|
||||
table.insert(workspaces, {
|
||||
id = ws_id,
|
||||
name = tostring(ws_id),
|
||||
output_id = output.id
|
||||
})
|
||||
ws_id = ws_id + 1
|
||||
end
|
||||
end
|
||||
|
||||
return workspaces
|
||||
end
|
||||
```
|
||||
|
||||
**可见性规则**:
|
||||
- 普通非 =sticky= 窗口是否进入可见集,先看它的 =workspace= 是否正被归属 output 选中
|
||||
- 多输出下,窗口最终是否出现在某个 output 上,还取决于 =frame_rect= 是否与该 output 的几何相交
|
||||
- =sticky= 窗口只放宽 workspace 可见性,不提供 output 复制语义;它同样按最终矩形与 output 的相交关系决定可见性
|
||||
- 切换 workspace 只改变归属 output 的 =current_workspace_id= ,不改变 workspace/output 的固定绑定
|
||||
|
||||
* 策略配置
|
||||
|
||||
前一版草案里有几处“建议作为可选策略”的描述,现在收敛为显式 =wm_policy_config_t= :
|
||||
|
||||
1. =focus_raises=
|
||||
焦点切换时是否隐式执行 raise。
|
||||
|
||||
2. =pointer_enter_focuses_window=
|
||||
收到 =WM_EVENT_POINTER_ENTER= 时是否切换焦点(focus-follows-mouse)。
|
||||
|
||||
3. =sticky_windows_participate_in_direction_focus=
|
||||
sticky 窗口是否参与方向焦点搜索。
|
||||
|
||||
4. =manage_sets_focus=
|
||||
新受管窗口是否默认夺取所属 workspace 的焦点。
|
||||
|
||||
5. =switch_workspace_restores_last_focus=
|
||||
切换工作区时是否恢复该 workspace 的最近焦点。
|
||||
|
||||
6. =minimize_clears_focus=
|
||||
最小化当前焦点窗口时是否清空或重选该 workspace 焦点。
|
||||
|
||||
* 边框约定
|
||||
|
||||
当前最小核心只保留窗口边框,不支持标题栏和其他窗口装饰。
|
||||
|
||||
1. =wm_window_t= 不存储 =border_width= 或 =border_color= ;边框样式不是窗口真状态。
|
||||
2. =wm_runtime_bootstrap_t.border_width= 提供全局统一边框宽度。
|
||||
3. =wm_runtime_bootstrap_t.border_palette= 提供全局边框颜色集(普通/焦点)。
|
||||
4. =float_rect= 和 =frame_rect= 都表示“包含边框后的外框矩形”。
|
||||
5. =runtime= 在提交 =WM_EFFECT_CONFIGURE_WINDOW= 时解析有效边框宽度:
|
||||
- =WM_GEOMETRY_FULLSCREEN= : 边框宽度为 0
|
||||
- 其余模式 : 使用全局边框宽度
|
||||
6. =runtime= 在提交 =WM_EFFECT_CONFIGURE_WINDOW= 时解析有效边框颜色:
|
||||
- 当前 workspace 的 focused window : =border_palette.focused_rgba=
|
||||
- 其余窗口 : =border_palette.normal_rgba=
|
||||
- 若有效边框宽度为 0,则 backend 可忽略颜色字段
|
||||
|
||||
* 交互态
|
||||
|
||||
move/resize 这类拖拽交互不属于 =wm_state_t= 真状态,但也不能继续散落在平台分支里。runtime 应维护一个单一交互会话:
|
||||
|
||||
1. 空闲
|
||||
2. 正在移动 floating 窗口
|
||||
3. 正在调整 floating 窗口大小
|
||||
|
||||
交互会话至少记录目标窗口、起始指针位置、起始窗口矩形和起始 output,供 =policy.route_event()= 生成连续的 move/resize 命令。
|
||||
|
||||
* 核心外服务接入约定
|
||||
|
||||
既然 =render/status/bar= 被明确放在核心外,就需要一条稳定的接入边界:
|
||||
|
||||
1. =WM_EFFECT_RENDER_OUTPUT= 不是平台副作用,而是 runtime 发给服务层的失效通知。
|
||||
2. =backend.apply_effect()= 只接收 map/unmap/configure/focus/restack 这类平台 effect。
|
||||
3. =runtime= 在处理完 plan 后,把 render 和 metadata 相关事件广播给已注册服务。
|
||||
4. 服务只读 =state/meta/descriptor= 快照,不直接改 =wm_state_t= 。
|
||||
|
||||
* 配置集成约定
|
||||
|
||||
=config= 仍然在核心外,但它的输出必须改成 core-native 数据,而不是旧的 =client_t/tag_t= 钩子:
|
||||
|
||||
1. 配置加载结果应产出 workspace 描述、policy 配置、layout 注册、规则表、
|
||||
键盘/鼠标绑定到 command 的映射和服务设置。
|
||||
以及全局统一边框宽度和边框颜色集。
|
||||
这里的输入绑定应直接收敛为 =wm_key_binding_table_t= /
|
||||
=wm_pointer_binding_table_t= 。
|
||||
2. 配置层不得直接持有 =client_t= 、=tag_t= 或直接修改 =wm_state_t= 。
|
||||
3. 配置重载的边界是“重建 bootstrap 和服务配置,再显式应用到 runtime”,而不是任意时刻从外部写内存。
|
||||
|
||||
补充约定:
|
||||
|
||||
1. 鼠标绑定在最小核心里只区分 =root= / =window= 两类目标。
|
||||
2. 状态栏、标签栏、任务栏等点击区域属于核心外服务,不进入 core 的 pointer binding 表。
|
||||
|
||||
* 对象关系
|
||||
|
||||
这套草案里的关系固定为:
|
||||
|
||||
1. =window -> workspace=
|
||||
每个窗口只属于一个工作区。
|
||||
|
||||
2. =workspace -> output=
|
||||
每个工作区固定归属某个输出,运行时不再改变。
|
||||
|
||||
3. =output -> current_workspace=
|
||||
每个输出同一时刻只显示一个当前工作区(从归属的工作区中选择)。
|
||||
|
||||
4. =window visible on output=
|
||||
这是派生结果,不是独立存储的真相。
|
||||
|
||||
5. =stack_order[]=
|
||||
这是全局 z-order 的唯一真相,从下到上排列。
|
||||
|
||||
6. =window metadata=
|
||||
窗口标题、类名等字符串信息直接存储在 =wm_window_t= 中。
|
||||
核心算法(layout/policy)不应依赖这些字段,仅用于匹配规则和服务层展示。
|
||||
|
||||
7. =workspace name=
|
||||
工作区名称直接存储在 =wm_workspace_t= 中,用于状态栏显示。
|
||||
核心算法不应依赖此字段。
|
||||
|
||||
8. =workspace layouts=
|
||||
每个 workspace 维护一个可用布局列表(=available_layouts=),在启动时根据配置分配,之后固定不变。
|
||||
workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个),运行时允许切换。
|
||||
使用 =CYCLE_LAYOUT= / =SET_LAYOUT= 命令切换当前活动布局。
|
||||
|
||||
9. =window rectangles=
|
||||
=frame_rect= 是窗口当前最终外框矩形;=float_rect= 是 floating 模式下记忆的外框矩形。
|
||||
这两个矩形都包含边框。
|
||||
|
||||
* 核心不变量
|
||||
|
||||
1. 每个窗口必须且只能属于一个工作区。
|
||||
2. 每个工作区必须且只能固定归属一个输出,运行时不再改变。
|
||||
3. 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)。
|
||||
4. 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导。
|
||||
5. =geometry_mode= 只负责几何模式,不负责 z-order;其中
|
||||
=WM_GEOMETRY_MINIMIZED= 统一覆盖 X11 =IconicState= 这类“最小化/图标化”状态。
|
||||
6. =stack_order[]= 只负责 z-order,不负责几何模式。
|
||||
7. =floating= 是二值状态:窗口要么由 layout 决定基础几何,要么由 =float_rect= 决定基础几何。
|
||||
8. =sticky => floating= ;=sticky= 只放宽 workspace 可见性,不改变窗口归属工作区。
|
||||
9. floating / sticky 窗口都可以跨多个输出显示,但归属工作区仍然只有一个。
|
||||
10. 多输出下 floating / sticky 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定。
|
||||
11. layout 的 =symbol= 字段用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M");=name= 应保持稳定、可读(如 "tile", "monocle")。
|
||||
12. 窗口标题、类名等元数据存储在 =wm_window_t= 中,与控制状态一同管理。
|
||||
13. 工作区名称存储在 =wm_workspace_t= 中,核心算法不应依赖此字段。
|
||||
14. 每个 workspace 的可用布局列表在启动时分配,之后固定不变;运行时只允许通过 =CYCLE_LAYOUT= 或 =SET_LAYOUT= 命令切换当前活动布局。
|
||||
15. =frame_rect= 和 =float_rect= 都表示包含边框后的外框矩形。
|
||||
16. =border_width= / =border_color= 都不是 =wm_window_t= 真状态;它们由 runtime
|
||||
从全局配置、几何模式和焦点关系推导。
|
||||
|
||||
* Generation 字段
|
||||
|
||||
=generation= 是版本号,不是业务关系字段。
|
||||
|
||||
当前草案只建议保留:
|
||||
|
||||
1. =wm_state_t.generation=
|
||||
每次成功提交状态变更后递增,用于缓存失效、增量重算和调试。
|
||||
|
||||
以后如果有必要,再给渲染缓存或状态服务增加更细粒度的版本号。
|
||||
|
||||
* 主执行流程
|
||||
|
||||
=runtime= 负责固定主循环顺序:
|
||||
|
||||
1. =backend.next_event()=
|
||||
2. runtime 先处理窗口元数据更新这类辅助事件,并直接更新 =wm_window_t= 中的元数据字段
|
||||
3. =policy.route_event()=
|
||||
4. =policy.apply_command()=
|
||||
5. 根据 =dirty_flags= 决定是否重新运行 =layout= 、解析最终外框矩形,并补全
|
||||
=configure= effect 的边框宽度和边框颜色
|
||||
6. 将平台 effect 发送给 =backend=
|
||||
7. 将 render 失效通知发送给服务层
|
||||
8. =backend.flush()=
|
||||
|
||||
若后端发现显示器配置变化,不进入 output 增量更新分支,而是让 =backend.next_event()=
|
||||
返回 =WM_BACKEND_NEXT_RESTART_REQUIRED= ,由外层退出并整机重启。
|
||||
|
||||
* 文件说明
|
||||
|
||||
1. =wm_types.h=
|
||||
通用标量类型、矩形、窗口几何模式和策略枚举。
|
||||
|
||||
2. =wm_state.h=
|
||||
全局状态容器;当前草案同时保留公开字段和统一访问 API,便于先把语义定清楚。
|
||||
|
||||
3. =wm_event.h=
|
||||
统一后的 runtime 输入事件;不承载 stop/restart 这类控制信号。
|
||||
像 =_NET_ACTIVE_WINDOW= 这类平台协议请求,也应在这一层归一化为独立的
|
||||
request event。
|
||||
客户端 property / hint 变化(如 =urgent= / =fixed_size= / =skip_taskbar=)也应在
|
||||
这一层归一化为辅助事件,而不是伪装成 configure request。
|
||||
|
||||
4. =wm_binding.h=
|
||||
=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 使用的输入绑定表和匹配规则。
|
||||
|
||||
5. =wm_command.h=
|
||||
强类型语义命令。
|
||||
命令语义补充见 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] ;
|
||||
=sticky= 多显示器与跨屏行为补充见 [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
|
||||
|
||||
6. =wm_plan.h=
|
||||
脏标记和后端副作用。
|
||||
|
||||
7. =wm_layout.h=
|
||||
布局注册表、布局输入和布局输出。
|
||||
|
||||
8. =wm_backend.h=
|
||||
后端子系统接口。
|
||||
|
||||
9. =wm_policy_config.h=
|
||||
显式策略开关,替代散落在文档正文里的"可选策略"。
|
||||
|
||||
10. =wm_policy.h=
|
||||
事件路由和命令应用接口。
|
||||
|
||||
11. =wm_runtime.h=
|
||||
运行时上下文和生命周期接口。
|
||||
|
||||
12. =wm_service.h=
|
||||
核心外服务的注册和事件订阅接口。
|
||||
|
||||
13. [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]]
|
||||
关键命令的前置条件、状态转移、可见性和副作用规则。
|
||||
|
||||
14. [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]]
|
||||
=sticky= 窗口在 workspace、taskbar、多显示器和跨 output 拖动下的补充规则。
|
||||
|
||||
15. [[file:WM_POLICY_APPLY_COMMAND_SKELETON.org][WM_POLICY_APPLY_COMMAND_SKELETON.org]]
|
||||
=wm_policy_apply_command()= 的伪代码骨架和实现顺序建议。
|
||||
|
||||
16. [[file:WORKSPACE_CONFIG_EXAMPLES.org][WORKSPACE_CONFIG_EXAMPLES.org]]
|
||||
Workspace-Output 配置示例和最佳实践。
|
||||
|
||||
* 下一步实施顺序
|
||||
|
||||
当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下:
|
||||
|
||||
1. 在 =src/core/= 下建立最小骨架,至少放 =wm_types/binding/state/plan/layout/policy/policy_config/runtime/service= 的头文件和空实现,先不要替换现有 =src/= 逻辑。
|
||||
|
||||
2. 优先实现 =state + query + plan= 这一层,把动态数组管理、=find_*= 查询函数和 =stack_order[]= 操作补齐。
|
||||
|
||||
3. 再实现 =wm_policy_apply_command()= 的一小批核心命令:
|
||||
=MANAGE_WINDOW=
|
||||
=UNMANAGE_WINDOW=
|
||||
=SWITCH_WORKSPACE=
|
||||
=SET_LAYOUT=
|
||||
=CYCLE_LAYOUT=
|
||||
=TOGGLE_FLOATING=
|
||||
=SET_MAXIMIZED=
|
||||
=MOVE_FLOATING_WINDOW=
|
||||
=BEGIN_MOVE_FLOATING_INTERACTION=
|
||||
=BEGIN_RESIZE_FLOATING_INTERACTION=
|
||||
|
||||
4. 给上述命令建立一个无 X11 依赖的测试入口,直接构造 =wm_command_t= ,检查 =wm_state_t= 和 =wm_plan_t= 的变化是否符合文档规则。
|
||||
|
||||
5. 等 =event -> command -> state -> plan/effect= 这条链路跑通后,再编写一层很薄的 adapter,把现有事件翻译为 =wm_event_t= ,并把 effect 翻译回当前 X11 操作。
|
||||
其中 fullscreen / maximize / minimize 这类平台请求,应先归一化为
|
||||
=WM_EVENT_WINDOW_STATE_REQUEST= ,再由路由层翻译成 =SET_*= 命令。
|
||||
|
||||
6. =status/render/bar= 暂时不要先动。它们属于核心外服务,应在最小核心链路稳定后再迁移。
|
||||
@@ -1,506 +0,0 @@
|
||||
* ZDWM 最小核心命令规则
|
||||
|
||||
这份文档定义 =wm_policy_apply_command()= 应遵守的最小语义。目标不是覆盖所有未来扩展,而是先把最关键的状态转移规则固定下来,避免后续实现时在 =floating=、=sticky=、=geometry_mode= 和 =stack_order[]= 上出现分叉语义。
|
||||
|
||||
=sticky= 在多显示器、taskbar 和跨 output 拖动下的补充语义见
|
||||
[[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
|
||||
|
||||
* 通用规则
|
||||
|
||||
1. 找不到目标 =window_id=、=workspace_id= 或 =output_id= 时,命令失败,不修改状态。
|
||||
2. 命令失败时,不递增 =state.generation= 。
|
||||
3. 命令成功且造成状态变化时,递增 =state.generation= ,并设置相应 =dirty_flags= 。
|
||||
4. =stack_order[]= 是全局 z-order 真相,=raise/lower/focus= 只能通过修改这个数组影响堆叠。
|
||||
5. =geometry_mode= 只表示几何显示模式,不表示窗口是否在最上层。
|
||||
6. =sticky => floating= 。
|
||||
7. =sticky= 只放宽 workspace 可见性,不改变 =workspace_id= 。
|
||||
8. =floating= 决定基础几何来源:
|
||||
=floating = false= 时,基础几何来自 layout 结果。
|
||||
=floating = true= 时,基础几何来自 =float_rect= 。
|
||||
9. output/workspace/layout registry 以及每个 workspace 的 =available_layouts=
|
||||
都是 bootstrap 后固定集合;运行时不允许增删。
|
||||
10. 文档里出现的可选行为,应优先固化到 =wm_policy_config_t= ,而不是继续留在 prose 里。
|
||||
11. =frame_rect= 和 =float_rect= 都表示包含边框后的外框矩形。
|
||||
12. =border_width= / =border_color= 都不是 =wm_window_t= 真状态;runtime 在提交
|
||||
=WM_EFFECT_CONFIGURE_WINDOW= 时按全局配置和焦点关系解析它们。
|
||||
|
||||
* 管理类命令
|
||||
|
||||
** =WM_COMMAND_MANAGE_WINDOW=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口尚未出现在 =state.windows[]= 中。
|
||||
2. 目标工作区存在。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 向 =state.windows[]= 添加一个新窗口。
|
||||
2. 设定其 =workspace_id= 。
|
||||
3. 初始 =floating= 、=sticky= 、=urgent= 、=geometry_mode= 由 =command.as.manage_window.initial_state= 显式给出。
|
||||
4. 若 =initial_state.sticky == true= ,则结果状态中必须满足 =floating == true= 。
|
||||
5. 若 =has_initial_float_rect == true= ,将其复制到 =float_rect= 。
|
||||
6. 将窗口追加到 =stack_order[]= 顶部。
|
||||
7. 是否将该窗口设为所属 workspace 的 =focused_window_id= ,由 =initial_state.set_focus= 或 =policy.manage_sets_focus= 决定。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_STATE=
|
||||
2. =WM_DIRTY_LAYOUT=
|
||||
3. =WM_DIRTY_STACK=
|
||||
4. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_UNMANAGE_WINDOW=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口存在。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 从 =state.windows[]= 中删除该窗口。
|
||||
2. 从 =stack_order[]= 中移除该窗口。
|
||||
3. 如果它是某个 workspace 的 =focused_window_id= ,清空或重新选择新的 focused window。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_STATE=
|
||||
2. =WM_DIRTY_LAYOUT=
|
||||
3. =WM_DIRTY_STACK=
|
||||
4. =WM_DIRTY_RENDER=
|
||||
|
||||
* 焦点和堆叠命令
|
||||
|
||||
** =WM_COMMAND_FOCUS_WINDOW=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 将目标窗口所属 workspace 的 =focused_window_id= 设置为该窗口。
|
||||
2. 不强制改变 =stack_order[]= 。
|
||||
|
||||
可选策略:
|
||||
|
||||
1. 若 =policy.focus_raises == true= ,则在实现中额外执行 =raise_window= 逻辑。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_DECORATION=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_FOCUS_DIRECTION=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 只在目标 =output_id= 当前显示的 workspace 内选择焦点。
|
||||
2. 候选窗口应排除 =geometry_mode == WM_GEOMETRY_MINIMIZED= 的窗口。
|
||||
3. =sticky= 窗口是否参与方向焦点切换,由 =policy.sticky_windows_participate_in_direction_focus= 决定。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_DECORATION=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SET_WINDOW_URGENT=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 将目标窗口的 =urgent= 设为命令给定值。
|
||||
2. 不修改 =focused_window_id= 。
|
||||
3. 不修改 =stack_order[]= 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_RAISE_WINDOW=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 在 =stack_order[]= 中找到目标窗口。
|
||||
2. 将它移动到数组末尾,也就是最顶层。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_STACK=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_LOWER_WINDOW=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 在 =stack_order[]= 中找到目标窗口。
|
||||
2. 将它移动到数组开头,也就是最底层。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_STACK=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
* 工作区命令
|
||||
|
||||
** =WM_COMMAND_SWITCH_WORKSPACE=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标 output 存在。
|
||||
2. 目标 workspace 存在。
|
||||
3. 目标 workspace 的 =output_id= 必须等于目标 =output_id= 。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 将目标输出的 =current_workspace_id= 改为指定工作区。
|
||||
2. 不修改任何窗口的 =workspace_id= 。
|
||||
3. 不修改 =stack_order[]= 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_OUTPUT=
|
||||
2. =WM_DIRTY_LAYOUT=
|
||||
3. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SEND_WINDOW_TO_WORKSPACE=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 修改目标窗口的 =workspace_id= 。
|
||||
2. 不改变 =sticky= 。
|
||||
3. 不改变 =floating= 。
|
||||
4. 不改变 =float_rect= 。
|
||||
5. 如果窗口是原 workspace 的 =focused_window_id= ,需要重新选择原 workspace 焦点。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_STATE=
|
||||
2. =WM_DIRTY_LAYOUT=
|
||||
3. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SEND_WINDOW_TO_OUTPUT=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口存在。
|
||||
2. 目标 output 存在。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 这是工作区迁移命令的语法糖。
|
||||
2. 取目标 output 当前的 =current_workspace_id= 。
|
||||
3. 若 =policy == WM_CROSS_OUTPUT_KEEP_WORKSPACE= ,则该命令本身不修改 core 真状态。
|
||||
仅想保留 =workspace_id= 并跨 output 调整几何时,应使用 =WM_COMMAND_MOVE_FLOATING_WINDOW= 。
|
||||
4. 若 =policy == WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE= ,则把窗口 =workspace_id= 改成目标 output 当前工作区。
|
||||
|
||||
副作用:
|
||||
|
||||
1. 若最终没有修改 =workspace_id= ,则不产生状态副作用。
|
||||
2. 若最终修改了 =workspace_id= ,则副作用与 =WM_COMMAND_SEND_WINDOW_TO_WORKSPACE= 一致。
|
||||
|
||||
* 布局命令
|
||||
|
||||
** =WM_COMMAND_SET_LAYOUT=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标 workspace 存在。
|
||||
2. 目标 =layout_id= 已在 layout registry 中注册。
|
||||
3. 目标 =layout_id= 必须出现在该 workspace 的 =available_layouts[]= 中。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 设置目标 workspace 的 =layout_id= 。
|
||||
2. 不改变窗口的 =workspace_id= 。
|
||||
3. 不改变 =stack_order[]= 。
|
||||
4. 不影响 floating 窗口的基础几何。
|
||||
5. 影响所有 =floating == false= 且 =geometry_mode == WM_GEOMETRY_NORMAL= 的窗口。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_CYCLE_LAYOUT=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标 workspace 存在。
|
||||
2. Workspace 的可用布局列表非空(=layout_count > 0=)。
|
||||
3. 当前 =layout_id= 必须存在于 =available_layouts[]= 中。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若 =direction == 1= :切换到下一个布局(循环)。
|
||||
2. 若 =direction == -1= :切换到上一个布局(循环)。
|
||||
3. 在 workspace 的 =available_layouts[]= 数组中找到当前 =layout_id= 的索引。
|
||||
4. 按方向移动到相邻索引,循环回到数组开头或末尾。
|
||||
5. 将新的布局 ID 设置为 workspace 的 =layout_id= 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
* 模式切换命令
|
||||
|
||||
** =WM_COMMAND_TOGGLE_FLOATING=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口存在。
|
||||
2. 若目标窗口当前 =sticky == true= ,命令失败。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若原来 =floating == false= :
|
||||
将 =floating= 设为 =true= 。
|
||||
将当前 =frame_rect= 复制到 =float_rect= ,作为浮动基准外框矩形。
|
||||
2. 若原来 =floating == true= :
|
||||
将 =floating= 设为 =false= 。
|
||||
保留 =float_rect= ,供未来再次进入 floating 时恢复。
|
||||
3. 不改变 =geometry_mode= 。
|
||||
4. 若窗口处于 =WM_GEOMETRY_MINIMIZED= ,允许切换 =floating= ,但不会立刻显示出来。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_TOGGLE_STICKY=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若原来 =sticky == false= :
|
||||
若当前 =floating == false= ,先将当前 =frame_rect= 复制到 =float_rect= ,再设 =floating = true= 。
|
||||
然后将 =sticky= 设为 =true= 。
|
||||
2. 若原来 =sticky == true= :
|
||||
将 =sticky= 设为 =false= 。
|
||||
3. 不改变 =workspace_id= 。
|
||||
4. 关闭 =sticky= 时不自动恢复平铺;=floating= 保持原值不变。
|
||||
5. 不改变 =geometry_mode= 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SET_MAXIMIZED=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MAXIMIZED= :
|
||||
将 =geometry_mode= 设为 =WM_GEOMETRY_MAXIMIZED= 。
|
||||
这会覆盖此前的 =FULLSCREEN= / =MINIMIZED= 状态。
|
||||
2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MAXIMIZED= :
|
||||
设回 =WM_GEOMETRY_NORMAL= 。
|
||||
3. 其余情况不产生状态变化。
|
||||
4. 不改变 =floating= 。
|
||||
5. 若窗口原本是 floating,退出 maximize 后回到 =float_rect= 。
|
||||
6. 若窗口原本不是 floating,退出 maximize 后重新参与 layout。
|
||||
7. maximize 不改变全局边框策略;有效边框宽度仍由 runtime 统一解析。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SET_FULLSCREEN=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_FULLSCREEN= :
|
||||
将 =geometry_mode= 设为 =WM_GEOMETRY_FULLSCREEN= 。
|
||||
这会覆盖此前的 =MAXIMIZED= / =MINIMIZED= 状态。
|
||||
2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_FULLSCREEN= :
|
||||
设回 =WM_GEOMETRY_NORMAL= 。
|
||||
3. 其余情况不产生状态变化。
|
||||
4. 不改变 =floating= 。
|
||||
5. fullscreen 只决定几何占用 =output.geometry= ,不自动改变 =stack_order[]= 。
|
||||
6. fullscreen 的有效边框宽度由 runtime 解析为 0 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_SET_MINIMIZED=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MINIMIZED= :
|
||||
将 =geometry_mode= 设为 =WM_GEOMETRY_MINIMIZED= 。
|
||||
在 X11 后端上,这同时覆盖 ICCCM =IconicState= / =WM_CHANGE_STATE(Iconic)=
|
||||
语义;core 不单独建模 =iconic= 。
|
||||
2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MINIMIZED= :
|
||||
设回 =WM_GEOMETRY_NORMAL= 。
|
||||
3. 其余情况不产生状态变化。
|
||||
4. 不改变 =floating= 。
|
||||
5. 不改变 =sticky= 。
|
||||
6. minimized 后窗口不再参与显示和焦点候选。
|
||||
7. 若本次将目标窗口切入 minimized,且它正是当前焦点,是否清空或重选焦点由 =policy.minimize_clears_focus= 决定。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
* 浮动几何命令
|
||||
|
||||
** =WM_COMMAND_MOVE_FLOATING_WINDOW=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口存在。
|
||||
2. 目标窗口应为 =floating == true= 。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 按 =dx/dy= 修改 =float_rect.x/y= 。
|
||||
2. 若 =commit_workspace_change == false= ,只修改几何,不修改 =workspace_id= 。
|
||||
3. 若 =commit_workspace_change == true= :
|
||||
依据 =anchor= 找到目标输出。
|
||||
再按 =policy= 决定是否修改 =workspace_id= 。
|
||||
该规则对普通 =floating= 窗口和 =sticky= 窗口一致适用。
|
||||
4. 不改变 =geometry_mode= 。
|
||||
5. =float_rect= 是外框矩形,因此移动的是包含边框后的最终窗口外框。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_RESIZE_FLOATING_WINDOW=
|
||||
|
||||
前置条件:
|
||||
|
||||
1. 目标窗口存在。
|
||||
2. 目标窗口应为 =floating == true= 。
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 按 =dw/dh= 修改 =float_rect.width/height= 。
|
||||
2. 应对宽高做最小值约束,避免得到零或负值。
|
||||
3. 不改变 =workspace_id= 。
|
||||
4. 不改变 =geometry_mode= 。
|
||||
5. =float_rect= 是外框矩形,因此缩放的是包含边框后的最终窗口外框。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_LAYOUT=
|
||||
2. =WM_DIRTY_RENDER=
|
||||
|
||||
* 边框约定
|
||||
|
||||
1. 当前最小核心只支持全局统一边框宽度和全局边框颜色集,不支持每窗口独立配置。
|
||||
2. 全局边框宽度由 =wm_runtime_bootstrap_t.border_width= 提供。
|
||||
3. 全局边框颜色由 =wm_runtime_bootstrap_t.border_palette= 提供。
|
||||
4. =layout= 和 =floating= 都产出包含边框后的外框矩形。
|
||||
5. =WM_EFFECT_CONFIGURE_WINDOW.rect= 也是外框矩形。
|
||||
6. runtime 在把最终矩形转成 =configure= effect 时解析有效边框宽度:
|
||||
=WM_GEOMETRY_FULLSCREEN= -> 0
|
||||
其余模式 -> 全局边框宽度
|
||||
7. runtime 在把最终矩形转成 =configure= effect 时解析有效边框颜色:
|
||||
focused window -> =border_palette.focused_rgba=
|
||||
其余窗口 -> =border_palette.normal_rgba=
|
||||
|
||||
* redraw 和 runtime 控制命令
|
||||
|
||||
** =WM_COMMAND_REDRAW=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 不修改 =state= 。
|
||||
|
||||
副作用:
|
||||
|
||||
1. =WM_DIRTY_RENDER=
|
||||
|
||||
** =WM_COMMAND_QUIT=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 不再接收新事件,交由 =runtime= 退出主循环。
|
||||
|
||||
副作用:
|
||||
|
||||
1. 不产生 =wm_plan_t= 平台 effect。
|
||||
|
||||
** =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 不修改 =wm_state_t= 。
|
||||
2. 由 =runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为
|
||||
=WM_INTERACTION_MOVE_FLOATING= 。
|
||||
3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口
|
||||
满足 =floating == true= 时,交互才真正开始。
|
||||
|
||||
副作用:
|
||||
|
||||
1. 不产生 =wm_plan_t= 平台 effect。
|
||||
|
||||
** =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION=
|
||||
|
||||
状态变化:
|
||||
|
||||
1. 不修改 =wm_state_t= 。
|
||||
2. 由 =runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为
|
||||
=WM_INTERACTION_RESIZE_FLOATING= 。
|
||||
3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口
|
||||
满足 =floating == true= 时,交互才真正开始。
|
||||
|
||||
副作用:
|
||||
|
||||
1. 不产生 =wm_plan_t= 平台 effect。
|
||||
|
||||
补充约定:
|
||||
|
||||
1. =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 属于 runtime control commands。
|
||||
2. 它们应在 =wm_policy_apply_command()= 外层被 =runtime= 拦截,而不是走状态写入分支。
|
||||
|
||||
* 可见性推导建议
|
||||
|
||||
建议实现顺序如下:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
bool wm_state_window_should_be_visible(const wm_state_t *state,
|
||||
const wm_window_t *window) {
|
||||
if (window->geometry_mode == WM_GEOMETRY_MINIMIZED) return false;
|
||||
if (window->sticky) return true;
|
||||
return wm_state_workspace_is_visible(state, window->workspace_id);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
然后再按输出判断:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
bool wm_state_window_should_be_visible_on_output(const wm_state_t *state,
|
||||
const wm_window_t *window,
|
||||
const wm_output_t *output) {
|
||||
if (!wm_state_window_should_be_visible(state, window)) return false;
|
||||
return wm_rect_intersects(window->frame_rect, output->geometry);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
若后续存在按 output 渲染的 taskbar ,建议在下列条件同时满足时显示窗口条目:
|
||||
|
||||
1. =window->skip_taskbar == false=
|
||||
2. =wm_state_window_should_be_visible_on_output(...) == true=
|
||||
|
||||
* 几何解析优先级
|
||||
|
||||
窗口最终矩形建议按下面的优先级解析:
|
||||
|
||||
这里的“最终矩形”统一指包含边框后的外框矩形。
|
||||
|
||||
1. =WM_GEOMETRY_MINIMIZED=
|
||||
不显示。
|
||||
|
||||
2. =WM_GEOMETRY_FULLSCREEN=
|
||||
使用 =output.geometry= 。
|
||||
|
||||
3. =WM_GEOMETRY_MAXIMIZED=
|
||||
使用 =output.workarea= 。
|
||||
|
||||
4. =floating == true=
|
||||
使用 =float_rect= 。
|
||||
|
||||
5. 其余情况
|
||||
使用 layout 输出。
|
||||
@@ -1,953 +0,0 @@
|
||||
* ZDWM 最小核心事件路由设计
|
||||
|
||||
这份文档定义 =wm_event_t= 到 =wm_command_t= 的完整路由规则,确保事件处理链路的每个环节都有明确的行为定义。
|
||||
|
||||
* 事件路由总体流程
|
||||
|
||||
#+BEGIN_SRC c
|
||||
void wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event) {
|
||||
bool consumed = false;
|
||||
bool quit_requested = false;
|
||||
|
||||
wm_command_buffer_reset(&runtime->command_buffer);
|
||||
|
||||
// 1. 辅助事件特殊处理(不经过命令系统)
|
||||
if (is_auxiliary_event(event)) {
|
||||
runtime_handle_auxiliary_event(runtime, event);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 交互态下的特殊路由
|
||||
if (runtime->interaction.mode != WM_INTERACTION_NONE) {
|
||||
consumed = route_interaction_event(runtime, event, &runtime->command_buffer);
|
||||
}
|
||||
|
||||
// 3. 常规事件路由
|
||||
if (!consumed) {
|
||||
wm_policy_route_event(
|
||||
&runtime->state,
|
||||
&runtime->policy,
|
||||
&runtime->keybindings,
|
||||
&runtime->pointer_bindings,
|
||||
event,
|
||||
&runtime->command_buffer
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 应用命令
|
||||
for (size_t i = 0; i < runtime->command_buffer.count; i++) {
|
||||
const wm_command_t *command = &runtime->command_buffer.items[i];
|
||||
|
||||
if (runtime_handle_control_command(runtime, event, command,
|
||||
&quit_requested)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wm_policy_apply_command(
|
||||
&runtime->state,
|
||||
&runtime->policy,
|
||||
&runtime->layouts,
|
||||
command,
|
||||
&runtime->plan
|
||||
);
|
||||
}
|
||||
|
||||
// 5. runtime 控制命令在命令应用之后统一处理
|
||||
if (quit_requested) {
|
||||
wm_runtime_stop(runtime);
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
这里的 runtime control command 至少包括:
|
||||
|
||||
1. =WM_COMMAND_QUIT=
|
||||
2. =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION=
|
||||
3. =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION=
|
||||
|
||||
=QUIT= 只修改 runtime 主循环状态;
|
||||
=BEGIN_*_INTERACTION= 只在当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 且目标窗口
|
||||
有效时更新 =runtime->interaction= ,不进入 =wm_policy_apply_command()= 。
|
||||
|
||||
* 辅助事件处理规则
|
||||
|
||||
辅助事件直接更新 runtime 或广播给服务层,不经过命令系统:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool is_auxiliary_event(const wm_event_t *event) {
|
||||
return event->type == WM_EVENT_WINDOW_METADATA_CHANGED ||
|
||||
event->type == WM_EVENT_WINDOW_HINTS_CHANGED;
|
||||
}
|
||||
|
||||
static void runtime_handle_auxiliary_event(wm_runtime_t *runtime,
|
||||
const wm_event_t *event) {
|
||||
switch (event->type) {
|
||||
case WM_EVENT_WINDOW_METADATA_CHANGED:
|
||||
runtime_handle_metadata_event(runtime, event);
|
||||
return;
|
||||
|
||||
case WM_EVENT_WINDOW_HINTS_CHANGED:
|
||||
runtime_handle_hints_event(runtime, event);
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
**=WM_EVENT_WINDOW_METADATA_CHANGED=**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
void runtime_handle_metadata_event(wm_runtime_t *runtime, const wm_event_t *event) {
|
||||
const wm_window_metadata_changed_event_t *e = &event->data.window_metadata_changed;
|
||||
|
||||
// 1. 查找窗口
|
||||
const wm_window_t *win = wm_state_window_get(&runtime->state, e->window);
|
||||
if (!win) {
|
||||
return; // 窗口已被销毁
|
||||
}
|
||||
|
||||
// 2. 通过 state 接口更新窗口元数据字段
|
||||
if (e->changed_fields & WM_WINDOW_META_CHANGED_TITLE) {
|
||||
wm_state_window_set_title(&runtime->state, e->window, e->title);
|
||||
}
|
||||
if (e->changed_fields & WM_WINDOW_META_CHANGED_APP_ID) {
|
||||
wm_state_window_set_app_id(&runtime->state, e->window, e->app_id);
|
||||
}
|
||||
if (e->changed_fields & WM_WINDOW_META_CHANGED_CLASS) {
|
||||
wm_state_window_set_class(&runtime->state, e->window, e->class_name);
|
||||
}
|
||||
if (e->changed_fields & WM_WINDOW_META_CHANGED_INSTANCE) {
|
||||
wm_state_window_set_instance(&runtime->state, e->window, e->instance_name);
|
||||
}
|
||||
|
||||
// 3. 标记 render 脏(状态栏需要更新)
|
||||
runtime->plan.dirty_flags |= WM_DIRTY_RENDER;
|
||||
|
||||
// 4. 通知订阅的服务(如状态栏组件)
|
||||
wm_service_event_t service_event = {
|
||||
.type = WM_SERVICE_EVENT_WINDOW_METADATA_CHANGED,
|
||||
.as.window_metadata_changed = {
|
||||
.window_id = e->window,
|
||||
.changed_fields = e->changed_fields,
|
||||
},
|
||||
};
|
||||
wm_service_registry_emit(&runtime->services, &service_event, &runtime->state);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
**=WM_EVENT_WINDOW_HINTS_CHANGED=**
|
||||
|
||||
客户端属性 / hint 更新事件,也不经过命令系统:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
void runtime_handle_hints_event(wm_runtime_t *runtime, const wm_event_t *event) {
|
||||
const wm_window_hints_changed_event_t *e = &event->data.window_hints_changed;
|
||||
const wm_window_t *win = wm_state_window_get(&runtime->state, e->window);
|
||||
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_URGENT) {
|
||||
wm_state_window_set_urgent(&runtime->state, e->window, e->urgent);
|
||||
}
|
||||
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_FIXED_SIZE) {
|
||||
wm_state_window_set_fixed_size(&runtime->state, e->window, e->fixed_size);
|
||||
runtime->plan.dirty_flags |= WM_DIRTY_LAYOUT;
|
||||
}
|
||||
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR) {
|
||||
wm_state_window_set_skip_taskbar(&runtime->state, e->window,
|
||||
e->skip_taskbar);
|
||||
}
|
||||
|
||||
runtime->plan.dirty_flags |= WM_DIRTY_RENDER;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
这里的边界是:
|
||||
|
||||
1. `window hints changed` 不是 control request,不生成 command。
|
||||
2. `fixed_size` 属于 capability hint,不等同于 `floating`;
|
||||
但当前最小实现允许 state setter 在 `fixed_size == true` 时派生出
|
||||
`floating == true` 这种约束。
|
||||
3. `skip_taskbar` 主要影响核心外 taskbar/pager 服务的可见性过滤。
|
||||
|
||||
* 输入绑定系统
|
||||
|
||||
**键盘绑定配置格式**:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// keybindings.conf
|
||||
keybindings = [
|
||||
{
|
||||
// 退出 WM
|
||||
keysym: "SUPER+SHIFT+q",
|
||||
command: {
|
||||
type: WM_COMMAND_QUIT
|
||||
}
|
||||
},
|
||||
{
|
||||
// 焦点切到前一个窗口
|
||||
keysym: "SUPER+j",
|
||||
command: {
|
||||
type: WM_COMMAND_FOCUS_DIRECTION,
|
||||
output_id: 0,
|
||||
direction: "prev"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 循环切换布局
|
||||
keysym: "SUPER+space",
|
||||
command: {
|
||||
type: WM_COMMAND_CYCLE_LAYOUT,
|
||||
workspace_id: 0,
|
||||
direction: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
// 切换到 workspace 0
|
||||
keysym: "SUPER+1",
|
||||
command: {
|
||||
type: WM_COMMAND_SWITCH_WORKSPACE,
|
||||
output_id: 0,
|
||||
workspace_id: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
#+END_SRC
|
||||
|
||||
**鼠标按键绑定配置格式**:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// pointer_bindings.conf
|
||||
pointer_bindings = [
|
||||
{
|
||||
// 点击窗口聚焦。window_id == INVALID 表示“使用当前点击的窗口”
|
||||
button: "BUTTON1",
|
||||
target: "window",
|
||||
command: {
|
||||
type: WM_COMMAND_FOCUS_WINDOW,
|
||||
window_id: WM_WINDOW_ID_INVALID
|
||||
}
|
||||
},
|
||||
{
|
||||
// SUPER + 左键拖动 floating 窗口
|
||||
button: "SUPER+BUTTON1",
|
||||
target: "window",
|
||||
command: {
|
||||
type: WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION,
|
||||
window_id: WM_WINDOW_ID_INVALID
|
||||
}
|
||||
},
|
||||
{
|
||||
// SUPER + 右键调整 floating 窗口大小
|
||||
button: "SUPER+BUTTON3",
|
||||
target: "window",
|
||||
command: {
|
||||
type: WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION,
|
||||
window_id: WM_WINDOW_ID_INVALID
|
||||
}
|
||||
}
|
||||
]
|
||||
#+END_SRC
|
||||
|
||||
**输入绑定匹配优先级**:
|
||||
|
||||
1. 完全匹配(keysym + 所有 modifiers)
|
||||
2. 通配符匹配(keysym + 部分修饰符)
|
||||
|
||||
配置加载阶段应先把 =SUPER+m= 这类字符串解析成规范化按键组合。
|
||||
这里的 =keysym= 指规范化后的逻辑主键编码,不是平台原始扫描码:
|
||||
事件里的 =keycode= 只是保留 backend 的物理键位信息;
|
||||
常规绑定仍以 =keysym + modifiers= 为准。
|
||||
像 =SUPER+1= 这类行为也必须显式写成一条绑定,而不是由 core 内建生成。
|
||||
鼠标按键绑定只区分 =root= / =window= 两类命中目标,不覆盖 bar/status 点击区域。
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef enum wm_binding_match_mode_t {
|
||||
WM_BINDING_MATCH_EXACT,
|
||||
WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS,
|
||||
} wm_binding_match_mode_t;
|
||||
|
||||
typedef struct wm_key_binding_t {
|
||||
wm_keysym_t keysym;
|
||||
wm_modifier_mask_t modifiers;
|
||||
wm_binding_match_mode_t match_mode;
|
||||
wm_command_t command;
|
||||
} wm_key_binding_t;
|
||||
|
||||
typedef struct wm_key_binding_table_t {
|
||||
const wm_key_binding_t *items;
|
||||
size_t count;
|
||||
} wm_key_binding_table_t;
|
||||
|
||||
typedef enum wm_pointer_binding_target_t {
|
||||
WM_POINTER_BINDING_TARGET_ANY,
|
||||
WM_POINTER_BINDING_TARGET_ROOT,
|
||||
WM_POINTER_BINDING_TARGET_WINDOW,
|
||||
} wm_pointer_binding_target_t;
|
||||
|
||||
typedef struct wm_pointer_binding_t {
|
||||
wm_button_t button;
|
||||
wm_modifier_mask_t modifiers;
|
||||
wm_binding_match_mode_t match_mode;
|
||||
wm_pointer_binding_target_t target;
|
||||
wm_command_t command;
|
||||
} wm_pointer_binding_t;
|
||||
|
||||
typedef struct wm_pointer_binding_table_t {
|
||||
const wm_pointer_binding_t *items;
|
||||
size_t count;
|
||||
} wm_pointer_binding_table_t;
|
||||
#+END_SRC
|
||||
|
||||
绑定表属于 bootstrap 固定输入:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
typedef struct wm_runtime_bootstrap_t {
|
||||
...
|
||||
wm_key_binding_table_t keybindings;
|
||||
wm_pointer_binding_table_t pointer_bindings;
|
||||
...
|
||||
} wm_runtime_bootstrap_t;
|
||||
#+END_SRC
|
||||
|
||||
若 backend 需要平台侧被动抓键 / 抓按钮,则通过
|
||||
=wm_backend_api_t.set_keybindings()= / =set_pointer_bindings()= 接收同一组表。
|
||||
|
||||
**=wm_policy_route_event()= 实现**:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
bool wm_policy_route_event(const wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_key_binding_table_t *keybindings,
|
||||
const wm_pointer_binding_table_t *pointer_bindings,
|
||||
const wm_event_t *event,
|
||||
wm_command_buffer_t *out) {
|
||||
switch (event->type) {
|
||||
case WM_EVENT_KEY_PRESS:
|
||||
return route_key_press(state, keybindings,
|
||||
&event->data.key_press, out);
|
||||
|
||||
case WM_EVENT_POINTER_BUTTON_PRESS:
|
||||
return route_pointer_press(state, pointer_bindings,
|
||||
&event->data.pointer_button_press, out);
|
||||
|
||||
case WM_EVENT_POINTER_ENTER:
|
||||
return route_pointer_enter(state, policy,
|
||||
&event->data.pointer_enter, out);
|
||||
|
||||
case WM_EVENT_WINDOW_MAP_REQUEST:
|
||||
return route_map_request(state, &event->data.window_map_request, out);
|
||||
|
||||
case WM_EVENT_WINDOW_REMOVE:
|
||||
return route_window_remove(state, &event->data.window_remove, out);
|
||||
|
||||
case WM_EVENT_WINDOW_ACTIVATE_REQUEST:
|
||||
return route_activate_request(
|
||||
state,
|
||||
&event->data.window_activate_request,
|
||||
out
|
||||
);
|
||||
|
||||
case WM_EVENT_WINDOW_STATE_REQUEST:
|
||||
return route_window_state_request(
|
||||
state,
|
||||
&event->data.window_state_request,
|
||||
out
|
||||
);
|
||||
|
||||
case WM_EVENT_CONFIGURE_REQUEST:
|
||||
return route_configure_request(state, &event->data.configure_request, out);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件路由
|
||||
static bool route_key_press(const wm_state_t *state,
|
||||
const wm_key_binding_table_t *keybindings,
|
||||
const wm_key_press_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
// 1. 查找匹配的键盘绑定
|
||||
const wm_key_binding_t *binding = wm_key_binding_find(
|
||||
keybindings,
|
||||
e->keysym,
|
||||
e->modifiers
|
||||
);
|
||||
|
||||
if (!binding) {
|
||||
return false; // 没有匹配的绑定
|
||||
}
|
||||
|
||||
// 2. 直接复制绑定里的命令模板
|
||||
return wm_command_buffer_push(out, binding->command);
|
||||
}
|
||||
|
||||
static bool resolve_pointer_window_target(wm_command_t *cmd,
|
||||
wm_window_id_t event_window) {
|
||||
switch (cmd->type) {
|
||||
case WM_COMMAND_UNMANAGE_WINDOW:
|
||||
if (cmd->as.unmanage_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.unmanage_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.unmanage_window.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_FOCUS_WINDOW:
|
||||
if (cmd->as.focus_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.focus_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.focus_window.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_RAISE_WINDOW:
|
||||
if (cmd->as.raise_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.raise_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.raise_window.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_LOWER_WINDOW:
|
||||
if (cmd->as.lower_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.lower_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.lower_window.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_SEND_WINDOW_TO_WORKSPACE:
|
||||
if (cmd->as.send_window_to_workspace.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.send_window_to_workspace.window_id = event_window;
|
||||
}
|
||||
return cmd->as.send_window_to_workspace.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_SEND_WINDOW_TO_OUTPUT:
|
||||
if (cmd->as.send_window_to_output.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.send_window_to_output.window_id = event_window;
|
||||
}
|
||||
return cmd->as.send_window_to_output.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_TOGGLE_FLOATING:
|
||||
if (cmd->as.toggle_floating.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.toggle_floating.window_id = event_window;
|
||||
}
|
||||
return cmd->as.toggle_floating.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_TOGGLE_STICKY:
|
||||
if (cmd->as.toggle_sticky.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.toggle_sticky.window_id = event_window;
|
||||
}
|
||||
return cmd->as.toggle_sticky.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_SET_MAXIMIZED:
|
||||
if (cmd->as.set_maximized.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.set_maximized.window_id = event_window;
|
||||
}
|
||||
return cmd->as.set_maximized.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_SET_FULLSCREEN:
|
||||
if (cmd->as.set_fullscreen.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.set_fullscreen.window_id = event_window;
|
||||
}
|
||||
return cmd->as.set_fullscreen.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_SET_MINIMIZED:
|
||||
if (cmd->as.set_minimized.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.set_minimized.window_id = event_window;
|
||||
}
|
||||
return cmd->as.set_minimized.window_id != WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_MOVE_FLOATING_WINDOW:
|
||||
if (cmd->as.move_floating_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.move_floating_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.move_floating_window.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_RESIZE_FLOATING_WINDOW:
|
||||
if (cmd->as.resize_floating_window.window_id == WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.resize_floating_window.window_id = event_window;
|
||||
}
|
||||
return cmd->as.resize_floating_window.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION:
|
||||
if (cmd->as.begin_move_floating_interaction.window_id ==
|
||||
WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.begin_move_floating_interaction.window_id = event_window;
|
||||
}
|
||||
return cmd->as.begin_move_floating_interaction.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION:
|
||||
if (cmd->as.begin_resize_floating_interaction.window_id ==
|
||||
WM_WINDOW_ID_INVALID) {
|
||||
cmd->as.begin_resize_floating_interaction.window_id = event_window;
|
||||
}
|
||||
return cmd->as.begin_resize_floating_interaction.window_id !=
|
||||
WM_WINDOW_ID_INVALID;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool route_pointer_press(const wm_state_t *state,
|
||||
const wm_pointer_binding_table_t *pointer_bindings,
|
||||
const wm_pointer_button_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
const wm_pointer_binding_t *binding = wm_pointer_binding_find(
|
||||
pointer_bindings,
|
||||
e->button,
|
||||
e->modifiers,
|
||||
e->window != WM_WINDOW_ID_INVALID
|
||||
);
|
||||
|
||||
wm_command_t cmd;
|
||||
|
||||
(void)state;
|
||||
|
||||
if (!binding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cmd = binding->command;
|
||||
if (!resolve_pointer_window_target(&cmd, e->window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return wm_command_buffer_push(out, cmd);
|
||||
}
|
||||
|
||||
static bool route_pointer_enter(const wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_pointer_enter_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
const wm_window_t *win;
|
||||
const wm_workspace_t *ws;
|
||||
|
||||
if (!policy->pointer_enter_focuses_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e->window == WM_WINDOW_ID_INVALID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
win = wm_state_window_get(state, e->window);
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (win->workspace_id >= state->workspace_count) {
|
||||
return false;
|
||||
}
|
||||
ws = &state->workspaces[win->workspace_id];
|
||||
|
||||
if (ws->focused_window_id == win->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return wm_command_buffer_push(out, (wm_command_t){
|
||||
.type = WM_COMMAND_FOCUS_WINDOW,
|
||||
.as.focus_window = {
|
||||
.window_id = win->id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 窗口映射请求路由
|
||||
static bool route_map_request(const wm_state_t *state,
|
||||
const wm_window_map_request_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
// 1. 检查窗口是否已存在
|
||||
if (wm_state_window_get(state, e->info.id)) {
|
||||
return false; // 窗口已管理,忽略
|
||||
}
|
||||
|
||||
// 2. 生成 MANAGE_WINDOW 命令
|
||||
wm_command_t cmd = {
|
||||
.type = WM_COMMAND_MANAGE_WINDOW,
|
||||
.as.manage_window = {
|
||||
.window_id = e->info.id,
|
||||
// route 层结合 e->info / transient_for / is_dialog
|
||||
// 推导 initial_state、workspace_id 和初始 float_rect
|
||||
}
|
||||
};
|
||||
|
||||
return wm_command_buffer_push(out, cmd);
|
||||
}
|
||||
|
||||
static bool route_window_remove(const wm_state_t *state,
|
||||
const wm_window_remove_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
if (!wm_state_window_get(state, e->window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 当前最小核心不区分 withdrawn / destroy,统一转成 unmanage。
|
||||
(void)e->reason;
|
||||
|
||||
return wm_command_buffer_push(out, (wm_command_t){
|
||||
.type = WM_COMMAND_UNMANAGE_WINDOW,
|
||||
.as.unmanage_window = {
|
||||
.window_id = e->window,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static bool route_activate_request(const wm_state_t *state,
|
||||
const wm_window_activate_request_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
const wm_window_t *win = wm_state_window_get(state, e->window);
|
||||
const wm_workspace_t *ws;
|
||||
const wm_output_t *output;
|
||||
wm_command_t cmd;
|
||||
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e->source == WM_WINDOW_ACTIVATION_SOURCE_PAGER) {
|
||||
ws = &state->workspaces[win->workspace_id];
|
||||
output = &state->outputs[ws->output_id];
|
||||
|
||||
if (output->current_workspace_id != ws->id) {
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_SWITCH_WORKSPACE,
|
||||
.as.switch_workspace = {
|
||||
.output_id = output->id,
|
||||
.workspace_id = ws->id,
|
||||
},
|
||||
};
|
||||
if (!wm_command_buffer_push(out, cmd)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_FOCUS_WINDOW,
|
||||
.as.focus_window = {
|
||||
.window_id = win->id,
|
||||
},
|
||||
};
|
||||
return wm_command_buffer_push(out, cmd);
|
||||
}
|
||||
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_SET_WINDOW_URGENT,
|
||||
.as.set_window_urgent = {
|
||||
.window_id = win->id,
|
||||
.urgent = true,
|
||||
},
|
||||
};
|
||||
return wm_command_buffer_push(out, cmd);
|
||||
}
|
||||
|
||||
static bool route_window_state_request(const wm_state_t *state,
|
||||
const wm_window_state_request_event_t *e,
|
||||
wm_command_buffer_t *out) {
|
||||
const wm_window_t *win = wm_state_window_get(state, e->window);
|
||||
bool current_enabled;
|
||||
bool enabled;
|
||||
wm_command_t cmd;
|
||||
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (e->kind) {
|
||||
case WM_WINDOW_STATE_REQUEST_FULLSCREEN:
|
||||
current_enabled = win->geometry_mode == WM_GEOMETRY_FULLSCREEN;
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_MAXIMIZED:
|
||||
current_enabled = win->geometry_mode == WM_GEOMETRY_MAXIMIZED;
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_MINIMIZED:
|
||||
current_enabled = win->geometry_mode == WM_GEOMETRY_MINIMIZED;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (e->action) {
|
||||
case WM_WINDOW_STATE_REQUEST_ACTION_ADD:
|
||||
enabled = true;
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_ACTION_REMOVE:
|
||||
enabled = false;
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE:
|
||||
enabled = !current_enabled;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (e->kind) {
|
||||
case WM_WINDOW_STATE_REQUEST_FULLSCREEN:
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_SET_FULLSCREEN,
|
||||
.as.set_fullscreen = {
|
||||
.window_id = win->id,
|
||||
.enabled = enabled,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_MAXIMIZED:
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_SET_MAXIMIZED,
|
||||
.as.set_maximized = {
|
||||
.window_id = win->id,
|
||||
.enabled = enabled,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case WM_WINDOW_STATE_REQUEST_MINIMIZED:
|
||||
cmd = (wm_command_t){
|
||||
.type = WM_COMMAND_SET_MINIMIZED,
|
||||
.as.set_minimized = {
|
||||
.window_id = win->id,
|
||||
.enabled = enabled,
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return wm_command_buffer_push(out, cmd);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
这里的最小策略是:
|
||||
|
||||
1. `pager` 来源请求可以切到目标窗口所属 workspace,并把焦点交给该窗口。
|
||||
2. `application` / `legacy` 来源不直接抢焦点,而是转成 =SET_WINDOW_URGENT(true)= 。
|
||||
3. `WM_EVENT_WINDOW_STATE_REQUEST` 保留 add/remove/toggle 语义,不在 backend 侧提前求最终布尔值。
|
||||
4. route 层结合当前 =geometry_mode= ,把它翻译成 =SET_FULLSCREEN= / =SET_MAXIMIZED= / =SET_MINIMIZED= 。
|
||||
5. 这层抽象可以同时承接 X11 的 =_NET_WM_STATE= 与 Wayland 的窗口状态请求。
|
||||
6. 若以后需要更细的 focus-stealing prevention,可以再下沉到 =wm_policy_config_t= 。
|
||||
|
||||
* 交互态事件路由
|
||||
|
||||
交互态会覆盖正常的事件路由:
|
||||
|
||||
**=WM_INTERACTION_MOVE_FLOATING=**
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool route_interaction_event(wm_runtime_t *runtime,
|
||||
const wm_event_t *event,
|
||||
wm_command_buffer_t *out) {
|
||||
switch (runtime->interaction.mode) {
|
||||
case WM_INTERACTION_MOVE_FLOATING:
|
||||
return route_move_floating_event(runtime, event, out);
|
||||
|
||||
case WM_INTERACTION_RESIZE_FLOATING:
|
||||
return route_resize_floating_event(runtime, event, out);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool route_move_floating_event(wm_runtime_t *runtime,
|
||||
const wm_event_t *event,
|
||||
wm_command_buffer_t *out) {
|
||||
switch (event->type) {
|
||||
case WM_EVENT_POINTER_BUTTON_RELEASE:
|
||||
// 结束移动交互。这是 runtime 局部状态,不需要额外命令。
|
||||
runtime->interaction = (wm_interaction_state_t){0};
|
||||
return true;
|
||||
|
||||
case WM_EVENT_POINTER_MOTION:
|
||||
// 生成移动命令
|
||||
wm_command_t move_cmd = {
|
||||
.type = WM_COMMAND_MOVE_FLOATING_WINDOW,
|
||||
.as.move_floating_window = {
|
||||
.window_id = runtime->interaction.window_id,
|
||||
.dx = event->data.pointer_motion.root.x -
|
||||
runtime->interaction.pointer_origin.x,
|
||||
.dy = event->data.pointer_motion.root.y -
|
||||
runtime->interaction.pointer_origin.y
|
||||
}
|
||||
};
|
||||
return wm_command_buffer_push(out, move_cmd);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
* 命令生成规则
|
||||
|
||||
每种事件类型对应一个或多个命令的生成规则:
|
||||
|
||||
**输入事件 → 命令映射**
|
||||
|
||||
| 事件类型 | 可能生成的命令 | 条件 |
|
||||
|-------------------------+-----------------------------------------------------+----------------------------------------------------------|
|
||||
| KEY_PRESS | 任意命令(通过键盘绑定) | 绑定存在 |
|
||||
| POINTER_BUTTON_PRESS | 任意窗口命令 / BEGIN_*_INTERACTION(通过鼠标绑定) | 绑定存在 |
|
||||
| POINTER_ENTER | FOCUS_WINDOW | policy.pointer_enter_focuses_window == true 且窗口已管理 |
|
||||
| WINDOW_MAP_REQUEST | MANAGE_WINDOW | 窗口未管理 |
|
||||
| WINDOW_REMOVE | UNMANAGE_WINDOW | 窗口已管理 |
|
||||
| WINDOW_ACTIVATE_REQUEST | SWITCH_WORKSPACE + FOCUS_WINDOW / SET_WINDOW_URGENT | source == pager / source != pager |
|
||||
| WINDOW_STATE_REQUEST | SET_FULLSCREEN / SET_MAXIMIZED / SET_MINIMIZED | 窗口已管理 |
|
||||
| CONFIGURE_REQUEST | (可能不生成命令) | floating 窗口 |
|
||||
| POINTER_MOTION | MOVE/RESIZE 命令 | 交互态下 |
|
||||
|
||||
补充约定:
|
||||
|
||||
1. `WM_EVENT_CONFIGURE_REQUEST` 只携带稀疏几何字段,不直接镜像 X11 全量协议载荷。
|
||||
2. 最小核心当前只会把这些几何请求映射到 floating 窗口的 move/resize 语义。
|
||||
3. `WM_EVENT_POINTER_ENTER` 是“pointer focus 进入窗口”的输入事实,不经过 pointer binding 表。
|
||||
4. 类似 `raise_or_run` 这类程序化恢复鼠标位置导致的单次 crossing 抑制,应由 runtime 维护瞬时状态后在路由前过滤,而不是下沉到 backend 业务逻辑。
|
||||
5. `border_width` / `sibling` / `stack_mode` 这类协议细节留在 backend / adapter 层。
|
||||
6. fullscreen / maximize / minimize 这类窗口模式切换不属于 configure request。
|
||||
7. `WM_EVENT_WINDOW_STATE_REQUEST` 表达后端看到的原始 add/remove/toggle 请求;
|
||||
core command 仍保持 =SET_*= 这种最终状态语义。
|
||||
|
||||
* 事件处理的边界条件
|
||||
|
||||
**1. 辅助事件边界**
|
||||
|
||||
辅助事件满足以下条件时直接处理,不经过命令系统:
|
||||
- 事件类型是 `WM_EVENT_WINDOW_METADATA_CHANGED`
|
||||
- 事件类型是 `WM_EVENT_WINDOW_HINTS_CHANGED`
|
||||
|
||||
**2. 交互态边界**
|
||||
|
||||
交互态满足以下条件时,优先路由到交互处理器:
|
||||
- `runtime->interaction.mode != WM_INTERACTION_NONE`
|
||||
- 事件类型与交互相关(POINTER_MOTION, POINTER_BUTTON_RELEASE)
|
||||
|
||||
**3. 显示器变化边界**
|
||||
|
||||
显示器配置变化不进入 core 的增量更新逻辑:
|
||||
- backend 检测到显示器增删或几何变化时,使 `next_event()` 返回 `WM_BACKEND_NEXT_RESTART_REQUIRED`
|
||||
- runtime 退出并由外层重启,再重新 bootstrap 固定的 output/workspace/layout 集合
|
||||
|
||||
**4. 窗口管理边界**
|
||||
|
||||
窗口管理的边界条件:
|
||||
- `WM_EVENT_WINDOW_MAP_REQUEST`:窗口已存在则忽略
|
||||
- `WM_EVENT_WINDOW_REMOVE`:窗口不存在则忽略
|
||||
- `WM_EVENT_WINDOW_ACTIVATE_REQUEST`:窗口不存在则忽略
|
||||
- `WM_EVENT_WINDOW_STATE_REQUEST`:窗口不存在则忽略
|
||||
- `WM_EVENT_CONFIGURE_REQUEST`:只有 floating 窗口的几何请求才进入 core 命令链
|
||||
|
||||
* 事件处理顺序保证
|
||||
|
||||
为了保证确定性行为,事件处理必须按以下顺序进行:
|
||||
|
||||
1. **辅助事件优先**:元数据和状态服务更新不走命令系统
|
||||
2. **交互态优先**:拖拽操作优先于常规输入绑定
|
||||
3. **输入绑定匹配优先级**:完全匹配 → 通配符
|
||||
4. **命令执行顺序**:命令缓冲中的命令按顺序执行
|
||||
|
||||
* 主执行流程实现
|
||||
|
||||
#+BEGIN_SRC c
|
||||
void wm_runtime_run(wm_runtime_t *runtime) {
|
||||
wm_event_t event;
|
||||
|
||||
while (runtime->running) {
|
||||
// 1. 获取事件
|
||||
wm_backend_next_result_t poll_result =
|
||||
runtime->backend.api->next_event(&runtime->backend, &event);
|
||||
|
||||
if (poll_result == WM_BACKEND_NEXT_EVENT) {
|
||||
// 2. 处理事件
|
||||
wm_runtime_process_event(runtime, &event);
|
||||
} else if (poll_result == WM_BACKEND_NEXT_RESTART_REQUIRED) {
|
||||
wm_runtime_stop(runtime);
|
||||
break;
|
||||
} else if (poll_result == WM_BACKEND_NEXT_STOP ||
|
||||
poll_result == WM_BACKEND_NEXT_ERROR) {
|
||||
wm_runtime_stop(runtime);
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. 如果有副作用,应用它们
|
||||
if (runtime->plan.dirty_flags) {
|
||||
// 3.1 重新计算布局(如果需要)
|
||||
if (runtime->plan.dirty_flags & WM_DIRTY_LAYOUT) {
|
||||
wm_runtime_relayout(runtime);
|
||||
}
|
||||
|
||||
// 3.1.5 若布局或装饰发生变化,重建 configure effects。
|
||||
// 这里需要同时补全 rect / border_width / border_rgba。
|
||||
if (runtime->plan.dirty_flags &
|
||||
(WM_DIRTY_LAYOUT | WM_DIRTY_DECORATION)) {
|
||||
wm_runtime_rebuild_configure_effects(runtime);
|
||||
}
|
||||
|
||||
// 3.2 依次提交 effect。RENDER_OUTPUT 留给服务层处理。
|
||||
for (size_t i = 0; i < runtime->plan.effect_count; i++) {
|
||||
const wm_effect_t *effect = &runtime->plan.effects[i];
|
||||
|
||||
if (effect->type == WM_EFFECT_RENDER_OUTPUT) {
|
||||
wm_service_event_t service_event = {
|
||||
.type = WM_SERVICE_EVENT_RENDER_OUTPUT,
|
||||
.as.render_output = {
|
||||
.output_id = effect->as.render_output.output_id,
|
||||
},
|
||||
};
|
||||
wm_service_registry_emit(&runtime->services,
|
||||
&service_event,
|
||||
&runtime->state);
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime->backend.api->apply_effect(&runtime->backend, effect);
|
||||
}
|
||||
|
||||
// 3.3 刷新平台
|
||||
if (runtime->backend.api->flush) {
|
||||
runtime->backend.api->flush(&runtime->backend);
|
||||
}
|
||||
|
||||
// 3.4 重置 plan
|
||||
wm_plan_reset(&runtime->plan);
|
||||
}
|
||||
|
||||
// 4. 处理定时器(可选,降低 CPU 使用)
|
||||
usleep(1000); // 1ms
|
||||
}
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
* 与现有功能的兼容性
|
||||
|
||||
这个事件路由设计完全兼容现有 ZDWM 功能:
|
||||
|
||||
1. **输入绑定系统**:通过 keybindings.conf / pointer_bindings.conf 配置
|
||||
2. **规则系统**:在 MANAGE_WINDOW 命令中应用
|
||||
3. **布局切换**:通过 SET_LAYOUT / CYCLE_LAYOUT 命令
|
||||
4. **拖拽交互**:通过交互态管理
|
||||
5. **多显示器**:显示器配置变化时通过 `WM_BACKEND_NEXT_RESTART_REQUIRED` 触发重启
|
||||
|
||||
* 缺失的文档总结
|
||||
|
||||
通过补充本文档,mini_core_draft 的事件处理链路就完整了:
|
||||
|
||||
1. ✅ 事件定义(wm_event.h)
|
||||
2. ✅ 命令定义(wm_command.h + WM_COMMAND_RULES.org)
|
||||
3. ✅ 路由接口(wm_policy.h)
|
||||
4. ✅ 命令应用(WM_POLICY_APPLY_COMMAND_SKELETON.org)
|
||||
5. ✅ **路由规则**(本文档补充)
|
||||
6. ✅ **输入绑定系统**(本文档补充)
|
||||
7. ✅ **交互态处理**(本文档补充)
|
||||
8. ✅ **主执行流程**(本文档补充)
|
||||
@@ -1,742 +0,0 @@
|
||||
* ZDWM =wm_policy_apply_command()= 伪代码骨架
|
||||
|
||||
这份文档给出 =wm_policy_apply_command()= 的最小实现骨架。目标是把实际代码的控制流固定住,使其和 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 中定义的命令规则一一对应。
|
||||
|
||||
=sticky= 在多显示器、taskbar 和跨 output 拖动下的补充语义见
|
||||
[[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
|
||||
|
||||
* 函数职责
|
||||
|
||||
=wm_policy_apply_command()= 只做四件事:
|
||||
|
||||
1. 校验命令输入是否合法。
|
||||
2. 修改 =wm_state_t= 中的运行状态。
|
||||
3. 向 =wm_plan_t= 记录脏标记和后端副作用。
|
||||
4. 在状态真的发生变化时递增 =state.generation= 。
|
||||
|
||||
补充约定:
|
||||
|
||||
1. =border_width= / =border_color= 不是 =wm_window_t= 真状态。
|
||||
2. =apply_command()= 只负责更新 =frame_rect= / =float_rect= 和脏标记。
|
||||
3. runtime 在布局求值和 effect 提交阶段,根据全局边框配置补全
|
||||
=WM_EFFECT_CONFIGURE_WINDOW.border_width= /
|
||||
=WM_EFFECT_CONFIGURE_WINDOW.border_rgba= 。
|
||||
|
||||
它不负责:
|
||||
|
||||
1. 从平台读取事件。
|
||||
2. 直接调用 X11/Wayland API。
|
||||
3. 直接执行渲染。
|
||||
4. 直接格式化状态栏文本。
|
||||
|
||||
* 顶层控制流
|
||||
|
||||
建议骨架如下:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
bool wm_policy_apply_command(wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_layout_registry_t *layouts,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
bool changed = false;
|
||||
bool generation_relevant = false;
|
||||
|
||||
if (!state || !policy || !command || !plan) return false;
|
||||
|
||||
generation_relevant =
|
||||
command->type != WM_COMMAND_REDRAW &&
|
||||
command->type != WM_COMMAND_QUIT &&
|
||||
command->type != WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION &&
|
||||
command->type != WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION;
|
||||
|
||||
switch (command->type) {
|
||||
case WM_COMMAND_NONE:
|
||||
return false;
|
||||
|
||||
case WM_COMMAND_MANAGE_WINDOW:
|
||||
changed = apply_manage_window(state, policy, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_UNMANAGE_WINDOW:
|
||||
changed = apply_unmanage_window(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_FOCUS_WINDOW:
|
||||
changed = apply_focus_window(state, policy, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_FOCUS_DIRECTION:
|
||||
changed = apply_focus_direction(state, policy, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SET_WINDOW_URGENT:
|
||||
changed = apply_set_window_urgent(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_RAISE_WINDOW:
|
||||
changed = apply_raise_window(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_LOWER_WINDOW:
|
||||
changed = apply_lower_window(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SWITCH_WORKSPACE:
|
||||
changed = apply_switch_workspace(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SEND_WINDOW_TO_WORKSPACE:
|
||||
changed = apply_send_window_to_workspace(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SEND_WINDOW_TO_OUTPUT:
|
||||
changed = apply_send_window_to_output(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_TOGGLE_FLOATING:
|
||||
changed = apply_toggle_floating(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_TOGGLE_STICKY:
|
||||
changed = apply_toggle_sticky(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SET_MAXIMIZED:
|
||||
changed = apply_set_maximized(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SET_FULLSCREEN:
|
||||
changed = apply_set_fullscreen(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_SET_MINIMIZED:
|
||||
changed = apply_set_minimized(state, policy, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_MOVE_FLOATING_WINDOW:
|
||||
changed = apply_move_floating_window(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_RESIZE_FLOATING_WINDOW:
|
||||
changed = apply_resize_floating_window(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION:
|
||||
case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION:
|
||||
case WM_COMMAND_QUIT:
|
||||
return false;
|
||||
|
||||
case WM_COMMAND_SET_LAYOUT:
|
||||
changed = apply_set_layout(state, layouts, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_CYCLE_LAYOUT:
|
||||
changed = apply_cycle_layout(state, command, plan);
|
||||
break;
|
||||
|
||||
case WM_COMMAND_REDRAW:
|
||||
changed = apply_redraw(state, command, plan);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (changed && generation_relevant) state->generation++;
|
||||
return changed;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
* 辅助函数建议
|
||||
|
||||
建议先把这些低层辅助函数写出来,避免在每个命令分支中重复拼逻辑:
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static wm_window_t *require_window(wm_state_t *state, wm_window_id_t id);
|
||||
static wm_workspace_t *require_workspace(wm_state_t *state, wm_workspace_id_t id);
|
||||
static wm_output_t *require_output(wm_state_t *state, wm_output_id_t id);
|
||||
|
||||
static bool state_append_window(wm_state_t *state, wm_window_t window);
|
||||
static bool state_remove_window(wm_state_t *state, wm_window_id_t id);
|
||||
|
||||
static bool stack_raise(wm_state_t *state, wm_window_id_t id);
|
||||
static bool stack_lower(wm_state_t *state, wm_window_id_t id);
|
||||
static bool stack_remove(wm_state_t *state, wm_window_id_t id);
|
||||
static bool stack_append(wm_state_t *state, wm_window_id_t id);
|
||||
static bool workspace_has_layout(const wm_workspace_t *workspace,
|
||||
wm_layout_id_t id);
|
||||
|
||||
static void mark_layout_dirty(wm_plan_t *plan);
|
||||
static void mark_stack_dirty(wm_plan_t *plan);
|
||||
static void mark_render_dirty(wm_plan_t *plan);
|
||||
static void mark_output_dirty(wm_plan_t *plan);
|
||||
|
||||
static void push_render_all_outputs(const wm_state_t *state, wm_plan_t *plan);
|
||||
static void push_restack_effect(const wm_state_t *state, wm_plan_t *plan);
|
||||
static void maybe_refocus_workspace(wm_state_t *state, wm_workspace_t *workspace);
|
||||
static const wm_output_t *find_output_by_point(const wm_state_t *state,
|
||||
wm_point_t point);
|
||||
#+END_SRC
|
||||
|
||||
* 命令分支骨架
|
||||
|
||||
** =apply_manage_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_manage_window(wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_id_t id = command->as.manage_window.window_id;
|
||||
wm_workspace_id_t ws_id = command->as.manage_window.workspace_id;
|
||||
|
||||
if (wm_state_window_get(state, id)) return false;
|
||||
wm_workspace_t *ws = require_workspace(state, ws_id);
|
||||
if (!ws) return false;
|
||||
|
||||
wm_window_t win = {
|
||||
.id = id,
|
||||
.workspace_id = ws_id,
|
||||
.geometry_mode = command->as.manage_window.initial_state.geometry_mode,
|
||||
.floating = command->as.manage_window.initial_state.floating,
|
||||
.sticky = command->as.manage_window.initial_state.sticky,
|
||||
.urgent = command->as.manage_window.initial_state.urgent,
|
||||
};
|
||||
|
||||
if (win.sticky) {
|
||||
win.floating = true;
|
||||
}
|
||||
|
||||
if (command->as.manage_window.has_initial_float_rect) {
|
||||
win.float_rect = command->as.manage_window.initial_float_rect;
|
||||
}
|
||||
|
||||
if (!state_append_window(state, win)) return false;
|
||||
if (!stack_append(state, id)) return false;
|
||||
|
||||
if (command->as.manage_window.initial_state.set_focus ||
|
||||
policy->manage_sets_focus) {
|
||||
ws->focused_window_id = id;
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT |
|
||||
WM_DIRTY_STACK | WM_DIRTY_RENDER;
|
||||
push_restack_effect(state, plan);
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_unmanage_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_unmanage_window(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_id_t id = command->as.unmanage_window.window_id;
|
||||
wm_window_t *win = require_window(state, id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
|
||||
if (!ws) return false;
|
||||
|
||||
if (!stack_remove(state, id)) return false;
|
||||
if (!state_remove_window(state, id)) return false;
|
||||
|
||||
if (ws->focused_window_id == id) maybe_refocus_workspace(state, ws);
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT |
|
||||
WM_DIRTY_STACK | WM_DIRTY_RENDER;
|
||||
push_restack_effect(state, plan);
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_focus_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_focus_window(wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_id_t id = command->as.focus_window.window_id;
|
||||
wm_window_t *win = require_window(state, id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
|
||||
if (!ws) return false;
|
||||
if (ws->focused_window_id == id) return false;
|
||||
|
||||
ws->focused_window_id = id;
|
||||
|
||||
if (policy->focus_raises && stack_raise(state, id)) {
|
||||
plan->dirty_flags |= WM_DIRTY_STACK;
|
||||
push_restack_effect(state, plan);
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER;
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
|
||||
** =apply_toggle_sticky()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_toggle_sticky(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win = require_window(state, command->as.toggle_sticky.window_id);
|
||||
if (!win) return false;
|
||||
|
||||
if (!win->sticky) {
|
||||
if (!win->floating) {
|
||||
win->float_rect = win->frame_rect;
|
||||
win->floating = true;
|
||||
}
|
||||
win->sticky = true;
|
||||
} else {
|
||||
win->sticky = false;
|
||||
/* 关闭 sticky 时保持 floating 不变,不自动恢复平铺。 */
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
实际实现中,若关闭 =sticky= 后窗口对当前正在查看的 workspace 不再可见,
|
||||
且它曾是相关 workspace 的焦点窗口,则应重选该 workspace 的焦点。
|
||||
|
||||
** =apply_focus_direction()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_focus_direction(wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_output_id_t output_id = command->as.focus_direction.output_id;
|
||||
wm_output_t *out = require_output(state, output_id);
|
||||
if (!out) return false;
|
||||
|
||||
wm_workspace_t *ws = require_workspace(state, out->current_workspace_id);
|
||||
if (!ws) return false;
|
||||
|
||||
wm_window_id_t next =
|
||||
find_focus_candidate_in_workspace(state, ws,
|
||||
command->as.focus_direction.direction,
|
||||
policy->sticky_windows_participate_in_direction_focus);
|
||||
if (!next || ws->focused_window_id == next) return false;
|
||||
|
||||
ws->focused_window_id = next;
|
||||
plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER;
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_set_window_urgent()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_set_window_urgent(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.set_window_urgent.window_id);
|
||||
bool urgent = command->as.set_window_urgent.urgent;
|
||||
|
||||
if (!win) return false;
|
||||
if (win->urgent == urgent) return false;
|
||||
|
||||
win->urgent = urgent;
|
||||
plan->dirty_flags |= WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_raise_window()= 和 =apply_lower_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_raise_window(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
if (!stack_raise(state, command->as.raise_window.window_id)) return false;
|
||||
plan->dirty_flags |= WM_DIRTY_STACK | WM_DIRTY_RENDER;
|
||||
push_restack_effect(state, plan);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool apply_lower_window(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
if (!stack_lower(state, command->as.lower_window.window_id)) return false;
|
||||
plan->dirty_flags |= WM_DIRTY_STACK | WM_DIRTY_RENDER;
|
||||
push_restack_effect(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_switch_workspace()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_switch_workspace(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_output_t *out =
|
||||
require_output(state, command->as.switch_workspace.output_id);
|
||||
if (!out) return false;
|
||||
wm_workspace_t *ws =
|
||||
require_workspace(state, command->as.switch_workspace.workspace_id);
|
||||
if (!ws) return false;
|
||||
if (ws->output_id != out->id) return false;
|
||||
|
||||
if (out->current_workspace_id == ws->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out->current_workspace_id = ws->id;
|
||||
plan->dirty_flags |= WM_DIRTY_OUTPUT | WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_send_window_to_workspace()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_send_window_to_workspace(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.send_window_to_workspace.window_id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_workspace_id_t dst_id = command->as.send_window_to_workspace.workspace_id;
|
||||
if (!require_workspace(state, dst_id)) return false;
|
||||
if (win->workspace_id == dst_id) return false;
|
||||
|
||||
wm_workspace_t *src = require_workspace(state, win->workspace_id);
|
||||
win->workspace_id = dst_id;
|
||||
|
||||
if (src && src->focused_window_id == win->id) maybe_refocus_workspace(state, src);
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_send_window_to_output()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_send_window_to_output(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.send_window_to_output.window_id);
|
||||
wm_output_t *out =
|
||||
require_output(state, command->as.send_window_to_output.output_id);
|
||||
if (!win || !out) return false;
|
||||
|
||||
if (command->as.send_window_to_output.policy ==
|
||||
WM_CROSS_OUTPUT_KEEP_WORKSPACE) {
|
||||
/*
|
||||
* 保留 workspace 但跨 output 调整几何,不属于这个命令的职责;
|
||||
* 调用方应使用 MOVE_FLOATING_WINDOW 只改 float_rect。
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
wm_command_t forwarded = {
|
||||
.type = WM_COMMAND_SEND_WINDOW_TO_WORKSPACE,
|
||||
.as.send_window_to_workspace = {
|
||||
.window_id = win->id,
|
||||
.workspace_id = out->current_workspace_id,
|
||||
},
|
||||
};
|
||||
return apply_send_window_to_workspace(state, &forwarded, plan);
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_toggle_floating()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_toggle_floating(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win = require_window(state, command->as.toggle_floating.window_id);
|
||||
if (!win) return false;
|
||||
if (win->sticky) return false;
|
||||
|
||||
if (!win->floating) {
|
||||
win->floating = true;
|
||||
win->float_rect = win->frame_rect;
|
||||
} else {
|
||||
win->floating = false;
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_set_maximized()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_set_maximized(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win = require_window(state, command->as.set_maximized.window_id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_window_geometry_mode_t next_mode = win->geometry_mode;
|
||||
|
||||
if (command->as.set_maximized.enabled) {
|
||||
next_mode = WM_GEOMETRY_MAXIMIZED;
|
||||
} else if (win->geometry_mode == WM_GEOMETRY_MAXIMIZED) {
|
||||
next_mode = WM_GEOMETRY_NORMAL;
|
||||
}
|
||||
|
||||
if (next_mode == win->geometry_mode) return false;
|
||||
|
||||
win->geometry_mode = next_mode;
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
实际实现中应按 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 处理 =enabled=
|
||||
的幂等性,以及与 =FULLSCREEN= / =MINIMIZED= 的互斥切换关系。
|
||||
|
||||
** =apply_set_fullscreen()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_set_fullscreen(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.set_fullscreen.window_id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_window_geometry_mode_t next_mode = win->geometry_mode;
|
||||
|
||||
if (command->as.set_fullscreen.enabled) {
|
||||
next_mode = WM_GEOMETRY_FULLSCREEN;
|
||||
} else if (win->geometry_mode == WM_GEOMETRY_FULLSCREEN) {
|
||||
next_mode = WM_GEOMETRY_NORMAL;
|
||||
}
|
||||
|
||||
if (next_mode == win->geometry_mode) return false;
|
||||
|
||||
win->geometry_mode = next_mode;
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_set_minimized()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_set_minimized(wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.set_minimized.window_id);
|
||||
if (!win) return false;
|
||||
|
||||
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
|
||||
wm_window_geometry_mode_t next_mode = win->geometry_mode;
|
||||
|
||||
if (command->as.set_minimized.enabled) {
|
||||
next_mode = WM_GEOMETRY_MINIMIZED;
|
||||
} else if (win->geometry_mode == WM_GEOMETRY_MINIMIZED) {
|
||||
next_mode = WM_GEOMETRY_NORMAL;
|
||||
}
|
||||
|
||||
if (next_mode == win->geometry_mode) return false;
|
||||
|
||||
win->geometry_mode = next_mode;
|
||||
|
||||
if (command->as.set_minimized.enabled &&
|
||||
policy->minimize_clears_focus &&
|
||||
ws &&
|
||||
ws->focused_window_id == win->id) {
|
||||
maybe_refocus_workspace(state, ws);
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_move_floating_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_move_floating_window(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.move_floating_window.window_id);
|
||||
if (!win || !win->floating) return false;
|
||||
|
||||
wm_workspace_id_t old_ws_id = win->workspace_id;
|
||||
|
||||
win->float_rect.x += command->as.move_floating_window.dx;
|
||||
win->float_rect.y += command->as.move_floating_window.dy;
|
||||
|
||||
if (command->as.move_floating_window.commit_workspace_change) {
|
||||
const wm_output_t *out =
|
||||
find_output_by_point(state, command->as.move_floating_window.anchor);
|
||||
if (out &&
|
||||
command->as.move_floating_window.policy ==
|
||||
WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE) {
|
||||
win->workspace_id = out->current_workspace_id;
|
||||
if (win->workspace_id != old_ws_id) {
|
||||
wm_workspace_t *src = require_workspace(state, old_ws_id);
|
||||
if (src && src->focused_window_id == win->id) {
|
||||
maybe_refocus_workspace(state, src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_resize_floating_window()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_resize_floating_window(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_window_t *win =
|
||||
require_window(state, command->as.resize_floating_window.window_id);
|
||||
if (!win || !win->floating) return false;
|
||||
|
||||
win->float_rect.width =
|
||||
clamp_i32(win->float_rect.width + command->as.resize_floating_window.dw,
|
||||
WM_MIN_WINDOW_WIDTH, INT32_MAX);
|
||||
win->float_rect.height =
|
||||
clamp_i32(win->float_rect.height + command->as.resize_floating_window.dh,
|
||||
WM_MIN_WINDOW_HEIGHT, INT32_MAX);
|
||||
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_set_layout()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_set_layout(wm_state_t *state,
|
||||
const wm_layout_registry_t *layouts,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_workspace_t *ws =
|
||||
require_workspace(state, command->as.set_layout.workspace_id);
|
||||
if (!ws) return false;
|
||||
if (!wm_layout_slot_get(layouts, command->as.set_layout.layout_id)) {
|
||||
return false;
|
||||
}
|
||||
if (!workspace_has_layout(ws, command->as.set_layout.layout_id)) {
|
||||
return false;
|
||||
}
|
||||
if (ws->layout_id == command->as.set_layout.layout_id) return false;
|
||||
|
||||
ws->layout_id = command->as.set_layout.layout_id;
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_cycle_layout()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_cycle_layout(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
wm_workspace_t *ws =
|
||||
require_workspace(state, command->as.cycle_layout.workspace_id);
|
||||
if (!ws) return false;
|
||||
if (ws->layout_count == 0) return false;
|
||||
|
||||
// 找到当前布局在列表中的索引
|
||||
size_t current_idx = 0;
|
||||
bool found_current = false;
|
||||
for (size_t i = 0; i < ws->layout_count; i++) {
|
||||
if (ws->available_layouts[i] == ws->layout_id) {
|
||||
current_idx = i;
|
||||
found_current = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_current) return false;
|
||||
|
||||
// 计算下一个索引(支持前后循环)
|
||||
size_t next_idx;
|
||||
if (command->as.cycle_layout.direction > 0) {
|
||||
next_idx = (current_idx + 1) % ws->layout_count;
|
||||
} else {
|
||||
next_idx = (current_idx == 0) ? (ws->layout_count - 1) : (current_idx - 1);
|
||||
}
|
||||
|
||||
// 如果新旧布局相同,直接返回
|
||||
wm_layout_id_t next_layout = ws->available_layouts[next_idx];
|
||||
if (next_layout == ws->layout_id) return false;
|
||||
|
||||
ws->layout_id = next_layout;
|
||||
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** =apply_redraw()=
|
||||
|
||||
#+BEGIN_SRC c
|
||||
static bool apply_redraw(wm_state_t *state,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan) {
|
||||
(void)state;
|
||||
(void)command;
|
||||
plan->dirty_flags |= WM_DIRTY_RENDER;
|
||||
push_render_all_outputs(state, plan);
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
通常情况下 =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 都会由
|
||||
=runtime= 在命令应用外层直接拦截,因此 =wm_policy_apply_command()= 不应真正看见
|
||||
这些 runtime control commands。
|
||||
|
||||
* 推荐的后处理步骤
|
||||
|
||||
在 =apply_*()= 返回后,建议由上层统一做这些事,而不是在每个分支里直接跑布局:
|
||||
|
||||
1. 如果有 =WM_DIRTY_LAYOUT= ,则对每个 output 的当前 workspace 重新计算 layout。
|
||||
2. 若有 =WM_DIRTY_LAYOUT= 或 =WM_DIRTY_DECORATION= ,则根据窗口的
|
||||
=geometry_mode= 、=floating= 、焦点关系和全局边框配置解析最终矩形、
|
||||
有效边框宽度与边框颜色。
|
||||
=sticky= 窗口由于满足 =sticky => floating= ,因此同样走 =float_rect= 路径。
|
||||
3. 生成 =WM_EFFECT_CONFIGURE_WINDOW= 。
|
||||
4. 若有 =WM_DIRTY_STACK= ,生成 =WM_EFFECT_RESTACK_WINDOWS= 。
|
||||
5. 若有 =WM_DIRTY_RENDER= ,生成 =WM_EFFECT_RENDER_OUTPUT= 。
|
||||
6. =WM_EFFECT_RENDER_OUTPUT= 由 runtime 分发给服务层,而不是直接交给 backend 。
|
||||
|
||||
* 为什么要拆成多个 =apply_*()=
|
||||
|
||||
1. 便于单元测试,每个命令可以单独测试。
|
||||
2. 便于和 =WM_COMMAND_RULES.org= 对照审查。
|
||||
3. 便于以后替换部分策略,例如 =focus raises= 或 sticky 参与焦点候选的规则。
|
||||
4. 便于将复杂逻辑下沉到更小的辅助函数,而不是把 =wm_policy_apply_command()= 写成一个超长 switch。
|
||||
@@ -1,200 +0,0 @@
|
||||
* Sticky 窗口规则
|
||||
|
||||
这份文档补充 =sticky= 窗口在最小核心中的约束,重点覆盖以下问题:
|
||||
|
||||
1. =sticky= 与 =floating= 的关系
|
||||
2. =sticky= 窗口是否参与自动布局
|
||||
3. =sticky= 窗口在 taskbar 中的显示规则
|
||||
4. 多显示器下 =sticky= 窗口的可见性与跨屏移动语义
|
||||
|
||||
* 核心结论
|
||||
|
||||
1. =sticky => floating=
|
||||
2. =sticky= 窗口不参与任何自动布局
|
||||
3. =sticky= 只放宽 workspace 可见性,不改变窗口的唯一归属 =workspace_id=
|
||||
4. =sticky= 窗口不复制到每个 output;是否出现在某个 output 上,只由其最终矩形是否覆盖该 output 决定
|
||||
5. 拖动 =sticky= 或普通 =floating= 窗口跨 output 时,默认只修改 =float_rect= ,不隐式修改 =workspace_id=
|
||||
6. 只有显式提交跨 output 迁移时,才允许把窗口归属 workspace 改到目标 output 当前显示的 workspace
|
||||
|
||||
* 状态约束
|
||||
|
||||
** =sticky= 与 =floating=
|
||||
|
||||
最小核心中,=sticky= 不是独立于 =floating= 的另一套几何模式,而是附加在 floating 语义之上的可见性扩展。
|
||||
|
||||
因此规定:
|
||||
|
||||
1. 若窗口进入 =sticky= 状态,则必须满足 =floating == true=
|
||||
2. 若窗口原本是平铺窗口,开启 =sticky= 时,应先把当前最终外框矩形写入 =float_rect= ,再设 =floating = true= ,最后设 =sticky = true=
|
||||
3. 关闭 =sticky= 时,只清除 =sticky= 标记,不自动恢复到平铺模式
|
||||
|
||||
不自动恢复平铺的原因是:否则核心必须额外保存“进入 sticky 前是否为 floating”的历史状态,复杂度不值得。
|
||||
|
||||
** 唯一归属 workspace
|
||||
|
||||
每个窗口始终只有一个归属 =workspace_id= 。
|
||||
|
||||
=sticky= 的作用只是:
|
||||
|
||||
- 让窗口在非归属 workspace 上也可见
|
||||
|
||||
而不是:
|
||||
|
||||
- 把窗口复制到多个 workspace
|
||||
- 让窗口同时归属于多个 workspace
|
||||
|
||||
因此:
|
||||
|
||||
1. =workspace_id= 仍然是窗口的唯一归属
|
||||
2. layout、focus 历史、命令目标和后续迁移规则都仍以该归属 workspace 为准
|
||||
|
||||
* 布局规则
|
||||
|
||||
** 不参与自动布局
|
||||
|
||||
=sticky= 窗口不参与任何自动布局。
|
||||
|
||||
这条规则在单显示器和多显示器下都成立:
|
||||
|
||||
1. 不进入任何 workspace 的平铺输入
|
||||
2. 不进入任何 output 的布局输入
|
||||
3. 几何始终来自 =float_rect=
|
||||
|
||||
原因:
|
||||
|
||||
1. 自动布局应是 workspace-local / output-local 的
|
||||
2. =sticky= 如果参与布局,会让同一个窗口对多个 workspace 或多个 output 的布局结果产生耦合
|
||||
3. 同一个窗口不能同时接受多份独立 layout 结果
|
||||
|
||||
** =fn == NULL= 的 floating 布局槽位
|
||||
|
||||
若当前 workspace 的活动布局槽位满足 =fn == NULL= ,则该布局表示 =floating= 布局。
|
||||
|
||||
这只表示当前 workspace 上的普通窗口不进行平铺计算,不改变 =sticky= 窗口的特殊规则:
|
||||
|
||||
1. 普通 floating 窗口:几何来自各自的 =float_rect=
|
||||
2. =sticky= 窗口:同样几何来自 =float_rect=
|
||||
3. 二者都不参与 layout 函数计算
|
||||
|
||||
* Taskbar 规则
|
||||
|
||||
** 基本规则
|
||||
|
||||
如果 taskbar 表达的是“当前 workspace 上用户可见的窗口列表”,则 =sticky= 窗口应被视为可见窗口,因此应出现在 taskbar 中。
|
||||
|
||||
推荐的基础判断是:
|
||||
|
||||
1. =skip_taskbar == false=
|
||||
2. 窗口在当前上下文中实际可见
|
||||
|
||||
** 单显示器
|
||||
|
||||
单显示器下,若当前 workspace 与窗口归属 workspace 不同,只要窗口是 =sticky= 且当前几何可见,仍应出现在 taskbar 中。
|
||||
|
||||
** 多显示器 / 每 output 一条 taskbar
|
||||
|
||||
若系统为每个 output 提供独立 taskbar ,推荐规则是:
|
||||
|
||||
1. 只在窗口实际可见的 output 上显示该窗口
|
||||
2. 不在所有 output 的 taskbar 中无条件复制显示
|
||||
|
||||
也就是说:
|
||||
|
||||
1. 窗口只落在左屏,则只显示在左屏 taskbar
|
||||
2. 窗口只落在右屏,则只显示在右屏 taskbar
|
||||
3. 窗口跨越两个 output ,则可同时出现在两个 output 的 taskbar
|
||||
|
||||
这样 taskbar 与屏幕上的真实可见内容保持一致。
|
||||
|
||||
* 多显示器规则
|
||||
|
||||
** 坐标空间
|
||||
|
||||
=float_rect= 和 =frame_rect= 使用全局桌面坐标,而不是某个 output 的局部坐标。
|
||||
|
||||
因此:
|
||||
|
||||
1. 一个 floating/sticky 窗口可以自然跨越多个 output
|
||||
2. output 可见性可以通过“窗口最终矩形是否与 output 矩形相交”来判断
|
||||
|
||||
** 可见性
|
||||
|
||||
=sticky= 只放宽 workspace 可见性,不提供 output 复制语义。
|
||||
|
||||
因此:
|
||||
|
||||
1. 窗口不会因为 =sticky= 自动在所有 output 上都出现
|
||||
2. 它只会出现在最终矩形实际覆盖到的 output 上
|
||||
3. 若窗口完全移到另一块屏幕,则旧屏幕上不再可见
|
||||
|
||||
* 跨 output 移动规则
|
||||
|
||||
** 拖动过程中
|
||||
|
||||
无论窗口是普通 =floating= 还是 =sticky= ,拖动过程中都只更新 =float_rect= 。
|
||||
|
||||
不应在拖动过程中频繁修改 =workspace_id= ,否则归属语义会抖动,也不利于测试。
|
||||
|
||||
** 拖动结束后的归属处理
|
||||
|
||||
拖动结束后,是否迁移窗口归属 workspace,应由显式策略控制,而不是隐式发生。
|
||||
|
||||
推荐沿用以下策略:
|
||||
|
||||
1. =WM_CROSS_OUTPUT_KEEP_WORKSPACE=
|
||||
- 只更新 =float_rect=
|
||||
- 不修改 =workspace_id=
|
||||
2. =WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE=
|
||||
- 使用稳定锚点确定目标 output
|
||||
- 若锚点落在另一个 output 上,则把 =workspace_id= 改为该 output 当前显示的 workspace
|
||||
|
||||
推荐锚点:
|
||||
|
||||
1. 窗口中心点
|
||||
2. 或命令中显式提供的 =anchor=
|
||||
|
||||
不建议按覆盖面积决定归属,因为该规则在窗口跨屏时不稳定,也不直观。
|
||||
|
||||
** =sticky= 下的跨 output 移动
|
||||
|
||||
=sticky= 窗口跨屏拖动时,默认也不自动修改 =workspace_id= 。
|
||||
|
||||
原因:
|
||||
|
||||
1. =sticky= 本来就可以跨 workspace 可见
|
||||
2. 若移动几何时顺便修改归属,会让“移动窗口”和“迁移窗口”两个动作发生耦合
|
||||
|
||||
因此推荐:
|
||||
|
||||
1. 默认只更新 =float_rect=
|
||||
2. 若用户明确要求迁移,则再执行归属 workspace 切换
|
||||
|
||||
* 关闭 Sticky 的语义
|
||||
|
||||
关闭 =sticky= 时:
|
||||
|
||||
1. 只清除 =sticky= 标记
|
||||
2. 不自动修改 =workspace_id=
|
||||
3. 不自动修改 =floating=
|
||||
4. 不自动恢复平铺
|
||||
|
||||
若当前查看的 workspace 不是窗口归属的 =workspace_id= ,则关闭 =sticky= 后窗口应立即从当前视图消失。
|
||||
|
||||
如果该窗口在关闭 =sticky= 前是当前焦点窗口,则需要重新选择当前 workspace 的焦点窗口。
|
||||
|
||||
* 设计原则总结
|
||||
|
||||
这套规则的目标是把几个概念彻底拆开:
|
||||
|
||||
1. =floating= 管几何来源
|
||||
2. =sticky= 管跨 workspace 可见性
|
||||
3. =workspace_id= 管唯一归属
|
||||
4. 跨 output 拖动只改几何
|
||||
5. workspace 迁移必须显式提交
|
||||
|
||||
这样可以避免以下几类混乱语义:
|
||||
|
||||
1. 拖动窗口时归属 workspace 频繁跳变
|
||||
2. =sticky= 窗口同时参与多个布局
|
||||
3. taskbar 显示内容与实际可见窗口不一致
|
||||
4. 关闭 =sticky= 时隐式触发额外状态回滚
|
||||
@@ -1,286 +0,0 @@
|
||||
* Workspace-Output 配置示例
|
||||
|
||||
本文档展示在不同场景下如何配置 workspace 和 output 的归属关系。
|
||||
|
||||
** 单显示器配置
|
||||
|
||||
最简单的配置:所有 workspace 归属到唯一输出。
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- zdwm.lua
|
||||
|
||||
outputs = {
|
||||
{ id = 0, name = "DP-1", primary = true },
|
||||
}
|
||||
|
||||
-- 所有 workspace 都归属 DP-1
|
||||
workspaces = {
|
||||
{ id = 1, name = "1", output_id = 0 },
|
||||
{ id = 2, name = "2", output_id = 0 },
|
||||
{ id = 3, name = "3", output_id = 0 },
|
||||
{ id = 4, name = "4", output_id = 0 },
|
||||
{ id = 5, name = "5", output_id = 0 },
|
||||
{ id = 6, name = "6", output_id = 0 },
|
||||
{ id = 7, name = "7", output_id = 0 },
|
||||
{ id = 8, name = "8", output_id = 0 },
|
||||
{ id = 9, name = "9", output_id = 0 },
|
||||
{ id = 10, name = "10", output_id = 0 },
|
||||
}
|
||||
|
||||
-- 或使用自动生成
|
||||
workspaces = auto_generate_workspaces({
|
||||
output_id = 0,
|
||||
count = 10,
|
||||
name_pattern = "number" -- 1, 2, 3, ...
|
||||
})
|
||||
#+END_SRC
|
||||
|
||||
** 双显示器配置
|
||||
|
||||
*** 场景 1:平分 workspace
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 左侧显示器 1-5,右侧显示器 6-10
|
||||
outputs = {
|
||||
{ id = 0, name = "DP-1", primary = true },
|
||||
{ id = 1, name = "DP-2" },
|
||||
}
|
||||
|
||||
workspaces = {
|
||||
-- DP-1 的 workspace
|
||||
{ id = 1, name = "1", output_id = 0 },
|
||||
{ id = 2, name = "2", output_id = 0 },
|
||||
{ id = 3, name = "3", output_id = 0 },
|
||||
{ id = 4, name = "4", output_id = 0 },
|
||||
{ id = 5, name = "5", output_id = 0 },
|
||||
|
||||
-- DP-2 的 workspace
|
||||
{ id = 6, name = "6", output_id = 1 },
|
||||
{ id = 7, name = "7", output_id = 1 },
|
||||
{ id = 8, name = "8", output_id = 1 },
|
||||
{ id = 9, name = "9", output_id = 1 },
|
||||
{ id = 10, name = "10", output_id = 1 },
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
*** 场景 2:偶数/奇数分配
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 奇数在左,偶数在右
|
||||
workspaces = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workspaces, {
|
||||
id = i,
|
||||
name = tostring(i),
|
||||
output_id = (i % 2 == 1) and 0 or 1 -- 奇数->0,偶数->1
|
||||
})
|
||||
end
|
||||
#+END_SRC
|
||||
|
||||
*** 场景 3:主题化 workspace
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 左侧开发相关,右侧娱乐相关
|
||||
outputs = {
|
||||
{ id = 0, name = "DP-1", primary = true },
|
||||
{ id = 1, name = "HDMI-1" },
|
||||
}
|
||||
|
||||
workspaces = {
|
||||
-- DP-1 (开发)
|
||||
{ id = 1, name = "code", output_id = 0 },
|
||||
{ id = 2, name = "term", output_id = 0 },
|
||||
{ id = 3, name = "docs", output_id = 0 },
|
||||
{ id = 4, name = "debug", output_id = 0 },
|
||||
|
||||
-- HDMI-1 (娱乐)
|
||||
{ id = 5, name = "chat", output_id = 1 },
|
||||
{ id = 6, name = "web", output_id = 1 },
|
||||
{ id = 7, name = "media", output_id = 1 },
|
||||
{ id = 8, name = "game", output_id = 1 },
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** 三显示器配置
|
||||
|
||||
笔记本 + 外接显示器的常见配置:
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- eDP-1: 日常工作 (1-3)
|
||||
-- DP-1: 代码 (4-6)
|
||||
-- HDMI-1: 娱乐 (7-9)
|
||||
|
||||
outputs = {
|
||||
{ id = 0, name = "eDP-1", primary = true },
|
||||
{ id = 1, name = "DP-1" },
|
||||
{ id = 2, name = "HDMI-1" },
|
||||
}
|
||||
|
||||
workspaces = {
|
||||
-- eDP-1 (笔记本)
|
||||
{ id = 1, name = "mail", output_id = 0 },
|
||||
{ id = 2, name = "chat", output_id = 0 },
|
||||
{ id = 3, name = "docs", output_id = 0 },
|
||||
|
||||
-- DP-1 (外接显示器 - 左)
|
||||
{ id = 4, name = "code", output_id = 1 },
|
||||
{ id = 5, name = "term", output_id = 1 },
|
||||
{ id = 6, name = "debug", output_id = 1 },
|
||||
|
||||
-- HDMI-1 (外接显示器 - 右)
|
||||
{ id = 7, name = "web", output_id = 2 },
|
||||
{ id = 8, name = "media", output_id = 2 },
|
||||
{ id = 9, name = "game", output_id = 2 },
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** 启动阶段脚本生成
|
||||
|
||||
根据启动阶段检测到的输出数量生成:
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
function configure_workspaces(outputs)
|
||||
local workspaces = {}
|
||||
local ws_id = 1
|
||||
|
||||
-- 每个 output 分配 3 个 workspace
|
||||
for _, output in ipairs(outputs) do
|
||||
for i = 1, 3 do
|
||||
table.insert(workspaces, {
|
||||
id = ws_id,
|
||||
name = string.format("%d:%s", ws_id, output.name),
|
||||
output_id = output.id,
|
||||
layout_id = 0, -- 默认布局
|
||||
})
|
||||
ws_id = ws_id + 1
|
||||
end
|
||||
end
|
||||
|
||||
return workspaces
|
||||
end
|
||||
#+END_SRC
|
||||
|
||||
** 配置加载流程
|
||||
|
||||
#+BEGIN_SRC c
|
||||
// config_loader.c
|
||||
|
||||
typedef struct workspace_config_t {
|
||||
wm_workspace_id_t id;
|
||||
const char *name;
|
||||
wm_output_id_t output_id;
|
||||
wm_layout_id_t layout_id;
|
||||
} workspace_config_t;
|
||||
|
||||
bool load_workspace_config(const char *path,
|
||||
workspace_config_t **out_workspaces,
|
||||
size_t *out_count) {
|
||||
// 1. 扫描输出
|
||||
output_config_t *outputs;
|
||||
size_t output_count;
|
||||
scan_outputs(&outputs, &output_count);
|
||||
|
||||
// 2. 加载 workspace 配置
|
||||
workspace_config_t *workspaces;
|
||||
size_t workspace_count;
|
||||
parse_config_file(path, &workspaces, &workspace_count);
|
||||
|
||||
// 3. 验证:每个 workspace 的 output_id 必须有效
|
||||
for (size_t i = 0; i < workspace_count; i++) {
|
||||
if (workspaces[i].output_id >= output_count) {
|
||||
fprintf(stderr, "Workspace %d: invalid output_id %d\n",
|
||||
workspaces[i].id, workspaces[i].output_id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
*out_workspaces = workspaces;
|
||||
*out_count = workspace_count;
|
||||
return true;
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** 切换行为示例
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 切换到 workspace 4(在 DP-1 上)
|
||||
-- 效果:只有 DP-1 切换,DP-2 保持当前 workspace
|
||||
|
||||
-- 将窗口从 workspace 1 移动到 workspace 4
|
||||
-- 效果:窗口从 DP-1 移动到 DP-1(但在不同的 workspace)
|
||||
-- 如果 workspace 4 当前在 DP-2 显示,窗口会在 DP-2 出现
|
||||
#+END_SRC
|
||||
|
||||
** 布局列表配置
|
||||
|
||||
每个 workspace 可以配置自己的可用布局列表。
|
||||
|
||||
*** 基础配置
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 声明布局注册表
|
||||
-- layout_id 按注册顺序稳定分配:tile=0, monocle=1, floating=2, ...
|
||||
layouts = {
|
||||
{ name = "tile", symbol = "T", fn = layout_tile },
|
||||
{ name = "monocle", symbol = "M", fn = layout_monocle },
|
||||
{ name = "floating", symbol = "F", fn = layout_floating },
|
||||
{ name = "stack", symbol = "S", fn = layout_stack },
|
||||
{ name = "grid", symbol = "G", fn = layout_grid },
|
||||
}
|
||||
|
||||
-- 单显示器:每个 workspace 可用所有布局
|
||||
workspaces = {
|
||||
{ id = 1, name = "1", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
|
||||
{ id = 2, name = "2", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
|
||||
{ id = 3, name = "3", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
*** 主题化布局配置
|
||||
|
||||
不同用途的 workspace 配置不同的布局集合:
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
workspaces = {
|
||||
-- 代码 workspace:平铺、单列、堆叠(适合代码+终端)
|
||||
{ id = 1, name = "code", output_id = 0,
|
||||
layouts = {0, 3, 4}, default_layout = 0 }, -- T, S, G
|
||||
|
||||
-- 浏览器 workspace:单列、浮动、全屏(适合阅读)
|
||||
{ id = 2, name = "web", output_id = 0,
|
||||
layouts = {1, 2}, default_layout = 1 }, -- M, F
|
||||
|
||||
-- 游戏 workspace:仅浮动(游戏窗口通常需要浮动)
|
||||
{ id = 3, name = "game", output_id = 1,
|
||||
layouts = {2}, default_layout = 2 }, -- F only
|
||||
|
||||
-- 聊天 workspace:堆叠、单列
|
||||
{ id = 4, name = "chat", output_id = 1,
|
||||
layouts = {3, 1}, default_layout = 3 }, -- S, M
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
*** 默认布局配置
|
||||
|
||||
如果不指定 layouts 字段,使用全局默认布局列表:
|
||||
|
||||
#+BEGIN_SRC lua
|
||||
-- 全局默认
|
||||
default_layouts = {0, 1, 2, 3, 4} -- 所有布局可用
|
||||
|
||||
-- workspace 省略 layouts 时使用全局默认
|
||||
workspaces = {
|
||||
{ id = 1, name = "1", output_id = 0 }, -- 使用全局默认
|
||||
{ id = 2, name = "2", output_id = 0,
|
||||
layouts = {0, 1}, default_layout = 0 }, -- 自定义
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** 注意事项
|
||||
|
||||
1. *固定归属*:workspace.output_id 在创建后不应改变
|
||||
2. *输出变化*:显示器配置变化时,WM 会重启,重新扫描和配置
|
||||
3. *可见性*:普通非 =sticky= 窗口先受 workspace 可见性约束,再按最终矩形与 output 的相交关系决定具体显示在哪些 output 上;=sticky= 窗口只放宽 workspace 可见性
|
||||
4. *独立性*:切换不同 output 的 workspace 互不影响
|
||||
5. *布局列表*:workspace.layouts 在启动时分配,运行时固定
|
||||
6. *布局切换*:使用 =CYCLE_LAYOUT= 或 =SET_LAYOUT= 命令在可用布局集合内切换当前布局
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_binding.h"
|
||||
#include "wm_event.h"
|
||||
#include "wm_plan.h"
|
||||
|
||||
typedef struct wm_backend_t wm_backend_t;
|
||||
|
||||
typedef enum wm_backend_next_result_t {
|
||||
// 成功产出一个 runtime 输入事件,out 有效
|
||||
WM_BACKEND_NEXT_EVENT,
|
||||
// backend 请求 runtime 正常停止(例如连接关闭)
|
||||
WM_BACKEND_NEXT_STOP,
|
||||
// backend 检测到必须重建 bootstrap 的平台变化(例如显示器配置变化)
|
||||
WM_BACKEND_NEXT_RESTART_REQUIRED,
|
||||
// backend 遇到无法恢复的错误
|
||||
WM_BACKEND_NEXT_ERROR,
|
||||
} wm_backend_next_result_t;
|
||||
|
||||
typedef struct wm_backend_api_t {
|
||||
bool (*init)(wm_backend_t *backend);
|
||||
/*
|
||||
* 安装 bootstrap 固定下来的键盘绑定表。
|
||||
* X11 这类需要被动抓键的平台可以在这里建立平台侧 grab;
|
||||
* 总能收到所有键盘事件的 backend 可以把它留空。
|
||||
*/
|
||||
bool (*set_keybindings)(
|
||||
wm_backend_t *backend,
|
||||
const wm_key_binding_table_t *keybindings
|
||||
);
|
||||
/*
|
||||
* 安装 bootstrap 固定下来的鼠标按键绑定表。
|
||||
* X11 这类需要被动抓按钮的平台可以把它缓存下来,并在 root 或新受管窗口
|
||||
* 上按 target 规则建立 grab;总能收到所有 pointer button 事件的 backend
|
||||
* 可以把它留空。
|
||||
*/
|
||||
bool (*set_pointer_bindings)(
|
||||
wm_backend_t *backend,
|
||||
const wm_pointer_binding_table_t *pointer_bindings
|
||||
);
|
||||
/*
|
||||
* 返回一个 backend 轮询结果。
|
||||
* 只有返回 WM_BACKEND_NEXT_EVENT 时,out 才包含一个完整的 runtime 输入事件。
|
||||
*/
|
||||
wm_backend_next_result_t (*next_event)(
|
||||
wm_backend_t *backend,
|
||||
wm_event_t *out
|
||||
);
|
||||
/*
|
||||
* Runtime filters out service-side effects such as WM_EFFECT_RENDER_OUTPUT.
|
||||
* Backends only receive platform effects here.
|
||||
*/
|
||||
bool (*apply_effect)(wm_backend_t *backend, const wm_effect_t *effect);
|
||||
void (*flush)(wm_backend_t *backend);
|
||||
void (*shutdown)(wm_backend_t *backend);
|
||||
} wm_backend_api_t;
|
||||
|
||||
struct wm_backend_t {
|
||||
const wm_backend_api_t *api;
|
||||
void *impl;
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "wm_command.h"
|
||||
#include "wm_types.h"
|
||||
|
||||
/*
|
||||
* 输入绑定只匹配 backend 已归一化后的输入事件。
|
||||
* backend 负责把平台原始输入翻译为 wm_event_t,并决定是否折叠 auto-repeat。
|
||||
*/
|
||||
typedef enum wm_binding_match_mode_t {
|
||||
// modifiers 必须与事件完全一致
|
||||
WM_BINDING_MATCH_EXACT,
|
||||
// 事件允许带有更多修饰键,但必须至少包含 binding.modifiers
|
||||
WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS,
|
||||
} wm_binding_match_mode_t;
|
||||
|
||||
typedef struct wm_key_binding_t {
|
||||
wm_keysym_t keysym;
|
||||
wm_modifier_mask_t modifiers;
|
||||
wm_binding_match_mode_t match_mode;
|
||||
// 命中的绑定会直接复制这条命令模板进入 command buffer
|
||||
wm_command_t command;
|
||||
} wm_key_binding_t;
|
||||
|
||||
typedef struct wm_key_binding_table_t {
|
||||
const wm_key_binding_t *items;
|
||||
size_t count;
|
||||
} wm_key_binding_table_t;
|
||||
|
||||
/*
|
||||
* 核心只关心 root / window 两类命中目标:
|
||||
* - bar / status / widget 等点击区域属于核心外服务,不进入最小核心绑定系统
|
||||
* - 当事件未命中受管窗口时,wm_event_t.window == WM_WINDOW_ID_INVALID
|
||||
*/
|
||||
typedef enum wm_pointer_binding_target_t {
|
||||
WM_POINTER_BINDING_TARGET_ANY,
|
||||
WM_POINTER_BINDING_TARGET_ROOT,
|
||||
WM_POINTER_BINDING_TARGET_WINDOW,
|
||||
} wm_pointer_binding_target_t;
|
||||
|
||||
/*
|
||||
* 最小核心当前只定义“按下触发”的鼠标绑定;
|
||||
* button release 主要保留给 move/resize 交互结束逻辑。
|
||||
*
|
||||
* 若 command 是窗口目标命令,且模板中的 window_id 为
|
||||
* WM_WINDOW_ID_INVALID,则 route_pointer_press() 应把它替换为当前点击的
|
||||
* event->window。
|
||||
*/
|
||||
typedef struct wm_pointer_binding_t {
|
||||
wm_button_t button;
|
||||
wm_modifier_mask_t modifiers;
|
||||
wm_binding_match_mode_t match_mode;
|
||||
wm_pointer_binding_target_t target;
|
||||
wm_command_t command;
|
||||
} wm_pointer_binding_t;
|
||||
|
||||
typedef struct wm_pointer_binding_table_t {
|
||||
const wm_pointer_binding_t *items;
|
||||
size_t count;
|
||||
} wm_pointer_binding_table_t;
|
||||
|
||||
/*
|
||||
* 查找与当前按键事件匹配的绑定。
|
||||
* 匹配顺序固定为:
|
||||
* 1. 所有 exact 绑定,按数组顺序扫描
|
||||
* 2. 所有 allow-extra-modifiers 绑定,按数组顺序扫描
|
||||
*
|
||||
* 当前最小核心不内建“默认绑定生成规则”;像 SUPER+1 这类行为也必须显式
|
||||
* 出现在 key binding table 里。
|
||||
*/
|
||||
const wm_key_binding_t *wm_key_binding_find(
|
||||
const wm_key_binding_table_t *table,
|
||||
wm_keysym_t keysym,
|
||||
wm_modifier_mask_t modifiers
|
||||
);
|
||||
|
||||
/*
|
||||
* 查找与当前按钮按下事件匹配的鼠标绑定。
|
||||
* hits_window == true 表示事件命中了某个受管窗口;
|
||||
* hits_window == false 表示事件落在 root / 空白区域。
|
||||
*
|
||||
* 匹配顺序与键盘绑定一致:先 exact,再 allow-extra-modifiers。
|
||||
*/
|
||||
const wm_pointer_binding_t *wm_pointer_binding_find(
|
||||
const wm_pointer_binding_table_t *table,
|
||||
wm_button_t button,
|
||||
wm_modifier_mask_t modifiers,
|
||||
bool hits_window
|
||||
);
|
||||
@@ -1,149 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_types.h"
|
||||
|
||||
typedef struct wm_manage_window_init_t {
|
||||
wm_window_geometry_mode_t geometry_mode;
|
||||
bool floating;
|
||||
bool sticky;
|
||||
bool urgent;
|
||||
bool set_focus;
|
||||
} wm_manage_window_init_t;
|
||||
|
||||
typedef enum wm_command_type_t {
|
||||
WM_COMMAND_NONE,
|
||||
WM_COMMAND_MANAGE_WINDOW,
|
||||
WM_COMMAND_UNMANAGE_WINDOW,
|
||||
WM_COMMAND_FOCUS_WINDOW,
|
||||
WM_COMMAND_FOCUS_DIRECTION,
|
||||
WM_COMMAND_SET_WINDOW_URGENT,
|
||||
WM_COMMAND_RAISE_WINDOW,
|
||||
WM_COMMAND_LOWER_WINDOW,
|
||||
WM_COMMAND_SWITCH_WORKSPACE,
|
||||
WM_COMMAND_SEND_WINDOW_TO_WORKSPACE,
|
||||
WM_COMMAND_SEND_WINDOW_TO_OUTPUT,
|
||||
WM_COMMAND_TOGGLE_FLOATING,
|
||||
WM_COMMAND_TOGGLE_STICKY,
|
||||
WM_COMMAND_SET_MAXIMIZED,
|
||||
WM_COMMAND_SET_FULLSCREEN,
|
||||
WM_COMMAND_SET_MINIMIZED,
|
||||
WM_COMMAND_MOVE_FLOATING_WINDOW,
|
||||
WM_COMMAND_RESIZE_FLOATING_WINDOW,
|
||||
WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION,
|
||||
WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION,
|
||||
WM_COMMAND_SET_LAYOUT,
|
||||
WM_COMMAND_CYCLE_LAYOUT,
|
||||
WM_COMMAND_REDRAW,
|
||||
WM_COMMAND_QUIT,
|
||||
} wm_command_type_t;
|
||||
|
||||
typedef struct wm_command_t {
|
||||
wm_command_type_t type;
|
||||
union {
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
wm_workspace_id_t workspace_id;
|
||||
wm_manage_window_init_t initial_state;
|
||||
bool has_initial_float_rect;
|
||||
wm_rect_t initial_float_rect;
|
||||
} manage_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} unmanage_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} focus_window;
|
||||
|
||||
struct {
|
||||
wm_output_id_t output_id;
|
||||
wm_focus_direction_t direction;
|
||||
} focus_direction;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
bool urgent;
|
||||
} set_window_urgent;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} raise_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} lower_window;
|
||||
|
||||
struct {
|
||||
wm_output_id_t output_id;
|
||||
wm_workspace_id_t workspace_id;
|
||||
} switch_workspace;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
wm_workspace_id_t workspace_id;
|
||||
} send_window_to_workspace;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
wm_output_id_t output_id;
|
||||
wm_cross_output_policy_t policy;
|
||||
} send_window_to_output;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} toggle_floating;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} toggle_sticky;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
bool enabled;
|
||||
} set_maximized;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
bool enabled;
|
||||
} set_fullscreen;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
bool enabled;
|
||||
} set_minimized;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
bool commit_workspace_change;
|
||||
wm_point_t anchor;
|
||||
wm_cross_output_policy_t policy;
|
||||
} move_floating_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
int16_t dw;
|
||||
int16_t dh;
|
||||
} resize_floating_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} begin_move_floating_interaction;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} begin_resize_floating_interaction;
|
||||
|
||||
struct {
|
||||
wm_workspace_id_t workspace_id;
|
||||
wm_layout_id_t layout_id;
|
||||
} set_layout;
|
||||
|
||||
struct {
|
||||
wm_workspace_id_t workspace_id;
|
||||
int direction; // 1: 下一个布局, -1: 上一个布局
|
||||
} cycle_layout;
|
||||
} as;
|
||||
} wm_command_t;
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_types.h"
|
||||
|
||||
typedef struct wm_output_info_t {
|
||||
char *name;
|
||||
wm_rect_t geometry;
|
||||
} wm_output_info_t;
|
||||
|
||||
/*
|
||||
* 由 backend 提供的窗口基础信息。
|
||||
*
|
||||
* 这些字符串由调用方提供;state 如需长期持有,应自行复制。
|
||||
*/
|
||||
typedef struct wm_window_info_t {
|
||||
wm_window_id_t id;
|
||||
wm_rect_t frame_rect;
|
||||
|
||||
const char *title;
|
||||
const char *app_id;
|
||||
const char *class_name;
|
||||
const char *instance_name;
|
||||
|
||||
wm_window_geometry_mode_t geometry_mode;
|
||||
bool urgent;
|
||||
bool fixed_size;
|
||||
bool skip_taskbar;
|
||||
} wm_window_info_t;
|
||||
|
||||
typedef struct wm_workspace_desc_t {
|
||||
size_t output_index;
|
||||
const char *name;
|
||||
const wm_layout_id_t *layout_ids;
|
||||
size_t layout_count;
|
||||
wm_layout_id_t initial_layout_id;
|
||||
} wm_workspace_desc_t;
|
||||
@@ -1,228 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_desc.h"
|
||||
|
||||
/*
|
||||
* wm_event_t 表示“runtime 真正关心的输入事实”:
|
||||
* - 它不是平台原始事件的逐字段照抄
|
||||
* - 它允许 backend 对平台事件做最小必要的归一化 / 过滤 / 聚合
|
||||
* - 它不承载 runtime 控制语义;stop / restart / error 由 backend.next_event()
|
||||
* 的返回结果表达,而不是伪装成一个事件类型
|
||||
*/
|
||||
typedef enum wm_event_type_t {
|
||||
// 最小核心里的快捷键只由 key press 触发,不单独暴露 key release 事件
|
||||
WM_EVENT_KEY_PRESS,
|
||||
// 鼠标绑定只匹配 button press;button release 主要供交互态结束拖拽使用
|
||||
WM_EVENT_POINTER_BUTTON_PRESS,
|
||||
WM_EVENT_POINTER_BUTTON_RELEASE,
|
||||
WM_EVENT_POINTER_MOTION,
|
||||
// 指针焦点进入某个窗口;对应 X11 EnterNotify / Wayland wl_pointer.enter
|
||||
WM_EVENT_POINTER_ENTER,
|
||||
WM_EVENT_WINDOW_MAP_REQUEST,
|
||||
WM_EVENT_WINDOW_REMOVE,
|
||||
WM_EVENT_WINDOW_METADATA_CHANGED,
|
||||
WM_EVENT_WINDOW_HINTS_CHANGED,
|
||||
WM_EVENT_WINDOW_ACTIVATE_REQUEST,
|
||||
WM_EVENT_WINDOW_STATE_REQUEST,
|
||||
WM_EVENT_CONFIGURE_REQUEST,
|
||||
} wm_event_type_t;
|
||||
|
||||
typedef struct wm_key_press_event_t {
|
||||
/*
|
||||
* 规范化后的逻辑主键编码(xkb keysym)。
|
||||
* backend 应先结合 keymap / xkb state 把原始按键解析成逻辑键,再写入这里。
|
||||
* 它适合与 "SUPER+m" 这类配置字符串解析结果做匹配。
|
||||
*/
|
||||
wm_keysym_t keysym;
|
||||
// backend 归一化后的修饰键掩码
|
||||
wm_modifier_mask_t modifiers;
|
||||
/*
|
||||
* backend 原始物理键编码。
|
||||
* 它保留平台侧按键信息,便于调试、日志或未来按物理键位绑定;
|
||||
* 常规快捷键匹配不应直接使用它。
|
||||
*/
|
||||
uint32_t keycode;
|
||||
} wm_key_press_event_t;
|
||||
|
||||
typedef struct wm_pointer_button_event_t {
|
||||
wm_button_t button;
|
||||
wm_modifier_mask_t modifiers;
|
||||
// backend 原始按钮码,仅供调试或未来平台特化逻辑使用
|
||||
uint32_t raw_button;
|
||||
// 全局桌面坐标
|
||||
wm_point_t root;
|
||||
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID,则该值未定义
|
||||
wm_point_t local;
|
||||
// 当前命中的窗口;若未命中窗口,则为 WM_WINDOW_ID_INVALID
|
||||
wm_window_id_t window;
|
||||
} wm_pointer_button_event_t;
|
||||
|
||||
typedef struct wm_pointer_motion_event_t {
|
||||
// 全局桌面坐标
|
||||
wm_point_t root;
|
||||
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID,则该值未定义
|
||||
wm_point_t local;
|
||||
// 当前命中的窗口;若未命中窗口,则为 WM_WINDOW_ID_INVALID
|
||||
wm_window_id_t window;
|
||||
} wm_pointer_motion_event_t;
|
||||
|
||||
typedef struct wm_pointer_enter_event_t {
|
||||
// 全局桌面坐标
|
||||
wm_point_t root;
|
||||
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID,则该值未定义
|
||||
wm_point_t local;
|
||||
// 当前进入并获得 pointer focus 的窗口;若未命中窗口,则为
|
||||
// WM_WINDOW_ID_INVALID
|
||||
wm_window_id_t window;
|
||||
} wm_pointer_enter_event_t;
|
||||
|
||||
typedef struct wm_window_map_request_event_t {
|
||||
wm_window_info_t info;
|
||||
// 仅用于 manage 路由判断,不进入持久 state
|
||||
wm_window_id_t transient_for;
|
||||
// 仅用于 manage 路由判断,不进入持久 state
|
||||
bool is_dialog;
|
||||
} wm_window_map_request_event_t;
|
||||
|
||||
/*
|
||||
* 归一化后的窗口移除事件。
|
||||
* backend 把 withdraw / destroy 这类平台差异折叠成单一 remove 事件,
|
||||
* core 只按 reason 区分“为何离开 managed 集合”。
|
||||
*/
|
||||
typedef enum wm_window_remove_reason_t {
|
||||
WM_WINDOW_REMOVE_WITHDRAWN,
|
||||
WM_WINDOW_REMOVE_DESTROY,
|
||||
} wm_window_remove_reason_t;
|
||||
|
||||
typedef struct wm_window_remove_event_t {
|
||||
wm_window_id_t window;
|
||||
wm_window_remove_reason_t reason;
|
||||
} wm_window_remove_event_t;
|
||||
|
||||
typedef enum wm_window_meta_changed_flags_t {
|
||||
WM_WINDOW_META_CHANGED_NONE = 0,
|
||||
WM_WINDOW_META_CHANGED_TITLE = 1u << 0,
|
||||
WM_WINDOW_META_CHANGED_APP_ID = 1u << 1,
|
||||
WM_WINDOW_META_CHANGED_CLASS = 1u << 2,
|
||||
WM_WINDOW_META_CHANGED_INSTANCE = 1u << 3,
|
||||
} wm_window_meta_changed_flags_t;
|
||||
|
||||
typedef struct wm_window_metadata_changed_event_t {
|
||||
wm_window_id_t window;
|
||||
uint32_t changed_fields;
|
||||
/*
|
||||
* These strings are borrowed from the backend adapter and are valid until
|
||||
* wm_runtime_process_event() returns. changed_fields decides which ones are
|
||||
* meaningful for the current event.
|
||||
* runtime 会复制这些字符串到 wm_window_t 中的对应字段。
|
||||
*/
|
||||
const char *title;
|
||||
const char *app_id;
|
||||
const char *class_name;
|
||||
const char *instance_name;
|
||||
} wm_window_metadata_changed_event_t;
|
||||
|
||||
typedef enum wm_window_hint_changed_flags_t {
|
||||
WM_WINDOW_HINT_CHANGED_NONE = 0,
|
||||
WM_WINDOW_HINT_CHANGED_URGENT = 1u << 0,
|
||||
WM_WINDOW_HINT_CHANGED_FIXED_SIZE = 1u << 1,
|
||||
WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR = 1u << 2,
|
||||
} wm_window_hint_changed_flags_t;
|
||||
|
||||
/*
|
||||
* 客户端属性 / hint 变化的归一化结果。
|
||||
* 这类事件不是控制请求,不经过 command 路由;
|
||||
* runtime 直接把结果同步到 wm_window_t 中的 hint/capability 字段。
|
||||
*/
|
||||
typedef struct wm_window_hints_changed_event_t {
|
||||
wm_window_id_t window;
|
||||
uint32_t changed_fields;
|
||||
bool urgent;
|
||||
bool fixed_size;
|
||||
bool skip_taskbar;
|
||||
} wm_window_hints_changed_event_t;
|
||||
|
||||
/*
|
||||
* 归一化后的窗口激活请求来源。
|
||||
* 取值对齐 EWMH _NET_ACTIVE_WINDOW 的 source indication:
|
||||
* - 0: 旧客户端 / 未指明来源
|
||||
* - 1: 应用自身发起
|
||||
* - 2: pager / taskbar / window switcher 发起
|
||||
*/
|
||||
typedef enum wm_window_activation_source_t {
|
||||
WM_WINDOW_ACTIVATION_SOURCE_LEGACY = 0,
|
||||
WM_WINDOW_ACTIVATION_SOURCE_APPLICATION = 1,
|
||||
WM_WINDOW_ACTIVATION_SOURCE_PAGER = 2,
|
||||
} wm_window_activation_source_t;
|
||||
|
||||
typedef struct wm_window_activate_request_event_t {
|
||||
wm_window_id_t window;
|
||||
wm_window_activation_source_t source;
|
||||
} wm_window_activate_request_event_t;
|
||||
|
||||
/*
|
||||
* 归一化后的窗口模式切换请求。
|
||||
* backend 保留 add/remove/toggle 这类原始协议语义;
|
||||
* route_event() 再结合当前 wm_state_t 翻译成最终的 SET_* 命令。
|
||||
*/
|
||||
typedef enum wm_window_state_request_kind_t {
|
||||
WM_WINDOW_STATE_REQUEST_FULLSCREEN,
|
||||
WM_WINDOW_STATE_REQUEST_MAXIMIZED,
|
||||
WM_WINDOW_STATE_REQUEST_MINIMIZED,
|
||||
} wm_window_state_request_kind_t;
|
||||
|
||||
typedef enum wm_window_state_request_action_t {
|
||||
WM_WINDOW_STATE_REQUEST_ACTION_ADD,
|
||||
WM_WINDOW_STATE_REQUEST_ACTION_REMOVE,
|
||||
WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE,
|
||||
} wm_window_state_request_action_t;
|
||||
|
||||
typedef struct wm_window_state_request_event_t {
|
||||
wm_window_id_t window;
|
||||
wm_window_state_request_kind_t kind;
|
||||
wm_window_state_request_action_t action;
|
||||
} wm_window_state_request_event_t;
|
||||
|
||||
typedef enum wm_configure_request_field_t {
|
||||
WM_CONFIGURE_REQUEST_FIELD_NONE = 0,
|
||||
WM_CONFIGURE_REQUEST_FIELD_X = 1u << 0,
|
||||
WM_CONFIGURE_REQUEST_FIELD_Y = 1u << 1,
|
||||
WM_CONFIGURE_REQUEST_FIELD_WIDTH = 1u << 2,
|
||||
WM_CONFIGURE_REQUEST_FIELD_HEIGHT = 1u << 3,
|
||||
} wm_configure_request_field_t;
|
||||
|
||||
/*
|
||||
* 归一化后的窗口配置请求。
|
||||
* 这类事件只表达稀疏几何请求,不承载 fullscreen/maximize/minimize
|
||||
* 之类窗口模式切换;后者应进入独立的 state request event。
|
||||
*
|
||||
* x / y / width / height 中只有 changed_fields 指示的成员才有意义。
|
||||
*/
|
||||
typedef struct wm_configure_request_event_t {
|
||||
wm_window_id_t window;
|
||||
uint32_t changed_fields;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
} wm_configure_request_event_t;
|
||||
|
||||
typedef struct wm_event_t {
|
||||
wm_event_type_t type;
|
||||
// backend 归一化后的事件时间值(毫秒),不是 wall-clock 时间戳
|
||||
wm_time_ms_t time_ms;
|
||||
union {
|
||||
wm_key_press_event_t key_press;
|
||||
wm_pointer_button_event_t pointer_button_press;
|
||||
wm_pointer_button_event_t pointer_button_release;
|
||||
wm_pointer_motion_event_t pointer_motion;
|
||||
wm_pointer_enter_event_t pointer_enter;
|
||||
wm_window_map_request_event_t window_map_request;
|
||||
wm_window_remove_event_t window_remove;
|
||||
wm_window_metadata_changed_event_t window_metadata_changed;
|
||||
wm_window_hints_changed_event_t window_hints_changed;
|
||||
wm_window_activate_request_event_t window_activate_request;
|
||||
wm_window_state_request_event_t window_state_request;
|
||||
wm_configure_request_event_t configure_request;
|
||||
} data;
|
||||
} wm_event_t;
|
||||
@@ -1,88 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_types.h"
|
||||
|
||||
typedef struct wm_layout_window_ref_t {
|
||||
wm_window_id_t window_id;
|
||||
wm_window_geometry_mode_t geometry_mode;
|
||||
} wm_layout_window_ref_t;
|
||||
|
||||
typedef struct wm_layout_ctx_t {
|
||||
wm_output_id_t output_id;
|
||||
wm_workspace_id_t workspace_id;
|
||||
wm_window_id_t focused_window_id;
|
||||
wm_rect_t workarea;
|
||||
const wm_layout_window_ref_t *windows;
|
||||
size_t window_count;
|
||||
} wm_layout_ctx_t;
|
||||
|
||||
typedef struct wm_layout_item_t {
|
||||
wm_window_id_t window_id;
|
||||
wm_rect_t rect; // 平铺窗口的目标外框矩形(包含边框)
|
||||
} wm_layout_item_t;
|
||||
|
||||
typedef struct wm_layout_result_t {
|
||||
wm_layout_item_t *items;
|
||||
size_t item_count;
|
||||
size_t item_capacity;
|
||||
} wm_layout_result_t;
|
||||
|
||||
typedef bool (*wm_layout_fn)(
|
||||
const wm_layout_ctx_t *ctx,
|
||||
wm_layout_result_t *out
|
||||
);
|
||||
|
||||
typedef struct wm_layout_slot_t {
|
||||
wm_layout_id_t id;
|
||||
/*
|
||||
* 布局稳定名称,用于:
|
||||
* - 配置引用
|
||||
* - 日志输出
|
||||
* - 调试输出
|
||||
*
|
||||
* 推荐使用可读名字,如 "tile"、"monocle"、"floating"。
|
||||
*/
|
||||
const char *name;
|
||||
/*
|
||||
* 布局符号,用于:
|
||||
* - 状态栏显示(应简短,如 "T", "M", "F")
|
||||
* - 紧凑场景下的快速识别
|
||||
*
|
||||
* 推荐使用 1-2 个字符的简短标识符。
|
||||
*/
|
||||
const char *symbol;
|
||||
wm_layout_fn fn;
|
||||
} wm_layout_slot_t;
|
||||
|
||||
typedef struct wm_layout_registry_t {
|
||||
wm_layout_slot_t *slots;
|
||||
size_t slot_count;
|
||||
size_t slot_capacity;
|
||||
} wm_layout_registry_t;
|
||||
|
||||
void wm_layout_result_init(wm_layout_result_t *result);
|
||||
void wm_layout_result_reset(wm_layout_result_t *result);
|
||||
void wm_layout_result_shutdown(wm_layout_result_t *result);
|
||||
|
||||
bool wm_layout_result_push(wm_layout_result_t *result, wm_layout_item_t item);
|
||||
|
||||
void wm_layout_registry_init(wm_layout_registry_t *registry);
|
||||
void wm_layout_registry_shutdown(wm_layout_registry_t *registry);
|
||||
size_t wm_layout_registry_count(const wm_layout_registry_t *registry);
|
||||
const wm_layout_slot_t *
|
||||
wm_layout_registry_at(const wm_layout_registry_t *registry, size_t index);
|
||||
|
||||
wm_layout_id_t wm_layout_register(
|
||||
wm_layout_registry_t *registry,
|
||||
const char *name,
|
||||
const char *symbol,
|
||||
wm_layout_fn fn
|
||||
);
|
||||
/*
|
||||
* layout registry 只允许在 bootstrap / 初始化阶段注册。
|
||||
* 运行时布局集合保持固定,只允许切换 workspace 当前选中的 layout_id。
|
||||
*/
|
||||
wm_layout_fn
|
||||
wm_layout_lookup(const wm_layout_registry_t *registry, wm_layout_id_t id);
|
||||
const wm_layout_slot_t *
|
||||
wm_layout_slot_get(const wm_layout_registry_t *registry, wm_layout_id_t id);
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_types.h"
|
||||
|
||||
typedef enum wm_dirty_flags_t {
|
||||
WM_DIRTY_NONE = 0,
|
||||
WM_DIRTY_STATE = 1u << 0,
|
||||
WM_DIRTY_LAYOUT = 1u << 1,
|
||||
WM_DIRTY_STACK = 1u << 2,
|
||||
WM_DIRTY_OUTPUT = 1u << 3,
|
||||
// 只影响窗口装饰,不改变 layout 结果;例如焦点切换导致边框颜色变化
|
||||
WM_DIRTY_DECORATION = 1u << 4,
|
||||
WM_DIRTY_RENDER = 1u << 5,
|
||||
} wm_dirty_flags_t;
|
||||
|
||||
typedef enum wm_effect_type_t {
|
||||
WM_EFFECT_NONE,
|
||||
WM_EFFECT_MAP_WINDOW,
|
||||
WM_EFFECT_UNMAP_WINDOW,
|
||||
WM_EFFECT_CONFIGURE_WINDOW,
|
||||
WM_EFFECT_FOCUS_WINDOW,
|
||||
WM_EFFECT_RESTACK_WINDOWS,
|
||||
WM_EFFECT_RENDER_OUTPUT,
|
||||
} wm_effect_type_t;
|
||||
|
||||
typedef struct wm_effect_t {
|
||||
wm_effect_type_t type;
|
||||
union {
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} map_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} unmap_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
wm_rect_t rect; // 最终外框矩形(包含边框)
|
||||
uint16_t border_width; // runtime 解析出的有效边框宽度
|
||||
wm_rgba32_t border_rgba; // runtime 解析出的有效边框颜色
|
||||
} configure_window;
|
||||
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
} focus_window;
|
||||
|
||||
struct {
|
||||
const wm_window_id_t *stack_order;
|
||||
size_t stack_count;
|
||||
} restack_windows;
|
||||
|
||||
struct {
|
||||
wm_output_id_t output_id;
|
||||
} render_output;
|
||||
} as;
|
||||
} wm_effect_t;
|
||||
|
||||
typedef struct wm_plan_t {
|
||||
wm_effect_t *effects;
|
||||
size_t effect_count;
|
||||
size_t effect_capacity;
|
||||
uint32_t dirty_flags;
|
||||
} wm_plan_t;
|
||||
|
||||
void wm_plan_init(wm_plan_t *plan);
|
||||
void wm_plan_reset(wm_plan_t *plan);
|
||||
void wm_plan_shutdown(wm_plan_t *plan);
|
||||
|
||||
bool wm_plan_push_effect(wm_plan_t *plan, wm_effect_t effect);
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "wm_binding.h"
|
||||
#include "wm_command.h"
|
||||
#include "wm_event.h"
|
||||
#include "wm_layout.h"
|
||||
#include "wm_plan.h"
|
||||
#include "wm_policy_config.h"
|
||||
#include "wm_state.h"
|
||||
|
||||
typedef struct wm_command_buffer_t {
|
||||
wm_command_t *items;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
} wm_command_buffer_t;
|
||||
|
||||
void wm_command_buffer_init(wm_command_buffer_t *buffer);
|
||||
void wm_command_buffer_reset(wm_command_buffer_t *buffer);
|
||||
void wm_command_buffer_shutdown(wm_command_buffer_t *buffer);
|
||||
|
||||
bool wm_command_buffer_push(wm_command_buffer_t *buffer, wm_command_t command);
|
||||
|
||||
bool wm_policy_route_event(
|
||||
const wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_key_binding_table_t *keybindings,
|
||||
const wm_pointer_binding_table_t *pointer_bindings,
|
||||
const wm_event_t *event,
|
||||
wm_command_buffer_t *out
|
||||
);
|
||||
|
||||
bool wm_policy_apply_command(
|
||||
wm_state_t *state,
|
||||
const wm_policy_config_t *policy,
|
||||
const wm_layout_registry_t *layouts,
|
||||
const wm_command_t *command,
|
||||
wm_plan_t *plan
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct wm_policy_config_t {
|
||||
bool focus_raises;
|
||||
bool pointer_enter_focuses_window;
|
||||
bool sticky_windows_participate_in_direction_focus;
|
||||
bool manage_sets_focus;
|
||||
bool switch_workspace_restores_last_focus;
|
||||
bool minimize_clears_focus;
|
||||
} wm_policy_config_t;
|
||||
|
||||
void wm_policy_config_init_default(wm_policy_config_t *config);
|
||||
@@ -1,136 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "wm_backend.h"
|
||||
#include "wm_binding.h"
|
||||
#include "wm_layout.h"
|
||||
#include "wm_plan.h"
|
||||
#include "wm_policy.h"
|
||||
#include "wm_policy_config.h"
|
||||
#include "wm_service.h"
|
||||
#include "wm_state.h"
|
||||
|
||||
typedef enum wm_interaction_mode_t {
|
||||
WM_INTERACTION_NONE,
|
||||
WM_INTERACTION_MOVE_FLOATING,
|
||||
WM_INTERACTION_RESIZE_FLOATING,
|
||||
} wm_interaction_mode_t;
|
||||
|
||||
typedef struct wm_interaction_state_t {
|
||||
wm_interaction_mode_t mode;
|
||||
wm_window_id_t window_id;
|
||||
wm_output_id_t origin_output_id;
|
||||
wm_point_t pointer_origin;
|
||||
wm_rect_t window_origin_rect;
|
||||
} wm_interaction_state_t;
|
||||
|
||||
typedef struct wm_border_palette_t {
|
||||
wm_rgba32_t normal_rgba;
|
||||
wm_rgba32_t focused_rgba;
|
||||
} wm_border_palette_t;
|
||||
|
||||
typedef struct wm_runtime_bootstrap_t {
|
||||
/*
|
||||
* 输出配置(启动时扫描显示器后填充)。
|
||||
* 这是 bootstrap 后固定的 output 集合。
|
||||
*/
|
||||
const wm_output_t *outputs;
|
||||
size_t output_count;
|
||||
|
||||
/*
|
||||
* 工作区配置(已绑定 output)
|
||||
* workspace.output_id 指向 outputs 中的某个输出
|
||||
* 这是 bootstrap 后固定的 workspace 集合。
|
||||
*/
|
||||
const wm_workspace_t *workspaces;
|
||||
size_t workspace_count;
|
||||
|
||||
/*
|
||||
* 布局算法注册。
|
||||
* layout registry 在 bootstrap 后固定;运行时只切换 workspace.layout_id。
|
||||
*/
|
||||
struct {
|
||||
wm_layout_fn fn;
|
||||
const char *name;
|
||||
const char *symbol;
|
||||
} *layouts;
|
||||
size_t layout_count;
|
||||
|
||||
/*
|
||||
* 策略配置
|
||||
*/
|
||||
wm_policy_config_t policy;
|
||||
|
||||
/*
|
||||
* 不可变键盘绑定表。
|
||||
* 当前草案把它视为 bootstrap 提供的只读视图;items 的生命周期必须覆盖
|
||||
* runtime。
|
||||
*/
|
||||
wm_key_binding_table_t keybindings;
|
||||
|
||||
/*
|
||||
* 不可变鼠标按键绑定表。
|
||||
* bar / status 点击属于核心外服务,因此这里只覆盖 root / window 目标。
|
||||
*/
|
||||
wm_pointer_binding_table_t pointer_bindings;
|
||||
|
||||
/*
|
||||
* 全局统一边框宽度。
|
||||
* 它不是窗口真状态。runtime 在提交 configure effect 时:
|
||||
* - fullscreen -> 0
|
||||
* - 其余模式 -> border_width
|
||||
*/
|
||||
uint16_t border_width;
|
||||
|
||||
/*
|
||||
* 全局边框颜色配置。
|
||||
* 它不是窗口真状态。runtime 在提交 configure effect 时根据当前焦点关系解析:
|
||||
* - 当前 workspace 的 focused window -> focused_rgba
|
||||
* - 其余窗口 -> normal_rgba
|
||||
*/
|
||||
wm_border_palette_t border_palette;
|
||||
|
||||
/*
|
||||
* Existing windows discovered during startup should be translated into these
|
||||
* commands instead of being injected into wm_state_t directly.
|
||||
*/
|
||||
const wm_command_t *initial_commands;
|
||||
size_t initial_command_count;
|
||||
} wm_runtime_bootstrap_t;
|
||||
|
||||
// 当前草案直接展示 runtime 结构,便于讨论;实现阶段可再封装
|
||||
typedef struct wm_runtime_t {
|
||||
bool running;
|
||||
|
||||
wm_state_t state;
|
||||
wm_plan_t plan;
|
||||
wm_command_buffer_t command_buffer;
|
||||
wm_layout_registry_t layouts;
|
||||
wm_policy_config_t policy;
|
||||
wm_key_binding_table_t keybindings;
|
||||
wm_pointer_binding_table_t pointer_bindings;
|
||||
uint16_t border_width;
|
||||
wm_border_palette_t border_palette;
|
||||
wm_interaction_state_t interaction;
|
||||
wm_backend_t backend;
|
||||
wm_service_registry_t services;
|
||||
} wm_runtime_t;
|
||||
|
||||
bool wm_runtime_init(
|
||||
wm_runtime_t *runtime,
|
||||
wm_backend_t backend,
|
||||
const wm_runtime_bootstrap_t *bootstrap
|
||||
);
|
||||
bool wm_runtime_register_service(wm_runtime_t *runtime, wm_service_t service);
|
||||
|
||||
/*
|
||||
* 处理单个事件,方便做无平台依赖的单元测试。
|
||||
* 元数据事件允许 runtime 直接更新 wm_window_t 中的元数据字段;
|
||||
* 控制状态变更仍经由 route_event() + apply_command()。
|
||||
*/
|
||||
bool wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event);
|
||||
|
||||
void wm_runtime_run(wm_runtime_t *runtime);
|
||||
void wm_runtime_stop(wm_runtime_t *runtime);
|
||||
void wm_runtime_shutdown(wm_runtime_t *runtime);
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "wm_state.h"
|
||||
|
||||
typedef enum wm_service_event_type_t {
|
||||
WM_SERVICE_EVENT_NONE,
|
||||
WM_SERVICE_EVENT_RUNTIME_STARTED,
|
||||
WM_SERVICE_EVENT_RUNTIME_STOPPING,
|
||||
WM_SERVICE_EVENT_WINDOW_METADATA_CHANGED,
|
||||
WM_SERVICE_EVENT_RENDER_OUTPUT,
|
||||
} wm_service_event_type_t;
|
||||
|
||||
typedef struct wm_service_event_t {
|
||||
wm_service_event_type_t type;
|
||||
union {
|
||||
struct {
|
||||
wm_window_id_t window_id;
|
||||
uint32_t changed_fields;
|
||||
} window_metadata_changed;
|
||||
|
||||
struct {
|
||||
wm_output_id_t output_id;
|
||||
} render_output;
|
||||
} as;
|
||||
} wm_service_event_t;
|
||||
|
||||
typedef struct wm_service_t wm_service_t;
|
||||
|
||||
typedef struct wm_service_api_t {
|
||||
bool (*init)(wm_service_t *service);
|
||||
void (*handle_event)(
|
||||
wm_service_t *service,
|
||||
const wm_service_event_t *event,
|
||||
const wm_state_t *state
|
||||
);
|
||||
void (*shutdown)(wm_service_t *service);
|
||||
} wm_service_api_t;
|
||||
|
||||
struct wm_service_t {
|
||||
const wm_service_api_t *api;
|
||||
void *impl;
|
||||
};
|
||||
|
||||
typedef struct wm_service_registry_t {
|
||||
wm_service_t *items;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
} wm_service_registry_t;
|
||||
|
||||
void wm_service_registry_init(wm_service_registry_t *registry);
|
||||
void wm_service_registry_shutdown(wm_service_registry_t *registry);
|
||||
|
||||
bool wm_service_registry_register(
|
||||
wm_service_registry_t *registry,
|
||||
wm_service_t service
|
||||
);
|
||||
void wm_service_registry_emit(
|
||||
wm_service_registry_t *registry,
|
||||
const wm_service_event_t *event,
|
||||
const wm_state_t *state
|
||||
);
|
||||
@@ -1,266 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "wm_types.h"
|
||||
|
||||
// ========== 核心实体定义 ==========
|
||||
|
||||
typedef struct wm_window_t {
|
||||
wm_window_id_t id;
|
||||
wm_workspace_id_t workspace_id;
|
||||
|
||||
// 几何模式
|
||||
wm_window_geometry_mode_t geometry_mode;
|
||||
bool floating;
|
||||
bool sticky;
|
||||
bool urgent;
|
||||
bool fixed_size;
|
||||
|
||||
// 几何信息(均为包含边框后的外框矩形)
|
||||
wm_rect_t float_rect; // floating 模式下记忆的外框矩形
|
||||
wm_rect_t frame_rect; // 当前最终外框矩形(由 layout 或 float_rect 解析)
|
||||
|
||||
// 元数据(核心算法不依赖,仅用于规则匹配和服务层展示)
|
||||
char *title;
|
||||
char *app_id;
|
||||
char *class_name;
|
||||
char *instance_name;
|
||||
|
||||
// 任务栏可见性提示
|
||||
bool skip_taskbar;
|
||||
} wm_window_t;
|
||||
|
||||
typedef struct wm_workspace_t {
|
||||
wm_workspace_id_t id;
|
||||
wm_output_id_t output_id; // 固定归属某个输出
|
||||
wm_layout_id_t layout_id; // 当前活动布局(可用布局列表中的一个)
|
||||
wm_window_id_t focused_window_id;
|
||||
|
||||
// 可用布局列表(启动时根据配置分配,之后固定不变)
|
||||
const wm_layout_id_t *available_layouts;
|
||||
size_t layout_count;
|
||||
|
||||
// 名称(核心算法不依赖,仅用于状态栏显示)
|
||||
char *name;
|
||||
} wm_workspace_t;
|
||||
|
||||
typedef struct wm_output_t {
|
||||
wm_output_id_t id;
|
||||
wm_rect_t geometry; // 输出完整几何(bootstrap 后固定)
|
||||
wm_rect_t workarea; // 可用区域(排除面板等,bootstrap 后固定)
|
||||
wm_workspace_id_t current_workspace_id;
|
||||
} wm_output_t;
|
||||
|
||||
// ========== 全局状态容器 ==========
|
||||
|
||||
typedef struct wm_state_t {
|
||||
// 头文件级草案当前直接公开字段,便于讨论;实现阶段可再封装
|
||||
wm_workspace_t *workspaces;
|
||||
size_t workspace_count;
|
||||
|
||||
wm_output_t *outputs;
|
||||
size_t output_count;
|
||||
|
||||
wm_window_t *windows;
|
||||
size_t window_count;
|
||||
size_t window_capacity;
|
||||
|
||||
// 公开的字段
|
||||
wm_window_id_t *stack_order;
|
||||
size_t stack_count;
|
||||
size_t stack_capacity;
|
||||
|
||||
uint64_t generation;
|
||||
bool initialized;
|
||||
} wm_state_t;
|
||||
|
||||
// ========== API 函数 ==========
|
||||
|
||||
// 初始化/清理
|
||||
// workspace/output 集合大小在 init 时一次性确定,之后不再变化。
|
||||
void wm_state_init(
|
||||
wm_state_t *state,
|
||||
size_t workspace_count,
|
||||
size_t output_count
|
||||
);
|
||||
void wm_state_shutdown(wm_state_t *state);
|
||||
|
||||
// ========== Workspace 访问 ==========
|
||||
|
||||
wm_workspace_t *wm_state_workspace(wm_state_t *state, wm_workspace_id_t id);
|
||||
wm_workspace_t *wm_state_workspace_at(wm_state_t *state, size_t index);
|
||||
size_t wm_state_workspace_count(const wm_state_t *state);
|
||||
bool wm_state_workspace_valid(wm_state_t *state, wm_workspace_id_t id);
|
||||
void wm_state_workspace_set_focused_window(
|
||||
wm_state_t *state,
|
||||
wm_workspace_id_t workspace_id,
|
||||
wm_window_id_t window_id
|
||||
);
|
||||
|
||||
// 查询某个 output 的所有 workspace(只读视图)
|
||||
size_t wm_state_workspace_get_by_output(
|
||||
const wm_state_t *state,
|
||||
wm_output_id_t output_id,
|
||||
wm_workspace_t **out_workspaces
|
||||
);
|
||||
|
||||
// ========== Workspace 布局操作 ==========
|
||||
|
||||
/*
|
||||
* 设置 workspace 的可用布局列表。
|
||||
* 仅允许在 bootstrap / 初始化阶段调用;运行时不应修改。
|
||||
*/
|
||||
void wm_workspace_set_layouts(
|
||||
wm_workspace_t *workspace,
|
||||
const wm_layout_id_t *layouts,
|
||||
size_t count
|
||||
);
|
||||
|
||||
/*
|
||||
* 获取 workspace 的可用布局列表。
|
||||
* 该列表在 bootstrap 完成后保持不变。
|
||||
*/
|
||||
const wm_layout_id_t *
|
||||
wm_workspace_get_layouts(const wm_workspace_t *workspace, size_t *out_count);
|
||||
|
||||
/*
|
||||
* 切换到下一个布局(循环)
|
||||
*/
|
||||
bool wm_workspace_cycle_layout(wm_workspace_t *workspace);
|
||||
|
||||
/*
|
||||
* 切换到指定索引的布局
|
||||
*/
|
||||
bool wm_workspace_set_layout_by_index(wm_workspace_t *workspace, size_t index);
|
||||
|
||||
/*
|
||||
* 切换到指定 ID 的布局(如果该布局在可用列表中)
|
||||
*/
|
||||
bool wm_workspace_set_layout_by_id(
|
||||
wm_workspace_t *workspace,
|
||||
wm_layout_id_t id
|
||||
);
|
||||
|
||||
// ========== Output 访问 ==========
|
||||
|
||||
wm_output_t *wm_state_output(wm_state_t *state, wm_output_id_t id);
|
||||
wm_output_t *wm_state_output_at(wm_state_t *state, size_t index);
|
||||
size_t wm_state_output_count(const wm_state_t *state);
|
||||
bool wm_state_output_valid(wm_state_t *state, wm_output_id_t id);
|
||||
/*
|
||||
* 切换某个 output 当前显示的 workspace。
|
||||
* 这不会修改 output 集合本身,也不会改变 workspace->output_id 绑定。
|
||||
* 调用方必须保证目标 workspace 固定归属于该 output。
|
||||
*/
|
||||
void wm_state_output_set_current_workspace(
|
||||
wm_state_t *state,
|
||||
wm_output_id_t output_id,
|
||||
wm_workspace_id_t workspace_id
|
||||
);
|
||||
|
||||
// ========== Window 操作 ==========
|
||||
|
||||
wm_window_t *wm_state_window_add(wm_state_t *state, wm_window_id_t id);
|
||||
wm_window_t *wm_state_window_get(wm_state_t *state, wm_window_id_t id);
|
||||
wm_window_t *wm_state_window_at(wm_state_t *state, size_t index);
|
||||
void wm_state_window_remove(wm_state_t *state, wm_window_id_t id);
|
||||
size_t wm_state_window_count(const wm_state_t *state);
|
||||
void wm_state_window_set_workspace(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
wm_workspace_id_t workspace_id
|
||||
);
|
||||
void wm_state_window_set_geometry_mode(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
wm_window_geometry_mode_t geometry_mode
|
||||
);
|
||||
void wm_state_window_set_floating(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
bool floating
|
||||
);
|
||||
void wm_state_window_set_sticky(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
bool sticky
|
||||
);
|
||||
void wm_state_window_set_urgent(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
bool urgent
|
||||
);
|
||||
void wm_state_window_set_fixed_size(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
bool fixed_size
|
||||
);
|
||||
void wm_state_window_set_skip_taskbar(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
bool skip_taskbar
|
||||
);
|
||||
void wm_state_window_set_float_rect(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
wm_rect_t float_rect
|
||||
);
|
||||
void wm_state_window_set_frame_rect(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
wm_rect_t frame_rect
|
||||
);
|
||||
bool wm_state_window_set_title(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
const char *title
|
||||
);
|
||||
bool wm_state_window_set_app_id(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
const char *app_id
|
||||
);
|
||||
bool wm_state_window_set_class(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
const char *class_name
|
||||
);
|
||||
bool wm_state_window_set_instance(
|
||||
wm_state_t *state,
|
||||
wm_window_id_t window_id,
|
||||
const char *instance_name
|
||||
);
|
||||
|
||||
// ========== 查询辅助函数 ==========
|
||||
|
||||
/*
|
||||
* 判断工作区是否可见(某个 output 正在显示它)
|
||||
*/
|
||||
bool wm_state_workspace_is_visible(
|
||||
const wm_state_t *state,
|
||||
wm_workspace_id_t workspace_id
|
||||
);
|
||||
|
||||
/*
|
||||
* 判断窗口是否可见
|
||||
* 窗口可见 iff:
|
||||
* 1. 若窗口是 sticky,则只要求窗口未最小化
|
||||
* 2. 否则,窗口的 workspace 固定归属某个 output
|
||||
* 3. 且该 output 当前正显示这个 workspace
|
||||
*/
|
||||
bool wm_state_window_should_be_visible(
|
||||
const wm_state_t *state,
|
||||
const wm_window_t *window
|
||||
);
|
||||
|
||||
/*
|
||||
* 判断窗口是否在特定 output 上可见
|
||||
*/
|
||||
bool wm_state_window_should_be_visible_on_output(
|
||||
const wm_state_t *state,
|
||||
const wm_window_t *window,
|
||||
const wm_output_t *output
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// ID 类型定义
|
||||
// workspace/output/layout 在启动时分配固定数量,运行时不再增删
|
||||
// window 由后端管理,使用后端原生 ID
|
||||
typedef uint32_t wm_window_id_t;
|
||||
typedef uint16_t wm_workspace_id_t;
|
||||
typedef uint16_t wm_output_id_t;
|
||||
typedef uint32_t wm_layout_id_t;
|
||||
/*
|
||||
* backend 归一化后的毫秒时间值。
|
||||
* 它对齐 X11 / Wayland 常见的 32-bit ms 时间域,用于事件排序和短时差计算;
|
||||
* 不是 wall-clock / Unix epoch,基准也不保证可见,因此允许回绕。
|
||||
*/
|
||||
typedef uint32_t wm_time_ms_t;
|
||||
/*
|
||||
* 逻辑键编码,使用 xkb keysym 值空间。
|
||||
* 它表示“当前 keymap 下,这次按键对应的逻辑主键”,而不是平台原始扫描码。
|
||||
*/
|
||||
typedef uint32_t wm_keysym_t;
|
||||
typedef uint32_t wm_modifier_mask_t;
|
||||
/*
|
||||
* 通用 32-bit RGBA 颜色值,编码为 0xRRGGBBAA。
|
||||
* backend 若不支持 alpha,可忽略最低 8 bit 或在提交平台 effect 时自行量化。
|
||||
*/
|
||||
typedef uint32_t wm_rgba32_t;
|
||||
|
||||
// 无效 ID 标记
|
||||
#define WM_WINDOW_ID_INVALID ((wm_window_id_t)0)
|
||||
#define WM_WORKSPACE_ID_INVALID ((wm_workspace_id_t) - 1)
|
||||
#define WM_OUTPUT_ID_INVALID ((wm_output_id_t) - 1)
|
||||
#define WM_LAYOUT_ID_INVALID ((wm_layout_id_t) - 1)
|
||||
#define WM_KEYSYM_INVALID ((wm_keysym_t)0)
|
||||
|
||||
/*
|
||||
* 归一化后的修饰键掩码:
|
||||
* - backend 应在产出 wm_event_t 之前合并 left/right 变体
|
||||
* - CapsLock / NumLock 这类锁定位不应进入该掩码
|
||||
*/
|
||||
typedef enum wm_modifier_bit_t {
|
||||
WM_MOD_NONE = 0,
|
||||
WM_MOD_SHIFT = 1u << 0,
|
||||
WM_MOD_CTRL = 1u << 1,
|
||||
WM_MOD_ALT = 1u << 2,
|
||||
WM_MOD_SUPER = 1u << 3,
|
||||
} wm_modifier_bit_t;
|
||||
|
||||
/*
|
||||
* 归一化后的指针按键语义。
|
||||
* backend 应把平台原始按钮码映射到这些稳定枚举值;
|
||||
* 若需要保留平台原始值,可放在事件里的 raw_button 字段。
|
||||
*/
|
||||
typedef enum wm_button_t {
|
||||
WM_BUTTON_NONE = 0,
|
||||
WM_BUTTON_LEFT,
|
||||
WM_BUTTON_MIDDLE,
|
||||
WM_BUTTON_RIGHT,
|
||||
WM_BUTTON_WHEEL_UP,
|
||||
WM_BUTTON_WHEEL_DOWN,
|
||||
WM_BUTTON_WHEEL_LEFT,
|
||||
WM_BUTTON_WHEEL_RIGHT,
|
||||
WM_BUTTON_BACK,
|
||||
WM_BUTTON_FORWARD,
|
||||
} wm_button_t;
|
||||
|
||||
typedef struct wm_point_t {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
} wm_point_t;
|
||||
|
||||
typedef struct wm_rect_t {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
} wm_rect_t;
|
||||
|
||||
/*
|
||||
* 核心统一使用 geometry_mode 表示窗口的显示几何模式。
|
||||
* 其中 WM_GEOMETRY_MINIMIZED 覆盖所有“被最小化/被图标化”的不可见状态:
|
||||
* - 在 X11 上,它对应 ICCCM 的 IconicState
|
||||
* - 来自 WM_CHANGE_STATE(Iconic) 的请求也应折叠到这个模式
|
||||
*
|
||||
* 最小核心不单独暴露 "iconic" 枚举,避免与 minimized 形成两套并行语义。
|
||||
*/
|
||||
typedef enum wm_window_geometry_mode_t {
|
||||
WM_GEOMETRY_NORMAL,
|
||||
WM_GEOMETRY_MAXIMIZED,
|
||||
WM_GEOMETRY_FULLSCREEN,
|
||||
WM_GEOMETRY_MINIMIZED,
|
||||
} wm_window_geometry_mode_t;
|
||||
|
||||
typedef enum wm_focus_direction_t {
|
||||
WM_FOCUS_PREV,
|
||||
WM_FOCUS_NEXT,
|
||||
} wm_focus_direction_t;
|
||||
|
||||
typedef enum wm_cross_output_policy_t {
|
||||
WM_CROSS_OUTPUT_KEEP_WORKSPACE,
|
||||
WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE,
|
||||
} wm_cross_output_policy_t;
|
||||
@@ -1,136 +0,0 @@
|
||||
#+title: ZDWM 模块分层与依赖方向
|
||||
#+OPTIONS: toc:2
|
||||
|
||||
* 动机
|
||||
|
||||
随着模块增加,跨模块的"契约"和"共享数据结构"开始散落:有的进了 =core/=(和窗口管理逻辑混在一起),有的不得不靠依赖倒置塞进消费方目录(如 tray 契约放 =bar/= 导致 backend 反向依赖 bar)。结果是 core 职责不清、=bar ↔ backend= 出现交叉依赖、依赖方向不再单向。
|
||||
|
||||
本文档定义一套分层约定,把"跨模块共享的东西"从具体实现里抽出来,集中到两个目录,使依赖方向回到干净的单向。
|
||||
|
||||
* 核心标准:按"谁直接消费类型"归类
|
||||
|
||||
判断一个跨模块的东西进哪个目录,不看它"属于哪个模块",也不只看"是声明还是实现",而是看**它的类型被哪些层直接消费**:
|
||||
|
||||
- 类型被**两个或以上对等的实现层**直接消费 → 类型放 =interface/=(跨模块声明)
|
||||
- 只有**操作函数**被多个实现层共用、且这些层不该互相依赖 → 操作放 =common/=
|
||||
- 类型 + 操作都只被一个实现层(及其依赖者)用 → 整个 ADT 放 =common/=,或就近放该实现层
|
||||
|
||||
关键推论:**只需消费"类型"的层,不该被迫背上"操作函数"的依赖**。这是把类型和操作分到两个目录的根本理由。
|
||||
|
||||
* 目录的定位
|
||||
|
||||
先明确三层关系:=base/= 是**项目无关**的通用底层(任何 C 项目都能直接用),=interface/= 和 =common/= 是**项目业务相关**的通用内容。后两者依赖前者(业务依赖基础设施)。
|
||||
|
||||
** =src/base/= :项目无关的通用底层,零业务依赖
|
||||
|
||||
放任何 C 项目都能直接用的纯工具(=memory= / =array= / =macros= / =color= / =log= / =time= / =process=)。不依赖 interface/common/任何实现层——它根本不知道 ZDWM 的业务。=interface/= 和 =common/= 都可以依赖它。
|
||||
|
||||
** =src/interface/= :跨模块共享的【声明】,零 .c
|
||||
|
||||
只放类型定义、多态接口的函数声明。绝对不放假装、零实现代码。被所有实现层依赖,自身不依赖任何实现层(可依赖 =base/= 的纯类型)。
|
||||
|
||||
** =src/common/= :被多实现层共用的【操作函数】,.h + .c
|
||||
|
||||
放自包含的 ADT 操作(构造/清理/查询)。操作只依赖 =interface/= 里的类型(和 =base/= 工具),不依赖任何实现层的内部状态。被"需要这些操作的层"依赖。
|
||||
|
||||
注意:=common/= 不是"什么都往里塞的杂物间"。只有当操作被**多个不该互相依赖的实现层**共用时,才进 =common/=;只被单一实现层用的操作,就近放该层。
|
||||
|
||||
* 依赖方向
|
||||
|
||||
#+begin_example
|
||||
base/ ← 项目无关的通用底层(任何 C 项目可用),零业务依赖
|
||||
↑
|
||||
interface/ ← 零业务依赖(纯类型声明,含 backend 抽象接口);可依赖 base
|
||||
↑
|
||||
common/ ← 项目业务通用的共享 ADT 操作;依赖 interface + base
|
||||
↑
|
||||
core/ runtime/ backend/ bar/ ← 实现层,都依赖 common + interface(+ base)
|
||||
#+end_example
|
||||
|
||||
注意:没有实现层"只依赖 interface"——所有实现层都用 =common/= 的业务通用操作。interface 的纯净性约束的是 interface *内部*(含 backend 抽象接口不依赖 interface 之外的任何东西),不是约束某个实现层的依赖深度。
|
||||
|
||||
不变量:
|
||||
- =base/= 不依赖任何业务层(interface/common/实现层),是项目无关的纯工具。
|
||||
- =interface/=(含 backend 抽象接口)不依赖 =interface/= 之外的任何业务内容;可依赖 =base/= 的纯类型(业务层依赖基础设施,合理)。
|
||||
- =common/= 只依赖 =interface/= 和 =base/=,不依赖任何实现层。
|
||||
- 所有实现层(core/runtime/backend/bar)都依赖 =common/= + =interface/=(+ =base/=);它们之间不直接相互依赖跨模块契约,都经由 =interface/= 或 =common/=。
|
||||
- 因此 =bar ↔ backend= 这类对等层之间不再有交叉依赖:都通过 =interface/tray.h= 通信。
|
||||
|
||||
* 判定示例:effect / plan / event / listeners
|
||||
|
||||
用上面的标准,逐个判定现有跨模块类型:
|
||||
|
||||
** =effect_t= → interface(类型),无 .c
|
||||
|
||||
消费方:core(policy/plan 产生 =effect_t=)+ backend(执行 =effect_t=)。两个对等层直接消费类型,所以类型进 =interface/effect.h=。=effect_t= 是纯数据,构造它的活(=plan_push_*=)属于 plan 的操作,所以 effect 在 interface 里只有类型、没有 .c。
|
||||
|
||||
#+begin_src c
|
||||
/* backend 只消费展开后的 effect 数组,从不碰 plan_t */
|
||||
bool backend_apply_effect(backend_t *backend, const effect_t *effects, size_t effect_count);
|
||||
#+end_src
|
||||
|
||||
** =plan_t= → 留 core(完整 ADT:类型 + 操作)
|
||||
|
||||
消费方:只 core(policy 产生、收集 effect)和 runtime(提交后 reset/cleanup)。两者之间是 *runtime→core 的天然单向依赖*(runtime 是桥接层,本就依赖 core 的 policy/state 等)。把 =plan_t= 放 =core/plan.h= + =core/plan.c=,runtime 正向 include 即可,不破坏任何方向——所以它*不必*进 common。
|
||||
|
||||
这把 common 的门槛精确化:common 是为"对等、无天然依赖"的消费者中立化(见下文 =listeners_t=);若消费者之间已有单向依赖(如 plan 的 core+runtime),共享操作放被依赖方(core)即可,不必上浮 common。
|
||||
|
||||
** =event_t= → interface(类型)+ common(操作)
|
||||
|
||||
=event_t= 类型进 =interface/event.h= 的关键理由:interface 内的 =backend.h= 契约直接 *#include 了 event.h*——=backend_poll_event= / =backend_next_event= 的参数是 =event_t *=,且契约注释讨论 event 的所有权与 =event_cleanup= 语义。一份完整的后端契约要让读者看到 event_t 全貌(字段、联合、生命周期责任),而非甩一个前向声明。所以 event_t 必须在 interface:若放 common,=backend.h= 要么退化为前向声明(契约不完整),要么 interface 反向依赖 common(破坏零依赖)。
|
||||
|
||||
=event_reset= / =event_cleanup= 这些操作被 backend 和 runtime 共用,两者对等、不该互相依赖 → 操作落 =common/event.h= + =common/event.c=。于是 event 是"类型在 interface、操作在 common"的典型。
|
||||
|
||||
** =listeners_t= → common(类型 + 维护)+ core(notify)
|
||||
|
||||
=listeners_t= 类型被 bar(订阅)、core(发布)、runtime(生命周期)消费,但 **backend 完全不碰它**。判定关键:它的消费者(bar/config/runtime)都*既用类型又用维护操作*(=add_*= / =cleanup=),没有哪个层"只需类型、不需操作"——按核心推论(只有"只需消费类型"的层才迫使类型单独上提 interface),类型不必进 interface。与 =event_t= 的对照就在这里:=event_t= 因被 interface 内的 =backend.h= 契约需要完整定义而必须留 interface;=listeners_t= 则*不被 interface 内任何契约依赖*(=backend.h= 完全不碰它),所以能整个进 common:
|
||||
|
||||
- 类型 =listeners_t= + 维护操作 =add_*= / =cleanup=:自包含容器操作,被 bar/config/runtime 多个对等层共用 → 整个 ADT 进 =common/listeners.{h,c}=。
|
||||
- =notify_*=(触发):单消费者(只 core/runtime 发布侧调用),且依赖 core 内部(把 =state_t= 翻译成 =zdwm_window_t= 等 DTO)→ 声明 + 实现都留 =core/listeners=。
|
||||
|
||||
真正的跨层契约(回调签名 =zdwm_window_added= 等 + DTO)已在 =include/zdwm/listeners.h=(比 interface 更公开的那层),=listeners_t= 只是这些回调的容器,所以它整个待在 common 自洽,不必单独占一个 interface 头。
|
||||
|
||||
* 汇总表
|
||||
|
||||
| 东西 | 类型在哪 | 操作在哪 | 判定理由 |
|
||||
|------------------------------+-----------------------+-------------------------------------------+--------------------------------------------------------------------------|
|
||||
| =effect_t= | =interface/effect.h= | (无,构造归 plan 操作) | 类型跨模块(core 产 + backend 执行),纯数据 |
|
||||
| =plan_t= | =core/plan.h= | =core/plan.c= | 消费者 runtime→core 单向,放 core 不破坏方向,不必 common |
|
||||
| =event_t= | =interface/event.h= | =common/event.h= + =common/event.c= | 类型跨模块;操作对等共用 |
|
||||
| 后端接口(=backend_t= 等) | =interface/backend.h= | =backend/x11/*.c= | 多态接口:声明在 interface,实现在具体后端 |
|
||||
| tray 契约(=tray_t= 等) | =interface/tray.h= | =backend/x11/tray.c= | 多态接口:bar 用、backend 实现,声明在 interface 消除 =bar↔backend= 交叉 |
|
||||
| 通知协议(listeners) | =common/listeners.h= | =common/listeners.c= + =core/listeners.c= | 消费者 bar↔core 对等,必须 common 中立化;notify 依赖 core 留 core |
|
||||
| 基础类型(=window_id_t= 等) | =interface/types.h= | (按需) | 所有层共用 |
|
||||
|
||||
* 多态接口 vs 共享 ADT:.c 归属的不同
|
||||
|
||||
=interface/= 里有两类声明,它们的 .c 归属不同,务必区分:
|
||||
|
||||
- **多态接口**(=backend.h=、=tray.h=):声明一组"由谁来实现"的函数,实现依赖具体实现层(xcb / 未来 wayland)。这类 .c **必须在实现层**(=backend/x11/=、=core/=),不能在 interface——否则 interface 就"知道"了具体实现。
|
||||
- **共享 ADT 的操作**(event 的操作):实现自包含、不依赖任何实现层。这类 .c 进 =common/=。
|
||||
|
||||
换句话说:=interface/= 永远零 .c。多态接口的 .c 在实现层,共享操作的 .c 在 =common/=。
|
||||
|
||||
* 和 include/zdwm/ 的区别
|
||||
|
||||
- =include/zdwm/= 是**对外公共 API**(给用户配置、插件使用)。
|
||||
- =src/interface/= 是**内部跨模块契约**(给各子系统互相通信)。
|
||||
|
||||
两者职责不同,不合并。像 =backend.h=、=event.h=、=effect.h= 是内部的,不该进 =include/zdwm/= 暴露给外部。
|
||||
|
||||
* 迁移指引
|
||||
|
||||
把现有代码按本分层整理时,建议顺序:
|
||||
|
||||
1. 建 =src/interface/= 和 =src/common/= 目录,加入 CMakeLists。
|
||||
2. 先迁移"纯类型、零依赖"的声明进 =interface/=:=types.h=、=effect.h=(从 =core/plan.h= 抽出 =effect_t=)、=event.h= 的类型部分。
|
||||
3. 多态接口声明迁移:=backend.h= 进 =interface/=(=.c= 留原处)。=listeners= 走共享 ADT、不进 interface:=listeners_t= 类型 + =add_*= / =cleanup= 整体进 =common/listeners.{h,c}=,=notify_*= 留 =core/listeners=(详见上方判定示例)。
|
||||
4. 共享 ADT 迁移:=event.h= 操作部分 + =event.c= 进 =common/=。(=plan= 留 core:消费者 runtime→core 单向,不必上浮。)
|
||||
5. 更新全项目 include 路径(="core/types.h"= → ="interface/types.h"= 等)。
|
||||
6. 每步后编译验证,确保依赖方向单向、无循环。
|
||||
|
||||
注意边界:遇到"看起来该进 interface、但其类型依赖还在 core"的情况(如某个 event 子结构引用了 =core/window.h= 的类型),要么把那个被引用的类型也提到 =interface/=,要么承认它暂不进 interface——**进 interface 的前提是它的所有依赖都在 interface**,不能硬塞。
|
||||
|
||||
* tray 的落点
|
||||
|
||||
tray 的契约(=tray_t= / =tray_api_t=)是 bar 与 backend 之间的多态接口,按本分层落 =interface/tray.h=(声明),实现在 =backend/x11/tray.c=。这样 bar 和 backend 都只依赖 =interface/tray.h=,彻底消除 =bar ↔ backend= 交叉依赖——这正是引入这套分层的最初动机之一。详见 [[file:tray-design.org][tray-design.org]]。
|
||||
@@ -1,256 +0,0 @@
|
||||
#+title: ZDWM Tray 系统托盘设计
|
||||
#+OPTIONS: toc:2
|
||||
|
||||
* 背景与动机
|
||||
|
||||
ZDWM 当前没有系统托盘(system tray / systray)功能。本文档描述如何在 bar 中实现 X11 系统托盘,让外部应用(输入法、音量、蓝牙、网络等)的托盘图标显示在状态栏右侧并正常交互,同时保持现有前后端分离架构,为未来 Wayland 后端预留扩展空间。
|
||||
|
||||
* 设计目标与约束
|
||||
|
||||
** 前后端分离
|
||||
|
||||
tray 协议(=_NET_SYSTEM_TRAY= + XEMBED)必须*闭环在后端内部*。core 的 =event_t= / =policy= / =plan= 完全不感知 tray——tray 不产生 =event_t=、不进 policy 路由、不进 plan/effect。
|
||||
|
||||
这与现有架构的三条解耦通道一致:
|
||||
- =backend -> core=:后端把平台原始事件归一化为 =event_t=
|
||||
- =core -> backend=:core 产出 =effect_t=,后端执行
|
||||
- =runtime= 作为唯一桥接点:bar 模块本身拿不到 backend,所有平台访问经 runtime 中转
|
||||
|
||||
** Wayland 预留
|
||||
|
||||
bar 与 runtime 层必须后端无关、零 xcb。未来加 Wayland 后端时,只需新增一个后端实现——Wayland 用 StatusNotifierItem(SNI)over D-Bus,icon 是像素数据而非真实窗口,由前端 cairo 绘制。只要后端接口形状不变,上层(bar/runtime)零改动。
|
||||
|
||||
** tray 是 bar 的附属组件
|
||||
|
||||
没有 bar 就没有 tray。tray 的生命周期随 bar 窗口,*不*作为独立公共接口(不进 =include/zdwm/=)。tray 能力内嵌在 bar 窗口的创建/销毁里。
|
||||
|
||||
* 方案选型:嵌入式容器子窗口(A')
|
||||
|
||||
X11 系统托盘协议只要求:tray 创建一个 window 成为 =_NET_SYSTEM_TRAY_S<n>= 的 selection owner,客户端向 owner 发 =_NET_SYSTEM_TRAY_REQUEST_DOCK= 请求,tray 用 XEMBED reparent icon。协议本身*不*规定 icon reparent 到 bar 窗口还是独立窗口。
|
||||
|
||||
** 为何不用独立 top-level 窗口
|
||||
|
||||
完全独立的 top-level tray 窗口(如 stalonetray)是为*没有状态栏*的环境设计的。ZDWM 已有 bar,用独立窗口会引入"两个窗口的几何/对齐同步"问题(tray 窗口要贴 bar 右端、随 icon 数动态 resize、跟随 bar 移动),且无功能收益。
|
||||
|
||||
** 为何用专门的容器子窗口
|
||||
|
||||
虽然直接 reparent 到 bar 的 cairo 绘制窗口技术上可行(bar 已是 ARGB,子窗口叠加在 cairo 内容之上),但用一个*专门的 tray 容器子窗口*做 selection owner 更正统、更干净:
|
||||
|
||||
1. *职责分离*:bar 窗口专注 cairo 绘制,tray 容器专注 XEMBED / icon 管理。dock 请求和 =_XEMBED= 消息定向到容器,不进 bar 的 =handle_client_message=。
|
||||
2. *事件隔离*:tray 协议事件在容器窗口闭环,bar 的窗口管理事件流零污染。
|
||||
3. *视觉不变*:容器是 bar 窗口的子窗口,位于 tray region 内,icon 跟随 bar 移动/resize(X 子窗口语义),不占额外屏幕空间。
|
||||
|
||||
* 后端接口设计
|
||||
|
||||
tray 契约独立成头 =interface/tray.h= —— 落 interface 层:不放 core(tray 与 core 无关),也不放 bar(backend 也要实现它)。bar 和 backend 作为两个对等实现层,都依赖 =interface/tray.h= 来解耦——这正是分层消除 bar↔backend 交叉依赖的落点(详见 [[file:module-layering.org][module-layering.org]] 的 tray 落点)。它只依赖 =interface/types.h=(=window_id_t=),不含 =backend_t=,所以 bar include 它而不碰 =interface/backend.h= 的内部。=interface/backend.h= 反过来 include =interface/tray.h=,让 =backend_bar_window_t= 内嵌 =tray_t=。
|
||||
|
||||
#+begin_src c
|
||||
typedef void (*tray_icons_change_cb_t)(void *user_data);
|
||||
|
||||
typedef struct tray_api_t {
|
||||
/* 返回 tray 当前挂载的 bar window_id;invalid 表示无 tray 挂载 */
|
||||
window_id_t (*host_window)(void *handle);
|
||||
/* 当前 icon 数量 */
|
||||
size_t (*icon_count)(void *handle);
|
||||
/* icon 边长(= bar height),bar 据此算 cell 宽度 */
|
||||
int32_t (*icon_size)(void *handle);
|
||||
/* 把容器 + icon 摆到 region 起点 x */
|
||||
void (*place)(void *handle, int32_t x);
|
||||
/* 注册 icon 数量变化回调 */
|
||||
void (*set_listener)(void *handle,
|
||||
tray_icons_change_cb_t cb,
|
||||
void *user_data);
|
||||
} tray_api_t;
|
||||
|
||||
typedef struct tray_t {
|
||||
tray_api_t api; /* 值类型,后端总填充,一直可用 */
|
||||
void *handle; /* 指向 backend(bar 不知其类型) */
|
||||
} tray_t;
|
||||
|
||||
typedef struct backend_bar_window_t {
|
||||
window_id_t window_id;
|
||||
cairo_t *cr;
|
||||
tray_t tray;
|
||||
} backend_bar_window_t;
|
||||
|
||||
/* 扩展:加 enable_tray;icon_size 直接用 geometry.height */
|
||||
backend_bar_window_t backend_create_bar_window(
|
||||
backend_t *backend, rect_t geometry, uint32_t bg_pixel, bool enable_tray);
|
||||
#+end_src
|
||||
|
||||
** 关键设计点
|
||||
|
||||
- *api 是值类型*:后端用一个全局 =static const= vtable 填充,保证一直可用,bar 不需判 NULL。这与项目现有 =zdwm_bar_item_type_t=(纯 vtable)+ =zdwm_bar_item_t=(实例持有 api + state)的模式一致。
|
||||
- *handle 总有效*:实际指向 =backend_t*=(tray api 函数内用 =backend->conn=、=backend->tray= host),对 bar 透明(=void*=)。有无 tray 统一由 =host_window()= 返回值判断(invalid = 无),不靠 api 是否 NULL,避免"先判 api、再判 host_window"两个判断点。
|
||||
- *icon_size 来自 handle*(单一数据源):后端 configure icon 用的尺寸 = bar 查布局用的尺寸,杜绝两边各算导致 icon 错位/溢出。这比在 =tray_t= 冗余存一份快照更纯粹。
|
||||
- *生命周期随 bar/backend*:=backend_create_bar_window(enable_tray=true)= 时创建/迁移 host;=backend_destroy= 释放。无 =tray_host_create/destroy= 独立入口,体现"tray 是 bar 附属"。
|
||||
|
||||
** 挂载语义
|
||||
|
||||
X11 的 =_NET_SYSTEM_TRAY_S0= selection owner *整个 display 只能有一个*,但 =backend_create_bar_window= 是 per-output 调用的。处理方式:
|
||||
|
||||
- tray host 是后端单例(内联在 =backend_t=)。
|
||||
- 每次传 =enable_tray=true= 的 =create_bar_window= 把容器 reparent 到本次 bar 窗口,最终挂在*最后一个* true 的 output 上。
|
||||
- =host_window()= 返回当前挂载的 bar window_id,bar 据此决定哪个 output 渲染 tray。
|
||||
|
||||
** 为何 set_listener 不配 remove_listener
|
||||
|
||||
tray icon 变化的唯一消费者是 bar 的 tray item,且其生命周期 = host 生命周期(=backend_destroy= 时自然消失)。与 core 的 =listeners= 模块一致(listener 跟宿主容器走,靠容器销毁回收),不引入 =remove_listener=。
|
||||
|
||||
* bar item 框架扩展
|
||||
|
||||
tray 作为 right-side 的普通 item,复用 =zdwm_bar_item_type_t= 框架。为支持 tray 的两个本质差异(cell 无文本 + 需要在 layout 后把结果同步到后端真实窗口),对框架做最小*通用*扩展——不为 tray 特例化。
|
||||
|
||||
** cell_api 全包 cell 相关(不引入 item_api)
|
||||
|
||||
=set_cell_count= / =get_cell_count= 本质是 *cell 集合的管理*(类比 =std::vector= 的 =size()/resize()= 与 =operator[]= 同属 vector),留在 =zdwm_bar_cell_api_t=。
|
||||
|
||||
新增两个 cell 级接口:
|
||||
|
||||
#+begin_src c
|
||||
/* 固定宽度:>0 = 固定宽度,0 = 走文本测量(零值默认),<0 = 隐藏 */
|
||||
void cell_set_fixed_width(zdwm_bar_item_t *, size_t index, int32_t width);
|
||||
/* 单个 cell 的 region(layout 后的坐标) */
|
||||
bar_x_region_t cell_get_region(zdwm_bar_item_t *, size_t index);
|
||||
#+end_src
|
||||
|
||||
=bar_cell_t= 加 =int32_t fixed_width=,默认 =0=(走文本测量)。把零值留给最常见的默认行为(测量),=p_clear= 清零后新 cell 天然正确,=set_cell_count= 无需补初始化;=-1= 当"隐藏"哨兵(负数宽度本就无意义),与 =0=(测量)明确区分,没有歧义。
|
||||
|
||||
=bar_output_layout= 改一处:
|
||||
|
||||
#+begin_src c
|
||||
int32_t width;
|
||||
if (cell->fixed_width > 0) width = cell->fixed_width; /* 固定 */
|
||||
else if (cell->fixed_width == 0) width = 文本测量; /* 测量(默认)*/
|
||||
else width = 0; /* 隐藏 */
|
||||
#+end_src
|
||||
|
||||
** after_layout / after_draw 钩子
|
||||
|
||||
普通 item 是 cairo 绘制;tray 是"layout 后把 region 应用到后端真实窗口"。给 =zdwm_bar_item_type_t= 加两个*可空*通用钩子,参数打包(仿现有 =zdwm_bar_click_params_t= 先例):
|
||||
|
||||
#+begin_src c
|
||||
typedef struct zdwm_bar_item_layout_params_t {
|
||||
zdwm_bar_item_t *item;
|
||||
const zdwm_bar_cell_api_t *cell_api;
|
||||
bar_x_region_t region;
|
||||
void *state;
|
||||
} zdwm_bar_item_layout_params_t;
|
||||
|
||||
void (*after_layout)(const zdwm_bar_item_layout_params_t *); /* 可空 */
|
||||
void (*after_draw)(const zdwm_bar_item_layout_params_t *); /* 可空 */
|
||||
#+end_src
|
||||
|
||||
设计要点:
|
||||
- 钩子为 NULL 则跳过,普通 item 零影响。
|
||||
- =after_layout= / =after_draw= 比 =update= 多一个 =region=(它们在 layout 之后调用),且通过 =cell_api= 可调 =cell_get_region= 取 per-cell 位置。
|
||||
- =update= 不打包参数(3 个参数不多,保持现有 =workspaces= / =binding= / =windows= 三个 item 零改动)。
|
||||
|
||||
=bar_draw= 流程:
|
||||
|
||||
#+begin_src c
|
||||
bar_output_layout(bo, ctx);
|
||||
for each item: if (after_layout) after_layout(params); /* params 含 region */
|
||||
bar_output_draw(bo, ctx, bg);
|
||||
for each item: if (after_draw) after_draw(params);
|
||||
#+end_src
|
||||
|
||||
* bar tray item
|
||||
|
||||
新增 =src/bar/tray.c=,作为普通 right-side item(参考 [[file:../src/bar/workspaces.c][workspaces.c]] 的实现模式):
|
||||
|
||||
#+begin_src c
|
||||
zdwm_bar_item_type_t bar_tray = {
|
||||
.create_state = bar_tray_create_state,
|
||||
.update = bar_tray_update,
|
||||
.on_click = bar_tray_on_click, /* 返回 NONE:icon 真实子窗口自收点击 */
|
||||
.after_layout = bar_tray_after_layout,
|
||||
.destroy_state = bar_tray_destroy_state,
|
||||
};
|
||||
#+end_src
|
||||
|
||||
- =create_state=:拷贝 =tray_t=,调 =set_listener= 注册回调(回调置 =state->dirty = true=)。
|
||||
- =update=:=dirty= 时 =set_cell_count(icon_count)= + 循环 =cell_set_fixed_width(i, icon_size)=。
|
||||
- =after_layout=:=place(handle, params->region.start)=。
|
||||
|
||||
tray cell 不画 cairo 内容(text 为空,=bar_item_draw= 只画融入 bar 的背景色块,被真实 icon 窗口盖住)。
|
||||
|
||||
* runtime 桥接
|
||||
|
||||
=runtime_init_bar=(=src/runtime/runtime.c=)的 create 循环:
|
||||
|
||||
#+begin_src c
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bool enable_tray = ((int32_t)i == runtime->bar.config.tray_output_index);
|
||||
auto bar_window = backend_create_bar_window(backend, bar_rect, color, enable_tray);
|
||||
...
|
||||
runtime->bar.tray = bar_window.tray; /* 每个返回都一样,存一份 */
|
||||
}
|
||||
bar_init(&runtime->bar, &runtime->listeners);
|
||||
#+end_src
|
||||
|
||||
=bar_init= 遍历 =bar_output=,对 =host_window()= 匹配自身 =window_id= 的那个 output 调 =bar_output_add_tray=(right side),其余不加。tray 生命周期随 =backend_destroy=,runtime 无特殊清理。
|
||||
|
||||
* 配置
|
||||
|
||||
=zdwm_bar_config_t=(=include/zdwm/config.h=)加:
|
||||
|
||||
#+begin_src c
|
||||
int32_t tray_output_index; /* 默认 0 = 第一个 output */
|
||||
#+end_src
|
||||
|
||||
配置 setup 中可通过 output 信息确定具体 index。
|
||||
|
||||
* 数据流(触发链)
|
||||
|
||||
#+begin_example
|
||||
icon 增删
|
||||
-> 后端 set_listener 回调 -> state->dirty = true
|
||||
-> 下个 timerfd tick -> bar_update -> bar_tray_update
|
||||
-> set_cell_count + cell_set_fixed_width -> item 标 dirty
|
||||
-> bar_draw -> bar_output_layout 重算 region(cell 数变了)
|
||||
-> tray 的 after_layout -> backend place(handle, region.start)
|
||||
-> 后端 move 容器 + configure 每个 icon 到 (region.start + i*icon_size, 0)
|
||||
#+end_example
|
||||
|
||||
bar 重绘是 timerfd(fps)驱动,非事件驱动。tray icon 是真实窗口,X server 自管绘制,不依赖 cairo 重绘。
|
||||
|
||||
* 实现范围(第一版)
|
||||
|
||||
第一版做*基础可用*:
|
||||
|
||||
- =_NET_SYSTEM_TRAY_S0= selection owner + MANAGER 广播
|
||||
- 基础 XEMBED(reparent 容器、=_XEMBED_EMBEDDED_NOTIFY=)
|
||||
- icon 显示与点击
|
||||
- icon 尺寸 = bar 高度(统一,不缩放)
|
||||
|
||||
放后续迭代:
|
||||
- 完整 XEMBED 焦点 / 激活转发
|
||||
- icon 尺寸协商与缩放
|
||||
- 客户端异常断开的清理
|
||||
|
||||
*透明背景*第一版基本白送——bar 窗口已是 ARGB visual(见 =backend_create_bar_window= 调用 =window_get_visual(backend, true)=),tray 容器作为子窗口继承。
|
||||
|
||||
* 关键文件清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------------------------------+------------------------------------------------------------------------------------------------------------------------|
|
||||
| =interface/tray.h= | **新增** tray 契约:=tray_t= / =tray_api_t= / =tray_icons_change_cb_t=(bar 与 backend 共享,落 interface 解耦) |
|
||||
| =interface/backend.h= | include =interface/tray.h=;=backend_bar_window_t= 内嵌 =tray_t=;扩展 =backend_create_bar_window=(加 =enable_tray=) |
|
||||
| =src/backend/x11/tray.c= | *新增*:selection / MANAGER / 基础 XEMBED / icon 管理 / =place= |
|
||||
| =src/backend/x11/internal.h= | =ATOM_LIST= 追加 tray/XEMBED atoms;=backend_t= 加 =tray= 字段 |
|
||||
| =src/backend/x11/event.c= | 消化 tray 相关 client message(不产 =event_t=) |
|
||||
| =src/backend/x11/backend.c= | =backend_create_bar_window= 实现 enable_tray 分支;=backend_destroy= 释放 tray |
|
||||
| =src/bar/types.h= | =bar_cell_t= 加 =fixed_width=;=bar_t= 加 =tray= |
|
||||
| =src/bar/cell.h= / =cell.c= | =cell_api= 加 =cell_set_fixed_width= / =cell_get_region= |
|
||||
| =src/bar/bar.c= | layout 支持 =fixed_width=;=bar_draw= 调 after 钩子;=bar_init= 加 tray item;新增 layout params 类型 |
|
||||
| =src/bar/tray.c= | *新增*:tray item 实现 |
|
||||
| =src/runtime/runtime.c= | =runtime_init_bar= 传 =enable_tray=、存 =bar->tray= |
|
||||
| =include/zdwm/config.h= | =zdwm_bar_config_t= 加 =tray_output_index= |
|
||||
|
||||
* 复用现有
|
||||
|
||||
- =backend_create_bar_window=(=src/backend/x11/backend.c=):bar 窗口已是 ARGB,tray 容器继承透明。
|
||||
- =zdwm_bar_item_type_t= vtable 框架 + [[file:../src/bar/workspaces.c][workspaces.c]]:tray item 同模式落地。
|
||||
- =zdwm_bar_click_params_t=:=after_layout= / =after_draw= 的 params 打包仿此先例。
|
||||
- =cell_api= 机制:tray =update= 通过它操作 cell(与 workspaces 一致)。
|
||||
@@ -1,91 +0,0 @@
|
||||
#+title: ZDWM Tray 系统托盘实现计划
|
||||
#+OPTIONS: toc:2
|
||||
|
||||
* 背景
|
||||
|
||||
bar 当前没有系统托盘。目标是让外部应用(输入法、音量、蓝牙、网络等)的 tray icon 显示在 bar 右侧并正常交互。
|
||||
|
||||
设计文档 [[file:tray-design.org][tray-design.org]] 已把方案定死。刚完成的模块分层重构([[file:module-layering.org][module-layering.org]])为 tray 铺了路——=interface/= 层让 bar 与 backend 经契约解耦,tray 契约落 =interface/tray.h= 消除 =bar↔backend= 交叉依赖(分层的核心动机之一)。
|
||||
|
||||
* 关键决策
|
||||
|
||||
- *契约落 =interface/tray.h=*(分层后的位置,非设计文档旧版的 =src/bar/tray.h=)。
|
||||
- *tray 闭环后端*:core 的 =event_t= / =policy= / =plan= 完全不感知;tray 消息在 =event.c= 拦截后 =return false=,不进 policy 路由。
|
||||
- *tray 是 bar 附属 + 后端单例*:生命周期随 bar 窗口;=_NET_SYSTEM_TRAY_S0= selection owner 全 display 唯一,挂到最后一个 =enable_tray=true= 的 output。
|
||||
- *第一版范围*:selection owner + MANAGER 广播 + 基础 XEMBED(reparent + =_XEMBED_EMBEDDED_NOTIFY=)+ icon 显示与点击 + icon 尺寸 = bar 高度(不缩放)。焦点/激活转发、尺寸协商、客户端异常断开清理放后续迭代。
|
||||
- *tray 存 =bar_t.tray=(一份)*:单例语义,=bar_init= 时用 =host_window()= 匹配决定哪个 output 渲染。
|
||||
|
||||
* 实现步骤
|
||||
|
||||
每步可独立编译,建议每步一个提交。
|
||||
|
||||
** 步骤 1:契约层 + 调用点占位
|
||||
|
||||
签名变更与唯一调用点耦合,必须一起改才能编译。
|
||||
|
||||
- *新增 =src/interface/tray.h=*(纯声明,零 .c):=tray_icons_change_cb_t=、=tray_api_t=(=host_window= / =icon_count= / =icon_size= / =place= / =set_listener=)、=tray_t=(=api= + =handle=)。只依赖 =interface/types.h=。
|
||||
- *=src/interface/backend.h=*:=#include "interface/tray.h"=;=backend_bar_window_t= 内嵌 =tray_t tray=;=backend_create_bar_window= 加 =bool enable_tray=。
|
||||
- *=src/runtime/runtime.c=*:临时传 =enable_tray=false=。
|
||||
- *=src/backend/x11/backend.c=*:签名加参数,函数体先忽略,返回 =.tray = {0}=。
|
||||
- 验证:编译通过,bar 行为不变。
|
||||
|
||||
** 步骤 2:bar 框架扩展(零影响现有 3 个 item)
|
||||
|
||||
为 tray item 铺路;钩子可空、=fixed_width= 默认 0,现有 item 走原路径。
|
||||
|
||||
- *=include/zdwm/bar.h=*:=zdwm_bar_item_type_t= 加可空钩子 =after_layout= / =after_draw=;新增 =zdwm_bar_item_layout_params_t=(仿 =zdwm_bar_click_params_t=:=item= / =cell_api= / =region= / =state=);=zdwm_bar_cell_api_t= 加 =cell_set_fixed_width=、=cell_get_region=。
|
||||
- *=src/bar/types.h=*:=bar_cell_t= 加 =int32_t fixed_width=(默认 0 = 走文本测量,=p_clear= 后天然正确;<0 = 隐藏哨兵)。
|
||||
- *=src/bar/cell.c=*:实现 =bar_cell_set_fixed_width= / =bar_cell_get_region=,注册进 =bar_cell_api=。
|
||||
- *=src/bar/bar.c=*:=bar_output_layout= 三处宽度计算加 =fixed_width= 分支(>0 固定 / ==0 测量 / <0 隐藏);=bar_draw= 在 layout 后遍历调 =after_layout=、draw 后调 =after_draw=(NULL 跳过)。
|
||||
- 验证:编译 + 运行,*workspaces/binding/windows 三个 item 视觉与点击零变化*(关键回归点)。
|
||||
|
||||
** 步骤 3:后端协议层(核心)
|
||||
|
||||
tray 能力全部 X11 细节,"已实现但未启用"。
|
||||
|
||||
- *新增 =src/backend/x11/tray.c=*:tray host 状态 + 成为 =_NET_SYSTEM_TRAY_S0= owner + 广播 MANAGER + 容器子窗口(reparent 到当前 bar)+ 基础 XEMBED(reparent icon + =_XEMBED_EMBEDDED_NOTIFY=)+ dock 请求处理 + =place(handle,x)=(move 容器、configure 每个 icon 到 =x + i*icon_size=)+ 填充 =static const tray_api_t=(=handle= 实际是 =backend_t*=)。=icon_size= 用 bar 窗口高度(单一数据源)。
|
||||
- *=src/backend/x11/internal.h=*:=ATOM_LIST= 追加 =_NET_SYSTEM_TRAY_S0= / =_NET_SYSTEM_TRAY_OPCODE= / =_NET_SYSTEM_TRAY_MESSAGE_DATA= / =_XEMBED= / =_XEMBED_INFO= / =Manager=;=backend_t= 加 tray host 字段(单例:host_window、icon 列表、listener、enabled)。
|
||||
- *=src/backend/x11/event.c=*(=handle_client_message=):开头拦截 tray 消息(=Manager= 且 =data.data32[1]==_NET_SYSTEM_TRAY_S0=;=_NET_SYSTEM_TRAY_OPCODE= 的 =REQUEST_DOCK=),处理后 =return false=(不产 =event_t=)。
|
||||
- *=src/backend/x11/backend.c=*:=backend_create_bar_window= 的 =enable_tray=true= 分支(创建/迁移 host 容器到此 bar 窗口,返回 =tray= 填 backend 的 api + =handle=backend=);=backend_destroy= 释放 tray(selection、容器、icon 列表)。
|
||||
- *=CMakeLists.txt=*:加 =src/backend/x11/tray.c=。
|
||||
- 验证:编译通过。runtime 仍传 false,行为不变。
|
||||
|
||||
** 步骤 4:bar tray item
|
||||
|
||||
消费步骤 2 的框架 + 步骤 3 的契约,tray 作为 right-side item 接入。
|
||||
|
||||
- *新增 =src/bar/tray.c=*(参考 =workspaces.c= 模式):=bar_tray= vtable = =create_state= / =update= / =on_click= / =after_layout= / =destroy_state=。=create_state= 拷贝 =tray_t= + =set_listener= 注册回调(置 dirty);=update= 在 dirty 时 =set_cell_count(icon_count)= + 循环 =cell_set_fixed_width(i, icon_size)=;=on_click= 返回 =ACTION_NONE=(icon 真实子窗口自收点击);=after_layout= 调 =place(handle, region.start)=。cell 不画 cairo(text 空,被真实 icon 窗口盖住)。
|
||||
- *=src/bar/bar.h=*:=bar_t= 加 =tray_t tray=(一份,单例)。
|
||||
- *=src/bar/bar.c=*(=bar_init=):遍历 output,对 =bar->tray.host_window()= 匹配自身 =window_id= 的 output 调 =bar_output_add_tray=(right side),其余不加。
|
||||
- *=CMakeLists.txt=*:加 =src/bar/tray.c=。
|
||||
- 验证:编译通过。right side 仍空(runtime 未注入 tray)。
|
||||
|
||||
** 步骤 5:runtime/配置串起来(启用)
|
||||
|
||||
- *=include/zdwm/bar.h=*(=zdwm_bar_config_t=):加 =int32_t tray_output_index=(默认 0 = 第一个 output)。
|
||||
- *=src/runtime/runtime.c=*(=runtime_init_bar=):=bool enable_tray = ((int32_t)i == runtime->bar.config.tray_output_index);= 传给 =backend_create_bar_window=;=runtime->bar.tray = bar_window.tray;=(每个返回都一样,存一份)。
|
||||
- *=src/config/defaults.c=*:=tray_output_index= 默认 0。
|
||||
- 验证:编译 + 运行,tray 启用。
|
||||
|
||||
* 风险点
|
||||
|
||||
- *after 钩子对现有 item*:现有三个 item 的 vtable 不显式初始化新钩子 → NULL(C 部分初始化保证);=bar_draw= 调用前判 NULL。=fixed_width= 默认 0 走原测量路径。步骤 2 后必须确认三个 item 零变化。
|
||||
- *selection owner 单例 vs per-output create*:host 是 backend 单例;每次 =enable_tray=true= 把容器 reparent 到本次 bar 窗口,最终挂最后一个 true 的 output;=host_window()= 返回当前挂载 window_id。
|
||||
- *XEMBED 第一版只做基础*:reparent + =EMBEDDED_NOTIFY=;不做焦点/激活/缩放。某些高级 applet(要求焦点的输入法)可能受限,基础音量/网络 icon 无影响。
|
||||
- *core 零感知*:tray 消息在 =event.c= 拦截 =return false=,不进 policy/plan。icon 增删不触发 core 重排——icon 是真实 X 子窗口,X server 自管绘制,bar 重绘靠 timerfd 驱动,region 变化经 =after_layout= → =place= 同步。
|
||||
- *tray cell padding*:=fixed_width = icon_size=,确保 tray item 的 cell padding 为 0,避免 icon 间额外间距。
|
||||
- *透明背景*:bar 窗口已 ARGB(=backend.c= 用 =window_get_visual(true)=),tray 容器子窗口继承,第一版白送。
|
||||
|
||||
* 端到端验证
|
||||
|
||||
1. *每步编译*:=cmake --build build=,零 warning(项目 =-Wall=)。
|
||||
2. *步骤 5 后运行*:=./build/zdwm=。
|
||||
3. *tray icon 验证*:启动一个提供 tray icon 的 app(=volumeicon= / =nm-applet= / =fcitx5= 等,不要用 stalonetray——它会抢 selection owner)。验证:icon 显示在 bar *右侧*、尺寸 = bar 高度无溢出、多 icon 横向无重叠、点击有响应、透明背景融入 bar。
|
||||
4. *动态增删*:启动/退出 tray app,观察 icon 实时增删 + 布局重排(经 =set_listener= 回调 → dirty → 下个 timerfd tick → =update= 重算 → =after_layout= 重排),无残留空位。
|
||||
5. *回归*(每步都做,重点步骤 2):workspaces tag 切换、binding 模式、windows 标题、bar 点击命中全部不变。
|
||||
|
||||
* 关键文件
|
||||
|
||||
- *新增*:=src/interface/tray.h=(契约)、=src/backend/x11/tray.c=(协议核心)、=src/bar/tray.c=(bar item)。
|
||||
- *修改*:=src/interface/backend.h=、=src/backend/x11/{internal.h,event.c,backend.c}=、=include/zdwm/bar.h=、=src/bar/{types.h,cell.c,bar.c,bar.h}=、=src/runtime/runtime.c=、=src/config/defaults.c=、=CMakeLists.txt=。
|
||||
- *复用*:=workspaces.c=(item 模板)、=zdwm_bar_click_params_t=(layout params 打包先例)、=ATOM_LIST= 宏(atom 注册)、=window_get_visual(true)=(ARGB)、=bar_cell_api= vtable 机制。
|
||||
@@ -1,95 +0,0 @@
|
||||
#ifndef ZDWM_ACTION_H
|
||||
#define ZDWM_ACTION_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum zdwm_action_type_t {
|
||||
ZDWM_ACTION_NONE,
|
||||
ZDWM_ACTION_SPAWN,
|
||||
ZDWM_ACTION_QUIT,
|
||||
ZDWM_ACTION_RAISE_OR_RUN,
|
||||
|
||||
ZDWM_ACTION_OUTPUT_CYCLE,
|
||||
ZDWM_ACTION_WORKSPACE_SWITCH,
|
||||
ZDWM_ACTION_WORKSPACE_SWITCH_SAME_OUTPUT_BY_INDEX,
|
||||
ZDWM_ACTION_LAYOUT_CYCLE,
|
||||
ZDWM_ACTION_BINDING_MODE_CYCLE,
|
||||
|
||||
ZDWM_ACTION_WINDOW_TOGGLE_FULLSCREEN,
|
||||
ZDWM_ACTION_WINDOW_TOGGLE_MAXIMIZE,
|
||||
ZDWM_ACTION_WINDOW_TOGGLE_MINIMIZE,
|
||||
ZDWM_ACTION_WINDOW_TOGGLE_FLOATING,
|
||||
ZDWM_ACTION_WINDOW_TOGGLE_STICKY,
|
||||
|
||||
ZDWM_ACTION_WINDOW_FOCUS_CYCLE,
|
||||
ZDWM_ACTION_WINDOW_KILL,
|
||||
|
||||
ZDWM_ACTION_WINDOW_SEND_TO_WORKSPACE_SAME_OUTPUT_BY_INDEX,
|
||||
ZDWM_ACTION_WINDOW_CYCLE_OUTPUT,
|
||||
|
||||
ZDWM_ACTION_BAR_TOGGLE_VISIBILITY,
|
||||
} zdwm_action_type_t;
|
||||
|
||||
typedef struct zdwm_action_data_spawn_t {
|
||||
const char *command;
|
||||
} zdwm_action_data_spawn_t;
|
||||
|
||||
typedef struct zdwm_action_data_quit_t {
|
||||
bool restart;
|
||||
} zdwm_action_data_quit_t;
|
||||
|
||||
typedef struct zdwm_action_data_raise_or_run_t {
|
||||
const char *class_name;
|
||||
const char *command;
|
||||
} zdwm_action_data_raise_or_run_t;
|
||||
|
||||
typedef struct zdwm_action_data_delta_only_t {
|
||||
int32_t delta;
|
||||
} zdwm_action_data_delta_only_t;
|
||||
|
||||
typedef struct zdwm_action_data_index_only_t {
|
||||
uint32_t index;
|
||||
} zdwm_action_data_index_only_t;
|
||||
|
||||
typedef struct zdwm_action_data_switch_workspace_t {
|
||||
zdwm_workspace_id_t workspace;
|
||||
} zdwm_action_data_switch_workspace_t;
|
||||
|
||||
typedef struct zdwm_action_data_window_send_to_workspace_t {
|
||||
uint32_t index;
|
||||
bool switch_workspace;
|
||||
} zdwm_action_data_window_send_to_workspace_t;
|
||||
|
||||
typedef struct zdwm_action_data_window_cycle_output_t {
|
||||
int32_t delta;
|
||||
bool keep_focus;
|
||||
} zdwm_action_data_window_cycle_output_t;
|
||||
|
||||
typedef struct zdwm_action_t {
|
||||
zdwm_action_type_t type;
|
||||
union {
|
||||
zdwm_action_data_spawn_t spawn;
|
||||
zdwm_action_data_quit_t quit;
|
||||
zdwm_action_data_raise_or_run_t raise_or_run;
|
||||
zdwm_action_data_delta_only_t output_cycle;
|
||||
zdwm_action_data_index_only_t workspace_switch_same_output_by_index;
|
||||
zdwm_action_data_switch_workspace_t switch_workspace;
|
||||
zdwm_action_data_delta_only_t layout_cycle;
|
||||
zdwm_action_data_delta_only_t binding_mode_cycle;
|
||||
zdwm_action_data_delta_only_t window_focus_cycle;
|
||||
zdwm_action_data_window_send_to_workspace_t
|
||||
window_send_to_workspace_same_output_by_index;
|
||||
zdwm_action_data_window_cycle_output_t window_cycle_output;
|
||||
} as;
|
||||
} zdwm_action_t;
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_ACTION_H */
|
||||
@@ -1,111 +0,0 @@
|
||||
#ifndef ZDWM_BAR_H
|
||||
#define ZDWM_BAR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct zdwm_bar_config_t {
|
||||
int32_t height;
|
||||
bool show_top; /* 如果为 false 则 bar 显示在屏幕底部 */
|
||||
uint8_t padding_x;
|
||||
uint16_t fps;
|
||||
const char *font_family;
|
||||
uint32_t font_size;
|
||||
uint32_t dpi;
|
||||
|
||||
const char *bg;
|
||||
const char *fg;
|
||||
|
||||
uint32_t tag_cell_padding_x;
|
||||
uint32_t tag_indicator_width;
|
||||
const char *tag_bg;
|
||||
const char *tag_fg;
|
||||
const char *tag_active_bg;
|
||||
const char *tag_active_fg;
|
||||
const char *tag_urgent_bg;
|
||||
const char *tag_urgent_fg;
|
||||
|
||||
const char *layout_bg;
|
||||
const char *layout_fg;
|
||||
|
||||
bool binding_show_default;
|
||||
uint32_t binding_padding_x;
|
||||
const char *binding_mode_bg;
|
||||
const char *binding_mode_fg;
|
||||
|
||||
int32_t tray_output_index;
|
||||
|
||||
uint32_t window_padding_x;
|
||||
const char *window_bg;
|
||||
const char *window_fg;
|
||||
const char *window_focused_bg;
|
||||
const char *window_focused_fg;
|
||||
} zdwm_bar_config_t;
|
||||
|
||||
typedef struct zdwm_bar_item_t zdwm_bar_item_t;
|
||||
|
||||
typedef struct zdwm_bar_x_region_t {
|
||||
int32_t start;
|
||||
int32_t end;
|
||||
} zdwm_bar_x_region_t;
|
||||
|
||||
typedef struct zdwm_bar_cell_api_t {
|
||||
size_t (*get_cell_count)(zdwm_bar_item_t *item);
|
||||
void (*set_cell_count)(zdwm_bar_item_t *item, size_t count);
|
||||
|
||||
void (*cell_set_text)(zdwm_bar_item_t *item, size_t index, const char *text);
|
||||
void (*cell_set_bg)(zdwm_bar_item_t *item, size_t index, const char *color);
|
||||
void (*cell_set_fg)(zdwm_bar_item_t *item, size_t index, const char *color);
|
||||
void (*cell_set_icon)(zdwm_bar_item_t *item, size_t index, zdwm_icon_t icon);
|
||||
void (*cell_set_indicator)(zdwm_bar_item_t *item, size_t index, bool show);
|
||||
void (*cell_set_fixed_width)(
|
||||
zdwm_bar_item_t *item,
|
||||
size_t index,
|
||||
int32_t width
|
||||
);
|
||||
|
||||
zdwm_bar_x_region_t (*cell_get_region)(zdwm_bar_item_t *item, size_t index);
|
||||
} zdwm_bar_cell_api_t;
|
||||
|
||||
typedef struct zdwm_bar_click_params_t {
|
||||
zdwm_bar_item_t *item;
|
||||
size_t cell_index;
|
||||
int32_t x;
|
||||
zdwm_modifier_mask_t modifiers;
|
||||
zdwm_button_t button;
|
||||
void *state;
|
||||
} zdwm_bar_click_params_t;
|
||||
|
||||
typedef struct zdwm_bar_item_hook_params_t {
|
||||
zdwm_bar_item_t *item;
|
||||
const zdwm_bar_cell_api_t *cell_api;
|
||||
zdwm_bar_x_region_t region;
|
||||
void *state;
|
||||
} zdwm_bar_item_hook_params_t;
|
||||
|
||||
typedef struct zdwm_bar_item_type_t {
|
||||
void *(*create_state)(zdwm_output_id_t output_id, void *config);
|
||||
void (*update)(
|
||||
zdwm_bar_item_t *item,
|
||||
const zdwm_bar_cell_api_t *cells,
|
||||
void *state
|
||||
);
|
||||
void (*after_layout)(const zdwm_bar_item_hook_params_t *params);
|
||||
void (*after_draw)(const zdwm_bar_item_hook_params_t *params);
|
||||
zdwm_action_t (*on_click)(zdwm_bar_click_params_t *params);
|
||||
void (*destroy_state)(void *state);
|
||||
zdwm_bar_item_t *instance;
|
||||
uint32_t update_interval_ms;
|
||||
} zdwm_bar_item_type_t;
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_BAR_H */
|
||||
@@ -1,251 +0,0 @@
|
||||
#ifndef ZDWM_CONFIG_H
|
||||
#define ZDWM_CONFIG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/layout.h>
|
||||
#include <zdwm/listeners.h>
|
||||
#include <zdwm/rules.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ZDWM_CONFIG_ABI_VERSION 1u
|
||||
|
||||
typedef struct zdwm_config_builder_t zdwm_config_builder_t;
|
||||
|
||||
typedef struct zdwm_builtin_layouts_t {
|
||||
zdwm_layout_fn fair;
|
||||
zdwm_layout_fn fullscreen;
|
||||
zdwm_layout_fn maximize;
|
||||
} zdwm_builtin_layouts_t;
|
||||
|
||||
typedef struct zdwm_api_t {
|
||||
uint32_t abi_version;
|
||||
zdwm_builtin_layouts_t builtin_layouts;
|
||||
|
||||
/**
|
||||
* 用户调用这个函数注册布局算法
|
||||
* @param builder 配置构建上下文
|
||||
* @param name 布局算法名称,不能为 nullptr
|
||||
* @param symbol 布局算法标志符号,用于在状态栏中显示,不能为 nullptr
|
||||
* @param description 布局算法描述,可以为 nullptr
|
||||
* @param fn 布局算法,可以为 nullptr ,如果为 nullptr 则代表不自动布局
|
||||
* @returns 返回布局算法对应的布局 id ,如果注册失败,返回
|
||||
* ZDWM_LAYOUT_ID_INVALID
|
||||
*/
|
||||
zdwm_layout_id_t (*register_layout)(
|
||||
zdwm_config_builder_t *builder,
|
||||
const char *name,
|
||||
const char *symbol,
|
||||
const char *description,
|
||||
zdwm_layout_fn fn
|
||||
);
|
||||
|
||||
/**
|
||||
* 用户调用这个函数添加 output 中的工作空间
|
||||
* @param builder 配置构建上下文
|
||||
* @param output_index 该工作空间归属的 output 索引
|
||||
* @param name 工作空间名称,用于状态栏显示,不能为 nullptr
|
||||
* @param layout_ids 该工作空间支持的布局算法列表
|
||||
* @param layout_count 支持的布局算法总数
|
||||
* @param initial_layout_id 初始布局算法 id
|
||||
* @returns 返回工作空间 id 。如果注册失败,返回 ZDWM_WORKSPACE_ID_INVALID
|
||||
*/
|
||||
zdwm_workspace_id_t (*define_workspace)(
|
||||
zdwm_config_builder_t *builder,
|
||||
size_t output_index,
|
||||
const char *name,
|
||||
const zdwm_layout_id_t *layout_ids,
|
||||
size_t layout_count,
|
||||
zdwm_layout_id_t initial_layout_id
|
||||
);
|
||||
|
||||
/**
|
||||
* 用户调用这个函数添加窗口规则
|
||||
* @param builder 配置构建上下文
|
||||
* @param match 窗口匹配条件,不能为 nullptr
|
||||
* @param action 匹配后执行的动作,不能为 nullptr
|
||||
* @returns 添加成功返回 true ,不合法时返回 false
|
||||
* @details 不合法的情况:match 所有字段全为 nullptr;
|
||||
* 或 action 无任何动作;或 action 指定的 workspace 不存在
|
||||
*/
|
||||
bool (*add_rule)(
|
||||
zdwm_config_builder_t *builder,
|
||||
const zdwm_rule_match_t *match,
|
||||
const zdwm_rule_action_t *action
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 用户调用这个函数添加按键模式
|
||||
*
|
||||
* @details 按键模式名不能重复,如果添加重复的名字仅第一次成功,后续将添加失败
|
||||
*
|
||||
* @param builder 配置构建上下文
|
||||
* @param mode_name 按键模式名字,不能为 nullptr 或空字符串
|
||||
*
|
||||
* @return 添加成功返回新增模式的 id 否则返回 ZDWM_BINDING_MODE_ID_INVALID
|
||||
*/
|
||||
zdwm_binding_mode_id_t (*add_mode)(
|
||||
zdwm_config_builder_t *builder,
|
||||
const char *mode_name
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 添加按键绑定
|
||||
*
|
||||
* @details mode_id 对应的模式不存在或 key_sequence 不合法,都会添加失败
|
||||
*
|
||||
* @param builder 配置构建上下文
|
||||
* @param bind_mode 模式 ID
|
||||
* @param key_sequence 按键序列的字符串表达
|
||||
* @param action_fn 用户行为函数
|
||||
* @param action_arg 用户行为函数所需的参数
|
||||
*
|
||||
* @return 添加成功返回 true 否则返回 false
|
||||
*/
|
||||
bool (*bind)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_binding_mode_id_t bind_mode,
|
||||
const char *key_sequence,
|
||||
zdwm_action_t action
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 设置默认模式
|
||||
*
|
||||
* @param table 按键绑定表
|
||||
* @param mode_id 按键模式 ID
|
||||
*
|
||||
* @return 切换成功返回 true 否则返回 false
|
||||
*/
|
||||
bool (*set_default_mode)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_binding_mode_id_t mode_id
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 设置初始模式
|
||||
*
|
||||
* @param builder 配置构建上下文
|
||||
* @param mode_id 按键模式 ID
|
||||
*
|
||||
* @return 切换成功返回 true 否则返回 false
|
||||
*/
|
||||
bool (*set_initial_mode)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_binding_mode_id_t mode_id
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 设置边框配置信息
|
||||
*
|
||||
* @param builder 配置构建上下文
|
||||
* @param width 边框宽度
|
||||
* @param normal 普通窗口边框颜色
|
||||
* @param focused 焦点窗口边框颜色
|
||||
*/
|
||||
void (*set_border_config)(
|
||||
zdwm_config_builder_t *builder,
|
||||
uint32_t width,
|
||||
const char *normal,
|
||||
const char *focused
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 设置窗口移动和调整大小时的刷新率
|
||||
*
|
||||
* @param builder 配置构建上下文
|
||||
* @param fps 刷新帧率,最小 10 帧,若设置小于 10 帧会设置为 10 帧
|
||||
*/
|
||||
void (*set_interaction_fps)(zdwm_config_builder_t *builder, uint32_t fps);
|
||||
|
||||
/** @name Listener 注册
|
||||
* @{ */
|
||||
|
||||
void (*subscribe_current_output)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_current_output_id_listener *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_initial_workspace_list)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_initial_workspace_list *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_workspace_active)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_workspace_active_updated *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_layout)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_layout_notify *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_binding_mode)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_binding_mode_notify *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_initial_window_list)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_initial_window_list *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_window_added)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_window_added *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_window_updated)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_window_updated *fn,
|
||||
void *user_data
|
||||
);
|
||||
void (*subscribe_window_removed)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_window_removed *fn,
|
||||
void *user_data
|
||||
);
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @name bar 配置
|
||||
* @{ */
|
||||
void (*set_bar_config)(
|
||||
zdwm_config_builder_t *builder,
|
||||
zdwm_bar_config_t config
|
||||
);
|
||||
/** @} */
|
||||
} zdwm_api_t;
|
||||
|
||||
/**
|
||||
* 用户配置入口函数类型定义
|
||||
* @param api 用户自定义时可以使用的 api 接口及数据,api
|
||||
* 中的函数和信息只能在函数内部使用,如果用户将其保存后在函数外使用是未定义行为
|
||||
* @param builder 配置构建上下文
|
||||
* @param outputs 屏幕信息列表
|
||||
* @param output_count 屏幕个数
|
||||
* @returns 用户配置成功返回 true ,配置失败返回 false
|
||||
*/
|
||||
typedef bool zdwm_config_setup_fn(
|
||||
const zdwm_api_t *api,
|
||||
zdwm_config_builder_t *builder,
|
||||
const zdwm_output_info_t *outputs,
|
||||
size_t output_count
|
||||
);
|
||||
|
||||
/* 用户在自己的动态链接库配置中必须实现该函数 */
|
||||
extern zdwm_config_setup_fn setup;
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_CONFIG_H */
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifndef ZDWM_LAYOUT_H
|
||||
#define ZDWM_LAYOUT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct zdwm_layout_ctx_t {
|
||||
zdwm_workspace_id_t workspace_id;
|
||||
zdwm_window_id_t focused_window_id;
|
||||
zdwm_rect_t output_geometry;
|
||||
zdwm_rect_t workarea;
|
||||
const zdwm_window_id_t *window_ids;
|
||||
size_t window_count;
|
||||
} zdwm_layout_ctx_t;
|
||||
|
||||
typedef struct zdwm_layout_item_t {
|
||||
zdwm_window_id_t window_id;
|
||||
zdwm_rect_t rect;
|
||||
} zdwm_layout_item_t;
|
||||
|
||||
typedef struct zdwm_layout_result_t {
|
||||
zdwm_layout_item_t *items;
|
||||
size_t item_count;
|
||||
size_t item_capacity;
|
||||
} zdwm_layout_result_t;
|
||||
|
||||
/**
|
||||
* @brief 自动布局算法
|
||||
*
|
||||
* @return out 有变化返回 true 否则返回 true
|
||||
*/
|
||||
typedef bool (*zdwm_layout_fn)(
|
||||
const zdwm_layout_ctx_t *ctx,
|
||||
zdwm_layout_result_t *out
|
||||
);
|
||||
|
||||
static inline void zdwm_layout_result_reset(zdwm_layout_result_t *result) {
|
||||
if (!result) abort();
|
||||
result->item_count = 0;
|
||||
}
|
||||
|
||||
static inline void
|
||||
zdwm_layout_result_push(zdwm_layout_result_t *result, zdwm_layout_item_t item) {
|
||||
if (!result) abort();
|
||||
|
||||
if (result->item_count == result->item_capacity) {
|
||||
size_t capacity = result->item_capacity ? result->item_capacity * 2u : 4u;
|
||||
zdwm_layout_item_t *items =
|
||||
realloc(result->items, capacity * sizeof(*result->items));
|
||||
if (!items) abort();
|
||||
|
||||
result->items = items;
|
||||
result->item_capacity = capacity;
|
||||
}
|
||||
|
||||
result->items[result->item_count++] = item;
|
||||
}
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_LAYOUT_H */
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifndef ZDWM_LISTENERS_H
|
||||
#define ZDWM_LISTENERS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void
|
||||
zdwm_current_output_id_listener(zdwm_output_id_t output_id, void *user_data);
|
||||
|
||||
typedef struct zdwm_workspace_t {
|
||||
zdwm_output_id_t output_id;
|
||||
zdwm_workspace_id_t id;
|
||||
const char *name; /* 不持有内存 */
|
||||
} zdwm_workspace_t;
|
||||
typedef void zdwm_initial_workspace_list(
|
||||
const zdwm_workspace_t *list,
|
||||
size_t count,
|
||||
void *user_data
|
||||
);
|
||||
typedef void zdwm_workspace_active_updated(
|
||||
zdwm_output_id_t output_id,
|
||||
zdwm_workspace_id_t workspace_id,
|
||||
void *user_data
|
||||
);
|
||||
|
||||
typedef struct zdwm_layout_notify_t {
|
||||
zdwm_workspace_id_t workspace;
|
||||
zdwm_layout_id_t id;
|
||||
const char *name; /* 不持有内存 */
|
||||
const char *symbol; /* 不持有内存 */
|
||||
const char *description; /* 不持有内存 */
|
||||
} zdwm_layout_notify_t;
|
||||
typedef void zdwm_layout_notify(zdwm_layout_notify_t layout, void *user_data);
|
||||
|
||||
typedef struct zdwm_binding_mode_notify_t {
|
||||
bool is_default_mode;
|
||||
zdwm_binding_mode_id_t id;
|
||||
const char *name; /* 不持有内存 */
|
||||
} zdwm_binding_mode_notify_t;
|
||||
typedef void zdwm_binding_mode_notify(
|
||||
zdwm_binding_mode_notify_t binding_mode,
|
||||
void *user_data
|
||||
);
|
||||
|
||||
typedef struct zdwm_window_t {
|
||||
zdwm_workspace_id_t workspace;
|
||||
zdwm_window_id_t id;
|
||||
const char *title; /* 不持有内存 */
|
||||
bool focused;
|
||||
bool urgent;
|
||||
bool skip_taskbar;
|
||||
} zdwm_window_t;
|
||||
typedef void zdwm_initial_window_list(
|
||||
const zdwm_window_t *list,
|
||||
size_t count,
|
||||
void *user_data
|
||||
);
|
||||
typedef void zdwm_window_added(const zdwm_window_t *window, void *user_data);
|
||||
typedef void zdwm_window_updated(const zdwm_window_t *window, void *user_data);
|
||||
typedef void zdwm_window_removed(zdwm_window_id_t window_id, void *user_data);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_LISTENERS_H */
|
||||
@@ -1,38 +0,0 @@
|
||||
#ifndef ZDWM_RULES_H
|
||||
#define ZDWM_RULES_H
|
||||
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 窗口匹配条件
|
||||
* @details 仅匹配不为 nullptr 的字段,多个字段按且规则匹配
|
||||
*/
|
||||
typedef struct zdwm_rule_match_t {
|
||||
const char *app_id;
|
||||
const char *role;
|
||||
const char *class_name;
|
||||
const char *instance_name;
|
||||
} zdwm_rule_match_t;
|
||||
|
||||
typedef struct zdwm_rule_action_t {
|
||||
/**
|
||||
* 目标 workspace id
|
||||
* - 若规则不想指定 workspace ,则将其设为 ZDWM_WORKSPACE_ID_INVALID
|
||||
*/
|
||||
zdwm_workspace_id_t workspace;
|
||||
|
||||
bool switch_to_workspace; /* 自动切换到目标 workspace(仅 true 时生效) */
|
||||
bool fullscreen; /* 强制全屏(仅 true 时生效) */
|
||||
bool maximize; /* 强制最大化(仅 true 时生效) */
|
||||
bool floating; /* 强制浮动(仅 true 时生效) */
|
||||
} zdwm_rule_action_t;
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_RULES_H */
|
||||
@@ -1,72 +0,0 @@
|
||||
#ifndef ZDWM_TYPES_H
|
||||
#define ZDWM_TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef uint32_t zdwm_output_id_t;
|
||||
typedef uint32_t zdwm_layout_id_t;
|
||||
typedef uint32_t zdwm_window_id_t;
|
||||
typedef uint32_t zdwm_workspace_id_t;
|
||||
typedef uint32_t zdwm_binding_mode_id_t;
|
||||
|
||||
#define ZDWM_LAYOUT_ID_INVALID ((zdwm_layout_id_t)UINT32_MAX)
|
||||
#define ZDWM_WINDOW_ID_INVALID ((zdwm_window_id_t)0)
|
||||
#define ZDWM_WORKSPACE_ID_INVALID ((zdwm_workspace_id_t)UINT32_MAX)
|
||||
#define ZDWM_BINDING_MODE_ID_INVALID ((zdwm_binding_mode_id_t)UINT32_MAX)
|
||||
|
||||
typedef enum zdwm_button_t {
|
||||
ZDWM_BUTTON_NONE,
|
||||
ZDWM_BUTTON_LEFT,
|
||||
ZDWM_BUTTON_RIGHT,
|
||||
ZDWM_BUTTON_MIDDLE,
|
||||
} zdwm_button_t;
|
||||
|
||||
/* clang-format off */
|
||||
typedef enum zdwm_modifier_bit_t {
|
||||
ZDWM_MOD_NONE = 0u,
|
||||
ZDWM_MOD_SHIFT = 1u << 0,
|
||||
ZDWM_MOD_CONTROL = 1u << 1,
|
||||
ZDWM_MOD_1 = 1u << 2,
|
||||
ZDWM_MOD_2 = 1u << 3,
|
||||
ZDWM_MOD_3 = 1u << 4,
|
||||
ZDWM_MOD_4 = 1u << 5,
|
||||
ZDWM_MOD_5 = 1u << 6,
|
||||
} zdwm_modifier_bit_t;
|
||||
/* clang-format on */
|
||||
|
||||
typedef uint32_t zdwm_modifier_mask_t;
|
||||
|
||||
typedef struct zdwm_rect_t {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
} zdwm_rect_t;
|
||||
|
||||
typedef struct zdwm_output_info_t {
|
||||
const char *name;
|
||||
zdwm_rect_t geometry;
|
||||
} zdwm_output_info_t;
|
||||
|
||||
typedef enum zdwm_icon_type_t {
|
||||
ZDWM_ICON_TEXT,
|
||||
ZDWM_ICON_IMAGE,
|
||||
} zdwm_icon_type_t;
|
||||
|
||||
typedef struct zdwm_icon_t {
|
||||
zdwm_icon_type_t type;
|
||||
union {
|
||||
const char *text;
|
||||
const char *image_path;
|
||||
} as;
|
||||
} zdwm_icon_t;
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ZDWM_TYPES_H */
|
||||
158
src/action.c
Normal file
158
src/action.c
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "action.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "client.h"
|
||||
#include "monitor.h"
|
||||
#include "types.h"
|
||||
#include "wm.h"
|
||||
#include "xcursor.h"
|
||||
|
||||
void focus_client_in_same_tag(const user_action_arg_t *arg) {
|
||||
bool next = arg->b;
|
||||
tag_t *tag = wm.current_monitor->selected_tag;
|
||||
|
||||
task_in_tag_t *task = nullptr;
|
||||
if (next) {
|
||||
task = client_get_next_task_in_tag(wm.client_focused, tag);
|
||||
} else {
|
||||
task = client_get_previous_task_in_tag(wm.client_focused, tag);
|
||||
}
|
||||
|
||||
if (!task) return;
|
||||
|
||||
client_stack_raise(task->client);
|
||||
client_focus(task->client);
|
||||
}
|
||||
|
||||
void select_tag_of_current_monitor(const user_action_arg_t *arg) {
|
||||
monitor_select_tag(wm.current_monitor, arg->ui);
|
||||
}
|
||||
|
||||
void send_client_to_tag(const user_action_arg_t *arg) {
|
||||
if (wm.client_focused) client_send_to_tag(wm.client_focused, arg->ui);
|
||||
}
|
||||
|
||||
void send_client_to_next_monitor(const user_action_arg_t *arg) {
|
||||
if (!wm.client_focused) return;
|
||||
|
||||
monitor_t *next_monitor = wm_get_next_monitor(wm.client_focused->monitor);
|
||||
if (wm.client_focused->monitor == next_monitor) return;
|
||||
|
||||
client_send_to_monitor(wm.client_focused, next_monitor);
|
||||
}
|
||||
|
||||
void focus_next_monitor(const user_action_arg_t *arg) {
|
||||
monitor_t *next_monitor = wm_get_next_monitor(wm.current_monitor);
|
||||
if (next_monitor == wm.current_monitor) return;
|
||||
|
||||
wm_set_current_monitor(next_monitor, true);
|
||||
if (wm.current_monitor->selected_tag->task_list) {
|
||||
monitor_deal_focus(wm.current_monitor);
|
||||
}
|
||||
}
|
||||
|
||||
void spawn(const user_action_arg_t *arg) {
|
||||
const char *cmd = (const char *)arg->ptr;
|
||||
if (cmd == nullptr || strlen(cmd) == 0) return;
|
||||
|
||||
g_spawn_command_line_async((const char *)arg->ptr, nullptr);
|
||||
}
|
||||
|
||||
void quit(const user_action_arg_t *arg) {
|
||||
const bool restart = arg->b;
|
||||
if (restart) {
|
||||
wm_restart();
|
||||
} else {
|
||||
wm_quit();
|
||||
}
|
||||
}
|
||||
|
||||
void raise_or_run(const user_action_arg_t *arg) {
|
||||
const char *class = ((const char **)arg->ptr)[0];
|
||||
client_t *client = client_get_next_by_class(wm.client_focused, class);
|
||||
|
||||
if (client) {
|
||||
bool monitor_changed = client->monitor != wm.current_monitor;
|
||||
bool tag_changed = client->tags != client->monitor->selected_tag->mask;
|
||||
if (monitor_changed || tag_changed) {
|
||||
point_t point = monitor_changed ? monitor_get_restore_cursor_point(
|
||||
client->monitor)
|
||||
: xcursor_query_pointer_position();
|
||||
wm_ignore_enter_notify_at_point(point);
|
||||
}
|
||||
wm_set_current_monitor(client->monitor, true);
|
||||
monitor_select_tag(client->monitor, client->tags);
|
||||
client_stack_raise(client);
|
||||
client_focus(client);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *command = ((const char **)arg->ptr)[1];
|
||||
user_action_arg_t arguments = {.ptr = command};
|
||||
spawn(&arguments);
|
||||
}
|
||||
|
||||
void toggle_mute(const user_action_arg_t *arg) {
|
||||
if (wm.status->pulse_context) toggle_pulse_mute(wm.status->pulse_context);
|
||||
}
|
||||
|
||||
void change_volume(const user_action_arg_t *arg) {
|
||||
if (wm.status->pulse_context) {
|
||||
change_pulse_volume(wm.status->pulse_context, arg->i);
|
||||
}
|
||||
}
|
||||
|
||||
void toggle_client_floating(const user_action_arg_t *arg) {
|
||||
if (!wm.client_focused) return;
|
||||
|
||||
client_set_floating(wm.client_focused, !wm.client_focused->floating);
|
||||
}
|
||||
|
||||
void toggle_client_fullscreen(const user_action_arg_t *arg) {
|
||||
if (!wm.client_focused) return;
|
||||
|
||||
client_set_fullscreen(wm.client_focused, !wm.client_focused->fullscreen);
|
||||
}
|
||||
|
||||
void toggle_client_maximize(const user_action_arg_t *arg) {
|
||||
if (!wm.client_focused) return;
|
||||
|
||||
client_set_maximize(wm.client_focused, !wm.client_focused->maximize);
|
||||
}
|
||||
|
||||
void toggle_client_minimize(const user_action_arg_t *arg) {
|
||||
client_t *client = wm.client_focused;
|
||||
if (arg->ptr) client = (client_t *)arg->ptr;
|
||||
if (!client) return;
|
||||
|
||||
client_set_minimize(client, !client->minimize);
|
||||
}
|
||||
|
||||
void kill_client(const user_action_arg_t *arg) {
|
||||
if (wm.client_focused) {
|
||||
client_kill(wm.client_focused);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据 client 的当前状态切换 client 下一状态
|
||||
* @description 如果当前 client 被遮挡了或者最小化了,则显示出来,否则最小化
|
||||
*/
|
||||
void change_client_state_dwim(const user_action_arg_t *arg) {
|
||||
client_t *client = (client_t *)arg->ptr;
|
||||
if (!client) return;
|
||||
|
||||
if (client != wm.client_stack_list &&
|
||||
client_get_obscured_state(client, wm.client_stack_list) !=
|
||||
obscured_none) {
|
||||
client_stack_raise(client);
|
||||
client_focus(client);
|
||||
return;
|
||||
}
|
||||
|
||||
client_set_minimize(client, !client->minimize);
|
||||
if (!client->minimize) client_stack_raise(client);
|
||||
}
|
||||
67
src/action.h
Normal file
67
src/action.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
typedef union user_action_arg_t {
|
||||
bool b;
|
||||
int32_t i;
|
||||
uint32_t ui;
|
||||
const void *ptr;
|
||||
} user_action_arg_t;
|
||||
|
||||
typedef enum click_area_t {
|
||||
click_none,
|
||||
click_tag,
|
||||
click_client_name,
|
||||
} click_area_t;
|
||||
|
||||
typedef enum modifier_t {
|
||||
modifier_none = 0,
|
||||
modifier_shift = XCB_MOD_MASK_SHIFT,
|
||||
modifier_control = XCB_MOD_MASK_CONTROL,
|
||||
modifier_alt = XCB_MOD_MASK_1,
|
||||
modifier_super = XCB_MOD_MASK_4,
|
||||
modifier_any = XCB_MOD_MASK_ANY,
|
||||
} modifier_t;
|
||||
|
||||
typedef uint16_t modifier_value_t;
|
||||
|
||||
typedef enum button_index_t {
|
||||
button_any = XCB_BUTTON_INDEX_ANY,
|
||||
button_left = XCB_BUTTON_INDEX_1,
|
||||
button_right = XCB_BUTTON_INDEX_2,
|
||||
button_middle = XCB_BUTTON_INDEX_3,
|
||||
} button_index_t;
|
||||
|
||||
typedef struct button_t {
|
||||
click_area_t click_area : 16;
|
||||
modifier_value_t modifiers;
|
||||
button_index_t button;
|
||||
void (*func)(const user_action_arg_t *arg);
|
||||
const user_action_arg_t arg;
|
||||
} button_t;
|
||||
|
||||
typedef struct keyboard_t {
|
||||
modifier_value_t modifiers;
|
||||
xcb_keysym_t keysym;
|
||||
void (*func)(const user_action_arg_t *arg);
|
||||
const user_action_arg_t arg;
|
||||
} keyboard_t;
|
||||
|
||||
void focus_client_in_same_tag(const user_action_arg_t *arg);
|
||||
void select_tag_of_current_monitor(const user_action_arg_t *arg);
|
||||
void send_client_to_tag(const user_action_arg_t *arg);
|
||||
void send_client_to_next_monitor(const user_action_arg_t *arg);
|
||||
void focus_next_monitor(const user_action_arg_t *arg);
|
||||
void spawn(const user_action_arg_t *arg);
|
||||
void quit(const user_action_arg_t *arg);
|
||||
void raise_or_run(const user_action_arg_t *arg);
|
||||
void toggle_mute(const user_action_arg_t *arg);
|
||||
void change_volume(const user_action_arg_t *arg);
|
||||
void toggle_client_floating(const user_action_arg_t *arg);
|
||||
void toggle_client_fullscreen(const user_action_arg_t *arg);
|
||||
void toggle_client_maximize(const user_action_arg_t *arg);
|
||||
void toggle_client_minimize(const user_action_arg_t *arg);
|
||||
void kill_client(const user_action_arg_t *arg);
|
||||
void change_client_state_dwim(const user_action_arg_t *arg);
|
||||
5
src/app.h.in
Normal file
5
src/app.h.in
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define APP_NAME "@APP_NAME@"
|
||||
|
||||
#cmakedefine HAS_EXECINFO
|
||||
34
src/atoms-list.txt
Normal file
34
src/atoms-list.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
_XKB_RULES_NAMES
|
||||
|
||||
_XROOTPMAP_ID
|
||||
ESETROOT_PMAP_ID
|
||||
|
||||
WM_CHANGE_STATE
|
||||
WM_DELETE_WINDOW
|
||||
WM_PROTOCOLS
|
||||
WM_TAKE_FOCUS
|
||||
WM_NAME
|
||||
WM_WINDOW_ROLE
|
||||
|
||||
UTF8_STRING
|
||||
COMPOUND_TEXT
|
||||
MANAGER
|
||||
_XEMBED
|
||||
_XEMBED_INFO
|
||||
|
||||
_NET_ACTIVE_WINDOW
|
||||
_NET_CLIENT_LIST
|
||||
_NET_SYSTEM_TRAY_COLORS
|
||||
_NET_SYSTEM_TRAY_OPCODE
|
||||
_NET_SYSTEM_TRAY_ORIENTATION
|
||||
_NET_WM_DESKTOP
|
||||
_NET_WM_NAME
|
||||
_NET_WM_STATE
|
||||
_NET_WM_STATE_FULLSCREEN
|
||||
_NET_WM_STATE_MAXIMIZED_HORZ
|
||||
_NET_WM_STATE_MAXIMIZED_VERT
|
||||
_NET_WM_STATE_SKIP_TASKBAR
|
||||
_NET_WM_WINDOW_TYPE
|
||||
_NET_WM_WINDOW_TYPE_DIALOG
|
||||
_NET_SUPPORTED
|
||||
_NET_SUPPORTING_WM_CHECK
|
||||
39
src/atoms.c
Normal file
39
src/atoms.c
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "atoms.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "atoms-intern.h"
|
||||
#include "utils.h"
|
||||
#include "wm.h"
|
||||
|
||||
void atoms_init(xcb_connection_t *conn) {
|
||||
xcb_intern_atom_cookie_t cookies[countof(ATOM_LIST)];
|
||||
for (int i = 0; i < countof(ATOM_LIST); i++) {
|
||||
atom_item_t atom = ATOM_LIST[i];
|
||||
cookies[i] = xcb_intern_atom_unchecked(conn, false, atom.len, atom.name);
|
||||
}
|
||||
|
||||
atom_item_t *atom = nullptr;
|
||||
uint32_t supported_length = 0;
|
||||
xcb_atom_t supported_list[countof(ATOM_LIST)] = {XCB_ATOM_NONE};
|
||||
|
||||
xcb_intern_atom_reply_t *reply = nullptr;
|
||||
for (int i = 0; i < countof(ATOM_LIST); i++) {
|
||||
reply = xcb_intern_atom_reply(conn, cookies[i], nullptr);
|
||||
if (reply) {
|
||||
atom = &ATOM_LIST[i];
|
||||
*atom->atom = reply->atom;
|
||||
p_delete(&reply);
|
||||
|
||||
if (strstr(atom->name, "_NET_") == atom->name) {
|
||||
supported_list[supported_length++] = *atom->atom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, wm.screen->root,
|
||||
_NET_SUPPORTED, XCB_ATOM_ATOM, 32, supported_length,
|
||||
supported_list);
|
||||
}
|
||||
5
src/atoms.h
Normal file
5
src/atoms.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <xcb/xcb.h>
|
||||
|
||||
void atoms_init(xcb_connection_t *conn);
|
||||
182
src/audio.c
Normal file
182
src/audio.c
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "audio.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <math.h>
|
||||
#include <pulse/context.h>
|
||||
#include <pulse/def.h>
|
||||
#include <pulse/glib-mainloop.h>
|
||||
#include <pulse/introspect.h>
|
||||
#include <pulse/mainloop-api.h>
|
||||
#include <pulse/proplist.h>
|
||||
#include <pulse/subscribe.h>
|
||||
#include <pulse/volume.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "app.h"
|
||||
#include "utils.h"
|
||||
|
||||
struct pulse_context_t {
|
||||
GMainContext *g_context;
|
||||
pulse_notify_t callback;
|
||||
pa_glib_mainloop *loop;
|
||||
pa_context *context;
|
||||
pa_cvolume volume;
|
||||
pulse_t pulse;
|
||||
uint32_t index;
|
||||
bool inited;
|
||||
};
|
||||
|
||||
static void clean_pulse_context(pulse_context_t *context, bool full_cleanup);
|
||||
static void init_pulse_context(pulse_context_t *context, bool create_loop);
|
||||
static void state_callback(pa_context *context, void *userdata);
|
||||
static void subscribe_callback(pa_context *context,
|
||||
pa_subscription_event_type_t t, uint32_t index,
|
||||
void *userdata);
|
||||
static void server_info_callback(pa_context *context,
|
||||
const pa_server_info *info, void *userdata);
|
||||
static void sink_info_callback(pa_context *context, const pa_sink_info *info,
|
||||
int eol, void *userdata);
|
||||
static void parse_sink(const pa_sink_info *info, pulse_t *pulse);
|
||||
|
||||
pulse_context_t *init_pulse(GMainContext *context, pulse_notify_t callback) {
|
||||
pulse_context_t *ctx = p_new(pulse_context_t, 1);
|
||||
ctx->g_context = context;
|
||||
ctx->callback = callback;
|
||||
init_pulse_context(ctx, true);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void change_pulse_volume(pulse_context_t *context, int step) {
|
||||
if (!context->inited || !step || step > 100 || step < -100) return;
|
||||
|
||||
pa_cvolume vol = context->volume;
|
||||
if (step > 0) {
|
||||
pa_cvolume_inc(&vol, PA_VOLUME_NORM * step / 100);
|
||||
if (pa_cvolume_max(&vol) > PA_VOLUME_NORM) {
|
||||
pa_cvolume_set(&vol, vol.channels, PA_VOLUME_NORM);
|
||||
}
|
||||
} else {
|
||||
pa_cvolume_dec(&vol, PA_VOLUME_NORM * (-step) / 100);
|
||||
if (pa_cvolume_min(&vol) < PA_VOLUME_MUTED) {
|
||||
pa_cvolume_set(&vol, vol.channels, PA_VOLUME_MUTED);
|
||||
}
|
||||
}
|
||||
pa_context *ctx = context->context;
|
||||
uint32_t index = context->index;
|
||||
pa_context_set_sink_volume_by_index(ctx, index, &vol, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void toggle_pulse_mute(pulse_context_t *context) {
|
||||
if (!context->inited) return;
|
||||
|
||||
pa_context *ctx = context->context;
|
||||
uint32_t index = context->index;
|
||||
bool mute = !context->pulse.mute;
|
||||
pa_context_set_sink_mute_by_index(ctx, index, mute, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void clean_pulse(pulse_context_t *context) {
|
||||
clean_pulse_context(context, true);
|
||||
p_delete(&context);
|
||||
}
|
||||
|
||||
void clean_pulse_context(pulse_context_t *context, bool full_cleanup) {
|
||||
memset(&context->pulse, 0, sizeof(context->pulse));
|
||||
if (context->callback) context->callback(&context->pulse);
|
||||
if (context->context) {
|
||||
pa_context_set_state_callback(context->context, nullptr, nullptr);
|
||||
pa_context_set_subscribe_callback(context->context, nullptr, nullptr);
|
||||
pa_context_disconnect(context->context);
|
||||
pa_context_unref(context->context);
|
||||
context->context = nullptr;
|
||||
}
|
||||
context->inited = false;
|
||||
if (full_cleanup && context->loop) {
|
||||
pa_glib_mainloop_free(context->loop);
|
||||
context->loop = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void init_pulse_context(pulse_context_t *context, bool create_loop) {
|
||||
if (create_loop) {
|
||||
if (context->loop) pa_glib_mainloop_free(context->loop);
|
||||
context->loop = pa_glib_mainloop_new(context->g_context);
|
||||
}
|
||||
pa_mainloop_api *api = pa_glib_mainloop_get_api(context->loop);
|
||||
context->context = pa_context_new(api, APP_NAME "-audio-status");
|
||||
pa_context_connect(context->context, nullptr, PA_CONTEXT_NOFAIL, nullptr);
|
||||
pa_context_set_state_callback(context->context, state_callback, context);
|
||||
}
|
||||
|
||||
void state_callback(pa_context *context, void *userdata) {
|
||||
pulse_context_t *ctx = userdata;
|
||||
|
||||
pa_context_state_t state = pa_context_get_state(context);
|
||||
if (PA_CONTEXT_IS_GOOD(state)) {
|
||||
pa_context_set_subscribe_callback(context, subscribe_callback, ctx);
|
||||
pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, nullptr, nullptr);
|
||||
pa_context_get_server_info(context, server_info_callback, ctx);
|
||||
} else if (state == PA_CONTEXT_FAILED) {
|
||||
clean_pulse_context(ctx, false);
|
||||
init_pulse_context(ctx, false);
|
||||
}
|
||||
}
|
||||
|
||||
void subscribe_callback(pa_context *context, pa_subscription_event_type_t t,
|
||||
uint32_t index, void *userdata) {
|
||||
auto facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
|
||||
if (facility != PA_SUBSCRIPTION_EVENT_SINK) return;
|
||||
|
||||
pa_subscription_event_type_t event = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
|
||||
if (event == PA_SUBSCRIPTION_EVENT_CHANGE) {
|
||||
pa_context_get_server_info(context, server_info_callback, userdata);
|
||||
}
|
||||
}
|
||||
|
||||
void server_info_callback(pa_context *context, const pa_server_info *info,
|
||||
void *userdata) {
|
||||
pa_context_get_sink_info_by_name(context, info->default_sink_name,
|
||||
sink_info_callback, userdata);
|
||||
}
|
||||
|
||||
void sink_info_callback(pa_context *context, const pa_sink_info *info, int eol,
|
||||
void *userdata) {
|
||||
if (eol != 0 || !info) return;
|
||||
|
||||
pulse_context_t *ctx = userdata;
|
||||
ctx->inited = true;
|
||||
ctx->volume = info->volume;
|
||||
ctx->index = info->index;
|
||||
parse_sink(info, &ctx->pulse);
|
||||
if (ctx->callback) ctx->callback(&ctx->pulse);
|
||||
}
|
||||
|
||||
static inline int volume_to_percent(pa_volume_t volume) {
|
||||
return (int)round(((double)volume) * 100 / PA_VOLUME_NORM);
|
||||
}
|
||||
|
||||
void parse_sink(const pa_sink_info *info, pulse_t *pulse) {
|
||||
size_t size = sizeof(pulse->device_name);
|
||||
pulse->mute = info->mute;
|
||||
pulse->volume_percent = volume_to_percent(pa_cvolume_avg(&info->volume));
|
||||
strncpy(pulse->device_name, info->description, size);
|
||||
|
||||
const char *key = nullptr;
|
||||
void *state = nullptr;
|
||||
while ((key = pa_proplist_iterate(info->proplist, &state)) != nullptr) {
|
||||
if (strcmp(key, "api.alsa.path") != 0 &&
|
||||
strcmp(key, "device.description") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *value = pa_proplist_gets(info->proplist, key);
|
||||
size_t len = strnlen(value, size);
|
||||
if (len && (len < strnlen(pulse->device_name, size))) {
|
||||
strncpy(pulse->device_name, value, size);
|
||||
pulse->device_name[size - 1] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/audio.h
Normal file
19
src/audio.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct pulse_context_t pulse_context_t;
|
||||
|
||||
typedef struct pulse_t {
|
||||
int volume_percent;
|
||||
bool mute;
|
||||
char device_name[255];
|
||||
} pulse_t;
|
||||
|
||||
typedef void (*pulse_notify_t)(pulse_t *pulse);
|
||||
|
||||
pulse_context_t *init_pulse(GMainContext *context, pulse_notify_t callback);
|
||||
void change_pulse_volume(pulse_context_t *context, int step);
|
||||
void toggle_pulse_mute(pulse_context_t *context);
|
||||
void clean_pulse(pulse_context_t *context);
|
||||
@@ -1,55 +0,0 @@
|
||||
#include "backend/output_utils.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "base/memory.h"
|
||||
#include "interface/backend.h"
|
||||
#include "interface/types.h"
|
||||
|
||||
static inline int32_t right(rect_t a) { return a.x + a.width; }
|
||||
static inline int32_t bottom(rect_t a) { return a.y + a.height; }
|
||||
static inline bool fully_contains(rect_t outer, rect_t inner) {
|
||||
return inner.x >= outer.x && inner.y >= outer.y &&
|
||||
right(inner) <= right(outer) && bottom(inner) <= bottom(outer);
|
||||
}
|
||||
|
||||
backend_detect_t *
|
||||
output_remove_duplication(const output_info_t *output, const size_t count) {
|
||||
if (!output || count < 1) return nullptr;
|
||||
|
||||
bool *remove = p_new(bool, count);
|
||||
|
||||
for (int32_t i = (int32_t)count - 1; i >= 0; i--) {
|
||||
if (remove[i]) continue;
|
||||
for (int32_t j = i - 1; j >= 0; j--) {
|
||||
if (remove[j]) continue;
|
||||
if (fully_contains(output[j].geometry, output[i].geometry)) {
|
||||
remove[i] = true;
|
||||
} else if (fully_contains(output[i].geometry, output[j].geometry)) {
|
||||
remove[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t amount = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!remove[i]) amount++;
|
||||
}
|
||||
if (!amount) {
|
||||
p_delete(&remove);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
output_info_t *list = p_new(output_info_t, amount);
|
||||
amount = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (remove[i]) continue;
|
||||
list[amount++] = output[i];
|
||||
}
|
||||
p_delete(&remove);
|
||||
backend_detect_t *detect = p_new(backend_detect_t, 1);
|
||||
detect->outputs = list;
|
||||
detect->output_count = amount;
|
||||
return detect;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "interface/backend.h"
|
||||
|
||||
backend_detect_t *
|
||||
output_remove_duplication(const output_info_t *output, const size_t count);
|
||||
@@ -1,858 +0,0 @@
|
||||
#include "interface/backend.h"
|
||||
|
||||
#include <cairo-xcb.h>
|
||||
#include <cairo.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <xcb/randr.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xcb_keysyms.h>
|
||||
#include <xcb/xfixes.h>
|
||||
#include <xcb/xinerama.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "backend/output_utils.h"
|
||||
#include "backend/x11/cursor.h"
|
||||
#include "backend/x11/window.h"
|
||||
#include "base/app.h"
|
||||
#include "base/array.h"
|
||||
#include "base/log.h"
|
||||
#include "base/macros.h"
|
||||
#include "base/memory.h"
|
||||
#include "common/window.h"
|
||||
#include "internal.h"
|
||||
|
||||
typedef struct atom_item_t {
|
||||
const char *name;
|
||||
size_t len;
|
||||
xcb_atom_t *atom;
|
||||
} atom_item_t;
|
||||
|
||||
static void atoms_init(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
atoms_t *atoms = &backend->atoms;
|
||||
|
||||
#define ATOM_ITEM(name) {#name, sizeof(#name) - 1, &atoms->name},
|
||||
atom_item_t atom_list[] = {ATOM_LIST(ATOM_ITEM)};
|
||||
#undef ATOM_ITEM
|
||||
|
||||
xcb_intern_atom_cookie_t cookies[countof(atom_list)];
|
||||
for (size_t i = 0; i < countof(atom_list); ++i) {
|
||||
const atom_item_t *atom = &atom_list[i];
|
||||
cookies[i] = xcb_intern_atom_unchecked(conn, false, atom->len, atom->name);
|
||||
}
|
||||
|
||||
xcb_intern_atom_reply_t *reply = nullptr;
|
||||
for (size_t i = 0; i < countof(atom_list); ++i) {
|
||||
reply = xcb_intern_atom_reply(conn, cookies[i], nullptr);
|
||||
if (!reply) continue;
|
||||
|
||||
atom_item_t *atom = &atom_list[i];
|
||||
*atom->atom = reply->atom;
|
||||
p_delete(&reply);
|
||||
}
|
||||
|
||||
#define EWMH_ITEM(name) atoms->name,
|
||||
xcb_atom_t ewmh_supported_atoms[] = {EWMH_ATOMS(EWMH_ITEM)};
|
||||
#undef EWMH_ITEM
|
||||
auto root = backend->screen->root;
|
||||
xcb_change_property(
|
||||
conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
root,
|
||||
atoms->_NET_SUPPORTED,
|
||||
XCB_ATOM_ATOM,
|
||||
32,
|
||||
countof(ewmh_supported_atoms),
|
||||
ewmh_supported_atoms
|
||||
);
|
||||
}
|
||||
|
||||
static void create_wm_check_window(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_window_t root = backend->screen->root;
|
||||
uint8_t depth = backend->screen->root_depth;
|
||||
xcb_visualid_t v = backend->screen->root_visual;
|
||||
uint16_t _c = XCB_WINDOW_CLASS_COPY_FROM_PARENT;
|
||||
uint32_t m = XCB_NONE;
|
||||
void *p = nullptr;
|
||||
|
||||
xcb_window_t win = xcb_generate_id(conn);
|
||||
xcb_create_window(conn, depth, win, root, -1, -1, 1, 1, 0, _c, v, m, p);
|
||||
window_set_class_instance(conn, win);
|
||||
|
||||
window_set_name_static(conn, win, APP_NAME);
|
||||
uint8_t mode = XCB_PROP_MODE_REPLACE;
|
||||
xcb_atom_t prop = backend->atoms._NET_WM_NAME;
|
||||
xcb_atom_t type = backend->atoms.UTF8_STRING;
|
||||
auto data_len = sizeof(APP_NAME) - 1;
|
||||
xcb_change_property(conn, mode, win, prop, type, 8, data_len, APP_NAME);
|
||||
|
||||
prop = backend->atoms._NET_SUPPORTING_WM_CHECK;
|
||||
type = XCB_ATOM_WINDOW;
|
||||
xcb_change_property(conn, mode, win, prop, type, 32, 1, &win);
|
||||
xcb_change_property(conn, mode, root, prop, type, 32, 1, &win);
|
||||
|
||||
prop = backend->atoms._NET_WM_PID;
|
||||
type = XCB_ATOM_CARDINAL;
|
||||
pid_t pid = getpid();
|
||||
xcb_change_property(conn, mode, win, prop, type, 32, 1, &pid);
|
||||
}
|
||||
|
||||
static void create_no_focus_window(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_window_t root = backend->screen->root;
|
||||
uint8_t d = backend->screen->root_depth;
|
||||
xcb_visualid_t v = backend->screen->root_visual;
|
||||
uint16_t _c = XCB_WINDOW_CLASS_COPY_FROM_PARENT;
|
||||
|
||||
xcb_window_t win = xcb_generate_id(conn);
|
||||
uint32_t m = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
|
||||
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_COLORMAP;
|
||||
const xcb_create_window_value_list_t p = {
|
||||
.override_redirect = true,
|
||||
.background_pixel = backend->screen->black_pixel,
|
||||
.border_pixel = backend->screen->black_pixel,
|
||||
.colormap = backend->screen->default_colormap,
|
||||
};
|
||||
xcb_create_window_aux(conn, d, win, root, -1, -1, 1, 1, 0, _c, v, m, &p);
|
||||
window_set_class_instance(conn, win);
|
||||
window_set_name_static(conn, win, APP_NAME " no input window");
|
||||
xcb_map_window(conn, win);
|
||||
|
||||
backend->window_no_focus = win;
|
||||
}
|
||||
|
||||
backend_t *backend_create(const char *display_name) {
|
||||
int screen_num = 0;
|
||||
xcb_connection_t *conn = xcb_connect(display_name, &screen_num);
|
||||
int xcb_conn_error = xcb_connection_has_error(conn);
|
||||
if (xcb_conn_error) {
|
||||
fatal("cannot open display %s, error %d", display_name, xcb_conn_error);
|
||||
}
|
||||
|
||||
xcb_screen_t *screen = xcb_aux_get_screen(conn, screen_num);
|
||||
if (!screen) fatal("cannot get screen info");
|
||||
xcb_window_t root = screen->root;
|
||||
|
||||
uint32_t mask = XCB_CW_EVENT_MASK;
|
||||
const xcb_params_cw_t params = {
|
||||
.event_mask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
|
||||
};
|
||||
xcb_void_cookie_t cookie =
|
||||
xcb_aux_change_window_attributes_checked(conn, root, mask, ¶ms);
|
||||
if (xcb_request_check(conn, cookie)) {
|
||||
fatal(
|
||||
"another window manager is already running (cannot select "
|
||||
"SubstructureRedirect)"
|
||||
);
|
||||
}
|
||||
|
||||
backend_t *backend = p_new(backend_t, 1);
|
||||
backend->conn = conn;
|
||||
backend->screen = screen;
|
||||
backend->screenp = screen_num;
|
||||
|
||||
xcb_prefetch_extension_data(conn, &xcb_xfixes_id);
|
||||
const xcb_query_extension_reply_t *query =
|
||||
xcb_get_extension_data(conn, &xcb_xfixes_id);
|
||||
if (query && query->present) {
|
||||
xcb_xfixes_query_version_cookie_t cookie = xcb_xfixes_query_version(
|
||||
conn,
|
||||
XCB_XFIXES_MAJOR_VERSION,
|
||||
XCB_XFIXES_MINOR_VERSION
|
||||
);
|
||||
xcb_xfixes_query_version_reply_t *reply =
|
||||
xcb_xfixes_query_version_reply(conn, cookie, nullptr);
|
||||
|
||||
if (reply) {
|
||||
backend->have_xfixes = true;
|
||||
p_delete(&reply);
|
||||
}
|
||||
} else {
|
||||
backend->have_xfixes = false;
|
||||
}
|
||||
|
||||
backend->key_symbols = xcb_key_symbols_alloc(conn);
|
||||
|
||||
cursor_init(backend);
|
||||
atoms_init(backend);
|
||||
create_wm_check_window(backend);
|
||||
create_no_focus_window(backend);
|
||||
|
||||
window_change_cursor(backend, root, ZDWM_CURSOR_NORMAL);
|
||||
|
||||
return backend;
|
||||
}
|
||||
|
||||
void backend_destroy(backend_t *backend) {
|
||||
if (!backend) return;
|
||||
|
||||
p_delete(&backend->config_list.cfgs);
|
||||
p_clear(&backend->config_list, 1);
|
||||
|
||||
window_list_cleanup(&backend->unmap);
|
||||
window_list_cleanup(&backend->map);
|
||||
window_list_cleanup(&backend->kill);
|
||||
|
||||
xcb_key_symbols_free(backend->key_symbols);
|
||||
backend->key_symbols = nullptr;
|
||||
|
||||
cursor_cleanup(backend);
|
||||
|
||||
xcb_disconnect(backend->conn);
|
||||
backend->conn = nullptr;
|
||||
free(backend);
|
||||
}
|
||||
|
||||
int backend_get_fd(backend_t *backend) {
|
||||
if (!backend || !backend->conn) return -1;
|
||||
|
||||
return xcb_get_file_descriptor(backend->conn);
|
||||
}
|
||||
|
||||
static bool detect_monitor_by_randr(
|
||||
const backend_t *backend,
|
||||
output_info_t **outputs,
|
||||
size_t *count
|
||||
) {
|
||||
xcb_prefetch_extension_data(backend->conn, &xcb_randr_id);
|
||||
const xcb_query_extension_reply_t *query =
|
||||
xcb_get_extension_data(backend->conn, &xcb_randr_id);
|
||||
if (!query || !query->present) return false;
|
||||
|
||||
xcb_randr_query_version_cookie_t cookie = xcb_randr_query_version(
|
||||
backend->conn,
|
||||
XCB_RANDR_MAJOR_VERSION,
|
||||
XCB_RANDR_MINOR_VERSION
|
||||
);
|
||||
xcb_randr_query_version_reply_t *reply =
|
||||
xcb_randr_query_version_reply(backend->conn, cookie, nullptr);
|
||||
if (!reply) return false;
|
||||
|
||||
/* xcb_randr_get_monitors 接口是 1.5 引入的 */
|
||||
uint32_t major_version = reply->major_version;
|
||||
uint32_t minor_version = reply->minor_version;
|
||||
if (major_version < 1 || (major_version == 1 && minor_version < 5)) {
|
||||
p_delete(&reply);
|
||||
return false;
|
||||
}
|
||||
p_delete(&reply);
|
||||
|
||||
xcb_randr_get_monitors_cookie_t monitors_cookie =
|
||||
xcb_randr_get_monitors(backend->conn, backend->screen->root, 1);
|
||||
xcb_randr_get_monitors_reply_t *monitors_reply =
|
||||
xcb_randr_get_monitors_reply(backend->conn, monitors_cookie, nullptr);
|
||||
if (!monitors_reply) {
|
||||
warn("RandR get monitor failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
int len = xcb_randr_get_monitors_monitors_length(monitors_reply);
|
||||
if (len <= 0) {
|
||||
p_delete(&monitors_reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
output_info_t *output_list = p_new(output_info_t, len);
|
||||
xcb_randr_monitor_info_iterator_t iter =
|
||||
xcb_randr_get_monitors_monitors_iterator(monitors_reply);
|
||||
for (int i = 0; iter.rem; xcb_randr_monitor_info_next(&iter), i++) {
|
||||
output_info_t *output = &output_list[i];
|
||||
output->geometry.x = iter.data->x;
|
||||
output->geometry.y = iter.data->y;
|
||||
output->geometry.width = iter.data->width;
|
||||
output->geometry.height = iter.data->height;
|
||||
|
||||
xcb_get_atom_name_cookie_t name_cookie =
|
||||
xcb_get_atom_name_unchecked(backend->conn, iter.data->name);
|
||||
xcb_get_atom_name_reply_t *name_reply =
|
||||
xcb_get_atom_name_reply(backend->conn, name_cookie, nullptr);
|
||||
|
||||
if (name_reply) {
|
||||
char *name = xcb_get_atom_name_name(name_reply);
|
||||
int length = xcb_get_atom_name_name_length(name_reply);
|
||||
output->name = p_strndup(name, length);
|
||||
p_delete(&name_reply);
|
||||
}
|
||||
}
|
||||
|
||||
p_delete(&monitors_reply);
|
||||
|
||||
if (count) *count = (size_t)len;
|
||||
if (outputs) {
|
||||
*outputs = output_list;
|
||||
} else {
|
||||
for (size_t i = 0; i < (size_t)len; i++) p_delete(&output_list[i].name);
|
||||
p_delete(&output_list);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool detect_monitor_by_xinerama(
|
||||
const backend_t *backend,
|
||||
output_info_t **outputs,
|
||||
size_t *count
|
||||
) {
|
||||
xcb_prefetch_extension_data(backend->conn, &xcb_xinerama_id);
|
||||
const xcb_query_extension_reply_t *query =
|
||||
xcb_get_extension_data(backend->conn, &xcb_xinerama_id);
|
||||
if (!query || !query->present) return false;
|
||||
|
||||
xcb_xinerama_query_version_cookie_t cookie = xcb_xinerama_query_version(
|
||||
backend->conn,
|
||||
XCB_XINERAMA_MAJOR_VERSION,
|
||||
XCB_XINERAMA_MINOR_VERSION
|
||||
);
|
||||
xcb_xinerama_query_version_reply_t *version_reply =
|
||||
xcb_xinerama_query_version_reply(backend->conn, cookie, nullptr);
|
||||
if (!version_reply) return false;
|
||||
p_delete(&version_reply);
|
||||
|
||||
xcb_xinerama_is_active_cookie_t active_cookie =
|
||||
xcb_xinerama_is_active(backend->conn);
|
||||
xcb_xinerama_is_active_reply_t *active_reply =
|
||||
xcb_xinerama_is_active_reply(backend->conn, active_cookie, nullptr);
|
||||
bool active = active_reply && active_reply->state;
|
||||
if (active_reply) p_delete(&active_reply);
|
||||
if (!active) return false;
|
||||
|
||||
xcb_xinerama_query_screens_cookie_t screens_cookie =
|
||||
xcb_xinerama_query_screens(backend->conn);
|
||||
xcb_xinerama_query_screens_reply_t *screens_reply =
|
||||
xcb_xinerama_query_screens_reply(backend->conn, screens_cookie, nullptr);
|
||||
|
||||
if (!screens_reply) return false;
|
||||
|
||||
int len = xcb_xinerama_query_screens_screen_info_length(screens_reply);
|
||||
if (len <= 0) {
|
||||
p_delete(&screens_reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
xcb_xinerama_screen_info_t *screen_info =
|
||||
xcb_xinerama_query_screens_screen_info(screens_reply);
|
||||
if (!screen_info) {
|
||||
p_delete(&screens_reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
output_info_t *output_list = p_new(output_info_t, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
output_info_t *output = &output_list[i];
|
||||
output->geometry.x = screen_info[i].x_org;
|
||||
output->geometry.y = screen_info[i].y_org;
|
||||
output->geometry.width = screen_info[i].width;
|
||||
output->geometry.height = screen_info[i].height;
|
||||
}
|
||||
p_delete(&screens_reply);
|
||||
|
||||
if (count) *count = (size_t)len;
|
||||
if (outputs) {
|
||||
*outputs = output_list;
|
||||
} else {
|
||||
for (size_t i = 0; i < (size_t)len; i++) p_delete(&output_list[i].name);
|
||||
p_delete(&output_list);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
backend_detect_t *backend_detect(backend_t *backend) {
|
||||
output_info_t *outputs = nullptr;
|
||||
size_t count = 0;
|
||||
|
||||
if (detect_monitor_by_randr(backend, &outputs, &count)) {
|
||||
backend_detect_t *detect = output_remove_duplication(outputs, count);
|
||||
p_delete(&outputs);
|
||||
return detect;
|
||||
}
|
||||
|
||||
if (detect_monitor_by_xinerama(backend, &outputs, &count)) {
|
||||
backend_detect_t *detect = output_remove_duplication(outputs, count);
|
||||
p_delete(&outputs);
|
||||
return detect;
|
||||
}
|
||||
|
||||
output_info_t *output = p_new(output_info_t, 1);
|
||||
output->geometry.x = 0;
|
||||
output->geometry.y = 0;
|
||||
output->geometry.width = backend->screen->width_in_pixels;
|
||||
output->geometry.height = backend->screen->height_in_pixels;
|
||||
|
||||
backend_detect_t *detect = p_new(backend_detect_t, 1);
|
||||
detect->outputs = output;
|
||||
detect->output_count = 1;
|
||||
return detect;
|
||||
}
|
||||
|
||||
void backend_detect_destroy(backend_detect_t *detect) {
|
||||
if (!detect) return;
|
||||
|
||||
for (size_t i = 0; i < detect->output_count; i++) {
|
||||
p_delete(&detect->outputs[i].name);
|
||||
}
|
||||
|
||||
p_delete(&detect->outputs);
|
||||
free(detect);
|
||||
}
|
||||
|
||||
static void backend_focus_window(backend_t *backend, xcb_window_t window) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_window_t root = backend->screen->root;
|
||||
const atoms_t *atoms = &backend->atoms;
|
||||
xcb_atom_t property = atoms->_NET_ACTIVE_WINDOW;
|
||||
xcb_timestamp_t time = XCB_TIME_CURRENT_TIME;
|
||||
if (window == XCB_WINDOW_NONE || window == root) {
|
||||
window = backend->window_no_focus;
|
||||
xcb_set_input_focus(conn, XCB_INPUT_FOCUS_PARENT, window, time);
|
||||
xcb_delete_property(conn, root, property);
|
||||
} else {
|
||||
uint8_t mode = XCB_PROP_MODE_REPLACE;
|
||||
xcb_atom_t type = XCB_ATOM_WINDOW;
|
||||
xcb_set_input_focus(conn, XCB_INPUT_FOCUS_PARENT, window, time);
|
||||
xcb_change_property(conn, mode, root, property, type, 32, 1, &window);
|
||||
window_takefocus(backend, window);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void backend_change_window_list(
|
||||
backend_t *backend,
|
||||
bool stack_list,
|
||||
const effect_window_list_t *list
|
||||
) {
|
||||
xcb_change_property(
|
||||
backend->conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
backend->screen->root,
|
||||
stack_list ? backend->atoms._NET_CLIENT_LIST_STACKING
|
||||
: backend->atoms._NET_CLIENT_LIST,
|
||||
XCB_ATOM_WINDOW,
|
||||
32,
|
||||
list->count,
|
||||
list->windows
|
||||
);
|
||||
}
|
||||
|
||||
static void
|
||||
backend_restack_windows(backend_t *backend, const effect_window_list_t *list) {
|
||||
if (list->count == 0) return;
|
||||
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_window_t sibling_window = backend->window_no_focus;
|
||||
for (size_t i = 0; i < list->count; ++i) {
|
||||
uint16_t mask = XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE;
|
||||
xcb_configure_window_value_list_t params = {
|
||||
.sibling = sibling_window,
|
||||
.stack_mode = XCB_STACK_MODE_ABOVE
|
||||
};
|
||||
sibling_window = list->windows[i];
|
||||
xcb_configure_window_aux(conn, sibling_window, mask, ¶ms);
|
||||
}
|
||||
}
|
||||
|
||||
static void window_configure_list_reset(window_configure_list_t *configs) {
|
||||
p_clear(configs->cfgs, configs->count);
|
||||
configs->count = 0;
|
||||
}
|
||||
|
||||
static void backend_apply_window_configure_list(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
window_configure_list_t *configs = &backend->config_list;
|
||||
for (size_t i = 0; i < configs->count; ++i) {
|
||||
window_configure_t *cfg = &configs->cfgs[i];
|
||||
if (!cfg->mask) continue;
|
||||
|
||||
xcb_configure_window_aux(conn, cfg->window, cfg->mask, &cfg->value);
|
||||
}
|
||||
}
|
||||
|
||||
static window_configure_t *
|
||||
find_or_push_configure(window_configure_list_t *configs, xcb_window_t window) {
|
||||
window_configure_t *cfg = nullptr;
|
||||
for (size_t i = 0; i < configs->count; ++i) {
|
||||
cfg = &configs->cfgs[i];
|
||||
if (cfg->window == window) return cfg;
|
||||
}
|
||||
|
||||
cfg = array_push(configs->cfgs, configs->count, configs->capacity);
|
||||
p_clear(cfg, 1);
|
||||
cfg->window = window;
|
||||
cfg->mask = 0;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static void merge_window_configure_params(
|
||||
backend_t *backend,
|
||||
const configure_data_t *data
|
||||
) {
|
||||
auto cfg = find_or_push_configure(&backend->config_list, data->window);
|
||||
|
||||
#define FIELD_MERGE(MASK, VALUE_FIELD, DATA_FIELD) \
|
||||
if (data->changed_fields & (MASK)) { \
|
||||
cfg->mask |= (MASK); \
|
||||
cfg->value.VALUE_FIELD = data->DATA_FIELD; \
|
||||
}
|
||||
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_X, x, x);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_Y, y, y);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_WIDTH, width, width);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_HEIGHT, height, height);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_BORDER_WIDTH, border_width, border_width);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_SIBLING, sibling, sibling);
|
||||
FIELD_MERGE(ZDWM_CONFIGURE_FIELD_STACK_MODE, stack_mode, stack_mode);
|
||||
|
||||
#undef FIELD_MERGE
|
||||
}
|
||||
|
||||
static void
|
||||
backend_bind_key(backend_t *backend, const effect_bind_key_t *bind_key) {
|
||||
auto root = backend->screen->root;
|
||||
window_grab_keys(backend, root, bind_key->keys, bind_key->count);
|
||||
}
|
||||
|
||||
static void backend_grab_button(
|
||||
backend_t *backend,
|
||||
const effect_grab_button_t *grab_button
|
||||
) {
|
||||
auto conn = backend->conn;
|
||||
auto window = grab_button->window;
|
||||
xcb_ungrab_button(conn, XCB_BUTTON_INDEX_ANY, window, XCB_MOD_MASK_ANY);
|
||||
|
||||
auto buttons = grab_button->buttons;
|
||||
for (size_t i = 0; i < grab_button->count; ++i) {
|
||||
auto button = buttons[i].button;
|
||||
auto modifiers = buttons[i].modifiers;
|
||||
window_grab_button(backend, window, button, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
static void backend_grab_pointer(backend_t *backend, cursor_t cursor) {
|
||||
auto conn = backend->conn;
|
||||
auto root = backend->screen->root;
|
||||
auto mask = XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE |
|
||||
XCB_EVENT_MASK_POINTER_MOTION;
|
||||
auto mode = XCB_GRAB_MODE_ASYNC;
|
||||
auto xcursor = cursor_get_xcb_cursor(backend, cursor);
|
||||
auto time = XCB_TIME_CURRENT_TIME;
|
||||
xcb_grab_pointer(conn, false, root, mask, mode, mode, root, xcursor, time);
|
||||
}
|
||||
|
||||
static void backend_merge_effects(
|
||||
backend_t *backend,
|
||||
const effect_t *effects,
|
||||
size_t effect_count
|
||||
) {
|
||||
for (size_t i = 0; i < effect_count; ++i) {
|
||||
const effect_t *e = &effects[i];
|
||||
switch (e->type) {
|
||||
case ZDWM_EFFECT_MAP_WINDOW:
|
||||
window_list_push(&backend->map, e->as.map.window);
|
||||
break;
|
||||
case ZDWM_EFFECT_UNMAP_WINDOW:
|
||||
window_list_push(&backend->unmap, e->as.unmap.window);
|
||||
break;
|
||||
case ZDWM_EFFECT_FOCUS_WINDOW:
|
||||
backend->focus_window = e->as.focus.window;
|
||||
backend->update_focus = true;
|
||||
break;
|
||||
case ZDWM_EFFECT_KILL_WINDOW:
|
||||
window_list_push(&backend->kill, e->as.kill.window);
|
||||
break;
|
||||
case ZDWM_EFFECT_WITHDRAW_WINDOW:
|
||||
window_set_icccm_wm_state(
|
||||
backend->conn,
|
||||
e->as.withdraw.window,
|
||||
XCB_ICCCM_WM_STATE_WITHDRAWN
|
||||
);
|
||||
break;
|
||||
case ZDWM_EFFECT_START_MOVE_WINDOW:
|
||||
backend_grab_pointer(backend, ZDWM_CURSOR_MOVE);
|
||||
break;
|
||||
case ZDWM_EFFECT_START_RESIZE_WINDOW:
|
||||
backend_grab_pointer(backend, ZDWM_CURSOR_RESIZE);
|
||||
break;
|
||||
case ZDWM_EFFECT_MINIMIZE_WINDOW: {
|
||||
auto window = e->as.minimize.window;
|
||||
xcb_icccm_wm_state_t state = XCB_ICCCM_WM_STATE_NORMAL;
|
||||
if (e->as.minimize.value) state = XCB_ICCCM_WM_STATE_ICONIC;
|
||||
window_set_icccm_wm_state(backend->conn, window, state);
|
||||
} break;
|
||||
case ZDWM_EFFECT_MAXIMIZE_WINDOW: {
|
||||
auto window = e->as.maximize.window;
|
||||
if (e->as.maximize.value) {
|
||||
xcb_atom_t max_atoms[] = {
|
||||
backend->atoms._NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
backend->atoms._NET_WM_STATE_MAXIMIZED_VERT
|
||||
};
|
||||
window_set_net_wm_state(backend, window, countof(max_atoms), max_atoms);
|
||||
} else {
|
||||
window_set_net_wm_state(backend, window, 0, nullptr);
|
||||
}
|
||||
} break;
|
||||
case ZDWM_EFFECT_FULLSCREEN_WINDOW: {
|
||||
auto window = e->as.fullscreen.window;
|
||||
if (e->as.fullscreen.value) {
|
||||
auto fullscreen_atom = backend->atoms._NET_WM_STATE_FULLSCREEN;
|
||||
window_set_net_wm_state(backend, window, 1, &fullscreen_atom);
|
||||
} else {
|
||||
window_set_net_wm_state(backend, window, 0, nullptr);
|
||||
}
|
||||
} break;
|
||||
case ZDWM_EFFECT_CONFIGURE_WINDOW:
|
||||
merge_window_configure_params(backend, &e->as.configure);
|
||||
break;
|
||||
case ZDWM_EFFECT_CHANGE_BORDER_COLOR: {
|
||||
xcb_change_window_attributes_value_list_t value = {
|
||||
.border_pixel = e->as.change_border_color.color->argb
|
||||
};
|
||||
xcb_change_window_attributes_aux(
|
||||
backend->conn,
|
||||
e->as.change_border_color.window,
|
||||
XCB_CW_BORDER_PIXEL,
|
||||
&value
|
||||
);
|
||||
} break;
|
||||
case ZDWM_EFFECT_CHANGE_WINDOW_LIST:
|
||||
backend_change_window_list(backend, false, &e->as.change_window_list);
|
||||
break;
|
||||
case ZDWM_EFFECT_RESTACK_WINDOWS:
|
||||
backend_restack_windows(backend, &e->as.restack_windows);
|
||||
backend_change_window_list(backend, true, &e->as.restack_windows);
|
||||
break;
|
||||
case ZDWM_EFFECT_BIND_KEY:
|
||||
backend_bind_key(backend, &e->as.bind_key);
|
||||
break;
|
||||
case ZDWM_EFFECT_GRAB_BUTTON:
|
||||
backend_grab_button(backend, &e->as.grab_button);
|
||||
break;
|
||||
case ZDWM_EFFECT_UNGRAB_POINTER:
|
||||
xcb_ungrab_pointer(backend->conn, XCB_CURRENT_TIME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void backend_batch_apply_effects(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
if (backend->unmap.count) {
|
||||
xcb_grab_server(conn);
|
||||
window_clean_event_mask(conn, backend->screen->root);
|
||||
|
||||
window_list_t *unmap = &backend->unmap;
|
||||
for (size_t i = 0; i < unmap->count; ++i) {
|
||||
xcb_window_t window = unmap->windows[i];
|
||||
window_clean_event_mask(conn, window);
|
||||
xcb_unmap_window(conn, window);
|
||||
}
|
||||
|
||||
root_set_event_mask(backend);
|
||||
xcb_ungrab_server(conn);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < backend->map.count; ++i) {
|
||||
xcb_window_t window = backend->map.windows[i];
|
||||
window_set_event_mask(conn, window);
|
||||
xcb_map_window(conn, window);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < backend->kill.count; ++i) {
|
||||
window_kill(backend, backend->kill.windows[i]);
|
||||
}
|
||||
|
||||
backend_apply_window_configure_list(backend);
|
||||
|
||||
if (backend->update_focus) {
|
||||
backend_focus_window(backend, backend->focus_window);
|
||||
}
|
||||
}
|
||||
|
||||
bool backend_apply_effect(
|
||||
backend_t *backend,
|
||||
const effect_t *effects,
|
||||
size_t effect_count
|
||||
) {
|
||||
backend->update_focus = false;
|
||||
window_configure_list_reset(&backend->config_list);
|
||||
window_list_reset(&backend->unmap);
|
||||
window_list_reset(&backend->map);
|
||||
window_list_reset(&backend->kill);
|
||||
|
||||
backend_merge_effects(backend, effects, effect_count);
|
||||
backend_batch_apply_effects(backend);
|
||||
|
||||
xcb_flush(backend->conn);
|
||||
return true;
|
||||
}
|
||||
|
||||
backend_scan_result_t *backend_scan_windows(backend_t *backend) {
|
||||
auto conn = backend->conn;
|
||||
auto root = backend->screen->root;
|
||||
auto cookie = xcb_query_tree_unchecked(conn, root);
|
||||
auto reply = xcb_query_tree_reply(conn, cookie, nullptr);
|
||||
if (!reply) return nullptr;
|
||||
|
||||
auto length = xcb_query_tree_children_length(reply);
|
||||
if (!length) {
|
||||
p_delete(&reply);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto list = xcb_query_tree_children(reply);
|
||||
|
||||
auto result = p_new(backend_scan_result_t, 1);
|
||||
auto sub_result = p_new(backend_scan_result_t, 1);
|
||||
|
||||
for (typeof(length) i = 0; i < length; ++i) {
|
||||
auto window = list[i];
|
||||
auto wa = window_get_attributes(backend, window);
|
||||
if (!wa) continue;
|
||||
|
||||
auto override_redirect = wa->override_redirect;
|
||||
auto map_state = wa->map_state;
|
||||
p_delete(&wa);
|
||||
if (override_redirect) continue;
|
||||
|
||||
auto hints = (xcb_icccm_wm_hints_t){0};
|
||||
auto hints_getted = window_get_wm_hints(backend, window, &hints);
|
||||
if (!(map_state == XCB_MAP_STATE_VIEWABLE ||
|
||||
(hints_getted && hints.initial_state == XCB_ICCCM_WM_STATE_ICONIC))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto transient_for = window_get_transient_for(backend, window);
|
||||
auto r = transient_for == XCB_WINDOW_NONE ? result : sub_result;
|
||||
auto slot = array_push(r->windows, r->count, r->capacity);
|
||||
if (
|
||||
!populate_window_event(backend, list[i], slot) || slot->override_redirect
|
||||
) {
|
||||
window_layer_props_cleanup(&slot->props);
|
||||
window_metadata_cleanup(&slot->metadata);
|
||||
r->count--;
|
||||
}
|
||||
}
|
||||
|
||||
p_delete(&reply);
|
||||
|
||||
for (size_t i = 0; i < sub_result->count; ++i) {
|
||||
auto slot = array_push(result->windows, result->count, result->capacity);
|
||||
*slot = sub_result->windows[i];
|
||||
}
|
||||
|
||||
p_clear(sub_result->windows, sub_result->count);
|
||||
backend_scan_result_destroy(sub_result);
|
||||
|
||||
if (result->count == 0) {
|
||||
backend_scan_result_destroy(result);
|
||||
result = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void backend_scan_result_destroy(backend_scan_result_t *result) {
|
||||
if (!result) return;
|
||||
|
||||
for (size_t i = 0; i < result->count; i++) {
|
||||
window_map_request_event_t *ev = &result->windows[i];
|
||||
window_layer_props_cleanup(&ev->props);
|
||||
window_metadata_cleanup(&ev->metadata);
|
||||
}
|
||||
|
||||
p_delete(&result->windows);
|
||||
p_delete(&result);
|
||||
}
|
||||
|
||||
backend_bar_window_t backend_create_bar_window(
|
||||
backend_t *backend,
|
||||
rect_t geometry,
|
||||
uint32_t bg_pixel,
|
||||
bool enable_tray
|
||||
) {
|
||||
auto conn = backend->conn;
|
||||
auto root = backend->screen->root;
|
||||
auto visual = window_get_visual(backend, true);
|
||||
|
||||
static xcb_colormap_t colormap = XCB_NONE;
|
||||
if (colormap == XCB_NONE) {
|
||||
colormap = xcb_generate_id(conn);
|
||||
uint8_t alloc = XCB_COLORMAP_ALLOC_NONE;
|
||||
xcb_create_colormap(conn, alloc, colormap, root, visual->visual->visual_id);
|
||||
}
|
||||
|
||||
window_clean_event_mask(conn, root);
|
||||
xcb_grab_server(conn);
|
||||
|
||||
auto window_id = xcb_generate_id(conn);
|
||||
uint16_t _class = XCB_WINDOW_CLASS_INPUT_OUTPUT;
|
||||
uint32_t value_mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_BACK_PIXEL |
|
||||
XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK |
|
||||
XCB_CW_COLORMAP;
|
||||
|
||||
const xcb_create_window_value_list_t value_list = {
|
||||
.override_redirect = true,
|
||||
.background_pixel = bg_pixel,
|
||||
.border_pixel = 0,
|
||||
.event_mask = XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_EXPOSURE,
|
||||
.colormap = colormap,
|
||||
};
|
||||
auto cookie = xcb_create_window_aux_checked(
|
||||
conn,
|
||||
visual->depth,
|
||||
window_id,
|
||||
root,
|
||||
geometry.x,
|
||||
geometry.y,
|
||||
geometry.width,
|
||||
geometry.height,
|
||||
0,
|
||||
_class,
|
||||
visual->visual->visual_id,
|
||||
value_mask,
|
||||
&value_list
|
||||
);
|
||||
if (xcb_request_check(conn, cookie)) fatal("cannot create bar window");
|
||||
|
||||
cookie = xcb_map_window_checked(conn, window_id);
|
||||
if (xcb_request_check(conn, cookie)) fatal("cannot map bar window");
|
||||
|
||||
window_set_class_instance(conn, window_id);
|
||||
window_set_name_static(conn, window_id, APP_NAME "_bar");
|
||||
|
||||
xcb_ungrab_server(conn);
|
||||
root_set_event_mask(backend);
|
||||
xcb_aux_sync(conn);
|
||||
|
||||
auto width = geometry.width;
|
||||
auto height = geometry.height;
|
||||
auto vid = visual->visual;
|
||||
auto surface = cairo_xcb_surface_create(conn, window_id, vid, width, height);
|
||||
auto cr = cairo_create(surface);
|
||||
|
||||
cairo_surface_destroy(surface);
|
||||
p_delete(&visual);
|
||||
|
||||
auto statue = cairo_status(cr);
|
||||
if (statue != CAIRO_STATUS_SUCCESS) {
|
||||
fatal("cannot create cairo context: %s", cairo_status_to_string(statue));
|
||||
}
|
||||
|
||||
return (backend_bar_window_t){
|
||||
.window_id = window_id,
|
||||
.cr = cr,
|
||||
.tray = {
|
||||
.api = tray_api,
|
||||
.handle = backend,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
void backend_flush(backend_t *backend) { xcb_flush(backend->conn); }
|
||||
@@ -1,71 +0,0 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "interface/types.h"
|
||||
|
||||
typedef struct modifier_map_t {
|
||||
modifier_bit_t modifier;
|
||||
xcb_mod_mask_t mask;
|
||||
} modifier_map_t;
|
||||
|
||||
static constexpr modifier_map_t modifier_map[] = {
|
||||
{ZDWM_MOD_SHIFT, XCB_MOD_MASK_SHIFT},
|
||||
{ZDWM_MOD_CONTROL, XCB_MOD_MASK_CONTROL},
|
||||
{ZDWM_MOD_1, XCB_MOD_MASK_1},
|
||||
{ZDWM_MOD_2, XCB_MOD_MASK_2},
|
||||
{ZDWM_MOD_3, XCB_MOD_MASK_3},
|
||||
{ZDWM_MOD_4, XCB_MOD_MASK_4},
|
||||
{ZDWM_MOD_5, XCB_MOD_MASK_5},
|
||||
};
|
||||
|
||||
modifier_mask_t modifiers_xcb_to_zdwm(uint16_t mask) {
|
||||
modifier_mask_t mods = ZDWM_MOD_NONE;
|
||||
for (size_t i = 0; i < countof(modifier_map); ++i) {
|
||||
auto item = &modifier_map[i];
|
||||
if (mask & item->mask) mods |= item->modifier;
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
uint16_t modifiers_zdwm_to_xcb(modifier_mask_t mask) {
|
||||
uint16_t mods = 0U;
|
||||
for (size_t i = 0; i < countof(modifier_map); ++i) {
|
||||
auto item = &modifier_map[i];
|
||||
if (mask & item->modifier) mods |= item->mask;
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
typedef struct button_map_t {
|
||||
button_t zdwm_button;
|
||||
xcb_button_index_t xcb_button;
|
||||
} button_map_t;
|
||||
|
||||
static constexpr button_map_t button_map[] = {
|
||||
{ZDWM_BUTTON_LEFT, XCB_BUTTON_INDEX_1},
|
||||
{ZDWM_BUTTON_RIGHT, XCB_BUTTON_INDEX_3},
|
||||
{ZDWM_BUTTON_MIDDLE, XCB_BUTTON_INDEX_2},
|
||||
/* TODO: 后续可以考虑加上鼠标滚轮事件 */
|
||||
};
|
||||
|
||||
button_t button_xcb_to_zdwm(xcb_button_t button) {
|
||||
for (size_t i = 0; i < countof(button_map); ++i) {
|
||||
auto item = &button_map[i];
|
||||
if (button == item->xcb_button) return item->zdwm_button;
|
||||
}
|
||||
|
||||
return ZDWM_BUTTON_NONE;
|
||||
}
|
||||
|
||||
xcb_button_t button_zdwm_to_xcb(button_t button) {
|
||||
for (size_t i = 0; i < countof(button_map); ++i) {
|
||||
auto item = &button_map[i];
|
||||
if (button == item->zdwm_button) return item->xcb_button;
|
||||
}
|
||||
|
||||
return XCB_BUTTON_INDEX_ANY;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "interface/types.h"
|
||||
|
||||
modifier_mask_t modifiers_xcb_to_zdwm(uint16_t mask);
|
||||
uint16_t modifiers_zdwm_to_xcb(modifier_mask_t mask);
|
||||
|
||||
button_t button_xcb_to_zdwm(xcb_button_t button);
|
||||
xcb_button_t button_zdwm_to_xcb(button_t button);
|
||||
@@ -1,51 +0,0 @@
|
||||
#include <stddef.h>
|
||||
#include <xcb/xcb_cursor.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/memory.h"
|
||||
#include "internal.h"
|
||||
|
||||
static const char *const cursor_name_map[] = {
|
||||
[ZDWM_CURSOR_NORMAL] = "left_ptr",
|
||||
[ZDWM_CURSOR_MOVE] = "fleur",
|
||||
[ZDWM_CURSOR_RESIZE] = "bottom_right_corner",
|
||||
};
|
||||
|
||||
static xcb_cursor_context_t *get_xcb_cursor_context(backend_t *backend) {
|
||||
if (backend->cursor_context) return backend->cursor_context;
|
||||
|
||||
auto conn = backend->conn;
|
||||
auto screen = backend->screen;
|
||||
if (xcb_cursor_context_new(conn, screen, &backend->cursor_context) == 0) {
|
||||
return backend->cursor_context;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void cursor_init(backend_t *backend) {
|
||||
auto ctx = get_xcb_cursor_context(backend);
|
||||
if (!ctx) return;
|
||||
|
||||
for (size_t i = 0; i < countof(backend->cursors); ++i) {
|
||||
auto cursor_name = cursor_name_map[i];
|
||||
backend->cursors[i] = xcb_cursor_load_cursor(ctx, cursor_name);
|
||||
}
|
||||
}
|
||||
|
||||
void cursor_cleanup(backend_t *backend) {
|
||||
constexpr size_t count = countof(backend->cursors);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xcb_free_cursor(backend->conn, backend->cursors[i]);
|
||||
}
|
||||
p_clear(backend->cursors, count);
|
||||
xcb_cursor_context_free(backend->cursor_context);
|
||||
backend->cursor_context = nullptr;
|
||||
}
|
||||
|
||||
xcb_cursor_t cursor_get_xcb_cursor(backend_t *backend, cursor_t cursor) {
|
||||
if (backend->cursor_context == nullptr) return XCB_CURSOR_NONE;
|
||||
|
||||
return backend->cursors[cursor];
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
void cursor_init(backend_t *backend);
|
||||
void cursor_cleanup(backend_t *backend);
|
||||
xcb_cursor_t cursor_get_xcb_cursor(backend_t *backend, cursor_t cursor);
|
||||
@@ -1,566 +0,0 @@
|
||||
#include "common/event.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_event.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xcb_keysyms.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "backend/x11/common.h"
|
||||
#include "backend/x11/window.h"
|
||||
#include "base/memory.h"
|
||||
#include "interface/backend.h"
|
||||
#include "interface/event.h"
|
||||
#include "internal.h"
|
||||
|
||||
static window_state_t *derive_window_states(
|
||||
const atoms_t *atoms,
|
||||
const xcb_atom_t *raw,
|
||||
uint32_t count,
|
||||
size_t *out_count
|
||||
) {
|
||||
if (!raw || count == 0) {
|
||||
*out_count = 0;
|
||||
return nullptr;
|
||||
}
|
||||
window_state_t *buf = p_new(window_state_t, count);
|
||||
size_t valid = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
window_state_t s = atom_to_window_state(atoms, raw[i]);
|
||||
if (s != (window_state_t)-1) buf[valid++] = s;
|
||||
}
|
||||
if (valid == 0) {
|
||||
p_delete(&buf);
|
||||
*out_count = 0;
|
||||
return nullptr;
|
||||
}
|
||||
*out_count = valid;
|
||||
return buf;
|
||||
}
|
||||
|
||||
static bool derive_skip_taskbar(
|
||||
const atoms_t *atoms,
|
||||
const xcb_atom_t *raw,
|
||||
uint32_t count
|
||||
) {
|
||||
for (uint32_t i = 0; i < count; i++)
|
||||
if (raw[i] == atoms->_NET_WM_STATE_SKIP_TASKBAR) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool maximized_from_states(
|
||||
const atoms_t *atoms,
|
||||
const xcb_atom_t *state_atoms,
|
||||
uint32_t state_count
|
||||
) {
|
||||
for (uint32_t i = 0; i < state_count; i++) {
|
||||
if (
|
||||
state_atoms[i] == atoms->_NET_WM_STATE_MAXIMIZED_VERT ||
|
||||
state_atoms[i] == atoms->_NET_WM_STATE_MAXIMIZED_HORZ
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool fullscreen_from_states(
|
||||
const atoms_t *atoms,
|
||||
const xcb_atom_t *state_atoms,
|
||||
uint32_t state_count
|
||||
) {
|
||||
for (uint32_t i = 0; i < state_count; i++) {
|
||||
if (state_atoms[i] == atoms->_NET_WM_STATE_FULLSCREEN) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool urgent_from_states(
|
||||
const atoms_t *atoms,
|
||||
const xcb_atom_t *state_atoms,
|
||||
uint32_t state_count
|
||||
) {
|
||||
for (uint32_t i = 0; i < state_count; ++i) {
|
||||
if (state_atoms[i] == atoms->_NET_WM_STATE_DEMANDS_ATTENTION) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void parse_size_hints(
|
||||
const xcb_size_hints_t *hints,
|
||||
zdwm_size_t *min,
|
||||
zdwm_size_t *max
|
||||
) {
|
||||
assert(hints != nullptr);
|
||||
assert(min != nullptr);
|
||||
assert(max != nullptr);
|
||||
|
||||
*min = (zdwm_size_t){.width = 0, .height = 0};
|
||||
*max = (zdwm_size_t){.width = INT32_MAX, .height = INT32_MAX};
|
||||
|
||||
if (hints->flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) {
|
||||
min->width = hints->min_width;
|
||||
min->height = hints->min_height;
|
||||
}
|
||||
if (hints->flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE) {
|
||||
max->width = hints->max_width;
|
||||
max->height = hints->max_height;
|
||||
}
|
||||
}
|
||||
|
||||
bool populate_window_event(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
window_map_request_event_t *ev
|
||||
) {
|
||||
ev->window = (window_id_t)window;
|
||||
ev->transient_for = window_get_transient_for(backend, window);
|
||||
|
||||
auto wa = window_get_attributes(backend, window);
|
||||
if (!wa) return false;
|
||||
ev->override_redirect = (bool)wa->override_redirect;
|
||||
p_delete(&wa);
|
||||
|
||||
xcb_icccm_wm_hints_t wm_hints = {0};
|
||||
if (window_get_wm_hints(backend, window, &wm_hints)) {
|
||||
ev->urgent = (bool)xcb_icccm_wm_hints_get_urgency(&wm_hints);
|
||||
ev->minimized = wm_hints.initial_state == XCB_ICCCM_WM_STATE_ICONIC;
|
||||
}
|
||||
|
||||
xcb_size_hints_t size_hints = {0};
|
||||
if (!window_get_size_hints(backend, window, &size_hints)) return false;
|
||||
parse_size_hints(&size_hints, &ev->min_size, &ev->max_size);
|
||||
|
||||
if (!window_get_geometry(backend, window, &ev->rect)) return false;
|
||||
|
||||
const atoms_t *atoms = &backend->atoms;
|
||||
xcb_atom_t *state_atoms = nullptr;
|
||||
uint32_t state_count = 0;
|
||||
if (!window_get_atom_array(
|
||||
backend,
|
||||
window,
|
||||
atoms->_NET_WM_STATE,
|
||||
&state_atoms,
|
||||
&state_count
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state_atoms) {
|
||||
ev->maximized = maximized_from_states(atoms, state_atoms, state_count);
|
||||
ev->fullscreen = fullscreen_from_states(atoms, state_atoms, state_count);
|
||||
ev->skip_taskbar = derive_skip_taskbar(atoms, state_atoms, state_count);
|
||||
ev->props.states = derive_window_states(
|
||||
atoms,
|
||||
state_atoms,
|
||||
state_count,
|
||||
&ev->props.state_count
|
||||
);
|
||||
if (!ev->urgent) {
|
||||
ev->urgent = urgent_from_states(atoms, state_atoms, state_count);
|
||||
}
|
||||
}
|
||||
|
||||
p_delete(&state_atoms);
|
||||
|
||||
window_layer_props_t *props = &ev->props;
|
||||
if (!window_get_types(backend, window, &props->types, &props->type_count)) {
|
||||
p_delete(&props->states);
|
||||
return false;
|
||||
}
|
||||
|
||||
ev->metadata.role = window_get_role(backend, window);
|
||||
ev->metadata.title = window_get_title(backend, window);
|
||||
window_get_class(
|
||||
backend,
|
||||
window,
|
||||
&ev->metadata.class_name,
|
||||
&ev->metadata.instance_name
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_map_request(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_map_request_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_WINDOW_MAP_REQUEST;
|
||||
return populate_window_event(
|
||||
backend,
|
||||
xcb_event->window,
|
||||
&event->as.window_map_request
|
||||
);
|
||||
}
|
||||
|
||||
static bool handle_unmap_notify(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_unmap_notify_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_WINDOW_REMOVE;
|
||||
event->as.window_remove.window = xcb_event->window;
|
||||
if (XCB_EVENT_SENT(xcb_event)) {
|
||||
event->as.window_remove.reason = ZDWM_WINDOW_REMOVE_WITHDRAWN;
|
||||
} else {
|
||||
event->as.window_remove.reason = ZDWM_WINDOW_REMOVE_DESTROY;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_destroy_notify(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_destroy_notify_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_WINDOW_REMOVE;
|
||||
event->as.window_remove.window = xcb_event->window;
|
||||
event->as.window_remove.reason = ZDWM_WINDOW_REMOVE_DESTROY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_configure_request(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_configure_request_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_CONFIGURE_REQUEST;
|
||||
auto data = &event->as.configure_request;
|
||||
data->window = xcb_event->window;
|
||||
data->changed_fields = 0u;
|
||||
|
||||
#define EXTRACT_FIELD(XCB_MASK, FIELD_MASK, DATA_FIELD, EVENT_FIELD) \
|
||||
if (xcb_event->value_mask & XCB_CONFIG_WINDOW_##XCB_MASK) { \
|
||||
data->DATA_FIELD = xcb_event->EVENT_FIELD; \
|
||||
data->changed_fields |= ZDWM_CONFIGURE_FIELD_##FIELD_MASK; \
|
||||
}
|
||||
|
||||
EXTRACT_FIELD(X, X, x, x);
|
||||
EXTRACT_FIELD(Y, Y, y, y);
|
||||
EXTRACT_FIELD(WIDTH, WIDTH, width, width);
|
||||
EXTRACT_FIELD(HEIGHT, HEIGHT, height, height);
|
||||
EXTRACT_FIELD(BORDER_WIDTH, BORDER_WIDTH, border_width, border_width);
|
||||
EXTRACT_FIELD(SIBLING, SIBLING, sibling, sibling);
|
||||
EXTRACT_FIELD(STACK_MODE, STACK_MODE, stack_mode, stack_mode);
|
||||
|
||||
#undef EXTRACT_FIELD
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_key_press(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_key_press_event_t *xcb_event
|
||||
) {
|
||||
auto keycode = xcb_event->detail;
|
||||
|
||||
/* keysym without any modifiers */
|
||||
auto keysym = xcb_key_symbols_get_keysym(backend->key_symbols, keycode, 0);
|
||||
|
||||
event->type = ZDWM_EVENT_KEY_PRESS;
|
||||
event->as.key_press.keycode = keycode;
|
||||
event->as.key_press.keysym = keysym;
|
||||
event->as.key_press.modifiers = modifiers_xcb_to_zdwm(xcb_event->state);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool fill_button_event(
|
||||
const xcb_button_press_event_t *xcb_event,
|
||||
pointer_button_event_t *data
|
||||
) {
|
||||
auto button = button_xcb_to_zdwm(xcb_event->detail);
|
||||
|
||||
data->modifiers = modifiers_xcb_to_zdwm(xcb_event->state);
|
||||
data->button = button;
|
||||
data->window = xcb_event->event;
|
||||
data->root = (point_t){.x = xcb_event->root_x, .y = xcb_event->root_y};
|
||||
data->local = (point_t){.x = xcb_event->event_x, .y = xcb_event->event_y};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_button_press(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_button_press_event_t *xcb_event
|
||||
) {
|
||||
auto press = &event->as.pointer_button_press;
|
||||
auto handled = fill_button_event(xcb_event, press);
|
||||
if (handled) event->type = ZDWM_EVENT_POINTER_BUTTON_PRESS;
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
static bool handle_button_release(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_button_release_event_t *xcb_event
|
||||
) {
|
||||
auto release = &event->as.pointer_button_release;
|
||||
auto handled = fill_button_event(xcb_event, release);
|
||||
if (handled) event->type = ZDWM_EVENT_POINTER_BUTTON_RELEASE;
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
static bool handle_motion_notify(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_motion_notify_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_POINTER_MOTION;
|
||||
|
||||
auto data = &event->as.pointer_motion;
|
||||
data->window = xcb_event->event;
|
||||
data->root = (point_t){.x = xcb_event->root_x, .y = xcb_event->root_y};
|
||||
data->local = (point_t){.x = xcb_event->event_x, .y = xcb_event->event_y};
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_enter_notify(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_enter_notify_event_t *xcb_event
|
||||
) {
|
||||
event->type = ZDWM_EVENT_POINTER_ENTER;
|
||||
event->as.pointer_enter.window = xcb_event->event;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_property_notify(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_property_notify_event_t *xcb_event
|
||||
) {
|
||||
auto window_id = xcb_event->window;
|
||||
if (
|
||||
xcb_event->atom == backend->atoms.WM_NAME ||
|
||||
xcb_event->atom == backend->atoms._NET_WM_NAME
|
||||
) {
|
||||
event->type = ZDWM_EVENT_WINDOW_METADATA_CHANGED;
|
||||
|
||||
auto data = &event->as.window_metadata_change;
|
||||
data->window = window_id;
|
||||
data->metadata.title = window_get_title(backend, window_id);
|
||||
data->changed_fields = ZDWM_WINDOW_METADATA_CHANGE_TITLE;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xcb_event->atom == backend->atoms.WM_WINDOW_ROLE) {
|
||||
event->type = ZDWM_EVENT_WINDOW_METADATA_CHANGED;
|
||||
|
||||
auto data = &event->as.window_metadata_change;
|
||||
data->window = window_id;
|
||||
data->metadata.role = window_get_role(backend, window_id);
|
||||
data->changed_fields = ZDWM_WINDOW_METADATA_CHANGE_ROLE;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xcb_event->atom == XCB_ATOM_WM_CLASS) {
|
||||
event->type = ZDWM_EVENT_WINDOW_METADATA_CHANGED;
|
||||
|
||||
auto data = &event->as.window_metadata_change;
|
||||
data->window = window_id;
|
||||
window_get_class(
|
||||
backend,
|
||||
window_id,
|
||||
&data->metadata.class_name,
|
||||
&data->metadata.instance_name
|
||||
);
|
||||
data->changed_fields =
|
||||
ZDWM_WINDOW_METADATA_CHANGE_CLASS | ZDWM_WINDOW_METADATA_CHANGE_INSTANCE;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xcb_event->atom == XCB_ATOM_WM_HINTS) {
|
||||
xcb_icccm_wm_hints_t hints = {0};
|
||||
if (!window_get_wm_hints(backend, window_id, &hints)) return false;
|
||||
|
||||
event->type = ZDWM_EVENT_WINDOW_HINTS_CHANGED;
|
||||
|
||||
auto data = &event->as.hints;
|
||||
data->window = window_id;
|
||||
if (hints.flags & XCB_ICCCM_WM_HINT_X_URGENCY) {
|
||||
data->changed_fields |= ZDWM_HINT_FIELD_URGENT;
|
||||
data->urgent = xcb_icccm_wm_hints_get_urgency(&hints);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xcb_event->atom == XCB_ATOM_WM_NORMAL_HINTS) {
|
||||
xcb_size_hints_t hints = {0};
|
||||
if (!window_get_size_hints(backend, window_id, &hints)) return false;
|
||||
|
||||
event->type = ZDWM_EVENT_WINDOW_HINTS_CHANGED;
|
||||
|
||||
auto data = &event->as.hints;
|
||||
data->window = window_id;
|
||||
|
||||
data->changed_fields |= ZDWM_HINT_FIELD_SIZE;
|
||||
parse_size_hints(&hints, &data->min_size, &data->max_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool handle_client_message(
|
||||
backend_t *backend,
|
||||
event_t *event,
|
||||
const xcb_client_message_event_t *xcb_event
|
||||
) {
|
||||
auto atoms = &backend->atoms;
|
||||
|
||||
if (xcb_event->type == atoms->_NET_ACTIVE_WINDOW) {
|
||||
event->type = ZDWM_EVENT_WINDOW_ACTIVATE_REQUEST;
|
||||
event->as.window_activate_request.window = xcb_event->window;
|
||||
event->as.window_activate_request.source = xcb_event->data.data32[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xcb_event->type == atoms->WM_CHANGE_STATE) {
|
||||
if (xcb_event->data.data32[0] == XCB_ICCCM_WM_STATE_ICONIC) {
|
||||
event->type = ZDWM_EVENT_WINDOW_STATE_REQUEST;
|
||||
event->as.window_state_request.window = xcb_event->window;
|
||||
event->as.window_state_request.type = ZDWM_WINDOW_STATE_REQUEST_MINIMIZED;
|
||||
event->as.window_state_request.action = ZDWM_WINDOW_STATE_ACTION_ADD;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xcb_event->type == atoms->_NET_WM_STATE) {
|
||||
event->type = ZDWM_EVENT_WINDOW_STATE_REQUEST;
|
||||
auto data = &event->as.window_state_request;
|
||||
data->window = xcb_event->window;
|
||||
/**
|
||||
* _NET_WM_STATE 协议数据格式为
|
||||
* data.data32 = {
|
||||
* action, // 操作类型(添加/移除/切换)
|
||||
* property1, // 第一个状态原子(如_NET_WM_STATE_FULLSCREEN)
|
||||
* property2, // 第二个状态原子(可选,通常为0)
|
||||
* 0, // 预留字段(固定为0)
|
||||
* 0 // 预留字段(固定为0)
|
||||
* };
|
||||
* action 值可能为
|
||||
* _NET_WM_STATE_ADD(1):添加全屏状态,窗口进入全屏模式
|
||||
* _NET_WM_STATE_REMOVE(0):移除全屏状态,窗口退出全屏模式
|
||||
* _NET_WM_STATE_TOGGLE(2):切换全屏状态(若当前全屏则退出,反之进入)
|
||||
*/
|
||||
auto action = xcb_event->data.data32[0];
|
||||
switch (action) {
|
||||
case 0:
|
||||
data->action = ZDWM_WINDOW_STATE_ACTION_REMOVE;
|
||||
break;
|
||||
case 1:
|
||||
data->action = ZDWM_WINDOW_STATE_ACTION_ADD;
|
||||
break;
|
||||
case 2:
|
||||
data->action = ZDWM_WINDOW_STATE_ACTION_TOGGLE;
|
||||
break;
|
||||
}
|
||||
auto properties = &xcb_event->data.data32[1];
|
||||
uint32_t count = 2;
|
||||
|
||||
if (maximized_from_states(atoms, properties, count)) {
|
||||
data->type = ZDWM_WINDOW_STATE_REQUEST_MAXIMIZED;
|
||||
return true;
|
||||
}
|
||||
if (fullscreen_from_states(atoms, properties, count)) {
|
||||
data->type = ZDWM_WINDOW_STATE_REQUEST_FULLSCREEN;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (derive_skip_taskbar(atoms, properties, count)) {
|
||||
data->type = ZDWM_WINDOW_STATE_REQUEST_SKIP_TASKBAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (urgent_from_states(atoms, properties, count)) {
|
||||
event->type = ZDWM_EVENT_WINDOW_HINTS_CHANGED;
|
||||
event->as.hints = (typeof(event->as.hints)){
|
||||
.urgent = true,
|
||||
.changed_fields = ZDWM_HINT_FIELD_URGENT,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool handle_event(
|
||||
backend_t *backend,
|
||||
xcb_generic_event_t *raw_event,
|
||||
event_t *event
|
||||
) {
|
||||
uint8_t response_type = XCB_EVENT_RESPONSE_TYPE(raw_event);
|
||||
bool handled = false;
|
||||
|
||||
auto label = xcb_event_get_label(response_type);
|
||||
printf("xcb event type: %s[%u]\n", label, response_type);
|
||||
|
||||
switch (response_type) {
|
||||
#define EVENT(type, handler) \
|
||||
case type: \
|
||||
event_reset(event); \
|
||||
handled = handler(backend, event, (void *)raw_event); \
|
||||
break
|
||||
|
||||
EVENT(XCB_MAP_REQUEST, handle_map_request);
|
||||
EVENT(XCB_UNMAP_NOTIFY, handle_unmap_notify);
|
||||
EVENT(XCB_DESTROY_NOTIFY, handle_destroy_notify);
|
||||
EVENT(XCB_CONFIGURE_REQUEST, handle_configure_request);
|
||||
EVENT(XCB_KEY_PRESS, handle_key_press);
|
||||
EVENT(XCB_BUTTON_PRESS, handle_button_press);
|
||||
EVENT(XCB_BUTTON_RELEASE, handle_button_release);
|
||||
EVENT(XCB_MOTION_NOTIFY, handle_motion_notify);
|
||||
EVENT(XCB_ENTER_NOTIFY, handle_enter_notify);
|
||||
EVENT(XCB_PROPERTY_NOTIFY, handle_property_notify);
|
||||
EVENT(XCB_CLIENT_MESSAGE, handle_client_message);
|
||||
|
||||
#undef EVENT
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
p_delete(&raw_event);
|
||||
if (handled) return true;
|
||||
event_reset(event);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool backend_poll_event(backend_t *backend, event_t *event) {
|
||||
if (!backend || !backend->conn || !event) return false;
|
||||
|
||||
auto raw_event = xcb_poll_for_event(backend->conn);
|
||||
if (!raw_event) return false;
|
||||
|
||||
handle_event(backend, raw_event, event);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool backend_next_event(backend_t *backend, event_t *event) {
|
||||
if (!backend || !backend->conn || !event) return false;
|
||||
|
||||
for (;;) {
|
||||
xcb_generic_event_t *raw_event = xcb_wait_for_event(backend->conn);
|
||||
if (!raw_event) return false;
|
||||
|
||||
if (handle_event(backend, raw_event, event)) return true;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_cursor.h>
|
||||
#include <xcb/xcb_keysyms.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "common/window.h"
|
||||
#include "interface/event.h"
|
||||
#include "interface/tray.h"
|
||||
|
||||
#define EWMH_ATOMS(X) \
|
||||
X(_NET_WM_NAME) \
|
||||
X(_NET_ACTIVE_WINDOW) \
|
||||
X(_NET_WM_PID) \
|
||||
\
|
||||
X(_NET_CLIENT_LIST) \
|
||||
X(_NET_CLIENT_LIST_STACKING) \
|
||||
\
|
||||
X(_NET_WM_STATE) \
|
||||
X(_NET_WM_STATE_FULLSCREEN) \
|
||||
X(_NET_WM_STATE_ABOVE) \
|
||||
X(_NET_WM_STATE_STICKY) \
|
||||
X(_NET_WM_STATE_MODAL) \
|
||||
X(_NET_WM_STATE_SKIP_TASKBAR) \
|
||||
X(_NET_WM_STATE_MAXIMIZED_VERT) \
|
||||
X(_NET_WM_STATE_MAXIMIZED_HORZ) \
|
||||
\
|
||||
X(_NET_WM_STATE_DEMANDS_ATTENTION) \
|
||||
\
|
||||
X(_NET_WM_WINDOW_TYPE) \
|
||||
X(_NET_WM_WINDOW_TYPE_NORMAL) \
|
||||
X(_NET_WM_WINDOW_TYPE_DESKTOP) \
|
||||
X(_NET_WM_WINDOW_TYPE_DOCK) \
|
||||
X(_NET_WM_WINDOW_TYPE_TOOLBAR) \
|
||||
X(_NET_WM_WINDOW_TYPE_DIALOG) \
|
||||
X(_NET_WM_WINDOW_TYPE_UTILITY) \
|
||||
X(_NET_WM_WINDOW_TYPE_SPLASH) \
|
||||
X(_NET_WM_WINDOW_TYPE_MENU) \
|
||||
X(_NET_WM_WINDOW_TYPE_DROPDOWN_MENU) \
|
||||
X(_NET_WM_WINDOW_TYPE_POPUP_MENU) \
|
||||
X(_NET_WM_WINDOW_TYPE_TOOLTIP) \
|
||||
X(_NET_WM_WINDOW_TYPE_COMBO) \
|
||||
X(_NET_WM_WINDOW_TYPE_DND) \
|
||||
X(_NET_WM_WINDOW_TYPE_NOTIFICATION)
|
||||
|
||||
#define ATOM_LIST(X) \
|
||||
X(COMPOUND_TEXT) \
|
||||
X(UTF8_STRING) \
|
||||
\
|
||||
X(WM_WINDOW_ROLE) \
|
||||
X(WM_NAME) \
|
||||
X(WM_CHANGE_STATE) \
|
||||
\
|
||||
X(WM_PROTOCOLS) \
|
||||
X(WM_TAKE_FOCUS) \
|
||||
X(WM_DELETE_WINDOW) \
|
||||
\
|
||||
X(_NET_SUPPORTING_WM_CHECK) \
|
||||
X(_NET_SUPPORTED) \
|
||||
EWMH_ATOMS(X)
|
||||
|
||||
typedef struct atoms_t {
|
||||
#define DECLARATION_ATOM(name) xcb_atom_t name;
|
||||
ATOM_LIST(DECLARATION_ATOM);
|
||||
#undef DECLARATION_ATOM
|
||||
} atoms_t;
|
||||
|
||||
typedef struct window_configure_t {
|
||||
xcb_window_t window;
|
||||
uint32_t mask;
|
||||
xcb_configure_window_value_list_t value;
|
||||
} window_configure_t;
|
||||
|
||||
typedef struct window_configure_list_t {
|
||||
window_configure_t *cfgs;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
} window_configure_list_t;
|
||||
|
||||
typedef enum cursor_t {
|
||||
ZDWM_CURSOR_NORMAL,
|
||||
ZDWM_CURSOR_MOVE,
|
||||
ZDWM_CURSOR_RESIZE,
|
||||
ZDWM_CURSOR_COUNT,
|
||||
} cursor_t;
|
||||
|
||||
typedef struct tray_host_t {
|
||||
bool enabled;
|
||||
xcb_window_t host_window;
|
||||
xcb_window_t container;
|
||||
int32_t icon_size;
|
||||
size_t icon_count;
|
||||
tray_icons_change_cb_t icon_change_cb;
|
||||
void *cb_user_data;
|
||||
} tray_host_t;
|
||||
|
||||
extern const tray_api_t tray_api;
|
||||
|
||||
typedef struct backend_t {
|
||||
xcb_key_symbols_t *key_symbols;
|
||||
xcb_connection_t *conn;
|
||||
xcb_screen_t *screen;
|
||||
int screenp;
|
||||
bool have_xfixes;
|
||||
|
||||
atoms_t atoms;
|
||||
|
||||
/* When no window should take focus, then focus this window */
|
||||
xcb_window_t window_no_focus;
|
||||
xcb_window_t wm_check_window;
|
||||
|
||||
xcb_window_t focus_window;
|
||||
bool update_focus;
|
||||
|
||||
xcb_cursor_t cursors[ZDWM_CURSOR_COUNT];
|
||||
xcb_cursor_context_t *cursor_context;
|
||||
|
||||
window_configure_list_t config_list;
|
||||
window_list_t unmap;
|
||||
window_list_t map;
|
||||
window_list_t kill;
|
||||
|
||||
tray_host_t tray;
|
||||
} backend_t;
|
||||
|
||||
bool populate_window_event(
|
||||
struct backend_t *backend,
|
||||
xcb_window_t window,
|
||||
window_map_request_event_t *ev
|
||||
);
|
||||
@@ -1,59 +0,0 @@
|
||||
#include "interface/tray.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "interface/types.h"
|
||||
#include "internal.h"
|
||||
|
||||
static window_id_t tray_host_window(void *handle) {
|
||||
backend_t *backend = handle;
|
||||
if (backend == nullptr || !backend->tray.enabled) {
|
||||
return ZDWM_WINDOW_ID_INVALID;
|
||||
}
|
||||
|
||||
return backend->tray.host_window;
|
||||
}
|
||||
|
||||
static size_t tray_icon_count(void *handle) {
|
||||
backend_t *backend = handle;
|
||||
if (backend == nullptr || !backend->tray.enabled) return 0;
|
||||
|
||||
return backend->tray.icon_count;
|
||||
}
|
||||
|
||||
static int32_t tray_icon_size(void *handle) {
|
||||
backend_t *backend = handle;
|
||||
if (backend == nullptr || !backend->tray.enabled) return 0;
|
||||
|
||||
return backend->tray.icon_size;
|
||||
}
|
||||
|
||||
static void tray_place(void *handle, int32_t x) {
|
||||
backend_t *backend = handle;
|
||||
if (backend == nullptr || !backend->tray.enabled) return;
|
||||
|
||||
auto conn = backend->conn;
|
||||
auto tray = &backend->tray;
|
||||
uint16_t value_mask = XCB_CONFIG_WINDOW_X;
|
||||
xcb_configure_window_value_list_t value_list = {.x = x};
|
||||
xcb_configure_window_aux(conn, tray->container, value_mask, &value_list);
|
||||
}
|
||||
|
||||
static void
|
||||
tray_set_listeners(void *handle, tray_icons_change_cb_t cb, void *user_data) {
|
||||
backend_t *backend = handle;
|
||||
if (backend == nullptr) return;
|
||||
|
||||
backend->tray.icon_change_cb = cb;
|
||||
backend->tray.cb_user_data = user_data;
|
||||
}
|
||||
|
||||
const tray_api_t tray_api = {
|
||||
.host_window = tray_host_window,
|
||||
.icon_count = tray_icon_count,
|
||||
.icon_size = tray_icon_size,
|
||||
.place = tray_place,
|
||||
.set_listener = tray_set_listeners,
|
||||
};
|
||||
@@ -1,485 +0,0 @@
|
||||
#include "window.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xcb_keysyms.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "backend/x11/common.h"
|
||||
#include "backend/x11/cursor.h"
|
||||
#include "base/macros.h"
|
||||
#include "base/memory.h"
|
||||
#include "internal.h"
|
||||
|
||||
static char *window_get_text_property(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_atom_t property
|
||||
) {
|
||||
xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(
|
||||
backend->conn,
|
||||
false,
|
||||
window,
|
||||
property,
|
||||
XCB_ATOM_ANY,
|
||||
0,
|
||||
UINT32_MAX
|
||||
);
|
||||
xcb_get_property_reply_t *reply =
|
||||
xcb_get_property_reply(backend->conn, cookie, nullptr);
|
||||
if (!reply) return nullptr;
|
||||
|
||||
int length = xcb_get_property_value_length(reply);
|
||||
char *value = (char *)xcb_get_property_value(reply);
|
||||
|
||||
char *text = nullptr;
|
||||
if (
|
||||
length && reply->format == 8 &&
|
||||
(reply->type == XCB_ATOM_STRING ||
|
||||
reply->type == backend->atoms.UTF8_STRING ||
|
||||
reply->type == backend->atoms.COMPOUND_TEXT)
|
||||
) {
|
||||
text = p_strndup(value, (size_t)length);
|
||||
}
|
||||
|
||||
p_delete(&reply);
|
||||
return text;
|
||||
}
|
||||
|
||||
char *window_get_role(backend_t *backend, xcb_window_t window) {
|
||||
xcb_atom_t property = backend->atoms.WM_WINDOW_ROLE;
|
||||
return window_get_text_property(backend, window, property);
|
||||
}
|
||||
|
||||
char *window_get_title(backend_t *backend, xcb_window_t window) {
|
||||
xcb_atom_t property = backend->atoms._NET_WM_NAME;
|
||||
char *name = window_get_text_property(backend, window, property);
|
||||
if (!name) {
|
||||
property = backend->atoms.WM_NAME;
|
||||
name = window_get_text_property(backend, window, property);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
void window_get_class(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
char **class_out,
|
||||
char **instance_out
|
||||
) {
|
||||
xcb_icccm_get_wm_class_reply_t prop;
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_class_unchecked(backend->conn, window);
|
||||
if (!xcb_icccm_get_wm_class_reply(backend->conn, cookie, &prop, nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (class_out) *class_out = p_strdup_nullable(prop.class_name);
|
||||
if (instance_out) *instance_out = p_strdup_nullable(prop.instance_name);
|
||||
|
||||
xcb_icccm_get_wm_class_reply_wipe(&prop);
|
||||
}
|
||||
|
||||
xcb_window_t window_get_transient_for(backend_t *backend, xcb_window_t window) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_transient_for_unchecked(conn, window);
|
||||
xcb_window_t trans_for = XCB_WINDOW_NONE;
|
||||
if (xcb_icccm_get_wm_transient_for_reply(conn, cookie, &trans_for, nullptr)) {
|
||||
return trans_for;
|
||||
}
|
||||
|
||||
return XCB_WINDOW_NONE;
|
||||
}
|
||||
|
||||
xcb_get_window_attributes_reply_t *
|
||||
window_get_attributes(backend_t *backend, xcb_window_t window) {
|
||||
xcb_get_window_attributes_cookie_t cookie =
|
||||
xcb_get_window_attributes(backend->conn, window);
|
||||
return xcb_get_window_attributes_reply(backend->conn, cookie, nullptr);
|
||||
}
|
||||
|
||||
static window_type_t
|
||||
atom_to_window_type(const atoms_t *atoms, xcb_atom_t atom) {
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_DESKTOP)
|
||||
return ZDWM_WINDOW_TYPE_DESKTOP;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_DOCK) return ZDWM_WINDOW_TYPE_DOCK;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_TOOLBAR)
|
||||
return ZDWM_WINDOW_TYPE_TOOLBAR;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_DIALOG) return ZDWM_WINDOW_TYPE_DIALOG;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_UTILITY)
|
||||
return ZDWM_WINDOW_TYPE_UTILITY;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_SPLASH) return ZDWM_WINDOW_TYPE_SPLASH;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_MENU) return ZDWM_WINDOW_TYPE_MENU;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU)
|
||||
return ZDWM_WINDOW_TYPE_DROPDOWN_MENU;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_POPUP_MENU)
|
||||
return ZDWM_WINDOW_TYPE_POPUP_MENU;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_TOOLTIP)
|
||||
return ZDWM_WINDOW_TYPE_TOOLTIP;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_COMBO) return ZDWM_WINDOW_TYPE_COMBO;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_DND) return ZDWM_WINDOW_TYPE_DND;
|
||||
if (atom == atoms->_NET_WM_WINDOW_TYPE_NOTIFICATION)
|
||||
return ZDWM_WINDOW_TYPE_NOTIFICATION;
|
||||
return ZDWM_WINDOW_TYPE_NORMAL;
|
||||
}
|
||||
|
||||
bool window_get_types(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
window_type_t **types,
|
||||
size_t *count
|
||||
) {
|
||||
uint32_t atom_count = 0;
|
||||
xcb_atom_t *atoms = nullptr;
|
||||
if (!window_get_atom_array(
|
||||
backend,
|
||||
window,
|
||||
backend->atoms._NET_WM_WINDOW_TYPE,
|
||||
&atoms,
|
||||
&atom_count
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
if (!atoms) {
|
||||
*types = nullptr;
|
||||
*count = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
*count = atom_count;
|
||||
*types = p_new(window_type_t, atom_count);
|
||||
for (uint32_t i = 0; i < atom_count; i++) {
|
||||
(*types)[i] = atom_to_window_type(&backend->atoms, atoms[i]);
|
||||
}
|
||||
|
||||
p_delete(&atoms);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool window_get_size_hints(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_size_hints_t *out
|
||||
) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_normal_hints_unchecked(conn, window);
|
||||
if (!xcb_icccm_get_wm_normal_hints_reply(conn, cookie, out, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool window_get_geometry(backend_t *backend, xcb_window_t window, rect_t *out) {
|
||||
xcb_get_geometry_cookie_t cookie = xcb_get_geometry(backend->conn, window);
|
||||
xcb_get_geometry_reply_t *reply =
|
||||
xcb_get_geometry_reply(backend->conn, cookie, nullptr);
|
||||
if (!reply) return false;
|
||||
|
||||
out->x = reply->x;
|
||||
out->y = reply->y;
|
||||
out->width = reply->width;
|
||||
out->height = reply->height;
|
||||
|
||||
p_delete(&reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool window_get_atom_array(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_atom_t property,
|
||||
xcb_atom_t **out_atoms,
|
||||
uint32_t *out_len
|
||||
) {
|
||||
xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(
|
||||
backend->conn,
|
||||
false,
|
||||
window,
|
||||
property,
|
||||
XCB_ATOM_ATOM,
|
||||
0,
|
||||
UINT32_MAX
|
||||
);
|
||||
xcb_get_property_reply_t *reply =
|
||||
xcb_get_property_reply(backend->conn, cookie, nullptr);
|
||||
if (!reply) return false;
|
||||
|
||||
if (reply->type == XCB_ATOM_ATOM && reply->format == 32 && reply->value_len) {
|
||||
*out_len = reply->value_len;
|
||||
*out_atoms =
|
||||
p_copy((xcb_atom_t *)xcb_get_property_value(reply), reply->value_len);
|
||||
} else {
|
||||
*out_len = 0;
|
||||
*out_atoms = nullptr;
|
||||
}
|
||||
|
||||
p_delete(&reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool window_get_wm_hints(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_icccm_wm_hints_t *out
|
||||
) {
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_hints_unchecked(backend->conn, window);
|
||||
if (!xcb_icccm_get_wm_hints_reply(backend->conn, cookie, out, nullptr)) {
|
||||
p_clear(out, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
window_state_t atom_to_window_state(const atoms_t *atoms, xcb_atom_t atom) {
|
||||
if (atom == atoms->_NET_WM_STATE_FULLSCREEN)
|
||||
return ZDWM_WINDOW_STATE_FULLSCREEN;
|
||||
if (atom == atoms->_NET_WM_STATE_ABOVE) return ZDWM_WINDOW_STATE_ABOVE;
|
||||
if (atom == atoms->_NET_WM_STATE_STICKY) return ZDWM_WINDOW_STATE_STICKY;
|
||||
if (atom == atoms->_NET_WM_STATE_MODAL) return ZDWM_WINDOW_STATE_MODAL;
|
||||
return (window_state_t)-1;
|
||||
}
|
||||
|
||||
static bool
|
||||
window_send_event(backend_t *backend, xcb_window_t window, xcb_atom_t atom) {
|
||||
bool exist = false;
|
||||
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
auto WM_PROTOCOLS = backend->atoms.WM_PROTOCOLS;
|
||||
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_protocols_unchecked(conn, window, WM_PROTOCOLS);
|
||||
xcb_icccm_get_wm_protocols_reply_t reply = {0};
|
||||
if (xcb_icccm_get_wm_protocols_reply(conn, cookie, &reply, nullptr)) {
|
||||
for (uint32_t i = 0; !exist && i < reply.atoms_len; ++i) {
|
||||
exist = reply.atoms[i] == atom;
|
||||
}
|
||||
xcb_icccm_get_wm_protocols_reply_wipe(&reply);
|
||||
}
|
||||
|
||||
if (exist) {
|
||||
xcb_client_message_event_t ev = {
|
||||
.response_type = XCB_CLIENT_MESSAGE,
|
||||
.format = 32,
|
||||
.window = window,
|
||||
.type = WM_PROTOCOLS,
|
||||
.data.data32 = {atom, XCB_CURRENT_TIME},
|
||||
};
|
||||
xcb_send_event(conn, false, window, XCB_EVENT_MASK_NO_EVENT, (char *)&ev);
|
||||
}
|
||||
|
||||
return exist;
|
||||
}
|
||||
|
||||
void window_takefocus(backend_t *backend, xcb_window_t window) {
|
||||
window_send_event(backend, window, backend->atoms.WM_TAKE_FOCUS);
|
||||
}
|
||||
|
||||
void window_kill(backend_t *backend, xcb_window_t window) {
|
||||
if (window_send_event(backend, window, backend->atoms.WM_DELETE_WINDOW)) {
|
||||
xcb_kill_client(backend->conn, window);
|
||||
}
|
||||
}
|
||||
|
||||
void root_set_event_mask(backend_t *backend) {
|
||||
xcb_connection_t *conn = backend->conn;
|
||||
xcb_window_t root = backend->screen->root;
|
||||
xcb_cw_t change_mask = XCB_CW_EVENT_MASK;
|
||||
xcb_change_window_attributes_value_list_t value_list = {
|
||||
.event_mask =
|
||||
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_KEY_PRESS,
|
||||
};
|
||||
xcb_change_window_attributes_aux(conn, root, change_mask, &value_list);
|
||||
}
|
||||
|
||||
void window_set_event_mask(xcb_connection_t *conn, xcb_window_t window) {
|
||||
xcb_cw_t change_mask = XCB_CW_EVENT_MASK;
|
||||
xcb_change_window_attributes_value_list_t value_list = {
|
||||
.event_mask = XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_FOCUS_CHANGE |
|
||||
XCB_EVENT_MASK_PROPERTY_CHANGE |
|
||||
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
|
||||
};
|
||||
xcb_change_window_attributes_aux(conn, window, change_mask, &value_list);
|
||||
}
|
||||
|
||||
void window_clean_event_mask(xcb_connection_t *conn, xcb_window_t window) {
|
||||
xcb_cw_t change_mask = XCB_CW_EVENT_MASK;
|
||||
xcb_change_window_attributes_value_list_t value_list = {
|
||||
.event_mask = XCB_EVENT_MASK_NO_EVENT,
|
||||
};
|
||||
xcb_change_window_attributes_aux(conn, window, change_mask, &value_list);
|
||||
}
|
||||
|
||||
void window_set_icccm_wm_state(
|
||||
xcb_connection_t *conn,
|
||||
xcb_window_t window,
|
||||
xcb_icccm_wm_state_t state
|
||||
) {
|
||||
xcb_icccm_wm_hints_t hints;
|
||||
xcb_icccm_wm_hints_set_none(&hints);
|
||||
switch (state) {
|
||||
case XCB_ICCCM_WM_STATE_WITHDRAWN:
|
||||
xcb_icccm_wm_hints_set_withdrawn(&hints);
|
||||
break;
|
||||
case XCB_ICCCM_WM_STATE_NORMAL:
|
||||
xcb_icccm_wm_hints_set_normal(&hints);
|
||||
break;
|
||||
case XCB_ICCCM_WM_STATE_ICONIC:
|
||||
xcb_icccm_wm_hints_set_iconic(&hints);
|
||||
break;
|
||||
}
|
||||
xcb_icccm_set_wm_hints(conn, window, &hints);
|
||||
}
|
||||
|
||||
void window_set_net_wm_state(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
size_t count,
|
||||
xcb_atom_t *atoms
|
||||
) {
|
||||
xcb_change_property(
|
||||
backend->conn,
|
||||
XCB_PROP_MODE_REPLACE,
|
||||
window,
|
||||
backend->atoms._NET_WM_STATE,
|
||||
XCB_ATOM_ATOM,
|
||||
32,
|
||||
count,
|
||||
atoms
|
||||
);
|
||||
}
|
||||
|
||||
static uint16_t get_xcb_modifier(modifier_mask_t modifiers) {
|
||||
typedef struct modifier_map_t {
|
||||
modifier_bit_t modifier;
|
||||
xcb_mod_mask_t mask;
|
||||
} modifier_map_t;
|
||||
static constexpr modifier_map_t modifier_map[] = {
|
||||
{ZDWM_MOD_SHIFT, XCB_MOD_MASK_SHIFT},
|
||||
{ZDWM_MOD_CONTROL, XCB_MOD_MASK_CONTROL},
|
||||
{ZDWM_MOD_1, XCB_MOD_MASK_1},
|
||||
{ZDWM_MOD_2, XCB_MOD_MASK_2},
|
||||
{ZDWM_MOD_3, XCB_MOD_MASK_3},
|
||||
{ZDWM_MOD_4, XCB_MOD_MASK_4},
|
||||
{ZDWM_MOD_5, XCB_MOD_MASK_5},
|
||||
};
|
||||
|
||||
uint16_t mods = ZDWM_MOD_NONE;
|
||||
for (size_t i = 0; i < countof(modifier_map); ++i) {
|
||||
auto item = &modifier_map[i];
|
||||
if (modifiers & item->modifier) mods |= item->mask;
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
void window_grab_keys(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
const key_bind_t *keys,
|
||||
size_t count
|
||||
) {
|
||||
auto conn = backend->conn;
|
||||
auto key_symbols = backend->key_symbols;
|
||||
|
||||
xcb_ungrab_key(conn, XCB_GRAB_ANY, window, XCB_MOD_MASK_ANY);
|
||||
|
||||
xcb_grab_mode_t mode = XCB_GRAB_MODE_ASYNC;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto key = &keys[i];
|
||||
auto keycodes = xcb_key_symbols_get_keycode(key_symbols, key->keysym);
|
||||
if (!keycodes) continue;
|
||||
|
||||
auto modifiers = get_xcb_modifier(key->modifiers);
|
||||
for (auto keycode = keycodes; *keycode != XCB_NO_SYMBOL; ++keycode) {
|
||||
xcb_grab_key(conn, true, window, modifiers, *keycode, mode, mode);
|
||||
}
|
||||
p_delete(&keycodes);
|
||||
}
|
||||
}
|
||||
|
||||
static visual_t *get_alpha_visual(backend_t *backend) {
|
||||
auto depth_iter = xcb_screen_allowed_depths_iterator(backend->screen);
|
||||
for (; depth_iter.rem; xcb_depth_next(&depth_iter)) {
|
||||
if (depth_iter.data->depth != 32) continue;
|
||||
|
||||
auto visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
|
||||
for (; visual_iter.rem; xcb_visualtype_next(&visual_iter)) {
|
||||
if (visual_iter.data->_class == XCB_VISUAL_CLASS_TRUE_COLOR) {
|
||||
auto visual = p_new(visual_t, 1);
|
||||
visual->visual = visual_iter.data;
|
||||
visual->depth = depth_iter.data->depth;
|
||||
return visual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static visual_t *get_root_visual(backend_t *backend) {
|
||||
auto depth_iter = xcb_screen_allowed_depths_iterator(backend->screen);
|
||||
auto visual_id = backend->screen->root_visual;
|
||||
for (; depth_iter.rem; xcb_depth_next(&depth_iter)) {
|
||||
auto visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
|
||||
for (; visual_iter.rem; xcb_visualtype_next(&visual_iter)) {
|
||||
if (visual_iter.data->visual_id == visual_id) {
|
||||
auto visual = p_new(visual_t, 1);
|
||||
visual->visual = visual_iter.data;
|
||||
visual->depth = depth_iter.data->depth;
|
||||
return visual;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
visual_t *window_get_visual(backend_t *backend, bool prefer_alpha) {
|
||||
if (!prefer_alpha) return get_root_visual(backend);
|
||||
|
||||
auto visual = get_alpha_visual(backend);
|
||||
if (!visual) visual = get_root_visual(backend);
|
||||
|
||||
return visual;
|
||||
}
|
||||
|
||||
void window_change_cursor(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
cursor_t cursor
|
||||
) {
|
||||
auto conn = backend->conn;
|
||||
xcb_params_cw_t params = {.cursor = cursor_get_xcb_cursor(backend, cursor)};
|
||||
xcb_aux_change_window_attributes(conn, window, XCB_CW_CURSOR, ¶ms);
|
||||
}
|
||||
|
||||
void window_grab_button(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
button_t button,
|
||||
modifier_mask_t modifiers
|
||||
) {
|
||||
auto xcb_button = button_zdwm_to_xcb(button);
|
||||
if (xcb_button == XCB_BUTTON_INDEX_ANY) return;
|
||||
|
||||
xcb_grab_button(
|
||||
backend->conn,
|
||||
false, /* 事件仅发送给抓取者,不发送给其他窗口 */
|
||||
window,
|
||||
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE,
|
||||
XCB_GRAB_MODE_ASYNC,
|
||||
XCB_GRAB_MODE_SYNC,
|
||||
XCB_WINDOW_NONE,
|
||||
XCB_CURSOR_NONE,
|
||||
xcb_button,
|
||||
modifiers_zdwm_to_xcb(modifiers)
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "base/app.h"
|
||||
#include "interface/backend.h"
|
||||
#include "internal.h"
|
||||
|
||||
typedef struct atoms_t atoms_t;
|
||||
typedef struct window_list_t window_list_t;
|
||||
|
||||
/**
|
||||
* @brief 获取窗口的 role 属性
|
||||
* @param backend 后端实例指针
|
||||
* @param window 目标窗口
|
||||
* @return 返回窗口的 role 属性字符串,调用者持有其内存并负责调用 free 释放
|
||||
*/
|
||||
char *window_get_role(backend_t *backend, xcb_window_t window);
|
||||
|
||||
/**
|
||||
* @brief 获取窗口标题
|
||||
* @details 获取窗口的 _NET_WM_NAME/WM_NAME 属性作为其 title
|
||||
* @param backend 后端实例指针
|
||||
* @param window 目标窗口
|
||||
* @return 返回窗口标题字符串,由调用者持有其内存并负责调用 free 释放
|
||||
*/
|
||||
char *window_get_title(backend_t *backend, xcb_window_t window);
|
||||
|
||||
/**
|
||||
* @brief 获取窗口的 class 和 instance 信息
|
||||
* @param backend 后端实例指针
|
||||
* @param window 目标窗口
|
||||
* @param class_out class 信息返回指针,调用者持有其内存并负责调用 free 释放
|
||||
* @param instance_out instance 信息返回指针,调用者持有其内存并负责调用 free
|
||||
* 释放
|
||||
*/
|
||||
void window_get_class(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
char **class_out,
|
||||
char **instance_out
|
||||
);
|
||||
xcb_window_t window_get_transient_for(backend_t *backend, xcb_window_t window);
|
||||
xcb_get_window_attributes_reply_t *
|
||||
window_get_attributes(backend_t *backend, xcb_window_t window);
|
||||
bool window_get_types(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
window_type_t **types,
|
||||
size_t *count
|
||||
);
|
||||
bool window_get_size_hints(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_size_hints_t *out
|
||||
);
|
||||
bool window_get_geometry(backend_t *backend, xcb_window_t window, rect_t *out);
|
||||
bool window_get_atom_array(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_atom_t property,
|
||||
xcb_atom_t **out_atoms,
|
||||
uint32_t *out_len
|
||||
);
|
||||
bool window_get_wm_hints(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
xcb_icccm_wm_hints_t *out
|
||||
);
|
||||
window_state_t atom_to_window_state(const atoms_t *atoms, xcb_atom_t atom);
|
||||
|
||||
#define window_set_name_static(conn, win, name) \
|
||||
xcb_icccm_set_wm_name(conn, win, XCB_ATOM_STRING, 8, sizeof(name) - 1, name)
|
||||
|
||||
#define window_set_class_instance(conn, win) \
|
||||
window_set_class_instance_static(conn, win, APP_NAME, APP_NAME)
|
||||
#define window_set_class_instance_static(conn, win, instance, class) \
|
||||
_window_set_class_instance_static(conn, win, instance "\0" class)
|
||||
#define _window_set_class_instance_static(conn, win, instance_class) \
|
||||
xcb_icccm_set_wm_class(conn, win, sizeof(instance_class), instance_class)
|
||||
|
||||
void window_takefocus(backend_t *backend, xcb_window_t window);
|
||||
void window_kill(backend_t *backend, xcb_window_t window);
|
||||
|
||||
void root_set_event_mask(backend_t *backend);
|
||||
void window_set_event_mask(xcb_connection_t *conn, xcb_window_t window);
|
||||
void window_clean_event_mask(xcb_connection_t *conn, xcb_window_t window);
|
||||
void window_set_icccm_wm_state(
|
||||
xcb_connection_t *conn,
|
||||
xcb_window_t window,
|
||||
xcb_icccm_wm_state_t state
|
||||
);
|
||||
void window_set_net_wm_state(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
size_t count,
|
||||
xcb_atom_t *atoms
|
||||
);
|
||||
|
||||
void window_grab_keys(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
const key_bind_t *keys,
|
||||
size_t count
|
||||
);
|
||||
|
||||
typedef struct visual_t {
|
||||
uint8_t depth;
|
||||
xcb_visualtype_t *visual;
|
||||
} visual_t;
|
||||
|
||||
/**
|
||||
* @brief 获取 xcb 的 visual 及其对应的 depth
|
||||
*
|
||||
* @param prefer_alpha 支持 alpha 的 visual 优先
|
||||
*
|
||||
* @returns 返回的信息使用后记得使用 free 释放
|
||||
*/
|
||||
visual_t *window_get_visual(backend_t *backend, bool prefer_alpha);
|
||||
|
||||
void window_change_cursor(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
cursor_t cursor
|
||||
);
|
||||
|
||||
void window_grab_button(
|
||||
backend_t *backend,
|
||||
xcb_window_t window,
|
||||
button_t button,
|
||||
modifier_mask_t modifiers
|
||||
);
|
||||
37
src/backtrace.c
Normal file
37
src/backtrace.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "backtrace.h"
|
||||
|
||||
#include "app.h"
|
||||
#include "buffer.h"
|
||||
|
||||
#ifdef HAS_EXECINFO
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
|
||||
#define MAX_STACK_SIZE 32
|
||||
|
||||
/**
|
||||
* Get a backtrace.
|
||||
* @param buffer The buffer to fill with backtrace.
|
||||
*/
|
||||
void backtrace_get(buffer_t *buffer) {
|
||||
buffer_init(buffer);
|
||||
|
||||
#ifdef HAS_EXECINFO
|
||||
void *stack[MAX_STACK_SIZE];
|
||||
char **bt;
|
||||
int stack_size;
|
||||
|
||||
stack_size = backtrace(stack, countof(stack));
|
||||
bt = backtrace_symbols(stack, stack_size);
|
||||
|
||||
if (bt) {
|
||||
for (int i = 0; i < stack_size; i++) {
|
||||
if (i > 0) buffer_addsl(buffer, "\n");
|
||||
buffer_adds(buffer, bt[i]);
|
||||
}
|
||||
p_delete(&bt);
|
||||
} else
|
||||
#endif
|
||||
|
||||
buffer_addsl(buffer, "Cannot get backtrace symbols.");
|
||||
}
|
||||
5
src/backtrace.h
Normal file
5
src/backtrace.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "buffer.h"
|
||||
|
||||
void backtrace_get(buffer_t *buffer);
|
||||
527
src/bar/bar.c
527
src/bar/bar.c
@@ -1,527 +0,0 @@
|
||||
#include "bar/bar.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <cairo.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "bar/binding.h"
|
||||
#include "bar/cell.h"
|
||||
#include "bar/text.h"
|
||||
#include "bar/tray.h"
|
||||
#include "bar/types.h"
|
||||
#include "bar/windows.h"
|
||||
#include "bar/workspaces.h"
|
||||
#include "base/array.h"
|
||||
#include "base/color.h"
|
||||
#include "base/macros.h"
|
||||
#include "base/memory.h"
|
||||
#include "base/time.h"
|
||||
|
||||
static zdwm_bar_item_t *bar_add_item(
|
||||
zdwm_output_id_t output_id,
|
||||
bar_side_t *side,
|
||||
zdwm_bar_item_type_t item_type,
|
||||
void *config
|
||||
) {
|
||||
auto item = array_push(side->items, side->count, side->capacity);
|
||||
item->api = item_type;
|
||||
|
||||
auto create_state = item_type.create_state;
|
||||
if (create_state) item->state = create_state(output_id, config);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
static void bar_item_cleanup(zdwm_bar_item_t *item) {
|
||||
auto destroy_state = item->api.destroy_state;
|
||||
if (destroy_state) destroy_state(item->state);
|
||||
item->state = nullptr;
|
||||
|
||||
for (size_t i = 0; i < item->count; ++i) {
|
||||
auto cell = &item->cells[i];
|
||||
p_delete(&cell->text);
|
||||
p_delete(&cell->fg_text);
|
||||
p_delete(&cell->bg_text);
|
||||
}
|
||||
|
||||
p_delete(&item->cells);
|
||||
p_clear(item, 1);
|
||||
}
|
||||
|
||||
#define VALUE(value, fallback) ((value) ? (value) : (fallback))
|
||||
|
||||
void bar_output_add_workspace(
|
||||
bar_output_t *bar_output,
|
||||
zdwm_bar_config_t *config,
|
||||
listeners_t *listeners
|
||||
) {
|
||||
bar_workspace_config_t workspace_config = {
|
||||
.cell_padding = VALUE(config->tag_cell_padding_x, config->padding_x),
|
||||
.indicator_width = VALUE(config->tag_indicator_width, 4),
|
||||
.bg = VALUE(config->tag_bg, config->bg),
|
||||
.fg = VALUE(config->tag_fg, config->fg),
|
||||
.active_bg = config->tag_active_bg,
|
||||
.active_fg = config->tag_active_fg,
|
||||
.urgent_bg = config->tag_urgent_bg,
|
||||
.urgent_fg = config->tag_urgent_fg,
|
||||
.layout_bg = VALUE(config->layout_bg, config->bg),
|
||||
.layout_fg = VALUE(config->layout_fg, config->fg),
|
||||
};
|
||||
auto item = bar_add_item(
|
||||
bar_output->output_id,
|
||||
&bar_output->left,
|
||||
bar_workspace,
|
||||
&workspace_config
|
||||
);
|
||||
item->cell_padding = workspace_config.cell_padding;
|
||||
item->indicator_width = workspace_config.indicator_width;
|
||||
|
||||
bar_workspace_add_listeners(listeners, item->state);
|
||||
}
|
||||
|
||||
static void bar_output_add_binding(
|
||||
bar_output_t *bar_output,
|
||||
zdwm_bar_config_t *config,
|
||||
listeners_t *listeners
|
||||
) {
|
||||
bar_binding_config_t binding_config = {
|
||||
.show_default = config->binding_show_default,
|
||||
.cell_padding = VALUE(config->binding_padding_x, config->padding_x),
|
||||
.bg = VALUE(config->binding_mode_bg, config->bg),
|
||||
.fg = VALUE(config->binding_mode_fg, config->fg),
|
||||
};
|
||||
auto item = bar_add_item(
|
||||
bar_output->output_id,
|
||||
&bar_output->left,
|
||||
bar_binding,
|
||||
&binding_config
|
||||
);
|
||||
item->cell_padding = binding_config.cell_padding;
|
||||
|
||||
bar_binding_add_listeners(listeners, item->state);
|
||||
}
|
||||
|
||||
static void bar_output_add_windows(
|
||||
bar_output_t *bar_output,
|
||||
zdwm_bar_config_t *config,
|
||||
listeners_t *listeners
|
||||
) {
|
||||
auto item = &bar_output->center;
|
||||
|
||||
bar_windows_config_t window_config = {
|
||||
.cell_padding = VALUE(config->window_padding_x, config->padding_x),
|
||||
.bg = VALUE(config->window_bg, config->bg),
|
||||
.fg = VALUE(config->window_fg, config->fg),
|
||||
.focused_bg = VALUE(config->window_focused_bg, config->bg),
|
||||
.focused_fg = VALUE(config->window_focused_fg, config->fg),
|
||||
};
|
||||
item->cell_padding = window_config.cell_padding;
|
||||
|
||||
item->api = bar_windows;
|
||||
|
||||
auto create_state = item->api.create_state;
|
||||
auto output_id = bar_output->output_id;
|
||||
if (create_state) item->state = create_state(output_id, &window_config);
|
||||
|
||||
bar_windows_add_listeners(listeners, item->state);
|
||||
}
|
||||
|
||||
static inline void bar_output_add_tray(bar_output_t *bar_output, tray_t *tray) {
|
||||
bar_add_item(bar_output->output_id, &bar_output->right, bar_tray, tray);
|
||||
}
|
||||
|
||||
void bar_init(bar_t *bar, listeners_t *listeners) {
|
||||
auto c = &bar->config;
|
||||
bar->ctx = text_context_create(c->font_family, c->font_size, c->dpi);
|
||||
auto tray = &bar->tray;
|
||||
|
||||
for (size_t i = 0; i < bar->count; ++i) {
|
||||
auto bar_output = &bar->bars[i];
|
||||
|
||||
bar_output->height = bar->config.height;
|
||||
bar_output_add_workspace(bar_output, &bar->config, listeners);
|
||||
bar_output_add_binding(bar_output, &bar->config, listeners);
|
||||
bar_output_add_windows(bar_output, &bar->config, listeners);
|
||||
|
||||
if (tray->api.host_window(tray->handle) == bar_output->window_id) {
|
||||
bar_output_add_tray(bar_output, &bar->tray);
|
||||
}
|
||||
}
|
||||
|
||||
bar->timerfd = time_create_monotonic_timerfd_by_fps(bar->config.fps);
|
||||
}
|
||||
|
||||
static void bar_side_cleanup(bar_side_t *side) {
|
||||
for (size_t i = 0; i < side->count; ++i) bar_item_cleanup(&side->items[i]);
|
||||
|
||||
p_delete(&side->items);
|
||||
p_clear(side, 1);
|
||||
}
|
||||
|
||||
void bar_cleanup(bar_t *bar) {
|
||||
close(bar->timerfd);
|
||||
bar->timerfd = -1;
|
||||
|
||||
text_context_destory(bar->ctx);
|
||||
bar->ctx = nullptr;
|
||||
|
||||
for (size_t i = 0; i < bar->count; ++i) {
|
||||
auto bar_output = &bar->bars[i];
|
||||
bar_side_cleanup(&bar_output->left);
|
||||
bar_side_cleanup(&bar_output->right);
|
||||
bar_item_cleanup(&bar_output->center);
|
||||
|
||||
cairo_destroy(bar_output->cr);
|
||||
bar_output->cr = nullptr;
|
||||
}
|
||||
p_delete(&bar->bars);
|
||||
bar->count = 0;
|
||||
}
|
||||
|
||||
static inline void bar_item_update(zdwm_bar_item_t *item) {
|
||||
auto update = item->api.update;
|
||||
if (!update) return;
|
||||
|
||||
auto update_interval_ms = item->api.update_interval_ms;
|
||||
auto last_updated_time = item->last_updated_time;
|
||||
auto now = time_monotonic_ms();
|
||||
if (now < last_updated_time + update_interval_ms) return;
|
||||
|
||||
item->last_updated_time = now;
|
||||
update(item, &bar_cell_api, item->state);
|
||||
}
|
||||
|
||||
static inline void bar_side_update(bar_side_t *side) {
|
||||
for (size_t i = 0; i < side->count; ++i) {
|
||||
bar_item_update(&side->items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void bar_update(bar_t *bar) {
|
||||
for (size_t i = 0; i < bar->count; ++i) {
|
||||
auto bar_output = &bar->bars[i];
|
||||
bar_side_update(&bar_output->left);
|
||||
bar_side_update(&bar_output->right);
|
||||
bar_item_update(&bar_output->center);
|
||||
}
|
||||
}
|
||||
|
||||
static void bar_output_layout(bar_output_t *bar_output, text_context_t *ctx) {
|
||||
int32_t start = 0;
|
||||
auto end = bar_output->width;
|
||||
|
||||
auto left = &bar_output->left;
|
||||
for (size_t i = 0; i < left->count; ++i) {
|
||||
auto item = &left->items[i];
|
||||
|
||||
item->region.start = start;
|
||||
for (size_t j = 0; j < item->count; ++j) {
|
||||
auto cell = &item->cells[j];
|
||||
auto fixed_width = cell->fixed_width;
|
||||
|
||||
bar_x_region_t region = {.start = start, .end = start};
|
||||
if (fixed_width == 0) {
|
||||
int32_t width = 0;
|
||||
text_context_get_text_size(ctx, cell->text, &width, nullptr);
|
||||
region.end += width + 2 * item->cell_padding;
|
||||
} else if (fixed_width > 0) {
|
||||
region.end += fixed_width;
|
||||
}
|
||||
bar_cell_set_region(item, j, region);
|
||||
|
||||
start = region.end;
|
||||
}
|
||||
item->region.end = start;
|
||||
}
|
||||
if (left->count) {
|
||||
left->region.start = left->items[0].region.start;
|
||||
left->region.end = left->items[left->count - 1].region.end;
|
||||
}
|
||||
|
||||
auto right = &bar_output->right;
|
||||
for (size_t i = right->count; i > 0; --i) {
|
||||
auto item_index = i - 1;
|
||||
auto item = &right->items[item_index];
|
||||
|
||||
item->region.end = end;
|
||||
for (size_t j = item->count; j > 0; --j) {
|
||||
auto cell_index = j - 1;
|
||||
auto cell = &item->cells[cell_index];
|
||||
auto fixed_width = cell->fixed_width;
|
||||
|
||||
bar_x_region_t region = {.start = end, .end = end};
|
||||
if (fixed_width == 0) {
|
||||
int32_t width = 0;
|
||||
text_context_get_text_size(ctx, cell->text, &width, nullptr);
|
||||
region.start -= width + item->cell_padding * 2;
|
||||
} else if (fixed_width > 0) {
|
||||
region.start -= fixed_width;
|
||||
}
|
||||
bar_cell_set_region(item, cell_index, region);
|
||||
|
||||
end = region.start;
|
||||
}
|
||||
item->region.start = end;
|
||||
}
|
||||
if (right->count) {
|
||||
right->region.start = right->items[0].region.start;
|
||||
right->region.end = right->items[right->count - 1].region.end;
|
||||
}
|
||||
|
||||
auto center = &bar_output->center;
|
||||
if (center->count == 0) return;
|
||||
center->region = (bar_x_region_t){.start = start, .end = end};
|
||||
|
||||
auto center_width = end - start;
|
||||
auto center_count = center->count;
|
||||
for (size_t i = 0; i < center->count; ++i) {
|
||||
auto cell = ¢er->cells[i];
|
||||
auto fixed_width = cell->fixed_width;
|
||||
if (fixed_width > 0) {
|
||||
center_width -= fixed_width;
|
||||
} else if (fixed_width < 0) {
|
||||
/* 隐藏的 cell 不占任何空间,平分宽度时不参与 */
|
||||
--center_count;
|
||||
}
|
||||
}
|
||||
|
||||
auto width = center_width / (int32_t)center_count;
|
||||
for (size_t i = 0; i < center->count; ++i) {
|
||||
auto cell = ¢er->cells[i];
|
||||
auto fixed_width = cell->fixed_width;
|
||||
|
||||
bar_x_region_t region = {.start = start, .end = start};
|
||||
if (fixed_width == 0) {
|
||||
region.end += width + center->cell_padding * 2;
|
||||
} else if (fixed_width > 0) {
|
||||
region.end += fixed_width;
|
||||
}
|
||||
start = region.end;
|
||||
bar_cell_set_region(center, i, region);
|
||||
}
|
||||
}
|
||||
|
||||
static void bar_item_draw(
|
||||
cairo_t *cr,
|
||||
zdwm_bar_item_t *item,
|
||||
text_context_t *ctx,
|
||||
int32_t height
|
||||
) {
|
||||
for (size_t i = 0; i < item->count; ++i) {
|
||||
auto cell = &item->cells[i];
|
||||
auto region = &cell->region;
|
||||
|
||||
zdwm_rect_t cell_area = {
|
||||
.x = region->start,
|
||||
.y = 0,
|
||||
.width = region->end - region->start,
|
||||
.height = height,
|
||||
};
|
||||
if (cell_area.width <= 0) {
|
||||
cell->dirty = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
draw_background(cr, &cell->bg, cell_area);
|
||||
|
||||
if (cell->show_indicator) {
|
||||
auto size = MIN(item->indicator_width, cell_area.width);
|
||||
size = MIN(size, height);
|
||||
|
||||
if (size > 0) {
|
||||
zdwm_rect_t indicator_area = {
|
||||
.x = region->start,
|
||||
.y = 0,
|
||||
.width = size,
|
||||
.height = size,
|
||||
};
|
||||
draw_background(cr, &cell->fg, indicator_area);
|
||||
}
|
||||
}
|
||||
|
||||
zdwm_rect_t text_area = {
|
||||
.x = cell_area.x + item->cell_padding,
|
||||
.y = 0,
|
||||
.width = cell_area.width - item->cell_padding * 2,
|
||||
.height = height,
|
||||
};
|
||||
if (text_area.width > 0 && cell->text && cell->text[0] != '\0') {
|
||||
draw_text(cr, ctx, cell->text, &cell->fg, text_area);
|
||||
}
|
||||
|
||||
cell->dirty = false;
|
||||
}
|
||||
|
||||
item->dirty = false;
|
||||
}
|
||||
|
||||
static bool bar_item_is_dirty(zdwm_bar_item_t *item) {
|
||||
if (item->dirty) return true;
|
||||
|
||||
for (size_t i = 0; i < item->count; ++i) {
|
||||
auto cell = &item->cells[i];
|
||||
if (cell->dirty) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool bar_side_is_dirty(bar_side_t *side) {
|
||||
for (size_t j = 0; j < side->count; ++j) {
|
||||
auto item = &side->items[j];
|
||||
if (bar_item_is_dirty(item)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool bar_output_is_dirty(bar_output_t *bar_output) {
|
||||
if (bar_side_is_dirty(&bar_output->left)) return true;
|
||||
if (bar_side_is_dirty(&bar_output->right)) return true;
|
||||
if (bar_item_is_dirty(&bar_output->center)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline void clean_cairo_context(cairo_t *cr) {
|
||||
cairo_save(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
static void
|
||||
bar_output_draw(bar_output_t *bar_output, text_context_t *ctx, color_t *bg) {
|
||||
auto cr = bar_output->cr;
|
||||
clean_cairo_context(cr);
|
||||
|
||||
zdwm_rect_t output_area = {
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = bar_output->width,
|
||||
.height = bar_output->height,
|
||||
};
|
||||
draw_background(cr, bg, output_area);
|
||||
|
||||
auto left = &bar_output->left;
|
||||
for (size_t i = 0; i < left->count; ++i) {
|
||||
bar_item_draw(cr, &left->items[i], ctx, bar_output->height);
|
||||
}
|
||||
auto right = &bar_output->right;
|
||||
for (size_t i = 0; i < right->count; ++i) {
|
||||
bar_item_draw(cr, &right->items[i], ctx, bar_output->height);
|
||||
}
|
||||
|
||||
bar_item_draw(cr, &bar_output->center, ctx, bar_output->height);
|
||||
}
|
||||
|
||||
static inline void bar_item_run_hook(
|
||||
zdwm_bar_item_t *item,
|
||||
void (*hook)(const zdwm_bar_item_hook_params_t *params)
|
||||
) {
|
||||
if (!hook) return;
|
||||
|
||||
zdwm_bar_item_hook_params_t params = {
|
||||
.item = item,
|
||||
.cell_api = &bar_cell_api,
|
||||
.region = item->region,
|
||||
.state = item->state
|
||||
};
|
||||
hook(¶ms);
|
||||
}
|
||||
|
||||
static void visitor_after_layout(zdwm_bar_item_t *item) {
|
||||
bar_item_run_hook(item, item->api.after_layout);
|
||||
}
|
||||
|
||||
static void visitor_after_draw(zdwm_bar_item_t *item) {
|
||||
bar_item_run_hook(item, item->api.after_draw);
|
||||
}
|
||||
|
||||
typedef void (*bar_item_visitor_t)(zdwm_bar_item_t *item);
|
||||
|
||||
static void
|
||||
bar_output_foreach_item(bar_output_t *bar_output, bar_item_visitor_t visit) {
|
||||
for (size_t i = 0; i < bar_output->left.count; ++i) {
|
||||
visit(&bar_output->left.items[i]);
|
||||
}
|
||||
visit(&bar_output->center);
|
||||
for (size_t i = 0; i < bar_output->right.count; ++i) {
|
||||
visit(&bar_output->right.items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool bar_draw(bar_t *bar) {
|
||||
auto ctx = bar->ctx;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
for (size_t i = 0; i < bar->count; ++i) {
|
||||
auto bar_output = &bar->bars[i];
|
||||
if (!bar_output_is_dirty(bar_output)) continue;
|
||||
|
||||
bar_output_layout(bar_output, ctx);
|
||||
bar_output_foreach_item(bar_output, visitor_after_layout);
|
||||
bar_output_draw(bar_output, ctx, &bar->palette.bg);
|
||||
bar_output_foreach_item(bar_output, visitor_after_draw);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
static inline bool bar_item_click(
|
||||
zdwm_bar_item_t *item,
|
||||
bar_click_info_t info,
|
||||
zdwm_action_t *action
|
||||
) {
|
||||
auto x = info.x;
|
||||
if (!(item->region.start <= x && item->region.end >= x)) return false;
|
||||
|
||||
if (!item->api.on_click) return true;
|
||||
|
||||
for (size_t i = 0; i < item->count; ++i) {
|
||||
auto cell = &item->cells[i];
|
||||
if (cell->region.start <= x && cell->region.end >= x) {
|
||||
zdwm_bar_click_params_t params = {
|
||||
.item = item,
|
||||
.cell_index = i,
|
||||
.x = x,
|
||||
.modifiers = info.modifiers,
|
||||
.button = info.button,
|
||||
.state = item->state,
|
||||
};
|
||||
*action = item->api.on_click(¶ms);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
bar_side_click(bar_side_t *side, bar_click_info_t info, zdwm_action_t *action) {
|
||||
if (side->region.start > info.x || side->region.end < info.x) return false;
|
||||
|
||||
for (size_t i = 0; i < side->count; ++i) {
|
||||
if (bar_item_click(&side->items[i], info, action)) return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bar_click(bar_t *bar, bar_click_info_t info, zdwm_action_t *action) {
|
||||
for (size_t i = 0; i < bar->count; ++i) {
|
||||
auto bar_output = &bar->bars[i];
|
||||
if (bar_output->window_id != info.window) continue;
|
||||
|
||||
if (bar_side_click(&bar_output->left, info, action)) return true;
|
||||
if (bar_item_click(&bar_output->center, info, action)) return true;
|
||||
if (bar_side_click(&bar_output->right, info, action)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cairo.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "bar/text.h"
|
||||
#include "bar/types.h"
|
||||
#include "base/color.h"
|
||||
#include "common/listeners.h"
|
||||
#include "interface/tray.h"
|
||||
|
||||
typedef struct bar_output_t {
|
||||
cairo_t *cr;
|
||||
zdwm_window_id_t window_id;
|
||||
zdwm_output_id_t output_id;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
bar_side_t left;
|
||||
bar_side_t right;
|
||||
zdwm_bar_item_t center;
|
||||
} bar_output_t;
|
||||
|
||||
typedef struct bar_palette_t {
|
||||
color_t bg;
|
||||
} bar_palette_t;
|
||||
|
||||
typedef struct bar_t {
|
||||
bar_output_t *bars;
|
||||
size_t count;
|
||||
|
||||
bool visible;
|
||||
int timerfd;
|
||||
|
||||
zdwm_bar_config_t config;
|
||||
bar_palette_t palette;
|
||||
text_context_t *ctx;
|
||||
tray_t tray;
|
||||
} bar_t;
|
||||
|
||||
void bar_init(bar_t *bar, listeners_t *listeners);
|
||||
void bar_cleanup(bar_t *bar);
|
||||
void bar_update(bar_t *bar);
|
||||
bool bar_draw(bar_t *bar);
|
||||
|
||||
typedef struct bar_click_info_t {
|
||||
zdwm_window_id_t window;
|
||||
int32_t x;
|
||||
zdwm_modifier_mask_t modifiers;
|
||||
zdwm_button_t button;
|
||||
} bar_click_info_t;
|
||||
bool bar_click(bar_t *bar, bar_click_info_t info, zdwm_action_t *action);
|
||||
@@ -1,99 +0,0 @@
|
||||
#include "bar/binding.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/listeners.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/memory.h"
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_binding_state_t {
|
||||
zdwm_binding_mode_notify_t mode;
|
||||
|
||||
bar_binding_config_t config;
|
||||
|
||||
bool inited;
|
||||
bool dirty;
|
||||
} bar_binding_state_t;
|
||||
|
||||
static void *
|
||||
bar_binding_create_state(zdwm_output_id_t output_id, void *config) {
|
||||
const bar_binding_config_t *bar_config = config;
|
||||
assert(bar_config);
|
||||
|
||||
auto state = p_new(bar_binding_state_t, 1);
|
||||
state->config = *bar_config;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static void bar_binding_update(
|
||||
zdwm_bar_item_t *item,
|
||||
const zdwm_bar_cell_api_t *cells,
|
||||
void *state
|
||||
) {
|
||||
auto data = (bar_binding_state_t *)state;
|
||||
if (!data->inited || !data->dirty) return;
|
||||
|
||||
auto config = &data->config;
|
||||
auto mode = &data->mode;
|
||||
|
||||
if (!config->show_default && mode->is_default_mode) {
|
||||
cells->set_cell_count(item, 0);
|
||||
} else {
|
||||
constexpr auto index = 0;
|
||||
cells->set_cell_count(item, 1);
|
||||
cells->cell_set_text(item, index, mode->name);
|
||||
cells->cell_set_bg(item, index, config->bg);
|
||||
cells->cell_set_fg(item, index, config->fg);
|
||||
}
|
||||
|
||||
data->dirty = false;
|
||||
}
|
||||
|
||||
static zdwm_action_t bar_binding_on_click(zdwm_bar_click_params_t *params) {
|
||||
return (zdwm_action_t){
|
||||
.type = ZDWM_ACTION_BINDING_MODE_CYCLE,
|
||||
.as.binding_mode_cycle = {.delta = 1},
|
||||
};
|
||||
}
|
||||
|
||||
static void bar_binding_destroy_state(void *state) {
|
||||
if (!state) return;
|
||||
|
||||
auto data = (bar_binding_state_t *)state;
|
||||
p_delete(&data);
|
||||
}
|
||||
|
||||
zdwm_bar_item_type_t bar_binding = {
|
||||
.create_state = bar_binding_create_state,
|
||||
.update = bar_binding_update,
|
||||
.on_click = bar_binding_on_click,
|
||||
.destroy_state = bar_binding_destroy_state,
|
||||
.update_interval_ms = 0,
|
||||
};
|
||||
|
||||
static void
|
||||
bar_binding_mode_notify(zdwm_binding_mode_notify_t mode, void *user_data) {
|
||||
auto state = (bar_binding_state_t *)user_data;
|
||||
if (!state->inited) {
|
||||
state->mode = mode;
|
||||
|
||||
state->inited = true;
|
||||
state->dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->mode.id == mode.id) return;
|
||||
state->mode = mode;
|
||||
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
void bar_binding_add_listeners(listeners_t *listeners, void *state) {
|
||||
listeners_add_binding_mode_notify(listeners, bar_binding_mode_notify, state);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <zdwm/bar.h>
|
||||
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_binding_config_t {
|
||||
bool show_default;
|
||||
uint32_t cell_padding;
|
||||
const char *bg;
|
||||
const char *fg;
|
||||
} bar_binding_config_t;
|
||||
|
||||
extern zdwm_bar_item_type_t bar_binding;
|
||||
|
||||
void bar_binding_add_listeners(listeners_t *listeners, void *state);
|
||||
173
src/bar/cell.c
173
src/bar/cell.c
@@ -1,173 +0,0 @@
|
||||
#include "bar/cell.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "bar/types.h"
|
||||
#include "base/color.h"
|
||||
#include "base/memory.h"
|
||||
|
||||
static size_t bar_cell_get_count(zdwm_bar_item_t *item) { return item->count; }
|
||||
|
||||
static void bar_cell_set_count(zdwm_bar_item_t *item, size_t count) {
|
||||
if (item->count == count) return;
|
||||
|
||||
for (size_t i = 0; i < item->count; ++i) {
|
||||
auto cell = &item->cells[i];
|
||||
p_delete(&cell->text);
|
||||
p_delete(&cell->fg_text);
|
||||
p_delete(&cell->bg_text);
|
||||
}
|
||||
|
||||
item->count = count;
|
||||
p_realloc(&item->cells, count);
|
||||
p_clear(item->cells, count);
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_text(zdwm_bar_item_t *item, size_t index, const char *text) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
if (cell->text && text && strcmp(cell->text, text) == 0) return;
|
||||
|
||||
p_delete(&cell->text);
|
||||
|
||||
cell->text = p_strdup_nullable(text);
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_bg(zdwm_bar_item_t *item, size_t index, const char *color) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
|
||||
if (cell->bg_text && strcmp(color, cell->bg_text) == 0) return;
|
||||
|
||||
p_delete(&cell->bg_text);
|
||||
cell->bg_text = p_strdup(color);
|
||||
color_parse(color, &cell->bg);
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_fg(zdwm_bar_item_t *item, size_t index, const char *color) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
if (cell->fg_text && strcmp(color, cell->fg_text) == 0) return;
|
||||
|
||||
p_delete(&cell->fg_text);
|
||||
cell->fg_text = p_strdup(color);
|
||||
color_parse(color, &cell->fg);
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_icon(zdwm_bar_item_t *item, size_t index, zdwm_icon_t icon) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
auto icon_ptr = &cell->icon;
|
||||
|
||||
if (icon_ptr->type == icon.type) {
|
||||
switch (icon.type) {
|
||||
case ZDWM_ICON_TEXT:
|
||||
if (
|
||||
icon_ptr->as.text == icon.as.text ||
|
||||
strcmp(icon_ptr->as.text, icon.as.text) == 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
case ZDWM_ICON_IMAGE:
|
||||
if (
|
||||
icon_ptr->as.image_path == icon.as.image_path ||
|
||||
strcmp(icon_ptr->as.image_path, icon.as.image_path) == 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*icon_ptr = icon;
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_indicator(zdwm_bar_item_t *item, size_t index, bool show) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
if (cell->show_indicator == show) return;
|
||||
|
||||
cell->show_indicator = show;
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_cell_set_fixed_width(zdwm_bar_item_t *item, size_t index, int32_t width) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
if (cell->fixed_width == width) return;
|
||||
|
||||
cell->fixed_width = width;
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
|
||||
static bar_x_region_t bar_cell_get_region(zdwm_bar_item_t *item, size_t index) {
|
||||
if (index >= item->count) return (bar_x_region_t){0};
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
return cell->region;
|
||||
}
|
||||
|
||||
/* clang-format off */
|
||||
const zdwm_bar_cell_api_t bar_cell_api = {
|
||||
.get_cell_count = bar_cell_get_count,
|
||||
.set_cell_count = bar_cell_set_count,
|
||||
.cell_set_text = bar_cell_set_text,
|
||||
.cell_set_bg = bar_cell_set_bg,
|
||||
.cell_set_fg = bar_cell_set_fg,
|
||||
.cell_set_icon = bar_cell_set_icon,
|
||||
.cell_set_indicator = bar_cell_set_indicator,
|
||||
.cell_set_fixed_width = bar_cell_set_fixed_width,
|
||||
.cell_get_region = bar_cell_get_region,
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
void bar_cell_set_region(
|
||||
zdwm_bar_item_t *item,
|
||||
size_t index,
|
||||
bar_x_region_t region
|
||||
) {
|
||||
if (index >= item->count) return;
|
||||
|
||||
auto cell = &item->cells[index];
|
||||
auto r = &cell->region;
|
||||
|
||||
if (r->start == region.start && r->end == region.end) return;
|
||||
|
||||
*r = region;
|
||||
|
||||
cell->dirty = true;
|
||||
item->dirty = true;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "bar/types.h"
|
||||
|
||||
void bar_cell_set_region(
|
||||
zdwm_bar_item_t *item,
|
||||
size_t index,
|
||||
bar_x_region_t region
|
||||
);
|
||||
|
||||
extern const zdwm_bar_cell_api_t bar_cell_api;
|
||||
141
src/bar/text.c
141
src/bar/text.c
@@ -1,141 +0,0 @@
|
||||
#include "bar/text.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <cairo.h>
|
||||
#include <glib-object.h>
|
||||
#include <pango/pango-attributes.h>
|
||||
#include <pango/pango-context.h>
|
||||
#include <pango/pango-font.h>
|
||||
#include <pango/pango-layout.h>
|
||||
#include <pango/pango-types.h>
|
||||
#include <pango/pangocairo.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "base/memory.h"
|
||||
|
||||
struct text_context_t {
|
||||
PangoContext *context;
|
||||
PangoLayout *layout;
|
||||
PangoAttrList *attr_list;
|
||||
};
|
||||
|
||||
text_context_t *
|
||||
text_context_create(const char *family, uint32_t size, uint32_t dpi) {
|
||||
auto fontmap = pango_cairo_font_map_new();
|
||||
auto context = pango_font_map_create_context(fontmap);
|
||||
pango_cairo_context_set_resolution(context, (double)dpi);
|
||||
|
||||
auto desc = pango_font_description_from_string(family);
|
||||
pango_font_description_set_size(desc, size * PANGO_SCALE);
|
||||
auto attr = pango_attr_font_desc_new(desc);
|
||||
|
||||
auto attr_list = pango_attr_list_new();
|
||||
pango_attr_list_insert(attr_list, attr);
|
||||
|
||||
auto layout = pango_layout_new(context);
|
||||
pango_layout_set_attributes(layout, attr_list);
|
||||
pango_layout_set_wrap(layout, PANGO_WRAP_NONE);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
|
||||
|
||||
pango_font_description_free(desc);
|
||||
g_object_unref(fontmap);
|
||||
|
||||
auto ctx = p_new(text_context_t, 1);
|
||||
*ctx = (text_context_t){
|
||||
.context = context,
|
||||
.layout = layout,
|
||||
.attr_list = attr_list,
|
||||
};
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void text_context_destory(text_context_t *context) {
|
||||
if (!context) return;
|
||||
|
||||
if (context->layout) {
|
||||
g_object_unref(context->layout);
|
||||
context->layout = nullptr;
|
||||
}
|
||||
|
||||
if (context->attr_list) {
|
||||
pango_attr_list_unref(context->attr_list);
|
||||
context->attr_list = nullptr;
|
||||
}
|
||||
|
||||
if (context->context) {
|
||||
g_object_unref(context->context);
|
||||
context->context = nullptr;
|
||||
}
|
||||
|
||||
p_delete(&context);
|
||||
}
|
||||
|
||||
static inline void
|
||||
get_layout_size(PangoLayout *layout, int32_t *width, int32_t *height) {
|
||||
pango_layout_get_pixel_size(layout, width, height);
|
||||
}
|
||||
|
||||
void text_context_get_text_size(
|
||||
text_context_t *context,
|
||||
const char *text,
|
||||
int32_t *width,
|
||||
int32_t *height
|
||||
) {
|
||||
assert(context && text);
|
||||
if (!width && !height) return;
|
||||
|
||||
auto layout = context->layout;
|
||||
|
||||
pango_layout_set_width(layout, -1);
|
||||
pango_layout_set_height(layout, 0);
|
||||
pango_layout_set_text(layout, text, -1);
|
||||
|
||||
get_layout_size(layout, width, height);
|
||||
}
|
||||
|
||||
void draw_text(
|
||||
cairo_t *cr,
|
||||
text_context_t *context,
|
||||
const char *text,
|
||||
const color_t *color,
|
||||
zdwm_rect_t area
|
||||
) {
|
||||
assert(cr);
|
||||
assert(context);
|
||||
assert(text);
|
||||
assert(color);
|
||||
|
||||
auto layout = context->layout;
|
||||
pango_layout_set_width(layout, area.width * PANGO_SCALE);
|
||||
pango_layout_set_text(layout, text, -1);
|
||||
|
||||
cairo_set_source_rgba(
|
||||
cr,
|
||||
color->red,
|
||||
color->green,
|
||||
color->blue,
|
||||
color->alpha
|
||||
);
|
||||
|
||||
int32_t height = 0;
|
||||
get_layout_size(layout, nullptr, &height);
|
||||
|
||||
double offset_y = (area.height - height) / 2.0;
|
||||
pango_cairo_update_layout(cr, layout);
|
||||
cairo_move_to(cr, area.x, offset_y + area.y);
|
||||
pango_cairo_show_layout(cr, layout);
|
||||
}
|
||||
|
||||
void draw_background(cairo_t *cr, const color_t *color, zdwm_rect_t area) {
|
||||
cairo_move_to(cr, area.x, area.y);
|
||||
cairo_set_source_rgba(
|
||||
cr,
|
||||
color->red,
|
||||
color->green,
|
||||
color->blue,
|
||||
color->alpha
|
||||
);
|
||||
cairo_rectangle(cr, area.x, area.y, area.width, area.height);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cairo.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/color.h"
|
||||
|
||||
typedef struct text_context_t text_context_t;
|
||||
|
||||
text_context_t *
|
||||
text_context_create(const char *family, uint32_t size, uint32_t dpi);
|
||||
void text_context_destory(text_context_t *context);
|
||||
void text_context_get_text_size(
|
||||
text_context_t *context,
|
||||
const char *text,
|
||||
int32_t *width,
|
||||
int32_t *height
|
||||
);
|
||||
void draw_text(
|
||||
cairo_t *cr,
|
||||
text_context_t *context,
|
||||
const char *text,
|
||||
const color_t *color,
|
||||
zdwm_rect_t area
|
||||
);
|
||||
void draw_background(cairo_t *cr, const color_t *color, zdwm_rect_t area);
|
||||
@@ -1,78 +0,0 @@
|
||||
#include "bar/tray.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/memory.h"
|
||||
#include "interface/tray.h"
|
||||
|
||||
typedef struct bar_tray_state_t {
|
||||
tray_t tray;
|
||||
bool dirty;
|
||||
} bar_tray_state_t;
|
||||
|
||||
static void bar_tray_icons_changed(void *user_data) {
|
||||
bar_tray_state_t *state = user_data;
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void *bar_tray_create_state(zdwm_output_id_t output_id, void *config) {
|
||||
tray_t *tray = config;
|
||||
assert(tray != nullptr);
|
||||
|
||||
auto state = p_new(bar_tray_state_t, 1);
|
||||
state->tray = *tray;
|
||||
tray->api.set_listener(tray->handle, bar_tray_icons_changed, state);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static void bar_tray_update(
|
||||
zdwm_bar_item_t *item,
|
||||
const zdwm_bar_cell_api_t *cell_api,
|
||||
void *state
|
||||
) {
|
||||
bar_tray_state_t *data = state;
|
||||
if (!data->dirty) return;
|
||||
|
||||
auto tray_handle = data->tray.handle;
|
||||
auto tray_api = &data->tray.api;
|
||||
auto icon_count = tray_api->icon_count(tray_handle);
|
||||
auto cell_count = cell_api->get_cell_count(item);
|
||||
if (cell_count == icon_count) {
|
||||
data->dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cell_api->set_cell_count(item, icon_count);
|
||||
auto cell_width = tray_api->icon_size(tray_handle);
|
||||
for (size_t i = 0; i < icon_count; ++i) {
|
||||
cell_api->cell_set_fixed_width(item, i, cell_width);
|
||||
}
|
||||
|
||||
data->dirty = false;
|
||||
}
|
||||
|
||||
static void bar_tray_after_layout(const zdwm_bar_item_hook_params_t *params) {
|
||||
bar_tray_state_t *state = params->state;
|
||||
auto tray_handle = state->tray.handle;
|
||||
auto tray_api = &state->tray.api;
|
||||
tray_api->place(tray_handle, params->region.start);
|
||||
}
|
||||
|
||||
static void bar_tray_destroy_state(void *state) {
|
||||
if (!state) return;
|
||||
|
||||
bar_tray_state_t *data = state;
|
||||
p_delete(&data);
|
||||
}
|
||||
|
||||
zdwm_bar_item_type_t bar_tray = {
|
||||
.create_state = bar_tray_create_state,
|
||||
.update = bar_tray_update,
|
||||
.after_layout = bar_tray_after_layout,
|
||||
.destroy_state = bar_tray_destroy_state,
|
||||
.update_interval_ms = 0,
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <zdwm/bar.h>
|
||||
|
||||
extern zdwm_bar_item_type_t bar_tray;
|
||||
@@ -1,43 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/color.h"
|
||||
|
||||
typedef struct zdwm_bar_x_region_t bar_x_region_t;
|
||||
|
||||
typedef struct bar_cell_t {
|
||||
zdwm_icon_t icon;
|
||||
char *text; /* 持有内存,避免野指针问题 */
|
||||
char *fg_text;
|
||||
char *bg_text;
|
||||
color_t fg;
|
||||
color_t bg;
|
||||
bar_x_region_t region;
|
||||
int32_t fixed_width;
|
||||
bool show_indicator;
|
||||
bool dirty;
|
||||
} bar_cell_t;
|
||||
|
||||
struct zdwm_bar_item_t {
|
||||
bar_cell_t *cells;
|
||||
size_t count;
|
||||
bar_x_region_t region;
|
||||
int32_t cell_padding;
|
||||
int32_t indicator_width;
|
||||
|
||||
void *state;
|
||||
zdwm_bar_item_type_t api;
|
||||
bool dirty;
|
||||
uint64_t last_updated_time;
|
||||
};
|
||||
|
||||
typedef struct bar_side_t {
|
||||
zdwm_bar_item_t *items;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
bar_x_region_t region;
|
||||
} bar_side_t;
|
||||
@@ -1,175 +0,0 @@
|
||||
#include "bar/windows.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/listeners.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/array.h"
|
||||
#include "base/memory.h"
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_windows_state_t {
|
||||
zdwm_output_id_t output_id;
|
||||
zdwm_workspace_id_t workspace_id;
|
||||
|
||||
zdwm_window_t *windows;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
|
||||
bar_windows_config_t config;
|
||||
|
||||
bool workspace_inited;
|
||||
bool dirty;
|
||||
} bar_windows_state_t;
|
||||
|
||||
static zdwm_window_t *
|
||||
state_get_window(bar_windows_state_t *state, zdwm_window_id_t window_id) {
|
||||
for (size_t i = 0; i < state->count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->id == window_id) return window;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void *
|
||||
bar_windows_create_state(zdwm_output_id_t output_id, void *config) {
|
||||
const bar_windows_config_t *windows_config = config;
|
||||
assert(windows_config);
|
||||
|
||||
auto state = p_new(bar_windows_state_t, 1);
|
||||
state->output_id = output_id;
|
||||
state->workspace_id = ZDWM_WORKSPACE_ID_INVALID;
|
||||
state->config = *windows_config;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static void bar_windows_update(
|
||||
zdwm_bar_item_t *item,
|
||||
const zdwm_bar_cell_api_t *cells,
|
||||
void *state
|
||||
) {
|
||||
bar_windows_state_t *data = state;
|
||||
if (!data->workspace_inited || !data->dirty) return;
|
||||
|
||||
zdwm_window_t *windows = nullptr;
|
||||
size_t count = 0, capacity = 0;
|
||||
for (size_t i = 0; i < data->count; ++i) {
|
||||
auto win = &data->windows[i];
|
||||
if (win->workspace != data->workspace_id || win->skip_taskbar) continue;
|
||||
|
||||
auto window = array_push(windows, count, capacity);
|
||||
*window = *win;
|
||||
}
|
||||
|
||||
cells->set_cell_count(item, count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto window = &windows[i];
|
||||
auto bg = window->focused ? data->config.focused_bg : data->config.bg;
|
||||
auto fg = window->focused ? data->config.focused_fg : data->config.fg;
|
||||
cells->cell_set_text(item, i, window->title);
|
||||
cells->cell_set_bg(item, i, bg);
|
||||
cells->cell_set_fg(item, i, fg);
|
||||
}
|
||||
p_delete(&windows);
|
||||
|
||||
data->dirty = false;
|
||||
}
|
||||
|
||||
static void bar_windows_destroy_state(void *state) {
|
||||
bar_windows_state_t *data = state;
|
||||
|
||||
p_delete(&data->windows);
|
||||
p_delete(&data);
|
||||
}
|
||||
|
||||
zdwm_bar_item_type_t bar_windows = {
|
||||
.create_state = bar_windows_create_state,
|
||||
.update = bar_windows_update,
|
||||
.destroy_state = bar_windows_destroy_state,
|
||||
.update_interval_ms = 0,
|
||||
};
|
||||
|
||||
static void bar_windows_workspace_active(
|
||||
zdwm_output_id_t output_id,
|
||||
zdwm_workspace_id_t workspace_id,
|
||||
void *user_data
|
||||
) {
|
||||
bar_windows_state_t *state = user_data;
|
||||
if (state->output_id != output_id) return;
|
||||
if (state->workspace_id == workspace_id) return;
|
||||
|
||||
state->workspace_id = workspace_id;
|
||||
|
||||
state->workspace_inited = true;
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_windows_add_window(const zdwm_window_t *window, void *user_data) {
|
||||
bar_windows_state_t *state = user_data;
|
||||
auto win = array_push(state->windows, state->count, state->capacity);
|
||||
*win = *window;
|
||||
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_windows_initial(const zdwm_window_t *list, size_t count, void *user_data) {
|
||||
bar_windows_state_t *state = user_data;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto window = &list[i];
|
||||
bar_windows_add_window(window, state);
|
||||
}
|
||||
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_windows_update_window(const zdwm_window_t *window, void *user_data) {
|
||||
bar_windows_state_t *state = user_data;
|
||||
|
||||
auto old_window = state_get_window(state, window->id);
|
||||
*old_window = *window;
|
||||
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_windows_remove_window(zdwm_window_id_t window_id, void *user_data) {
|
||||
bar_windows_state_t *state = user_data;
|
||||
|
||||
bool found = false;
|
||||
size_t index = 0;
|
||||
|
||||
for (size_t i = 0; i < state->count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->id == window_id) {
|
||||
found = true;
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return;
|
||||
|
||||
if (array_erase(state->windows, state->count, index)) {
|
||||
state->dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void bar_windows_add_listeners(listeners_t *listeners, void *state) {
|
||||
#define ADD(ADD_FN, LISTENER) ADD_FN(listeners, LISTENER, state)
|
||||
|
||||
ADD(listeners_add_active_workspace_listener, bar_windows_workspace_active);
|
||||
ADD(listeners_add_initial_window_listener, bar_windows_initial);
|
||||
ADD(listeners_add_window_added_listener, bar_windows_add_window);
|
||||
ADD(listeners_add_window_updated_listener, bar_windows_update_window);
|
||||
ADD(listeners_add_window_removed_listener, bar_windows_remove_window);
|
||||
|
||||
#undef ADD
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <zdwm/bar.h>
|
||||
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_windows_config_t {
|
||||
uint32_t cell_padding;
|
||||
const char *bg;
|
||||
const char *fg;
|
||||
const char *focused_bg;
|
||||
const char *focused_fg;
|
||||
} bar_windows_config_t;
|
||||
|
||||
extern zdwm_bar_item_type_t bar_windows;
|
||||
|
||||
void bar_windows_add_listeners(listeners_t *listeners, void *state);
|
||||
@@ -1,305 +0,0 @@
|
||||
#include "bar/workspaces.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <zdwm/action.h>
|
||||
#include <zdwm/bar.h>
|
||||
#include <zdwm/listeners.h>
|
||||
#include <zdwm/types.h>
|
||||
|
||||
#include "base/array.h"
|
||||
#include "base/memory.h"
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_workspace_t {
|
||||
zdwm_workspace_t info;
|
||||
zdwm_layout_notify_t layout;
|
||||
size_t window_count;
|
||||
size_t urgent_window_count;
|
||||
} bar_workspace_t;
|
||||
|
||||
typedef struct bar_workspace_state_t {
|
||||
zdwm_output_id_t output_id;
|
||||
zdwm_workspace_id_t workspace_id;
|
||||
bar_workspace_t *workspaces;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
|
||||
zdwm_window_t *windows;
|
||||
size_t window_count;
|
||||
size_t window_capacity;
|
||||
|
||||
bar_workspace_config_t config;
|
||||
|
||||
bool list_inited;
|
||||
bool active_inited;
|
||||
bool dirty;
|
||||
} bar_workspace_state_t;
|
||||
|
||||
static size_t state_workspace_get_urgent_window_count(
|
||||
const bar_workspace_state_t *state,
|
||||
zdwm_workspace_id_t workspace_id
|
||||
) {
|
||||
size_t count = 0;
|
||||
|
||||
for (size_t i = 0; i < state->window_count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->workspace == workspace_id && window->urgent) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static size_t state_workspace_get_window_count(
|
||||
const bar_workspace_state_t *state,
|
||||
zdwm_workspace_id_t workspace_id
|
||||
) {
|
||||
size_t count = 0;
|
||||
|
||||
for (size_t i = 0; i < state->window_count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->workspace == workspace_id && !window->skip_taskbar) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static zdwm_window_t *state_get_window(
|
||||
const bar_workspace_state_t *state,
|
||||
zdwm_window_id_t window_id
|
||||
) {
|
||||
for (size_t i = 0; i < state->window_count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->id == window_id) return window;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void *
|
||||
bar_workspaces_create_state(zdwm_output_id_t output_id, void *config) {
|
||||
const bar_workspace_config_t *workspace_config = config;
|
||||
assert(workspace_config);
|
||||
|
||||
auto state = p_new(bar_workspace_state_t, 1);
|
||||
state->output_id = output_id;
|
||||
state->workspace_id = ZDWM_WORKSPACE_ID_INVALID;
|
||||
state->config = *workspace_config;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static void bar_workspaces_update(
|
||||
zdwm_bar_item_t *item,
|
||||
const zdwm_bar_cell_api_t *cell_api,
|
||||
void *state
|
||||
) {
|
||||
auto data = (bar_workspace_state_t *)state;
|
||||
if (!data->list_inited || !data->active_inited || !data->dirty) return;
|
||||
|
||||
/* 当前 workspace 的 layout 指示器放在最后 */
|
||||
cell_api->set_cell_count(item, data->count + 1);
|
||||
auto layout_index = data->count;
|
||||
|
||||
auto config = &data->config;
|
||||
for (size_t i = 0; i < data->count; ++i) {
|
||||
auto workspace = &data->workspaces[i];
|
||||
auto info = &workspace->info;
|
||||
|
||||
workspace->window_count = state_workspace_get_window_count(state, info->id);
|
||||
workspace->urgent_window_count =
|
||||
state_workspace_get_urgent_window_count(state, info->id);
|
||||
|
||||
bool show_indicator = workspace->window_count;
|
||||
cell_api->cell_set_indicator(item, i, show_indicator);
|
||||
|
||||
cell_api->cell_set_text(item, i, info->name);
|
||||
if (info->id == data->workspace_id) {
|
||||
cell_api->cell_set_bg(item, i, config->active_bg);
|
||||
cell_api->cell_set_fg(item, i, config->active_fg);
|
||||
|
||||
cell_api->cell_set_bg(item, layout_index, config->layout_bg);
|
||||
cell_api->cell_set_fg(item, layout_index, config->layout_fg);
|
||||
cell_api->cell_set_text(item, layout_index, workspace->layout.symbol);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (workspace->urgent_window_count > 0) {
|
||||
cell_api->cell_set_bg(item, i, config->urgent_bg);
|
||||
cell_api->cell_set_fg(item, i, config->urgent_fg);
|
||||
continue;
|
||||
}
|
||||
|
||||
cell_api->cell_set_bg(item, i, config->bg);
|
||||
cell_api->cell_set_fg(item, i, config->fg);
|
||||
}
|
||||
|
||||
data->dirty = false;
|
||||
}
|
||||
|
||||
static zdwm_action_t bar_workspace_on_click(zdwm_bar_click_params_t *params) {
|
||||
auto cell_index = params->cell_index;
|
||||
auto data = (bar_workspace_state_t *)params->state;
|
||||
|
||||
if (cell_index == data->count) {
|
||||
return (zdwm_action_t){
|
||||
.type = ZDWM_ACTION_LAYOUT_CYCLE,
|
||||
.as.layout_cycle = {.delta = 1},
|
||||
};
|
||||
}
|
||||
|
||||
auto workspace = &data->workspaces[cell_index];
|
||||
if (workspace->info.id == data->workspace_id) {
|
||||
return (zdwm_action_t){.type = ZDWM_ACTION_NONE};
|
||||
}
|
||||
|
||||
return (zdwm_action_t){
|
||||
.type = ZDWM_ACTION_WORKSPACE_SWITCH,
|
||||
.as.switch_workspace.workspace = workspace->info.id,
|
||||
};
|
||||
}
|
||||
|
||||
static void bar_workspace_destroy_state(void *state) {
|
||||
if (!state) return;
|
||||
|
||||
auto data = (bar_workspace_state_t *)state;
|
||||
p_delete(&data->workspaces);
|
||||
p_delete(&data->windows);
|
||||
p_delete(&data);
|
||||
}
|
||||
|
||||
zdwm_bar_item_type_t bar_workspace = {
|
||||
.create_state = bar_workspaces_create_state,
|
||||
.update = bar_workspaces_update,
|
||||
.on_click = bar_workspace_on_click,
|
||||
.destroy_state = bar_workspace_destroy_state,
|
||||
.update_interval_ms = 0,
|
||||
};
|
||||
|
||||
typedef struct bar_workspace_listener_user_data_t {
|
||||
zdwm_bar_item_t *bar;
|
||||
bar_workspace_state_t *state;
|
||||
} bar_workspace_listener_user_data_t;
|
||||
|
||||
static void bar_workspace_list_filter(
|
||||
const zdwm_workspace_t *list,
|
||||
size_t count,
|
||||
void *user_data
|
||||
) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto workspace = &list[i];
|
||||
if (workspace->output_id != state->output_id) continue;
|
||||
|
||||
auto item = array_push(state->workspaces, state->count, state->capacity);
|
||||
p_clear(item, 1);
|
||||
item->info = *workspace;
|
||||
}
|
||||
|
||||
state->list_inited = true;
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void bar_workspace_active_updated(
|
||||
zdwm_output_id_t output_id,
|
||||
zdwm_workspace_id_t workspace_id,
|
||||
void *user_data
|
||||
) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
if (state->output_id != output_id) return;
|
||||
if (state->workspace_id == workspace_id) return;
|
||||
|
||||
state->workspace_id = workspace_id;
|
||||
|
||||
state->active_inited = true;
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
bar_workspace_layout_notify(zdwm_layout_notify_t layout, void *user_data) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
|
||||
for (size_t i = 0; i < state->count; ++i) {
|
||||
auto workspace = &state->workspaces[i];
|
||||
if (workspace->info.id == layout.workspace) {
|
||||
workspace->layout = layout;
|
||||
state->dirty = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void bar_workspace_window_added(const zdwm_window_t *window, void *user_data) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
|
||||
auto win =
|
||||
array_push(state->windows, state->window_count, state->window_capacity);
|
||||
*win = *window;
|
||||
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
static void bar_workspace_initial_windows(
|
||||
const zdwm_window_t *list,
|
||||
size_t count,
|
||||
void *user_data
|
||||
) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto window = &list[i];
|
||||
bar_workspace_window_added(window, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
void bar_workspace_window_updated(
|
||||
const zdwm_window_t *window,
|
||||
void *user_data
|
||||
) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
|
||||
auto old_window = state_get_window(state, window->id);
|
||||
*old_window = *window;
|
||||
state->dirty = true;
|
||||
}
|
||||
|
||||
void bar_workspace_window_removed(zdwm_window_id_t window_id, void *user_data) {
|
||||
auto state = (bar_workspace_state_t *)user_data;
|
||||
|
||||
bool found = false;
|
||||
size_t index = 0;
|
||||
|
||||
for (size_t i = 0; i < state->window_count; ++i) {
|
||||
auto window = &state->windows[i];
|
||||
if (window->id == window_id) {
|
||||
found = true;
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) return;
|
||||
|
||||
if (array_erase(state->windows, state->window_count, index)) {
|
||||
state->dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void bar_workspace_add_listeners(listeners_t *listeners, void *state) {
|
||||
#define ADD(ADD_FN, LISTENER) ADD_FN(listeners, LISTENER, state)
|
||||
|
||||
ADD(listeners_add_initial_workspace_listener, bar_workspace_list_filter);
|
||||
ADD(listeners_add_active_workspace_listener, bar_workspace_active_updated);
|
||||
ADD(listeners_add_layout_notify, bar_workspace_layout_notify);
|
||||
ADD(listeners_add_initial_window_listener, bar_workspace_initial_windows);
|
||||
ADD(listeners_add_window_added_listener, bar_workspace_window_added);
|
||||
ADD(listeners_add_window_updated_listener, bar_workspace_window_updated);
|
||||
ADD(listeners_add_window_removed_listener, bar_workspace_window_removed);
|
||||
|
||||
#undef ADD
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <zdwm/bar.h>
|
||||
|
||||
#include "common/listeners.h"
|
||||
|
||||
typedef struct bar_workspace_config_t {
|
||||
uint32_t cell_padding;
|
||||
uint32_t indicator_width;
|
||||
const char *bg;
|
||||
const char *fg;
|
||||
const char *active_bg;
|
||||
const char *active_fg;
|
||||
const char *urgent_bg;
|
||||
const char *urgent_fg;
|
||||
const char *layout_bg;
|
||||
const char *layout_fg;
|
||||
} bar_workspace_config_t;
|
||||
|
||||
extern zdwm_bar_item_type_t bar_workspace;
|
||||
|
||||
void bar_workspace_add_listeners(listeners_t *listeners, void *state);
|
||||
17
src/base.h
Normal file
17
src/base.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct area_t {
|
||||
int16_t x, y;
|
||||
uint16_t width, height;
|
||||
} area_t;
|
||||
|
||||
typedef struct extent_in_bar_t {
|
||||
int16_t start;
|
||||
int16_t end;
|
||||
} extent_in_bar_t;
|
||||
|
||||
typedef struct point_t {
|
||||
int16_t x, y;
|
||||
} point_t;
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define APP_NAME "zdwm"
|
||||
109
src/base/array.h
109
src/base/array.h
@@ -1,109 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "base/memory.h"
|
||||
|
||||
static constexpr size_t INIT_CAPACITY = 4;
|
||||
static inline size_t next_capacity(size_t capacity) {
|
||||
if (capacity) return capacity * 2;
|
||||
return INIT_CAPACITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @file array.h
|
||||
* @brief 动态数组基础操作。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 确保数组容量至少为 need。
|
||||
*
|
||||
* @param items 数组指针地址
|
||||
* @param item_size 单个元素大小
|
||||
* @param capacity 当前容量地址
|
||||
* @param need 目标容量
|
||||
*/
|
||||
static inline void array_reserve_impl(
|
||||
void **items,
|
||||
size_t item_size,
|
||||
size_t *capacity,
|
||||
size_t need
|
||||
) {
|
||||
if (*capacity >= need) return;
|
||||
|
||||
size_t new_capacity = next_capacity(*capacity);
|
||||
while (new_capacity < need) {
|
||||
new_capacity = next_capacity(new_capacity);
|
||||
}
|
||||
|
||||
if (*items) {
|
||||
xrealloc(items, item_size * new_capacity);
|
||||
} else {
|
||||
*items = xmalloc(item_size * new_capacity);
|
||||
}
|
||||
*capacity = new_capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 追加一个槽位并返回其地址。
|
||||
*
|
||||
* @param items 数组指针地址
|
||||
* @param item_size 单个元素大小
|
||||
* @param count 元素个数地址
|
||||
* @param capacity 容量地址
|
||||
*
|
||||
* @return void* 新槽位地址
|
||||
*/
|
||||
static inline void *array_push_impl(
|
||||
void **items,
|
||||
size_t item_size,
|
||||
size_t *count,
|
||||
size_t *capacity
|
||||
) {
|
||||
array_reserve_impl(items, item_size, capacity, *count + 1);
|
||||
|
||||
void *slot = (char *)*items + *count * item_size;
|
||||
(*count)++;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 删除指定下标元素,并将后续元素左移。
|
||||
*
|
||||
* @param items 数组首地址
|
||||
* @param item_size 单个元素大小
|
||||
* @param count 元素个数地址
|
||||
* @param index 要删除的下标
|
||||
*
|
||||
* @return true 删除成功
|
||||
* @return false 下标越界
|
||||
*/
|
||||
static inline bool
|
||||
array_erase_impl(void *items, size_t item_size, size_t *count, size_t index) {
|
||||
if (index >= *count) return false;
|
||||
|
||||
size_t tail_count = *count - index - 1;
|
||||
if (tail_count > 0) {
|
||||
char *dest = (char *)items + index * item_size;
|
||||
char *src = dest + item_size;
|
||||
memmove(dest, src, tail_count * item_size);
|
||||
}
|
||||
|
||||
(*count)--;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief 为具体类型数组预留容量。 */
|
||||
#define array_reserve(items, capacity, need) \
|
||||
array_reserve_impl((void **)&(items), sizeof(*(items)), &(capacity), (need))
|
||||
|
||||
/* clang-format off */
|
||||
/** @brief 为具体类型数组追加一个槽位。 */
|
||||
#define array_push(items, count, capacity)\
|
||||
(typeof(items))array_push_impl((void **)&(items), sizeof(*(items)), &(count), &(capacity))
|
||||
/* clang-format on */
|
||||
|
||||
/** @brief 删除具体类型数组中指定下标的元素。 */
|
||||
#define array_erase(items, count, index) \
|
||||
array_erase_impl((items), sizeof(*(items)), &(count), (index))
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct color_t {
|
||||
uint32_t rgba;
|
||||
uint32_t argb;
|
||||
|
||||
/* RGBA color channels for Cairo, each channel in the range [0, 1] */
|
||||
double red, green, blue, alpha;
|
||||
} color_t;
|
||||
|
||||
/**
|
||||
* @brief 解析 16 进制表示的颜色
|
||||
*
|
||||
* @param hex 颜色的 16 进制表示,支持的格式有
|
||||
* RGB/RGBA/RRGGBB/RRGGBBAA/#RGB/#RGBA/#RRGGBB/#RRGGBBAA
|
||||
* @param color 存储颜色值的结构体指针,不能为 nullptr
|
||||
*/
|
||||
void color_parse(const char *hex, color_t *const color);
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define fatal(format, ...) \
|
||||
_fatal(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
|
||||
void _fatal(
|
||||
int line,
|
||||
const char *function,
|
||||
const char *file,
|
||||
const char *format,
|
||||
...
|
||||
) __attribute__((noreturn)) __attribute__((format(printf, 4, 5)));
|
||||
|
||||
#define warn(format, ...) \
|
||||
_warn(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
|
||||
void _warn(
|
||||
int line,
|
||||
const char *function,
|
||||
const char *file,
|
||||
const char *format,
|
||||
...
|
||||
) __attribute__((format(printf, 4, 5)));
|
||||
|
||||
const char *current_time_str(void);
|
||||
|
||||
#if defined(RELEASE)
|
||||
#define logger(format, ...)
|
||||
#elif defined(LOG_FILE)
|
||||
static inline void logger(const char *format, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, format);
|
||||
FILE *log_file = fopen(LOG_FILE, "a+t");
|
||||
vfprintf(log_file, format, ap);
|
||||
va_end(ap);
|
||||
fclose(log_file);
|
||||
}
|
||||
#else
|
||||
#define logger(format, ...) printf(format, ##__VA_ARGS__)
|
||||
#endif
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define ssizeof(foo) (ssize_t)sizeof(foo)
|
||||
#define countof(foo) (ssizeof(foo) / ssizeof(foo[0]))
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define likely(expr) __builtin_expect(!!(expr), 1)
|
||||
#define unlikely(expr) __builtin_expect((expr), 0)
|
||||
#else
|
||||
#define likely(expr) expr
|
||||
#define unlikely(expr) expr
|
||||
#endif
|
||||
|
||||
#ifdef MIN
|
||||
#undef MIN
|
||||
#endif
|
||||
#ifndef MIN
|
||||
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#ifdef MAX
|
||||
#undef MAX
|
||||
#endif
|
||||
#ifndef MAX
|
||||
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
|
||||
#endif
|
||||
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define p_alloc_nr(x) (((x) + 16) * 3 / 2)
|
||||
#define p_new(type, count) ((type *)xmalloc(sizeof(type) * (count)))
|
||||
#define p_clear(p, count) ((void)memset((p), 0, sizeof(*(p)) * (count)))
|
||||
#define p_realloc(pp, count) xrealloc((void *)(pp), sizeof(**(pp)) * (count))
|
||||
#define p_copy(src, count) xmemcopy((src), sizeof(*(src)) * (count))
|
||||
|
||||
#define p_delete(mem_p) \
|
||||
do { \
|
||||
void **__ptr = (void **)(mem_p); \
|
||||
free(*__ptr); \
|
||||
*(void **)__ptr = nullptr; \
|
||||
} while (0)
|
||||
|
||||
#define p_take(dst_p, src_p) \
|
||||
do { \
|
||||
p_delete(dst_p); \
|
||||
*(dst_p) = *(src_p); \
|
||||
*(src_p) = nullptr; \
|
||||
} while (0)
|
||||
|
||||
static inline char *p_strdup(const char *text) {
|
||||
char *r = strdup(text);
|
||||
if (!r) abort();
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline char *p_strndup(const char *text, size_t n) {
|
||||
char *r = strndup(text, n);
|
||||
if (!r) abort();
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline char *p_strdup_nullable(const char *text) {
|
||||
if (!text) return nullptr;
|
||||
return p_strdup(text);
|
||||
}
|
||||
|
||||
static inline void *__attribute__((malloc)) xmalloc(ssize_t size) {
|
||||
void *ptr;
|
||||
|
||||
if (size <= 0) return nullptr;
|
||||
|
||||
ptr = calloc(1, size);
|
||||
|
||||
if (!ptr) abort();
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static inline void *xmemcopy(const void *src, size_t n) {
|
||||
if (!n) return nullptr;
|
||||
|
||||
void *mem = xmalloc(n);
|
||||
void *ret = memcpy(mem, src, n);
|
||||
if (ret != mem) abort();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline void xrealloc(void **ptr, ssize_t newsize) {
|
||||
if (newsize <= 0) p_delete(ptr);
|
||||
else {
|
||||
*ptr = realloc(*ptr, newsize);
|
||||
if (!*ptr) abort();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Unlike strlen(), a_strlen() accepts nullptr and returns 0 in that case.
|
||||
*/
|
||||
static inline ssize_t a_strlen(const char *s) { return s ? strlen(s) : 0; }
|
||||
@@ -1,23 +0,0 @@
|
||||
#include "base/process.h"
|
||||
|
||||
#include <paths.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "base/log.h"
|
||||
|
||||
void spawn(const char *command) {
|
||||
if (fork() == 0) {
|
||||
setsid();
|
||||
|
||||
if (fork() == 0) {
|
||||
execl(_PATH_BSHELL, _PATH_BSHELL, "-c", command, nullptr);
|
||||
fatal("execl fail: %s", command);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
wait(0);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void spawn(const char *command);
|
||||
@@ -1,54 +0,0 @@
|
||||
#include "base/time.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "base/log.h"
|
||||
|
||||
static int time_create_monotonic_timerfd(const struct itimerspec *itimerspec) {
|
||||
auto timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
|
||||
if (timerfd == -1) {
|
||||
auto err = errno;
|
||||
warn("time_create_timerfd failed: %s", strerror(err));
|
||||
return timerfd;
|
||||
}
|
||||
|
||||
timerfd_settime(timerfd, 0, itimerspec, nullptr);
|
||||
return timerfd;
|
||||
}
|
||||
|
||||
static constexpr auto NANOSECONDS_OF_ONE_SECOND = 1'000'000'000ULL;
|
||||
|
||||
int time_create_monotonic_timerfd_by_fps(uint32_t fps) {
|
||||
if (fps == 0) fps = 30;
|
||||
|
||||
auto interval_ns = NANOSECONDS_OF_ONE_SECOND / fps;
|
||||
|
||||
/* clang-format off */
|
||||
struct itimerspec timer_spec = {
|
||||
.it_value = {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = interval_ns % NANOSECONDS_OF_ONE_SECOND,
|
||||
},
|
||||
.it_interval = {
|
||||
.tv_sec = interval_ns / NANOSECONDS_OF_ONE_SECOND,
|
||||
.tv_nsec = interval_ns % NANOSECONDS_OF_ONE_SECOND,
|
||||
},
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
return time_create_monotonic_timerfd(&timer_spec);
|
||||
}
|
||||
|
||||
static uint64_t time_monotonic_ns(void) {
|
||||
struct timespec timestamp = {0};
|
||||
if (clock_gettime(CLOCK_MONOTONIC, ×tamp) == -1) {
|
||||
warn("clock_gettime failed: %s", strerror(errno));
|
||||
}
|
||||
return timestamp.tv_sec * NANOSECONDS_OF_ONE_SECOND + timestamp.tv_nsec;
|
||||
}
|
||||
|
||||
uint64_t time_monotonic_ms(void) { return time_monotonic_ns() / 1'000'000U; }
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int time_create_monotonic_timerfd_by_fps(uint32_t fps);
|
||||
uint64_t time_monotonic_ms(void);
|
||||
37
src/buffer.c
Normal file
37
src/buffer.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "buffer.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sysexits.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
char buffer_slop[1];
|
||||
|
||||
void buffer_ensure(buffer_t *buf, int newlen) {
|
||||
if (newlen < 0) exit(EX_SOFTWARE);
|
||||
|
||||
if (newlen < buf->size) return;
|
||||
|
||||
if (newlen < buf->offs + buf->size && buf->offs > buf->size / 4) {
|
||||
/* Data fits in the current area, shift it left */
|
||||
memmove(buf->s - buf->offs, buf->s, buf->len + 1);
|
||||
buf->s -= buf->offs;
|
||||
buf->size += buf->offs;
|
||||
buf->offs = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
buf->size = p_alloc_nr(buf->size + buf->offs);
|
||||
if (buf->size < newlen + 1) buf->size = newlen + 1;
|
||||
if (buf->alloced && !buf->offs)
|
||||
p_realloc(&buf->s, buf->size);
|
||||
else {
|
||||
char *new_area = xmalloc(buf->size);
|
||||
memcpy(new_area, buf->s, buf->len + 1);
|
||||
if (buf->alloced) free(buf->s - buf->offs);
|
||||
buf->alloced = true;
|
||||
buf->s = new_area;
|
||||
buf->offs = 0;
|
||||
}
|
||||
}
|
||||
78
src/buffer.h
Normal file
78
src/buffer.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
typedef struct buffer_t {
|
||||
char *s;
|
||||
int len, size;
|
||||
unsigned alloced : 1;
|
||||
unsigned offs : 31;
|
||||
} buffer_t;
|
||||
|
||||
extern char buffer_slop[1];
|
||||
|
||||
#define BUFFER_INIT (buffer_t){.s = buffer_slop, .size = 1}
|
||||
|
||||
/**
|
||||
* Initialize a buffer.
|
||||
* @param buf A buffer pointer.
|
||||
* @return The same buffer pointer.
|
||||
*/
|
||||
static inline buffer_t *buffer_init(buffer_t *buf) {
|
||||
*buf = BUFFER_INIT;
|
||||
return buf;
|
||||
}
|
||||
|
||||
void buffer_ensure(buffer_t *buf, int len);
|
||||
|
||||
/**
|
||||
* Add data in the buffer.
|
||||
* @param buf Buffer where to add.
|
||||
* @param pos Position where to add.
|
||||
* @param len Length.
|
||||
* @param data Data to add.
|
||||
* @param dlen Data length.
|
||||
*/
|
||||
static inline void buffer_splice(buffer_t *buf, int pos, int len,
|
||||
const void *data, int dlen) {
|
||||
assert(pos >= 0 && len >= 0 && dlen >= 0);
|
||||
|
||||
if (unlikely(pos > buf->len)) pos = buf->len;
|
||||
if (unlikely(len > buf->len - pos)) len = buf->len - pos;
|
||||
if (pos == 0 && len + buf->offs >= dlen) {
|
||||
buf->offs += len - dlen;
|
||||
buf->s += len - dlen;
|
||||
buf->size -= len - dlen;
|
||||
buf->len -= len - dlen;
|
||||
} else if (len != dlen) {
|
||||
buffer_ensure(buf, buf->len + dlen - len);
|
||||
memmove(buf->s + pos + dlen, buf->s + pos + len, buf->len - pos - len);
|
||||
buf->len += dlen - len;
|
||||
buf->s[buf->len] = '\0';
|
||||
}
|
||||
memcpy(buf->s + pos, data, dlen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data at the end of buffer.
|
||||
* @param buf Buffer where to add.
|
||||
* @param data Data to add.
|
||||
* @param len Data length.
|
||||
*/
|
||||
static inline void buffer_add(buffer_t *buf, const void *data, int len) {
|
||||
buffer_splice(buf, buf->len, 0, data, len);
|
||||
}
|
||||
|
||||
#define buffer_addsl(buf, data) buffer_add(buf, data, sizeof(data) - 1);
|
||||
|
||||
/**
|
||||
* Add a string to the and of a buffer.
|
||||
* @param buf The buffer where to add.
|
||||
* @param s The string to add.
|
||||
*/
|
||||
static inline void buffer_adds(buffer_t *buf, const char *s) {
|
||||
buffer_splice(buf, buf->len, 0, s, a_strlen(s));
|
||||
}
|
||||
706
src/client.c
Normal file
706
src/client.c
Normal file
@@ -0,0 +1,706 @@
|
||||
#include "client.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xcb_icccm.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
#include "atoms-extern.h"
|
||||
#include "base.h"
|
||||
#include "monitor.h"
|
||||
#include "rect.h"
|
||||
#include "types.h"
|
||||
#include "utils.h"
|
||||
#include "wm.h"
|
||||
#include "xwindow.h"
|
||||
|
||||
client_t *client_get_by_window(xcb_window_t window) {
|
||||
for (client_t *c = wm.client_list; c; c = c->next) {
|
||||
if (c->window == window) return c;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
client_t *client_get_next_by_class(client_t *current, const char *class) {
|
||||
client_t *start = current ? current->next : wm.client_list;
|
||||
for (client_t *c = start; c; c = c->next) {
|
||||
if (strcmp(class, c->class) == 0) return c;
|
||||
}
|
||||
|
||||
for (client_t *c = wm.client_list; c && c != start; c = c->next) {
|
||||
if (strcmp(class, c->class) == 0) return c;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void client_attach_list(client_t *client) {
|
||||
client->next = wm.client_list;
|
||||
wm.client_list = client;
|
||||
}
|
||||
|
||||
static void client_detach_list(client_t *client) {
|
||||
client_t **tc = nullptr;
|
||||
for (tc = &wm.client_list; *tc && *tc != client; tc = &(*tc)->next);
|
||||
*tc = client->next;
|
||||
}
|
||||
|
||||
static void client_attach_stack(client_t *client) {
|
||||
client->stack_next = wm.client_stack_list;
|
||||
wm.client_stack_list = client;
|
||||
}
|
||||
|
||||
static void client_detach_stack(client_t *client) {
|
||||
client_t **tc = &wm.client_stack_list;
|
||||
for (; *tc && *tc != client; tc = &(*tc)->stack_next);
|
||||
*tc = client->stack_next;
|
||||
|
||||
if (client == wm.client_focused) {
|
||||
client_t *t = wm.client_stack_list;
|
||||
for (; t && !client_is_visible(t); t = t->stack_next);
|
||||
wm.client_focused = t;
|
||||
}
|
||||
}
|
||||
|
||||
task_in_tag_t *client_get_task_in_tag(client_t *client, tag_t *tag) {
|
||||
for (task_in_tag_t *task = tag->task_list; task; task = task->next) {
|
||||
if (task->client == client) return task;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
task_in_tag_t *client_get_next_task_in_tag(client_t *client, tag_t *tag) {
|
||||
if (!tag->task_list) return nullptr;
|
||||
|
||||
task_in_tag_t *task = tag->task_list;
|
||||
for (; task && task->client != client; task = task->next);
|
||||
return task && task->next ? task->next : tag->task_list;
|
||||
}
|
||||
|
||||
task_in_tag_t *client_get_previous_task_in_tag(client_t *client, tag_t *tag) {
|
||||
if (!tag->task_list) return nullptr;
|
||||
|
||||
task_in_tag_t *task = tag->task_list;
|
||||
for (; task && task->next && task->next->client != client; task = task->next);
|
||||
return task ? task : tag->task_list;
|
||||
}
|
||||
|
||||
static void client_add_to_tag(client_t *client, tag_t *tag) {
|
||||
if (client_get_task_in_tag(client, tag)) return;
|
||||
|
||||
xwindow_set_wm_desktop(client->window, tag->index);
|
||||
|
||||
task_in_tag_t *task = p_new(task_in_tag_t, 1);
|
||||
task->client = client;
|
||||
task->next = tag->task_list;
|
||||
tag->task_list = task;
|
||||
task->geometry = client->geometry;
|
||||
}
|
||||
|
||||
static void client_remove_from_tag(client_t *client, tag_t *tag) {
|
||||
task_in_tag_t **task = &tag->task_list;
|
||||
while (*task) {
|
||||
if ((*task)->client == client) {
|
||||
task_in_tag_t *t = *task;
|
||||
*task = (*task)->next;
|
||||
p_delete(&t);
|
||||
continue;
|
||||
}
|
||||
|
||||
task = &(*task)->next;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void client_wipe(client_t *client) {
|
||||
p_delete(&client->class);
|
||||
p_delete(&client->instance);
|
||||
p_delete(&client->name);
|
||||
p_delete(&client->net_name);
|
||||
p_delete(&client->role);
|
||||
p_delete(&client);
|
||||
}
|
||||
|
||||
static inline void client_tags_apply(client_t *client) {
|
||||
for (tag_t *tag = client->monitor->tag_list; tag; tag = tag->next) {
|
||||
if (client->tags & tag->mask) {
|
||||
client_add_to_tag(client, tag);
|
||||
} else {
|
||||
client_remove_from_tag(client, tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (client->tags == 0) client_wipe(client);
|
||||
}
|
||||
|
||||
static inline void client_update_names(client_t *c) {
|
||||
xwindow_get_text_property(c->window, WM_NAME, &c->name);
|
||||
xwindow_get_text_property(c->window, _NET_WM_NAME, &c->net_name);
|
||||
}
|
||||
|
||||
static inline void client_init_geometry(client_t *c) {
|
||||
if (c->maximize || c->fullscreen || c->minimize) return;
|
||||
|
||||
area_t workarea = c->monitor->workarea;
|
||||
if (c->geometry.x + client_width(c) > workarea.x + workarea.width) {
|
||||
c->geometry.x = workarea.x + workarea.width - client_width(c);
|
||||
}
|
||||
if (c->geometry.y + client_height(c) > workarea.y + workarea.height) {
|
||||
c->geometry.y = workarea.y + workarea.height - client_height(c);
|
||||
}
|
||||
c->geometry.x = MAX(c->geometry.x, workarea.x);
|
||||
c->geometry.y = MAX(c->geometry.y, workarea.y);
|
||||
|
||||
client_move_to(c, c->geometry.x, c->geometry.y);
|
||||
client_resize(c, c->geometry.width, c->geometry.height);
|
||||
client_change_border_color(c, &wm.color_set.active_border_color);
|
||||
client_change_border_width(c, wm.border_width);
|
||||
}
|
||||
|
||||
static void client_init_tag_by_wm_desktop(client_t *client, uint32_t tag_mask) {
|
||||
uint32_t tag_index = 0;
|
||||
if (!xwindow_get_wm_desktop(client->window, &tag_index)) {
|
||||
goto tag_fallback;
|
||||
}
|
||||
|
||||
for (monitor_t *m = wm.monitor_list; m; m = m->next) {
|
||||
for (tag_t *tag = m->tag_list; tag; tag = tag->next) {
|
||||
if (tag->index == tag_index) {
|
||||
client->monitor = m;
|
||||
client->tags = tag->mask;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag_fallback:
|
||||
client->tags = tag_mask;
|
||||
}
|
||||
|
||||
static void set_window_event_mask(xcb_window_t window, bool clean) {
|
||||
xcb_cw_t change_mask = XCB_CW_EVENT_MASK;
|
||||
uint32_t init_event_mask =
|
||||
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_FOCUS_CHANGE |
|
||||
XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_STRUCTURE_NOTIFY |
|
||||
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY;
|
||||
uint32_t event_mask = clean ? XCB_EVENT_MASK_NO_EVENT : init_event_mask;
|
||||
xcb_params_cw_t params = {.event_mask = event_mask};
|
||||
xcb_aux_change_window_attributes(wm.xcb_conn, window, change_mask, ¶ms);
|
||||
}
|
||||
|
||||
void client_manage(xcb_window_t window,
|
||||
xcb_get_geometry_reply_t *geometry_reply) {
|
||||
client_t *c = p_new(client_t, 1);
|
||||
c->window = window;
|
||||
c->old_geometry.x = geometry_reply->x;
|
||||
c->old_geometry.y = geometry_reply->y;
|
||||
c->old_geometry.width = geometry_reply->width;
|
||||
c->old_geometry.height = geometry_reply->height;
|
||||
c->geometry = c->old_geometry;
|
||||
c->old_border_width = geometry_reply->border_width;
|
||||
|
||||
c->minimize = xwindow_get_state(window) == XCB_ICCCM_WM_STATE_ICONIC;
|
||||
|
||||
set_window_event_mask(window, false);
|
||||
|
||||
client_update_names(c);
|
||||
xwindow_get_text_property(c->window, WM_WINDOW_ROLE, &c->role);
|
||||
client_update_wm_hints(c);
|
||||
|
||||
{
|
||||
xcb_icccm_get_wm_class_reply_t prop;
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_class_unchecked(wm.xcb_conn, c->window);
|
||||
if (xcb_icccm_get_wm_class_reply(wm.xcb_conn, cookie, &prop, nullptr)) {
|
||||
c->class = strdup(prop.class_name);
|
||||
c->instance = strdup(prop.instance_name);
|
||||
xcb_icccm_get_wm_class_reply_wipe(&prop);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_transient_for_unchecked(wm.xcb_conn, window);
|
||||
xcb_icccm_get_wm_transient_for_reply(wm.xcb_conn, cookie,
|
||||
&c->transient_for_window, nullptr);
|
||||
|
||||
client_t *transient_for_client = nullptr;
|
||||
if (c->transient_for_window && (transient_for_client = client_get_by_window(
|
||||
c->transient_for_window))) {
|
||||
c->monitor = transient_for_client->monitor;
|
||||
client_init_tag_by_wm_desktop(c, transient_for_client->tags);
|
||||
} else {
|
||||
c->monitor = wm.current_monitor;
|
||||
client_init_tag_by_wm_desktop(c, c->monitor->selected_tag->mask);
|
||||
client_apply_rules(c, wm.rules, wm.rules_count);
|
||||
}
|
||||
|
||||
client_init_geometry(c);
|
||||
client_tags_apply(c);
|
||||
}
|
||||
|
||||
client_attach_list(c);
|
||||
client_attach_stack(c);
|
||||
client_update_window_type(c);
|
||||
client_update_size_hints(c);
|
||||
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_APPEND, wm.screen->root,
|
||||
_NET_CLIENT_LIST, XCB_ATOM_WINDOW, 32, 1, &window);
|
||||
|
||||
if (c->minimize) {
|
||||
xwindow_set_state(window, XCB_ICCCM_WM_STATE_ICONIC);
|
||||
} else {
|
||||
xwindow_set_state(window, XCB_ICCCM_WM_STATE_NORMAL);
|
||||
xcb_map_window(wm.xcb_conn, window);
|
||||
}
|
||||
|
||||
monitor_arrange(c->monitor);
|
||||
monitor_draw_bar(c->monitor);
|
||||
|
||||
wm_restack_clients();
|
||||
if (c->tags & c->monitor->selected_tag->mask) client_focus(c);
|
||||
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
char **client_get_task_title(client_t *client) {
|
||||
if (client->net_name) return &client->net_name;
|
||||
if (client->name) return &client->name;
|
||||
if (client->instance) return &client->instance;
|
||||
if (client->class) return &client->class;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool client_need_layout(client_t *client) {
|
||||
if (client->floating || client->fullscreen || client->maximize ||
|
||||
client->minimize || client->size_freeze) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void client_send_to_tag(client_t *client, uint32_t tag_mask) {
|
||||
if (client->tags == tag_mask) return;
|
||||
client->tags = tag_mask;
|
||||
client_tags_apply(client);
|
||||
monitor_arrange(client->monitor);
|
||||
monitor_draw_bar(client->monitor);
|
||||
monitor_deal_focus(client->monitor);
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
void client_send_to_monitor(client_t *client, monitor_t *monitor) {
|
||||
if (client->monitor == monitor) return;
|
||||
|
||||
for (tag_t *tag = client->monitor->tag_list; tag; tag = tag->next) {
|
||||
client_remove_from_tag(client, tag);
|
||||
}
|
||||
|
||||
monitor_t *m = client->monitor;
|
||||
|
||||
client->monitor = monitor;
|
||||
client->tags = monitor->selected_tag->mask;
|
||||
client_add_to_tag(client, monitor->selected_tag);
|
||||
|
||||
wm_set_current_monitor(monitor, true);
|
||||
monitor_arrange(m);
|
||||
monitor_arrange(monitor);
|
||||
monitor_draw_bar(m);
|
||||
monitor_draw_bar(monitor);
|
||||
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
void client_move_to(client_t *client, int16_t x, int16_t y) {
|
||||
client->geometry.x = x;
|
||||
client->geometry.y = y;
|
||||
uint16_t mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y;
|
||||
const xcb_params_configure_window_t params = {.x = x, .y = y};
|
||||
xcb_aux_configure_window(wm.xcb_conn, client->window, mask, ¶ms);
|
||||
logger("== client 0x%x move to x: %d, y: %d\n", client->window, x, y);
|
||||
}
|
||||
|
||||
void client_resize(client_t *client, uint16_t width, uint16_t height) {
|
||||
client->geometry.width = width;
|
||||
client->geometry.height = height;
|
||||
uint16_t mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
|
||||
const xcb_params_configure_window_t params = {
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
xcb_aux_configure_window(wm.xcb_conn, client->window, mask, ¶ms);
|
||||
logger("== client 0x%x resize to width: %d, height: %d\n", client->window,
|
||||
width, height);
|
||||
}
|
||||
|
||||
void client_change_border_color(client_t *client, color_t *color) {
|
||||
uint16_t mask = XCB_CW_BORDER_PIXEL;
|
||||
const xcb_params_cw_t params = {.border_pixel = color->argb};
|
||||
xcb_aux_change_window_attributes(wm.xcb_conn, client->window, mask, ¶ms);
|
||||
client->border_color = color;
|
||||
logger("== client 0x%x border color: RGBA(#%08x), ARGB(#%08x)\n",
|
||||
client->window, color->rgba, color->argb);
|
||||
}
|
||||
|
||||
void client_change_border_width(client_t *client, uint16_t border_width) {
|
||||
if (client->border_width == border_width) return;
|
||||
uint16_t mask = XCB_CONFIG_WINDOW_BORDER_WIDTH;
|
||||
const xcb_params_configure_window_t params = {.border_width = border_width};
|
||||
xcb_aux_configure_window(wm.xcb_conn, client->window, mask, ¶ms);
|
||||
client->border_width = border_width;
|
||||
logger("== client 0x%x border width: %u\n", client->window, border_width);
|
||||
}
|
||||
|
||||
void client_update_window_type(client_t *client) {
|
||||
xcb_window_t window = client->window;
|
||||
xcb_get_property_cookie_t state_cookie = xcb_get_property_unchecked(
|
||||
wm.xcb_conn, false, window, _NET_WM_STATE, XCB_ATOM_ATOM, 0, 1);
|
||||
xcb_get_property_cookie_t window_type_cookie = xcb_get_property_unchecked(
|
||||
wm.xcb_conn, false, window, _NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 0, 1);
|
||||
xcb_get_property_reply_t *state_reply =
|
||||
xcb_get_property_reply(wm.xcb_conn, state_cookie, nullptr);
|
||||
xcb_get_property_reply_t *window_type_reply =
|
||||
xcb_get_property_reply(wm.xcb_conn, window_type_cookie, nullptr);
|
||||
|
||||
if (state_reply) {
|
||||
xcb_atom_t state = *(xcb_atom_t *)xcb_get_property_value(state_reply);
|
||||
client->skip_taskbar = state == _NET_WM_STATE_SKIP_TASKBAR;
|
||||
if (state == _NET_WM_STATE_FULLSCREEN) {
|
||||
client_set_fullscreen(client, true);
|
||||
}
|
||||
p_delete(&state_reply);
|
||||
}
|
||||
|
||||
if (window_type_reply) {
|
||||
xcb_atom_t window_type =
|
||||
*(xcb_atom_t *)xcb_get_property_value(window_type_reply);
|
||||
if (window_type == _NET_WM_WINDOW_TYPE_DIALOG) {
|
||||
client_set_floating(client, true);
|
||||
}
|
||||
p_delete(&window_type_reply);
|
||||
}
|
||||
}
|
||||
|
||||
void client_update_wm_hints(client_t *client) {
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_hints(wm.xcb_conn, client->window);
|
||||
xcb_icccm_wm_hints_t hints;
|
||||
if (xcb_icccm_get_wm_hints_reply(wm.xcb_conn, cookie, &hints, nullptr)) {
|
||||
if (client == wm.client_focused &&
|
||||
(hints.flags & XCB_ICCCM_WM_HINT_X_URGENCY)) {
|
||||
hints.flags &= ~XCB_ICCCM_WM_HINT_X_URGENCY;
|
||||
xcb_icccm_set_wm_hints(wm.xcb_conn, client->window, &hints);
|
||||
} else {
|
||||
client->urgent = hints.flags & XCB_ICCCM_WM_HINT_X_URGENCY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void client_update_size_hints(client_t *client) {
|
||||
xcb_connection_t *conn = wm.xcb_conn;
|
||||
xcb_size_hints_t hints;
|
||||
xcb_get_property_cookie_t cookie =
|
||||
xcb_icccm_get_wm_normal_hints_unchecked(conn, client->window);
|
||||
if (!xcb_icccm_get_wm_normal_hints_reply(conn, cookie, &hints, nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t min_width = 0, min_height = 0, max_width = 0, max_height = 0;
|
||||
if (hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) {
|
||||
min_width = hints.min_width;
|
||||
min_height = hints.min_height;
|
||||
}
|
||||
if (hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE) {
|
||||
max_width = hints.max_width;
|
||||
max_height = hints.max_height;
|
||||
}
|
||||
if (min_width == max_width && min_height == max_height) {
|
||||
client->size_freeze = true;
|
||||
client_set_floating(client, true);
|
||||
}
|
||||
}
|
||||
|
||||
void client_set_floating(client_t *client, bool floating) {
|
||||
if (client->floating == floating || client->fullscreen) return;
|
||||
|
||||
logger("== client set floating: %s\n", floating ? "true" : "false");
|
||||
logger("== old geometry: x -> %d, y -> %d, width -> %u, height -> %u\n",
|
||||
client->old_geometry.x, client->old_geometry.y,
|
||||
client->old_geometry.width, client->old_geometry.height);
|
||||
client->floating = floating;
|
||||
if (floating) {
|
||||
client_apply_workarea_geometry(client, client->old_geometry);
|
||||
client->old_geometry = client->geometry;
|
||||
}
|
||||
|
||||
monitor_arrange(client->monitor);
|
||||
}
|
||||
|
||||
void client_set_fullscreen(client_t *client, bool fullscreen) {
|
||||
if (client->fullscreen == fullscreen) return;
|
||||
|
||||
client->fullscreen = fullscreen;
|
||||
if (fullscreen) {
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, 1,
|
||||
&_NET_WM_STATE_FULLSCREEN);
|
||||
client->old_border_width = client->border_width;
|
||||
if (client->floating) client->old_geometry = client->geometry;
|
||||
client_change_border_width(client, 0);
|
||||
client_apply_geometry(client, client->monitor->geometry);
|
||||
client_stack_raise(client);
|
||||
} else {
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, 0, 0);
|
||||
client_change_border_width(client, client->old_border_width);
|
||||
client_apply_geometry(client, client->old_geometry);
|
||||
}
|
||||
monitor_arrange(client->monitor);
|
||||
client_focus(client);
|
||||
}
|
||||
|
||||
void client_set_maximize(client_t *client, bool maximize) {
|
||||
if (client->maximize == maximize) return;
|
||||
|
||||
client->maximize = maximize;
|
||||
if (maximize) {
|
||||
xcb_atom_t max_atoms[] = {
|
||||
_NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
_NET_WM_STATE_MAXIMIZED_VERT,
|
||||
};
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, countof(max_atoms),
|
||||
max_atoms);
|
||||
client->old_border_width = client->border_width;
|
||||
if (client->floating) client->old_geometry = client->geometry;
|
||||
client_change_border_width(client, 0);
|
||||
client_apply_geometry(client, client->monitor->workarea);
|
||||
client_stack_raise(client);
|
||||
} else {
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, 0, 0);
|
||||
client_change_border_width(client, client->old_border_width);
|
||||
client_apply_geometry(client, client->old_geometry);
|
||||
}
|
||||
monitor_arrange(client->monitor);
|
||||
client_focus(client);
|
||||
}
|
||||
|
||||
void client_set_minimize(client_t *client, bool minimize) {
|
||||
if (client->minimize == minimize) return;
|
||||
|
||||
client->minimize = minimize;
|
||||
if (minimize) {
|
||||
xwindow_set_state(client->window, XCB_ICCCM_WM_STATE_ICONIC);
|
||||
|
||||
xcb_grab_server(wm.xcb_conn);
|
||||
set_window_event_mask(client->window, true);
|
||||
xcb_unmap_window(wm.xcb_conn, client->window);
|
||||
set_window_event_mask(client->window, false);
|
||||
xcb_ungrab_server(wm.xcb_conn);
|
||||
} else {
|
||||
xwindow_set_state(client->window, XCB_ICCCM_WM_STATE_NORMAL);
|
||||
xcb_map_window(wm.xcb_conn, client->window);
|
||||
}
|
||||
monitor_arrange(client->monitor);
|
||||
client_focus(client);
|
||||
}
|
||||
|
||||
static void update_client_list(void) {
|
||||
xcb_connection_t *conn = wm.xcb_conn;
|
||||
xcb_window_t root = wm.screen->root;
|
||||
xcb_atom_t atom = _NET_CLIENT_LIST;
|
||||
uint8_t mode = XCB_PROP_MODE_PREPEND;
|
||||
xcb_atom_t type = XCB_ATOM_WINDOW;
|
||||
|
||||
xcb_delete_property(conn, root, atom);
|
||||
for (client_t *c = wm.client_list; c; c = c->next) {
|
||||
xcb_change_property(conn, mode, root, atom, type, 32, 1, &c->window);
|
||||
}
|
||||
}
|
||||
|
||||
void client_kill(client_t *client) {
|
||||
if (!xwindow_send_event(client->window, WM_DELETE_WINDOW)) {
|
||||
xwindow_kill_window(client->window);
|
||||
}
|
||||
}
|
||||
|
||||
void client_unmanage(client_t *client, bool destroyed) {
|
||||
monitor_t *m = client->monitor;
|
||||
|
||||
client_detach_list(client);
|
||||
client_detach_stack(client);
|
||||
|
||||
for (tag_t *tag = m->tag_list; tag; tag = tag->next) {
|
||||
if (tag->mask & client->tags) client_remove_from_tag(client, tag);
|
||||
}
|
||||
|
||||
if (!destroyed) {
|
||||
set_window_event_mask(client->window, true);
|
||||
client_change_border_width(client, client->old_border_width);
|
||||
xwindow_set_state(client->window, XCB_ICCCM_WM_STATE_WITHDRAWN);
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
client_wipe(client);
|
||||
|
||||
update_client_list();
|
||||
|
||||
wm_restack_clients();
|
||||
monitor_deal_focus(m);
|
||||
monitor_arrange(m);
|
||||
monitor_draw_bar(m);
|
||||
xcb_flush(wm.xcb_conn);
|
||||
}
|
||||
|
||||
void client_apply_geometry(client_t *client, area_t geometry) {
|
||||
uint16_t mask = 0;
|
||||
const xcb_params_configure_window_t params = {
|
||||
.x = geometry.x,
|
||||
.y = geometry.y,
|
||||
.width = geometry.width,
|
||||
.height = geometry.height,
|
||||
};
|
||||
if (client->geometry.x != geometry.x) mask |= XCB_CONFIG_WINDOW_X;
|
||||
if (client->geometry.y != geometry.y) mask |= XCB_CONFIG_WINDOW_Y;
|
||||
if (client->geometry.width != geometry.width) mask |= XCB_CONFIG_WINDOW_WIDTH;
|
||||
if (client->geometry.height != geometry.height) {
|
||||
mask |= XCB_CONFIG_WINDOW_HEIGHT;
|
||||
}
|
||||
|
||||
client->geometry = geometry;
|
||||
xcb_aux_configure_window(wm.xcb_conn, client->window, mask, ¶ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 调整 client 的几何信息但需让 client 任在其 monitor 的 workarea 中
|
||||
* @param client 要调整几何信息的 client
|
||||
* @param geometry 目标几何信息
|
||||
*/
|
||||
void client_apply_workarea_geometry(client_t *client, area_t geometry) {
|
||||
area_t workarea = client->monitor->workarea;
|
||||
uint16_t width = MIN(geometry.width, workarea.width);
|
||||
uint16_t height = MIN(geometry.height, workarea.height);
|
||||
int16_t x = MAX(geometry.x, workarea.x);
|
||||
int16_t y = MAX(geometry.y, workarea.y);
|
||||
if (x + width + client->border_width * 2 > workarea.x + workarea.width) {
|
||||
x = workarea.x + workarea.width - width - client->border_width * 2;
|
||||
}
|
||||
if (y + height + client->border_width * 2 > workarea.y + workarea.height) {
|
||||
y = workarea.y + workarea.height - height - client->border_width * 2;
|
||||
}
|
||||
|
||||
area_t rect = {.x = x, .y = y, .width = width, .height = height};
|
||||
client_apply_geometry(client, rect);
|
||||
}
|
||||
|
||||
void client_apply_rules(client_t *client, const rule_t rules[],
|
||||
size_t rules_count) {
|
||||
for (size_t i = 0; i < rules_count; i++) {
|
||||
const rule_t *r = &rules[i];
|
||||
if (!((!r->role || (client->role && strstr(client->role, r->role))) &&
|
||||
(!r->class || (client->class && strstr(client->class, r->class))))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
client->fullscreen = r->fullscreen;
|
||||
client->maximize = r->maximize;
|
||||
client->floating = r->floating;
|
||||
|
||||
for (monitor_t *m = wm.monitor_list; m; m = m->next) {
|
||||
for (tag_t *t = m->tag_list; t; t = t->next) {
|
||||
if (t->index != r->tag_index) continue;
|
||||
|
||||
client->monitor = m;
|
||||
client->tags = t->mask;
|
||||
|
||||
if (r->switch_to_tag) {
|
||||
wm_set_current_monitor(m, true);
|
||||
m->selected_tag = t;
|
||||
|
||||
logger("++ switch to tag: %u\n", t->index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (client->fullscreen) {
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, 1,
|
||||
&_NET_WM_STATE_FULLSCREEN);
|
||||
} else if (client->maximize) {
|
||||
xcb_atom_t max_atoms[] = {
|
||||
_NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
_NET_WM_STATE_MAXIMIZED_VERT,
|
||||
};
|
||||
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, client->window,
|
||||
_NET_WM_STATE, XCB_ATOM_ATOM, 32, countof(max_atoms),
|
||||
max_atoms);
|
||||
}
|
||||
}
|
||||
|
||||
void client_focus(client_t *client) {
|
||||
wm.client_focused = client;
|
||||
xwindow_focus(client ? client->window : XCB_WINDOW_NONE);
|
||||
}
|
||||
|
||||
void client_stack_raise(client_t *client) {
|
||||
if (wm.client_stack_list == client) return;
|
||||
|
||||
client_detach_stack(client);
|
||||
client_attach_stack(client);
|
||||
wm_restack_clients();
|
||||
}
|
||||
|
||||
bool client_is_visible(client_t *client) {
|
||||
return client->tags & client->monitor->selected_tag->mask;
|
||||
}
|
||||
|
||||
static inline rect_t client_to_rect(client_t *client) {
|
||||
return (rect_t){
|
||||
.x1 = client->geometry.x,
|
||||
.y1 = client->geometry.y,
|
||||
.x2 = client->geometry.x + (int32_t)client_width(client),
|
||||
.y2 = client->geometry.y + (int32_t)client_height(client),
|
||||
};
|
||||
}
|
||||
|
||||
obscured_t client_get_obscured_state(client_t *client, client_t *client_stack) {
|
||||
if (!client || !client_stack) return obscured_none;
|
||||
|
||||
rect_t source = client_to_rect(client);
|
||||
size_t clip_count = 0;
|
||||
size_t clip_capacity = 0;
|
||||
rect_t *clips = nullptr;
|
||||
|
||||
bool has_overlap = false;
|
||||
for (client_t *c = client_stack; c; c = c->stack_next) {
|
||||
if (c == client) break;
|
||||
if (c->minimize || !client_is_visible(c)) continue;
|
||||
|
||||
rect_t clip = client_to_rect(c);
|
||||
if (!rect_intersection(source, clip, nullptr)) continue;
|
||||
|
||||
has_overlap = true;
|
||||
if (clip_count == clip_capacity) {
|
||||
clip_capacity = clip_capacity ? clip_capacity * 2 : 4;
|
||||
p_realloc(&clips, clip_capacity);
|
||||
}
|
||||
clips[clip_count++] = clip;
|
||||
}
|
||||
|
||||
if (!has_overlap) {
|
||||
p_delete(&clips);
|
||||
return obscured_none;
|
||||
}
|
||||
|
||||
rect_t *remaining = nullptr;
|
||||
size_t remaining_count =
|
||||
rect_subtract_many(source, clips, clip_count, &remaining);
|
||||
|
||||
p_delete(&clips);
|
||||
p_delete(&remaining);
|
||||
return remaining_count == 0 ? obscured_fully : obscured_partially;
|
||||
}
|
||||
76
src/client.h
Normal file
76
src/client.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "color.h"
|
||||
#include "types.h"
|
||||
|
||||
client_t *client_get_by_window(xcb_window_t window);
|
||||
client_t *client_get_next_by_class(client_t *current, const char *class);
|
||||
task_in_tag_t *client_get_task_in_tag(client_t *client, tag_t *tag);
|
||||
task_in_tag_t *client_get_next_task_in_tag(client_t *client, tag_t *tag);
|
||||
task_in_tag_t *client_get_previous_task_in_tag(client_t *client, tag_t *tag);
|
||||
void client_manage(xcb_window_t window,
|
||||
xcb_get_geometry_reply_t *geometry_reply);
|
||||
char **client_get_task_title(client_t *client);
|
||||
bool client_need_layout(client_t *client);
|
||||
|
||||
void client_send_to_tag(client_t *client, uint32_t tag_mask);
|
||||
void client_send_to_monitor(client_t *client, monitor_t *monitor);
|
||||
|
||||
void client_move_to(client_t *client, int16_t x, int16_t y);
|
||||
void client_resize(client_t *client, uint16_t width, uint16_t height);
|
||||
void client_change_border_color(client_t *client, color_t *color);
|
||||
void client_change_border_width(client_t *client, uint16_t border_width);
|
||||
|
||||
void client_update_window_type(client_t *client);
|
||||
void client_update_wm_hints(client_t *client);
|
||||
void client_update_size_hints(client_t *client);
|
||||
|
||||
void client_set_floating(client_t *client, bool floating);
|
||||
void client_set_fullscreen(client_t *client, bool fullscreen);
|
||||
void client_set_maximize(client_t *client, bool maximize);
|
||||
void client_set_minimize(client_t *client, bool minimize);
|
||||
void client_set_sticky(client_t *client, bool sticky);
|
||||
void client_set_urgent(client_t *client, bool urgent);
|
||||
|
||||
void client_set_class_instance(client_t *client, const char *class,
|
||||
const char *instance);
|
||||
|
||||
void client_set_name(client_t *client, char *name);
|
||||
void client_set_icon_name(client_t *client, char *icon_name);
|
||||
void client_set_net_name(client_t *client, char *name);
|
||||
void client_set_net_icon_name(client_t *client, char *icon_name);
|
||||
|
||||
void client_set_role(client_t *client, char *role);
|
||||
|
||||
void client_set_transient_for_window(client_t *client,
|
||||
xcb_window_t transient_for_window);
|
||||
void client_set_leader_window(client_t *client, xcb_window_t leader_window);
|
||||
|
||||
void client_kill(client_t *client);
|
||||
void client_unmanage(client_t *client, bool destroyed);
|
||||
void client_apply_geometry(client_t *client, area_t geometry);
|
||||
void client_apply_workarea_geometry(client_t *client, area_t geometry);
|
||||
void client_apply_rules(client_t *client, const rule_t rules[],
|
||||
size_t rules_count);
|
||||
|
||||
void client_focus(client_t *client);
|
||||
void client_stack_raise(client_t *client);
|
||||
|
||||
bool client_is_visible(client_t *client);
|
||||
|
||||
typedef enum obscured_t {
|
||||
obscured_none,
|
||||
obscured_partially,
|
||||
obscured_fully,
|
||||
} obscured_t;
|
||||
obscured_t client_get_obscured_state(client_t *client, client_t *client_stack);
|
||||
|
||||
static inline uint16_t client_width(client_t *c) {
|
||||
return c->geometry.width + c->border_width * 2;
|
||||
}
|
||||
static inline uint16_t client_height(client_t *c) {
|
||||
return c->geometry.height + c->border_width * 2;
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
#include "base/color.h"
|
||||
#include "color.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "base/log.h"
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
* @brief 将 16 进制的颜色配置转换成 uint32 格式的 rgba数据
|
||||
@@ -29,26 +24,16 @@ static uint32_t rgba_to_argb(uint32_t rgba);
|
||||
* @param blue blue channel, range 0~1
|
||||
* @param alpha alpha channel, range 0~1
|
||||
*/
|
||||
static void extract_color_channel(
|
||||
const uint32_t rgba,
|
||||
double *red,
|
||||
double *green,
|
||||
double *blue,
|
||||
double *alpha
|
||||
);
|
||||
static void extract_color_channel(const uint32_t rgba, double *red,
|
||||
double *green, double *blue, double *alpha);
|
||||
|
||||
void color_parse(const char *hex, color_t *const color) {
|
||||
bool success = hex_color_to_rgba(hex, &color->rgba);
|
||||
if (!success) fatal("invalid hex color: %s", hex);
|
||||
|
||||
color->argb = rgba_to_argb(color->rgba);
|
||||
extract_color_channel(
|
||||
color->rgba,
|
||||
&color->red,
|
||||
&color->green,
|
||||
&color->blue,
|
||||
&color->alpha
|
||||
);
|
||||
extract_color_channel(color->rgba, &color->red, &color->green, &color->blue,
|
||||
&color->alpha);
|
||||
}
|
||||
|
||||
bool hex_color_to_rgba(const char *hex, uint32_t *rgba) {
|
||||
@@ -56,7 +41,7 @@ bool hex_color_to_rgba(const char *hex, uint32_t *rgba) {
|
||||
|
||||
size_t len = strlen(hex);
|
||||
char extended[9] = {0};
|
||||
uint32_t color = 0x00'00'00'00;
|
||||
uint32_t color = 0x00000000;
|
||||
|
||||
if (len == 3 || len == 4) {
|
||||
extended[0] = extended[1] = hex[0];
|
||||
@@ -76,7 +61,7 @@ bool hex_color_to_rgba(const char *hex, uint32_t *rgba) {
|
||||
return false;
|
||||
}
|
||||
|
||||
color = strtoul(extended, nullptr, 16) & 0xff'ff'ff'ff;
|
||||
color = strtoul(extended, nullptr, 16) & 0xffffffff;
|
||||
*rgba = color;
|
||||
return true;
|
||||
}
|
||||
@@ -93,13 +78,8 @@ uint32_t rgba_to_argb(uint32_t rgba) {
|
||||
}
|
||||
|
||||
#define COLOR_SPLIT(C, R) (EXTRACE_COLOR_BIT((C), (R)) / (double)0xff)
|
||||
void extract_color_channel(
|
||||
const uint32_t rgba,
|
||||
double *red,
|
||||
double *green,
|
||||
double *blue,
|
||||
double *alpha
|
||||
) {
|
||||
void extract_color_channel(const uint32_t rgba, double *red, double *green,
|
||||
double *blue, double *alpha) {
|
||||
*red = COLOR_SPLIT(rgba, 24);
|
||||
*green = COLOR_SPLIT(rgba, 16);
|
||||
*blue = COLOR_SPLIT(rgba, 8);
|
||||
22
src/color.h
Normal file
22
src/color.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct color_t {
|
||||
uint32_t rgba; /* use for human reading */
|
||||
uint32_t argb; /* use for xcb */
|
||||
|
||||
/* channels of color, use for cairo */
|
||||
double red; /* red channel */
|
||||
double green; /* green channel */
|
||||
double blue; /* blue channel */
|
||||
double alpha; /* alpha channel */
|
||||
} color_t;
|
||||
|
||||
/**
|
||||
* @brief parsing color represented in hexadecimal format
|
||||
* @param hex color represented in hexadecimal format, the supported format
|
||||
have: RGB/RGBA/RRGGBB/RRGGBBAA/#RGB/#RGBA/#RRGGBB/#RRGGBBAA
|
||||
* @param color return the parsed color
|
||||
*/
|
||||
void color_parse(const char *hex, color_t *const color);
|
||||
@@ -1,27 +0,0 @@
|
||||
#include "interface/event.h"
|
||||
|
||||
#include "base/memory.h"
|
||||
#include "common/window.h"
|
||||
|
||||
void event_cleanup(event_t *event) {
|
||||
if (!event || event->type == ZDWM_EVENT_NONE) return;
|
||||
|
||||
switch (event->type) {
|
||||
case ZDWM_EVENT_WINDOW_MAP_REQUEST:
|
||||
window_layer_props_cleanup(&event->as.window_map_request.props);
|
||||
window_metadata_cleanup(&event->as.window_map_request.metadata);
|
||||
break;
|
||||
case ZDWM_EVENT_WINDOW_METADATA_CHANGED:
|
||||
window_metadata_cleanup(&event->as.window_metadata_change.metadata);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void event_reset(event_t *event) {
|
||||
if (!event) return;
|
||||
|
||||
event_cleanup(event);
|
||||
p_clear(event, 1);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "interface/event.h"
|
||||
|
||||
void event_cleanup(event_t *event);
|
||||
void event_reset(event_t *event);
|
||||
@@ -1,107 +0,0 @@
|
||||
#include "common/listeners.h"
|
||||
|
||||
#include <zdwm/listeners.h>
|
||||
|
||||
#include "base/array.h"
|
||||
#include "base/memory.h"
|
||||
|
||||
#define ADD_LISTENER(FIELD) \
|
||||
auto list = &listeners->FIELD; \
|
||||
auto item = array_push(list->items, list->count, list->capacity); \
|
||||
item->fn = fn; \
|
||||
item->user_data = user_data
|
||||
|
||||
void listeners_add_output_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_current_output_id_listener fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(output_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_initial_workspace_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_initial_workspace_list fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(initial_workspace_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_active_workspace_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_workspace_active_updated fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(active_workspace_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_layout_notify(
|
||||
listeners_t *listeners,
|
||||
zdwm_layout_notify fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(layout_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_binding_mode_notify(
|
||||
listeners_t *listeners,
|
||||
zdwm_binding_mode_notify fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(binding_mode_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_initial_window_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_initial_window_list fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(initial_window_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_window_added_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_added fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(window_added_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_window_updated_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_updated fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(window_updated_listeners);
|
||||
}
|
||||
|
||||
void listeners_add_window_removed_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_removed fn,
|
||||
void *user_data
|
||||
) {
|
||||
ADD_LISTENER(window_removed_listeners);
|
||||
}
|
||||
|
||||
#undef ADD_LISTENER
|
||||
|
||||
#define CLEANUP_LIST(FIELD) \
|
||||
p_delete(&listeners->FIELD.items); \
|
||||
listeners->FIELD.count = 0; \
|
||||
listeners->FIELD.capacity = 0
|
||||
|
||||
void listeners_cleanup(listeners_t *listeners) {
|
||||
if (!listeners) return;
|
||||
|
||||
CLEANUP_LIST(output_listeners);
|
||||
CLEANUP_LIST(initial_workspace_listeners);
|
||||
CLEANUP_LIST(active_workspace_listeners);
|
||||
CLEANUP_LIST(layout_listeners);
|
||||
CLEANUP_LIST(binding_mode_listeners);
|
||||
CLEANUP_LIST(initial_window_listeners);
|
||||
CLEANUP_LIST(window_added_listeners);
|
||||
CLEANUP_LIST(window_updated_listeners);
|
||||
CLEANUP_LIST(window_removed_listeners);
|
||||
}
|
||||
|
||||
#undef CLEANUP_LIST
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <zdwm/listeners.h>
|
||||
|
||||
typedef struct binding_table_t binding_table_t;
|
||||
typedef struct layout_registry_t layout_registry_t;
|
||||
typedef struct state_t state_t;
|
||||
|
||||
#define ITEM(NAME, FN_TYPE) \
|
||||
typedef struct NAME##_listener_item_t { \
|
||||
FN_TYPE *fn; \
|
||||
void *user_data; \
|
||||
} NAME##_listener_item_t
|
||||
|
||||
#define LISTENERS(NAME, FN_TYPE) \
|
||||
ITEM(NAME, FN_TYPE); \
|
||||
typedef struct NAME##_listeners_t { \
|
||||
NAME##_listener_item_t *items; \
|
||||
size_t count; \
|
||||
size_t capacity; \
|
||||
} NAME##_listeners_t
|
||||
|
||||
LISTENERS(output, zdwm_current_output_id_listener);
|
||||
LISTENERS(initial_workspace_list, zdwm_initial_workspace_list);
|
||||
LISTENERS(workspace_active, zdwm_workspace_active_updated);
|
||||
LISTENERS(layout, zdwm_layout_notify);
|
||||
LISTENERS(binding_mode, zdwm_binding_mode_notify);
|
||||
LISTENERS(initial_window_list, zdwm_initial_window_list);
|
||||
LISTENERS(window_added, zdwm_window_added);
|
||||
LISTENERS(window_updated, zdwm_window_updated);
|
||||
LISTENERS(window_removed, zdwm_window_removed);
|
||||
|
||||
#undef ITEM
|
||||
#undef LISTENERS
|
||||
|
||||
typedef struct listeners_t {
|
||||
output_listeners_t output_listeners;
|
||||
initial_workspace_list_listeners_t initial_workspace_listeners;
|
||||
workspace_active_listeners_t active_workspace_listeners;
|
||||
layout_listeners_t layout_listeners;
|
||||
binding_mode_listeners_t binding_mode_listeners;
|
||||
initial_window_list_listeners_t initial_window_listeners;
|
||||
window_added_listeners_t window_added_listeners;
|
||||
window_updated_listeners_t window_updated_listeners;
|
||||
window_removed_listeners_t window_removed_listeners;
|
||||
} listeners_t;
|
||||
|
||||
void listeners_add_output_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_current_output_id_listener fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_initial_workspace_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_initial_workspace_list fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_active_workspace_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_workspace_active_updated fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_layout_notify(
|
||||
listeners_t *listeners,
|
||||
zdwm_layout_notify fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_binding_mode_notify(
|
||||
listeners_t *listeners,
|
||||
zdwm_binding_mode_notify fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_initial_window_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_initial_window_list fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_window_added_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_added fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_window_updated_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_updated fn,
|
||||
void *user_data
|
||||
);
|
||||
void listeners_add_window_removed_listener(
|
||||
listeners_t *listeners,
|
||||
zdwm_window_removed fn,
|
||||
void *user_data
|
||||
);
|
||||
|
||||
void listeners_cleanup(listeners_t *listeners);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user