Skip to content
Snippets Groups Projects
lib.rs 56.7 KiB
Newer Older
StaffEngineer's avatar
StaffEngineer committed
                        }
                    }
                    if !readonly && command && keys.just_pressed(KeyCode::X) {
                        if let Some(text) = editor.0.copy_selection() {
                            clipboard.set_text(text).unwrap();
                            editor.0.delete_selection();
StaffEngineer's avatar
StaffEngineer committed
                        }
                        is_clipboard = true;
                    }
                    if !readonly && command && keys.just_pressed(KeyCode::V) {
                        if let Ok(text) = clipboard.get_text() {
                            for c in text.chars() {
                                if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
                                    if c == 0xA as char {
                                        if max_lines.0 == 0
                                            || editor.0.buffer().lines.len() < max_lines.0
                                        {
                                            editor.0.action(&mut font_system.0, Action::Insert(c));
                                        }
                                    } 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 (padding_x, padding_y) = match text_position {
                CosmicTextPosition::Center => (
                    get_x_offset_center(width * scale_factor, editor.0.buffer()),
                    get_y_offset_center(height * scale_factor, editor.0.buffer()),
                CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
                CosmicTextPosition::Left { padding } => (
                    *padding,
                    get_y_offset_center(height * scale_factor, editor.0.buffer()),
                ),
            };
            let point = |node_cursor_pos: (f32, f32)| {
                (
                    (node_cursor_pos.0 * scale_factor) as i32 - padding_x,
                    (node_cursor_pos.1 * scale_factor) as i32 - padding_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 (mut x, y) = point(node_cursor_pos);
                    x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
                        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 (mut x, y) = point(node_cursor_pos);
                    x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
                    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;
                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
                    is_edit = true;
                    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

            evw_changed.send(CosmicTextChanged((entity, editor.get_text())));

            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));
#[allow(clippy::too_many_arguments)]
StaffEngineer's avatar
StaffEngineer committed
fn redraw_buffer_common(
    mode: &CosmicMode,
    x_offset: &mut XOffset,
StaffEngineer's avatar
StaffEngineer committed
    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>,
StaffEngineer's avatar
StaffEngineer committed
    scale_factor: f32,
    original_width: f32,
    original_height: f32,
) {
    let widget_width = original_width * scale_factor;
    let widget_height = original_height * scale_factor;
StaffEngineer's avatar
StaffEngineer committed
    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];
                }
        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,
StaffEngineer's avatar
StaffEngineer committed
            }
    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,
            });
StaffEngineer's avatar
StaffEngineer committed
        }
    }

    editor.buffer_mut().set_redraw(false);
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,
        &CosmicTextPosition,
        &mut UiImage,
        &Node,
        &mut XOffset,
        &mut Style,
        &CosmicMode,
    )>,
    mut font_system: ResMut<CosmicFontSystem>,
StaffEngineer's avatar
StaffEngineer committed
) {
    let primary_window = windows.single();
    let scale = primary_window.scale_factor() as f32;

        background_image,
        text_position,
        mut img,
        node,
        mut x_offset,
    ) in &mut cosmic_edit_query.iter_mut()
    {
        editor.0.shape_as_needed(&mut font_system.0);
        if !editor.0.buffer().redraw() {
            continue;
        }

        let width = node.size().x;
        let mut height = node.size().y;
        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),
            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());
            let text_height = (text_size.1 / primary_window.scale_factor() as f32) + 30.;
            if text_height > height {
                height = text_height;
                style.height = Val::Px(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 img.texture,
            text_position,
            &mut font_system,
StaffEngineer's avatar
StaffEngineer committed
            width,
            height,
        );
    }
}

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

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

StaffEngineer's avatar
StaffEngineer committed
fn blink_cursor(
    mut visibility: ResMut<CursorVisibility>,
    mut timer: ResMut<CursorBlinkTimer>,
StaffEngineer's avatar
StaffEngineer committed
    time: Res<Time>,
StaffEngineer's avatar
StaffEngineer committed
    active_editor: ResMut<Focus>,
    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
StaffEngineer's avatar
StaffEngineer committed
    if let Some(e) = active_editor.0 {
        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
            timer.0.tick(time.delta());
            if !timer.0.just_finished() && !active_editor.is_changed() {
sam edelsten's avatar
sam edelsten committed
                return;
            }
            visibility.0 = !visibility.0;
sam edelsten's avatar
sam edelsten committed
            // always start cursor visible on focus
            if active_editor.is_changed() {
                visibility.0 = true;
                timer.0.set_elapsed(Duration::ZERO);
sam edelsten's avatar
sam edelsten committed

            let mut cursor = editor.0.cursor();
            let new_color = if visibility.0 {
sam edelsten's avatar
sam edelsten committed
                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);
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);
        }
    }
}

fn hide_inactive_or_readonly_cursor(
    mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>,
    mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>,
StaffEngineer's avatar
StaffEngineer committed
    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);
    }

    if active_editor.is_changed() || active_editor.0.is_none() {
StaffEngineer's avatar
StaffEngineer committed
        return;
    }
    for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() {
        if e != active_editor.0.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)>,
StaffEngineer's avatar
StaffEngineer committed
    active_editor: Res<Focus>,
StaffEngineer's avatar
StaffEngineer committed
    if !active_editor.is_changed() || active_editor.0.is_none() {
        return;
    }

    for (e, mut editor) in &mut cosmic_editor_q.iter_mut() {
StaffEngineer's avatar
StaffEngineer committed
        if e != active_editor.0.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,
        &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;
        }
        let width = sprite.custom_size.unwrap().x;
        let mut height = sprite.custom_size.unwrap().y;
        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());
            let text_height = (text_size.1 / primary_window.scale_factor() as f32) + 30.;
            if text_height > height {
                height = text_height;
                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,
        );
    }
}

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