Skip to content

feat: add welcome page#80

Merged
OlaProeis merged 1 commit intoOlaProeis:masterfrom
blizzard007dev:welcome_tab
Feb 17, 2026
Merged

feat: add welcome page#80
OlaProeis merged 1 commit intoOlaProeis:masterfrom
blizzard007dev:welcome_tab

Conversation

@blizzard007dev
Copy link

@blizzard007dev blizzard007dev commented Feb 17, 2026

Description

Brief description of the changes in this PR.

Fixes #(issue number)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

Changes Made

  • Change 1
  • Change 2
  • Change 3

Screenshots

If this PR includes UI changes, please add before/after screenshots:

Before After

Checklist

  • My code follows the project's code style
  • I have run cargo fmt and it produces no changes
  • I have run cargo clippy and it produces no warnings
  • I have run cargo test and all tests pass
  • I have run cargo build --release successfully
  • I have updated documentation if needed
  • I have added tests for new functionality (if applicable)
  • My changes generate no new warnings

Breaking Changes

If this PR introduces breaking changes, describe what changes users need to make:

Testing

Describe how you tested these changes:

  1. Test case 1
  2. Test case 2

Additional Notes

Any additional information that reviewers should know.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added welcome screen that displays on application startup
    • Introduced welcome panel for configuring application theme, font preferences, editor line width, word wrap, line numbers, minimap display, bracket highlighting, and auto-save defaults

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a welcome panel feature to the Ferrite application. It adds a new WelcomePanel UI component that displays on startup, expands the SpecialTabKind enum with a Welcome variant, introduces constants for large-file handling, and integrates welcome panel initialization into the app lifecycle through a new open_welcome_on_startup() method.

Changes

Cohort / File(s) Summary
App Initialization
src/app/mod.rs, src/main.rs
Introduces FerriteApp::open_welcome_on_startup() method and welcome_panel: WelcomePanel field. Main entry point invokes the startup method during initialization. Constructor initializes the welcome panel.
State Management
src/state.rs
Adds Welcome variant to SpecialTabKind enum, introduces show_welcome_tab() method to AppState, and exposes new constants: LARGE_FILE_THRESHOLD (1 MB), LARGE_FILE_MAX_UNDO (10), and COMMON_ENCODINGS public static list for encoding selection.
UI Components
src/ui/mod.rs, src/ui/welcome.rs
New welcome module with WelcomePanel component, WelcomeSection enum, and WelcomePanelOutput struct. Provides both modal overlay and inline rendering with theme selection, CJK font preferences, line width configuration, and various editor settings toggles.

Sequence Diagram

sequenceDiagram
    participant Main as Main Entry
    participant App as FerriteApp
    participant State as AppState
    participant UI as WelcomePanel

    Main->>App: new() + initialization
    App->>App: initialize welcome_panel: WelcomePanel::new()
    Main->>App: open_welcome_on_startup()
    App->>State: show_welcome_tab()
    State->>State: create Welcome tab variant
    App->>UI: show() or show_inline()
    UI->>UI: render welcome modal/inline
    Note over UI: Theme, CJK, Settings, Width Config
    UI-->>App: WelcomePanelOutput { close_requested }
    App->>App: handle welcome panel close/settings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A welcome awaits, fresh and bright,
With panels and themes—dark or light!
Settings dance, encodings gleam,
Large files handled, a coder's dream,
Ferrite's heart now warmly beams! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add welcome page' directly and clearly summarizes the main change—introducing a new welcome page feature across multiple files (new WelcomePanel struct, open_welcome_on_startup method, and Welcome tab variant).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/state.rs (1)

2318-2359: ⚠️ Potential issue | 🔴 Critical

Fix UTF‑8 index handling in search/selection slices.

Selection and cursor positions are documented as character indices, but find_next_occurrence and get_primary_selection_text slice strings using those indices and mix them with search_text.len() (bytes). This can panic on non‑ASCII input and return incorrect ranges. Convert char indices to byte indices (and back) or standardize on byte indices throughout.

