khora_editor/widgets/tile.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Asset-browser tile widget — type-coloured gradient thumbnails.
16
17use khora_sdk::editor_ui::{FontFamilyHint, Icon, TextAlign, UiBuilder, UiTheme};
18
19use super::paint::{paint_icon, paint_text_size, with_alpha};
20
21/// Visual category for an asset tile. Drives the gradient + icon + format
22/// glyph. Variants map onto the canonical type names produced by
23/// `khora_io::asset::IndexBuilder::asset_type_for_extension`. Add a new
24/// variant when a new asset *category* lands engine-side; per-extension
25/// distinctions belong in the IndexBuilder.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AssetTileKind {
28 Mesh,
29 Texture,
30 Audio,
31 Shader,
32 Scene,
33 /// Authored materials (`.kmat`) — RON material definitions referenced
34 /// by entities through `MaterialRef::Asset`.
35 Material,
36 /// Gameplay scripts (`.kscript`) — data-driven, hot-reloadable. Tier
37 /// 3 of the project's three "code" tiers; the scripting language
38 /// runtime itself isn't implemented yet, but the asset browser
39 /// surfaces them so authors can see where they live and the watcher
40 /// can hot-reload them.
41 Script,
42 Unknown,
43}
44
45impl AssetTileKind {
46 pub(crate) fn icon(self) -> Icon {
47 match self {
48 Self::Mesh => Icon::Cube,
49 Self::Texture => Icon::Image,
50 Self::Audio => Icon::Music,
51 Self::Shader => Icon::Zap,
52 Self::Scene => Icon::Globe,
53 Self::Material => Icon::Circle,
54 Self::Script => Icon::Code,
55 Self::Unknown => Icon::Box,
56 }
57 }
58
59 /// Returns the top accent colour for the tile gradient (the bottom
60 /// always falls into `theme.surface` so the tile blends with the panel
61 /// background regardless of category).
62 pub(crate) fn accent(self, theme: &UiTheme) -> [f32; 4] {
63 let raw = match self {
64 Self::Mesh => theme.accent_a,
65 Self::Texture => theme.accent_c,
66 Self::Audio => theme.success,
67 Self::Shader => theme.accent_a,
68 Self::Scene => theme.accent_a,
69 Self::Material => theme.accent_c,
70 Self::Script => theme.warning,
71 Self::Unknown => theme.surface_active,
72 };
73 with_alpha(raw, 0.85)
74 }
75
76 fn glyph_label(self) -> &'static str {
77 match self {
78 Self::Mesh => "FBX",
79 Self::Texture => "TEX",
80 Self::Audio => "OGG",
81 Self::Shader => "GLSL",
82 Self::Scene => "SCN",
83 Self::Material => "MAT",
84 Self::Script => "KSCR",
85 Self::Unknown => "—",
86 }
87 }
88}
89
90/// What the user did to an asset tile this frame.
91///
92/// Tiles surface both single- and double-click distinctly: single click
93/// selects (asset browser shows metadata), double-click is the "primary
94/// action" (load a scene, open a script in OS-default editor, etc.).
95#[derive(Debug, Default, Clone, Copy)]
96pub struct AssetTileInteraction {
97 pub clicked: bool,
98 pub double_clicked: bool,
99 /// Pointer is over the tile — kept here for callers that want to
100 /// surface a tooltip or preview without re-running the rect
101 /// interaction. Currently unused by the asset browser, but the
102 /// future inspector preview will read it.
103 #[allow(dead_code)]
104 pub hovered: bool,
105}
106
107/// Paints an asset tile (thumbnail + name) and reports interaction.
108///
109/// `origin` is the top-left in screen-space, `size` is the total tile box
110/// (thumbnail + name strip below). The tile self-measures so the caller only
111/// has to grid-place identical-size boxes.
112#[allow(clippy::too_many_arguments)] // UI paint helper — splitting hurts readability.
113pub fn paint_asset_tile(
114 ui: &mut dyn UiBuilder,
115 id_salt: &str,
116 origin: [f32; 2],
117 size: [f32; 2],
118 name: &str,
119 kind: AssetTileKind,
120 selected: bool,
121 theme: &UiTheme,
122) -> AssetTileInteraction {
123 let [w, h] = size;
124 let thumb_h = w; // square
125 let name_y = origin[1] + thumb_h + 4.0;
126
127 // Outer hit area (used for hover background)
128 let outer = [origin[0], origin[1], w, h];
129 let interaction = ui.interact_rect(id_salt, outer);
130
131 // Gold marks the selection, here as everywhere else in the editor.
132 if selected {
133 ui.paint_rect_filled(origin, [w, h], with_alpha(theme.accent_c, 0.12), 6.0);
134 } else if interaction.hovered {
135 ui.paint_rect_filled(origin, [w, h], with_alpha(theme.surface_elevated, 0.4), 6.0);
136 }
137
138 // Thumbnail
139 let thumb_x = origin[0] + 4.0;
140 let thumb_y = origin[1] + 4.0;
141 let tw = w - 8.0;
142 let th = thumb_h - 8.0;
143 let accent = kind.accent(theme);
144 ui.paint_rect_filled([thumb_x, thumb_y], [tw, th], accent, 4.0);
145 ui.paint_rect_stroke(
146 [thumb_x, thumb_y],
147 [tw, th],
148 with_alpha(theme.separator, 0.55),
149 4.0,
150 1.0,
151 );
152 if selected {
153 ui.paint_rect_stroke([thumb_x, thumb_y], [tw, th], theme.accent_c, 4.0, 1.5);
154 }
155
156 // Centered icon
157 let icon_size = (tw * 0.45).clamp(20.0, 36.0);
158 paint_icon(
159 ui,
160 [
161 thumb_x + (tw - icon_size) * 0.5,
162 thumb_y + (th - icon_size) * 0.5,
163 ],
164 kind.icon(),
165 icon_size,
166 with_alpha(theme.text, 0.85),
167 );
168
169 // Format glyph bottom-right
170 ui.paint_text_styled(
171 [thumb_x + tw - 4.0, thumb_y + th - 14.0],
172 kind.glyph_label(),
173 9.5,
174 with_alpha([1.0, 1.0, 1.0, 1.0], 0.55),
175 FontFamilyHint::Monospace,
176 TextAlign::Right,
177 );
178
179 // Name (clipped if too long — egui handles eg_text overflow per-character)
180 let truncated = if name.chars().count() > 14 {
181 let mut s: String = name.chars().take(13).collect();
182 s.push('…');
183 s
184 } else {
185 name.to_owned()
186 };
187 paint_text_size(
188 ui,
189 [origin[0] + 4.0, name_y],
190 &truncated,
191 11.0,
192 if selected { theme.text } else { theme.text_dim },
193 );
194
195 AssetTileInteraction {
196 clicked: interaction.clicked,
197 double_clicked: interaction.double_clicked,
198 hovered: interaction.hovered,
199 }
200}