alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Reactive UI chrome rendered with haalka.

use crate::{
    ecs::components::{
        buffer::{EditorView, FocusedEditorView},
        vim::VimModalState,
    },
    vim::VimConfig,
};
use bevy::prelude::{
    BackgroundColor, Color, DetectChanges, Node, PositionType, Query, Ref, Res, ResMut, Resource,
    Text, TextColor, TextFont, UiRect, Val, With,
};
use haalka::prelude::*;

use super::{
    STATUS_BAR_HEIGHT,
    status::{StatusLineKind, render_search_status},
};

/// Font size for the bottom Vim status line.
const STATUS_FONT_SIZE: f32 = 18.0;

/// Background color for the bottom Vim status line.
const STATUS_BAR_BACKGROUND: Color = Color::srgb(0.0, 0.12, 0.15);

/// Reactive view model for the bottom Vim chrome.
#[derive(Clone, Debug, Default, Eq, PartialEq, Resource)]
pub struct ChromeStatusLine {
    /// Rendered status text.
    text: String,
    /// Whether the status line represents an error.
    is_error: bool,
}

impl ChromeStatusLine {
    /// Creates a status-line view model.
    const fn new(text: String, kind: StatusLineKind) -> Self {
        Self {
            text,
            is_error: matches!(kind, StatusLineKind::Error),
        }
    }
}

/// Spawns haalka-owned UI chrome.
pub fn spawn_chrome(world: &mut bevy::prelude::World) {
    let _chrome_entity = chrome_root().spawn(world);
}

/// Synchronizes Vim resources into the haalka-facing chrome view model.
pub fn sync_chrome_status_line(
    vim_config: Res<VimConfig>,
    modal_query: Query<Ref<VimModalState>, (With<EditorView>, With<FocusedEditorView>)>,
    mut chrome_status_line: ResMut<ChromeStatusLine>,
) {
    let Some(modal_state) = modal_query.iter().next() else {
        return;
    };

    if !modal_state.is_changed() && !vim_config.is_changed() {
        return;
    }

    let vim_config = vim_config.into_inner();
    let rendered = render_search_status(
        &modal_state.search,
        &modal_state.command,
        &modal_state.leader,
        vim_config,
        &modal_state.status,
    );
    let next = ChromeStatusLine::new(rendered.text, rendered.kind);

    if *chrome_status_line != next {
        *chrome_status_line = next;
    }
}

/// Builds the haalka chrome root.
fn chrome_root() -> impl Element {
    El::<Node>::new()
        .with_node(|mut node| {
            node.position_type = PositionType::Absolute;
            node.left = Val::Px(0.0);
            node.right = Val::Px(0.0);
            node.bottom = Val::Px(0.0);
            node.height = Val::Px(STATUS_BAR_HEIGHT);
            node.padding = UiRect::horizontal(Val::Px(8.0));
        })
        .background_color(BackgroundColor(STATUS_BAR_BACKGROUND))
        .align_content(Align::new().left().center_y())
        .child(status_text())
}

/// Builds the reactive status text element.
fn status_text() -> impl Element {
    El::<Text>::new()
        .text_font(TextFont::from_font_size(STATUS_FONT_SIZE))
        .text_signal(
            signal::from_resource_changed::<ChromeStatusLine>()
                .map_in(|status| Some(Text::new(status.text))),
        )
        .text_color_signal(
            signal::from_resource_changed::<ChromeStatusLine>().map_in(|status| {
                Some(TextColor(if status.is_error {
                    Color::srgb_u8(0xff, 0x66, 0x66)
                } else {
                    Color::srgb_u8(0xD8, 0xF3, 0xF8)
                }))
            }),
        )
}