diff --git a/examples/restricted_input.rs b/examples/restricted_input.rs new file mode 100644 index 0000000000000000000000000000000000000000..c43c67814d312cbdc52df9aaa13ae89f12fd054f --- /dev/null +++ b/examples/restricted_input.rs @@ -0,0 +1,61 @@ +use bevy::prelude::*; +use bevy_cosmic_edit::{ + change_active_editor_sprite, change_active_editor_ui, ActiveEditor, CosmicAttrs, + CosmicEditPlugin, CosmicEditUiBundle, CosmicFontSystem, CosmicMaxChars, CosmicMaxLines, + CosmicMetrics, CosmicText, +}; +use cosmic_text::{Attrs, AttrsOwned}; + +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { + commands.spawn(Camera2dBundle::default()); + + let attrs = AttrsOwned::new(Attrs::new().color(cosmic_text::Color::rgb(69, 69, 69))); + + let editor = commands + .spawn( + CosmicEditUiBundle { + border_color: Color::LIME_GREEN.into(), + style: Style { + // Size and position of text box + border: UiRect::all(Val::Px(4.)), + width: Val::Percent(20.), + height: Val::Px(50.), + left: Val::Percent(40.), + top: Val::Px(100.), + ..default() + }, + cosmic_attrs: CosmicAttrs(attrs.clone()), + cosmic_metrics: CosmicMetrics { + font_size: 16., + line_height: 16., + ..Default::default() + }, + max_chars: CosmicMaxChars(15), + max_lines: CosmicMaxLines(1), + ..default() + } + .set_text( + CosmicText::OneStyle( + "1 line 15 chars! But this\n is longer\n than is\n allowed by\n the limits.\n" + .into(), + ), + attrs, + &mut font_system.0, + ), + ) + .id(); + + commands.insert_resource(ActiveEditor { + entity: Some(editor), + }); +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CosmicEditPlugin::default()) + .add_systems(Startup, setup) + .add_systems(Update, change_active_editor_ui) + .add_systems(Update, change_active_editor_sprite) + .run(); +} diff --git a/src/lib.rs b/src/lib.rs index ce3c3489063e023eaf44397a714002f5cc07fde6..3c5dca71b1a92c77914ef8992916e6223af000ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ pub enum CosmicTextPosition { TopLeft, } +// TODO docs #[derive(Clone, Component)] pub struct CosmicMetrics { pub font_size: f32, @@ -202,6 +203,12 @@ impl Default for CosmicAttrs { #[derive(Component, Default)] pub struct CosmicBackground(pub Option<Handle<Image>>); +#[derive(Component, Default)] +pub struct CosmicMaxLines(pub usize); + +#[derive(Component, Default)] +pub struct CosmicMaxChars(pub usize); + #[derive(Bundle)] pub struct CosmicEditUiBundle { // Bevy UI bits @@ -251,6 +258,10 @@ pub struct CosmicEditUiBundle { pub cosmic_attrs: CosmicAttrs, /// bg img pub background_image: CosmicBackground, + /// How many lines are allowed in buffer, 0 for no limit + pub max_lines: CosmicMaxLines, + /// How many characters are allowed in buffer, 0 for no limit + pub max_chars: CosmicMaxChars, } impl CosmicEditUiBundle { @@ -260,11 +271,93 @@ impl CosmicEditUiBundle { attrs: AttrsOwned, font_system: &mut FontSystem, ) -> Self { + let text = trim_text(text, self.max_chars.0, self.max_lines.0); self.editor.set_text(text, attrs, font_system); self } } +fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText { + if max_chars == 0 && max_lines == 0 { + // no limits, no work to do + return text; + } + + match text { + CosmicText::OneStyle(mut string) => { + string.truncate(max_chars); + + if max_lines == 0 { + return CosmicText::OneStyle(string); + } + + let mut line_acc = 0; + let mut char_pos = 0; + for c in string.chars() { + char_pos += 1; + if c == 0xA as char { + line_acc += 1; + if line_acc >= max_lines { + // break text to pos + string.truncate(char_pos); + break; + } + } + } + + CosmicText::OneStyle(string) + } + CosmicText::MultiStyle(lines) => { + let mut char_acc = 0; + let mut line_acc = 0; + + let mut trimmed_styles = vec![]; + + for line in lines.iter() { + line_acc += 1; + char_acc += 1; // count newlines for consistent behaviour + + if (line_acc >= max_lines && max_lines > 0) + || (char_acc >= max_chars && max_chars > 0) + { + break; + } + + let mut strs = vec![]; + + for (string, attrs) in line.iter() { + if char_acc >= max_chars && max_chars > 0 { + break; + } + + let mut string = string.clone(); + + if max_chars > 0 { + string.truncate(max_chars - char_acc); + char_acc += string.len(); + } + + if max_lines > 0 { + for c in string.chars() { + if c == 0xA as char { + line_acc += 1; + char_acc += 1; // count newlines for consistent behaviour + if line_acc >= max_lines { + break; + } + } + } + } + + strs.push((string, attrs.clone())); + } + trimmed_styles.push(strs); + } + CosmicText::MultiStyle(trimmed_styles) + } + } +} + impl Default for CosmicEditUiBundle { fn default() -> Self { Self { @@ -287,6 +380,8 @@ impl Default for CosmicEditUiBundle { cosmic_edit_history: Default::default(), cosmic_attrs: Default::default(), background_image: Default::default(), + max_lines: Default::default(), + max_chars: Default::default(), } } } @@ -317,6 +412,10 @@ pub struct CosmicEditSpriteBundle { pub cosmic_attrs: CosmicAttrs, /// bg img pub background_image: CosmicBackground, + /// How many lines are allowed in buffer, 0 for no limit + pub max_lines: CosmicMaxLines, + /// How many characters are allowed in buffer, 0 for no limit + pub max_chars: CosmicMaxChars, } impl CosmicEditSpriteBundle { @@ -326,6 +425,7 @@ impl CosmicEditSpriteBundle { attrs: AttrsOwned, font_system: &mut FontSystem, ) -> Self { + let text = trim_text(text, self.max_chars.0, self.max_lines.0); self.editor.set_text(text, attrs, font_system); self } @@ -340,15 +440,15 @@ impl Default for CosmicEditSpriteBundle { texture: DEFAULT_IMAGE_HANDLE.typed(), visibility: Visibility::Hidden, computed_visibility: Default::default(), - // background_color: Default::default(), - // editor: Default::default(), text_position: Default::default(), cosmic_metrics: Default::default(), cosmic_edit_history: Default::default(), cosmic_attrs: Default::default(), background_image: Default::default(), + max_lines: Default::default(), + max_chars: Default::default(), } } } @@ -608,6 +708,8 @@ pub fn cosmic_edit_bevy_events( &GlobalTransform, &CosmicAttrs, &CosmicTextPosition, + &CosmicMaxLines, + &CosmicMaxChars, Entity, ), With<CosmicEditor>, @@ -625,8 +727,16 @@ pub fn cosmic_edit_bevy_events( let primary_window = windows.single(); let scale_factor = primary_window.scale_factor() as f32; let (camera, camera_transform) = camera_q.iter().find(|(c, _)| c.is_active).unwrap(); - for (mut editor, mut edit_history, node_transform, attrs, text_position, entity) in - &mut cosmic_edit_query.iter_mut() + for ( + mut editor, + mut edit_history, + node_transform, + attrs, + text_position, + max_lines, + max_chars, + entity, + ) in &mut cosmic_edit_query.iter_mut() { let readonly = readonly_query.get(entity).is_ok(); @@ -639,7 +749,6 @@ pub fn cosmic_edit_bevy_events( } }; - let editor = &mut editor.0; let attrs = &attrs.0; if active_editor.entity == Some(entity) { @@ -657,10 +766,10 @@ pub fn cosmic_edit_bevy_events( let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]); // if shift key is pressed - let already_has_selection = editor.select_opt().is_some(); + let already_has_selection = editor.0.select_opt().is_some(); if shift && !already_has_selection { - let cursor = editor.cursor(); - editor.set_select_opt(Some(cursor)); + let cursor = editor.0.cursor(); + editor.0.set_select_opt(Some(cursor)); } #[cfg(target_os = "macos")] @@ -669,87 +778,87 @@ pub fn cosmic_edit_bevy_events( let should_jump = command; if should_jump && keys.just_pressed(KeyCode::Left) { - editor.action(&mut font_system.0, Action::PreviousWord); + editor.0.action(&mut font_system.0, Action::PreviousWord); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if should_jump && keys.just_pressed(KeyCode::Right) { - editor.action(&mut font_system.0, Action::NextWord); + editor.0.action(&mut font_system.0, Action::NextWord); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if should_jump && keys.just_pressed(KeyCode::Home) { - editor.action(&mut font_system.0, Action::BufferStart); + editor.0.action(&mut font_system.0, Action::BufferStart); // there's a bug with cosmic text where it doesn't update the visual cursor for this action // TODO: fix upstream - editor.buffer_mut().set_redraw(true); + editor.0.buffer_mut().set_redraw(true); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if should_jump && keys.just_pressed(KeyCode::End) { - editor.action(&mut font_system.0, Action::BufferEnd); + editor.0.action(&mut font_system.0, Action::BufferEnd); // there's a bug with cosmic text where it doesn't update the visual cursor for this action // TODO: fix upstream - editor.buffer_mut().set_redraw(true); + editor.0.buffer_mut().set_redraw(true); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::Left) { - editor.action(&mut font_system.0, Action::Left); + editor.0.action(&mut font_system.0, Action::Left); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::Right) { - editor.action(&mut font_system.0, Action::Right); + editor.0.action(&mut font_system.0, Action::Right); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::Up) { - editor.action(&mut font_system.0, Action::Up); + editor.0.action(&mut font_system.0, Action::Up); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::Down) { - editor.action(&mut font_system.0, Action::Down); + editor.0.action(&mut font_system.0, Action::Down); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if !readonly && keys.just_pressed(KeyCode::Back) { #[cfg(target_arch = "wasm32")] - editor.action(&mut font_system.0, Action::Backspace); + editor.0.action(&mut font_system.0, Action::Backspace); *is_deleting = true; } if !readonly && keys.just_released(KeyCode::Back) { *is_deleting = false; } if !readonly && keys.just_pressed(KeyCode::Delete) { - editor.action(&mut font_system.0, Action::Delete); + editor.0.action(&mut font_system.0, Action::Delete); } if keys.just_pressed(KeyCode::Escape) { - editor.action(&mut font_system.0, Action::Escape); + editor.0.action(&mut font_system.0, Action::Escape); } if command && keys.just_pressed(KeyCode::A) { - editor.action(&mut font_system.0, Action::BufferEnd); - let current_cursor = editor.cursor(); - editor.set_select_opt(Some(Cursor { + editor.0.action(&mut font_system.0, Action::BufferEnd); + let current_cursor = editor.0.cursor(); + editor.0.set_select_opt(Some(Cursor { line: 0, index: 0, affinity: current_cursor.affinity, @@ -758,30 +867,30 @@ pub fn cosmic_edit_bevy_events( return; } if keys.just_pressed(KeyCode::Home) { - editor.action(&mut font_system.0, Action::Home); + editor.0.action(&mut font_system.0, Action::Home); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::End) { - editor.action(&mut font_system.0, Action::End); + editor.0.action(&mut font_system.0, Action::End); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::PageUp) { - editor.action(&mut font_system.0, Action::PageUp); + editor.0.action(&mut font_system.0, Action::PageUp); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } if keys.just_pressed(KeyCode::PageDown) { - editor.action(&mut font_system.0, Action::PageDown); + editor.0.action(&mut font_system.0, Action::PageDown); if !shift { - editor.set_select_opt(None); + editor.0.set_select_opt(None); } return; } @@ -802,7 +911,7 @@ pub fn cosmic_edit_bevy_events( } let idx = edit_history.current_edit + 1; if let Some(current_edit) = edits.get(idx) { - editor.buffer_mut().lines.clear(); + editor.0.buffer_mut().lines.clear(); for line in current_edit.lines.iter() { let mut line_text = String::new(); let mut attrs_list = AttrsList::new(attrs.as_attrs()); @@ -812,14 +921,14 @@ pub fn cosmic_edit_bevy_events( let end = line_text.len(); attrs_list.add_span(start..end, attrs.as_attrs()); } - editor.buffer_mut().lines.push(BufferLine::new( + editor.0.buffer_mut().lines.push(BufferLine::new( line_text, attrs_list, Shaping::Advanced, )); } - editor.set_cursor(current_edit.cursor); - editor.buffer_mut().set_redraw(true); + editor.0.set_cursor(current_edit.cursor); + editor.0.buffer_mut().set_redraw(true); edit_history.current_edit += 1; } *undoredo_duration = Some(Duration::from_millis(now_ms as u64)); @@ -838,7 +947,7 @@ pub fn cosmic_edit_bevy_events( } let idx = edit_history.current_edit - 1; if let Some(current_edit) = edits.get(idx) { - editor.buffer_mut().lines.clear(); + editor.0.buffer_mut().lines.clear(); for line in current_edit.lines.iter() { let mut line_text = String::new(); let mut attrs_list = AttrsList::new(attrs.as_attrs()); @@ -848,14 +957,14 @@ pub fn cosmic_edit_bevy_events( let end = line_text.len(); attrs_list.add_span(start..end, attrs.as_attrs()); } - editor.buffer_mut().lines.push(BufferLine::new( + editor.0.buffer_mut().lines.push(BufferLine::new( line_text, attrs_list, Shaping::Advanced, )); } - editor.set_cursor(current_edit.cursor); - editor.buffer_mut().set_redraw(true); + editor.0.set_cursor(current_edit.cursor); + editor.0.buffer_mut().set_redraw(true); edit_history.current_edit -= 1; } *undoredo_duration = Some(Duration::from_millis(now_ms as u64)); @@ -867,22 +976,32 @@ pub fn cosmic_edit_bevy_events( { if let Ok(mut clipboard) = arboard::Clipboard::new() { if command && keys.just_pressed(KeyCode::C) { - if let Some(text) = editor.copy_selection() { + if let Some(text) = editor.0.copy_selection() { clipboard.set_text(text).unwrap(); return; } } if !readonly && command && keys.just_pressed(KeyCode::X) { - if let Some(text) = editor.copy_selection() { + if let Some(text) = editor.0.copy_selection() { clipboard.set_text(text).unwrap(); - editor.delete_selection(); + editor.0.delete_selection(); } is_clipboard = true; } if !readonly && command && keys.just_pressed(KeyCode::V) { if let Ok(text) = clipboard.get_text() { for c in text.chars() { - editor.action(&mut font_system.0, Action::Insert(c)); + 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)); + } + } } } is_clipboard = true; @@ -890,9 +1009,10 @@ pub fn cosmic_edit_bevy_events( } } let (offset_x, offset_y) = match text_position { - CosmicTextPosition::Center => { - (get_x_offset(editor.buffer()), get_y_offset(editor.buffer())) - } + 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)| { @@ -913,9 +1033,9 @@ pub fn cosmic_edit_bevy_events( ) { let (x, y) = point(node_cursor_pos); if shift { - editor.action(&mut font_system.0, Action::Drag { x, y }); + editor.0.action(&mut font_system.0, Action::Drag { x, y }); } else { - editor.action(&mut font_system.0, Action::Click { x, y }); + editor.0.action(&mut font_system.0, Action::Click { x, y }); } } return; @@ -931,9 +1051,9 @@ pub fn cosmic_edit_bevy_events( ) { let (x, y) = point(node_cursor_pos); if active_editor.is_changed() && !shift { - editor.action(&mut font_system.0, Action::Click { x, y }); + editor.0.action(&mut font_system.0, Action::Click { x, y }); } else { - editor.action(&mut font_system.0, Action::Drag { x, y }); + editor.0.action(&mut font_system.0, Action::Drag { x, y }); } } return; @@ -941,7 +1061,7 @@ pub fn cosmic_edit_bevy_events( for ev in scroll_evr.iter() { match ev.unit { MouseScrollUnit::Line => { - editor.action( + editor.0.action( &mut font_system.0, Action::Scroll { lines: -ev.y as i32, @@ -949,8 +1069,8 @@ pub fn cosmic_edit_bevy_events( ); } MouseScrollUnit::Pixel => { - let line_height = editor.buffer().metrics().line_height; - editor.action( + 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, @@ -965,9 +1085,10 @@ pub fn cosmic_edit_bevy_events( } // fix for issue #8 - if let Some(select) = editor.select_opt() { - if editor.cursor().line == select.line && editor.cursor().index == select.index { - editor.set_select_opt(None); + 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); } } @@ -976,17 +1097,23 @@ pub fn cosmic_edit_bevy_events( if keys.just_pressed(KeyCode::Return) { is_return = true; is_edit = true; - // to have new line on wasm rather than E - editor.action(&mut font_system.0, Action::Insert('\n')); + 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')); + } } if !(is_clipboard || is_return) { for char_ev in char_evr.iter() { is_edit = true; if *is_deleting { - editor.action(&mut font_system.0, Action::Backspace); - } else { - editor.action(&mut font_system.0, Action::Insert(char_ev.char)); + 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)); } } } @@ -999,11 +1126,11 @@ pub fn cosmic_edit_bevy_events( if Duration::from_millis(now_ms as u64) - last_edit_duration > Duration::from_millis(150) { - save_edit_history(editor, attrs, &mut edit_history); + save_edit_history(&mut editor.0, attrs, &mut edit_history); *edits_duration = Some(Duration::from_millis(now_ms as u64)); } } else { - save_edit_history(editor, attrs, &mut edit_history); + save_edit_history(&mut editor.0, attrs, &mut edit_history); *edits_duration = Some(Duration::from_millis(now_ms as u64)); } }