frozen_term/
terminal.rs

1use std::time::{Duration, Instant};
2
3use iced::{
4    Rectangle, Size, Vector,
5    advanced::{text::Paragraph, widget::operation::Focusable},
6    mouse::ScrollDelta,
7    widget::{container, row},
8};
9
10use crate::{
11    Style,
12    scrollbar::Scrollbar,
13    terminal_grid::{PreRenderer, TerminalGrid, VisiblePosition},
14    wezterm::{WeztermGrid, prerenderer::WeztermPreRenderer},
15};
16
17pub mod style;
18use style::CursorShape;
19
20#[derive(Debug, Clone)]
21pub struct Message(InnerMessage);
22
23#[derive(Debug, Clone)]
24enum InnerMessage {
25    Resize(crate::terminal_grid::Size),
26    KeyPress {
27        modified_key: iced::keyboard::key::Key,
28        modifiers: iced::keyboard::Modifiers,
29    },
30    Input(Vec<u8>),
31    Paste(Option<String>),
32    Scrolled(ScrollDelta),
33    ScrollTo(usize),
34    ScrollDone,
35    StartSelection(VisiblePosition),
36    MoveSelection(VisiblePosition),
37    EndSelection,
38    ShowContextMenu(iced::Point),
39    HideContextMenu,
40    ContextMenuCopy,
41    ContextMenuPaste,
42    IdChanged,
43}
44
45pub enum Action {
46    None,
47    Run(iced::Task<Message>),
48    Resize(crate::terminal_grid::Size),
49    Input(Vec<u8>),
50    IdChanged,
51}
52
53pub struct Terminal {
54    grid: WeztermGrid,
55    id: Id,
56    key_filter: Option<Box<dyn Fn(&iced::keyboard::Key, &iced::keyboard::Modifiers) -> bool>>,
57    // here to abort the task on drop
58    context_menu_position: Option<iced::Point>,
59    style: Style,
60    _handle: iced::task::Handle,
61}
62
63impl Terminal {
64    pub fn new() -> (Self, iced::Task<Message>) {
65        let (grid, stream) = WeztermGrid::new();
66        let (task, handle) = iced::Task::run(stream, InnerMessage::Input)
67            .map(Message)
68            .abortable();
69
70        let handle = handle.abort_on_drop();
71
72        (
73            Self {
74                grid,
75                id: Id(iced::advanced::widget::Id::unique()),
76                key_filter: None,
77                context_menu_position: None,
78                style: Style::default(),
79                _handle: handle,
80            },
81            task,
82        )
83    }
84
85    pub fn id(mut self, id: impl Into<Id>) -> Self {
86        self.id = id.into();
87        self
88    }
89
90    pub fn style(mut self, style: Style) -> Self {
91        self.set_style(style);
92        self
93    }
94
95    pub fn set_style(&mut self, style: Style) {
96        self.style = style;
97    }
98
99    /// Allows you to add a filter to stop the terminal from capturing keypresses you want to use for your application.
100    /// If the given filter returns `true`, the keypress will be ignored.
101    pub fn key_filter(
102        mut self,
103        key_filter: impl 'static + Fn(&iced::keyboard::Key, &iced::keyboard::Modifiers) -> bool,
104    ) -> Self {
105        self.set_key_filter(key_filter);
106        self
107    }
108
109    pub fn set_key_filter(
110        &mut self,
111        key_filter: impl 'static + Fn(&iced::keyboard::Key, &iced::keyboard::Modifiers) -> bool,
112    ) {
113        self.key_filter = Some(Box::new(key_filter));
114    }
115
116    pub fn focus<T>(&self) -> iced::Task<T>
117    where
118        T: Send + 'static,
119    {
120        Self::focus_with_id(self.id.clone())
121    }
122
123    pub fn focus_with_id<T>(id: impl Into<Id>) -> iced::Task<T>
124    where
125        T: Send + 'static,
126    {
127        iced::advanced::widget::operate(iced::advanced::widget::operation::focusable::focus(
128            id.into().0,
129        ))
130    }
131
132    pub fn get_title(&self) -> &str {
133        self.grid.get_title()
134    }
135
136    pub fn advance_bytes<B>(&mut self, bytes: B)
137    where
138        B: AsRef<[u8]>,
139    {
140        self.grid.advance_bytes(bytes.as_ref());
141    }
142
143    #[must_use]
144    pub fn update(&mut self, message: Message) -> Action {
145        match message.0 {
146            InnerMessage::Resize(size) => {
147                self.grid.resize(size);
148                Action::Resize(size)
149            }
150            InnerMessage::KeyPress {
151                modified_key,
152                modifiers,
153            } => {
154                if modified_key == iced::keyboard::Key::Character("V".into())
155                    && modifiers.control()
156                    && modifiers.shift()
157                {
158                    return self.paste();
159                }
160
161                if modified_key == iced::keyboard::Key::Character("C".into())
162                    && modifiers.control()
163                    && modifiers.shift()
164                {
165                    return self.copy();
166                }
167
168                if let Some(input) = self.grid.press_key(modified_key, modifiers) {
169                    Action::Input(input)
170                } else {
171                    Action::None
172                }
173            }
174            InnerMessage::Input(input) => Action::Input(input),
175            InnerMessage::Paste(paste) => {
176                if let Some(paste) = paste {
177                    if let Some(input) = self.grid.paste(&paste) {
178                        return Action::Input(input);
179                    }
180                }
181                Action::None
182            }
183            InnerMessage::Scrolled(scrolled) => {
184                match scrolled {
185                    ScrollDelta::Lines { y, .. } => {
186                        self.grid.scroll(y as isize);
187                    }
188                    ScrollDelta::Pixels { y, .. } => {
189                        self.grid.scroll(y as isize);
190                    }
191                };
192
193                Action::None
194            }
195            InnerMessage::ScrollTo(y) => {
196                self.grid.scroll_to(y);
197                Action::None
198            }
199            InnerMessage::ScrollDone => Action::Run(self.focus()),
200            InnerMessage::StartSelection(start) => {
201                self.grid.start_selection(start);
202                Action::None
203            }
204            InnerMessage::MoveSelection(position) => {
205                self.grid.move_selection(position);
206                Action::None
207            }
208            InnerMessage::EndSelection => {
209                self.grid.end_selection();
210                Action::None
211            }
212            InnerMessage::ShowContextMenu(position) => {
213                self.context_menu_position = Some(position);
214                Action::None
215            }
216            InnerMessage::HideContextMenu => {
217                self.context_menu_position = None;
218                Action::None
219            }
220            InnerMessage::ContextMenuCopy => {
221                self.context_menu_position = None;
222                self.copy()
223            }
224            InnerMessage::ContextMenuPaste => {
225                self.context_menu_position = None;
226                self.paste()
227            }
228            InnerMessage::IdChanged => Action::IdChanged,
229        }
230    }
231
232    fn copy(&self) -> Action {
233        if let Some(selected_text) = self.grid.selected_text() {
234            Action::Run(iced::clipboard::write(selected_text).chain(self.focus()))
235        } else {
236            Action::Run(self.focus())
237        }
238    }
239
240    fn paste(&self) -> Action {
241        Action::Run(
242            iced::clipboard::read()
243                .map(InnerMessage::Paste)
244                .map(Message)
245                .chain(self.focus()),
246        )
247    }
248
249    pub fn view<'a, Theme, Renderer>(&'a self) -> iced::Element<'a, Message, Theme, Renderer>
250    where
251        Renderer: iced::advanced::text::Renderer<Font = iced::Font> + 'static,
252        Theme: iced::widget::text::Catalog + 'static,
253        Theme: iced::widget::container::Catalog + iced::widget::button::Catalog,
254        <Theme as iced::widget::text::Catalog>::Class<'static>:
255            From<iced::widget::text::StyleFn<'static, Theme>>,
256        <Theme as iced::widget::container::Catalog>::Class<'static>:
257            From<iced::widget::container::StyleFn<'static, Theme>>,
258        Theme: iced::widget::scrollable::Catalog,
259    {
260        self.view_internal().map(Message)
261    }
262
263    fn view_internal<'a, Theme, Renderer>(
264        &'a self,
265    ) -> iced::Element<'a, InnerMessage, Theme, Renderer>
266    where
267        Renderer: iced::advanced::text::Renderer<Font = iced::Font> + 'static,
268        Theme: iced::widget::text::Catalog + 'static,
269        Theme: iced::widget::container::Catalog + iced::widget::button::Catalog,
270        <Theme as iced::widget::text::Catalog>::Class<'static>:
271            From<iced::widget::text::StyleFn<'static, Theme>>,
272        <Theme as iced::widget::container::Catalog>::Class<'static>:
273            From<iced::widget::container::StyleFn<'static, Theme>>,
274        Theme: iced::widget::scrollable::Catalog,
275    {
276        let total_rows = self.grid.available_lines();
277
278        let terminal_widget = row![
279            iced::Element::new(TerminalWidget::new(self)),
280            Scrollbar::new(
281                total_rows as u32,
282                self.grid.get_scroll() as u32,
283                self.grid.get_size().rows as u32
284            )
285            .on_scroll(move |scroll| {
286                let new_scroll = (total_rows as f32 * scroll) as usize;
287                InnerMessage::ScrollTo(new_scroll)
288            })
289            .on_scroll_done(InnerMessage::ScrollDone)
290        ];
291
292        if let Some(position) = self.context_menu_position {
293            let copy_button = iced::widget::button(iced::widget::text("Copy").size(14))
294                .padding([4, 8])
295                .width(iced::Length::Fill)
296                .on_press(InnerMessage::ContextMenuCopy);
297
298            let paste_button = iced::widget::button(iced::widget::text("Paste").size(14))
299                .padding([4, 8])
300                .width(iced::Length::Fill)
301                .on_press(InnerMessage::ContextMenuPaste);
302
303            let context_menu = iced::widget::column![copy_button, paste_button].spacing(2);
304
305            let positioned_menu = iced::widget::container(context_menu)
306                .style(|_theme| iced::widget::container::Style {
307                    background: Some(iced::Background::Color(iced::Color::from_rgb(
308                        0.2, 0.2, 0.2,
309                    ))),
310                    border: iced::Border {
311                        color: iced::Color::from_rgb(0.5, 0.5, 0.5),
312                        width: 1.0,
313                        radius: 4.0.into(),
314                    },
315                    ..Default::default()
316                })
317                .padding(4)
318                .width(100)
319                .height(iced::Length::Shrink);
320
321            // Position the menu using padding to offset it to the cursor position
322            let positioned_container = container(positioned_menu)
323                .width(iced::Length::Fill)
324                .height(iced::Length::Fill)
325                .padding(iced::Padding {
326                    top: position.y,
327                    right: 0.0,
328                    bottom: 0.0,
329                    left: position.x,
330                });
331
332            iced::widget::stack![terminal_widget, positioned_container].into()
333        } else {
334            iced::widget::stack![terminal_widget].into()
335        }
336    }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Hash)]
340pub struct Id(iced::advanced::widget::Id);
341
342impl Id {
343    /// Creates a custom [`Id`].
344    pub fn new(id: &'static str) -> Self {
345        Self(iced::advanced::widget::Id::new(id))
346    }
347
348    /// Creates a unique [`Id`].
349    ///
350    /// This function produces a different [`Id`] every time it is called.
351    pub fn unique() -> Self {
352        Self(iced::advanced::widget::Id::unique())
353    }
354}
355
356impl From<Id> for iced::advanced::widget::Id {
357    fn from(id: Id) -> Self {
358        id.0
359    }
360}
361
362impl From<&'static str> for Id {
363    fn from(id: &'static str) -> Self {
364        Self::new(id)
365    }
366}
367
368// impl From<String> for Id {
369//     fn from(id: String) -> Self {
370//         Self::new(id)
371//     }
372// }
373
374struct TerminalWidget<'a> {
375    term: &'a Terminal,
376}
377
378struct State<R: iced::advanced::text::Renderer> {
379    prerenderer: WeztermPreRenderer<R>,
380    focused: bool,
381    last_cursor_blink: Instant,
382    cursor_blink_currently_shown: bool,
383    now: Instant,
384    last_widget_width: f32,
385    last_widget_height: f32,
386    last_id: Option<Id>,
387}
388
389const CHAR_WIDTH: f32 = 0.6;
390const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
391
392impl<Renderer> iced::advanced::widget::operation::Focusable for State<Renderer>
393where
394    Renderer: iced::advanced::text::Renderer,
395{
396    fn is_focused(&self) -> bool {
397        self.focused
398    }
399
400    fn focus(&mut self) {
401        self.focused = true;
402        self.last_cursor_blink = Instant::now();
403        self.cursor_blink_currently_shown = true;
404    }
405
406    fn unfocus(&mut self) {
407        self.focused = false;
408        self.cursor_blink_currently_shown = false;
409    }
410}
411
412impl<Theme, Renderer> iced::advanced::widget::Widget<InnerMessage, Theme, Renderer>
413    for TerminalWidget<'_>
414where
415    Renderer: iced::advanced::text::Renderer<Font = iced::Font>,
416    Renderer: 'static,
417{
418    fn tag(&self) -> iced::advanced::widget::tree::Tag {
419        iced::advanced::widget::tree::Tag::of::<State<Renderer>>()
420    }
421
422    fn state(&self) -> iced::advanced::widget::tree::State {
423        iced::advanced::widget::tree::State::new(State::<Renderer> {
424            prerenderer: WeztermPreRenderer::new(self.term.style.clone()),
425            focused: false,
426            last_cursor_blink: Instant::now(),
427            cursor_blink_currently_shown: false,
428            now: Instant::now(),
429            // needs to be none to detect newly created widgets
430            last_id: None,
431            last_widget_height: 0.0,
432            last_widget_width: 0.0,
433        })
434    }
435
436    fn size(&self) -> iced::Size<iced::Length> {
437        iced::Size::new(iced::Length::Fill, iced::Length::Fill)
438    }
439
440    fn operate(
441        &mut self,
442        tree: &mut iced::advanced::widget::Tree,
443        layout: iced::advanced::Layout<'_>,
444        _renderer: &Renderer,
445        operation: &mut dyn iced::advanced::widget::Operation,
446    ) {
447        let state = tree.state.downcast_mut::<State<Renderer>>();
448
449        operation.focusable(Some(&self.term.id.0), layout.bounds(), state);
450    }
451
452    fn update(
453        &mut self,
454        state: &mut iced::advanced::widget::Tree,
455        event: &iced::Event,
456        layout: iced::advanced::Layout<'_>,
457        cursor: iced::advanced::mouse::Cursor,
458        renderer: &Renderer,
459        _clipboard: &mut dyn iced::advanced::Clipboard,
460        shell: &mut iced::advanced::Shell<'_, InnerMessage>,
461        _viewport: &iced::Rectangle,
462    ) {
463        match event {
464            iced::Event::Window(iced::window::Event::RedrawRequested(now)) => {
465                let state = state.state.downcast_mut::<State<Renderer>>();
466
467                let widget_width = layout.bounds().width - self.term.style.padding.x();
468                let widget_height = layout.bounds().height - self.term.style.padding.y();
469
470                // check if id has changed
471                let id_changed = state
472                    .last_id
473                    .as_ref()
474                    .map(|last_id| last_id != &self.term.id)
475                    .unwrap_or(true);
476
477                if id_changed {
478                    state.prerenderer.clear_cache();
479                    state.last_id = Some(self.term.id.clone());
480                    shell.publish(InnerMessage::IdChanged);
481                }
482
483                // check if widget size has changed
484                if state.last_widget_width != widget_width
485                    || state.last_widget_height != widget_height
486                    || id_changed
487                {
488                    state.last_widget_width = widget_width;
489                    state.last_widget_height = widget_height;
490
491                    let text_size = self.term.style.text_size.unwrap_or(renderer.default_size());
492                    let line_height = self.term.style.line_height.to_absolute(text_size);
493                    let char_width = text_size * CHAR_WIDTH;
494
495                    let target_line_count = (widget_height / line_height.0) as usize;
496                    let target_col_count = (widget_width / char_width.0) as usize;
497                    let size = self.term.grid.get_size();
498
499                    if size.rows != target_line_count || size.cols != target_col_count {
500                        let size = crate::terminal_grid::Size {
501                            cols: target_col_count,
502                            rows: target_line_count,
503                        };
504                        shell.publish(InnerMessage::Resize(size));
505                    }
506                }
507
508                // handle blinking cursor
509                if state.is_focused() {
510                    state.now = *now;
511                    let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
512                        .saturating_sub((*now - state.last_cursor_blink).as_millis());
513
514                    if millis_until_redraw == 0 {
515                        state.cursor_blink_currently_shown = !state.cursor_blink_currently_shown;
516                        state.last_cursor_blink = *now;
517                    }
518
519                    shell.request_redraw_at(
520                        *now + Duration::from_millis(millis_until_redraw as u64),
521                    );
522                } else if state.cursor_blink_currently_shown == true {
523                    state.cursor_blink_currently_shown = false;
524                    shell.request_redraw();
525                }
526            }
527            iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
528                if cursor.position_over(layout.bounds()).is_some() {
529                    shell.publish(InnerMessage::Scrolled(delta.clone()));
530                    shell.capture_event();
531                }
532            }
533            iced::Event::Mouse(iced::mouse::Event::ButtonPressed(button)) => {
534                let state = state.state.downcast_mut::<State<Renderer>>();
535                let newly_focused = cursor.position_over(layout.bounds()).is_some();
536
537                if newly_focused {
538                    state.focus();
539
540                    // Handle text selection start
541                    if *button == iced::mouse::Button::Left {
542                        // Hide context menu if visible
543                        if self.term.context_menu_position.is_some() {
544                            shell.publish(InnerMessage::HideContextMenu);
545                        }
546
547                        if let Some(cursor_position) = cursor.position() {
548                            if let Some(char_pos) =
549                                self.screen_to_visible_position(cursor_position, layout, renderer)
550                            {
551                                shell.publish(InnerMessage::StartSelection(char_pos));
552                            }
553                        }
554                    }
555
556                    // Handle right-click for context menu
557                    if *button == iced::mouse::Button::Right {
558                        if let Some(cursor_position) = cursor.position() {
559                            shell.publish(InnerMessage::ShowContextMenu(cursor_position));
560                        }
561                    }
562
563                    shell.capture_event();
564                } else {
565                    state.unfocus();
566                }
567            }
568            iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) => {
569                if self.term.grid.currently_selecting() {
570                    if let Some(char_pos) =
571                        self.screen_to_visible_position(*position, layout, renderer)
572                    {
573                        shell.publish(InnerMessage::MoveSelection(char_pos));
574                    }
575                    shell.capture_event();
576                }
577            }
578            iced::Event::Mouse(iced::mouse::Event::ButtonReleased(button)) => {
579                if *button == iced::mouse::Button::Left {
580                    if self.term.grid.currently_selecting() {
581                        shell.publish(InnerMessage::EndSelection);
582                        shell.capture_event();
583                    }
584                }
585            }
586            iced::Event::Touch(iced::touch::Event::FingerPressed { .. }) => {
587                let state = state.state.downcast_mut::<State<Renderer>>();
588                let newly_focused = cursor.position_over(layout.bounds()).is_some();
589
590                if newly_focused {
591                    state.focus();
592                    shell.capture_event();
593                } else {
594                    state.unfocus();
595                }
596            }
597            iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
598                modified_key,
599                modifiers,
600                ..
601            }) => {
602                let state = state.state.downcast_mut::<State<Renderer>>();
603
604                if state.is_focused() {
605                    if let Some(filter) = &self.term.key_filter {
606                        if filter(&modified_key, &modifiers) {
607                            return;
608                        }
609                    }
610
611                    state.last_cursor_blink = Instant::now();
612                    state.cursor_blink_currently_shown = true;
613
614                    let message = InnerMessage::KeyPress {
615                        modified_key: modified_key.clone(),
616                        modifiers: modifiers.clone(),
617                    };
618                    shell.publish(message);
619
620                    shell.capture_event();
621                }
622            }
623            iced::Event::Window(iced::window::Event::Focused) => {
624                let state = state.state.downcast_mut::<State<Renderer>>();
625                state.focus();
626                shell.request_redraw();
627            }
628            _ => (),
629        }
630    }
631
632    fn layout(
633        &mut self,
634        tree: &mut iced::advanced::widget::Tree,
635        renderer: &Renderer,
636        limits: &iced::advanced::layout::Limits,
637    ) -> iced::advanced::layout::Node {
638        let state = tree.state.downcast_mut::<State<Renderer>>();
639
640        state.prerenderer.update(&self.term.grid, renderer);
641
642        iced::advanced::layout::Node::new(limits.max())
643    }
644
645    fn draw(
646        &self,
647        tree: &iced::advanced::widget::Tree,
648        renderer: &mut Renderer,
649        _theme: &Theme,
650        _style: &iced::advanced::renderer::Style,
651        layout: iced::advanced::Layout<'_>,
652        _cursor: iced::advanced::mouse::Cursor,
653        viewport: &iced::Rectangle,
654    ) {
655        let Some(bounds) = layout.bounds().intersection(viewport) else {
656            return;
657        };
658
659        let state = tree.state.downcast_ref::<State<Renderer>>();
660        let padding_offset =
661            iced::Vector::new(self.term.style.padding.left, self.term.style.padding.top);
662        let translation = layout.position() - iced::Point::ORIGIN + padding_offset;
663
664        // terminal Background
665        renderer.fill_quad(
666            iced::advanced::renderer::Quad {
667                bounds: layout.bounds(),
668                ..Default::default()
669            },
670            self.term.style.background_color,
671        );
672
673        let size = self
674            .term
675            .style
676            .text_size
677            .unwrap_or_else(|| renderer.default_size());
678
679        let y_multiplier = self.term.style.line_height.to_absolute(size).0;
680
681        // drawing text background
682        for (row_index, render_data) in state.prerenderer.visible_rows().enumerate() {
683            let Some((paragraph, spans)) = render_data else {
684                continue;
685            };
686            let y_offset = y_multiplier * row_index as f32;
687
688            for (index, span) in spans.iter().enumerate() {
689                if let Some(highlight) = span.highlight {
690                    let regions = paragraph.span_bounds(index);
691
692                    for bounds in &regions {
693                        let position =
694                            bounds.position() - Vector::new(span.padding.left, span.padding.top);
695
696                        let size = bounds.size() + Size::new(span.padding.x(), span.padding.y());
697
698                        let position = position + translation + iced::Vector::new(0.0, y_offset);
699                        let bounds = Rectangle::new(position, size);
700
701                        renderer.fill_quad(
702                            iced::advanced::renderer::Quad {
703                                bounds,
704                                border: highlight.border,
705                                ..Default::default()
706                            },
707                            highlight.background,
708                        );
709                    }
710                }
711            }
712
713            renderer.fill_paragraph(
714                &paragraph,
715                bounds.position() + padding_offset + iced::Vector::new(0.0, y_offset),
716                self.term.style.foreground_color,
717                bounds,
718            );
719        }
720
721        self.draw_cursor(renderer, &state, translation);
722    }
723}
724
725impl<'a> TerminalWidget<'a> {
726    pub fn new(term: &'a Terminal) -> Self {
727        Self { term }
728    }
729
730    fn screen_to_visible_position<Renderer>(
731        &self,
732        screen_pos: iced::Point,
733        layout: iced::advanced::Layout<'_>,
734        renderer: &Renderer,
735    ) -> Option<VisiblePosition>
736    where
737        Renderer: iced::advanced::text::Renderer,
738    {
739        let padding_offset =
740            iced::Vector::new(self.term.style.padding.left, self.term.style.padding.top);
741        let translation = layout.position() - iced::Point::ORIGIN + padding_offset;
742
743        // Convert screen position to position relative to terminal content
744        let relative_pos = screen_pos - translation;
745
746        // Check if position is within terminal bounds
747        if relative_pos.x < 0.0 || relative_pos.y < 0.0 {
748            return None;
749        }
750
751        // Calculate character dimensions
752        let text_size = self
753            .term
754            .style
755            .text_size
756            .unwrap_or_else(|| renderer.default_size());
757        let line_height = self.term.style.line_height.to_absolute(text_size).0;
758        let text_size = text_size.0;
759        let char_width = text_size * CHAR_WIDTH;
760
761        // Convert to character coordinates
762        let char_x = (relative_pos.x / char_width) as usize;
763        let char_y = (relative_pos.y / line_height) as usize;
764
765        // Account for scroll offset - the displayed text is offset by scroll_pos
766        let absolute_y = char_y;
767
768        Some(VisiblePosition {
769            x: char_x,
770            y: absolute_y,
771        })
772    }
773
774    fn draw_cursor<Renderer>(
775        &self,
776        renderer: &mut Renderer,
777        state: &State<Renderer>,
778        translation: iced::Vector,
779    ) where
780        Renderer: iced::advanced::text::Renderer,
781    {
782        let Some(cursor) = self.term.grid.get_cursor() else {
783            return;
784        };
785        if !state.cursor_blink_currently_shown {
786            return;
787        }
788
789        // Calculate the scroll-adjusted cursor position using your custom scroll system
790        // The cursor position is absolute, but we need to adjust it by the scroll offset
791        let cursor_absolute_y = cursor.y as i64;
792        let scroll_offset = 0 as i64;
793        let visible_cursor_y = cursor_absolute_y + scroll_offset;
794
795        // Calculate character dimensions
796        let text_size = self
797            .term
798            .style
799            .text_size
800            .unwrap_or_else(|| renderer.default_size());
801
802        let line_height = self.term.style.line_height.to_absolute(text_size).0;
803        let text_size = text_size.0;
804        let char_width = text_size * CHAR_WIDTH;
805
806        let base_cursor_position = iced::Point::new(
807            cursor.x as f32 * char_width,
808            visible_cursor_y as f32 * line_height,
809        );
810
811        let padding = 1.0;
812
813        let cursor_bounds = match self.term.style.cursor_shape {
814            CursorShape::Underline => iced::Rectangle::new(
815                base_cursor_position
816                    + translation
817                    + iced::Vector::new(0.0, renderer.default_size().0 * 1.2),
818                iced::Size::new(renderer.default_size().0 * CHAR_WIDTH, 1.0),
819            ),
820            CursorShape::Block => iced::Rectangle::new(
821                base_cursor_position + translation + iced::Vector::new(padding, padding),
822                iced::Size::new(
823                    renderer.default_size().0 * CHAR_WIDTH - padding,
824                    renderer.default_size().0 * 1.3 - padding,
825                ),
826            ),
827            CursorShape::Bar => iced::Rectangle::new(
828                base_cursor_position + translation + iced::Vector::new(padding, padding),
829                iced::Size::new(1.0, renderer.default_size().0 * 1.3 - padding),
830            ),
831        };
832
833        renderer.fill_quad(
834            iced::advanced::renderer::Quad {
835                bounds: cursor_bounds,
836                border: iced::Border::default(),
837                ..Default::default()
838            },
839            iced::Color::WHITE,
840        );
841    }
842}