use crate::ecs::world::{Entity, World};
use crate::ui::builder::UiTreeBuilder;
use crate::ui::components::{UiNodeInteraction, UiScrollAreaData};
use crate::ui::layer::UiLayer;
use crate::ui::layout_types::UiLayoutType;
use crate::ui::state::{UiBase, UiStateTrait};
use crate::ui::units::Ab;
use crate::ui::widgets::world_interaction::ui_mark_layout_dirty;
use crate::ui::widgets::world_layout::{ui_set_text, ui_set_visible};
use nalgebra_glm::{Vec2, Vec4};
use super::ShellState;
const FONT_SIZE: f32 = 14.0;
const LINE_HEIGHT_RATIO: f32 = 1.4;
const PADDING: f32 = 10.0;
const INPUT_HEIGHT: f32 = 32.0;
const SEPARATOR_HEIGHT: f32 = 1.0;
const RESIZE_HANDLE_HEIGHT: f32 = 6.0;
const MIN_HEIGHT: f32 = 100.0;
const PROMPT_WIDTH: f32 = 18.0;
const MAX_SCROLLBACK_ROWS: usize = 2000;
const BACKGROUND: Vec4 = Vec4::new(0.06, 0.06, 0.08, 0.95);
const BORDER: Vec4 = Vec4::new(0.4, 0.4, 0.45, 1.0);
const PROMPT: Vec4 = Vec4::new(0.39, 0.78, 0.39, 1.0);
const COMMAND_TEXT: Vec4 = Vec4::new(0.59, 0.78, 1.0, 1.0);
const OUTPUT_TEXT: Vec4 = Vec4::new(0.78, 0.78, 0.78, 1.0);
const HANDLE_IDLE: Vec4 = Vec4::new(0.5, 0.5, 0.55, 0.8);
const HANDLE_ACTIVE: Vec4 = Vec4::new(0.6, 0.8, 1.0, 1.0);
struct LineState {
entity: Entity,
text: String,
is_command: bool,
visible: bool,
}
pub struct ShellOverlay {
panel: Entity,
scroll: Entity,
content: Entity,
lines: Vec<LineState>,
input_text: Entity,
resize_handle: Entity,
panel_visible: bool,
panel_size: Vec2,
input_cache: String,
handle_active: bool,
last_content_height: f32,
last_output_len: usize,
forced_ui_visible: bool,
}
fn set_node_color(world: &mut World, entity: Entity, color: Vec4) {
if let Some(node_color) = world.get_mut::<crate::ui::components::UiNodeColor>(entity) {
node_color.colors[UiBase::INDEX] = Some(color);
node_color.computed_color = color;
}
}
struct OutputRow {
text: String,
is_command: bool,
}
fn output_rows<C>(shell: &ShellState<C>) -> Vec<OutputRow> {
let mut rows = Vec::new();
for line in &shell.output {
if line.text.is_empty() {
rows.push(OutputRow {
text: String::new(),
is_command: line.is_command,
});
continue;
}
for segment in line.text.split('\n') {
rows.push(OutputRow {
text: segment.to_string(),
is_command: line.is_command,
});
}
}
if rows.len() > MAX_SCROLLBACK_ROWS {
rows.drain(..rows.len() - MAX_SCROLLBACK_ROWS);
}
rows
}
fn build_overlay(world: &mut World) -> ShellOverlay {
let forced_ui_visible = !world
.res::<crate::ui::resources::RetainedUiRuntime>()
.visible;
world
.res_mut::<crate::ui::resources::RetainedUiRuntime>()
.visible = true;
let mut tree = UiTreeBuilder::new(world);
let panel = tree
.add_node()
.window_at(Ab(Vec2::zeros()), Ab(Vec2::new(0.0, 0.0)))
.rect(0.0)
.color_raw::<UiBase>(BACKGROUND)
.with_layer(UiLayer::Tooltips)
.with_clip()
.with_interaction()
.flow_vertical()
.padding(0.0)
.gap(0.0)
.entity();
let (scroll, content, input_text, resize_handle) = tree.in_parent(panel, |tree| {
let scroll = tree.add_scroll_area_fill(PADDING, 0.0);
let content = tree
.world_mut()
.get::<UiScrollAreaData>(scroll)
.map(|data| data.content_entity)
.unwrap_or(scroll);
tree.add_node()
.fill_width()
.size_px(0.0, SEPARATOR_HEIGHT)
.rect(0.0)
.color_raw::<UiBase>(BORDER);
let input_row = tree
.add_node()
.fill_width()
.size_px(0.0, INPUT_HEIGHT)
.flow_horizontal()
.padding(0.0)
.gap(0.0)
.align_cross(crate::ui::layout_types::FlowAlignment::Center)
.entity();
let input_text = tree.in_parent(input_row, |tree| {
tree.add_node()
.size_px(PROMPT_WIDTH, INPUT_HEIGHT)
.with_text(">", FONT_SIZE)
.color_raw::<UiBase>(PROMPT);
tree.add_node()
.fill_width()
.flex_grow(1.0)
.with_text("", FONT_SIZE)
.color_raw::<UiBase>(OUTPUT_TEXT)
.entity()
});
let resize_handle = tree
.add_node()
.fill_width()
.size_px(0.0, RESIZE_HANDLE_HEIGHT)
.rect(RESIZE_HANDLE_HEIGHT * 0.5)
.color_raw::<UiBase>(HANDLE_IDLE)
.with_interaction()
.with_cursor_icon(nightshade_platform::CursorIcon::NsResize)
.entity();
(scroll, content, input_text, resize_handle)
});
tree.finish();
ShellOverlay {
panel,
scroll,
content,
lines: Vec::new(),
input_text,
resize_handle,
panel_visible: false,
panel_size: Vec2::new(-1.0, -1.0),
input_cache: String::new(),
handle_active: false,
last_content_height: f32::NAN,
last_output_len: usize::MAX,
forced_ui_visible,
}
}
fn sync_output(world: &mut World, overlay: &mut ShellOverlay, rows: &[OutputRow]) {
if overlay.lines.len() < rows.len() {
let content = overlay.content;
let mut tree = UiTreeBuilder::from_parent(&mut world.ecs, content);
for _ in overlay.lines.len()..rows.len() {
let entity = tree
.add_node()
.fill_width()
.size_px(0.0, (FONT_SIZE * LINE_HEIGHT_RATIO).round())
.with_text("", FONT_SIZE)
.color_raw::<UiBase>(OUTPUT_TEXT)
.entity();
overlay.lines.push(LineState {
entity,
text: String::new(),
is_command: false,
visible: true,
});
}
tree.finish();
}
for index in 0..overlay.lines.len() {
match rows.get(index) {
Some(row) => {
let entity = overlay.lines[index].entity;
if !overlay.lines[index].visible {
ui_set_visible(&mut world.ecs, entity, true);
overlay.lines[index].visible = true;
}
if overlay.lines[index].text != row.text {
ui_set_text(&mut world.ecs, entity, &row.text);
overlay.lines[index].text.clear();
overlay.lines[index].text.push_str(&row.text);
}
if overlay.lines[index].is_command != row.is_command {
let color = if row.is_command {
COMMAND_TEXT
} else {
OUTPUT_TEXT
};
set_node_color(world, entity, color);
overlay.lines[index].is_command = row.is_command;
}
}
None => {
if overlay.lines[index].visible {
let entity = overlay.lines[index].entity;
ui_set_visible(&mut world.ecs, entity, false);
overlay.lines[index].visible = false;
}
}
}
}
}
fn apply_resize<C>(
shell: &mut ShellState<C>,
world: &mut World,
overlay: &mut ShellOverlay,
max: f32,
) {
let interaction = world
.get::<UiNodeInteraction>(overlay.resize_handle)
.map(|interaction| (interaction.dragging, interaction.drag_start));
let Some((dragging, drag_start)) = interaction else {
return;
};
let scale = crate::platform::window::window_scale_factor(world).max(f32::EPSILON);
let mouse_y = crate::platform::input::access::mouse_for_active(world)
.position
.y
/ scale;
if dragging {
if !shell.dragging_resize {
shell.dragging_resize = true;
shell.drag_start_y = drag_start.map(|start| start.y / scale).unwrap_or(mouse_y);
shell.drag_start_height = shell.height;
}
shell.height = (shell.drag_start_height + (mouse_y - shell.drag_start_y))
.clamp(MIN_HEIGHT.min(max), max);
} else {
shell.dragging_resize = false;
}
if overlay.handle_active != shell.dragging_resize {
overlay.handle_active = shell.dragging_resize;
let color = if shell.dragging_resize {
HANDLE_ACTIVE
} else {
HANDLE_IDLE
};
set_node_color(world, overlay.resize_handle, color);
}
}
fn set_panel_rect(world: &mut World, overlay: &mut ShellOverlay, size: Vec2) {
if overlay.panel_size == size {
return;
}
overlay.panel_size = size;
if let Some(node) = world.get_mut::<crate::ui::components::UiLayoutNode>(overlay.panel)
&& let Some(UiLayoutType::Window(window)) = node.base_layout.as_mut()
{
window.position = Ab(Vec2::zeros()).into();
window.size = Ab(size).into();
}
ui_mark_layout_dirty(&mut world.ecs);
}
fn set_panel_visible(world: &mut World, overlay: &mut ShellOverlay, visible: bool) {
if overlay.panel_visible == visible {
return;
}
overlay.panel_visible = visible;
ui_set_visible(&mut world.ecs, overlay.panel, visible);
}
fn apply_scroll_to_bottom<C>(
shell: &mut ShellState<C>,
world: &mut World,
overlay: &mut ShellOverlay,
) {
let scale = crate::platform::window::window_scale_factor(world).max(f32::EPSILON);
let Some((content_height, visible_height)) = world
.get::<UiScrollAreaData>(overlay.scroll)
.map(|data| (data.content_height, data.visible_height))
else {
return;
};
let bottom = ((content_height - visible_height).max(0.0)) / scale;
if let Some(data) = world.get_mut::<UiScrollAreaData>(overlay.scroll) {
data.scroll_offset = bottom;
}
let settled = (content_height - overlay.last_content_height).abs() < f32::EPSILON;
overlay.last_content_height = content_height;
if settled {
shell.scroll_to_bottom = false;
}
}
pub fn shell_retained_ui<C>(shell: &mut ShellState<C>, world: &mut World) {
if !shell.should_render() {
if let Some(mut overlay) = shell.overlay.take() {
set_panel_visible(world, &mut overlay, false);
if overlay.forced_ui_visible {
world
.res_mut::<crate::ui::resources::RetainedUiRuntime>()
.visible = false;
}
shell.overlay = Some(overlay);
}
shell.pending_enter = false;
shell.pending_up = false;
shell.pending_down = false;
shell.pending_escape = false;
shell.dragging_resize = false;
world
.res_mut::<crate::user_interface::UserInterface>()
.hud_wants_keyboard = false;
return;
}
let mut overlay = match shell.overlay.take() {
Some(overlay) => overlay,
None => build_overlay(world),
};
if overlay.forced_ui_visible {
world
.res_mut::<crate::ui::resources::RetainedUiRuntime>()
.visible = true;
}
world
.res_mut::<crate::user_interface::UserInterface>()
.hud_wants_keyboard = shell.visible;
let scale = crate::platform::window::window_scale_factor(world).max(f32::EPSILON);
let viewport = crate::platform::window::window_viewport_size(world)
.map(|(width, height)| Vec2::new(width as f32, height as f32))
.unwrap_or(Vec2::new(1920.0, 1080.0))
/ scale;
let max_height = (viewport.y * 0.9).max(0.0);
shell.height = shell.height.clamp(MIN_HEIGHT.min(max_height), max_height);
apply_resize(shell, world, &mut overlay, max_height);
let open_height = shell.height * shell.animation_progress;
set_panel_visible(world, &mut overlay, true);
set_panel_rect(world, &mut overlay, Vec2::new(viewport.x, open_height));
if overlay.last_output_len != shell.output.len() {
overlay.last_output_len = shell.output.len();
let rows = output_rows(shell);
sync_output(world, &mut overlay, &rows);
}
let cursor = if shell.visible && shell.animation_progress > 0.9 {
"_"
} else {
""
};
let input_display = format!("{}{}", shell.input_buffer, cursor);
if overlay.input_cache != input_display {
ui_set_text(&mut world.ecs, overlay.input_text, &input_display);
overlay.input_cache = input_display;
}
if shell.scroll_to_bottom {
apply_scroll_to_bottom(shell, world, &mut overlay);
}
if shell.visible && shell.animation_progress > 0.9 {
if shell.pending_enter {
shell.pending_enter = false;
shell.execute_command(world);
}
if shell.pending_up {
shell.pending_up = false;
shell.history_up();
}
if shell.pending_down {
shell.pending_down = false;
shell.history_down();
}
if shell.pending_escape {
shell.pending_escape = false;
shell.visible = false;
}
}
shell.overlay = Some(overlay);
}