diff --git a/.clang-format b/.clang-format index 526b64afa..4580b11f5 100644 --- a/.clang-format +++ b/.clang-format @@ -33,3 +33,6 @@ SpacesInParentheses: false SpacesInSquareBrackets: false SortIncludes: false UseTab: Never +InsertNewlineAtEOF: true +DeriveLineEnding: false +UseCRLF: false \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 7acac1ddd..da2a74a89 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@lc-soft.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@lcui.dev. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3e7c08295..8bfd53716 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -74,9 +74,10 @@ To ensure consistency throughout the source code, keep these rules in mind as yo - All features or bug fixes **must be tested** by one or more specs (unit-tests). - All public API methods **must be documented**. (Details TBC). -- Follow the existing code style. You have the following two ways to format the code: - - Use [clang-format](http://clang.llvm.org/docs/ClangFormat.html) to format the changed files: `clang-format --style=file [src/????.c]`. - - Install the [NodeJS](https://nodejs.org/en/) Environment and run `npm install`, which will add a git hook to format the changed code, and it will run before you run `git commit`. +- Follow the existing code style. The project uses [clang-format](http://clang.llvm.org/docs/ClangFormat.html) with the rules defined in `.clang-format`. You have two options: + - Run `clang-format -i path/to/file.c` manually on the files you changed. + - Install [Node.js](https://nodejs.org/) and run `npm install` once. This installs [husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged), which formats every staged `.c` / `.h` file with clang-format on `git commit` automatically. + - CI checks formatting of only the files changed in each PR, so pre-existing style deviations won't block you. ## Commit Message Guidelines diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index 5368827d2..f902afebd 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -1,7 +1,36 @@ name: C/C++ CI -on: [push, pull_request] +on: + push: + branches: + - develop + - master + - main + tags: + - 'v*' + pull_request: jobs: + format: + name: Check C/C++ formatting + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: npm ci + + - name: Check formatting of changed files + run: npm run format:check -- origin/${{ github.base_ref || 'develop' }} + build: + needs: format env: TARGET_MAJOR: 3 TARGET_VERSION: 3.0.0 @@ -14,22 +43,21 @@ jobs: runs-on: ${{ matrix.os }} name: "Builds binaries on ${{ matrix.os }}" steps: - - uses: actions/checkout@v1 - - - uses: xmake-io/github-action-setup-xmake@v1 + - uses: actions/checkout@v4 with: - xmake-version: branch@dev + submodules: recursive - - name: Update git submodule - run: | - git submodule update --init + - uses: xmake-io/github-action-setup-xmake@v1 + with: + package-cache: true + actions-cache-folder: '.xmake-cache' - name: Install tools if: runner.os == 'Linux' run: | sudo apt-get update --fix-missing - sudo apt-get install debhelper lcov valgrind -yy - sudo apt-get install libfreetype6-dev libpng-dev libyaml-dev libomp-dev libx11-dev ninja-build fontconfig libfontconfig1-dev libjpeg-dev + sudo apt-get install debhelper lcov valgrind xvfb -yy + sudo apt-get install libfreetype6-dev libpng-dev libyaml-dev libomp-dev libx11-dev ninja-build fontconfig libfontconfig1-dev libjpeg-dev libwayland-dev libxkbcommon-dev wayland-protocols - name: Configure if: runner.os == 'Windows' @@ -37,45 +65,25 @@ jobs: - name: Configure for coverage mode if: runner.os == 'Linux' - run: xmake config -y -v -k ${{ env.TARGET_KIND }} -m coverage --ci-env=y + run: xmake config -y -v -k ${{ env.TARGET_KIND }} -m coverage --ci-env=y --memcheck=y - name: Build - run: | - xmake - xmake build yutil_test - xmake build pandagl_tests - xmake build libcss_tests - xmake build librouter_tests - xmake build libi18n_tests - xmake build lcui_tests - - - name: Run tests for libraries with memcheck - if: runner.os == 'Linux' - run: | - xmake run pandagl_tests - xmake run yutil_test --memcheck - xmake run libcss_tests --memcheck - xmake run librouter_tests --memcheck - xmake run libi18n_tests --memcheck + run: xmake - - name: Run tests for lcui with memcheck - if: runner.os == 'Linux' - run: | - xmake run lcui_tests --memcheck - - - name: Run tests + - name: Run all tests if: runner.os == 'Windows' - run: | - xmake run pandagl_tests - xmake run yutil_test - xmake run libcss_tests - xmake run librouter_tests - xmake run libi18n_tests - xmake run lcui_tests + run: xmake test + + - name: Run all tests with memcheck + if: runner.os == 'Linux' + run: xvfb-run -a xmake test - name: Upload reports to Codecov if: runner.os == 'Linux' - run: bash <(curl -s https://codecov.io/bash); + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false - name: Build for release run: | @@ -87,22 +95,40 @@ jobs: xmake package xmake install -o dist/lcui-package + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - uses: oven-sh/setup-bun@v2 + + - name: Install doc-viewer dependencies + working-directory: examples/doc-viewer + run: bun install + + - name: Install lcui-cli + run: npm install -g @lcui/cli + + - name: Generate doc-viewer sources + working-directory: examples/doc-viewer + run: bun run compile + + - name: Process doc-viewer TSX files + working-directory: examples/doc-viewer + run: lcui build + - name: Build examples - run: | - cd examples - xmake config -P . -y - xmake build -P . - xmake install -P . -o ../dist/lcui-examples - mv ../dist/lcui-examples/bin/* ../dist/lcui-examples/ - rm -r ../dist/lcui-examples/bin - - - uses: actions/upload-artifact@master + run: xmake build -yg lcui-examples + + - name: Install examples + run: xmake install -o dist/lcui-examples -g lcui-examples + + - uses: actions/upload-artifact@v4 with: name: lcui${{ env.TARGET_MAJOR }}-${{ env.TARGET_VERSION }}-${{ env.TARGET_KIND }} (${{ runner.os }}) path: | dist/lcui-package - - uses: actions/upload-artifact@master + - uses: actions/upload-artifact@v4 with: name: lcui${{ env.TARGET_MAJOR }}-examples (${{ runner.os }}) path: | @@ -116,10 +142,10 @@ jobs: ARTIFACT_DIR: ./release steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: ${{ env.ARTIFACT_DIR }} diff --git a/.gitignore b/.gitignore index 7186f43e6..a8d76765b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,5 @@ build/*.jpg config.h test/build vsxmake* -compile_commands.json \ No newline at end of file +compile_commands.json +docs/build/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index abde31371..000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "lib/yutil"] - path = lib/yutil - url = https://gitee.com/lcui-dev/yutil.git - branch = main diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..f841cf21a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +npx lint-staged diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a5c4578e0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,216 @@ +## 代码格式 + +遵循 .clang-format 文件中定义的规则,修改文件后需格式化。 + +### 缩进 + +使用八个空格缩进。 + +### 代码结构 + +源文件中的代码结构应该为: + +1. 预处理指令 +2. 类型 +3. 全局变量 +4. 函数声明 +5. 函数定义 + +其中“函数定义”应该按照功能类别分组、按依赖关系从基础到高级的顺序排列,例如: + +```c +static int var1; +static int var1; + +static void func1(); +static void func2(); + +// object 1 + +void object1_func1() +{ + // .. +} + +void object1_func2() { + // ... + object1_func1(); +} + + +// object 2 + +void object2_func1() +{ + // .. +} + +void object2_func2() { + // ... + object2_func1(); +} +``` + +注意!优先通过调整函数定义代码块的顺序来解决声明问题,而不是前置声明函数。 + +### 未使用参数 + +未使用的函数参数(包括回调签名里必须存在的 `void *arg`、事件回调的 `ui_event_t *e` 等) +**不要写 `(void)xxx;`**。GCC、Clang、MSVC 都默认不警告未使用参数名,只有未使用局部变量 +才会警告。`(void)xxx;` 是冗余噪音,掩盖真正该处理的警告。 + +❌ 禁止: +```c +static void on_event(ui_widget_t *w, ui_event_t *e, void *arg) +{ + (void)w; + (void)e; + (void)arg; + /* 实际逻辑 */ +} +``` + +✅ 允许: +```c +static void on_event(ui_widget_t *w, ui_event_t *e, void *arg) +{ + /* 直接用到的参数正常使用;用不到的参数名直接保留在签名里 */ +} +``` + +> 例外:C++ 模式下某些编译器会警告未使用参数,需要按上下文决定。LCUI 是纯 C, +> 不适用此例外。 + +## 测试用例 + +### 归属规则 + +按下面优先级判定一个测试归属: + +1. case 实际 `#include` 的库头文件集合(含传递依赖)。 +2. 调用的运行时入口(`lcui_init` / `ui_init` / `pd_*_init` 等)。 +3. xmake target 上需要的 `add_deps`。 + +仅触达单 lib(外加 yutil/ctest/标准库)的测试放 `lib//tests/`;触达两个及以上同级 lib,或依赖 `src/widgets/` 注册的 widget 类型的测试放 `tests/integration/`。 + +### 文件与函数命名 + +- 文件名:`test_.c` +- 套件入口(lib 内):`void test__(void)`,如 `test_ui_xml_parser`、`test_pandagl_image_reader` +- 套件入口(顶层集成):`void test_(void)`,无前缀,如 `test_settings` +- 套件入口必须在两个位置都注册:所属 lib 的 `tests/main.c` 与顶层 `tests/main.c` 中的 `suites[]` 表 +- 内部分组用 `static void <动词>_(void)`,由 `ctest_describe` 注册 + +### 描述文本风格 + +- `ctest_describe(name, fn)` 的 `name` 是名词性主题,全小写空格分词,无 `test` 前缀。例:`"widget opacity"`、`"flex layout"`、`"settings.fps_cap"` +- `ctest_equal_*(name, ...)` 的 `name` 用 `should ...` 行为陈述。例:`"should default fps_cap to 120"`、`"should match parent border color"` +- 当上下文清晰(例如 layout case 中描述某 selector 对应的 box)时,可保留 jQuery 选择器风格的描述,无需强行加 should + +### 资源文件 + +- 跨 lib 共享的 fixture 放 `tests/fixtures/` +- 仅本 lib 用的 fixture 也建议复制到 `tests/fixtures/`(顶层 lcui-tests 与单 lib binary 共用同一 rundir) +- xmake target 的 `set_rundir` 指向 `tests/fixtures/` +- 测试代码加载资源时直接用文件名,不带目录前缀 + +### 三种文件职责 + +- `tests/integration/test_.c`:跨 lib 集成测试,自动断言。不调用 `lcui_main`,必要时由 `tests/previews/preview_.c` 提供可视诊断 +- `tests/scenes/_scene.{c,h}`:可视化场景搭建模块,签名 `void _scene_build(...)`。只构造 widget 树和应用样式,不做断言、不调用 `ctest_*`、不调用 `lcui_main`/`lcui_quit`。给 cases 与未来的 examples demo 共用 +- `lib//tests/test_.c`:纯 lib 测试,仅断言 + +### 编写示例 + +```c +#include + +void test_my_case(void) +{ + ctest_equal_int("should add two numbers", 1 + 1, 2); +} +``` + +注册: + +```c +/* lib//tests/main.c 或 tests/main.c */ +extern void test_my_case(void); + +static const ctest_suite_t suites[] = { + { "my case", test_my_case }, + { NULL, NULL } +}; + +CTEST_MAIN(suites) +``` + +### 运行 + +- 全量:`xmake test` +- 按 pattern 过滤:`xmake test "*/widget*"`(匹配 target/test 名) +- 按 group 过滤:`xmake test -g tests` +- 单 binary 跑全部 suite:`xmake run -tests`,例如 `xmake run lcui-tests` +- 单 binary 内细粒度过滤:`xmake run lcui-tests --grep=""`,子串匹配 suite 名 +- 单 binary 列出 suite:`xmake run lcui-tests --list` +- 内存检查:`xmake f --memcheck=y && xmake test`,调用 drmemory(Windows)或 valgrind(Linux);恢复正常运行:`xmake f --memcheck=n` + +### 不要触碰 + +- `lib/yutil/tests/`:使用旧 libtest 框架自管理,不并入 ctest 体系,不被 lcui-tests 收集 + +## 重构约定 + +### 合并重复分支 + +当存在两个分支仅输入不同但后续处理相同(如选择 obs->root 或 ui_root),用局部变量合并公共逻辑,避免重复代码。 + +```c +/* before */ +if (ctx->logger) { + write_log(ctx->logger, msg); +} else { + logger_t *logger = get_default_logger(); + write_log(logger, msg); +} + +/* after */ +logger_t *logger = ctx->logger ? ctx->logger : get_default_logger(); +write_log(logger, msg); +``` + +### 提取公共逻辑 + +若公共逻辑较长,可提取为独立函数,并保持调用路径一致,减少分叉实现。 + +## LCUI CSS 引擎约束 + +### 选择器 + +- 仅支持:通配符 `*`、类型 `type`、类 `.cls`、ID `#id`、后代空格 `A B` +- 不支持:子代 `>`、相邻兄弟 `+`、通用兄弟 `~` +- 不支持:属性选择器 `[attr]` `[data-x]` +- 不支持:功能性伪类 `:not()` `:is()` `:where()` `:has()` `:nth-child()`(解析器无 `(` 语法) +- 不支持:伪元素 `::before` `::after` + +### 优先级 + +- Rank:`GENERAL=0`,`TYPE=1`,`CLASS=10`,`PCLASS=10`,`ID=100` +- 同 rank 用 `batch_num`(声明顺序)决胜,后声明覆盖先声明 +- 无 `!important` 机制 +- class 与 pclass rank 相同(都是 10) + +### 属性与值 + +- `position`:仅 `static` / `relative` / `absolute`,无 `fixed` +- `white-space`:仅 `normal` / `nowrap` +- `border-style`:仅 `none` / `solid` +- 无 `calc()` / `var()` / CSS 自定义属性 +- 无 `calc()` / `var()` / `em` / `rem` / `vh` / `vw` 单位(可用 `dp` / `px` / `pt` / `%`) +- 无 `overflow` 属性 + +## 指令 + +### gen-commit + +用于生成符合 Angular 规范的提交信息,scope 应为 lib 目录下的任意目录名(例如:ui、css),标题长度限制在 80 字符以内,应结合本次会话内容生成,无需读取实际改动文件内容。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c7270f6c..87278adaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `lib/ptk` - `lib/thread` - `lib/router` + - `lib/ui-router` - `lib/worker` - `lib/ui` - `lib/ui-xml` diff --git a/CHANGELOG.zh-cn.md b/CHANGELOG.zh-cn.md index b41b197da..c6df4ed54 100644 --- a/CHANGELOG.zh-cn.md +++ b/CHANGELOG.zh-cn.md @@ -10,6 +10,7 @@ - lib/ptk - lib/thread - lib/router + - lib/ui-router - lib/worker - lib/ui - lib/ui-xml diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1d89b317c..6e8a5533f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at root@lc-soft.io. All +reported by contacting the project team at hello@lcui.dev. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/CODE_OF_CONDUCT.zh-cn.md b/CODE_OF_CONDUCT.zh-cn.md index cfbf93366..45039ac4b 100644 --- a/CODE_OF_CONDUCT.zh-cn.md +++ b/CODE_OF_CONDUCT.zh-cn.md @@ -38,7 +38,7 @@ ## 强制执行 -可以通过 root@lc-soft.io,来联系项目团队来举报滥用、骚扰或其他不被接受的行为。 +可以通过 hello@lcui.dev,来联系项目团队来举报滥用、骚扰或其他不被接受的行为。 任何维护团队认为有必要且适合的所有投诉都将进行审查及调查,并做出相对应的回应。项目小组有对事件回报者有保密的义务。具体执行的方针近一步细节可能会单独公布。 diff --git a/docs/en/devtools/css-modules.mdx b/docs/en/devtools/css-modules.mdx new file mode 100644 index 000000000..f8e733da9 --- /dev/null +++ b/docs/en/devtools/css-modules.mdx @@ -0,0 +1,29 @@ +# CSS Modules + +CSS Modules create locally scoped CSS classes for TSX components, avoiding naming conflicts and improving maintainability. + +## Usage + +Create a file with the `.module.css` suffix: + +```css title="MyComponent.module.css" +.card { + border: 1px solid #eee; + border-radius: 4px; +} +``` + +Import it in a `.tsx` file: + +```tsx title="MyComponent.tsx" +import styles from "./MyComponent.module.css"; +``` + +Use the `styles` object instead of string class names in JSX: + +```diff +- ++ +``` + +During compilation, `@lcui/cli` converts `.module.css` into C identifier bindings, automatically generating unique C constant names for class names to avoid global conflicts. diff --git a/docs/en/devtools/icon-library.mdx b/docs/en/devtools/icon-library.mdx new file mode 100644 index 000000000..aae574bb9 --- /dev/null +++ b/docs/en/devtools/icon-library.mdx @@ -0,0 +1,29 @@ +# Icon Library + +`@lcui/fluent-icons` is an icon library adapted for LCUI. All icons come from Microsoft's [fluentui-system-icons](https://github.com/microsoft/fluentui-system-icons) project. + +## Installation + +```sh +npm install @lcui/fluent-icons +``` + +## Choosing icons + +Search and select icons on [flicon.io](https://www.flicon.io/). For example, the zoom-in icon is typically named "Zoom In" in English. + +fluentui-system-icons icons are available in sizes like 16, 20, and 24, named in the format "icon name + size + style". `@lcui/fluent-icons` uses the naming format "icon name + style", and when the style is Regular it can be omitted. + +## Usage + +```tsx +import { ZoomIn } from "@lcui/fluent-icons"; + + +``` + +The default size is 20. If the icon size is fixed and you want better rendering, specify the size parameter: + +```tsx + +``` diff --git a/docs/en/devtools/sass.mdx b/docs/en/devtools/sass.mdx new file mode 100644 index 000000000..4c13fdbd7 --- /dev/null +++ b/docs/en/devtools/sass.mdx @@ -0,0 +1,15 @@ +# Sass + +Sass is a popular CSS preprocessor that extends CSS with variables, nesting rules, mixins, and more. + +## Usage + +`@lcui/cli` has a built-in Sass preprocessor that is automatically invoked when compiling `.sass` and `.scss` files — no additional installation or configuration needed. + +Import Sass files directly in TSX: + +```tsx title="MyComponent.tsx" +import "./MyComponent.scss"; +``` + +The CLI compiles Sass files into CSS, then converts them to C code for runtime loading. diff --git a/docs/en/devtools/tailwind-css.mdx b/docs/en/devtools/tailwind-css.mdx new file mode 100644 index 000000000..3ce304d5b --- /dev/null +++ b/docs/en/devtools/tailwind-css.mdx @@ -0,0 +1,31 @@ +# Tailwind CSS + +Tailwind CSS is a utility-first CSS framework that helps developers quickly apply styles through predefined CSS classes. Compared to traditional CSS, you don't need to create new CSS files, write rules, or think up class names. + +## Installation + +```sh +npm install -D tailwindcss postcss @thedutchcoder/postcss-rem-to-px +``` + +## Configuration + +Copy the following files from the [lcui-quick-start](https://github.com/lcui-dev/lcui-quick-start) template project to your project root: + +- `postcss.config.js` +- `tailwind.config.js` +- `app/global.css` + +`global.css` contains the Tailwind directives: + +```css title="app/global.css" +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +When compiling, `@lcui/cli` processes Tailwind directives through the PostCSS chain (postcss + tailwindcss + postcss-rem-to-px) to generate the final CSS output. + +:::tip +If you don't want to place global.css inside the app directory, change the `content` path matching rules in `tailwind.config.js`. +::: diff --git a/docs/en/devtools/tsx-and-dev-tools.mdx b/docs/en/devtools/tsx-and-dev-tools.mdx new file mode 100644 index 000000000..0f112bad3 --- /dev/null +++ b/docs/en/devtools/tsx-and-dev-tools.mdx @@ -0,0 +1,201 @@ +# TSX & Dev Tools + +With `@lcui/cli`, you can write declarative UIs using TypeScript with JSX syntax. The CLI compiles TSX into C code, so you don't need to manually write widget tree construction logic. + +The benefit is that the declarative approach makes state binding, event binding, and resource imports more concise and intuitive. Handling logic and view in the same file reduces context switching. + +However, `@lcui/react` currently has limited capabilities — you can only declare component state, data bindings, and event bindings. For complex operations like conditional rendering or list rendering, you still need to write C code. + +## Installing dev tools + +`@lcui/cli` is a command-line tool that integrates a TypeScript compiler, Sass preprocessor, resource file loader, and more. It requires a Node.js runtime: + +```sh +npm install -g @lcui/cli +``` + +## How the preprocessor works + +LCUI uses a preprocessor approach: TSX files are only executed during preprocessing and do not become runtime code. You can think of TSX files as configuration files, where the TypeScript code acts as preprocessing directives. + +The preprocessor matches the appropriate loader by file extension, parses the TSX code, collects dependencies, executes component functions, and then generates C source files based on the returned JSX elements and Hook call results. + +## Usage overview + +```tsx title="src/App.tsx" +import { useState, TextInput, Button } from "@lcui/react"; + +export default function App() { + const inputRef = { current: { value: "" } }; + const [name, setName] = useState("LCUI"); + + return ( + + + Hello, {name}! + + + + + ); +} +``` + +## React library + +`@lcui/react` is a UI library tailored for LCUI's features and preprocessor workflow, providing built-in components, utility functions, and hooks. + +Installation: + +```sh +npm install @lcui/react +``` + +### Component functions + +The preprocessor executes component functions, collects data from `useState`, `useRef`, etc. and the returned JSX elements, then converts them into C code. + +:::warning +- Component function return values must be a single JSX element — ``, null, undefined, strings, numbers, or other objects are not supported. +- Declaring and passing component props is not yet supported. +::: + +### State management + +`useState` declares a state variable for a component: + +```tsx +import { useState } from "@lcui/react"; + +function MyComponent() { + const [age, setAge] = useState(23); + const [name, setName] = useState("Taylor"); + // ... +} +``` + +`useState` returns an array: + +- The state variable, initialized to the value passed to `useState`. +- A set function, which lets you change the state variable in response to interactions. + +`useState` adds a member to the component's state struct and generates initialization code in the init function: + +```c +/* Component state struct */ +struct MyComponent_state { + int age; + char *name; +}; + +/* Code in component init function */ +_that->state.age = 23; +_that->state.name = strdup2("Taylor"); +``` + +The state's set function inserts C code in the current scope, e.g. `setAge(30)` generates `_that->state.age = 30;`. + +:::warning +`useState` parameters can only be string or number types. +::: + +### Refs + +Use the `$ref` attribute to reference a widget object: + +```tsx + +``` + +Then operate on it in C code: + +```c +ui_textinput_set_content(_that->refs.input, "hello"); +``` + +The `$ref` value can also be a reference object returned by `useRef`: + +```tsx +import { useRef } from "@lcui/react"; + +function MyComponent() { + const inputRef = useRef(); + + return ; +} +``` + +`useRef` currently only implements value property read/write binding for TextInput: + +```tsx +inputRef.current.value = "World"; + + +``` + +:::warning +`@lcui/react`'s `useRef` is different from React's `useRef` — it is only for referencing widget objects. +::: + +### Responding to events + +Declare event handlers using the `on + event name` attribute: + +```tsx + +``` + +The preprocessor generates C code to bind the event: + +```c +ui_widget_on(_that->refs.ref_0, "click", handleClick, w); +``` + +You can also bind JavaScript functions to events: + +```tsx +import { useState, Text, Button } from "@lcui/react"; + +export default function Counter() { + const [text, setText] = useState("Click me"); + + function handleClick() { + setText("You clicked"); + } + + return ( + + ); +} +``` + +The preprocessor generates a C-language version of the event handler based on the code executed inside the handler function. + +:::warning +Currently, only state variable set functions can be called inside event handlers. Accessing the event object or executing other statements is not supported. +::: + +### Conditional rendering + +:::warning +Not yet supported. +We are considering implementing conditional rendering with a `` component, inspired by Solid.js's ``. +::: + +### Rendering lists + +:::warning +Not yet supported. +We are considering implementing array iteration with a `` component, inspired by Solid.js's ``. +::: diff --git a/docs/en/handbook/customization.mdx b/docs/en/handbook/customization.mdx new file mode 100644 index 000000000..2a271bbe9 --- /dev/null +++ b/docs/en/handbook/customization.mdx @@ -0,0 +1,136 @@ +# Customization + +You can create entirely new widget types or extend existing ones. + +## Creating a custom widget + +Use `ui_create_widget_prototype()` to register a new widget type: + +```c title="src/counter.c" +#include + +typedef struct { + int value; +} counter_data_t; + +static ui_widget_prototype_t *counter_proto; + +static void counter_init(ui_widget_t *w) +{ + counter_data_t *data; + ui_widget_t *text; + + data = ui_widget_add_data(w, counter_proto, sizeof(counter_data_t)); + data->value = 0; + + text = ui_create_widget("text"); + ui_text_set_content(text, "0"); + ui_widget_append(w, text); +} + +static void counter_destroy(ui_widget_t *w) +{ + /* Release owned resources (if any) */ +} + +void register_counter_widget(void) +{ + counter_proto = ui_create_widget_prototype("counter", NULL); + counter_proto->init = counter_init; + counter_proto->destroy = counter_destroy; +} +``` + +Use the custom widget: + +```c title="src/main.c" +extern void register_counter_widget(void); + +int main(void) +{ + ui_widget_t *counter; + + lcui_init(); + register_counter_widget(); + + counter = ui_create_widget("counter"); + ui_widget_append(ui_root(), counter); + return lcui_main(); +} +``` + +## Extending an existing widget + +Pass a parent type name as the second argument to `ui_create_widget_prototype()`: + +```c title="src/icon_button.c" +static ui_widget_prototype_t *icon_button_proto; + +static void icon_button_init(ui_widget_t *w) +{ + /* Call parent (button) init logic */ + icon_button_proto->proto->init(w); + ui_widget_add_class(w, "icon-button"); +} + +void register_icon_button(void) +{ + icon_button_proto = + ui_create_widget_prototype("icon-button", "button"); + icon_button_proto->init = icon_button_init; +} +``` + +:::warning +**Parent `init` must be called manually.** LCUI does not automatically call the parent type's `init` when creating a subtype widget. If you forget to call `proto->proto->init(w)`, the widget will lack the parent type's base behaviors (e.g. `button` won't register click events or set default styles). +::: + +## Widget lifecycle + +`ui_widget_prototype_t` has several callback hooks covering each lifecycle stage: + +- **`init`** — When the widget is created. Responsible for initializing owned data, child widgets, and event bindings. +- **`destroy`** — When the widget is destroyed. Responsible for releasing owned memory and unbinding events. +- **`update`** — When the widget needs updating, receives a `ui_task_type_t` parameter indicating the update type (style update, property update, etc.). Called by LCUI during each frame's update loop. +- **`setattr`** — When an XML attribute is set (for responding to attribute changes). +- **`settext`** — When text content is set. +- **`sizehint`** — Estimates the widget's natural size constraints (min/max content size) without external constraints. The layout engine uses this when computing child widget sizes. +- **`resize`** — When the widget size changes. Receives the new content area width and height as parameters. +- **`paint`** — When the widget needs to be redrawn. Used for custom drawing logic. + +## Custom painting + +Override the `paint` callback to draw custom graphics on a widget: + +```c title="src/my_widget.c" +static void my_widget_paint(ui_widget_t *w, pd_context_t *paint_ctx, + ui_widget_actual_style_t *style) +{ + pd_canvas_t *canvas = paint_ctx->canvas; + pd_color_t color = pd_color_from_rgb(255, 0, 0); + pd_rect_t rect = { style->left, style->top, w->width, w->height }; + + pd_canvas_fill_rect(canvas, color, rect); +} + +static void register_my_widget(void) +{ + ui_widget_prototype_t *proto = + ui_create_widget_prototype("my-widget", NULL); + proto->paint = my_widget_paint; +} +``` + +:::warning +**`paint` is only for drawing.** Widget size estimation is handled by `sizehint`; do not modify widget size or the child widget tree inside `paint`, as it may cause infinite loops. +::: + +## Caveats + +:::warning +**`destroy` must release owned resources.** If a custom widget allocated memory, opened files, or subscribed to system events (e.g. `lcui_settings_on_deserialize`) in `init`, it must perform symmetric cleanup in `destroy`. +::: + +:::warning +**Custom widgets must be registered after `lcui_init()` and before first instantiation.** Calling your `register_xxx()` function right at the start of `main()` is the safest place, ensuring all consumers (XML loading, router instantiation, TSX compilation results) can see the prototype. +::: diff --git a/docs/en/handbook/styling.mdx b/docs/en/handbook/styling.mdx new file mode 100644 index 000000000..3d120aca9 --- /dev/null +++ b/docs/en/handbook/styling.mdx @@ -0,0 +1,151 @@ +# Styling + +Use CSS to set colors, borders, padding, margins, dimensions, and layout for widgets. + +## How to set styles + +### Inline styles + +Use `ui_widget_set_style_string()` to set individual CSS properties directly: + +```c title="main.c" +ui_widget_t *w = ui_create_widget("text"); + +ui_widget_set_style_string(w, "color", "#336699"); +ui_widget_set_style_string(w, "font-size", "20px"); +ui_widget_set_style_string(w, "padding", "8px 16px"); +``` + +### CSS class selectors + +Use `ui_load_css_string()` or `ui_load_css_file()` to load CSS rules, then apply them to widgets with `ui_widget_add_class()`: + +```c title="main.c" +ui_load_css_file("styles.css"); + +ui_widget_t *w = ui_create_widget("text"); +ui_widget_add_class(w, "title"); +``` + +```css title="app/styles.css" +.title { + font-size: 24px; + font-weight: bold; + margin-bottom: 16px; +} +``` + +You can also use `ui_load_css_string()` to load CSS rules from a string. The second parameter is a source identifier used for debugging to locate conflicting rules: + +```c title="main.c" +ui_load_css_string(".title { font-size: 24px; }", "app.css"); +``` + +## Supported CSS features + +LCUI's CSS engine implements a subset of the web standard. This section lists all supported features and their supported values; **anything not listed is not supported by default**. + +### At Rules + +- **`@font-face`** — Load external fonts + +### Selectors + +- `*` (universal), `type`, `#id`, `.class` +- Pseudo-classes `:hover`, `:focus`, `:active`, `:first-child`, `:last-child` +- `!important` is not supported + +### Units + +- `px`, `dp`, `sp`, `pt`, `%` + +### Properties + +#### Layout + +- **`display`** — `none`, `inline-block`, `block`, `flex`, `inline-flex`, `table`, `inline-table`, `table-row`, `table-cell` +- **`position`** — `static`, `relative`, `absolute` +- **`top` / `right` / `bottom` / `left`** — `` or `` or `auto` +- **`z-index`** — `auto` or integer +- **`box-sizing`** — `content-box`, `border-box` + +#### Box model + +- **`width` / `height`** — ``, ``, `auto` +- **`min-width` / `max-width` / `min-height` / `max-height`** — same as above +- **`padding`** — shorthand, 1-4 `` values (e.g. `8px`, `4px 8px`, `4px 8px 12px`, `4px 8px 12px 16px`) +- **`padding-top` / `padding-right` / `padding-bottom` / `padding-left`** — longhand properties +- **`margin`** — shorthand, same syntax as padding +- **`margin-top` / `margin-right` / `margin-bottom` / `margin-left`** +- **`border`** — shorthand `1px solid #ccc` (width + style + color) +- **`border-color` / `border-width` / `border-style` / `border-radius`** — multi-value shorthand, same syntax as padding +- **`border-top` / `border-right` / `border-bottom` / `border-left`** — per-side shorthand +- **`border-top-color` / `border-right-color` / `border-bottom-color` / `border-left-color`** +- **`border-top-width` / `border-right-width` / `border-bottom-width` / `border-left-width`** +- **`border-top-style` / `border-right-style` / `border-bottom-style` / `border-left-style`** +- **`border-top-left-radius` / `border-top-right-radius` / `border-bottom-left-radius` / `border-bottom-right-radius`** +- **`border-style` values** — `none`, `solid` +- **`table-layout`** — `auto`, `fixed` +- **`border-spacing`** — `{1,2}` + +#### Background + +- **`background`** — shorthand `bg-image || bg-position || bg-size || repeat-style || color` (single layer only) +- **`background-color`** — `` +- **`background-image`** — `none` or `` +- **`background-position`** — two values: x y +- **`background-position-x` / `background-position-y`** +- **`background-repeat`** — `` +- **`background-size`** — `` +- **`background-clip`** — `border-box`, `padding-box`, `content-box` + +#### Flexbox layout + +- **`flex`** — shorthand (`flex-grow` + `flex-shrink` + `flex-basis`) +- **`flex-shrink` / `flex-grow` / `flex-basis`** +- **`flex-wrap`** — `nowrap`, `wrap` +- **`flex-direction`** — `row`, `column` +- **`justify-content`** — `flex-start`, `center`, `flex-end` +- **`align-items`** — `flex-start`, `center`, `flex-end`, `stretch` +- **`align-content`** — same as justify-content plus `space-between`, `space-around`, `space-evenly` +- **`gap`** — shorthand, sets both row-gap and column-gap +- **`row-gap` / `column-gap`** — `normal` or `` + +#### Typography + +- **`font-face`** — (load fonts via `@font-face` rule) +- **`font-family`** — `` (recommended to use built-in aliases like `monospace`) +- **`font-size`** — `` +- **`font-style`** — `normal`, `italic`, `oblique` +- **`font-weight`** — `normal`, `bold`, `` +- **`text-align`** — `left`, `center`, `right` +- **`line-height`** — `normal` or `` or `` +- **`color`** — `` +- **`white-space`** — `normal`, `nowrap` +- **`word-break`** — `normal`, `break-all` +- **`content`** — `` or `none` + +#### Other + +- **`opacity`** — `` or `` +- **`visibility`** — `visible`, `hidden` +- **`pointer-events`** — `auto`, `none` +- **`box-shadow`** — `none` or `` (format `{2,4} && ?`) + +## Caveats + +:::warning +**`white-space: pre` does not work.** LCUI's `white-space` only implements `normal` and `nowrap`. If you need to preserve leading whitespace from source code, replace spaces with U+00A0 (NBSP). +::: + +:::warning +**`overflow` does not clip content.** To clip or scroll child content, use the `scrollarea` built-in widget instead. +::: + +:::warning +**`background` shorthand does not support multiple layers.** Writing multi-background syntax like `background: url(a.png) no-repeat, url(b.png) center;` will not work. Use individual properties like `background-image` / `background-position` to set each layer separately. +::: + +:::warning +**CSS does not support inheritance.** Properties like `color`, `font-family`, and `font-size` do not cascade from parent to child widgets. Every widget that displays text (e.g. `text`, `button`) must explicitly set these properties. +::: diff --git a/docs/en/overview/about.mdx b/docs/en/overview/about.mdx new file mode 100644 index 000000000..87b910c5b --- /dev/null +++ b/docs/en/overview/about.mdx @@ -0,0 +1,59 @@ +# About LCUI + +LCUI is an open-source desktop graphical user interface library written in C, designed to provide C developers with a simple and easy-to-use GUI development experience, while incorporating CSS styling and declarative UI description from web development to lower the learning barrier. + +## Key features + +- **Cross-platform** — Supports Windows and Linux. +- **Fully self-drawn widgets** — Widgets maintain consistent appearance and behavior across platforms. +- **DPI-adaptive** — Automatically scales the UI on high-resolution screens for crisp display. +- **Built-in CSS engine** — Supports using CSS to define UI styles and layouts, making it easy to pick up for those with web development experience. +- **Modern development tools** — Through the `@lcui/cli` tool, you can use TypeScript with JSX syntax to write user interfaces. + +## Who should use LCUI + +LCUI is suitable for these developers: + +- Want to keep using C on the desktop, but want to escape the tedious Win32 / X11 native development experience. +- Already familiar with web frontend (HTML, CSS) and want to transfer that experience to desktop applications. +- Need to build single-window desktop tools with simple UI content. + +If your use case is a large commercial desktop product, a game engine, or a tool deeply integrated with the OS, LCUI may not be the best choice; in such scenarios, consider Qt, GTK, or native APIs instead. + +## Architecture + +LCUI is divided into four layers from top to bottom: + +### Application layer + +Your business code: custom widgets, CSS styles, TSX/JSX code, event handling. This is the only part tied to your project domain. + +### LCUI Runtime + +Initialization, event loop, application lifecycle management. Responsible for running the app, dispatching events, and driving rendering. + +### UI Helpers + +UI XML parsing, UI Router, cursor management, internationalization (i18n), and other helper modules. These package common UI behaviors into pluggable subsystems. + +### Foundation layer + +YUtil (general utility library), PandaGL (2D rendering engine), CSS engine, UI widget system, Thread / Worker abstractions. These modules can also be used independently. + +The platform layer (Windows / Linux) handles window management and input events. + +## License + +LCUI is released under the MIT License. See LICENSE.TXT in the repository root for details. + +## Contributing + +Contributions are welcome! Before submitting a Pull Request, please read CONTRIBUTING.md in the repository root. + +- **Bug reports** — Open an issue on GitHub Issues. +- **Feature requests** — Start a discussion on GitHub Discussions. +- **Code contributions** — Fork the repo, create a branch, and submit a PR. + +## Community + +If you run into issues, you can ask questions on GitHub Discussions (use the `Q&A` label). We also encourage experienced users to help newcomers. diff --git a/docs/en/overview/quick-start.mdx b/docs/en/overview/quick-start.mdx new file mode 100644 index 000000000..4dce35446 --- /dev/null +++ b/docs/en/overview/quick-start.mdx @@ -0,0 +1,44 @@ +# Quick Start + +Get up and running with LCUI in minutes. + +## Prerequisites + +- **Operating system** — Windows (recommended) or Linux +- **Node.js** — Required to run `@lcui/cli` +- **xmake** — C/C++ build tool +- **Git** — Download and manage source code + +## Installation + +Install the LCUI CLI globally: + +```sh +npm install -g @lcui/cli +``` + +Create and run your first app: + +```sh +lcui create my-app +cd my-app +lcui build +xmake run app +``` + +`lcui create` clones a minimal project from the lcui-quick-start template repository, including xmake.lua configuration, example code, and dependencies. Once done, run `lcui build` to compile TSX resources, then `xmake run app` to launch the application. + +## Next steps + +- Explore the sample project generated by `lcui create` — look at the code and file structure to understand the basics of an LCUI application. +- Read the widget reference (Button, Text, TextInput, Anchor, ScrollArea) to learn about built-in widgets. +- Read the Styling chapter to learn how to apply CSS properties to widgets. +- Read the Customization chapter to learn how to create custom widgets. +- Read the TSX & Dev Tools chapter to understand `@lcui/react` usage and limitations in depth. + +## Having trouble? + +If you run into problems while learning, you can get help through: + +- Ask questions on GitHub Discussions (use the `Q&A` label). +- Check CONTRIBUTING.md in the repository root to learn how to contribute. diff --git a/docs/en/widgets/anchor.mdx b/docs/en/widgets/anchor.mdx new file mode 100644 index 000000000..796a5e554 --- /dev/null +++ b/docs/en/widgets/anchor.mdx @@ -0,0 +1,61 @@ +# Anchor + +A navigation widget similar to the HTML `` element. It can open external URLs in the system browser or load an XML view into a target container widget. + + + +## Use cases + +- **Suitable** — placing external links in a page (opens in the system browser). +- **Not suitable** — performing an action (use `button`). + +## Usage + +```tsx +Link text +``` + +## API + +### `ui_anchor_open` + +```c +void ui_anchor_open(ui_widget_t *w); +``` + +Programmatically triggers the anchor's action (same as a click). Reads the `href` attribute from the widget: + +- If `href` starts with `http://` or `https://` — opens in the system browser. +- If `href` starts with `file:///` — opens the local file in the system browser. +- Otherwise — asynchronously loads an XML file at the given path and injects its content into the widget identified by the `target` attribute. + +## XML tag + +```xml +Link text +``` + +Attributes: + +| Attribute | Required | Description | +|---|---|---| +| `href` | Yes | URL to open, or relative XML file path to load | +| `target` | For view loading | ID of the container widget where the loaded view is injected | +| `key` | No | Arbitrary string passed as `event.data` to the `loaded.anchor` event | + +## Events + +### `loaded.anchor` + +Fired on `ui_root()` after a view has been loaded and injected: + +```c +static void on_view_loaded(ui_widget_t *w, ui_event_t *e, void *arg) +{ + /* e->data contains the value of the anchor's "key" attribute */ + printf("Loaded view key: %s\n", (char *)e->data); +} + +ui_widget_on(ui_root(), "loaded.anchor", on_view_loaded, NULL); +``` + diff --git a/docs/en/widgets/button.mdx b/docs/en/widgets/button.mdx new file mode 100644 index 000000000..ea421e0dc --- /dev/null +++ b/docs/en/widgets/button.mdx @@ -0,0 +1,25 @@ +# Button + +A clickable button widget, typically used to trigger an action. + + + +## Use cases + +- **Suitable**: users click to perform an action (submit a form, open a page, toggle a state) +- **Suitable**: interactive entry points in toolbars, dialogs, and navbars +- **Not suitable**: non-interactive text display (use `text` instead) + +## Usage + +```tsx +import { Button } from "@lcui/react" +``` + +```tsx + +``` + +## API Reference + + diff --git a/docs/en/widgets/checkbox.mdx b/docs/en/widgets/checkbox.mdx new file mode 100644 index 000000000..fec46c1c6 --- /dev/null +++ b/docs/en/widgets/checkbox.mdx @@ -0,0 +1,37 @@ +# Checkbox + +A control that allows the user to toggle between checked and not checked. + + + +## Use cases + +- **Suitable**: a single binary choice (accepting terms, toggling a subscription). +- **Suitable**: checkable options inside a form. +- **Not suitable**: mutually exclusive single-choice scenarios (use `radio-group`). + +## Usage + +```tsx +import { Checkbox } from "@lcui/react" +``` + +```tsx + +``` + +## Caveats + +:::caution +**indeterminate is a state, not a default value.** The half-selected state (`indeterminate="true"`) expresses the visual notion that some sub-items are selected (for example, a parent checkbox shown as indeterminate when only some of its children are checked). Clicking a checkbox in the indeterminate state transitions it to the checked state. +::: + +## Examples + +### Disabled + + + +## API Reference + + \ No newline at end of file diff --git a/docs/en/widgets/label.mdx b/docs/en/widgets/label.mdx new file mode 100644 index 000000000..b477af6b5 --- /dev/null +++ b/docs/en/widgets/label.mdx @@ -0,0 +1,25 @@ +# Label + +Renders an accessible label associated with controls. Click events are forwarded to the target widget referenced by the `for` attribute. + + + +## Use cases + +- **Suitable**: extending the clickable area of interactive widgets such as checkbox and text-input so that clicking the label text also triggers the widget. +- **Suitable**: associating text with a control for accessibility (a11y). +- **Not suitable**: purely decorative text display (use the `text` widget instead). + +## Usage + +```tsx +import { Label } from "@lcui/react" +``` + +```tsx + +``` + +## API Reference + + \ No newline at end of file diff --git a/docs/en/widgets/progress.mdx b/docs/en/widgets/progress.mdx new file mode 100644 index 000000000..ddb30d69e --- /dev/null +++ b/docs/en/widgets/progress.mdx @@ -0,0 +1,25 @@ +# Progress + +A linear progress bar widget used to indicate the completion of a task such as file uploads or loading states. + + + +## Use cases + +- **Suitable**: file upload/download, installation, or any long-running task that needs progress feedback +- **Suitable**: form submission or async operations that need a wait indicator +- **Not suitable**: scenarios that require precise numeric values or indeterminate progress (use `text` to render manually) + +## Usage + +```tsx +import { Progress } from "@lcui/react" +``` + +```tsx + +``` + +## API Reference + + \ No newline at end of file diff --git a/docs/en/widgets/radio-group.mdx b/docs/en/widgets/radio-group.mdx new file mode 100644 index 000000000..1b2f3f89e --- /dev/null +++ b/docs/en/widgets/radio-group.mdx @@ -0,0 +1,58 @@ +# RadioGroup + +A set of mutually exclusive radio buttons—only one item in the group can be selected at a time. + + + +## Use cases + +- **Suitable**: pick exactly one option from a mutually exclusive set (view density, sort order, single-choice enumerations). +- **Suitable**: simple cases with a fixed 2–7 options. +- **Not suitable**: scenarios that allow multiple concurrent selections (use `checkbox`). +- **Not suitable**: large option lists that need search or filtering (no `select` provided yet). + +## Usage + +```tsx +import { RadioGroup, RadioGroupItem } from "@lcui/react" +``` + +```tsx + + + + + +``` + +## Composition + +Use the following composition to build a `RadioGroup`: + +``` +RadioGroup +├── RadioGroupItem +└── RadioGroupItem +``` + +In practice, each item is paired with a `Label` placed in the same flex row, with the Label's `for` pointing at the RadioGroupItem's `id`—clicking the Label forwards the click event to the matching item. + +## Examples + +### Disabled + + + +## API Reference + +### RadioGroup + +The container that manages mutually exclusive selection across its items. + + + +### RadioGroupItem + +A single selectable option. At most one RadioGroupItem within the same RadioGroup is checked at any time. + + \ No newline at end of file diff --git a/docs/en/widgets/scrollarea.mdx b/docs/en/widgets/scrollarea.mdx new file mode 100644 index 000000000..828095f25 --- /dev/null +++ b/docs/en/widgets/scrollarea.mdx @@ -0,0 +1,133 @@ +# ScrollArea + +A container that provides scrollable content when children overflow. + + + +## Use cases + +- **Suitable** — content height or width may exceed the container (long lists, documents, logs). +- **Suitable** — scrollbars needed for visual feedback. +- **Not suitable** — content always fits within the container (use a plain `widget`). + +## Usage + +```tsx +import { ScrollArea, ScrollAreaContent, Scrollbar } from "@lcui/react" +``` + +```tsx + + {/* child content */} + + + +``` + +## Composition + +Use the following composition to build a `ScrollArea`: + +``` +ScrollArea +├── ScrollAreaContent +└── Scrollbar +``` + +## API + +### `ui_create_scrollarea` + +```c +ui_widget_t *ui_create_scrollarea(void); +``` + +Creates and returns a new scroll area widget. + +### `ui_create_scrollarea_content` + +```c +ui_widget_t *ui_create_scrollarea_content(void); +``` + +Creates and returns a new scroll content container to be appended inside a scroll area. + +### `ui_scrollarea_set_scroll_top` + +```c +void ui_scrollarea_set_scroll_top(ui_widget_t *w, float value); +``` + +Sets the vertical scroll position in pixels. + +### `ui_scrollarea_set_scroll_left` + +```c +void ui_scrollarea_set_scroll_left(ui_widget_t *w, float value); +``` + +Sets the horizontal scroll position in pixels. + +### `ui_scrollarea_get_scroll_top` + +```c +float ui_scrollarea_get_scroll_top(ui_widget_t *w); +``` + +Returns the current vertical scroll position. + +### `ui_scrollarea_get_scroll_left` + +```c +float ui_scrollarea_get_scroll_left(ui_widget_t *w); +``` + +Returns the current horizontal scroll position. + +### `ui_scrollarea_get_scroll_width` + +```c +float ui_scrollarea_get_scroll_width(ui_widget_t *w); +``` + +Returns the total scrollable width of the content. + +### `ui_scrollarea_get_scroll_height` + +```c +float ui_scrollarea_get_scroll_height(ui_widget_t *w); +``` + +Returns the total scrollable height of the content. + +### `ui_scrollarea_set_wheel_scroll_direction` + +```c +void ui_scrollarea_set_wheel_scroll_direction( + ui_widget_t *w, ui_scrollarea_direction_t direction); +``` + +Sets which direction the mouse wheel scrolls. + +| Value | Description | +|---|---| +| `UI_SCROLLAREA_AUTO` | Vertical if content is taller; horizontal if wider | +| `UI_SCROLLAREA_VERTICAL` | Always scroll vertically | +| `UI_SCROLLAREA_HORIZONTAL` | Always scroll horizontally | + +### `ui_scrollarea_update` + +```c +void ui_scrollarea_update(ui_widget_t *w); +``` + +Forces the scroll area to recalculate its layout and scrollbar positions. + +## XML tag + +```xml + + + +``` + diff --git a/docs/en/widgets/text-input.mdx b/docs/en/widgets/text-input.mdx new file mode 100644 index 000000000..b800d5823 --- /dev/null +++ b/docs/en/widgets/text-input.mdx @@ -0,0 +1,143 @@ +# TextInput + +An editable text input widget. Supports single-line, multiline, password, placeholder, and readonly modes. + + + +## Use cases + +- **Suitable** — user text input (form fields, search boxes). +- **Not suitable** — text display only (use `text`). + +## Usage + +```tsx +import { TextInput } from "@lcui/react" +``` + +```tsx + +``` + +## API + +### `ui_textinput_set_text` + +```c +int ui_textinput_set_text(ui_widget_t *widget, const char *utf8_str); +``` + +Sets the input value from a UTF-8 string. + +### `ui_textinput_set_text_w` + +```c +int ui_textinput_set_text_w(ui_widget_t *widget, const wchar_t *wstr); +``` + +Sets the input value from a wide character string. + +### `ui_textinput_get_text_w` + +```c +size_t ui_textinput_get_text_w(ui_widget_t *w, size_t start, + size_t max_len, wchar_t *buf); +``` + +Reads the current input value. Returns the number of characters written. + +### `ui_textinput_get_text_length` + +```c +size_t ui_textinput_get_text_length(ui_widget_t *w); +``` + +Returns the current text length in characters. + +### `ui_textinput_clear_text` + +```c +void ui_textinput_clear_text(ui_widget_t *widget); +``` + +Clears all text from the input. + +### `ui_textinput_set_placeholder` + +```c +int ui_textinput_set_placeholder(ui_widget_t *w, const char *str); +``` + +Sets the placeholder text shown when the input is empty. + +### `ui_textinput_set_placeholder_w` + +```c +int ui_textinput_set_placeholder_w(ui_widget_t *w, const wchar_t *wstr); +``` + +Sets the placeholder text from a wide character string. + +### `ui_textinput_set_password_char` + +```c +void ui_textinput_set_password_char(ui_widget_t *w, wchar_t ch); +``` + +Masks the input with the given character (e.g. `L'*'` for password fields). + +### `ui_textinput_enable_multiline` + +```c +void ui_textinput_enable_multiline(ui_widget_t *widget, bool enable); +``` + +Enables or disables multiline input mode. + +### `ui_textinput_enable_style_tag` + +```c +void ui_textinput_enable_style_tag(ui_widget_t *widget, bool enable); +``` + +Enables or disables inline style tag parsing in the input. + +### `ui_textinput_append_text_w` + +```c +int ui_textinput_append_text_w(ui_widget_t *widget, const wchar_t *wstr); +``` + +Appends text at the end of the current content. + +### `ui_textinput_insert_text_w` + +```c +int ui_textinput_insert_text_w(ui_widget_t *widget, const wchar_t *wstr); +``` + +Inserts text at the current caret position. + +### `ui_textinput_set_caret_blink` + +```c +void ui_textinput_set_caret_blink(ui_widget_t *w, bool visible, int time); +``` + +Controls the caret blink animation. `time` is the blink interval in milliseconds. + +## XML tag + +```xml + +``` + +Attributes: + +| Attribute | Description | +|---|---| +| `placeholder` | Placeholder text | +| `multiline` | `"true"` for multiline mode | +| `readonly` | `"true"` to make the input read-only | +| `type` | `"password"` to mask input | + diff --git a/docs/en/widgets/text.mdx b/docs/en/widgets/text.mdx new file mode 100644 index 000000000..e425825c5 --- /dev/null +++ b/docs/en/widgets/text.mdx @@ -0,0 +1,78 @@ +# Text + +A widget for displaying text. Supports single-line and multiline modes, inline style tags, and CSS styling. + + + +## Use cases + +- **Suitable** — static text display (titles, labels, descriptions). +- **Not suitable** — editable text input (use `textinput`). + +## Usage + +```tsx +import { Text } from "@lcui/react" +``` + +```tsx +Hello +``` + +## API + +### `ui_text_set_content` + +```c +int ui_text_set_content(ui_widget_t *w, const char *utf8_text); +``` + +Sets the text content from a UTF-8 string. Supports inline style tags. + +### `ui_text_set_content_w` + +```c +int ui_text_set_content_w(ui_widget_t *w, const wchar_t *text); +``` + +Sets the text content from a wide character string. + +### `ui_text_get_content_w` + +```c +size_t ui_text_get_content_w(ui_widget_t *w, wchar_t *buf, size_t size); +``` + +Reads the current text content into `buf` (up to `size` wide characters). Returns the number of characters written. + +### `ui_text_set_multiline` + +```c +void ui_text_set_multiline(ui_widget_t *w, bool enable); +``` + +Enables or disables multiline mode. When enabled, `\n` characters create new lines. + +## XML tag + +```xml +Text content here +``` + +Attributes: + +| Attribute | Description | +|---|---| +| `multiline` | Set to `"true"` to enable multiline mode | + +## Inline style tags + +The text widget supports a BBCode-like inline styling syntax: + +| Tag | Example | Effect | +|---|---|---| +| `[color=X]` | `[color=#ff0000]Red[/color]` | Text color | +| `[b]` | `[b]Bold[/b]` | Bold font | +| `[i]` | `[i]Italic[/i]` | Italic font | +| `[size=X]` | `[size=20]Large[/size]` | Font size in px | + diff --git a/docs/examples/anchor-basic-tsx/example.tsx b/docs/examples/anchor-basic-tsx/example.tsx new file mode 100644 index 000000000..f9dea4c00 --- /dev/null +++ b/docs/examples/anchor-basic-tsx/example.tsx @@ -0,0 +1,5 @@ +export default function App() { + return ( + LCUI Homepage + ); +} diff --git a/docs/examples/anchor-basic-tsx/main.c b/docs/examples/anchor-basic-tsx/main.c new file mode 100644 index 000000000..d997ce1c9 --- /dev/null +++ b/docs/examples/anchor-basic-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/anchor-basic-xml/main.c b/docs/examples/anchor-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/anchor-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/anchor-basic-xml/ui.xml b/docs/examples/anchor-basic-xml/ui.xml new file mode 100644 index 000000000..e43ee5af8 --- /dev/null +++ b/docs/examples/anchor-basic-xml/ui.xml @@ -0,0 +1,6 @@ + + + + Open LCUI Homepage + + diff --git a/docs/examples/anchor-basic/main.c b/docs/examples/anchor-basic/main.c new file mode 100644 index 000000000..db3acbd48 --- /dev/null +++ b/docs/examples/anchor-basic/main.c @@ -0,0 +1,11 @@ +#include + +void anchor_basic_init(ui_widget_t *parent) +{ + ui_widget_t *a; + + a = ui_create_widget("a"); + ui_text_set_content(a, "LCUI Homepage"); + ui_widget_set_attr(a, "href", "https://lcui.dev"); + ui_widget_append(parent, a); +} diff --git a/docs/examples/button-basic-tsx/example.tsx b/docs/examples/button-basic-tsx/example.tsx new file mode 100644 index 000000000..40746a081 --- /dev/null +++ b/docs/examples/button-basic-tsx/example.tsx @@ -0,0 +1,5 @@ +import { Button } from "@lcui/react"; + +export default function App() { + return ; +} diff --git a/docs/examples/button-basic-tsx/main.c b/docs/examples/button-basic-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/button-basic-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/button-basic-xml/main.c b/docs/examples/button-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/button-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/button-basic-xml/ui.xml b/docs/examples/button-basic-xml/ui.xml new file mode 100644 index 000000000..9f94d1610 --- /dev/null +++ b/docs/examples/button-basic-xml/ui.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/examples/button-basic/main.c b/docs/examples/button-basic/main.c new file mode 100644 index 000000000..4b66e3b2a --- /dev/null +++ b/docs/examples/button-basic/main.c @@ -0,0 +1,10 @@ +#include + +void button_basic_init(ui_widget_t *parent) +{ + ui_widget_t *btn; + + btn = ui_create_widget("button"); + ui_button_set_text(btn, "Click me"); + ui_widget_append(parent, btn); +} diff --git a/docs/examples/checkbox-basic-tsx/example.tsx b/docs/examples/checkbox-basic-tsx/example.tsx new file mode 100644 index 000000000..e1d3bab5e --- /dev/null +++ b/docs/examples/checkbox-basic-tsx/example.tsx @@ -0,0 +1,11 @@ +import { Checkbox } from "@lcui/react"; +import { Label } from "@lcui/react"; + +export default function App() { + return ( + + + + + ); +} \ No newline at end of file diff --git a/docs/examples/checkbox-basic-tsx/main.c b/docs/examples/checkbox-basic-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/checkbox-basic-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/checkbox-basic-xml/main.c b/docs/examples/checkbox-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/checkbox-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/checkbox-basic-xml/ui.xml b/docs/examples/checkbox-basic-xml/ui.xml new file mode 100644 index 000000000..59e41eb89 --- /dev/null +++ b/docs/examples/checkbox-basic-xml/ui.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/examples/checkbox-basic/main.c b/docs/examples/checkbox-basic/main.c new file mode 100644 index 000000000..141317ecc --- /dev/null +++ b/docs/examples/checkbox-basic/main.c @@ -0,0 +1,17 @@ +#include + +void checkbox_basic_init(ui_widget_t *parent) +{ + ui_widget_t *cb = ui_create_checkbox(); + ui_widget_t *label = ui_create_label(); + ui_widget_t *box = ui_create_widget(NULL); + + ui_widget_set_id(cb, "terms"); + ui_widget_append(box, cb); + ui_widget_set_style_string(box, "display", "flex"); + ui_widget_set_style_string(box, "gap", "8px"); + ui_label_set_for(label, "terms"); + ui_text_set_content(label, "Accept terms and conditions"); + ui_widget_append(box, label); + ui_widget_append(parent, box); +} diff --git a/docs/examples/checkbox-disabled-tsx/example.tsx b/docs/examples/checkbox-disabled-tsx/example.tsx new file mode 100644 index 000000000..c61408621 --- /dev/null +++ b/docs/examples/checkbox-disabled-tsx/example.tsx @@ -0,0 +1,17 @@ +import { Checkbox } from "@lcui/react"; +import { Label } from "@lcui/react"; + +export default function App() { + return ( + +
+ + +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/docs/examples/checkbox-disabled-tsx/main.c b/docs/examples/checkbox-disabled-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/checkbox-disabled-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/checkbox-disabled-xml/main.c b/docs/examples/checkbox-disabled-xml/main.c new file mode 100644 index 000000000..9f6a1e964 --- /dev/null +++ b/docs/examples/checkbox-disabled-xml/main.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/checkbox-disabled-xml/ui.xml b/docs/examples/checkbox-disabled-xml/ui.xml new file mode 100644 index 000000000..98cee7cfe --- /dev/null +++ b/docs/examples/checkbox-disabled-xml/ui.xml @@ -0,0 +1,15 @@ + + + +
+
+ + +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/docs/examples/checkbox-disabled/main.c b/docs/examples/checkbox-disabled/main.c new file mode 100644 index 000000000..1865e1e2d --- /dev/null +++ b/docs/examples/checkbox-disabled/main.c @@ -0,0 +1,40 @@ +#include + +void checkbox_disabled_init(ui_widget_t *parent) +{ + ui_widget_t *box; + ui_widget_t *rows[2]; + ui_widget_t *boxes[2]; + ui_widget_t *labels[2]; + const char *ids[2] = { "d1", "d2" }; + const char *texts[2] = { "Disabled (pre-checked)", "Normal" }; + int i; + + box = ui_create_widget(NULL); + ui_widget_set_style_string(box, "display", "flex"); + ui_widget_set_style_string(box, "flex-direction", "column"); + ui_widget_set_style_string(box, "gap", "8px"); + + for (i = 0; i < 2; ++i) { + rows[i] = ui_create_widget(NULL); + ui_widget_set_style_string(rows[i], "display", "flex"); + ui_widget_set_style_string(rows[i], "align-items", "center"); + ui_widget_set_style_string(rows[i], "gap", "8px"); + + boxes[i] = ui_create_checkbox(); + ui_widget_set_id(boxes[i], ids[i]); + ui_widget_set_attr(boxes[i], "checked", "true"); + if (i == 0) { + ui_widget_set_disabled(boxes[i], true); + } + ui_widget_append(rows[i], boxes[i]); + + labels[i] = ui_create_label(); + ui_label_set_for(labels[i], ids[i]); + ui_text_set_content(labels[i], texts[i]); + ui_widget_append(rows[i], labels[i]); + + ui_widget_append(box, rows[i]); + } + ui_widget_append(parent, box); +} diff --git a/docs/examples/label-basic-tsx/example.tsx b/docs/examples/label-basic-tsx/example.tsx new file mode 100644 index 000000000..bfb657ae9 --- /dev/null +++ b/docs/examples/label-basic-tsx/example.tsx @@ -0,0 +1,10 @@ +import { Label, Checkbox } from "@lcui/react"; + +export default function App() { + return ( + + + + + ); +} \ No newline at end of file diff --git a/docs/examples/label-basic-tsx/main.c b/docs/examples/label-basic-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/label-basic-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/label-basic-xml/main.c b/docs/examples/label-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/label-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/label-basic-xml/ui.xml b/docs/examples/label-basic-xml/ui.xml new file mode 100644 index 000000000..59e41eb89 --- /dev/null +++ b/docs/examples/label-basic-xml/ui.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/examples/label-basic/main.c b/docs/examples/label-basic/main.c new file mode 100644 index 000000000..af868339a --- /dev/null +++ b/docs/examples/label-basic/main.c @@ -0,0 +1,17 @@ +#include + +void label_basic_init(ui_widget_t *parent) +{ + ui_widget_t *cb = ui_create_checkbox(); + ui_widget_t *label = ui_create_label(); + ui_widget_t *box = ui_create_widget(NULL); + + ui_widget_set_id(cb, "terms"); + ui_widget_append(box, cb); + ui_widget_set_style_string(box, "display", "flex"); + ui_widget_set_style_string(box, "gap", "8px"); + ui_label_set_for(label, "terms"); + ui_text_set_content(label, "Accept terms and conditions"); + ui_widget_append(box, label); + ui_widget_append(parent, box); +} diff --git a/docs/examples/progress-basic-tsx/example.c b/docs/examples/progress-basic-tsx/example.c new file mode 100644 index 000000000..c6e5c18f5 --- /dev/null +++ b/docs/examples/progress-basic-tsx/example.c @@ -0,0 +1,58 @@ +#include "example.h" +#include "example.tsx.h" + +typedef struct { + progress_demo_react_t base; + int timer_id; +} progress_demo_t; + +static void on_timer(void *arg); + +static void progress_demo_react_init_state(ui_widget_t *w) +{ + progress_demo_t *_that = ui_widget_get_data(w, progress_demo_proto); + _that->state.value = 20; +} + +static void on_timer(void *arg) +{ + ui_widget_t *w = arg; + progress_demo_t *_that = ui_widget_get_data(w, progress_demo_proto); + + _that->state.value += 10; + if (_that->state.value > 80) { + if (_that->timer_id >= 0) { + ptk_clear_timer(_that->timer_id); + _that->timer_id = -1; + } + return; + } + progress_demo_react_update(w); +} + +static void progress_demo_react_init(ui_widget_t *w) +{ + progress_demo_t *_that = ui_widget_get_data(w, progress_demo_proto); + progress_demo_load_template(w); + progress_demo_react_init_state(w); + _that->timer_id = ptk_set_interval(500, on_timer, w); + progress_demo_react_update(w); +} + +static void progress_demo_init(ui_widget_t *w) +{ + progress_demo_t *_that = + ui_widget_add_data(w, root_page_proto, sizeof(root_page_t)); + + root_page_react_init(w); +} + +static void progress_demo_destroy(ui_widget_t *w) +{ + progress_demo_t *_that = ui_widget_get_data(w, progress_demo_proto); + if (_that->timer_id >= 0) { + ptk_clear_timer(_that->timer_id); + _that->timer_id = -1; + } + progress_demo_react_update(w); +} diff --git a/docs/examples/progress-basic-tsx/example.tsx b/docs/examples/progress-basic-tsx/example.tsx new file mode 100644 index 000000000..9e90aad02 --- /dev/null +++ b/docs/examples/progress-basic-tsx/example.tsx @@ -0,0 +1,6 @@ +import { Progress, useState, CType } from "@lcui/react"; + +export default function ProgressDemo() { + const [value] = useState(20, CType.Int); + return ; +} diff --git a/docs/examples/progress-basic-tsx/main.c b/docs/examples/progress-basic-tsx/main.c new file mode 100644 index 000000000..1d21be07d --- /dev/null +++ b/docs/examples/progress-basic-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + ui_root_append(ui_create_progress_demo()); + return lcui_main(); +} diff --git a/docs/examples/progress-basic-xml/main.c b/docs/examples/progress-basic-xml/main.c new file mode 100644 index 000000000..9f6a1e964 --- /dev/null +++ b/docs/examples/progress-basic-xml/main.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/progress-basic-xml/ui.xml b/docs/examples/progress-basic-xml/ui.xml new file mode 100644 index 000000000..4c04399b5 --- /dev/null +++ b/docs/examples/progress-basic-xml/ui.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/examples/progress-basic/main.c b/docs/examples/progress-basic/main.c new file mode 100644 index 000000000..afef036d4 --- /dev/null +++ b/docs/examples/progress-basic/main.c @@ -0,0 +1,37 @@ +#include + +static int timer_id = 0; +static float progress_value = 20.0f; + +static void progress_basic_destroy(void); + +static void on_timer(void *arg) +{ + ui_widget_t *progress = arg; + + progress_value += 10.0f; + if (progress_value > 80.0f) { + progress_basic_destroy(); + return; + } + ui_progress_set_value(progress, progress_value); +} + +void progress_basic_init(ui_widget_t *parent) +{ + ui_widget_t *progress; + + progress = ui_create_progress(); + ui_progress_set_value(progress, progress_value); + ui_widget_append(parent, progress); + timer_id = ptk_set_interval(500, on_timer, progress); + progress_value = 20.0f; +} + +void progress_basic_destroy(void) +{ + if (timer_id) { + ptk_clear_timer(timer_id); + timer_id = 0; + } +} diff --git a/docs/examples/radio-group-basic-tsx/example.tsx b/docs/examples/radio-group-basic-tsx/example.tsx new file mode 100644 index 000000000..fa8215fd2 --- /dev/null +++ b/docs/examples/radio-group-basic-tsx/example.tsx @@ -0,0 +1,21 @@ +import { RadioGroup, RadioGroupItem } from "@lcui/react"; +import { Label } from "@lcui/react"; + +export default function App() { + return ( + +
+ + +
+
+ + +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/docs/examples/radio-group-basic-tsx/main.c b/docs/examples/radio-group-basic-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/radio-group-basic-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/radio-group-basic-xml/main.c b/docs/examples/radio-group-basic-xml/main.c new file mode 100644 index 000000000..9f6a1e964 --- /dev/null +++ b/docs/examples/radio-group-basic-xml/main.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/radio-group-basic-xml/ui.xml b/docs/examples/radio-group-basic-xml/ui.xml new file mode 100644 index 000000000..6c30734db --- /dev/null +++ b/docs/examples/radio-group-basic-xml/ui.xml @@ -0,0 +1,19 @@ + + + + +
+ + +
+
+ + +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/docs/examples/radio-group-basic/main.c b/docs/examples/radio-group-basic/main.c new file mode 100644 index 000000000..76ccba78b --- /dev/null +++ b/docs/examples/radio-group-basic/main.c @@ -0,0 +1,39 @@ +#include + +void radio_group_basic_init(ui_widget_t *parent) +{ + ui_widget_t *group; + ui_widget_t *rows[3]; + ui_widget_t *items[3]; + ui_widget_t *labels[3]; + const char *ids[3] = { "r1", "r2", "r3" }; + const char *values[3] = { "default", "comfortable", "compact" }; + const char *texts[3] = { "Default", "Comfortable", "Compact" }; + int i; + + group = ui_create_radio_group(); + ui_widget_set_attr(group, "value", "comfortable"); + + for (i = 0; i < 3; ++i) { + rows[i] = ui_create_widget(NULL); + ui_widget_set_style_string(rows[i], "display", "flex"); + ui_widget_set_style_string(rows[i], "align-items", "center"); + ui_widget_set_style_string(rows[i], "gap", "8px"); + + items[i] = ui_create_radio_group_item(); + ui_widget_set_id(items[i], ids[i]); + ui_widget_set_attr(items[i], "value", values[i]); + if (i == 1) { + ui_widget_set_attr(items[i], "checked", "true"); + } + ui_widget_append(rows[i], items[i]); + + labels[i] = ui_create_label(); + ui_label_set_for(labels[i], ids[i]); + ui_text_set_content(labels[i], texts[i]); + ui_widget_append(rows[i], labels[i]); + + ui_widget_append(group, rows[i]); + } + ui_widget_append(parent, group); +} diff --git a/docs/examples/radio-group-disabled-tsx/example.tsx b/docs/examples/radio-group-disabled-tsx/example.tsx new file mode 100644 index 000000000..8e9dac19e --- /dev/null +++ b/docs/examples/radio-group-disabled-tsx/example.tsx @@ -0,0 +1,21 @@ +import { RadioGroup, RadioGroupItem } from "@lcui/react"; +import { Label } from "@lcui/react"; + +export default function App() { + return ( + +
+ + +
+
+ + +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/docs/examples/radio-group-disabled-tsx/main.c b/docs/examples/radio-group-disabled-tsx/main.c new file mode 100644 index 000000000..c508b339d --- /dev/null +++ b/docs/examples/radio-group-disabled-tsx/main.c @@ -0,0 +1,11 @@ +#include + +/* example.tsx is compiled to example.h by @lcui/cli */ +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/radio-group-disabled-xml/main.c b/docs/examples/radio-group-disabled-xml/main.c new file mode 100644 index 000000000..9f6a1e964 --- /dev/null +++ b/docs/examples/radio-group-disabled-xml/main.c @@ -0,0 +1,8 @@ +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/radio-group-disabled-xml/ui.xml b/docs/examples/radio-group-disabled-xml/ui.xml new file mode 100644 index 000000000..0764b8c6a --- /dev/null +++ b/docs/examples/radio-group-disabled-xml/ui.xml @@ -0,0 +1,19 @@ + + + + +
+ + +
+
+ + +
+
+ + +
+
+
+
\ No newline at end of file diff --git a/docs/examples/radio-group-disabled/main.c b/docs/examples/radio-group-disabled/main.c new file mode 100644 index 000000000..514358d4b --- /dev/null +++ b/docs/examples/radio-group-disabled/main.c @@ -0,0 +1,42 @@ +#include + +void radio_group_disabled_init(ui_widget_t *parent) +{ + ui_widget_t *group; + ui_widget_t *rows[3]; + ui_widget_t *items[3]; + ui_widget_t *labels[3]; + const char *ids[3] = { "rd1", "rd2", "rd3" }; + const char *values[3] = { "disabled", "option2", "option3" }; + const char *texts[3] = { "Disabled", "Option 2", "Option 3" }; + int i; + + group = ui_create_radio_group(); + ui_widget_set_attr(group, "value", "option2"); + + for (i = 0; i < 3; ++i) { + rows[i] = ui_create_widget(NULL); + ui_widget_set_style_string(rows[i], "display", "flex"); + ui_widget_set_style_string(rows[i], "align-items", "center"); + ui_widget_set_style_string(rows[i], "gap", "8px"); + + items[i] = ui_create_radio_group_item(); + ui_widget_set_id(items[i], ids[i]); + ui_widget_set_attr(items[i], "value", values[i]); + if (i == 0) { + ui_widget_set_disabled(items[i], true); + } + if (i == 1) { + ui_widget_set_attr(items[i], "checked", "true"); + } + ui_widget_append(rows[i], items[i]); + + labels[i] = ui_create_label(); + ui_label_set_for(labels[i], ids[i]); + ui_text_set_content(labels[i], texts[i]); + ui_widget_append(rows[i], labels[i]); + + ui_widget_append(group, rows[i]); + } + ui_widget_append(parent, group); +} diff --git a/docs/examples/scrollarea-basic-tsx/example.css b/docs/examples/scrollarea-basic-tsx/example.css new file mode 100644 index 000000000..8bd14e40e --- /dev/null +++ b/docs/examples/scrollarea-basic-tsx/example.css @@ -0,0 +1,5 @@ +.demo-scrollarea { + width: 300px; + height: 200px; + border: 1px solid #d0d7de; +} diff --git a/docs/examples/scrollarea-basic-tsx/example.tsx b/docs/examples/scrollarea-basic-tsx/example.tsx new file mode 100644 index 000000000..104217e18 --- /dev/null +++ b/docs/examples/scrollarea-basic-tsx/example.tsx @@ -0,0 +1,20 @@ +import { ScrollArea, ScrollAreaContent, Scrollbar, Text } from "@lcui/react"; +import "./example.css"; + +const content = `这是一段用于演示滚动区域功能的示例文本。 +当文本内容超出容器高度时,用户可以通过滚动条来查看其余内容。 +滚动区域适合阅读长篇文章、展示数据列表。 +在实际项目中,滚动区域的尺寸通常由其父布局决定,开发者只需关注内容本身。 +滚动区域可以嵌套使用,构建复杂的多层滚动界面。 +合理配置滚动方向和样式,可以让界面更加整洁且易于使用。`; + +export default function App() { + return ( + + + {content} + + + + ); +} diff --git a/docs/examples/scrollarea-basic-tsx/main.c b/docs/examples/scrollarea-basic-tsx/main.c new file mode 100644 index 000000000..d997ce1c9 --- /dev/null +++ b/docs/examples/scrollarea-basic-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/scrollarea-basic-xml/main.c b/docs/examples/scrollarea-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/scrollarea-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/scrollarea-basic-xml/ui.xml b/docs/examples/scrollarea-basic-xml/ui.xml new file mode 100644 index 000000000..8e48239cb --- /dev/null +++ b/docs/examples/scrollarea-basic-xml/ui.xml @@ -0,0 +1,17 @@ + + + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + Item 10 + + + diff --git a/docs/examples/scrollarea-basic/main.c b/docs/examples/scrollarea-basic/main.c new file mode 100644 index 000000000..1ea7dd1ce --- /dev/null +++ b/docs/examples/scrollarea-basic/main.c @@ -0,0 +1,30 @@ +#include + +void scrollarea_basic_init(ui_widget_t *parent) +{ + ui_widget_t *scroll, *content, *vbar, *text; + + scroll = ui_create_scrollarea(); + ui_widget_set_style_string(scroll, "width", "300px"); + ui_widget_set_style_string(scroll, "height", "200px"); + ui_widget_set_style_string(scroll, "border", "1px solid #d0d7de"); + + content = ui_create_scrollarea_content(); + text = ui_create_widget("text"); + ui_text_set_content_w( + text, + L"这是一段用于演示滚动区域功能的示例文本。\n" + L"当文本内容超出容器高度时,用户可以通过滚动条来查看其余内容。\n" + L"滚动区域适合阅读长篇文章、展示数据列表。\n" + L"在实际项目中,滚动区域的尺寸通常由其父布局决定," + L"开发者只需关注内容本身。\n" + L"滚动区域可以嵌套使用,构建复杂的多层滚动界面。\n" + L"合理配置滚动方向和样式,可以让界面更加整洁且易于使用。"); + ui_widget_append(content, text); + + vbar = ui_create_widget("scrollbar"); + + ui_widget_append(scroll, content); + ui_widget_append(scroll, vbar); + ui_widget_append(parent, scroll); +} diff --git a/docs/examples/scrollarea-dual-scrollbars-tsx/example.css b/docs/examples/scrollarea-dual-scrollbars-tsx/example.css new file mode 100644 index 000000000..39d567802 --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars-tsx/example.css @@ -0,0 +1,23 @@ +.demo-scrollarea { + width: 400px; + height: 300px; + border: 1px solid #d0d7de; +} + +.demo-grid { + display: flex; + flex-wrap: wrap; + width: 1088px; + height: 1088px; + gap: 8px; + padding: 8px; +} + +.demo-cell { + width: 100px; + height: 100px; + background: #e1e5e9; + border: 1px solid #b0b8c0; + text-align: center; + line-height: 100px; +} diff --git a/docs/examples/scrollarea-dual-scrollbars-tsx/example.tsx b/docs/examples/scrollarea-dual-scrollbars-tsx/example.tsx new file mode 100644 index 000000000..814fa7dcd --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars-tsx/example.tsx @@ -0,0 +1,18 @@ +import { ScrollArea, ScrollAreaContent, Scrollbar, Text } from "@lcui/react"; +import "./example.css"; + +export default function App() { + return ( + + + {Array.from({ length: 100 }, (_, i) => ( + + {i + 1} + + ))} + + + + + ); +} diff --git a/docs/examples/scrollarea-dual-scrollbars-tsx/main.c b/docs/examples/scrollarea-dual-scrollbars-tsx/main.c new file mode 100644 index 000000000..d997ce1c9 --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/scrollarea-dual-scrollbars-xml/main.c b/docs/examples/scrollarea-dual-scrollbars-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/scrollarea-dual-scrollbars-xml/ui.xml b/docs/examples/scrollarea-dual-scrollbars-xml/ui.xml new file mode 100644 index 000000000..e401a4bb5 --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars-xml/ui.xml @@ -0,0 +1,44 @@ + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + + + + + + diff --git a/docs/examples/scrollarea-dual-scrollbars/main.c b/docs/examples/scrollarea-dual-scrollbars/main.c new file mode 100644 index 000000000..4bd333d18 --- /dev/null +++ b/docs/examples/scrollarea-dual-scrollbars/main.c @@ -0,0 +1,44 @@ +#include +#include + +void scrollarea_dual_scrollbars_init(ui_widget_t *parent) +{ + int i; + char buf[8]; + ui_widget_t *area, *content, *hbar, *vbar, *box; + + area = ui_create_scrollarea(); + ui_widget_set_style_string(area, "width", "420px"); + ui_widget_set_style_string(area, "height", "300px"); + ui_widget_set_style_string(area, "border", "1px solid #d0d7de"); + + content = ui_create_scrollarea_content(); + ui_widget_set_style_string(content, "display", "flex"); + ui_widget_set_style_string(content, "flex-wrap", "wrap"); + ui_widget_set_style_string(content, "width", "1088px"); + ui_widget_set_style_string(content, "height", "1088px"); + ui_widget_set_style_string(content, "gap", "8px"); + ui_widget_set_style_string(content, "padding", "8px"); + + for (i = 0; i < 100; ++i) { + snprintf(buf, sizeof(buf), "%d", i + 1); + box = ui_create_widget("text"); + ui_text_set_content(box, buf); + ui_widget_set_style_string(box, "width", "100px"); + ui_widget_set_style_string(box, "height", "100px"); + ui_widget_set_style_string(box, "background", "#e1e5e9"); + ui_widget_set_style_string(box, "border", "1px solid #b0b8c0"); + ui_widget_set_style_string(box, "text-align", "center"); + ui_widget_set_style_string(box, "line-height", "100px"); + ui_widget_append(content, box); + } + + hbar = ui_create_widget("scrollbar"); + ui_widget_set_attr(hbar, "orientation", "horizontal"); + vbar = ui_create_widget("scrollbar"); + + ui_widget_append(area, content); + ui_widget_append(area, hbar); + ui_widget_append(area, vbar); + ui_widget_append(parent, area); +} diff --git a/docs/examples/text-basic-tsx/example.tsx b/docs/examples/text-basic-tsx/example.tsx new file mode 100644 index 000000000..46f4fec31 --- /dev/null +++ b/docs/examples/text-basic-tsx/example.tsx @@ -0,0 +1,5 @@ +import { Text } from "@lcui/react"; + +export default function App() { + return Hello, LCUI!; +} diff --git a/docs/examples/text-basic-tsx/main.c b/docs/examples/text-basic-tsx/main.c new file mode 100644 index 000000000..d997ce1c9 --- /dev/null +++ b/docs/examples/text-basic-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/text-basic-xml/main.c b/docs/examples/text-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/text-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/text-basic-xml/ui.xml b/docs/examples/text-basic-xml/ui.xml new file mode 100644 index 000000000..0be3c58ba --- /dev/null +++ b/docs/examples/text-basic-xml/ui.xml @@ -0,0 +1,6 @@ + + + + Hello, LCUI! + + diff --git a/docs/examples/text-basic/main.c b/docs/examples/text-basic/main.c new file mode 100644 index 000000000..908236c26 --- /dev/null +++ b/docs/examples/text-basic/main.c @@ -0,0 +1,10 @@ +#include + +void text_basic_init(ui_widget_t *parent) +{ + ui_widget_t *w; + + w = ui_create_widget("text"); + ui_text_set_content(w, "Hello, LCUI!"); + ui_widget_append(parent, w); +} diff --git a/docs/examples/text-input-basic-tsx/example.tsx b/docs/examples/text-input-basic-tsx/example.tsx new file mode 100644 index 000000000..c2d23dae2 --- /dev/null +++ b/docs/examples/text-input-basic-tsx/example.tsx @@ -0,0 +1,5 @@ +import { TextInput } from "@lcui/react"; + +export default function App() { + return ; +} diff --git a/docs/examples/text-input-basic-tsx/main.c b/docs/examples/text-input-basic-tsx/main.c new file mode 100644 index 000000000..d997ce1c9 --- /dev/null +++ b/docs/examples/text-input-basic-tsx/main.c @@ -0,0 +1,9 @@ +#include +#include "example.h" + +int main(void) +{ + lcui_init(); + example_load(); + return lcui_main(); +} diff --git a/docs/examples/text-input-basic-xml/main.c b/docs/examples/text-input-basic-xml/main.c new file mode 100644 index 000000000..d8d8b6973 --- /dev/null +++ b/docs/examples/text-input-basic-xml/main.c @@ -0,0 +1,9 @@ +#include +#include + +int main(void) +{ + lcui_init(); + ui_load_xml_file("ui.xml"); + return lcui_main(); +} diff --git a/docs/examples/text-input-basic-xml/ui.xml b/docs/examples/text-input-basic-xml/ui.xml new file mode 100644 index 000000000..f3362eeb9 --- /dev/null +++ b/docs/examples/text-input-basic-xml/ui.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/examples/text-input-basic/main.c b/docs/examples/text-input-basic/main.c new file mode 100644 index 000000000..7dc14f0a4 --- /dev/null +++ b/docs/examples/text-input-basic/main.c @@ -0,0 +1,10 @@ +#include + +void text_input_basic_init(ui_widget_t *parent) +{ + ui_widget_t *w; + + w = ui_create_widget("textinput"); + ui_textinput_set_placeholder(w, u8"请输入..."); + ui_widget_append(parent, w); +} diff --git a/docs/sidebars.json b/docs/sidebars.json new file mode 100644 index 000000000..ec7632de9 --- /dev/null +++ b/docs/sidebars.json @@ -0,0 +1,46 @@ +{ + "sidebar": [ + { + "slug": "overview", + "label": { "en": "Overview", "zh-CN": "概述" }, + "items": [ + { "slug": "quick-start", "label": { "en": "Quick Start", "zh-CN": "快速开始" } }, + { "slug": "about", "label": { "en": "About", "zh-CN": "关于" } } + ] + }, + { + "slug": "handbook", + "label": { "en": "Handbook", "zh-CN": "手册" }, + "items": [ + { "slug": "styling", "label": { "en": "Styling", "zh-CN": "样式" } }, + { "slug": "customization", "label": { "en": "Customization", "zh-CN": "自定义" } } + ] + }, + { + "slug": "devtools", + "label": { "en": "Dev Tools", "zh-CN": "开发工具" }, + "items": [ + { "slug": "tsx-and-dev-tools", "label": { "en": "TSX & @lcui/cli", "zh-CN": "TSX 与 @lcui/cli" } }, + { "slug": "css-modules", "label": { "en": "CSS Modules", "zh-CN": "CSS Modules" } }, + { "slug": "tailwind-css", "label": { "en": "Tailwind CSS", "zh-CN": "Tailwind CSS" } }, + { "slug": "sass", "label": { "en": "Sass", "zh-CN": "Sass" } }, + { "slug": "icon-library", "label": { "en": "Icon Library", "zh-CN": "图标库" } } + ] + }, + { + "slug": "widgets", + "label": { "en": "Widgets", "zh-CN": "组件" }, + "items": [ + { "slug": "anchor", "label": { "en": "Anchor", "zh-CN": "Anchor 锚点" } }, + { "slug": "button", "label": { "en": "Button", "zh-CN": "Button 按钮" } }, + { "slug": "checkbox", "label": { "en": "Checkbox", "zh-CN": "Checkbox 复选框" } }, + { "slug": "label", "label": { "en": "Label", "zh-CN": "Label 标签" } }, + { "slug": "progress", "label": { "en": "Progress", "zh-CN": "Progress 进度条" } }, + { "slug": "radio-group", "label": { "en": "RadioGroup", "zh-CN": "RadioGroup 单选组" } }, + { "slug": "scrollarea", "label": { "en": "ScrollArea", "zh-CN": "ScrollArea 滚动区域" } }, + { "slug": "text", "label": { "en": "Text", "zh-CN": "Text 文本" } }, + { "slug": "text-input", "label": { "en": "TextInput", "zh-CN": "TextInput 文本输入" } } + ] + } + ] +} diff --git a/docs/widget-fields/anchor.ts b/docs/widget-fields/anchor.ts new file mode 100644 index 000000000..0e7682e80 --- /dev/null +++ b/docs/widget-fields/anchor.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const anchorFields: FieldData[] = [ + ...widgetFields, + { + name: "href", + type: "string", + default: "-", + description: { + en: "The URL the link points to.", + "zh-CN": "链接的目标 URL。", + }, + }, +]; diff --git a/docs/widget-fields/button.ts b/docs/widget-fields/button.ts new file mode 100644 index 000000000..7f38e7aaf --- /dev/null +++ b/docs/widget-fields/button.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const buttonFields: FieldData[] = [ + ...widgetFields, + { + name: "disabled", + type: "string", + default: "-", + description: { + en: "Disables the button when set to \"true\".", + "zh-CN": "值为 \"true\" 时禁用按钮。", + }, + }, +]; diff --git a/docs/widget-fields/checkbox.ts b/docs/widget-fields/checkbox.ts new file mode 100644 index 000000000..97633accd --- /dev/null +++ b/docs/widget-fields/checkbox.ts @@ -0,0 +1,23 @@ +import { widgetFields, FieldData } from "./widget"; + +export const checkboxFields: FieldData[] = [ + ...widgetFields, + { + name: "checked", + type: "boolean", + default: "false", + description: { + en: "Whether the checkbox is in the checked state.", + "zh-CN": "checkbox 是否处于勾选状态。", + }, + }, + { + name: "indeterminate", + type: "boolean", + default: "false", + description: { + en: "Whether the checkbox is in the indeterminate (mixed) state. A click transitions it to the checked state.", + "zh-CN": "checkbox 是否处于半选状态。点击会转为已选状态。", + }, + }, +]; \ No newline at end of file diff --git a/docs/widget-fields/label.ts b/docs/widget-fields/label.ts new file mode 100644 index 000000000..c46221b2d --- /dev/null +++ b/docs/widget-fields/label.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const labelFields: FieldData[] = [ + ...widgetFields, + { + name: "for", + type: "string", + default: "-", + description: { + en: "ID of the target widget that this label is associated with. Clicks on the label forward a click event to the target.", + "zh-CN": "标签关联的目标部件 ID。点击标签时会把 click 事件转发给目标。", + }, + }, +]; \ No newline at end of file diff --git a/docs/widget-fields/progress.ts b/docs/widget-fields/progress.ts new file mode 100644 index 000000000..4e878ee81 --- /dev/null +++ b/docs/widget-fields/progress.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const progressFields: FieldData[] = [ + ...widgetFields, + { + name: "value", + type: "number", + default: "0", + description: { + en: "Current progress value in the range 0~100. Values outside the range are clamped.", + "zh-CN": "当前进度值,取值范围 0~100。超出范围的值会被截断。", + }, + }, +]; \ No newline at end of file diff --git a/docs/widget-fields/radio-group-item.ts b/docs/widget-fields/radio-group-item.ts new file mode 100644 index 000000000..f27bd7e5e --- /dev/null +++ b/docs/widget-fields/radio-group-item.ts @@ -0,0 +1,23 @@ +import { widgetFields, FieldData } from "./widget"; + +export const radioGroupItemFields: FieldData[] = [ + ...widgetFields, + { + name: "value", + type: "string", + default: "", + description: { + en: "The value submitted when this item is selected. Must be unique within the same RadioGroup.", + "zh-CN": "选中本项时提交的值。同一 RadioGroup 内各 item 的 value 必须唯一。", + }, + }, + { + name: "checked", + type: "boolean", + default: "false", + description: { + en: "Whether this item is in the selected state. Driven by the parent RadioGroup's value; you usually don't set this directly.", + "zh-CN": "本项是否处于选中状态。由父 RadioGroup 的 value 推导,一般不直接设置。", + }, + }, +]; \ No newline at end of file diff --git a/docs/widget-fields/radio-group.ts b/docs/widget-fields/radio-group.ts new file mode 100644 index 000000000..3c0186114 --- /dev/null +++ b/docs/widget-fields/radio-group.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const radioGroupFields: FieldData[] = [ + ...widgetFields, + { + name: "value", + type: "string", + default: "", + description: { + en: "Currently selected item value. Items whose value attribute matches are shown as selected.", + "zh-CN": "当前选中项的值。值匹配项的 value 属性的 item 会显示为选中状态。", + }, + }, +]; \ No newline at end of file diff --git a/docs/widget-fields/scrollarea-content.ts b/docs/widget-fields/scrollarea-content.ts new file mode 100644 index 000000000..32aed3890 --- /dev/null +++ b/docs/widget-fields/scrollarea-content.ts @@ -0,0 +1,3 @@ +import { widgetFields, FieldData } from "./widget"; + +export const scrollareaContentFields: FieldData[] = [...widgetFields]; diff --git a/docs/widget-fields/scrollarea.ts b/docs/widget-fields/scrollarea.ts new file mode 100644 index 000000000..d1068a53a --- /dev/null +++ b/docs/widget-fields/scrollarea.ts @@ -0,0 +1,3 @@ +import { widgetFields, FieldData } from "./widget"; + +export const scrollareaFields: FieldData[] = [...widgetFields]; diff --git a/docs/widget-fields/scrollbar.ts b/docs/widget-fields/scrollbar.ts new file mode 100644 index 000000000..3de4c3327 --- /dev/null +++ b/docs/widget-fields/scrollbar.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const scrollbarFields: FieldData[] = [ + ...widgetFields, + { + name: "orientation", + type: '"vertical" | "horizontal"', + default: '"vertical"', + description: { + en: "The scroll direction of the scrollbar.", + "zh-CN": "滚动条的滚动方向。", + }, + }, +]; diff --git a/docs/widget-fields/text-input.ts b/docs/widget-fields/text-input.ts new file mode 100644 index 000000000..31fc7dad9 --- /dev/null +++ b/docs/widget-fields/text-input.ts @@ -0,0 +1,14 @@ +import { widgetFields, FieldData } from "./widget"; + +export const textInputFields: FieldData[] = [ + ...widgetFields, + { + name: "placeholder", + type: "string", + default: "-", + description: { + en: "Placeholder text shown when the input is empty.", + "zh-CN": "输入为空时显示的占位文本。", + }, + }, +]; diff --git a/docs/widget-fields/text.ts b/docs/widget-fields/text.ts new file mode 100644 index 000000000..d67964c25 --- /dev/null +++ b/docs/widget-fields/text.ts @@ -0,0 +1,3 @@ +import { widgetFields, FieldData } from "./widget"; + +export const textFields: FieldData[] = [...widgetFields]; diff --git a/docs/widget-fields/widget.ts b/docs/widget-fields/widget.ts new file mode 100644 index 000000000..18133a1ba --- /dev/null +++ b/docs/widget-fields/widget.ts @@ -0,0 +1,27 @@ +export interface FieldData { + name: string; + type: string; + default: string; + description: { en: string; "zh-CN": string }; +} + +export const widgetFields: FieldData[] = [ + { + name: "className", + type: "string", + default: "-", + description: { + en: "CSS class name applied to the element.", + "zh-CN": "应用于元素的 CSS 类名。", + }, + }, + { + name: "children", + type: "any", + default: "-", + description: { + en: "Child elements.", + "zh-CN": "子元素。", + }, + }, +]; diff --git a/docs/zh-CN/devtools/css-modules.mdx b/docs/zh-CN/devtools/css-modules.mdx new file mode 100644 index 000000000..a688a927f --- /dev/null +++ b/docs/zh-CN/devtools/css-modules.mdx @@ -0,0 +1,29 @@ +# CSS Modules + +CSS Modules 为 TSX 组件创建局部作用域的 CSS 类,避免命名冲突并提高可维护性。 + +## 用法 + +创建 `.module.css` 后缀的文件: + +```css title="MyComponent.module.css" +.card { + border: 1px solid #eee; + border-radius: 4px; +} +``` + +在 `.tsx` 文件中导入: + +```tsx title="MyComponent.tsx" +import styles from "./MyComponent.module.css"; +``` + +在 JSX 中用 `styles` 对象替代字符串类名: + +```diff +- ++ +``` + +`@lcui/cli` 编译时会将 `.module.css` 转换为 C 标识符绑定,类名会自动生成唯一的 C 常量名,避免全局冲突。 diff --git a/docs/zh-CN/devtools/icon-library.mdx b/docs/zh-CN/devtools/icon-library.mdx new file mode 100644 index 000000000..1a444aa13 --- /dev/null +++ b/docs/zh-CN/devtools/icon-library.mdx @@ -0,0 +1,29 @@ +# 图标库 + +`@lcui/fluent-icons` 是专为 LCUI 适配的图标库,图标都来自 Microsoft 的 [fluentui-system-icons](https://github.com/microsoft/fluentui-system-icons) 项目。 + +## 安装 + +```sh +npm install @lcui/fluent-icons +``` + +## 选取图标 + +在 [flicon.io](https://www.flicon.io/) 网站中搜索和选取图标。以放大图标为例,英文名通常是 Zoom In。 + +fluentui-system-icons 的图标有 16、20、24 等几种尺寸可选,命名方式是"图标名+尺寸+风格"。`@lcui/fluent-icons` 的命名方式是"图标名+风格",当风格为 Regular 时可以省略它。 + +## 用法 + +```tsx +import { ZoomIn } from "@lcui/fluent-icons"; + + +``` + +默认尺寸是 20。如果图标尺寸固定且希望更好的渲染效果,可以指定 size 参数: + +```tsx + +``` diff --git a/docs/zh-CN/devtools/sass.mdx b/docs/zh-CN/devtools/sass.mdx new file mode 100644 index 000000000..a8d6a444d --- /dev/null +++ b/docs/zh-CN/devtools/sass.mdx @@ -0,0 +1,15 @@ +# Sass + +Sass 是一个流行的 CSS 预处理器,通过变量、嵌套规则和混合元素等功能扩展 CSS。 + +## 用法 + +`@lcui/cli` 已内置 Sass 预处理器,在编译 `.sass` 和 `.scss` 后缀的文件时会自动调用,无需额外安装或配置。 + +在 TSX 中直接导入 Sass 文件: + +```tsx title="MyComponent.tsx" +import "./MyComponent.scss"; +``` + +CLI 会将 Sass 文件编译为 CSS,再转换为 C 代码在运行时加载。 diff --git a/docs/zh-CN/devtools/tailwind-css.mdx b/docs/zh-CN/devtools/tailwind-css.mdx new file mode 100644 index 000000000..a91b66794 --- /dev/null +++ b/docs/zh-CN/devtools/tailwind-css.mdx @@ -0,0 +1,31 @@ +# Tailwind CSS + +Tailwind CSS 是一个功能类优先(Utility-First)的 CSS 框架,通过预定义的 CSS 类帮助开发者快速设置样式。相比传统 CSS 编写方式,无需新建 CSS 文件、编写规则和思考类名。 + +## 安装 + +```sh +npm install -D tailwindcss postcss @thedutchcoder/postcss-rem-to-px +``` + +## 配置 + +从 [lcui-quick-start](https://github.com/lcui-dev/lcui-quick-start) 模板项目复制以下文件到项目根目录: + +- `postcss.config.js` +- `tailwind.config.js` +- `app/global.css` + +`global.css` 中包含 Tailwind 的指令: + +```css title="app/global.css" +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +`@lcui/cli` 编译时会通过 PostCSS 链(postcss + tailwindcss + postcss-rem-to-px)处理 Tailwind 指令,生成最终的 CSS 输出。 + +:::tip +如果你不想将 global.css 放到 app 目录内,请更改 `tailwind.config.js` 中 `content` 配置项的路径匹配规则。 +::: diff --git a/docs/zh-CN/devtools/tsx-and-dev-tools.mdx b/docs/zh-CN/devtools/tsx-and-dev-tools.mdx new file mode 100644 index 000000000..aebd4655b --- /dev/null +++ b/docs/zh-CN/devtools/tsx-and-dev-tools.mdx @@ -0,0 +1,201 @@ +# TSX 与开发工具 + +配合 `@lcui/cli`,你可以用 TypeScript 搭配 JSX 语法编写声明式界面。CLI 会编译 TSX 为 C 代码,你无需手动编写部件树构建逻辑。 + +这样做的好处是:声明式编程方式使得状态绑定、事件绑定和资源引入更加简洁直观,在同一份文件中处理逻辑和视图可以减少上下文切换。 + +不过,`@lcui/react` 目前功能有限,你只能声明组件的状态、数据绑定和事件绑定。对于条件渲染、列表渲染等复杂操作,你仍需要编写 C 代码。 + +## 安装开发工具 + +`@lcui/cli` 是一个命令行工具,集成了 TypeScript 编译器、Sass 预处理器、资源文件加载器等功能。它依赖 Node.js 运行时环境: + +```sh +npm install -g @lcui/cli +``` + +## 预处理器工作原理 + +LCUI 采用预处理器方案:TSX 文件只在预处理阶段执行,不会成为运行时代码。你可以理解为 TSX 文件是配置文件,其中的 TypeScript 代码都是预处理指令。 + +预处理器按文件后缀名匹配合适的加载器,解析 TSX 代码、收集依赖、执行组件函数,然后根据返回的 JSX 元素和 Hook 调用结果生成 C 源文件。 + +## 用法概览 + +```tsx title="src/App.tsx" +import { useState, TextInput, Button } from "@lcui/react"; + +export default function App() { + const inputRef = { current: { value: "" } }; + const [name, setName] = useState("LCUI"); + + return ( + + + Hello, {name}! + + + + + ); +} +``` + +## React 库 + +`@lcui/react` 是针对 LCUI 特性和预处理器工作模式的用户界面库,提供预置组件、工具函数和 Hook 函数。 + +安装方法: + +```sh +npm install @lcui/react +``` + +### 组件函数 + +预处理器会执行组件函数,收集 `useState`、`useRef` 等产出的数据和返回的 JSX 元素,然后转换成 C 代码。 + +:::warning +- 组件函数返回值必须是一个 JSX 元素,不能是 ``、null、undefined、字符串、数字等对象。 +- 暂不支持声明和传递组件 props 参数。 +::: + +### 状态管理 + +`useState` 为组件声明状态变量: + +```tsx +import { useState } from "@lcui/react"; + +function MyComponent() { + const [age, setAge] = useState(23); + const [name, setName] = useState("Taylor"); + // ... +} +``` + +`useState` 返回一个数组: + +- 状态变量,初始值为你传给 `useState` 的值。 +- set 函数,允许你在响应交互时更改状态变量。 + +`useState` 会为组件的状态结构体添加成员,并在初始化函数中添加初始化代码: + +```c +/* 组件状态结构体 */ +struct MyComponent_state { + int age; + char *name; +}; + +/* 组件初始化函数中的代码 */ +_that->state.age = 23; +_that->state.name = strdup2("Taylor"); +``` + +状态的 set 函数会在当前作用域中插入 C 代码,例如 `setAge(30)` 会生成 `_that->state.age = 30;`。 + +:::warning +`useState` 的参数只能是 string、number 类型。 +::: + +### 引用 + +用 `$ref` 属性引用部件对象: + +```tsx + +``` + +之后在 C 代码中操作它: + +```c +ui_textinput_set_content(_that->refs.input, "hello"); +``` + +`$ref` 的值也可以是 `useRef` 返回的引用对象: + +```tsx +import { useRef } from "@lcui/react"; + +function MyComponent() { + const inputRef = useRef(); + + return ; +} +``` + +`useRef` 目前只实现了 TextInput 的 value 属性读写绑定: + +```tsx +inputRef.current.value = "World"; + + +``` + +:::warning +`@lcui/react` 的 `useRef` 和 React 的 `useRef` 不同,仅用于引用部件对象。 +::: + +### 响应事件 + +通过 `on + 事件名` 属性声明事件处理函数: + +```tsx + +``` + +预处理器会生成 C 代码绑定事件: + +```c +ui_widget_on(_that->refs.ref_0, "click", handleClick, w); +``` + +也可以将 JavaScript 函数与事件绑定: + +```tsx +import { useState, Text, Button } from "@lcui/react"; + +export default function Counter() { + const [text, setText] = useState("点我"); + + function handleClick() { + setText("你已点击"); + } + + return ( + + ); +} +``` + +预处理器会根据事件处理函数内部的代码执行结果,为其生成 C 语言版本的事件处理函数。 + +:::warning +暂时只支持在事件处理函数中执行状态变量的 set 函数,不支持访问事件对象,不支持执行其它语句。 +::: + +### 条件渲染 + +:::warning +暂不支持。 +我们正考虑使用 `` 组件实现条件渲染,参考 Solid.js 的 ``。 +::: + +### 渲染列表 + +:::warning +暂不支持。 +我们正考虑使用 `` 组件实现数组遍历,参考 Solid.js 的 ``。 +::: diff --git a/docs/zh-CN/handbook/customization.mdx b/docs/zh-CN/handbook/customization.mdx new file mode 100644 index 000000000..e27736db2 --- /dev/null +++ b/docs/zh-CN/handbook/customization.mdx @@ -0,0 +1,136 @@ +# 自定义 + +你可以创建全新的部件类型,也可以在现有部件基础上进行扩展。 + +## 创建自定义部件 + +使用 `ui_create_widget_prototype()` 注册新的部件类型: + +```c title="src/counter.c" +#include + +typedef struct { + int value; +} counter_data_t; + +static ui_widget_prototype_t *counter_proto; + +static void counter_init(ui_widget_t *w) +{ + counter_data_t *data; + ui_widget_t *text; + + data = ui_widget_add_data(w, counter_proto, sizeof(counter_data_t)); + data->value = 0; + + text = ui_create_widget("text"); + ui_text_set_content(text, "0"); + ui_widget_append(w, text); +} + +static void counter_destroy(ui_widget_t *w) +{ + /* 释放自有资源(如有) */ +} + +void register_counter_widget(void) +{ + counter_proto = ui_create_widget_prototype("counter", NULL); + counter_proto->init = counter_init; + counter_proto->destroy = counter_destroy; +} +``` + +使用这个自定义部件: + +```c title="src/main.c" +extern void register_counter_widget(void); + +int main(void) +{ + ui_widget_t *counter; + + lcui_init(); + register_counter_widget(); + + counter = ui_create_widget("counter"); + ui_widget_append(ui_root(), counter); + return lcui_main(); +} +``` + +## 扩展现有部件 + +把父类型名作为 `ui_create_widget_prototype()` 的第二个参数传入: + +```c title="src/icon_button.c" +static ui_widget_prototype_t *icon_button_proto; + +static void icon_button_init(ui_widget_t *w) +{ + /* 调用父类(button)的初始化逻辑 */ + icon_button_proto->proto->init(w); + ui_widget_add_class(w, "icon-button"); +} + +void register_icon_button(void) +{ + icon_button_proto = + ui_create_widget_prototype("icon-button", "button"); + icon_button_proto->init = icon_button_init; +} +``` + +:::warning +**父类的 `init` 必须手动调用。** LCUI 在创建子类型部件时不会自动调用父类型的 `init`。如果你忘记调用 `proto->proto->init(w)`,部件会缺少父类型的基础行为(例如 `button` 不会注册点击事件、不会设置默认样式)。 +::: + +## 部件生命周期 + +`ui_widget_prototype_t` 上有多个回调钩子,覆盖部件的各个生命周期阶段: + +- **`init`** — 部件创建时。负责初始化自有数据、子部件、事件绑定。 +- **`destroy`** — 部件销毁时。负责释放自有内存、解绑事件。 +- **`update`** — 部件需要更新时,接收 `ui_task_type_t` 参数标识更新类型(样式更新、属性更新等)。LCUI 内部在每帧更新循环中调用此钩子。 +- **`setattr`** — 设置 XML 属性时(用于响应属性变更)。 +- **`settext`** — 设置文本内容时。 +- **`sizehint`** — 估算部件在无外部约束时的自然尺寸约束(最小/最大内容尺寸)。布局引擎在计算子部件尺寸时会参考此结果。 +- **`resize`** — 部件尺寸变化时。接收新的内容区域宽高作为参数。 +- **`paint`** — 部件需要重绘时。用于自定义绘制逻辑。 + +## 自定义绘制 + +可以重写 `paint` 回调,在部件上绘制自定义图形: + +```c title="src/my_widget.c" +static void my_widget_paint(ui_widget_t *w, pd_context_t *paint_ctx, + ui_widget_actual_style_t *style) +{ + pd_canvas_t *canvas = paint_ctx->canvas; + pd_color_t color = pd_color_from_rgb(255, 0, 0); + pd_rect_t rect = { style->left, style->top, w->width, w->height }; + + pd_canvas_fill_rect(canvas, color, rect); +} + +static void register_my_widget(void) +{ + ui_widget_prototype_t *proto = + ui_create_widget_prototype("my-widget", NULL); + proto->paint = my_widget_paint; +} +``` + +:::warning +**`paint` 只负责绘制。** 部件尺寸估算由 `sizehint` 处理;不要在 `paint` 里修改部件尺寸或子部件树,否则可能触发无限循环。 +::: + +## 注意事项 + +:::warning +**`destroy` 回调要释放自有资源。** 如果自定义部件在 `init` 里分配了内存、打开了文件、订阅了系统事件(如 `lcui_settings_on_deserialize`),必须在 `destroy` 里做对称释放。 +::: + +:::warning +**自定义部件必须在 `lcui_init()` 之后、首次实例化之前注册。** 在 `main()` 开头立即调用你的 `register_xxx()` 函数是最稳妥的位置,可以确保所有使用方(XML 加载、路由实例化、TSX 编译结果)都能看到原型。 +::: diff --git a/docs/zh-CN/handbook/styling.mdx b/docs/zh-CN/handbook/styling.mdx new file mode 100644 index 000000000..9e87dc9d2 --- /dev/null +++ b/docs/zh-CN/handbook/styling.mdx @@ -0,0 +1,151 @@ +# 样式 + +用 CSS 给部件设置颜色、边框、内外边距、尺寸和布局。 + +## 设置方式 + +### 内联样式 + +用 `ui_widget_set_style_string()` 直接设置单个 CSS 属性: + +```c title="main.c" +ui_widget_t *w = ui_create_widget("text"); + +ui_widget_set_style_string(w, "color", "#336699"); +ui_widget_set_style_string(w, "font-size", "20px"); +ui_widget_set_style_string(w, "padding", "8px 16px"); +``` + +### CSS 类选择器 + +用 `ui_load_css_string()` 或 `ui_load_css_file()` 加载 CSS 规则,再用 `ui_widget_add_class()` 给部件加类名使其生效: + +```c title="main.c" +ui_load_css_file("styles.css"); + +ui_widget_t *w = ui_create_widget("text"); +ui_widget_add_class(w, "title"); +``` + +```css title="app/styles.css" +.title { + font-size: 24px; + font-weight: bold; + margin-bottom: 16px; +} +``` + +也可以用 `ui_load_css_string()` 从字符串加载 CSS 规则,第二个参数是来源标识,用于调试定位冲突规则: + +```c title="main.c" +ui_load_css_string(".title { font-size: 24px; }", "app.css"); +``` + +## 支持的 CSS 特性 + +LCUI 的 CSS 引擎实现了 Web 标准的一个子集。本节列出所有已支持的特性和它们支持的值;**未列出的特性默认不支持**。 + +### At Rules + +- **`@font-face`** — 加载外部字体 + +### 选择器 + +- `*`(通配符)、`type`、`#id`、`.class` +- 伪类 `:hover`、`:focus`、`:active`、`:first-child`、`:last-child` +- 不支持 `!important` + +### 单位 + +- `px`、`dp`、`sp`、`pt`、`%` + +### 属性 + +#### 布局 + +- **`display`** — `none`、`inline-block`、`block`、`flex`、`inline-flex`、`table`、`inline-table`、`table-row`、`table-cell` +- **`position`** — `static`、`relative`、`absolute` +- **`top` / `right` / `bottom` / `left`** — `` 或 `` 或 `auto` +- **`z-index`** — `auto` 或整数 +- **`box-sizing`** — `content-box`、`border-box` + +#### 盒模型 + +- **`width` / `height`** — ``、``、`auto` +- **`min-width` / `max-width` / `min-height` / `max-height`** — 同上 +- **`padding`** — 简写,1-4 个 `` 值(如 `8px`、`4px 8px`、`4px 8px 12px`、`4px 8px 12px 16px`) +- **`padding-top` / `padding-right` / `padding-bottom` / `padding-left`** — 长边属性 +- **`margin`** — 简写,同 padding +- **`margin-top` / `margin-right` / `margin-bottom` / `margin-left`** +- **`border`** — 简写 `1px solid #ccc`(width + style + color) +- **`border-color` / `border-width` / `border-style` / `border-radius`** — 同 padding 语法的多值简写 +- **`border-top` / `border-right` / `border-bottom` / `border-left`** — 单边简写 +- **`border-top-color` / `border-right-color` / `border-bottom-color` / `border-left-color`** +- **`border-top-width` / `border-right-width` / `border-bottom-width` / `border-left-width`** +- **`border-top-style` / `border-right-style` / `border-bottom-style` / `border-left-style`** +- **`border-top-left-radius` / `border-top-right-radius` / `border-bottom-left-radius` / `border-bottom-right-radius`** +- **`border-style` 可取值** — `none`、`solid` +- **`table-layout`** — `auto`、`fixed` +- **`border-spacing`** — `{1,2}` + +#### 背景 + +- **`background`** — 简写形式 `bg-image || bg-position || bg-size || repeat-style || color`(仅单图层) +- **`background-color`** — `` +- **`background-image`** — `none` 或 `` +- **`background-position`** — x y 两个值 +- **`background-position-x` / `background-position-y`** +- **`background-repeat`** — `` +- **`background-size`** — `` +- **`background-clip`** — `border-box`、`padding-box`、`content-box` + +#### Flexbox 布局 + +- **`flex`** — 简写(`flex-grow` + `flex-shrink` + `flex-basis`) +- **`flex-shrink` / `flex-grow` / `flex-basis`** +- **`flex-wrap`** — `nowrap`、`wrap` +- **`flex-direction`** — `row`、`column` +- **`justify-content`** — `flex-start`、`center`、`flex-end` +- **`align-items`** — `flex-start`、`center`、`flex-end`、`stretch` +- **`align-content`** — 同 justify-content 加 `space-between`、`space-around`、`space-evenly` +- **`gap`** — 简写,同时设 row-gap + column-gap +- **`row-gap` / `column-gap`** — `normal` 或 `` + +#### 排版 + +- **`font-face`** — (通过 `@font-face` 规则加载字体) +- **`font-family`** — ``(建议用内置别名如 `monospace`) +- **`font-size`** — `` +- **`font-style`** — `normal`、`italic`、`oblique` +- **`font-weight`** — `normal`、`bold`、`` +- **`text-align`** — `left`、`center`、`right` +- **`line-height`** — `normal` 或 `` 或 `` +- **`color`** — `` +- **`white-space`** — `normal`、`nowrap` +- **`word-break`** — `normal`、`break-all` +- **`content`** — `` 或 `none` + +#### 其他 + +- **`opacity`** — `` 或 `` +- **`visibility`** — `visible`、`hidden` +- **`pointer-events`** — `auto`、`none` +- **`box-shadow`** — `none` 或 ``(格式 `{2,4} && ?`) + +## 注意事项 + +:::warning +**`white-space: pre` 无效。** LCUI 的 `white-space` 仅实现 `normal` 和 `nowrap`。如需保留源代码前导缩进,把空格换成 U+00A0 (NBSP)。 +::: + +:::warning +**`overflow` 不裁切内容。** 要裁切或滚动子内容,改用 `scrollarea` 内置部件。 +::: + +:::warning +**`background` 简写不支持多图层。** 写 `background: url(a.png) no-repeat, url(b.png) center;` 这种多背景语法不会生效。请拆成 `background-image` / `background-position` 等分属性逐个设置。 +::: + +:::warning +**CSS 不支持继承。** 像 `color`、`font-family`、`font-size` 不会从父部件流向子部件。每个 `text` / `button` 等需要文本显示的部件都必须显式设置这些属性。 +::: diff --git a/docs/zh-CN/overview/about.mdx b/docs/zh-CN/overview/about.mdx new file mode 100644 index 000000000..d75dc65ee --- /dev/null +++ b/docs/zh-CN/overview/about.mdx @@ -0,0 +1,59 @@ +# 关于 LCUI + +LCUI 是一个用 C 语言编写的开源桌面图形界面库,旨在为 C 开发者提供简洁易用的 GUI 开发体验,同时融入 CSS 样式、声明式界面描述等 Web 开发技术降低学习门槛。 + +## 主要特性 + +- **跨平台** — 支持 Windows 和 Linux。 +- **全自绘组件** — 组件在多个平台中都能保持一致的外观和行为。 +- **DPI 自适应** — 自动在高分辨率屏幕上缩放 UI,保持清晰显示。 +- **自带 CSS 引擎** — 支持使用 CSS 来定义用户界面的样式和布局,对于有网页开发经验的人比较容易上手。 +- **提供现代化的开发工具** — 通过 `@lcui/cli` 工具,允许你使用 TypeScript 语言搭配 JSX 语法来编写用户界面。 + +## 适合谁使用 + +LCUI 适合这些开发者: + +- 想在桌面端继续使用 C 语言,但希望摆脱繁琐的 Win32 / X11 原生开发体验。 +- 已经熟悉 Web 前端(HTML、CSS),想把这套经验迁移到桌面应用。 +- 需要构建单窗口、界面内容简单的桌面小工具。 + +如果场景是大型商业桌面产品、游戏引擎或与操作系统深度集成的工具,LCUI 可能不是最合适的选择;这种场景下建议参考 Qt、GTK 或原生 API。 + +## 架构 + +LCUI 从上到下分为四层: + +### 应用层 + +你的业务代码:自定义组件、CSS 样式、TSX/JSX 代码、事件处理。这是唯一与你的项目领域相关的部分。 + +### LCUI 运行时 + +初始化、事件循环、应用生命周期管理。负责把应用跑起、派发事件、驱动渲染。 + +### UI 辅助层 + +UI XML 解析、UI Router 路由、光标管理、国际化(i18n)等辅助模块。把常用 UI 行为封装成可插拔的子系统。 + +### 基础设施层 + +YUtil(通用工具库)、PandaGL(2D 渲染引擎)、CSS 引擎、UI 部件系统、Thread / Worker 抽象。这些模块也可以单独使用。 + +平台层(Windows / Linux)负责窗口管理与输入事件。 + +## 许可证 + +LCUI 基于 MIT License 发布。详情请查看仓库根目录的 LICENSE.TXT。 + +## 如何参与贡献 + +欢迎贡献!在提交 Pull Request 之前,请先阅读仓库根目录的 CONTRIBUTING.md。 + +- **Bug 反馈** — 在 GitHub Issues 提交问题。 +- **功能建议** — 在 GitHub Discussions 发起讨论。 +- **代码贡献** — Fork 仓库,创建分支,然后提交 PR。 + +## 社区 + +遇到使用问题时,可以通过 GitHub Discussions 提问(建议使用 `Q&A` 标签)。同时我们也鼓励资深用户给新人提供帮助。 diff --git a/docs/zh-CN/overview/quick-start.mdx b/docs/zh-CN/overview/quick-start.mdx new file mode 100644 index 000000000..93c06b875 --- /dev/null +++ b/docs/zh-CN/overview/quick-start.mdx @@ -0,0 +1,44 @@ +# 快速开始 + +几分钟内上手 LCUI。 + +## 环境要求 + +- **操作系统** — Windows(推荐)或 Linux +- **Node.js** — 运行 `@lcui/cli` 工具 +- **xmake** — C/C++ 构建工具 +- **Git** — 下载和管理源码 + +## 安装 + +全局安装 LCUI CLI: + +```sh +npm install -g @lcui/cli +``` + +创建并运行你的第一个应用: + +```sh +lcui create my-app +cd my-app +lcui build +xmake run app +``` + +`lcui create` 会从 lcui-quick-start 模板仓库克隆一个最小项目,包含 xmake.lua 配置、示例代码和依赖。完成后运行 `lcui build` 编译 TSX 资源,再运行 `xmake run app` 启动应用。 + +## 下一步 + +- 查看样板项目(`lcui create` 生成的项目)中的代码和文件结构,了解 LCUI 应用的基本用法。 +- 阅读组件参考(Button、Text、TextInput、Anchor、ScrollArea),了解内置部件。 +- 阅读样式章节,学习如何用 CSS 为部件添加外观。 +- 阅读自定义章节,学习如何创建自定义部件。 +- 阅读 TSX 与开发工具章节,深入理解 `@lcui/react` 的用法和限制。 + +## 遇到问题? + +如果你在学习过程中遇到问题,可以通过以下途径获取帮助: + +- 在 GitHub Discussions 提问(建议使用 `Q&A` 标签)。 +- 在仓库根目录查看 CONTRIBUTING.md,了解如何参与贡献。 diff --git a/docs/zh-CN/widgets/anchor.mdx b/docs/zh-CN/widgets/anchor.mdx new file mode 100644 index 000000000..421e5c41a --- /dev/null +++ b/docs/zh-CN/widgets/anchor.mdx @@ -0,0 +1,20 @@ +# Anchor + +链接部件,用法与 HTML 的 `` 标签相同,用于打开外部 URL。 + + + +## 适用场景 + +- **适用** — 在页面中放置外部链接(点击后在系统浏览器中打开)。 +- **不适用** — 需要执行动作的按钮(用 `button`)。 + +## 用法 + +```tsx +链接 +``` + +## API 参考 + + diff --git a/docs/zh-CN/widgets/button.mdx b/docs/zh-CN/widgets/button.mdx new file mode 100644 index 000000000..0273b1a92 --- /dev/null +++ b/docs/zh-CN/widgets/button.mdx @@ -0,0 +1,25 @@ +# Button + +可点击的按钮部件,通常用于触发某个操作。 + + + +## 适用场景 + +- **适用**:用户点击执行动作(提交表单、打开页面、切换状态) +- **适用**:工具栏、对话框、导航栏中的交互入口 +- **不适用**:不需要交互的纯文本展示(用 `text`) + +## 用法 + +```tsx +import { Button } from "@lcui/react" +``` + +```tsx + +``` + +## API 参考 + + diff --git a/docs/zh-CN/widgets/checkbox.mdx b/docs/zh-CN/widgets/checkbox.mdx new file mode 100644 index 000000000..b349eb45f --- /dev/null +++ b/docs/zh-CN/widgets/checkbox.mdx @@ -0,0 +1,37 @@ +# Checkbox + +允许用户在已选和未选之间切换的控件。 + + + +## 适用场景 + +- **适用**:单条二元选择(同意条款、订阅开关) +- **适用**:表单中的勾选项 +- **不适用**:互斥的多选一场景(用 `radio-group`) + +## 用法 + +```tsx +import { Checkbox } from "@lcui/react" +``` + +```tsx + +``` + +## 注意事项 + +:::caution +**indeterminate 是状态,不是默认值**。半选态(`indeterminate="true"`)用于表达"子项部分被选中"的视觉状态(例如父级 checkbox 在部分子项被选时显示半选)。点击 indeterminate 状态的 checkbox 会转为 checked 状态。 +::: + +## 示例 + +### 禁用 + + + +## API 参考 + + \ No newline at end of file diff --git a/docs/zh-CN/widgets/label.mdx b/docs/zh-CN/widgets/label.mdx new file mode 100644 index 000000000..a931f3d30 --- /dev/null +++ b/docs/zh-CN/widgets/label.mdx @@ -0,0 +1,25 @@ +# Label + +渲染与控件关联的可访问标签,点击时将 click 事件转发到 `for` 指向的目标部件。 + + + +## 适用场景 + +- **适用**:为 checkbox、text-input 等可交互控件提供点击区域扩展(点击文字也能触发控件) +- **适用**:辅助功能(a11y)中将文本与控件关联 +- **不适用**:纯展示文本(用 `text` 部件) + +## 用法 + +```tsx +import { Label } from "@lcui/react" +``` + +```tsx + +``` + +## API 参考 + + \ No newline at end of file diff --git a/docs/zh-CN/widgets/progress.mdx b/docs/zh-CN/widgets/progress.mdx new file mode 100644 index 000000000..7f8b39f76 --- /dev/null +++ b/docs/zh-CN/widgets/progress.mdx @@ -0,0 +1,25 @@ +# Progress + +用于展示任务进度(如文件上传、加载状态)的线性进度条部件。 + + + +## 适用场景 + +- **适用**:文件上传/下载、安装、加载等长时间任务的进度展示 +- **适用**:表单提交、异步操作的等待反馈 +- **不适用**:需要精确数值或不确定进度的场景(用 `text` 自行绘制) + +## 用法 + +```tsx +import { Progress } from "@lcui/react" +``` + +```tsx + +``` + +## API 参考 + + diff --git a/docs/zh-CN/widgets/radio-group.mdx b/docs/zh-CN/widgets/radio-group.mdx new file mode 100644 index 000000000..40b7c7433 --- /dev/null +++ b/docs/zh-CN/widgets/radio-group.mdx @@ -0,0 +1,58 @@ +# RadioGroup + +一组互斥的单选按钮,同一时刻最多只有一项被选中。 + + + +## 适用场景 + +- **适用**:从一组互斥的选项中选出唯一一个(视图密度、排序方式、单选枚举等) +- **适用**:选项数量固定为 2~7 个的简单场景 +- **不适用**:需要并选多个选项的场景(用 `checkbox`) +- **不适用**:选项数量大到需要搜索或筛选(暂未提供 `select`) + +## 用法 + +```tsx +import { RadioGroup, RadioGroupItem } from "@lcui/react" +``` + +```tsx + + + + + +``` + +## 组合 + +使用以下组合来构建 `RadioGroup`: + +``` +RadioGroup +├── RadioGroupItem +└── RadioGroupItem +``` + +通常每项会与一个 `Label` 搭配放置在同一行 flex 容器里,Label 的 `for` 指向 RadioGroupItem 的 `id`,点击 Label 会把 click 事件转发到对应的 item。 + +## 示例 + +### 禁用 + + + +## API 参考 + +### RadioGroup + +单选组容器,管理组内各项的互斥选中状态。 + + + +### RadioGroupItem + +单选项,每一项代表一个可选值。同一 RadioGroup 内同一时刻最多只有一项处于 checked 状态。 + + \ No newline at end of file diff --git a/docs/zh-CN/widgets/scrollarea.mdx b/docs/zh-CN/widgets/scrollarea.mdx new file mode 100644 index 000000000..dc9d9a415 --- /dev/null +++ b/docs/zh-CN/widgets/scrollarea.mdx @@ -0,0 +1,63 @@ +# ScrollArea + +当子内容超出容器范围时,提供滚动能力的容器部件。 + + + +## 适用场景 + +- **适用** — 内容高度或宽度可能超过容器(长列表、文档、日志)。 +- **适用** — 需要滚动条可视化反馈的场景。 +- **不适用** — 内容总是小于容器(直接用普通 `widget` 即可)。 + +## 用法 + +```tsx +import { ScrollArea, ScrollAreaContent, Scrollbar } from "@lcui/react" +``` + +```tsx + + {/* 子内容 */} + + + +``` + +## 组合 + +使用以下组合来构建 `ScrollArea`: + +``` +ScrollArea +├── ScrollAreaContent +└── Scrollbar +``` + +## 示例 + +### 双向滚动 + + + +当内容需要在水平和垂直两个方向上滚动时,同时设置横向和纵向滚动条。 + +## API 参考 + +### ScrollArea + +滚动容器,负责裁剪超出范围的子内容并接收滚动事件。 + + + +### ScrollAreaContent + +滚动内容容器,承载实际的子部件。必须作为 ScrollArea 的直接子部件使用。 + + + +### Scrollbar + +滚动条部件,用于显示并操作滚动条。作为 ScrollArea 的直接子部件使用。 + + diff --git a/docs/zh-CN/widgets/text-input.mdx b/docs/zh-CN/widgets/text-input.mdx new file mode 100644 index 000000000..568bd13eb --- /dev/null +++ b/docs/zh-CN/widgets/text-input.mdx @@ -0,0 +1,24 @@ +# TextInput + +可编辑的文本输入部件。 + + + +## 适用场景 + +- **适用** — 用户输入文本(表单字段、搜索框)。 +- **不适用** — 仅需要展示文本(用 `text`)。 + +## 用法 + +```tsx +import { TextInput } from "@lcui/react" +``` + +```tsx + +``` + +## API 参考 + + diff --git a/docs/zh-CN/widgets/text.mdx b/docs/zh-CN/widgets/text.mdx new file mode 100644 index 000000000..72e84e4d8 --- /dev/null +++ b/docs/zh-CN/widgets/text.mdx @@ -0,0 +1,24 @@ +# Text + +用于显示文本的部件。 + + + +## 适用场景 + +- **适用** — 静态文本显示(标题、标签、说明文字)。 +- **不适用** — 需要用户编辑文本(用 `textinput`)。 + +## 用法 + +```tsx +import { Text } from "@lcui/react" +``` + +```tsx +Hello +``` + +## API 参考 + + diff --git a/examples/doc-viewer/.gitignore b/examples/doc-viewer/.gitignore new file mode 100644 index 000000000..bbac78cfb --- /dev/null +++ b/examples/doc-viewer/.gitignore @@ -0,0 +1,19 @@ +node_modules/ +build/ +dist/ +.lcui/ +.xmake/ + +# lcui-cli generated artifacts: +# *.tsx.h and *.css.h are emitted alongside source files by lcui build. +# *.mjs is the transpiled module output (kept under .lcui/build/). +# Per-widget *.h next to a TSX file is also emitted by lcui-cli. +*.tsx.h +*.css.h +*.json.mjs +app/widgets/*/*/index.h +app/examples/*/index.h +app/main.h +app/components/route-titles.h +app/components/code-snippets.h +app/components/code-snippets.c \ No newline at end of file diff --git a/examples/doc-viewer/AGENTS.md b/examples/doc-viewer/AGENTS.md new file mode 100644 index 000000000..053a70e99 --- /dev/null +++ b/examples/doc-viewer/AGENTS.md @@ -0,0 +1,319 @@ +# doc-viewer 文档编写规范 + +本文档为 AI agent 和人类贡献者编写 doc-viewer 项目文档源文件时的指令参考。 + +## 目录结构 + +``` +docs/ + sidebars.json # 导航栏数据 + widget-fields/ + widget.ts # FieldData 接口 + 公共字段 + {name}.ts # 各组件专属字段 + {locale}/widgets/{name}.mdx # 页面源文件 + examples/ + {widget}-basic/main.c # C 语言 demo + {widget}-basic-xml/main.c ui.xml # XML 变体 + {widget}-basic-tsx/main.c example.tsx # TSX 变体 +examples/doc-viewer/ + scripts/compiler/ # MDX → page.tsx 编译管线 +``` + +## Widget 页面模板 + +每个 widget 的 MDX 文件**必须**按以下固定顺序编写: + +```mdx +# WidgetName + +一句话功能简介。 + + + +## 适用场景 + +- **适用**:... +- **不适用**:... + +## 用法 + +\`\`\`tsx +import { WidgetName } from "@lcui/react" +\`\`\` + +\`\`\`tsx + +\`\`\` + +## 组合(可选) + +使用以下组合来构建 `WidgetName`: + +\`\`\` +WidgetName +├── SubComponent +└── SubComponent +\`\`\` + +## API 参考 + + +``` + +### 各小节说明 + +#### # 标题与简介 +- H1 标题:组件名(英文,PascalCase) +- 紧跟一段功能描述(一至两句话) + +#### WidgetExample(demo 嵌入) +- 紧跟简介后,放在任何 `##` 之前 +- `name` 属性对应 `docs/examples/` 目录名(不含 `-tsx` / `-xml` 后缀) +- 一个 widget 页面放一个 basic 示例 + +#### ## 适用场景 +- 每个列表项以 `- **适用**:` 或 `- **不适用**:` 开头 +- 中文用全角冒号 `:`,英文用半角冒号 `: ` + +#### ## 用法(Usage) +- 两个代码块,均为 ` ```tsx `(无 title 属性) +- 第一个代码块:import 语句 +- 第二个代码块:组件核心用法(无需构造完整函数实现) +- 代码块**前**无需引导语 + +#### ## 组合(Composition)—— 仅多部件组件需要 +- 仅当组件包含子部件(如 ScrollArea 由 ScrollAreaContent + Scrollbar 组成)时才写此小节,单组件省略 +- 代码块标签:` ``` `(无语言标记,纯文本块) +- 内容:用树形结构展示组件层次关系(`├──` / `└──`) +- 代码块**前**写一行引导语:`使用以下组合来构建 `WidgetName`:`(英文:`Use the following composition to build a `WidgetName`:`) +- 代码块后**禁止**写"X 由 Y + Z 组成"等内部实现说明 +- **可以**写行为/模式描述(如 Anchor 的 URL 与 XML 视图模式区别) + +#### ## 示例(可选) +- 用 `###` 三级标题分组 +- 每个示例一段代码(c / xml / tsx),须带 `title="..."` 元数据 +- 放在 API 参考之前或之后均可 + +#### ## API 参考 +- 只用 ``,编译器自动从 `docs/widget-fields/{name}.ts` 读取字段数据 +- 不手写 API 小节、属性列表或事件列表 +- **多部件组件**:每个 FieldTable 前必须用 `### ` 三级标题 + 一句话描述(参考 `docs/zh-CN/widgets/scrollarea.mdx`)。zh-CN 模板: + ```mdx + ## API 参考 + + ### RadioGroup + + 单选组容器,管理组内各项的互斥选中状态。 + + + + ### RadioGroupItem + + 单选项,每一项代表一个可选值。 + + + ``` + en 模板:标题 `## API Reference`,子标题 PascalCase,描述同步翻译。 + +#### ## 注意事项(可选) +- 每个注意点独立一个 admonition 容器 +- 容器类型:`:::note` / `:::info` / `:::tip` / `:::caution` / `:::warning` +- 容器内首句加粗:`:::warning` 包裹 `**核心警告**。详细补充...` + +#### ## 内联样式标签(可选) +- 仅支持 BBCode 标签的组件(Text、TextInput)需要此小节 + +## 非 Widget 页面模板 + +overview、handbook 等页面结构更自由,仅要求: +1. H1 标题 + 简介段落 +2. `##` 小节自由组织 +3. 示例代码块须带 `title="..."` 元数据 + +## 文档工作流 + +以中文文档为主,英文文档在中文文档更新完毕后再全量翻译。 + +1. **新增组件文档**:先在 `docs/zh-CN/widgets/{name}.mdx` 完成全部内容。 +2. **修改/补充文档**:同样先改 zh-CN,确认章节结构、示例、描述完整后再同步。 +3. **同步英文**:对照 zh-CN 版本逐节翻译到 `docs/en/widgets/{name}.mdx`。 + - 保留 `` / `` / 代码块 / admonition 标签原样 + - 仅翻译普通文本(标题、段落、list item、admonition 内容) + - 代码注释无需翻译 +4. **不要双语并行开发**:zh-CN 未定稿时不改 en,避免两边结构漂移。 + +## MDX 语法参考 + +| 语法 | 渲染效果 | +|------|----------| +| `# H1` | 页面标题 | +| `## H2` | 节标题 | +| `### H3` / `#### H4` | 节内子标题 | +| `**bold**` | `[b]...[/b]` | +| `*italic*` | `[i]...[/i]` | +| `` ``code`` `` | `[bgcolor=#eee] code [/bgcolor]` | +| ` ```lang title="x" ` | 代码块(带 header + Copy 按钮) | +| ` ```lang title="" ` | 代码块(无 header,仅 Copy 按钮) | +| ` ```lang ` | 代码块(带语言名 header + Copy 按钮) | +| `- item` 或 `* item` | 无序列表 | +| `1. item` | 有序列表 | +| `| 表格 |` | flexbox 模拟表格 | +| `:::note` / `info` / `tip` / `caution` / `warning` | admonition 容器 | +| `` | 嵌入 demo | +| `` | 嵌入 API 参考表 | + +## Demo 示例文件 + +每个示例是一个目录: + +``` +docs/examples/{name}/ + main.c +docs/examples/{name}-xml/ + main.c + ui.xml +docs/examples/{name}-tsx/ + main.c + example.tsx +``` + +- **三变体必齐**:C、XML、TSX 三种实现都要提供。即使 widget 暂未在 `@lcui/react` 中导出,也要给出 TSX 变体作为前瞻占位(example.tsx 仅作为代码块展示,不会实际编译)。 +- **TSX 优先展示**:编译器按 `tsx > xml > c` 排序变体,TSX 是默认激活的标签。 +- C 变体的 main.c 须定义 `{widget}_{variant}_init(ui_widget_t *parent)` +- TSX 变体的 main.c 须 `#include "example.h"` 并调用 `example_load()` +- 编译器自动用 highlight.js 高亮并提取 local symbols +- **`` 是聚合头**,已包含 ``、``、``、`` 等。Demo 的 main.c **只能**写 `#include `,不要重复 include 子头。 + +## widget-fields 数据 + +```typescript +interface FieldData { + name: string; + type: string; + default: string; // "-" 表示必填 + description: { + en: string; + "zh-CN": string; + }; +} +``` + +- `docs/widget-fields/widget.ts`:公共字段(className、children),所有组件通过 `...widgetFields` 继承 +- `docs/widget-fields/{name}.ts`:用 `...widgetFields` 展开后追加组件专属字段 +- 字段数据来源:lcui-toolkit types.d.ts 中的 props 类型信息,现阶段手动维护 + +## 中英对照 + +| zh-CN | en | +|-------|-----| +| 适用场景 | Use cases | +| 用法 | Usage | +| 组合 | Composition | +| API 参考 | API Reference | +| 示例 | Examples | +| 注意事项 | Caveats | +| 内联样式标签 | Inline style tags | +| 使用以下组合来构建 `WidgetName`: | Use the following composition to build a `WidgetName`: | + +### FieldTable labels + +| zh-CN | en | +|-------|-----| +| 属性名 | Prop | +| 类型 | Type | +| 默认值 | Default | +| 描述 | Description | + +## 构建验证 + +```bash +bun run compile # 全量编译 MDX → page.tsx +bun run compile --watch # 监听变更 +npx lcui build app --force # 重生成 C 层 .tsx.h / main.h +xmake build doc-viewer # 编译链接 +xmake run doc-viewer # 运行验证 +``` + +每次修改 MDX 或 widget-fields 后,须依次运行: + +```bash +bun run compile +npx lcui build app --force +xmake build doc-viewer +``` + +> ⚠️ `npx lcui build app --force` 输出末尾的 `unknown target(lcui) for doc-viewer.deps!` 警告无害,是已知问题,可忽略。`bun run compile` 自动复制 `widget-fields/` 并触发页面生成;任何一步失败都会产生连锁错误。 + +## Demo 变体命名约定 + +`docs/examples/` 下的 demo 目录名是文档里 `` 的 `name` 属性值: + +| 变体后缀 | 含义 | 内容 | +|---------|------|------| +| (无后缀) | 基础 / 唯一示例 | `main.c` 实现 `void _basic_init(ui_widget_t *parent)` | +| `-xml` | XML 标记声明式 | `ui.xml` + `main.c`(`lcui_init` + `ui_load_xml_file`) | +| `-tsx` | TSX 声明式 | `example.tsx` + `main.c`(`lcui_init` + `example_load()`) | +| `-` | 特性示例 | 镜像 `-tsx` 三件套结构,如 `-disabled`、`-dual-scrollbars` | + +**三变体必齐**:C、XML、TSX 三种实现都要提供。即使 widget 暂未在 `@lcui/react` 中导出,也要给出 TSX 变体作为前瞻占位(`example.tsx` 仅作为代码块展示,不会实际编译)。 + +## widget-fields 文件命名与目录复制陷阱 + +`` 解析为 `docs/widget-fields/radio-group-item.ts`。**文件名必须用 `-`(dash)**,不能用 `_`(underscore)。命名约定是 slug 原文,camelCase 变量名(如 `radioGroupItemFields`)由文件作者自己定。 + +⚠️ **`examples/doc-viewer/app/widget-fields/` 是编译时副本,不会自动清理**: + +`scripts/compiler/index.ts:55` 的 `copyWidgetFields()` 只 `copyFileSync` 不删除。如果在 `docs/widget-fields/` 重命名或删除文件,旧的 `app/widget-fields/.ts` 会残留。下次编译时编译器可能: + +- 找不到新名(如果旧文件还在 dest)→ 报 `ModuleLoaderError: File does not exist` +- 引用错文件(如果同名但内容已变) + +**修正步骤**:删 `examples/doc-viewer/app/widget-fields/` 下 stale 文件,或 `git clean -fd examples/doc-viewer/app/widget-fields` 强制重生成。 + +## Demo 预览容器布局陷阱 + +doc-viewer 的 demo preview 区域本身是 flex 容器,所以**多个独立 flex 行直接 append 到 parent 会横排**。需要在 C/XML demo 外层包一个 `display:flex; flex-direction:column; gap:8px` 容器: + +```c +ui_widget_t *box = ui_create_widget(NULL); +ui_widget_set_style_string(box, "display", "flex"); +ui_widget_set_style_string(box, "flex-direction", "column"); +ui_widget_set_style_string(box, "gap", "8px"); +ui_widget_append(box, row1); +ui_widget_append(box, row2); +ui_widget_append(parent, box); +``` + +XML 用外层 `
` 包裹。TSX 用 `...` 包裹。 + +RadioGroup / ScrollArea / Field 等本身是垂直容器,item 直接 append 进 group 即可,无需外层 wrapper。 + +## 生成的 `index.c` 在 Linux 上的 `static` 不一致问题 + +`lcui build app --force` 对 `examples/doc-viewer/app/examples//index.c` +使用 skeleton-once 策略:只在文件不存在时写入,后续构建保留手写改动。 +首次生成的 `index.c` 中 `_demo_update` 函数被声明为 `static`, +但同名的 `_demo_update(ui_widget_t *)` 在对应 `index.h` 里是非 +`static` 的全局声明。MSVC 默认不报警告;GCC/Clang 在开启 `-Wall` +或项目使用 `set_warnings("all", "error")`(LCUI 在 Ubuntu 上的 +默认配置)时会因声明/定义不一致而**编译失败**。 + +**触发场景**:每次新增 demo(含 `WidgetExample` 引用)或首次为新 +demo 生成 `index.c` 后。 + +**手动修复**:在生成的 `index.c` 里删除 `static` 关键字: + +```c +// before +static void checkbox_disabled_demo_update(ui_widget_t *w) { ... } + +// after +void checkbox_disabled_demo_update(ui_widget_t *w) { ... } +``` + +⚠️ 只去掉 `_demo_update` 上的 `static`,**保留** `_demo_init` / +`_demo_destroy` 上的 `static`——这两个没有对应 header 声明,是 +prototype 内部回调,加 `static` 是正确的。 + +详细说明见 `~/.config/opencode/skills/lcui-cli/SKILL.md` 的 +"Known limitations / open bugs" 节。 diff --git a/examples/doc-viewer/app/components/code-block-copy.c b/examples/doc-viewer/app/components/code-block-copy.c new file mode 100644 index 000000000..d87a4b6bb --- /dev/null +++ b/examples/doc-viewer/app/components/code-block-copy.c @@ -0,0 +1,119 @@ +/* + * app/components/code-block-copy.c - Click handler for the Copy + * button rendered by code-block-copy.tsx. + * + * The button lives inside `.code-block-body`, which itself sits + * under the demo wrapper `.code-block` (demo case) or directly + * under `.code-block` (mdx case). On click, the handler checks + * whether the body widget has a `data-source` attribute (demo case: + * the attribute is on the body); if not, it walks up to the + * `.code-block` ancestor and uses its `data-source`. The raw UTF-8 + * source is then looked up in the generated code-snippets table + * and written as wide characters to the system clipboard. + * + * After a successful copy the `copied` class is added to the button + * widget and a 1.5s timeout removes it again. global.css flips the + * visibility of the inner `.copy-icon` / `.check-icon` based on that + * class, so callers see the checkmark until the timer fires. + * Back-to-back clicks (same or different button) cancel any pending + * revert before starting a new timer, so the feedback always lines + * up with the most recent action. + */ + +#include +#include +#include +#include +#include "code-block-copy.tsx.h" +#include "code-block-copy.h" +#include "code-snippets.h" + +typedef struct { + code_block_copy_react_t base; +} code_block_copy_t; + +typedef struct { + ui_widget_t *button; + int timer_id; +} copy_feedback_t; + +static copy_feedback_t g_feedback; + +static ui_widget_t *find_ancestor_with_class(ui_widget_t *w, const char *cls) +{ + while (w) { + if (ui_widget_has_class(w, cls)) { + return w; + } + w = w->parent; + } + return NULL; +} + +static void revert_copied_class(void *arg) +{ + copy_feedback_t *fb = arg; + if (fb->button) { + ui_widget_remove_class(fb->button, "copied"); + } + fb->button = NULL; + fb->timer_id = 0; +} + +static void code_block_copy_on_click(ui_widget_t *w, ui_event_t *e, void *arg) +{ + ui_widget_t *block; + const char *id; + const char *src; + size_t len; + size_t wlen; + wchar_t *wbuf; + + block = find_ancestor_with_class(w, "code-block-body"); + id = ui_widget_get_attr(block, "data-source"); + if (!id) { + block = find_ancestor_with_class(w, "code-block"); + id = ui_widget_get_attr(block, "data-source"); + } + src = code_snippet_for(id); + + len = strlen(src); + wbuf = malloc(sizeof(wchar_t) * (len + 1)); + wlen = decode_utf8(wbuf, src, len + 1); + ptk_clipboard_set_text(wbuf, wlen); + free(wbuf); + + if (g_feedback.timer_id) { + ptk_clear_timeout(g_feedback.timer_id); + if (g_feedback.button && g_feedback.button != w) { + ui_widget_remove_class(g_feedback.button, "copied"); + } + } + ui_widget_add_class(w, "copied"); + g_feedback.button = w; + g_feedback.timer_id = + ptk_set_timeout(1500, revert_copied_class, &g_feedback); +} + +static void code_block_copy_init(ui_widget_t *w) +{ + ui_widget_add_data(w, code_block_copy_proto, sizeof(code_block_copy_t)); + code_block_copy_react_init(w); +} + +static void code_block_copy_destroy(ui_widget_t *w) +{ + code_block_copy_react_destroy(w); +} + +ui_widget_t *ui_create_code_block_copy(void) +{ + return ui_create_widget_with_prototype(code_block_copy_proto); +} + +void ui_register_code_block_copy(void) +{ + code_block_copy_init_prototype(); + code_block_copy_proto->init = code_block_copy_init; + code_block_copy_proto->destroy = code_block_copy_destroy; +} diff --git a/examples/doc-viewer/app/components/code-block-copy.h b/examples/doc-viewer/app/components/code-block-copy.h new file mode 100644 index 000000000..b642b1a1d --- /dev/null +++ b/examples/doc-viewer/app/components/code-block-copy.h @@ -0,0 +1,9 @@ +#include + +void ui_register_code_block_copy(void); + +ui_widget_t *ui_create_code_block_copy(void); + +void code_block_copy_update(ui_widget_t *w); + +void ui_load_code_block_copy_resources(void); diff --git a/examples/doc-viewer/app/components/code-block-copy.tsx b/examples/doc-viewer/app/components/code-block-copy.tsx new file mode 100644 index 000000000..9af9cbd53 --- /dev/null +++ b/examples/doc-viewer/app/components/code-block-copy.tsx @@ -0,0 +1,14 @@ +import { Widget } from "@lcui/react"; +import { Checkmark, Copy } from "@lcui/fluent-icons"; + +export default function CodeBlockCopy() { + return ( + + + + + ); +} diff --git a/examples/doc-viewer/app/components/demo-provider.c b/examples/doc-viewer/app/components/demo-provider.c new file mode 100644 index 000000000..9300a30d1 --- /dev/null +++ b/examples/doc-viewer/app/components/demo-provider.c @@ -0,0 +1,226 @@ +/* + * app/components/demo-provider.c - Shared interactive behaviour for + * code-demo widgets. + * + * One DemoProvider is mounted as the parent prototype of every + * `app/examples//index.tsx` demo (lcui-cli emits + * `ui_create_widget_prototype("..._index", "demo_provider")`). + * As the parent prototype, the `init` / `destroy` callbacks declared + * here apply to every demo automatically; the per-example index.c only + * needs to wire up its preview widget. + * + * The widget tree authored in TSX looks like: + * + * -> class="demo" + * -> class="demo-preview" + * -> wrapper card + * -> flex row + * -> inline-block file tabs + * + * + * -> inline-block lang tabs + * + * + * -> single copy button + * + * * + * + * ... more (language, file) bodies as flat siblings ... + * + * + * + * State-driven model: + * 1. Click / ready handlers write to `that->language` and + * `that->file` (the single source of truth), then call + * `demo_provider_update(w)`. + * 2. `demo_provider_update` reads state and renders the DOM: + * - Step 1: activate the language tab matching state. + * - Step 2: show/hide file tabs by language, activate the file + * tab matching state. When the file doesn't match any + * visible tab, fall back to the first file of the current + * language and write back into `that->file` so the state + * always reflects the UI truth. + * - Step 3: show the code-block body matching (language, file), + * hide the rest. + * + * Pointer ownership: `language` and `file` point at tab widget + * `data-value` attributes. LCUI widget attributes are stable for + * the widget lifetime, so no copy is needed. + */ + +#include +#include "demo-provider.tsx.h" +#include "demo-provider.h" + +typedef struct { + demo_provider_react_t base; + const char *language; + const char *file; +} demo_provider_t; + +/* -- helpers ------------------------------------------------------------- */ + +static ui_widget_t *find_child_by_class(ui_widget_t *parent, const char *cls) +{ + ui_widget_t *child = ui_widget_get_child(parent, 0); + + while (child) { + if (ui_widget_has_class(child, cls)) { + return child; + } + child = ui_widget_next(child); + } + return NULL; +} + +/* -- state-driven renderer ----------------------------------------------- */ + +void demo_provider_update(ui_widget_t *w) +{ + demo_provider_t *that = ui_widget_get_data(w, demo_provider_proto); + const char *language = that->language; + const char *file = that->file; + ui_widget_t *wrapper; + ui_widget_t *header; + ui_widget_t *languages; + ui_widget_t *files; + ui_widget_t *child; + ui_widget_t *first_for_lang = NULL; + int file_matched = 0; + + demo_provider_react_update(w); + + /* Step 0: locate containers. */ + wrapper = find_child_by_class(w, "code-block"); + header = find_child_by_class(wrapper, "code-block-header"); + languages = find_child_by_class(header, "demo-languages"); + files = find_child_by_class(header, "demo-files"); + + /* Step 1: activate the language tab matching `language`. */ + for (child = ui_widget_get_child(languages, 0); child; + child = ui_widget_next(child)) { + const char *v = ui_widget_get_attr(child, "data-value"); + if (v && strcmp(v, language) == 0) { + ui_widget_add_class(child, "active"); + } else { + ui_widget_remove_class(child, "active"); + } + } + + /* Step 2: show/hide file tabs by language, activate the one + * matching `file`. When no tab matches, remember the first + * file belonging to the current language and activate it. */ + for (child = ui_widget_get_child(files, 0); child; + child = ui_widget_next(child)) { + const char *la = ui_widget_get_attr(child, "data-language"); + const char *va = ui_widget_get_attr(child, "data-value"); + if (la && strcmp(la, language) == 0) { + ui_widget_show(child); + if (!first_for_lang) { + first_for_lang = child; + } + if (file && va && strcmp(va, file) == 0) { + ui_widget_add_class(child, "active"); + file_matched = 1; + } else { + ui_widget_remove_class(child, "active"); + } + } else { + ui_widget_hide(child); + ui_widget_remove_class(child, "active"); + } + } + + /* Fallback: file didn't match — adopt the first file of the + * current language and write back into state so the next + * render is consistent. */ + if (!file_matched && first_for_lang) { + that->file = ui_widget_get_attr(first_for_lang, "data-value"); + file = that->file; + ui_widget_add_class(first_for_lang, "active"); + } + + /* Step 3: show the code-block body matching (language, file) + * and hide every other body. */ + for (child = ui_widget_get_child(wrapper, 0); child; + child = ui_widget_next(child)) { + const char *la = ui_widget_get_attr(child, "data-language"); + const char *fi = ui_widget_get_attr(child, "data-file"); + if (la && fi && ui_widget_has_class(child, "code-block-body") && + strcmp(la, language) == 0 && strcmp(fi, file) == 0) { + ui_widget_show(child); + ui_widget_add_class(child, "active"); + } else if (fi) { + ui_widget_hide(child); + ui_widget_remove_class(child, "active"); + } + } +} + +/* -- event handlers ------------------------------------------------------ */ + +static void on_click(ui_widget_t *w, ui_event_t *e, void *arg) +{ + demo_provider_t *that = ui_widget_get_data(w, demo_provider_proto); + ui_widget_t *target = e->target; + ui_widget_t *parent = target->parent; + + if (ui_widget_has_class(parent, "demo-languages")) { + that->language = ui_widget_get_attr(target, "data-value"); + that->file = NULL; + demo_provider_update(w); + return; + } + if (ui_widget_has_class(parent, "demo-files")) { + that->language = ui_widget_get_attr(target, "data-language"); + that->file = ui_widget_get_attr(target, "data-value"); + demo_provider_update(w); + return; + } +} + +static void on_ready(ui_widget_t *w, ui_event_t *e, void *arg) +{ + demo_provider_t *that = ui_widget_get_data(w, demo_provider_proto); + ui_widget_t *wrapper = find_child_by_class(w, "code-block"); + ui_widget_t *header = find_child_by_class(wrapper, "code-block-header"); + ui_widget_t *languages = find_child_by_class(header, "demo-languages"); + ui_widget_t *first_tab = ui_widget_get_child(languages, 0); + + that->language = ui_widget_get_attr(first_tab, "data-value"); + that->file = NULL; + demo_provider_update(w); +} + +/* -- prototype glue ------------------------------------------------------ */ + +static void demo_provider_init(ui_widget_t *w) +{ + demo_provider_t *that; + + that = + ui_widget_add_data(w, demo_provider_proto, sizeof(demo_provider_t)); + that->language = NULL; + that->file = NULL; + demo_provider_react_init(w); + ui_widget_on(w, "click", on_click, NULL); + ui_widget_on(w, "ready", on_ready, NULL); +} + +static void demo_provider_destroy(ui_widget_t *w) +{ + demo_provider_react_destroy(w); +} + +ui_widget_t *ui_create_demo_provider(void) +{ + return ui_create_widget_with_prototype(demo_provider_proto); +} + +void ui_register_demo_provider(void) +{ + demo_provider_init_prototype(); + demo_provider_proto->init = demo_provider_init; + demo_provider_proto->destroy = demo_provider_destroy; +} diff --git a/examples/doc-viewer/app/components/demo-provider.h b/examples/doc-viewer/app/components/demo-provider.h new file mode 100644 index 000000000..40f173dd4 --- /dev/null +++ b/examples/doc-viewer/app/components/demo-provider.h @@ -0,0 +1,9 @@ +#include + +void ui_register_demo_provider(void); + +ui_widget_t *ui_create_demo_provider(void); + +void demo_provider_update(ui_widget_t *w); + +void ui_load_demo_provider_resources(void); diff --git a/examples/doc-viewer/app/components/demo-provider.tsx b/examples/doc-viewer/app/components/demo-provider.tsx new file mode 100644 index 000000000..8bfc089d0 --- /dev/null +++ b/examples/doc-viewer/app/components/demo-provider.tsx @@ -0,0 +1,41 @@ +import { Widget, type WidgetProps } from "@lcui/react"; + +/** + * DemoProvider — wrapper for code-demo widgets. + * + * Renders a plain ``. All interactive behaviour + * lives in `demo-provider.c`, which lcui-cli emits as a one-shot + * skeleton from this file: + * + * - on mount, pick the first `.demo-languages > .demo-tab` as active, + * then the first `.demo-files > .demo-tab[data-language=…]` whose + * language matches, then the matching `.code-block` block, and add + * the `active` class to each; + * - on click within `.demo-languages` or `.demo-files`, update the + * active selection and show/hide siblings accordingly. + * + * The lcui-cli compiler walks the JSX tree on the call site, so children + * of `…` are appended to this widget even + * though the function body itself does not reference `props.children`. + * The `WidgetProps` annotation is purely a TypeScript-level declaration + * so consumers can nest tabs/code-blocks (and pass any standard widget + * attribute) inside this provider; at runtime lcui-cli ignores + * `children` here. + * + * No other props are passed: the default selection is derived from the + * children's source order at runtime. This avoids relying on lcui-cli's + * (currently unverified) propagation of custom-component props. + * + * Consumers (each `app/examples/-tsx/index.tsx`) wrap their tabs, + * code blocks, and preview placeholder in this provider: + * + * + * + * ... .demo-tab[data-value] ... + * ... .demo-tab[data-language][data-value] ... + * ... .code-block[data-language][data-file] ... + * + */ +export default function DemoProvider(_props: WidgetProps) { + return ; +} diff --git a/examples/doc-viewer/app/components/field-table.c b/examples/doc-viewer/app/components/field-table.c new file mode 100644 index 000000000..d26df88b9 --- /dev/null +++ b/examples/doc-viewer/app/components/field-table.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include "field-table.tsx.h" +#include "field-table.h" + +typedef struct { + field_table_provider_react_t base; +} field_table_provider_t; + +static ui_widget_t *find_ancestor_with_class(ui_widget_t *w, const char *cls) +{ + while (w) { + if (ui_widget_has_class(w, cls)) + return w; + w = w->parent; + } + return NULL; +} + +static void on_click(ui_widget_t *w, ui_event_t *e, void *arg) +{ + ui_widget_t *target = e->target; + ui_widget_t *row, *details; + + if (ui_widget_has_class(target, "field-table-header")) + return; + row = find_ancestor_with_class(target, "field-table-row"); + if (!row) { + return; + } + details = ui_widget_next(row); + if (ui_widget_has_class(row, "expanded")) { + ui_widget_remove_class(row, "expanded"); + ui_widget_remove_class(details, "expanded"); + } else { + ui_widget_add_class(row, "expanded"); + ui_widget_add_class(details, "expanded"); + } +} + +static void field_table_provider_init(ui_widget_t *w) +{ + ui_widget_add_data(w, field_table_provider_proto, + sizeof(field_table_provider_t)); + field_table_provider_react_init(w); + ui_widget_on(w, "click", on_click, NULL); +} + +static void field_table_provider_destroy(ui_widget_t *w) +{ + field_table_provider_react_destroy(w); +} + +void ui_register_field_table_provider(void) +{ + field_table_provider_init_prototype(); + field_table_provider_proto->init = field_table_provider_init; + field_table_provider_proto->destroy = field_table_provider_destroy; +} diff --git a/examples/doc-viewer/app/components/field-table.css b/examples/doc-viewer/app/components/field-table.css new file mode 100644 index 000000000..a9752b45a --- /dev/null +++ b/examples/doc-viewer/app/components/field-table.css @@ -0,0 +1,77 @@ +.field-table-provider { + @apply my-4; +} +.field-table { + display: block; + @apply rounded border border-gray-200 bg-white; +} +.field-table-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + @apply items-center border-b border-gray-100; +} +.field-table-row:last-child { + @apply border-b-0; +} +.field-table-row:hover { + @apply bg-gray-50; +} +.field-table-row.field-table-header { + @apply bg-gray-100 border-b border-gray-200; +} +.field-table-cell { + @apply px-4 py-2.5 text-sm text-gray-800; +} +.field-table-cell.cell-name { + width: 160px; + font-family: monospace; + @apply font-medium; +} +.field-table-cell.cell-type { + flex: 1; + font-family: monospace; +} +.field-table-cell.cell-default { + width: 160px; + font-family: monospace; +} +.field-table-row.field-table-header .field-table-cell { + @apply font-bold text-gray-900; +} +.field-table-expand-icon, +.field-table-collapse-icon { + position: absolute; + top: 10px; + right: 16px; + @apply text-gray-400 cursor-pointer; +} +.field-table-collapse-icon { + display: none; +} +.field-table-row.expanded .field-table-expand-icon { + display: none; +} +.field-table-row.expanded .field-table-collapse-icon { + display: inline-block; +} +.field-table-details { + width: 100%; + display: none; + @apply bg-gray-100 px-4 py-3 border-t border-gray-200; +} +.field-table-details.expanded { + display: block; +} +.field-table-detail-row { + display: flex; + flex-direction: row; + @apply py-1 text-sm; +} +.field-table-detail-name { + width: 160px; + @apply font-medium text-gray-600; +} +.field-table-detail-value { + @apply text-gray-800; +} diff --git a/examples/doc-viewer/app/components/field-table.h b/examples/doc-viewer/app/components/field-table.h new file mode 100644 index 000000000..92e744667 --- /dev/null +++ b/examples/doc-viewer/app/components/field-table.h @@ -0,0 +1,4 @@ +#include + +void ui_load_field_table_resources(void); +void ui_register_field_table_provider(void); diff --git a/examples/doc-viewer/app/components/field-table.tsx b/examples/doc-viewer/app/components/field-table.tsx new file mode 100644 index 000000000..68cbfc94f --- /dev/null +++ b/examples/doc-viewer/app/components/field-table.tsx @@ -0,0 +1,102 @@ +import { Fragment, Text, Widget } from "@lcui/react"; +import { ChevronDown, ChevronUp } from "@lcui/fluent-icons"; +import "./field-table.css"; + +export interface FieldData { + name: string; + type: string; + default: string; + description: { en: string; "zh-CN": string }; +} + +export interface FieldTableProps { + fields: FieldData[]; + locale?: string; +} + +const labels: Record> = { + name: { en: "Prop", "zh-CN": "\u5C5E\u6027\u540D" }, + type: { en: "Type", "zh-CN": "\u7C7B\u578B" }, + default: { en: "Default", "zh-CN": "\u9ED8\u8BA4\u503C" }, + description: { en: "Description", "zh-CN": "\u63CF\u8FF0" }, +}; + +function t(key: string, locale: string): string { + return labels[key]?.[locale] ?? labels[key]?.en ?? key; +} + +function FieldTableProvider({ children }: { children?: any }) { + return {children}; +} + +function FieldTable({ fields, locale = "en" }: FieldTableProps) { + return ( + + + + + {t("name", locale)} + + + {t("type", locale)} + + + {t("default", locale)} + + + {fields.map((field) => ( + + + + {field.name} + + + {field.type} + + + {field.default} + + + + + + + + {t("name", locale)} + + {field.name} + + + + {t("type", locale)} + + {field.type} + + + + {t("default", locale)} + + + {field.default} + + + + + {t("description", locale)} + + + {locale === "zh-CN" + ? field.description["zh-CN"] + : field.description.en} + + + + + ))} + + + ); +} + +FieldTable.shouldPreRender = true; +export default FieldTable; diff --git a/examples/doc-viewer/app/components/list.c b/examples/doc-viewer/app/components/list.c new file mode 100644 index 000000000..7d358735d --- /dev/null +++ b/examples/doc-viewer/app/components/list.c @@ -0,0 +1,169 @@ +/* + * app/components/list.c - Implementations of
    ,