Skip to main content

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}