Skip to main content

khora_editor/widgets/inspector/
tabs.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//! Inspector tabs — `Properties` and `Debug`. Adding a third tab is a
10//! matter of implementing [`InspectorTab`] and adding it to the panel's
11//! `tabs` vector at construction time.
12//!
13//! `Events` and `Prefab` tabs are intentionally absent — see the editor
14//! design doc.
15
16use std::sync::{Arc, Mutex};
17
18use khora_sdk::editor_ui::{EditorState, InspectedEntity, PropertyEdit, UiBuilder, UiTheme};
19use khora_sdk::prelude::ecs::EntityId;
20use khora_sdk::CommandHistory;
21
22use super::add_component::{is_author_facing, render_add_component};
23use super::card::render_card;
24use super::display::icon_for_domain_tag;
25use super::tag_chips::render_tag_chips;
26use super::walker::render_value;
27
28/// Per-tab context handed to [`InspectorTab::render`]. Locked once by the
29/// caller and passed through.
30pub struct InspectorTabContext<'a> {
31    pub state: &'a Arc<Mutex<EditorState>>,
32    pub history: &'a Arc<Mutex<CommandHistory>>,
33    pub theme: &'a UiTheme,
34    pub entity: EntityId,
35    pub inspected: &'a InspectedEntity,
36}
37
38/// A self-contained body for one Inspector sub-tab. The panel iterates
39/// the registered tabs and dispatches the active one's `render`.
40pub trait InspectorTab: Send + Sync {
41    fn label(&self) -> &str;
42    fn render(
43        &mut self,
44        ui: &mut dyn UiBuilder,
45        body_rect: [f32; 4],
46        ctx: &mut InspectorTabContext<'_>,
47    );
48}
49
50/// Default tab — component cards plus "+ Add Component" menu.
51pub struct PropertiesTab;
52
53impl InspectorTab for PropertiesTab {
54    fn label(&self) -> &str {
55        "Properties"
56    }
57
58    fn render(
59        &mut self,
60        ui: &mut dyn UiBuilder,
61        body_rect: [f32; 4],
62        ctx: &mut InspectorTabContext<'_>,
63    ) {
64        let theme = ctx.theme.clone();
65        let entity = ctx.entity;
66        let inspected = ctx.inspected.clone();
67        let state_arc = ctx.state.clone();
68
69        // Component cards flow with the egui cursor rather than being placed at
70        // absolute rects, so the stock scroll area is the right tool here —
71        // unlike the Console and the Hierarchy, which paint into computed rects
72        // and use the cursor-offset helper instead.
73        ui.region_at("inspector-properties", body_rect, &mut |ui_region| {
74            ui_region.scroll_area("inspector-cards", &mut |ui_inner| {
75                let mut state_guard = match state_arc.lock() {
76                    Ok(s) => s,
77                    Err(_) => return,
78                };
79                let panel_rect_inner = ui_inner.panel_rect();
80                let card_x = panel_rect_inner[0];
81                let card_w = panel_rect_inner[2];
82
83                let mut edits: Vec<PropertyEdit> = Vec::new();
84
85                for cj in inspected.components_json.iter() {
86                    if !is_author_facing(&cj.type_name) {
87                        continue;
88                    }
89                    let title = cj.type_name.clone();
90                    let mut value = cj.value.clone();
91                    let icon = icon_for_domain_tag(cj.domain);
92                    let mut changed = false;
93                    render_card(
94                        ui_inner,
95                        entity,
96                        &title,
97                        icon,
98                        None,
99                        true, // removable — `is_author_facing` already filtered
100                        card_x,
101                        card_w,
102                        &theme,
103                        &mut state_guard,
104                        &mut |ui_b| {
105                            // Tag has a custom chip renderer — the generic JSON
106                            // walker would render it as `[0] = "alpha", …` rows.
107                            if cj.type_name == "Tag" {
108                                changed = render_tag_chips(ui_b, entity, &mut value);
109                            } else {
110                                changed = render_value(ui_b, &mut value, &theme);
111                            }
112                        },
113                    );
114                    if changed {
115                        edits.push(PropertyEdit::SetComponentJson {
116                            entity,
117                            type_name: cj.type_name.clone(),
118                            value,
119                        });
120                    }
121                }
122
123                ui_inner.spacing(8.0);
124                render_add_component(ui_inner, entity, &inspected, &mut state_guard);
125
126                for e in edits.drain(..) {
127                    state_guard.push_edit(e);
128                }
129            });
130        });
131    }
132}
133
134/// Debug tab — undo / redo stack inspection.
135pub struct DebugTab;
136
137impl InspectorTab for DebugTab {
138    fn label(&self) -> &str {
139        "Debug"
140    }
141
142    fn render(
143        &mut self,
144        ui: &mut dyn UiBuilder,
145        body_rect: [f32; 4],
146        ctx: &mut InspectorTabContext<'_>,
147    ) {
148        let theme = ctx.theme.clone();
149        let history = ctx.history.clone();
150        let undo_desc = history
151            .lock()
152            .ok()
153            .and_then(|h| h.undo_description().map(|s| s.to_owned()))
154            .unwrap_or_else(|| "(none)".to_owned());
155        let redo_desc = history
156            .lock()
157            .ok()
158            .and_then(|h| h.redo_description().map(|s| s.to_owned()))
159            .unwrap_or_else(|| "(none)".to_owned());
160        ui.region_at("inspector-debug", body_rect, &mut |ui_inner| {
161            ui_inner.colored_label(theme.text_dim, "Command history:");
162            ui_inner.colored_label(
163                theme.text_muted,
164                &format!("Undo: {} | Redo: {}", undo_desc, redo_desc),
165            );
166        });
167    }
168}