beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
#[path = "state_ime.rs"]
mod ime;
#[path = "state_keyboard.rs"]
mod keyboard;
#[path = "state_metrics.rs"]
mod metrics;
#[path = "state_pointer.rs"]
mod pointer;
#[path = "state_visuals.rs"]
mod visuals;

use super::text::update_input_text;
use super::value::{can_insert_number_char, normalize_numeric_value};
use super::{
    InputField, InputType, InputValueChangedMessage, is_printable_char, push_value_changed,
};
use crate::text::FontResource;
use bevy::input::{ButtonInput, keyboard::KeyCode};
use bevy::prelude::*;

pub(crate) use ime::{handle_ime_input, sync_input_ime_state};
pub(crate) use keyboard::handle_keyboard_input;
pub(crate) use pointer::{
    clear_input_focus_on_foreign_click, input_click, input_drag, input_drag_end, input_drag_start,
};
pub(crate) use visuals::{sync_input_edit_visuals, sync_input_focus_visuals};

const CARET_BLINK_RESUME_DELAY_SECS: f64 = 0.6;

fn keep_caret_visible(field: &mut InputField, time: &Time) {
    field.caret_blink_resume_at = time.elapsed_secs_f64() + CARET_BLINK_RESUME_DELAY_SECS;
}

fn sync_display_change(
    commands: &mut Commands,
    font_resource: &FontResource,
    field: &mut InputField,
    disabled: bool,
    time: &Time,
) {
    keep_caret_visible(field, time);
    if !field.is_multiline() {
        field.preferred_caret_x = None;
    }
    update_input_text(commands, font_resource, field, disabled);
}

fn shift_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight)
}

fn control_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight)
}

fn alt_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    keys.pressed(KeyCode::AltLeft) || keys.pressed(KeyCode::AltRight)
}

fn command_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    keys.pressed(KeyCode::SuperLeft) || keys.pressed(KeyCode::SuperRight)
}

fn command_modifier_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    control_pressed(keys) || command_pressed(keys)
}

fn word_modifier_pressed(keys: &ButtonInput<KeyCode>) -> bool {
    control_pressed(keys) || alt_pressed(keys)
}

fn can_insert_char(field: &InputField, chr: char) -> bool {
    match field.input_type {
        InputType::Text | InputType::Textarea | InputType::Password => is_printable_char(chr),
        InputType::Number => can_insert_number_char(chr, field.value(), field.min),
        InputType::Range => false,
        InputType::Checkbox | InputType::Radio => false,
    }
}

pub(crate) fn sync_radio_groups(
    mut queries: ParamSet<(
        Query<(Entity, &InputField), Changed<InputField>>,
        Query<(Entity, &mut InputField)>,
    )>,
) {
    let active_groups: Vec<(Entity, String)> = queries
        .p0()
        .iter()
        .filter(|(_, field)| field.input_type == InputType::Radio && field.checked)
        .map(|(entity, field)| (entity, field.name.clone()))
        .collect();

    for (entity, group_name) in active_groups {
        for (other_entity, mut other) in &mut queries.p1() {
            if other_entity == entity || other.input_type != InputType::Radio {
                continue;
            }
            if other.name == group_name && other.checked {
                other.checked = false;
            }
        }
    }
}

fn step_number_field(field: &mut InputField, direction: f32) -> bool {
    field.step_by(direction).is_some()
}

fn commit_numeric_field(
    entity: Entity,
    field: &mut InputField,
    value_changed: &mut MessageWriter<InputValueChangedMessage>,
) -> bool {
    if !matches!(field.input_type, InputType::Number) {
        return false;
    }
    let next = normalize_numeric_value(field.value(), field.min, field.max, field.step);
    if !field.edit_state.normalize_text(next) {
        return false;
    }
    push_value_changed(value_changed, entity, field);
    true
}

pub(crate) fn set_checkable_state(
    entity: Entity,
    field: &mut InputField,
    checked: bool,
    value_changed: &mut MessageWriter<InputValueChangedMessage>,
) -> bool {
    if !field.is_checkable() || field.checked == checked {
        return false;
    }
    field.checked = checked;
    push_value_changed(value_changed, entity, field);
    true
}