Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b2f021509 | ||
|
|
5f1bdfefca | ||
|
|
3273a43e20 | ||
|
|
61e439a1d4 |
@@ -1,6 +1,7 @@
|
|||||||
// src/canvas/actions/types.rs
|
// src/canvas/actions/types.rs
|
||||||
|
|
||||||
/// All available canvas actions
|
/// All available canvas actions
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum CanvasAction {
|
pub enum CanvasAction {
|
||||||
// Movement actions
|
// Movement actions
|
||||||
@@ -42,6 +43,7 @@ pub enum CanvasAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Result type for canvas actions
|
/// Result type for canvas actions
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
Success,
|
Success,
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ fn clip_with_indicator_line<'a>(s: &'a str, width: u16, indicator: char) -> Line
|
|||||||
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
|
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
const RIGHT_PAD: u16 = 3;
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
||||||
if max_cols == 0 {
|
if max_cols == 0 {
|
||||||
@@ -107,15 +110,9 @@ fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
const RIGHT_PAD: u16 = 3;
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn compute_h_scroll_with_padding(cursor_cols: u16, width: u16) -> (u16, u16) {
|
fn compute_h_scroll_with_padding(cursor_cols: u16, width: u16) -> (u16, u16) {
|
||||||
// Returns (h_scroll, left_cols). left_cols = 1 if a left indicator is shown.
|
|
||||||
// We pre-emptively keep the caret out of the last RIGHT_PAD columns.
|
|
||||||
let mut h = 0u16;
|
let mut h = 0u16;
|
||||||
// Two passes are enough to converge (second pass accounts for left indicator).
|
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let left_cols = if h > 0 { 1 } else { 0 };
|
let left_cols = if h > 0 { 1 } else { 0 };
|
||||||
let max_x_visible = width.saturating_sub(1 + RIGHT_PAD + left_cols);
|
let max_x_visible = width.saturating_sub(1 + RIGHT_PAD + left_cols);
|
||||||
@@ -130,21 +127,21 @@ fn compute_h_scroll_with_padding(cursor_cols: u16, width: u16) -> (u16, u16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn active_indicator_viewport(
|
fn render_active_line_with_indicator<T: CanvasTheme>(
|
||||||
s: &str,
|
typed_text: &str,
|
||||||
|
completion: Option<&str>,
|
||||||
width: u16,
|
width: u16,
|
||||||
indicator: char,
|
indicator: char,
|
||||||
cursor_chars: usize,
|
cursor_chars: usize,
|
||||||
_right_padding: u16, // kept for signature symmetry; we use RIGHT_PAD constant
|
theme: &T,
|
||||||
) -> (Line<'static>, u16, u16) {
|
) -> (Line<'static>, u16, u16) {
|
||||||
if width == 0 {
|
if width == 0 {
|
||||||
return (Line::from(""), 0, 0);
|
return (Line::from(""), 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total display width of the string and cursor display column
|
// Cursor display column
|
||||||
let total_cols = display_width(s);
|
|
||||||
let mut cursor_cols: u16 = 0;
|
let mut cursor_cols: u16 = 0;
|
||||||
for (i, ch) in s.chars().enumerate() {
|
for (i, ch) in typed_text.chars().enumerate() {
|
||||||
if i >= cursor_chars {
|
if i >= cursor_chars {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -152,22 +149,41 @@ fn active_indicator_viewport(
|
|||||||
.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-emptive scroll: never let caret enter the last RIGHT_PAD columns
|
|
||||||
let (h_scroll, left_cols) = compute_h_scroll_with_padding(cursor_cols, width);
|
let (h_scroll, left_cols) = compute_h_scroll_with_padding(cursor_cols, width);
|
||||||
|
|
||||||
// Right indicator if more content beyond the window start
|
let total_cols = display_width(typed_text);
|
||||||
let content_budget = width.saturating_sub(left_cols);
|
let content_budget = width.saturating_sub(left_cols);
|
||||||
let show_right = total_cols.saturating_sub(h_scroll) > content_budget;
|
let show_right = total_cols.saturating_sub(h_scroll) > content_budget;
|
||||||
let right_cols: u16 = if show_right { 1 } else { 0 };
|
let right_cols: u16 = if show_right { 1 } else { 0 };
|
||||||
|
|
||||||
let visible_cols = width.saturating_sub(left_cols + right_cols);
|
let visible_cols = width.saturating_sub(left_cols + right_cols);
|
||||||
let visible = slice_by_display_cols(s, h_scroll, visible_cols);
|
let visible_typed = slice_by_display_cols(typed_text, h_scroll, visible_cols);
|
||||||
|
|
||||||
|
let used_typed_cols = display_width(&visible_typed);
|
||||||
|
let mut remaining_cols = visible_cols.saturating_sub(used_typed_cols);
|
||||||
|
let mut visible_completion = String::new();
|
||||||
|
|
||||||
|
if let Some(comp) = completion {
|
||||||
|
if !comp.is_empty() && remaining_cols > 0 {
|
||||||
|
visible_completion = slice_by_display_cols(comp, 0, remaining_cols);
|
||||||
|
remaining_cols = remaining_cols.saturating_sub(display_width(&visible_completion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut spans: Vec<Span> = Vec::with_capacity(3);
|
let mut spans: Vec<Span> = Vec::with_capacity(3);
|
||||||
if left_cols == 1 {
|
if left_cols == 1 {
|
||||||
spans.push(Span::raw(indicator.to_string()));
|
spans.push(Span::raw(indicator.to_string()));
|
||||||
}
|
}
|
||||||
spans.push(Span::raw(visible));
|
spans.push(Span::styled(
|
||||||
|
visible_typed,
|
||||||
|
Style::default().fg(theme.fg()),
|
||||||
|
));
|
||||||
|
if !visible_completion.is_empty() {
|
||||||
|
spans.push(Span::styled(
|
||||||
|
visible_completion,
|
||||||
|
Style::default().fg(theme.suggestion_gray()),
|
||||||
|
));
|
||||||
|
}
|
||||||
if show_right {
|
if show_right {
|
||||||
spans.push(Span::raw(indicator.to_string()));
|
spans.push(Span::raw(indicator.to_string()));
|
||||||
}
|
}
|
||||||
@@ -175,7 +191,6 @@ fn active_indicator_viewport(
|
|||||||
(Line::from(spans), h_scroll, left_cols)
|
(Line::from(spans), h_scroll, left_cols)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default renderer: overflow indicator '$'
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub fn render_canvas<T: CanvasTheme, D: DataProvider>(
|
pub fn render_canvas<T: CanvasTheme, D: DataProvider>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -187,7 +202,6 @@ pub fn render_canvas<T: CanvasTheme, D: DataProvider>(
|
|||||||
render_canvas_with_options(f, area, editor, theme, opts)
|
render_canvas_with_options(f, area, editor, theme, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapped variant: opt into soft wrap instead of overflow indicator
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub fn render_canvas_with_options<T: CanvasTheme, D: DataProvider>(
|
pub fn render_canvas_with_options<T: CanvasTheme, D: DataProvider>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -198,10 +212,30 @@ pub fn render_canvas_with_options<T: CanvasTheme, D: DataProvider>(
|
|||||||
) -> Option<Rect> {
|
) -> Option<Rect> {
|
||||||
let highlight_state =
|
let highlight_state =
|
||||||
convert_selection_to_highlight(editor.ui_state().selection_state());
|
convert_selection_to_highlight(editor.ui_state().selection_state());
|
||||||
render_canvas_with_highlight_and_options(f, area, editor, theme, &highlight_state, opts)
|
|
||||||
|
#[cfg(feature = "suggestions")]
|
||||||
|
let active_completion = if editor.ui_state().is_suggestions_active()
|
||||||
|
&& editor.ui_state().suggestions.active_field
|
||||||
|
== Some(editor.ui_state().current_field())
|
||||||
|
{
|
||||||
|
editor.ui_state().suggestions.completion_text.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
#[cfg(not(feature = "suggestions"))]
|
||||||
|
let active_completion: Option<String> = None;
|
||||||
|
|
||||||
|
render_canvas_with_highlight_and_options(
|
||||||
|
f,
|
||||||
|
area,
|
||||||
|
editor,
|
||||||
|
theme,
|
||||||
|
&highlight_state,
|
||||||
|
active_completion,
|
||||||
|
opts,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render canvas with explicit highlight state (with options)
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn render_canvas_with_highlight_and_options<T: CanvasTheme, D: DataProvider>(
|
fn render_canvas_with_highlight_and_options<T: CanvasTheme, D: DataProvider>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -209,6 +243,7 @@ fn render_canvas_with_highlight_and_options<T: CanvasTheme, D: DataProvider>(
|
|||||||
editor: &FormEditor<D>,
|
editor: &FormEditor<D>,
|
||||||
theme: &T,
|
theme: &T,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
|
active_completion: Option<String>,
|
||||||
opts: CanvasDisplayOptions,
|
opts: CanvasDisplayOptions,
|
||||||
) -> Option<Rect> {
|
) -> Option<Rect> {
|
||||||
let ui_state = editor.ui_state();
|
let ui_state = editor.ui_state();
|
||||||
@@ -233,17 +268,6 @@ fn render_canvas_with_highlight_and_options<T: CanvasTheme, D: DataProvider>(
|
|||||||
let current_field_idx = ui_state.current_field();
|
let current_field_idx = ui_state.current_field();
|
||||||
let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit);
|
let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit);
|
||||||
|
|
||||||
#[cfg(feature = "suggestions")]
|
|
||||||
let active_completion = if ui_state.is_suggestions_active()
|
|
||||||
&& ui_state.suggestions.active_field == Some(current_field_idx)
|
|
||||||
{
|
|
||||||
ui_state.suggestions.completion_text.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
#[cfg(not(feature = "suggestions"))]
|
|
||||||
let active_completion: Option<String> = None;
|
|
||||||
|
|
||||||
render_canvas_fields_with_options(
|
render_canvas_fields_with_options(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
@@ -270,13 +294,7 @@ fn render_canvas_with_highlight_and_options<T: CanvasTheme, D: DataProvider>(
|
|||||||
},
|
},
|
||||||
#[cfg(not(feature = "validation"))]
|
#[cfg(not(feature = "validation"))]
|
||||||
|_field_idx| false,
|
|_field_idx| false,
|
||||||
|field_idx| {
|
active_completion,
|
||||||
if field_idx == current_field_idx {
|
|
||||||
active_completion.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
opts,
|
opts,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -302,7 +320,7 @@ fn convert_selection_to_highlight(
|
|||||||
|
|
||||||
/// Core canvas field rendering with options
|
/// Core canvas field rendering with options
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn render_canvas_fields_with_options<T: CanvasTheme, F1, F2, F3>(
|
fn render_canvas_fields_with_options<T: CanvasTheme, F1, F2>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
@@ -315,13 +333,12 @@ fn render_canvas_fields_with_options<T: CanvasTheme, F1, F2, F3>(
|
|||||||
has_unsaved_changes: bool,
|
has_unsaved_changes: bool,
|
||||||
get_display_value: F1,
|
get_display_value: F1,
|
||||||
has_display_override: F2,
|
has_display_override: F2,
|
||||||
get_completion: F3,
|
active_completion: Option<String>,
|
||||||
opts: CanvasDisplayOptions,
|
opts: CanvasDisplayOptions,
|
||||||
) -> Option<Rect>
|
) -> Option<Rect>
|
||||||
where
|
where
|
||||||
F1: Fn(usize) -> String,
|
F1: Fn(usize) -> String,
|
||||||
F2: Fn(usize) -> bool,
|
F2: Fn(usize) -> bool,
|
||||||
F3: Fn(usize) -> Option<String>,
|
|
||||||
{
|
{
|
||||||
let columns = Layout::default()
|
let columns = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
@@ -353,9 +370,6 @@ where
|
|||||||
|
|
||||||
let input_area = input_container.inner(input_block);
|
let input_area = input_container.inner(input_block);
|
||||||
|
|
||||||
// NOTE: We keep one visual row per field; Wrap mode renders wrapped content
|
|
||||||
// visually within that row (ratatui handles visual wrapping). To fully
|
|
||||||
// expand rows by wrapped height, we'd convert to per-field dynamic heights.
|
|
||||||
let input_rows = Layout::default()
|
let input_rows = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(vec![Constraint::Length(1); fields.len()])
|
.constraints(vec![Constraint::Length(1); fields.len()])
|
||||||
@@ -370,69 +384,71 @@ where
|
|||||||
let typed_text = get_display_value(i);
|
let typed_text = get_display_value(i);
|
||||||
let inner_width = input_rows[i].width;
|
let inner_width = input_rows[i].width;
|
||||||
|
|
||||||
|
// ---- BEGIN MODIFIED SECTION ----
|
||||||
let mut h_scroll_for_cursor: u16 = 0;
|
let mut h_scroll_for_cursor: u16 = 0;
|
||||||
let mut left_offset_for_cursor: u16 = 0;
|
let mut left_offset_for_cursor: u16 = 0;
|
||||||
|
|
||||||
let line = match (opts.overflow, highlight_state) {
|
let line = match highlight_state {
|
||||||
(OverflowMode::Indicator(ind), HighlightState::Off) => {
|
// Selection highlighting active: always use highlighting, even for the active field
|
||||||
if is_active {
|
HighlightState::Characterwise { .. } | HighlightState::Linewise { .. } => {
|
||||||
let (l, hs, left_cols) = active_indicator_viewport(
|
apply_highlighting(
|
||||||
&typed_text,
|
&typed_text,
|
||||||
inner_width,
|
i,
|
||||||
ind,
|
current_field_idx,
|
||||||
current_cursor_pos,
|
current_cursor_pos,
|
||||||
RIGHT_PAD,
|
highlight_state,
|
||||||
);
|
theme,
|
||||||
h_scroll_for_cursor = hs;
|
is_active,
|
||||||
left_offset_for_cursor = left_cols;
|
)
|
||||||
l
|
}
|
||||||
} else {
|
|
||||||
if display_width(&typed_text) <= inner_width {
|
// No selection highlighting
|
||||||
|
HighlightState::Off => match opts.overflow {
|
||||||
|
// Indicator mode: special-case the active field to preserve h-scroll + indicators
|
||||||
|
OverflowMode::Indicator(ind) => {
|
||||||
|
if is_active {
|
||||||
|
let (l, hs, left_cols) = render_active_line_with_indicator(
|
||||||
|
&typed_text,
|
||||||
|
active_completion.as_deref(),
|
||||||
|
inner_width,
|
||||||
|
ind,
|
||||||
|
current_cursor_pos,
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
h_scroll_for_cursor = hs;
|
||||||
|
left_offset_for_cursor = left_cols;
|
||||||
|
l
|
||||||
|
} else if display_width(&typed_text) <= inner_width {
|
||||||
Line::from(Span::raw(typed_text.clone()))
|
Line::from(Span::raw(typed_text.clone()))
|
||||||
} else {
|
} else {
|
||||||
clip_with_indicator_line(&typed_text, inner_width, ind)
|
clip_with_indicator_line(&typed_text, inner_width, ind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Existing highlighting paths (unchanged)
|
// Wrap mode: keep active completion for active line
|
||||||
(OverflowMode::Indicator(_ind), HighlightState::Characterwise { .. }) => {
|
OverflowMode::Wrap => {
|
||||||
apply_highlighting(
|
if is_active {
|
||||||
&typed_text,
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
i,
|
spans.push(Span::styled(
|
||||||
current_field_idx,
|
typed_text.clone(),
|
||||||
current_cursor_pos,
|
Style::default().fg(theme.fg()),
|
||||||
highlight_state,
|
));
|
||||||
theme,
|
if let Some(completion) = &active_completion {
|
||||||
is_active,
|
if !completion.is_empty() {
|
||||||
)
|
spans.push(Span::styled(
|
||||||
}
|
completion.clone(),
|
||||||
(OverflowMode::Indicator(_ind), HighlightState::Linewise { .. }) => {
|
Style::default().fg(theme.suggestion_gray()),
|
||||||
apply_highlighting(
|
));
|
||||||
&typed_text,
|
}
|
||||||
i,
|
}
|
||||||
current_field_idx,
|
Line::from(spans)
|
||||||
current_cursor_pos,
|
} else {
|
||||||
highlight_state,
|
Line::from(Span::raw(typed_text.clone()))
|
||||||
theme,
|
}
|
||||||
is_active,
|
}
|
||||||
)
|
},
|
||||||
}
|
};
|
||||||
|
// ---- END MODIFIED SECTION ----
|
||||||
// Wrap mode unchanged (Paragraph::wrap will handle it)
|
|
||||||
(OverflowMode::Wrap, HighlightState::Off) => Line::from(Span::raw(typed_text.clone())),
|
|
||||||
(OverflowMode::Wrap, _) => {
|
|
||||||
apply_highlighting(
|
|
||||||
&typed_text,
|
|
||||||
i,
|
|
||||||
current_field_idx,
|
|
||||||
current_cursor_pos,
|
|
||||||
highlight_state,
|
|
||||||
theme,
|
|
||||||
is_active,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut p = Paragraph::new(line).alignment(Alignment::Left);
|
let mut p = Paragraph::new(line).alignment(Alignment::Left);
|
||||||
|
|
||||||
@@ -675,10 +691,8 @@ fn set_cursor_position_scrolled(
|
|||||||
cols = cols.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
cols = cols.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visible x = (cursor columns - scroll) + left indicator column (if any)
|
|
||||||
let mut visible_x = cols.saturating_sub(h_scroll).saturating_add(left_offset);
|
let mut visible_x = cols.saturating_sub(h_scroll).saturating_add(left_offset);
|
||||||
|
|
||||||
// Hard clamp: keep RIGHT_PAD columns free at the right border
|
|
||||||
let limit = field_rect.width.saturating_sub(1 + RIGHT_PAD);
|
let limit = field_rect.width.saturating_sub(1 + RIGHT_PAD);
|
||||||
if visible_x > limit {
|
if visible_x > limit {
|
||||||
visible_x = limit;
|
visible_x = limit;
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ pub mod state;
|
|||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
#[cfg(feature = "keymaps")]
|
|
||||||
pub mod commands_impl;
|
|
||||||
|
|
||||||
pub use provider::TextAreaProvider;
|
pub use provider::TextAreaProvider;
|
||||||
pub use state::{TextAreaEditor, TextAreaState, TextOverflowMode};
|
pub use state::{TextAreaEditor, TextAreaState, TextOverflowMode};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// src/textarea/state.rs
|
// src/textarea/state.rs
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
|
||||||
use crate::editor::FormEditor;
|
use crate::editor::FormEditor;
|
||||||
@@ -14,45 +13,6 @@ use ratatui::{layout::Rect, widgets::Block};
|
|||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
pub(crate) fn wrapped_rows(s: &str, width: u16) -> u16 {
|
|
||||||
if width == 0 {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
let mut rows: u16 = 1;
|
|
||||||
let mut cols: u16 = 0;
|
|
||||||
for ch in s.chars() {
|
|
||||||
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
|
||||||
if cols.saturating_add(w) > width {
|
|
||||||
rows = rows.saturating_add(1);
|
|
||||||
cols = 0;
|
|
||||||
}
|
|
||||||
cols = cols.saturating_add(w);
|
|
||||||
}
|
|
||||||
rows
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
pub(crate) fn wrapped_rows_to_cursor(s: &str, width: u16, cursor_chars: usize) -> (u16, u16) {
|
|
||||||
if width == 0 {
|
|
||||||
return (0, 0);
|
|
||||||
}
|
|
||||||
let mut row: u16 = 0;
|
|
||||||
let mut cols: u16 = 0;
|
|
||||||
for (i, ch) in s.chars().enumerate() {
|
|
||||||
if i >= cursor_chars {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
|
||||||
if cols.saturating_add(w) > width {
|
|
||||||
row = row.saturating_add(1);
|
|
||||||
cols = 0;
|
|
||||||
}
|
|
||||||
cols = cols.saturating_add(w);
|
|
||||||
}
|
|
||||||
(row, cols)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub(crate) const RIGHT_PAD: u16 = 3;
|
pub(crate) const RIGHT_PAD: u16 = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ratatui::{
|
|||||||
style::Style,
|
style::Style,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, BorderType, Borders, Paragraph, StatefulWidget, Widget, Wrap,
|
Block, BorderType, Borders, Paragraph, StatefulWidget, Widget,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,31 +87,6 @@ fn display_cols_up_to(s: &str, char_count: usize) -> u16 {
|
|||||||
cols
|
cols
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
fn clip_with_indicator(s: &str, width: u16, indicator: char) -> Line<'static> {
|
|
||||||
if width == 0 {
|
|
||||||
return Line::from("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if display_width(s) <= width {
|
|
||||||
return Line::from(Span::raw(s.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let budget = width.saturating_sub(1);
|
|
||||||
let mut out = String::new();
|
|
||||||
let mut used: u16 = 0;
|
|
||||||
for ch in s.chars() {
|
|
||||||
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
|
||||||
if used + w > budget {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
out.push(ch);
|
|
||||||
used = used.saturating_add(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
||||||
if max_cols == 0 {
|
if max_cols == 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user