Skip to content
Snippets Groups Projects
lib.rs 42.5 KiB
Newer Older
            if text_height > height {
StaffEngineer's avatar
StaffEngineer committed
                height = text_height.ceil();
                style.height = Val::Px(height);
            }
        }

        redraw_buffer_common(
            mode,
            &mut x_offset,
            &mut images,
            &mut swash_cache_state,
sam's avatar
sam committed
            editor,
            attrs,
            background_image.0.clone(),
            fill_color.0,
            &mut img.texture,
            text_position,
            &mut font_system,
            scale,
            width,
            height,
        );
    }
}

StaffEngineer's avatar
StaffEngineer committed
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,
        &mut Sprite,
        &CosmicBackground,
        &CosmicTextPosition,
        &mut Handle<Image>,
        &mut XOffset,
        &CosmicMode,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
StaffEngineer's avatar
StaffEngineer committed
) {
    let primary_window = windows.single();
    let scale = primary_window.scale_factor() as f32;

        sprite,
        background_image,
        text_position,
        mut handle,
        mut x_offset,
        mode,
    ) in &mut cosmic_edit_query.iter_mut()
    {
        editor.0.shape_as_needed(&mut font_system.0);
        if !editor.0.buffer().redraw() {
            continue;
        }
StaffEngineer's avatar
StaffEngineer committed
        let width = sprite.custom_size.unwrap().x.ceil();
        let mut height = sprite.custom_size.unwrap().y.ceil();
        let widget_height = height * scale;
        let widget_width = width * scale;

        let (buffer_width, buffer_height) = match mode {
            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
            CosmicMode::AutoHeight => (widget_width, (i32::MAX / 2) as f32), // TODO: workaround
            CosmicMode::Wrap => (widget_width, widget_height),
        };
        editor
            .0
            .buffer_mut()
            .set_size(&mut font_system.0, buffer_width, buffer_height);

        if mode == &CosmicMode::AutoHeight {
            let text_size = get_text_size(editor.0.buffer());
StaffEngineer's avatar
StaffEngineer committed
            let text_height = (text_size.1 + 30.) / primary_window.scale_factor() as f32;
            if text_height > height {
StaffEngineer's avatar
StaffEngineer committed
                height = text_height.ceil();
                sprite.custom_size.unwrap().y = height;
            }
        }
StaffEngineer's avatar
StaffEngineer committed
        redraw_buffer_common(
            mode,
            &mut x_offset,
StaffEngineer's avatar
StaffEngineer committed
            &mut images,
            &mut swash_cache_state,
            &mut editor.0,
            background_image.0.clone(),
StaffEngineer's avatar
StaffEngineer committed
            &mut handle,
            text_position,
            &mut font_system,
StaffEngineer's avatar
StaffEngineer committed
            width,
            height,
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn redraw_buffer_common(
    mode: &CosmicMode,
    x_offset: &mut XOffset,
    images: &mut ResMut<Assets<Image>>,
    swash_cache_state: &mut ResMut<SwashCacheState>,
    editor: &mut Editor,
    attrs: &CosmicAttrs,
    background_image: Option<Handle<Image>>,
    fill_color: Color,
    cosmic_canvas_img_handle: &mut Handle<Image>,
    text_position: &CosmicTextPosition,
    font_system: &mut ResMut<CosmicFontSystem>,
    scale_factor: f32,
    original_width: f32,
    original_height: f32,
) {
    let widget_width = original_width * scale_factor;
    let widget_height = original_height * scale_factor;
    let swash_cache = &mut swash_cache_state.swash_cache;

    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)));
        }
    }

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

    let mut pixels = vec![0; widget_width as usize * widget_height as usize * 4];
    if let Some(bg_image) = background_image {
        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;
        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
        }
    }
    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()),
        ),
    };

    editor.draw(
        &mut font_system.0,
        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,
                    );
                }
            }
        },
    );

    if let Some(prev_image) = images.get_mut(cosmic_canvas_img_handle) {
        if *cosmic_canvas_img_handle == 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);
            *cosmic_canvas_img_handle = 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);
}

StaffEngineer's avatar
StaffEngineer 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 {
StaffEngineer's avatar
StaffEngineer committed
        // 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],
    );
StaffEngineer's avatar
StaffEngineer committed

    // 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());
StaffEngineer's avatar
StaffEngineer committed

    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] = (bg.a() * 255.0) as u8;
StaffEngineer's avatar
StaffEngineer committed
}

#[cfg(target_arch = "wasm32")]
pub fn get_timestamp() -> f64 {
    js_sys::Date::now()
}

#[cfg(not(target_arch = "wasm32"))]
pub fn get_timestamp() -> f64 {
    use std::time::SystemTime;
    use std::time::UNIX_EPOCH;
    let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
    duration.as_millis() as f64
}

#[cfg(test)]
mod tests {
    use crate::*;

    fn test_spawn_cosmic_edit_system(mut commands: Commands) {
        commands.spawn(CosmicEditUiBundle {
            text_setter: CosmicText::OneStyle("Blah".into()),
            ..Default::default()
        });
    }

    #[test]
    fn test_spawn_cosmic_edit() {
        let mut app = App::new();
        app.add_plugins(TaskPoolPlugin::default());
        app.add_plugins(AssetPlugin::default());
        app.insert_resource(CosmicFontSystem(create_cosmic_font_system(
            CosmicFontConfig::default(),
        )));
        app.add_systems(Update, test_spawn_cosmic_edit_system);

        let input = Input::<KeyCode>::default();
        app.insert_resource(input);
        let mouse_input: Input<MouseButton> = Input::<MouseButton>::default();
        app.insert_resource(mouse_input);
        app.add_asset::<Image>();

        app.add_event::<ReceivedCharacter>();

        app.update();

        let mut text_nodes_query = app.world.query::<&CosmicEditor>();
        for cosmic_editor in text_nodes_query.iter(&app.world) {
            insta::assert_debug_snapshot!(cosmic_editor
                .0
                .buffer()
                .lines
                .iter()
                .map(|line| line.text())
                .collect::<Vec<_>>());
        }
    }
}