#!/bin/bash
# Verify call sites and index semantics for find_next_occurrence.
rg -n "find_next_occurrence\\(" -g '!**/target/**'
rg -n "Selection::cursor\\(|Selection::new\\(|cursor_char_index" src/state.rs
🐛 Suggested fix (char → byte conversion)
     pub fn find_next_occurrence(
         &self,
         search_text: &str,
         after_pos: usize
     ) -> Option<(usize, usize)> {
         if search_text.is_empty() {
             return None;
         }
+
+        let to_byte = |char_idx: usize| {
+            self.content
+                .char_indices()
+                .nth(char_idx)
+                .map(|(i, _)| i)
+                .unwrap_or(self.content.len())
+        };
+        let after_byte = to_byte(after_pos);

         // Search from after_pos to end
-        if let Some(rel_pos) = self.content[after_pos..].find(search_text) {
-            let start = after_pos + rel_pos;
-            let end = start + search_text.len();
-            return Some((start, end));
+        if let Some(rel_byte) = self.content[after_byte..].find(search_text) {
+            let start_byte = after_byte + rel_byte;
+            let end_byte = start_byte + search_text.len();
+            let start = self.content[..start_byte].chars().count();
+            let end = self.content[..end_byte].chars().count();
+            return Some((start, end));
         }

         // Wrap around: search from beginning to after_pos
-        if let Some(rel_pos) = self.content[..after_pos].find(search_text) {
-            let end = rel_pos + search_text.len();
-            return Some((rel_pos, end));
+        if let Some(rel_byte) = self.content[..after_byte].find(search_text) {
+            let end_byte = rel_byte + search_text.len();
+            let start = self.content[..rel_byte].chars().count();
+            let end = self.content[..end_byte].chars().count();
+            return Some((start, end));
         }

         None
     }
@@
     pub fn get_primary_selection_text(&self) -> Option<String> {
         let primary = self.cursors.primary();

         if primary.is_selection() {
             // Return selected text
             let (start, end) = primary.range();
-            if end <= self.content.len() {
-                return Some(self.content[start..end].to_string());
-            }
+            let to_byte = |char_idx: usize| {
+                self.content
+                    .char_indices()
+                    .nth(char_idx)
+                    .map(|(i, _)| i)
+                    .unwrap_or(self.content.len())
+            };
+            let start_byte = to_byte(start);
+            let end_byte = to_byte(end);
+            if let Some(slice) = self.content.get(start_byte..end_byte) {
+                return Some(slice.to_string());
+            }
         } else {
             // No selection: find word at cursor
             return self.word_at_position(primary.head);
         }

         None
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state.rs` around lines 2318 - 2359, find_next_occurrence and
get_primary_selection_text treat cursor/selection values as char indices but use
them to slice by bytes, which breaks on UTF‑8; convert char indices to byte
offsets before slicing or searching. Specifically: in find_next_occurrence
convert after_pos (char index) to a byte_offset (e.g. sum of
.chars().take(after_pos).map(|c| c.len_utf8())) and use that byte_offset when
slicing self.content[..] and when computing start/end bytes; compute end bytes
as start_byte + search_text.len() (bytes). In get_primary_selection_text convert
primary.range() (start_char,end_char) and primary.head (char index) to byte
offsets before slicing or passing into word_at_position, and update
word_at_position/Selection handling if it currently assumes byte indices so all
callers are consistent; ensure all bounds checks compare against
self.content.len() (bytes).
🧹 Nitpick comments (1)
src/ui/welcome.rs (1)

137-139: Use a Welcome‑specific overlay id.

about_overlay looks reused from the About panel and could collide if both overlays coexist. A unique id keeps egui state isolated.

♻️ Suggested tweak
-        egui::Area
-            ::new(egui::Id::new("about_overlay"))
+        egui::Area
+            ::new(egui::Id::new("welcome_overlay"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/welcome.rs` around lines 137 - 139, The overlay in welcome.rs is using
the generic id "about_overlay" which may collide with the About panel; update
the egui::Area id creation (egui::Area::new(egui::Id::new("about_overlay"))) to
a Welcome-specific id such as "welcome_overlay" (or a tuple including a unique
marker) to isolate egui state for the Welcome overlay (adjust any related code
that references the old id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.rs`:
- Around line 287-296: Only call app.open_welcome_on_startup() when there were
no initial paths and the session has no existing tabs: after
app.open_initial_paths(initial_paths) check that initial_paths.is_empty() and
that the app/session has zero restored/open tabs (e.g. via a session/tab-count
accessor on app) before invoking app.open_welcome_on_startup(); replace the
unconditional call with this gated conditional.

In `@src/state.rs`:
- Around line 3073-3087: The closure in show_welcome_tab is attempting to move
t.kind out of a shared reference when using matches!(t.kind,
TabKind::Special(SpecialTabKind::Welcome)); change the pattern to match on a
reference so it borrows rather than moves (e.g., match on &t.kind or use
matches!(t.kind, ref x) style), so update the iterator position predicate to
compare &t.kind to TabKind::Special(SpecialTabKind::Welcome) (or use
matches!(t.kind, TabKind::Special(ref k) if needed); this preserves ownership of
TabKind in tabs and allows setting active_tab_index and calling open_special_tab
as before.

---

Outside diff comments:
In `@src/state.rs`:
- Around line 2318-2359: find_next_occurrence and get_primary_selection_text
treat cursor/selection values as char indices but use them to slice by bytes,
which breaks on UTF‑8; convert char indices to byte offsets before slicing or
searching. Specifically: in find_next_occurrence convert after_pos (char index)
to a byte_offset (e.g. sum of .chars().take(after_pos).map(|c| c.len_utf8()))
and use that byte_offset when slicing self.content[..] and when computing
start/end bytes; compute end bytes as start_byte + search_text.len() (bytes). In
get_primary_selection_text convert primary.range() (start_char,end_char) and
primary.head (char index) to byte offsets before slicing or passing into
word_at_position, and update word_at_position/Selection handling if it currently
assumes byte indices so all callers are consistent; ensure all bounds checks
compare against self.content.len() (bytes).

---

Nitpick comments:
In `@src/ui/welcome.rs`:
- Around line 137-139: The overlay in welcome.rs is using the generic id
"about_overlay" which may collide with the About panel; update the egui::Area id
creation (egui::Area::new(egui::Id::new("about_overlay"))) to a Welcome-specific
id such as "welcome_overlay" (or a tuple including a unique marker) to isolate
egui state for the Welcome overlay (adjust any related code that references the
old id).

Comment on lines 287 to +296
// Open files/directories from CLI arguments and Apple Events
app.open_initial_paths(initial_paths);


// app.open_welcome_on_startup();
app.open_welcome_on_startup();

log_memory("After app creation and initial paths");

Ok(Box::new(app))
}),
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -C3 'show_welcome_tab' src/state.rs

Repository: OlaProeis/Ferrite

Length of output: 313


🏁 Script executed:

rg -n -C3 'open_welcome_on_startup' src

Repository: OlaProeis/Ferrite

Length of output: 828


🏁 Script executed:

sed -n '3074,3120p' src/state.rs

Repository: OlaProeis/Ferrite

Length of output: 1576


🏁 Script executed:

rg -n 'active_tab_index|set_active_tab' src/app/mod.rs | head -20

Repository: OlaProeis/Ferrite

Length of output: 543


🏁 Script executed:

sed -n '170,210p' src/app/mod.rs

Repository: OlaProeis/Ferrite

Length of output: 2220


Gate the welcome tab to respect user intent.

The welcome screen currently opens unconditionally after initial paths are processed, even if the user has provided CLI/Apple Event arguments or a prior session exists. This can add an unnecessary tab and steal focus.

Suggested approach: Only show the welcome screen when there are no initial paths provided and the session has no existing tabs.

🔧 Suggested change
             // Open files/directories from CLI arguments and Apple Events
+            let has_initial_paths = !initial_paths.is_empty();
             app.open_initial_paths(initial_paths);
 
-            // app.open_welcome_on_startup();
-            app.open_welcome_on_startup();
+            if !has_initial_paths {
+                app.open_welcome_on_startup();
+            }

Note: This gates on initial paths specifically. Consider also checking whether the session restored any tabs, since a user with a prior session should not see the welcome tab on subsequent launches.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Open files/directories from CLI arguments and Apple Events
app.open_initial_paths(initial_paths);
// app.open_welcome_on_startup();
app.open_welcome_on_startup();
log_memory("After app creation and initial paths");
Ok(Box::new(app))
}),
})
// Open files/directories from CLI arguments and Apple Events
let has_initial_paths = !initial_paths.is_empty();
app.open_initial_paths(initial_paths);
if !has_initial_paths {
app.open_welcome_on_startup();
}
log_memory("After app creation and initial paths");
Ok(Box::new(app))
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 287 - 296, Only call app.open_welcome_on_startup()
when there were no initial paths and the session has no existing tabs: after
app.open_initial_paths(initial_paths) check that initial_paths.is_empty() and
that the app/session has zero restored/open tabs (e.g. via a session/tab-count
accessor on app) before invoking app.open_welcome_on_startup(); replace the
unconditional call with this gated conditional.

Comment on lines +3073 to +3087
/// This is code of view tab welcome at first launch time
pub fn show_welcome_tab(&mut self) {
// If Settings tab already exists, just activate it.
if
let Some(i) = self.tabs
.iter()
.position(|t| { matches!(t.kind, TabKind::Special(SpecialTabKind::Welcome)) })
{
self.active_tab_index = i;
return;
}

// Otherwise create it (this should also set active, but we’ll be safe)
self.open_special_tab(SpecialTabKind::Welcome);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check TabKind definition and derive macros
rg -A 5 "enum TabKind" src/state.rs | head -20

# Locate show_welcome_tab function
rg -n "fn show_welcome_tab" src/state.rs

# Check lines around 3073-3087
sed -n '3070,3090p' src/state.rs

Repository: OlaProeis/Ferrite

Length of output: 916


🏁 Script executed:

# Get more context before the TabKind enum to see derive macros
rg -B 3 "pub enum TabKind" src/state.rs

# Also check SpecialTabKind definition
rg -A 10 "enum SpecialTabKind" src/state.rs

Repository: OlaProeis/Ferrite

Length of output: 404


🏁 Script executed:

# Get the full SpecialTabKind enum with derive macros
rg -B 3 "pub enum SpecialTabKind" src/state.rs

Repository: OlaProeis/Ferrite

Length of output: 282


Avoid moving t.kind out of a shared reference.

TabKind doesn't derive Copy (only Debug, Clone, PartialEq), so matches!(t.kind, ...) attempts to move out of &Tab and won't compile. Match on a reference instead:

Suggested fix
-            .position(|t| { matches!(t.kind, TabKind::Special(SpecialTabKind::Welcome)) })
+            .position(|t| matches!(&t.kind, TabKind::Special(SpecialTabKind::Welcome)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state.rs` around lines 3073 - 3087, The closure in show_welcome_tab is
attempting to move t.kind out of a shared reference when using matches!(t.kind,
TabKind::Special(SpecialTabKind::Welcome)); change the pattern to match on a
reference so it borrows rather than moves (e.g., match on &t.kind or use
matches!(t.kind, ref x) style), so update the iterator position predicate to
compare &t.kind to TabKind::Special(SpecialTabKind::Welcome) (or use
matches!(t.kind, TabKind::Special(ref k) if needed); this preserves ownership of
TabKind in tabs and allows setting active_tab_index and calling open_special_tab
as before.

@OlaProeis OlaProeis merged commit 0cec0ea into OlaProeis:master Feb 17, 2026
1 check passed
OlaProeis added a commit that referenced this pull request Feb 17, 2026
- Mark welcome view as completed in ROADMAP.md (PR #80 by @blizzard007dev)

- Add @blizzard007dev to contributors in README.md

- Update release highlight to v0.2.7 features

Co-authored-by: Cursor <[email protected]>
@coderabbitai coderabbitai bot mentioned this pull request Feb 18, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants