Skip to content
Snippets Groups Projects
render.rs 17.9 KiB
Newer Older
use std::time::Duration;

use bevy::{
    asset::HandleId,
sam edelsten's avatar
sam edelsten committed
    prelude::*,
    render::render_resource::Extent3d,
    window::{PrimaryWindow, WindowScaleFactorChanged},
};
use cosmic_text::{Affinity, Edit, Metrics, SwashCache};
use image::{imageops::FilterType, GenericImageView};

use crate::{
    get_text_size, get_x_offset_center, get_y_offset_center, CosmicAttrs, CosmicBackground,
    CosmicCanvas, CosmicEditor, CosmicFontSystem, CosmicMetrics, CosmicMode, CosmicText,
    CosmicTextPosition, FillColor, Focus, PasswordInput, Placeholder, ReadOnly, XOffset,
    DEFAULT_SCALE_PLACEHOLDER,
};

#[derive(Resource)]
pub(crate) struct SwashCacheState {
    pub swash_cache: SwashCache,
}

#[derive(Resource)]
pub(crate) struct CursorBlinkTimer(pub Timer);

#[derive(Resource)]
pub(crate) struct CursorVisibility(pub bool);

pub(crate) fn cosmic_edit_redraw_buffer(
    windows: Query<&Window, With<PrimaryWindow>>,
    mut images: ResMut<Assets<Image>>,
    mut swash_cache_state: ResMut<SwashCacheState>,
    mut cosmic_edit_query: Query<(
        &mut CosmicEditor,
        &CosmicAttrs,
        &CosmicBackground,
        &FillColor,
        &mut CosmicCanvas,
        &CosmicTextPosition,
        Option<&Node>,
        Option<&mut Style>,
        Option<&mut Sprite>,
        &mut XOffset,
        &CosmicMode,
        Option<&mut Placeholder>,
sam edelsten's avatar
sam edelsten committed
        Option<&PasswordInput>,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
) {
    let primary_window = windows.single();
    let scale = primary_window.scale_factor() as f32;

    for (
sam edelsten's avatar
sam edelsten committed
        mut cosmic_editor,
        attrs,
        background_image,
        fill_color,
        text_position,
        node_opt,
        style_opt,
        sprite_opt,
        mut x_offset,
        mode,
        mut placeholder_opt,
sam edelsten's avatar
sam edelsten committed
        password_opt,
    ) in &mut cosmic_edit_query.iter_mut()
    {
sam edelsten's avatar
sam edelsten committed
        if !cosmic_editor.0.buffer().redraw() {
sam edelsten's avatar
sam edelsten committed
        let current_text = cosmic_editor.get_text();
        let current_select_opt = cosmic_editor.0.select_opt().clone();
        let current_cursor = cosmic_editor.0.cursor().clone();
sam edelsten's avatar
sam edelsten committed

        // intercept text for password inputs
        if let Some(password) = password_opt {
            if !current_text.is_empty() {
                cosmic_editor.set_text(
                    CosmicText::OneStyle(format!("{}", password.0).repeat(current_text.len())),
                    attrs.0.clone(),
                    &mut font_system.0,
                );

                cosmic_editor.0.set_select_opt(current_select_opt);
                cosmic_editor.0.set_cursor(current_cursor);
        // Check for placeholder, replace editor if found and buffer is empty
sam edelsten's avatar
sam edelsten committed
        let editor = if current_text.is_empty() && placeholder_opt.is_some() {
            let placeholder = &mut placeholder_opt.as_mut().unwrap().0 .0;
            placeholder.buffer_mut().set_redraw(true);

sam edelsten's avatar
sam edelsten committed
            cosmic_editor.0.buffer_mut().set_redraw(true);

            let mut cursor = placeholder.cursor();
            cursor.index = 0;
            placeholder.set_cursor(cursor);
            *x_offset = XOffset(None);
            placeholder
        } else {
sam edelsten's avatar
sam edelsten committed
            &mut cosmic_editor.0
        };

        editor.shape_as_needed(&mut font_system.0);

        // Get numbers, do maths to find and set cursor
sam edelsten's avatar
sam edelsten committed
        //
        let (base_width, mut base_height) = match node_opt {
            Some(node) => (node.size().x.ceil(), node.size().y.ceil()),
            None => (
                sprite_opt.as_ref().unwrap().custom_size.unwrap().x.ceil(),
                sprite_opt.as_ref().unwrap().custom_size.unwrap().y.ceil(),
sam edelsten's avatar
sam edelsten committed
            ),
        };

        let widget_width = base_width * scale;
        let widget_height = base_height * scale;

        let padding_x = match text_position {
            CosmicTextPosition::Center => 0.,
            CosmicTextPosition::TopLeft { padding } => *padding as f32,
            CosmicTextPosition::Left { padding } => *padding as f32,
        };

        let (buffer_width, buffer_height) = match mode {
            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
            CosmicMode::AutoHeight => (widget_width - padding_x, (i32::MAX / 2) as f32),
            CosmicMode::Wrap => (widget_width - padding_x, widget_height),
        };

        editor
            .buffer_mut()
            .set_size(&mut font_system.0, buffer_width, buffer_height);

        if mode == &CosmicMode::AutoHeight {
            let text_size = get_text_size(editor.buffer());
            let text_height = (text_size.1 + 30.) / primary_window.scale_factor() as f32;
            if text_height > base_height {
                base_height = text_height.ceil();
                match style_opt {
                    Some(mut style) => style.height = Val::Px(base_height),
                    None => sprite_opt.unwrap().custom_size.unwrap().y = base_height,
                }
            }
        }

        let mut cursor_x = 0.;
        if mode == &CosmicMode::InfiniteLine {
            if let Some(line) = editor.buffer().layout_runs().next() {
                for (idx, glyph) in line.glyphs.iter().enumerate() {
                    if editor.cursor().affinity == Affinity::Before {
                        if idx <= editor.cursor().index {
                            cursor_x += glyph.w;
                        }
                    } else if idx < editor.cursor().index {
                        cursor_x += glyph.w;
                    } else {
                        break;
                    }
                }
            }
        }

        if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() {
            let padding_x = match text_position {
                CosmicTextPosition::Center => get_x_offset_center(widget_width, editor.buffer()),
                CosmicTextPosition::TopLeft { padding } => *padding,
                CosmicTextPosition::Left { padding } => *padding,
            };
            *x_offset = XOffset(Some((0., widget_width - 2. * padding_x as f32)));
        }

        if let Some((x_min, x_max)) = x_offset.0 {
            if cursor_x > x_max {
                let diff = cursor_x - x_max;
                *x_offset = XOffset(Some((x_min + diff, cursor_x)));
            }
            if cursor_x < x_min {
                let diff = x_min - cursor_x;
                *x_offset = XOffset(Some((cursor_x, x_max - diff)));
            }
        }

        // Draw background
        let mut pixels = vec![0; widget_width as usize * widget_height as usize * 4];
        if let Some(bg_image) = background_image.0.clone() {
            if let Some(image) = images.get(&bg_image) {
                let mut dynamic_image = image.clone().try_into_dynamic().unwrap();
                if image.size().x != widget_width || image.size().y != widget_height {
                    dynamic_image = dynamic_image.resize_to_fill(
                        widget_width as u32,
                        widget_height as u32,
                        FilterType::Triangle,
                    );
                }
                for (i, (_, _, rgba)) in dynamic_image.pixels().enumerate() {
                    if let Some(p) = pixels.get_mut(i * 4..(i + 1) * 4) {
                        p[0] = rgba[0];
                        p[1] = rgba[1];
                        p[2] = rgba[2];
                        p[3] = rgba[3];
                    }
                }
            }
        } else {
            let bg = fill_color.0;
            for pixel in pixels.chunks_exact_mut(4) {
                pixel[0] = (bg.r() * 255.) as u8; // Red component
                pixel[1] = (bg.g() * 255.) as u8; // Green component
                pixel[2] = (bg.b() * 255.) as u8; // Blue component
                pixel[3] = (bg.a() * 255.) as u8; // Alpha component
            }
        }

        // Get values for glyph draw step
        let (padding_x, padding_y) = match text_position {
            CosmicTextPosition::Center => (
                get_x_offset_center(widget_width, editor.buffer()),
                get_y_offset_center(widget_height, editor.buffer()),
            ),
            CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
            CosmicTextPosition::Left { padding } => (
                *padding,
                get_y_offset_center(widget_height, editor.buffer()),
            ),
        };

        let font_color = attrs
            .0
            .color_opt
            .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));

        // Draw glyphs
        editor.draw(
            &mut font_system.0,
            &mut swash_cache_state.swash_cache,
            font_color,
            |x, y, w, h, color| {
                for row in 0..h as i32 {
                    for col in 0..w as i32 {
                        draw_pixel(
                            &mut pixels,
                            widget_width as i32,
                            widget_height as i32,
                            x + col + padding_x - x_offset.0.unwrap_or((0., 0.)).0 as i32,
                            y + row + padding_y,
                            color,
                        );
                    }
                }
            },
        );

        let canvas = &mut canvas.0;

        if let Some(prev_image) = images.get_mut(canvas) {
            if *canvas == bevy::render::texture::DEFAULT_IMAGE_HANDLE.typed() {
                let mut prev_image = prev_image.clone();
                prev_image.data.clear();
                prev_image.data.extend_from_slice(pixels.as_slice());
                prev_image.resize(Extent3d {
                    width: widget_width as u32,
                    height: widget_height as u32,
                    depth_or_array_layers: 1,
                });
                let handle_id: HandleId = HandleId::random::<Image>();
                let new_handle: Handle<Image> = Handle::weak(handle_id);
                let new_handle = images.set(new_handle, prev_image);
                *canvas = new_handle;
            } else {
                prev_image.data.clear();
                prev_image.data.extend_from_slice(pixels.as_slice());
                prev_image.resize(Extent3d {
                    width: widget_width as u32,
                    height: widget_height as u32,
                    depth_or_array_layers: 1,
                });
            }
        }

        editor.buffer_mut().set_redraw(false);
sam edelsten's avatar
sam edelsten committed

        // reset intercepted text
        if password_opt.is_some() && !current_text.is_empty() {
            cosmic_editor.set_text(
                crate::CosmicText::OneStyle(current_text),
                attrs.0.clone(),
                &mut font_system.0,
            );

            cosmic_editor.0.set_select_opt(current_select_opt);
            cosmic_editor.0.set_cursor(current_cursor);
sam edelsten's avatar
sam edelsten committed
        }
    }
}

fn draw_pixel(
    buffer: &mut [u8],
    width: i32,
    height: i32,
    x: i32,
    y: i32,
    color: cosmic_text::Color,
) {
    // TODO: perftest this fn against previous iteration
    let a_a = color.a() as u32;
    if a_a == 0 {
        // Do not draw if alpha is zero
        return;
    }

    if y < 0 || y >= height {
        // Skip if y out of bounds
        return;
    }

    if x < 0 || x >= width {
        // Skip if x out of bounds
        return;
    }

    let offset = (y as usize * width as usize + x as usize) * 4;

    let bg = Color::rgba_u8(
        buffer[offset],
        buffer[offset + 1],
        buffer[offset + 2],
        buffer[offset + 3],
    );

    // TODO: if alpha is 100% or bg is empty skip blending

    let fg = Color::rgba_u8(color.r(), color.g(), color.b(), color.a());

    let premul = fg * Vec3::splat(color.a() as f32 / 255.0);

    let out = premul + bg * (1.0 - fg.a());

    buffer[offset + 2] = (out.b() * 255.0) as u8;
    buffer[offset + 1] = (out.g() * 255.0) as u8;
    buffer[offset] = (out.r() * 255.0) as u8;
    buffer[offset + 3] = (out.a() * 255.0) as u8;
}

pub(crate) fn blink_cursor(
    mut visibility: ResMut<CursorVisibility>,
    mut timer: ResMut<CursorBlinkTimer>,
    time: Res<Time>,
    active_editor: ResMut<Focus>,
    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
    mut placeholder_editor_q: Query<&mut Placeholder, Without<ReadOnly>>,
) {
    if let Some(e) = active_editor.0 {
        timer.0.tick(time.delta());
        if !timer.0.just_finished() && !active_editor.is_changed() {
            return;
        }
        visibility.0 = !visibility.0;

        // always start cursor visible on focus
        if active_editor.is_changed() {
            visibility.0 = true;
            timer.0.set_elapsed(Duration::ZERO);
        }

        let new_color = if visibility.0 {
            None
        } else {
            Some(cosmic_text::Color::rgba(0, 0, 0, 0))
        };

        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
            let editor = &mut editor.0;
            let mut cursor = editor.cursor();
            cursor.color = new_color;
            editor.set_cursor(cursor);
            editor.buffer_mut().set_redraw(true);
        }

        if let Ok(mut placeholder) = placeholder_editor_q.get_mut(e) {
            let placeholder = &mut placeholder.0 .0;
            let mut cursor_p = placeholder.cursor();
            cursor_p.color = new_color;
            placeholder.set_cursor(cursor_p);
            placeholder.buffer_mut().set_redraw(true);
        }
    }
}

pub(crate) fn freeze_cursor_blink(
    mut visibility: ResMut<CursorVisibility>,
    mut timer: ResMut<CursorBlinkTimer>,
    active_editor: Res<Focus>,
    keys: Res<Input<KeyCode>>,
    char_evr: EventReader<ReceivedCharacter>,
    mut editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
) {
    let inputs = [
        KeyCode::Left,
        KeyCode::Right,
        KeyCode::Up,
        KeyCode::Down,
        KeyCode::Back,
        KeyCode::Return,
    ];
    if !keys.any_pressed(inputs) && char_evr.is_empty() {
        return;
    }

    if let Some(e) = active_editor.0 {
        if let Ok(mut editor) = editor_q.get_mut(e) {
            timer.0.set_elapsed(Duration::ZERO);
            visibility.0 = true;
            let mut cursor = editor.0.cursor();
            cursor.color = None;
            editor.0.set_cursor(cursor);
            editor.0.buffer_mut().set_redraw(true);
        }
    }
}

pub(crate) fn hide_inactive_or_readonly_cursor(
    mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>,
    mut cosmic_editor_q_placeholder: Query<(Entity, &mut Placeholder, Option<&ReadOnly>)>,
    mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>,
    active_editor: Res<Focus>,
) {
    for mut editor in &mut cosmic_editor_q_readonly.iter_mut() {
        let mut cursor = editor.0.cursor();
        cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
        editor.0.set_cursor(cursor);
        editor.0.buffer_mut().set_redraw(true);
    }

    for (e, mut editor, readonly_opt) in &mut cosmic_editor_q_placeholder.iter_mut() {
        // filthy short circuiting instead of correct unwrapping
        if active_editor.is_none() || e != active_editor.0.unwrap() || readonly_opt.is_some() {
            let editor = &mut editor.0;
            let mut cursor = editor.0.cursor();
            if cursor.color == Some(cosmic_text::Color::rgba(0, 0, 0, 0)) {
                return;
            }
            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
            editor.0.set_cursor(cursor);
            editor.0.buffer_mut().set_redraw(true);
        }
    }

    for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() {
        if active_editor.is_none() || e != active_editor.0.unwrap() {
            let mut cursor = editor.0.cursor();
            if cursor.color == Some(cosmic_text::Color::rgba(0, 0, 0, 0)) {
                return;
            }
            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
            editor.0.set_cursor(cursor);
            editor.0.buffer_mut().set_redraw(true);
        }
    }
}

pub(crate) fn set_initial_scale(
    window_q: Query<&Window, With<PrimaryWindow>>,
    mut metrics_q: Query<&mut CosmicMetrics, Added<CosmicMetrics>>,
) {
    let scale = window_q.single().scale_factor() as f32;

    for mut metrics in metrics_q.iter_mut() {
        if metrics.scale_factor == DEFAULT_SCALE_PLACEHOLDER {
            metrics.scale_factor = scale;
        }
    }
}

pub(crate) fn on_scale_factor_change(
    mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
    mut cosmic_query: Query<(&mut CosmicEditor, &CosmicMetrics, &mut XOffset)>,
    mut font_system: ResMut<CosmicFontSystem>,
) {
    if !scale_factor_changed.is_empty() {
        let new_scale_factor = scale_factor_changed.iter().last().unwrap().scale_factor as f32;
        for (mut editor, metrics, mut x_offset) in &mut cosmic_query.iter_mut() {
            let font_system = &mut font_system.0;
            let metrics =
                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);

            editor.0.buffer_mut().set_metrics(font_system, metrics);
            editor.0.buffer_mut().set_redraw(true);

            *x_offset = XOffset(None);
        }
    }
}

pub(crate) fn cosmic_ui_to_canvas(
    mut added_ui_images: Query<(&mut UiImage, &CosmicCanvas), Added<UiImage>>,
) {
    for (mut ui_image, canvas) in added_ui_images.iter_mut() {
        ui_image.texture = canvas.0.clone_weak();
    }
}

pub(crate) fn update_handle_ui(
    mut changed_handles: Query<(&mut UiImage, &CosmicCanvas), Changed<CosmicCanvas>>,
) {
    for (mut ui_image, canvas) in changed_handles.iter_mut() {
        ui_image.texture = canvas.0.clone_weak();
    }
}

pub(crate) fn cosmic_sprite_to_canvas(
    mut added_sprite_textures: Query<(&mut Handle<Image>, &CosmicCanvas), Added<Handle<Image>>>,
) {
    for (mut handle, canvas) in added_sprite_textures.iter_mut() {
        *handle = canvas.0.clone_weak();
    }
}

pub(crate) fn update_handle_sprite(
    mut changed_handles: Query<(&mut Handle<Image>, &CosmicCanvas), Changed<CosmicCanvas>>,
) {
    for (mut handle, canvas) in changed_handles.iter_mut() {
        *handle = canvas.0.clone_weak();
    }
}