creo_ui/lib.rs
1//! creo-ui
2//!
3//! creo-ui Design System tokens for Rust consumers.
4//!
5//! Phase 1: Style Dictionary から生成された token module を再 export する。
6//!
7//! ```ignore
8//! use creo_ui::tokens;
9//!
10//! let brand = tokens::COLOR_BRAND_PRIMARY; // Rgb (mint-dark default)
11//! let md = tokens::SPACING_M; // 18.0_f32 (論理 px, 5-step rule)
12//! ```
13//!
14//! ## dimension token は「論理 px」— 生描画では [`Scale`] を掛ける
15//!
16//! dimension 系 token (`SPACING_*` / `TYPOGRAPHY_*` / `RADIUS_*` 等) の f32 は
17//! **論理 px** — CSS px / SwiftUI pt と同じ土俵の値で、「同じ数字なら 3 platform で
18//! 同じ見た目の大きさ」が既定の契約。論理→物理の変換は platform ごとに担当が違う:
19//!
20//! | platform | 変換の担当 |
21//! |---|---|
22//! | Web | browser (creo-ui は rem で emit、user 設定にも追従) |
23//! | SwiftUI | OS (pt をそのまま渡せばよい) |
24//! | Rust 生描画 (wgpu / glyphon 等) | **consumer — [`Scale`] を掛ける** |
25//!
26//! wgpu の surface は物理ピクセルなので、token 値を素で渡すと Retina (2x) で
27//! 見た目が半分になる。window の scale factor (winit `window.scale_factor()` /
28//! AppKit `NSView.backingScaleFactor`) から [`Scale`] を作り、描画直前に通すこと。
29//! ratatui のようにセル単位の描画系では無関係 (色 / 比率 token のみ使う)。
30//!
31//! Phase 2 以降で ratatui / egui 等のヘルパー trait を追加する予定。
32
33#![forbid(unsafe_code)]
34
35// Cargo.toml の version を SSOT にする (手書き定数は 0.3.0 で止まったまま
36// Cargo 側が 0.7.0 まで進む drift を起こしていた — ladyland feedback 補足と同類)
37pub const VERSION: &str = env!("CARGO_PKG_VERSION");
38
39#[allow(dead_code)]
40pub mod tokens {
41 include!("generated/tokens.rs");
42}
43
44/// 表示 scale — 論理 px の token 値を物理 px へ変換する倍率。
45///
46/// creo-ui の dimension token は **論理 px** ([crate docs](crate) の契約表参照)。
47/// wgpu / glyphon のような物理ピクセル基準の生描画では、window の scale factor を
48/// ここに包んで描画直前に [`Scale::px`] を通す:
49///
50/// ```
51/// use creo_ui::{tokens, Scale};
52///
53/// let scale = Scale::new(2.0); // 例: Retina。実際は window.scale_factor() 等から
54/// let font_px = scale.px(tokens::TYPOGRAPHY_SIZE_M); // 論理 px → 物理 px (2 倍)
55/// assert_eq!(font_px, tokens::TYPOGRAPHY_SIZE_M * 2.0);
56/// ```
57///
58/// glyphon なら `TextArea.scale` にそのまま factor を渡す手もある。padding や
59/// 配置座標など layout 値も論理で組み、同じ [`Scale`] を通すこと (文字だけ
60/// 掛けると余白と釣り合いが崩れる)。
61///
62/// `Default` は意図的に実装しない — 「黙って 1.0」は Retina で半分サイズになる
63/// bug (scale 掛け忘れ) をそのまま既定化してしまうため、factor は常に明示する。
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct Scale(pub f32);
66
67impl Scale {
68 /// 等倍 (非 HiDPI)。テストや scale 非対応環境での明示的な選択肢。
69 pub const ONE: Scale = Scale(1.0);
70
71 /// window / surface の scale factor から作る。
72 /// winit: `window.scale_factor()` / AppKit: `NSView.backingScaleFactor`。
73 pub fn new(factor: f64) -> Scale {
74 Scale(factor as f32)
75 }
76
77 /// 論理 px の dimension token を物理 px へ。
78 pub fn px(self, logical: f32) -> f32 {
79 logical * self.0
80 }
81}
82
83// ratatui interop — opt-in via `features = ["ratatui"]`
84#[cfg(feature = "ratatui")]
85pub mod ratatui;
86
87// egui interop — opt-in via `features = ["egui"]`
88#[cfg(feature = "egui")]
89pub mod egui;
90
91// iced / dioxus は skeleton (feature gate のみ、実装は将来 issue)
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn version_tracks_cargo_manifest() {
99 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
100 }
101
102 #[test]
103 fn scrim_preserves_alpha() {
104 // 旧実装は alpha を落とし scrim が不透明の純黒だった (swift #11 と同根、
105 // rust-v0.8.0 で根治)。40% → 102、50% → 128 (× 255 round)
106 let scrim = tokens::COLOR_SURFACE_SCRIM;
107 assert_eq!((scrim.r, scrim.g, scrim.b, scrim.a), (0, 0, 0, 102));
108 assert_eq!(tokens::COLOR_SURFACE_SCRIM_MODAL.a, 128);
109 }
110
111 #[test]
112 fn opaque_colors_have_full_alpha() {
113 assert_eq!(tokens::COLOR_BRAND_PRIMARY.a, 255);
114 assert_eq!(tokens::COLOR_TEXT_PRIMARY.a, 255);
115 }
116
117 #[test]
118 fn alpha_f32_converts_to_unit_range() {
119 assert_eq!(tokens::COLOR_BRAND_PRIMARY.alpha_f32(), 1.0);
120 let a = tokens::COLOR_SURFACE_SCRIM.alpha_f32();
121 assert!((a - 0.4).abs() < 0.01, "scrim alpha_f32 = {a} should be ~0.4");
122 }
123
124 #[test]
125 fn brand_primary_is_mint_green_family() {
126 // mint-dark brand primary (theme system default)、値そのものは theme が
127 // 変わったら追従させる必要があるので "green 帯" の smoke check に留める。
128 let c = tokens::COLOR_BRAND_PRIMARY;
129 assert!(c.g > c.r && c.g > c.b, "brand primary should be green-dominant");
130 assert!(c.g > 150, "brand primary green channel should be high for mint");
131 }
132
133 #[test]
134 fn spacing_md_is_18px() {
135 // 5-step size-feel rule (2026-04-22): md is the middle of xs/sm/md/lg/xl
136 assert_eq!(tokens::SPACING_M, 18.0_f32);
137 }
138
139 #[test]
140 fn margin_md_matches_spacing_md() {
141 // creo-ui 規約: 同じ "medium" は spacing.md と margin.md を揃える
142 assert_eq!(tokens::MARGIN_M, tokens::SPACING_M);
143 }
144
145 #[test]
146 fn layout_target_tap_is_44pt() {
147 // Apple HIG minimum tap target
148 assert_eq!(tokens::LAYOUT_TARGET_TAP, 44.0_f32);
149 }
150
151 #[test]
152 fn radius_full_is_pill() {
153 assert_eq!(tokens::RADIUS_FULL, 9999.0_f32);
154 }
155
156 #[test]
157 fn scale_one_is_identity() {
158 assert_eq!(Scale::ONE.px(tokens::TYPOGRAPHY_SIZE_M), tokens::TYPOGRAPHY_SIZE_M);
159 }
160
161 #[test]
162 fn scale_doubles_on_retina() {
163 // MacBook Air (2x) で論理 px の本文が 2 倍の物理 px で描かれる。
164 // これを掛け忘れると「見た目半分」(ladyland cortex-gpu で実際に起きた)。
165 // token の具体値には依存させない — 梯子は Editor 実測で改定されうる
166 let retina = Scale::new(2.0);
167 assert_eq!(retina.px(10.0), 20.0);
168 assert_eq!(retina.px(tokens::TYPOGRAPHY_SIZE_M), tokens::TYPOGRAPHY_SIZE_M * 2.0);
169 }
170}