From b382f14a556f6f0364ad9e70a799e9821ba76bb4 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:16:36 -0600 Subject: [PATCH] feat: add word wrap toggle functionality Add the ability to toggle word wrap in the text editor with Alt+Z. Word wrap can be enabled or disabled, affecting how long lines are displayed in edit mode. Changes: - Add `is_word_wrap_on` state field with default value of false - Implement `toggle_word_wrap()` and `is_word_wrap_on()` methods - Add ToggleWordWrap variant to ViewAction enum - Bind Alt+Z keyboard shortcut to toggle word wrap - Refactor editor view to support dynamic wrapping behavior - Use responsive widget when wrap is enabled to constrain width - Apply Wrapping::WordOrGlyph when enabled, Wrapping::None otherwise - Update editor view signature to accept word wrap state parameter - Fix indentation inconsistencies in file.rs and state.rs --- src/components/editor.rs | 99 +++++++++++++++++++++++++--------------- src/file.rs | 8 ++-- src/handler.rs | 1 + src/key_bindings.rs | 5 ++ src/main.rs | 9 +++- src/message.rs | 3 ++ src/state.rs | 16 ++++++- 7 files changed, 97 insertions(+), 44 deletions(-) diff --git a/src/components/editor.rs b/src/components/editor.rs index 5beebc1..8148878 100644 --- a/src/components/editor.rs +++ b/src/components/editor.rs @@ -2,58 +2,85 @@ use iced::{ Background, Element, Length, Theme, border::{self}, highlighter, - widget::{container, markdown, row, scrollable, text_editor}, + widget::{container, markdown, responsive, row, scrollable, text::Wrapping, text_editor}, }; use crate::{constants, file::File, message::Message, state::Mode}; -pub fn view<'a>(file: &'a File, mode: Mode, font_size: u32) -> Element<'a, Message> { +pub fn view<'a>( + file: &'a File, + mode: Mode, + font_size: u32, + is_word_wrap_on: bool, +) -> Element<'a, Message> { match mode { Mode::Edit => { - let editor = text_editor(file.content()) - .highlight( - file.extension().unwrap_or("txt"), - highlighter::Theme::Base16Ocean, - ) - .padding(10) - .size(font_size) - .style(|theme: &Theme, status: text_editor::Status| { - let base = text_editor::default(theme, status); + let wrapping = if is_word_wrap_on { + Wrapping::WordOrGlyph + } else { + Wrapping::None + }; - text_editor::Style { - border: border::Border { - width: 0.0, - ..Default::default() - }, - ..base - } - }) - .on_action(Message::Edit); + let create_editor = move || { + text_editor(file.content()) + .wrapping(wrapping) + .highlight( + file.extension().unwrap_or("txt"), + highlighter::Theme::Base16Ocean, + ) + .padding(10) + .size(font_size) + .style(|theme: &Theme, status: text_editor::Status| { + let base = text_editor::default(theme, status); - scrollable(container(editor).height(Length::Fill)) - .auto_scroll(true) - .direction(scrollable::Direction::Both { - vertical: scrollable::Scrollbar::default(), - horizontal: scrollable::Scrollbar::default(), + text_editor::Style { + border: border::Border { + width: 0.0, + ..Default::default() + }, + ..base + } + }) + .on_action(Message::Edit) + }; + + let create_scrollable = |content: Element<'a, Message>| { + scrollable(content) + .auto_scroll(true) + .direction(scrollable::Direction::Both { + vertical: scrollable::Scrollbar::default(), + horizontal: scrollable::Scrollbar::default(), + }) + .style(|theme: &Theme, status: scrollable::Status| { + let mut style = scrollable::default(theme, status); + style.horizontal_rail.background = Some(Background::Color(theme.palette().background)); + style.horizontal_rail.scroller.background = Background::Color(theme.palette().primary); + style.vertical_rail.background = Some(Background::Color(theme.palette().background)); + style.vertical_rail.scroller.background = Background::Color(theme.palette().primary); + style + }) + .anchor_bottom() + .height(Length::Fill) + }; + + if is_word_wrap_on { + responsive(move |size| { + let editor = create_editor(); + create_scrollable(container(editor).width(size.width).into()).into() }) - .style(|theme: &Theme, status: scrollable::Status| { - let mut style = scrollable::default(theme, status); - style.horizontal_rail.background = Some(Background::Color(theme.palette().background)); - style.horizontal_rail.scroller.background = Background::Color(theme.palette().primary); - style.vertical_rail.background = Some(Background::Color(theme.palette().background)); - style.vertical_rail.scroller.background = Background::Color(theme.palette().primary); - style - }) - .anchor_bottom() - .height(Length::Fill) .into() + } else { + let editor = create_editor(); + create_scrollable(container(editor).height(Length::Fill).into()) + .width(Length::Fill) + .into() + } } Mode::Preview => { let mut style: markdown::Style = Theme::Dark.into(); style.font = constants::CUSTOM_FONT; let settings = markdown::Settings::with_text_size(font_size, style); - let markdown_preview = markdown::view(file.markdown(), settings).map(Message::LinkClicked); scrollable(row![markdown_preview].padding(10)) diff --git a/src/file.rs b/src/file.rs index 1570a20..3f351d2 100644 --- a/src/file.rs +++ b/src/file.rs @@ -93,12 +93,12 @@ impl File { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default() } - - pub fn needs_saving(&self) -> bool { - self.needs_saving + + pub fn needs_saving(&self) -> bool { + self.needs_saving } pub fn set_needs_saving(&mut self, state: bool) { - self.needs_saving = state + self.needs_saving = state } } diff --git a/src/handler.rs b/src/handler.rs index 349f560..92adb5e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -76,6 +76,7 @@ pub fn view_action(state: &mut State, action: ViewAction) -> Task { ViewAction::Decrease => state.decrease_font(), ViewAction::Reset => state.reset_font(), ViewAction::TogglePreview => state.toggle_preview(), + ViewAction::ToggleWordWrap => state.toggle_word_wrap(), } Task::none() diff --git a/src/key_bindings.rs b/src/key_bindings.rs index 192146f..b2e8c64 100644 --- a/src/key_bindings.rs +++ b/src/key_bindings.rs @@ -65,4 +65,9 @@ pub const ALL: &[Keybinding] = &[ modifiers: Modifiers::CTRL, message: Message::ViewActionSelected(ViewAction::Reset), }, + Keybinding { + key: "z", + modifiers: Modifiers::ALT, + message: Message::ViewActionSelected(ViewAction::ToggleWordWrap), + }, ]; diff --git a/src/main.rs b/src/main.rs index 2877ea4..c262d8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ mod state; use iced::theme::Palette; use iced::widget::column; use iced::window::icon; -use iced::{Color, Theme}; +use iced::{Color, Length, Theme}; use iced::{Element, Subscription, Task, event}; use iced::{keyboard, window}; @@ -84,7 +84,12 @@ fn view(state: &State, _id: iced::window::Id) -> Element<'_, Message> { column![ components::tabs::view(state.files(), state.current_file_index()), components::action_bar::view(state.selected_file_action(), state.selected_view_action()), - components::editor::view(current_file, state.mode(), state.font_size()), + components::editor::view( + current_file, + state.mode(), + state.font_size(), + state.is_word_wrap_on() + ), components::status_bar::view(current_file), ] .into() diff --git a/src/message.rs b/src/message.rs index 356ddeb..53d348e 100644 --- a/src/message.rs +++ b/src/message.rs @@ -39,6 +39,7 @@ pub enum ViewAction { Decrease, Reset, TogglePreview, + ToggleWordWrap, } impl ViewAction { @@ -47,6 +48,7 @@ impl ViewAction { ViewAction::Decrease, ViewAction::Reset, ViewAction::TogglePreview, + ViewAction::ToggleWordWrap, ]; } @@ -57,6 +59,7 @@ impl Display for ViewAction { ViewAction::Increase => write!(f, "Increase font"), ViewAction::Reset => write!(f, "Reset font"), ViewAction::TogglePreview => write!(f, "Toggle preview"), + ViewAction::ToggleWordWrap => write!(f, "Toggle word wrap"), } } } diff --git a/src/state.rs b/src/state.rs index 1e852fd..f31ec1f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -22,6 +22,7 @@ pub struct State { editor_font_size: u32, selected_file_action: Option, selected_view_action: Option, + is_word_wrap_on: bool, } impl State { @@ -33,6 +34,7 @@ impl State { files: vec![default_file], current_file: 0, editor_font_size: constants::DEFAULT_EDITOR_FONT_SIZE, + is_word_wrap_on: false, ..Default::default() } } @@ -42,8 +44,10 @@ impl State { } pub fn apply_edit(&mut self, action: text_editor::Action) { - self.files[self.current_file].content_mut().perform(action.clone()); - + self.files[self.current_file] + .content_mut() + .perform(action.clone()); + if matches!(action, text_editor::Action::Edit(_)) { self.files[self.current_file].set_needs_saving(true); } @@ -129,6 +133,14 @@ impl State { } } + pub fn toggle_word_wrap(&mut self) { + self.is_word_wrap_on = !self.is_word_wrap_on; + } + + pub fn is_word_wrap_on(&self) -> bool { + self.is_word_wrap_on + } + pub fn files(&self) -> &[file::File] { &self.files }