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