Skip to content
Snippets Groups Projects
render.rs 12.3 KiB
Newer Older
use crate::{cosmic_edit::ReadOnly, prelude::*};
use crate::{cosmic_edit::*, BufferMutExtras};
Caleb Yates's avatar
Caleb Yates committed
use bevy::render::render_resource::Extent3d;
use image::{imageops::FilterType, GenericImageView};
use render_implementations::CosmicWidgetSize;
/// System set for cosmic text rendering systems. Runs in [`PostUpdate`]
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
Caleb Yates's avatar
Caleb Yates committed
pub(crate) struct RenderSet;
sam edelsten's avatar
sam edelsten committed
pub(crate) fn plugin(app: &mut App) {
    app.add_systems(
        First,
        update_internal_target_handles.pipe(render_implementations::debug_error),
    )
    .add_systems(PostUpdate, (render_texture,).in_set(RenderSet));
/// Every frame updates the output (in [`CosmicRenderOutput`]) to its receiver
/// on the same entity, e.g. [`Sprite`]
fn update_internal_target_handles(
    mut buffers_q: Query<
        (&CosmicRenderOutput, render_implementations::OutputToEntity),
        With<CosmicEditBuffer>,
    >,
) -> render_implementations::Result<()> {
    for (CosmicRenderOutput(output_data), mut output_components) in buffers_q.iter_mut() {
        output_components.write_image_data(output_data)?;
fn draw_pixel(
    buffer: &mut [u8],
    width: i32,
    height: i32,
    x: i32,
    y: i32,
    color: cosmic_text::Color,
) {
sam edelsten's avatar
sam edelsten committed
    let a_a = color.a() as u32;
    if a_a == 0 {
        // Do not draw if alpha is zero
sam edelsten's avatar
sam edelsten committed

    if y < 0 || y >= height {
        // Skip if y out of bounds
        return;
sam edelsten's avatar
sam edelsten committed
    if x < 0 || x >= width {
        // Skip if x out of bounds
        return;
    }
sam edelsten's avatar
sam edelsten committed
    let offset = (y as usize * width as usize + x as usize) * 4;
    let bg = bevy::prelude::Color::srgba_u8(
sam edelsten's avatar
sam edelsten committed
        buffer[offset],
        buffer[offset + 1],
        buffer[offset + 2],
        buffer[offset + 3],
    );
sam edelsten's avatar
sam edelsten committed
    // TODO: if alpha is 100% or bg is empty skip blending

    let fg = Srgba::rgba_u8(color.r(), color.g(), color.b(), color.a());
    let premul = (fg * fg.alpha).with_alpha(color.a() as f32 / 255.0);
    let out = premul + (bg.to_srgba() * (1.0 - fg.alpha));
    buffer[offset] = (out.red * 255.0) as u8;
    buffer[offset + 1] = (out.green * 255.0) as u8;
    buffer[offset + 2] = (out.blue * 255.0) as u8;
    buffer[offset + 3] = (out.alpha * 255.0) as u8;
pub(crate) struct WidgetBufferCoordTransformation {
    /// Padding between the top of the render target and the
    /// top of the buffer
    top_padding: f32,

    render_target_size: Vec2,
}

impl WidgetBufferCoordTransformation {
    pub fn new(vertical_align: VerticalAlign, render_target_size: Vec2, buffer_size: Vec2) -> Self {
        let top_padding = match vertical_align {
            VerticalAlign::Top => 0.0,
            VerticalAlign::Bottom => (render_target_size.y - buffer_size.y).max(0.0),
            VerticalAlign::Center => ((render_target_size.y - buffer_size.y) / 2.0).max(0.0),
        };
        // debug!(?top_padding, ?render_target_height, ?buffer_height);
        Self {
            top_padding,
            render_target_size,
        }
    }

    /// If you have the buffer coord, used for rendering
    // Confusing ngl, but it works
    pub fn buffer_to_widget(&self, buffer: Vec2) -> Vec2 {
        Vec2::new(buffer.x, buffer.y + self.top_padding)
    }

    /// If you have the relative widget coord centered (0, 0) in the middle of the widget,
    /// returns the buffer coord starting (0, 0) top left and working downward
    pub fn widget_origined_to_buffer_topleft(&self, widget: Vec2) -> Vec2 {
        Vec2::new(
            widget.x + self.render_target_size.x / 2.,
            -widget.y + self.render_target_size.y / 2. - self.top_padding,
        )
    }

    pub fn widget_topleft_to_buffer_topleft(&self, widget: Vec2) -> Vec2 {
        Vec2::new(widget.x, widget.y - self.top_padding)
    }

    #[allow(dead_code)]
    pub(crate) fn debug_top_padding(&self) {
        debug!(?self.top_padding);
    }
}

Caleb Yates's avatar
Caleb Yates committed
/// Renders to the [CosmicRenderOutput]
fn render_texture(
    mut query: Query<(
sam edelsten's avatar
sam edelsten committed
        &DefaultAttrs,
        &CosmicBackgroundImage,
        &CosmicBackgroundColor,
sam edelsten's avatar
sam edelsten committed
        &CursorColor,
        &SelectionColor,
        Option<&SelectedTextColor>,
Caleb Yates's avatar
Caleb Yates committed
        &CosmicRenderOutput,
        CosmicWidgetSize,
sam edelsten's avatar
sam edelsten committed
        Option<&ReadOnly>,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
    mut images: ResMut<Assets<Image>>,
Caleb Yates's avatar
Caleb Yates committed
    mut swash_cache_state: ResMut<SwashCache>,
sam edelsten's avatar
sam edelsten committed
    for (
sam edelsten's avatar
sam edelsten committed
        attrs,
        background_image,
        fill_color,
        cursor_color,
        selection_color,
        selected_text_color_option,
sam edelsten's avatar
sam edelsten committed
        canvas,
        size,
        readonly_opt,
sam edelsten's avatar
sam edelsten committed
    ) in query.iter_mut()
        let font_system = &mut font_system.0;
        let Ok(render_target_size) = size.logical_size() else {
Caleb Yates's avatar
Caleb Yates committed
            continue;
        };

        // avoids a panic
        if render_target_size.x == 0. || render_target_size.y == 0. {
Caleb Yates's avatar
Caleb Yates committed
            debug!(
                message = "Size of buffer is zero, skipping",
                // once = "This log only appears once"
            );
            continue;
        }

        // Draw background
        let mut pixels = vec![0; render_target_size.x as usize * render_target_size.y 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() != render_target_size.as_uvec2() {
                    dynamic_image = dynamic_image.resize_to_fill(
                        render_target_size.x as u32,
                        render_target_size.y 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.to_cosmic();
            for pixel in pixels.chunks_exact_mut(4) {
                pixel[0] = bg.r(); // Red component
                pixel[1] = bg.g(); // Green component
                pixel[2] = bg.b(); // Blue component
                pixel[3] = bg.a(); // Alpha component
            }
        }

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

        // compute y-offset
        let buffer_size = editor.borrow_with(font_system).expected_size();
        let transformation = WidgetBufferCoordTransformation::new(
            text_align.vertical,
            render_target_size,
            buffer_size,
        );
        // let mut actually_rendered_max = IVec2::ZERO;
        // let mut actually_rendered_min = IVec2::new(i32::MAX, i32::MAX);
sam edelsten's avatar
sam edelsten committed
        let draw_closure = |x, y, w, h, color| {
            for row in 0..h as i32 {
                for col in 0..w as i32 {
                    let buffer_coord = IVec2::new(x + col, y + row);
                    // actually_rendered_max = actually_rendered_max.max(buffer_coord);
                    // actually_rendered_min = actually_rendered_min.min(buffer_coord);

                    // compute padding_top
                    let widget_coord = transformation
                        .buffer_to_widget(buffer_coord.as_vec2())
                        .as_ivec2();

                    // actually draw pixel
sam edelsten's avatar
sam edelsten committed
                    draw_pixel(
                        &mut pixels,
                        render_target_size.x as i32,
                        render_target_size.y as i32,
                        widget_coord.x,
                        widget_coord.y,
sam edelsten's avatar
sam edelsten committed
                        color,
                    );
sam edelsten's avatar
sam edelsten committed
        // BUG: overflow when using center/right/end aligned infinite wrap
        editor.set_size(
            font_system,
            Some(match wrap {
                CosmicWrap::Wrap => render_target_size.x,
                // probably high enough
                CosmicWrap::InfiniteLine => f32::MAX / 10f32.powi(3),
            }),
            Some(render_target_size.y),
        );
        if let Some(alignment) = text_align.horizontal {
            for line in &mut editor.lines {
                line.set_align(Some(alignment.into()));
            }
        }

sam edelsten's avatar
sam edelsten committed
        // Draw glyphs
        if let Some(editor) = editor.editor() {
            // todo: optimizations (see below comments)
            editor.set_redraw(true);
sam edelsten's avatar
sam edelsten committed
            if !editor.redraw() {
sam edelsten's avatar
sam edelsten committed
                continue;
            let cursor_color = cursor_color.0;
sam edelsten's avatar
sam edelsten committed
            let cursor_opacity = if editor.cursor_visible && readonly_opt.is_none() {
                cursor_color.alpha()
sam edelsten's avatar
sam edelsten committed
            } else {
            let cursor_color = cursor_color.with_alpha(cursor_opacity).to_cosmic();
            let selection_color = selection_color.0.to_cosmic();

            let selected_text_color = selected_text_color_option
                .map(|selected_text_color| selected_text_color.0.to_cosmic())
                .unwrap_or(font_color);
            // try to fix annoying scroll behaviour
            // by only allowing vertical scrolling if the buffer is actually larger than the canvas
            // let mut scroll = editor.with_buffer(|b| b.scroll());
            // if buffer_size.y + 10.0 < render_target_size.y {
            //     trace!(
            //         message = "Ignoring vertical scroll as buffer is smaller than canvas",
            //         ?buffer_size.y,
            //         ?render_target_size.y
            //     );
            //     scroll.vertical = 0.0;
            // }
            // editor.with_buffer_mut(|b| b.set_scroll(scroll));

            // let new_buffer_size = editor.expected_size();

            let mut editor = editor.borrow_with(font_system);
            editor.shape_as_needed(false);
sam edelsten's avatar
sam edelsten committed
            editor.draw(
Caleb Yates's avatar
Caleb Yates committed
                &mut swash_cache_state.0,
sam edelsten's avatar
sam edelsten committed
                font_color,
                cursor_color,
                selection_color,
                selected_text_color,
sam edelsten's avatar
sam edelsten committed
                draw_closure,

            // if coord calculations seem to be buggy, this code may help you to debug
            // let actually_rendered_buffer_size = actually_rendered_max - actually_rendered_min;
            // trace!(
            //     ?buffer_size,
            //     ?new_buffer_size,
            //     ?actually_rendered_buffer_size
            // );
            // transformation.debug_top_padding();
            // debug check only
            // if (new_buffer_size.as_ivec2() - actually_rendered_buffer_size)
            //     .as_vec2()
            //     .length()
            //     > 5.0
            // {
            //     warn_once!(
            //         message = "Calculations of buffer sizes are off by a significant amount",
            //         note = "This is likely an internal bug with bevy_cosmic_edit"
            //     );
            // }

sam edelsten's avatar
sam edelsten committed
            // PERF: Read all possible render-input changes and only redraw if necessary
Caleb Yates's avatar
Caleb Yates committed
            // editor.set_redraw(false);
sam edelsten's avatar
sam edelsten committed
        } else {
            // todo: performance optimizations (see comments above/below)
            editor.set_redraw(true);
            if !editor.redraw() {
sam edelsten's avatar
sam edelsten committed
                continue;

            // editor.borrow_with(font_system).compute_everything();
            editor.shape_until_scroll(font_system, false);
            editor.draw(
                font_system,
Caleb Yates's avatar
Caleb Yates committed
                &mut swash_cache_state.0,
sam edelsten's avatar
sam edelsten committed
                font_color,
                draw_closure,
            );
sam edelsten's avatar
sam edelsten committed
            // PERF: Read all possible render-input changes and only redraw if necessary
Caleb Yates's avatar
Caleb Yates committed
            // buffer.set_redraw(false);
Caleb Yates's avatar
Caleb Yates committed
        if let Some(prev_image) = images.get_mut(&canvas.0) {
sam edelsten's avatar
sam edelsten committed
            prev_image.data.clear();
            // Updates the stored asset image with the computed pixels
sam edelsten's avatar
sam edelsten committed
            prev_image.data.extend_from_slice(pixels.as_slice());
            prev_image.resize(Extent3d {
                width: render_target_size.x as u32,
                height: render_target_size.y as u32,
sam edelsten's avatar
sam edelsten committed
                depth_or_array_layers: 1,
            });