iced_code_editor/canvas_editor/
view.rs

1//! Iced UI view and rendering logic.
2
3use iced::widget::canvas::Canvas;
4use iced::widget::{Row, Scrollable, Space, container, scrollable};
5use iced::{Background, Border, Color, Element, Length, Shadow};
6
7use super::search_dialog;
8use super::wrapping::WrappingCalculator;
9use super::{CodeEditor, GUTTER_WIDTH, LINE_HEIGHT, Message};
10
11impl CodeEditor {
12    /// Creates the view element with scrollable wrapper.
13    ///
14    /// The backgrounds (editor and gutter) are handled by container styles
15    /// to ensure proper clipping when the pane is resized.
16    pub fn view(&self) -> Element<'_, Message> {
17        // Calculate total content height based on actual lines
18        // When wrapping is enabled, use visual line count
19        let wrapping_calc =
20            WrappingCalculator::new(self.wrap_enabled, self.wrap_column);
21
22        // Use viewport width for calculating visual lines
23        let visual_lines = wrapping_calc.calculate_visual_lines(
24            &self.buffer,
25            self.viewport_width,
26            self.gutter_width(),
27        );
28
29        let total_visual_lines = visual_lines.len();
30        let content_height = total_visual_lines as f32 * LINE_HEIGHT;
31
32        // Use max of content height and viewport height to ensure the canvas
33        // always covers the visible area (prevents visual artifacts when
34        // content is shorter than viewport after reset/file change)
35        let canvas_height = content_height.max(self.viewport_height);
36
37        // Create canvas with height that covers at least the viewport
38        let canvas = Canvas::new(self)
39            .width(Length::Fill)
40            .height(Length::Fixed(canvas_height));
41
42        // Capture style colors for closures
43        let scrollbar_bg = self.style.scrollbar_background;
44        let scroller_color = self.style.scroller_color;
45        let background_color = self.style.background;
46        let gutter_background = self.style.gutter_background;
47
48        // Wrap in scrollable for automatic scrollbar display with custom style
49        // Use Length::Fill to respect parent container constraints and enable proper clipping
50        // Background is TRANSPARENT here because it's handled by the Stack layer below
51        let scrollable = Scrollable::new(canvas)
52            .id(self.scrollable_id.clone())
53            .width(Length::Fill)
54            .height(Length::Fill)
55            .on_scroll(Message::Scrolled)
56            .style(move |_theme, _status| scrollable::Style {
57                container: container::Style {
58                    background: Some(Background::Color(Color::TRANSPARENT)),
59                    ..container::Style::default()
60                },
61                vertical_rail: scrollable::Rail {
62                    background: Some(scrollbar_bg.into()),
63                    border: Border {
64                        radius: 4.0.into(),
65                        width: 0.0,
66                        color: Color::TRANSPARENT,
67                    },
68                    scroller: scrollable::Scroller {
69                        background: scroller_color.into(),
70                        border: Border {
71                            radius: 4.0.into(),
72                            width: 0.0,
73                            color: Color::TRANSPARENT,
74                        },
75                    },
76                },
77                horizontal_rail: scrollable::Rail {
78                    background: Some(scrollbar_bg.into()),
79                    border: Border {
80                        radius: 4.0.into(),
81                        width: 0.0,
82                        color: Color::TRANSPARENT,
83                    },
84                    scroller: scrollable::Scroller {
85                        background: scroller_color.into(),
86                        border: Border {
87                            radius: 4.0.into(),
88                            width: 0.0,
89                            color: Color::TRANSPARENT,
90                        },
91                    },
92                },
93                gap: None,
94                auto_scroll: scrollable::AutoScroll {
95                    background: Color::TRANSPARENT.into(),
96                    border: Border::default(),
97                    shadow: Shadow::default(),
98                    icon: Color::TRANSPARENT,
99                },
100            });
101
102        // Gutter background container (fixed width, clipped by parent)
103        // Only create if line numbers are enabled
104        let gutter_container = if self.line_numbers_enabled {
105            Some(
106                container(
107                    Space::new().width(Length::Fill).height(Length::Fill),
108                )
109                .width(Length::Fixed(GUTTER_WIDTH))
110                .height(Length::Fill)
111                .style(move |_| container::Style {
112                    background: Some(Background::Color(gutter_background)),
113                    ..container::Style::default()
114                }),
115            )
116        } else {
117            None
118        };
119
120        // Code background container (fills remaining width)
121        let code_background_container =
122            container(Space::new().width(Length::Fill).height(Length::Fill))
123                .width(Length::Fill)
124                .height(Length::Fill)
125                .style(move |_| container::Style {
126                    background: Some(Background::Color(background_color)),
127                    ..container::Style::default()
128                });
129
130        // Main layout: use a Stack to layer the backgrounds behind the scrollable
131        // The scrollable has a transparent background so the colors show through
132        let background_row = if let Some(gutter) = gutter_container {
133            Row::new().push(gutter).push(code_background_container)
134        } else {
135            Row::new().push(code_background_container)
136        };
137
138        let mut editor_stack = iced::widget::Stack::new()
139            .push(
140                // Background layer (bottom): gutter + code backgrounds
141                background_row,
142            )
143            .push(
144                // Scrollable layer (top) - transparent, overlays the backgrounds
145                scrollable,
146            );
147
148        // Add search dialog overlay if open
149        if self.search_state.is_open {
150            let search_dialog =
151                search_dialog::view(&self.search_state, &self.translations);
152
153            // Position the dialog in top-right corner with 20px margin
154            // Use a Row with Fill space to push the dialog to the right
155            let positioned_dialog = container(
156                Row::new()
157                    .push(Space::new().width(Length::Fill)) // Push to right
158                    .push(search_dialog),
159            )
160            .padding(20) // 20px margin from edges
161            .width(Length::Fill)
162            .height(Length::Shrink);
163
164            editor_stack = editor_stack.push(positioned_dialog);
165        }
166
167        // Wrap in a container with clip to ensure proper bounds
168        container(editor_stack)
169            .width(Length::Fill)
170            .height(Length::Fill)
171            .clip(true)
172            .into()
173    }
174}