Skip to content
Snippets Groups Projects
lib.rs 51.5 KiB
Newer Older
                                    } else {
                                        editor.0.action(&mut font_system.0, Action::Insert(c));
                                    }
                                }
StaffEngineer's avatar
StaffEngineer committed
                            }
                        }
                        is_clipboard = true;
StaffEngineer's avatar
StaffEngineer committed
                    }
                }
            }
            let (offset_x, offset_y) = match text_position {
                CosmicTextPosition::Center => (
                    get_x_offset(editor.0.buffer()),
                    get_y_offset(editor.0.buffer()),
                ),
                CosmicTextPosition::TopLeft => (0, 0),
            };
            let point = |node_cursor_pos: (f32, f32)| {
                (
                    (node_cursor_pos.0 * scale_factor) as i32 - offset_x,
                    (node_cursor_pos.1 * scale_factor) as i32 - offset_y,
                )
            };

            if buttons.just_pressed(MouseButton::Left) {
                if let Some(node_cursor_pos) = get_node_cursor_pos(
                    primary_window,
                    node_transform,
                    (width, height),
                    is_ui_node,
                    camera,
                    camera_transform,
                ) {
                    let (x, y) = point(node_cursor_pos);
                    if shift {
                        editor.0.action(&mut font_system.0, Action::Drag { x, y });
                        editor.0.action(&mut font_system.0, Action::Click { x, y });
StaffEngineer's avatar
StaffEngineer committed
                    }
                }
                return;
            }
            if buttons.pressed(MouseButton::Left) {
                if let Some(node_cursor_pos) = get_node_cursor_pos(
                    primary_window,
                    node_transform,
                    (width, height),
                    is_ui_node,
                    camera,
                    camera_transform,
                ) {
                    let (x, y) = point(node_cursor_pos);
                    if active_editor.is_changed() && !shift {
                        editor.0.action(&mut font_system.0, Action::Click { x, y });
                        editor.0.action(&mut font_system.0, Action::Drag { x, y });
StaffEngineer's avatar
StaffEngineer committed
                }
                return;
            }
            for ev in scroll_evr.iter() {
                match ev.unit {
                    MouseScrollUnit::Line => {
                        editor.0.action(
                            &mut font_system.0,
                            Action::Scroll {
                                lines: -ev.y as i32,
                            },
                        );
                    }
                    MouseScrollUnit::Pixel => {
                        let line_height = editor.0.buffer().metrics().line_height;
                        editor.0.action(
                            &mut font_system.0,
                            Action::Scroll {
                                lines: -(ev.y / line_height) as i32,
                            },
                        );
StaffEngineer's avatar
StaffEngineer committed
                    }
                }
StaffEngineer's avatar
StaffEngineer committed

StaffEngineer's avatar
StaffEngineer committed

            // fix for issue #8
            if let Some(select) = editor.0.select_opt() {
                if editor.0.cursor().line == select.line && editor.0.cursor().index == select.index
                {
                    editor.0.set_select_opt(None);
            let mut is_edit = is_clipboard;
            let mut is_return = false;
            if keys.just_pressed(KeyCode::Return) {
                is_return = true;
                is_edit = true;
                if (max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0)
                    && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
                {
                    // to have new line on wasm rather than E
                    editor.0.action(&mut font_system.0, Action::Insert('\n'));
                }
StaffEngineer's avatar
StaffEngineer committed

            if !(is_clipboard || is_return) {
                for char_ev in char_evr.iter() {
                    is_edit = true;
                    if *is_deleting {
                        editor.0.action(&mut font_system.0, Action::Backspace);
                    } else if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
                        editor
                            .0
                            .action(&mut font_system.0, Action::Insert(char_ev.char));
StaffEngineer's avatar
StaffEngineer committed
                    }
                }
StaffEngineer's avatar
StaffEngineer committed

StaffEngineer's avatar
StaffEngineer committed

            if let Some(last_edit_duration) = *edits_duration {
                if Duration::from_millis(now_ms as u64) - last_edit_duration
                    > Duration::from_millis(150)
                {
                    save_edit_history(&mut editor.0, attrs, &mut edit_history);
StaffEngineer's avatar
StaffEngineer committed
                    *edits_duration = Some(Duration::from_millis(now_ms as u64));
                }
                save_edit_history(&mut editor.0, attrs, &mut edit_history);
                *edits_duration = Some(Duration::from_millis(now_ms as u64));
fn cosmic_edit_set_redraw(mut cosmic_edit_query: Query<&mut CosmicEditor, Added<CosmicEditor>>) {
    for mut editor in cosmic_edit_query.iter_mut() {
        editor.0.buffer_mut().set_redraw(true);
StaffEngineer's avatar
StaffEngineer committed
    }
}

#[allow(clippy::too_many_arguments)]
StaffEngineer's avatar
StaffEngineer committed
fn redraw_buffer_common(
    images: &mut ResMut<Assets<Image>>,
    swash_cache_state: &mut ResMut<SwashCacheState>,
    editor: &mut Editor,
    attrs: &CosmicAttrs,
    background_image: Option<Handle<Image>>,
    background_color: Color,
    cosmic_canvas_img_handle: &mut Handle<Image>,
    text_position: &CosmicTextPosition,
    font_system: &mut ResMut<CosmicFontSystem>,
StaffEngineer's avatar
StaffEngineer committed
    scale_factor: f32,
    original_width: f32,
    original_height: f32,
) {
    let width = original_width * scale_factor;
    let height = original_height * scale_factor;
    let swash_cache = &mut swash_cache_state.swash_cache;
    editor.shape_as_needed(&mut font_system.0);
    if editor.buffer().redraw() {
        editor
            .buffer_mut()
            .set_size(&mut font_system.0, width, height);

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

        let mut pixels = vec![0; width as usize * 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 != width || image.size().y != height {
                    dynamic_image = dynamic_image.resize_to_fill(
                        width as u32,
                        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];
                    }
StaffEngineer's avatar
StaffEngineer committed
                }
            }
        } else {
            let bg = background_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
            }
        }
StaffEngineer's avatar
StaffEngineer committed

        let (offset_y, offset_x) = match text_position {
            CosmicTextPosition::Center => {
                (get_y_offset(editor.buffer()), get_x_offset(editor.buffer()))
            }
            CosmicTextPosition::TopLeft => (0, 0),
        };

        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,
                            width as i32,
                            height as i32,
                            x + col + offset_x,
                            y + row + offset_y,
                            color,
                        );
StaffEngineer's avatar
StaffEngineer committed
                    }
                }
            },
        );
        editor.buffer_mut().set_redraw(false);

        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: width as u32,
                    height: 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: width as u32,
                    height: height as u32,
                    depth_or_array_layers: 1,
                });
StaffEngineer's avatar
StaffEngineer committed
            }
        }
    }
}

fn cosmic_edit_redraw_buffer_ui(
    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,
        &BackgroundColor,
        &CosmicTextPosition,
        &mut UiImage,
        &Node,
        &mut Visibility,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
StaffEngineer's avatar
StaffEngineer committed
) {
    let primary_window = windows.single();
        background_image,
        background_color,
        text_position,
        mut img,
        node,
        mut visibility,
    ) in &mut cosmic_edit_query.iter_mut()
    {
        // provide min sizes to prevent render panic
        let width = node.size().x.max(1.);
        let height = node.size().y.max(1.);
StaffEngineer's avatar
StaffEngineer committed

        redraw_buffer_common(
            &mut images,
            &mut swash_cache_state,
            &mut editor.0,
            background_image.0.clone(),
            background_color.0,
StaffEngineer's avatar
StaffEngineer committed
            &mut img.texture,
            text_position,
            &mut font_system,
StaffEngineer's avatar
StaffEngineer committed
            primary_window.scale_factor() as f32,
            width,
            height,
        );

        if *visibility == Visibility::Hidden
            && img.texture.clone() != bevy::render::texture::DEFAULT_IMAGE_HANDLE.typed()
        {
            *visibility = Visibility::Visible;
        }
    }
}

StaffEngineer's avatar
StaffEngineer committed
fn blink_cursor(
    mut visible: Local<bool>,
    mut timer: Local<Option<Timer>>,
    time: Res<Time>,
    active_editor: ResMut<ActiveEditor>,
    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
    if let Some(e) = active_editor.entity {
        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
sam edelsten's avatar
sam edelsten committed
            let timer =
                timer.get_or_insert_with(|| Timer::from_seconds(0.53, TimerMode::Repeating));
sam edelsten's avatar
sam edelsten committed
            timer.tick(time.delta());
            if !timer.just_finished() && !active_editor.is_changed() {
                return;
            }
            *visible = !*visible;
sam edelsten's avatar
sam edelsten committed
            // always start cursor visible on focus
            if active_editor.is_changed() {
                *visible = true;
                timer.set_elapsed(Duration::from_secs(0));
sam edelsten's avatar
sam edelsten committed

            let mut cursor = editor.0.cursor();
            let new_color = if *visible {
                None
            } else {
                Some(cosmic_text::Color::rgba(0, 0, 0, 0))
sam edelsten's avatar
sam edelsten committed
            };
            cursor.color = new_color;
            editor.0.set_cursor(cursor);
            editor.0.buffer_mut().set_redraw(true);
    mut cosmic_editor_q: Query<(Entity, &mut CosmicEditor)>,
    active_editor: Res<ActiveEditor>,
) {
    if !active_editor.is_changed() || active_editor.entity.is_none() {
StaffEngineer's avatar
StaffEngineer committed
        return;
    }
    for (e, mut editor) in &mut cosmic_editor_q.iter_mut() {
        if e != active_editor.entity.unwrap() {
            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);
        }
fn clear_inactive_selection(
    mut cosmic_editor_q: Query<(Entity, &mut CosmicEditor)>,
    active_editor: Res<ActiveEditor>,
) {
    if !active_editor.is_changed() || active_editor.entity.is_none() {
        return;
    }

    for (e, mut editor) in &mut cosmic_editor_q.iter_mut() {
        if e != active_editor.entity.unwrap() {
            editor.0.set_select_opt(None);
        }
    }
}

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,
        &Sprite,
        &CosmicBackground,
        &BackgroundColor,
        &CosmicTextPosition,
        &mut Handle<Image>,
        &mut Visibility,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
StaffEngineer's avatar
StaffEngineer committed
) {
    let primary_window = windows.single();
        sprite,
        background_image,
        background_color,
        text_position,
        mut handle,
        mut visibility,
    ) in &mut cosmic_edit_query.iter_mut()
    {
        // provide min sizes to prevent render panic
        let width = sprite.custom_size.unwrap().x.max(1.);
        let height = sprite.custom_size.unwrap().y.max(1.);

StaffEngineer's avatar
StaffEngineer committed
        redraw_buffer_common(
            &mut images,
            &mut swash_cache_state,
            &mut editor.0,
            background_image.0.clone(),
            background_color.0,
StaffEngineer's avatar
StaffEngineer committed
            &mut handle,
            text_position,
            &mut font_system,
StaffEngineer's avatar
StaffEngineer committed
            primary_window.scale_factor() as f32,
            width,
            height,
        );

        if *visibility == Visibility::Hidden
            && handle.clone() != bevy::render::texture::DEFAULT_IMAGE_HANDLE.typed()
        {
            *visibility = Visibility::Visible;
        }
    }
}

fn draw_pixel(
    buffer: &mut [u8],
    width: i32,
    height: i32,
    x: i32,
    y: i32,
    color: cosmic_text::Color,
) {
    let alpha = (color.0 >> 24) & 0xFF;
    if alpha == 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 mut current = buffer[offset + 2] as u32
        | (buffer[offset + 1] as u32) << 8
        | (buffer[offset] as u32) << 16
        | (buffer[offset + 3] as u32) << 24;

    if alpha >= 255 || current == 0 {
        // Alpha is 100% or current is null, replace with no blending
        current = color.0;
    } else {
        // Alpha blend with current value
        let n_alpha = 255 - alpha;
        let rb = ((n_alpha * (current & 0x00FF00FF)) + (alpha * (color.0 & 0x00FF00FF))) >> 8;
        let ag = (n_alpha * ((current & 0xFF00FF00) >> 8))
            + (alpha * (0x01000000 | ((color.0 & 0x0000FF00) >> 8)));
        current = (rb & 0x00FF00FF) | (ag & 0xFF00FF00);
    }

    buffer[offset + 2] = current as u8;
    buffer[offset + 1] = (current >> 8) as u8;
    buffer[offset] = (current >> 16) as u8;
    buffer[offset + 3] = (current >> 24) as u8;
}

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

    fn test_spawn_cosmic_edit_system(
        mut commands: Commands,
        mut font_system: ResMut<CosmicFontSystem>,
    ) {
        commands.spawn(CosmicEditUiBundle::default().set_text(
            CosmicText::OneStyle("Blah".into()),
            AttrsOwned::new(Attrs::new()),
            &mut font_system.0,
        ));
    }

    #[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<_>>());
        }
    }
}