Harzu / iced_term

Terminal emulator widget powered by ICED framework and alacritty terminal backend.
MIT License
75 stars 7 forks source link

Terminal not streching to parent's size #22

Open leonardosalsi opened 5 days ago

leonardosalsi commented 5 days ago

I am currently working on a project with iced 0.13.1 and use iced_term 0.5.0 to embed a terminal. I instantiate a terminal, which functionally works, and wrap the output of TerminalView::show with a container which is defined to take up its parent's dimensions.

pub fn view(&self) -> iced::Element<'_, TerminalMessage> {
      let terminal = TerminalView::show(&self.terminal)
          .map(TerminalMessage::Terminal);

      container(terminal)
          .width(Length::Fill)
          .height(Length::Fill)
          .into()
  }

Now I have the issue, that I cannot 'use' the full width for the terminal. The following screenshot shows the issue I'm currently having.

image

We can see that, according to the background color used by the terminal, the terminal itself does extend as expected. But whenever I write, i encounter some 'unwritable' area on the right-hand side (as seen by the command and the highlighted text) that does not render any characters. The text is not wrapping, but continuing, as seen from the way the command is cut off.

I followed the example for the fullscreen terminal and compared this implementation with mine extensively, but was not able to find any kind of setting that would fix this.

For context, this is the setup of the terminal itself

pub(crate) fn new(id: u64, name: &str) -> Self {
    let shell = std::env::var("SHELL")
        .unwrap_or(String::from(DEFAULT_SHELL))
        .to_string();
    let settings = iced_term::settings::Settings {
        font: iced_term::settings::FontSettings {
            size: 12.0,
            font_type: Font {
                family: Family::Name("MesloLGS NF"),
                ..Default::default()
            },
            ..Default::default()
        },
        backend: BackendSettings {
            shell,
        },
        ..Default::default()
    };
    Self {
        id,
        name: String::from(name),
        terminal: iced_term::Terminal::new(id, settings)
    }
}

Perhaps I missed something, but a hint would be very helpful.

Harzu commented 3 days ago

Did you follow the examples? You provided code looks like the basic full_screen example. I tested it on a different OS like (mac, ubunntu, fedora, windows under wsl) and everything was good.

Do you catch the resize event in your app? As I can understand iced provide the resize event within the app starts, after that if widget's subscription initialized, widget handle it and resize inner pty instance (number of rows and cols).

leonardosalsi commented 2 days ago

Ciao Harzu, thanks for your reply!

This is the event handler

Message::TerminalEvent(iced_term::Event::CommandReceived(session_id, cmd)) => {
                if let Some(terminal) = self.right_panel.view.terminal_drawer.terminals.get_mut(&session_id) {
                    match terminal.terminal.update(cmd) {
                        Action::Redraw => {}
                        Action::Shutdown => {}
                        Action::ChangeTitle(_) => {}
                        Action::Ignore => {}
                    }
                }
                Task::none()
            }

I do catch the event Action::Redraw and do nothing with it, since I do not need an action. In your example, you use the events Action::Shutdown and Action::ChangeTitle. Is there something I am missing when redrawing?

Harzu commented 2 days ago

It seems like I need the example for reproducing this behaviour, sorry. You can also look at the split view example.

How I can understand based on your screenshot you lost some events because as I can see container color was filled but content wasn't, it means that backend wasn't receive the resize event.

leonardosalsi commented 2 days ago

I can show you the relevant files.

This is where the terminals are defined

use crate::actions::Action;
use crate::backend::{Backend, BackendCommand};
use crate::bindings::{Binding, BindingAction, BindingsLayout, InputKind};
use crate::font::TermFont;
use crate::settings::{BackendSettings, FontSettings, Settings, ThemeSettings};
use crate::theme::{ColorPalette, Theme};
use crate::AlacrittyEvent;
use iced::widget::canvas::Cache;
use tokio::sync::mpsc::Sender;

#[derive(Debug, Clone)]
pub enum Event {
    CommandReceived(u64, Command),
}

#[derive(Debug, Clone)]
pub enum Command {
    InitBackend(Sender<AlacrittyEvent>),
    ChangeTheme(Box<ColorPalette>),
    ChangeFont(FontSettings),
    AddBindings(Vec<(Binding<InputKind>, BindingAction)>),
    ProcessBackendCommand(BackendCommand),
}

pub struct Terminal {
    pub id: u64,
    pub(crate) font: TermFont,
    pub(crate) theme: Theme,
    pub(crate) cache: Cache,
    pub(crate) bindings: BindingsLayout,
    pub(crate) backend: Option<Backend>,
    backend_settings: BackendSettings,
}

impl Terminal {
    pub fn new(id: u64, settings: Settings) -> Self {
        Self {
            id,
            font: TermFont::new(settings.font),
            theme: Theme::new(settings.theme),
            bindings: BindingsLayout::default(),
            cache: Cache::default(),
            backend_settings: settings.backend,
            backend: None,
        }
    }

    pub fn widget_id(&self) -> iced::widget::text_input::Id {
        iced::widget::text_input::Id::new(self.id.to_string())
    }

    pub fn update(&mut self, cmd: Command) -> Action {
        let mut action = Action::Ignore;
        match cmd {
            Command::InitBackend(sender) => {
                self.backend = Some(
                    Backend::new(
                        self.id,
                        sender,
                        self.backend_settings.clone(),
                        self.font.measure,
                    )
                    .unwrap_or_else(|_| {
                        panic!("init pty with ID: {} is failed", self.id);
                    }),
                );
            },
            Command::ChangeTheme(color_pallete) => {
                self.theme = Theme::new(ThemeSettings::new(color_pallete));
                action = Action::Redraw;
                self.sync_and_redraw();
            },
            Command::ChangeFont(font_settings) => {
                self.font = TermFont::new(font_settings);
                if let Some(ref mut backend) = self.backend {
                    action = backend.process_command(BackendCommand::Resize(
                        None,
                        Some(self.font.measure),
                    ));
                    if action == Action::Redraw {
                        self.redraw();
                    }
                }
            },
            Command::AddBindings(bindings) => {
                self.bindings.add_bindings(bindings);
            },
            Command::ProcessBackendCommand(c) => {
                if let Some(ref mut backend) = self.backend {
                    action = backend.process_command(c);
                    if action == Action::Redraw {
                        self.redraw();
                    }
                }
            },
        }

        action
    }

    fn sync_and_redraw(&mut self) {
        if let Some(ref mut backend) = self.backend {
            backend.sync();
            self.redraw();
        }
    }

    fn redraw(&mut self) {
        self.cache.clear();
    }
}

They are stored in an auxiliary class, which is used by the following file

use iced::{window, Background, Border, Color, Length, Padding, Radians, Shadow, Size, Subscription, Task, Theme, Vector};
use iced::futures::StreamExt;
use iced::gradient::Linear;
use iced::widget::{container, row, stack, text};
use iced::widget::container::Style;
use iced_term::actions::Action;
use iced_term::Event;
use crate::backend::yanaxi::Yanaxi;
use crate::ui::components::left_panel::image_button::ImageButtonMessage;
use crate::ui::components::left_panel::left_panel_menu::LeftPanelMenuMessage;
use crate::ui::components::left_panel::left_panel_menu_entry::LeftPanelMenuEntryMessage;
use crate::ui::components::left_panel::left_panel_topbar::LeftPanelTopBarMessage;
use crate::ui::components::right_panel::right_panel_view::RightPanelViewMessage;
use crate::ui::components::settings_overlay::settings_theme::SettingsThemeMessage;
use crate::ui::components::terminal_drawer::terminal_drawer::TerminalDrawerMessage;
use crate::ui::components::settings_overlay::theme_preview::ThemePreviewMessage;
use crate::ui::components::terminal_drawer::terminal::TerminalMessage;
use crate::ui::left_panel::{LeftPanel, LeftPanelMessage};
use crate::ui::overlay::{Overlay, OverlayMessage, OverlayView};
use crate::ui::overlays::contexts_overlay::ContextsOverlayMessage;
use crate::ui::right_panel::{RightPanel, RightPanelMessage};
use crate::ui::overlays::settings_overlay::SettingsOverlayMessage;
use crate::ui::util::color_ext::ColorExtensions;
use crate::ui::util::theme::{Convert, YanaxiTheme};
use crate::ui::util::theming::window_background_style;
use crate::ui::util::view::{MenuEntryName, MenuGroupName};

const TERMINAL_FONT: &[u8] = include_bytes!("../../resources/fonts/MesloLGS.ttf");

pub struct YanaxiUI {
    yanaxi: Yanaxi,
    theme: YanaxiTheme,
    left_panel: LeftPanel,
    right_panel: RightPanel,
    overlay: Overlay,
    show_overlay: bool,
    window_id: window::Id
}

#[derive(Debug, Clone)]
pub enum Message{
    LeftPanel(LeftPanelMessage),
    RightPanel(RightPanelMessage),
    Overlay(OverlayMessage),
    TerminalDrawer(TerminalDrawerMessage),
    OpenWindow(window::Id),
    TerminalDrawerStoreSize(Size),
    Close(window::Id),
    TerminalEvent(Event),
    LoadTerminalFont(Result<(), iced::font::Error>)
}
const MIN_LEFTMENU_WIDTH: f32 = 200.0;
const MAX_LEFTMENU_WIDTH: f32 = 450.0;

impl YanaxiUI {
    pub(crate) fn new(yanaxi: Yanaxi) -> (Self, Task<Message>) {
        let theme = yanaxi.app_data.user_settings.theme;
        let window_settings = window::Settings {
            min_size: Option::from(Size {
                width: 500.0,
                height: 400.0,
            }),
            exit_on_close_request: true,
            ..window::Settings::default()

        };

        let (id, open) = window::open(window_settings);
        (
            Self {
                yanaxi,
                theme,
                left_panel: LeftPanel::new(),
                right_panel: RightPanel::new(theme),
                overlay: Overlay::new(),
                show_overlay: false,
                window_id: id
            },
            Task::batch(vec![
                open.map(Message::OpenWindow),
                iced::font::load(TERMINAL_FONT).map(Message::LoadTerminalFont)
            ])

        )
    }

    pub(crate) fn title(&self, window_id: window::Id) -> String {
        String::from("Yanaxi")
    }

    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::LeftPanel(left_panel_message) => {
                match left_panel_message {
                    LeftPanelMessage::TopBar(ref top_bar_message) => {
                        match top_bar_message {
                            LeftPanelTopBarMessage::SettingsButton(settings_button_message) => {
                                match settings_button_message {
                                    ImageButtonMessage::Press => {
                                        self.show_overlay = !self.show_overlay;
                                        self.overlay.update(OverlayMessage::SwitchView(OverlayView::Settings));
                                        //self.right_panel.update(RightPanelMessage::SwitchView(MenuEntryName::Settings));
                                        //self.left_panel.menu.update(LeftPanelMenuMessage::LeftPanelMenuEntry(LeftPanelMenuEntryMessage::Select(MenuGroupName::None, MenuEntryName::None)));
                                        Task::none()
                                    }
                                    _ => {
                                        self.left_panel.update(left_panel_message);
                                        Task::none()
                                    }
                                }
                            }
                            LeftPanelTopBarMessage::ContextButton(context_button_message) => {
                                match context_button_message {
                                    ImageButtonMessage::Press => {
                                        self.show_overlay = !self.show_overlay;
                                        self.overlay.update(OverlayMessage::SwitchView(OverlayView::Contexts));
                                        //self.right_panel.update(RightPanelMessage::SwitchView(MenuEntryName::Contexts));
                                        //self.left_panel.menu.update(LeftPanelMenuMessage::LeftPanelMenuEntry(LeftPanelMenuEntryMessage::Select(MenuGroupName::None, MenuEntryName::None)));
                                        Task::none()
                                    }
                                    _ => {
                                        self.left_panel.update(left_panel_message);
                                        Task::none()
                                    }
                                }
                            },
                        }
                    }
                    LeftPanelMessage::LeftPanelMenu(left_panel_menu_message) => {
                        match left_panel_menu_message {
                            LeftPanelMenuMessage::LeftPanelMenuEntry(left_panel_menu_entry_message) => {
                                match left_panel_menu_entry_message {
                                    LeftPanelMenuEntryMessage::Select(group, entry) => {
                                        self.right_panel.update(RightPanelMessage::SwitchView(entry));
                                        self.left_panel.menu.update(LeftPanelMenuMessage::LeftPanelMenuEntry(left_panel_menu_entry_message));
                                        Task::none()
                                    }
                                    _ => {
                                        self.left_panel.menu.update(LeftPanelMenuMessage::LeftPanelMenuEntry(left_panel_menu_entry_message));
                                        Task::none()
                                    }
                                }
                            }
                        }
                    }
                    _ => {
                        self.left_panel.update(left_panel_message);
                        Task::none()
                    }
                }
            },
            Message::RightPanel(right_panel_message) => {
                match right_panel_message {
                    RightPanelMessage::RightPanelView(right_panel_view_message) => {
                        match right_panel_view_message {
                            _ => {
                                self.right_panel.view.update(right_panel_view_message);
                                Task::none()
                            }
                        }
                    }
                    _ => {
                        self.right_panel.update(right_panel_message);
                        Task::none()
                    }
                }
            },
            Message::TerminalDrawer(_terminal_drawer_message) => {
                self.right_panel.view.terminal_drawer.update(_terminal_drawer_message);
                window::get_size(self.window_id).map(Message::TerminalDrawerStoreSize)
            }
            Message::TerminalDrawerStoreSize(size) => {
                self.right_panel.view.terminal_drawer.update(TerminalDrawerMessage::StoreWindowHeight(size));
                Task::none()
            }
            Message::OpenWindow(window_id) => {
                Task::none()
            }
            Message::Close(window_id) => {
                if window_id == self.window_id {
                    iced::exit()
                } else {
                    Task::none()
                }
            }
            Message::TerminalEvent(iced_term::Event::CommandReceived(session_id, cmd)) => {
                if let Some(terminal) = self.right_panel.view.terminal_drawer.terminals.get_mut(&session_id) {
                    terminal.terminal.update(cmd);
                }
                Task::none()
            }
            Message::Overlay(message) => {
                match message {
                    OverlayMessage::Settings(_message) => {
                        match _message {
                            SettingsOverlayMessage::SettingsTheme(SettingsThemeMessage::ThemePreview(ThemePreviewMessage::Select(theme))) => {
                                self.theme = theme;
                                self.yanaxi.app_data.change_theme(theme);
                                self.right_panel.view.terminal_drawer.update(TerminalDrawerMessage::Terminal(TerminalMessage::Theme(theme)));
                            }
                        }

                    }
                    OverlayMessage::Contexts(_message) => {
                        match _message {
                            ContextsOverlayMessage::SelectContext(context) => {
                                println!("Selected context {}", context.name);
                                //TODO Connect to cluster
                            }
                            _ => {
                                self.overlay.contexts.update(_message);
                            }
                        }
                    }
                    OverlayMessage::SwitchView(_) => {}
                    OverlayMessage::Close => {
                        self.show_overlay = false;
                        self.overlay.update(message);
                    }
                }
                Task::none()
            }
            Message::LoadTerminalFont(_) => {
                Task::none()
            }
        }
    }

    pub fn view(&self, window_id: window::Id) -> iced::Element<'_, Message> {
        let mut stack = stack![];

        let left = self.left_panel.view().map(Message::LeftPanel);
        let right= self.right_panel.view().map(Message::RightPanel);

        stack = stack.push(row![left, right]);
        if self.show_overlay {
            let overlay = self.overlay.view().map(Message::Overlay);
            stack = stack.push(overlay);
        }

        container(stack)
            .height(Length::Fill)
            .padding(0)
            .style(|theme: &Theme| window_background_style(theme))
            .into()
    }

    pub fn theme(&self, window_id: window::Id) -> Theme {
        self.yanaxi.app_data.user_settings.theme.convert()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let mut subscriptions = vec![];
        let window_close_subscription = window::close_events().map(Message::Close);
        subscriptions.push(window_close_subscription);
        let left_panel_subscription = self.left_panel.subscription().map(Message::LeftPanel);
        subscriptions.push(left_panel_subscription);
        let terminal_drawer_subscription = self.right_panel.view.terminal_drawer.subscription().map(Message::TerminalDrawer);
        subscriptions.push(terminal_drawer_subscription);

        for (id, terminal) in &self.right_panel.view.terminal_drawer.terminals {
            let terminal_subscription = iced_term::Subscription::new(terminal.terminal.id);
            let terminal_event_stream = terminal_subscription.event_stream();
            let subscription = Subscription::run_with_id(terminal.terminal.id, terminal_event_stream)
                .map(Message::TerminalEvent);
            subscriptions.push(subscription);
        }

        Subscription::batch(subscriptions)
    }
}
leonardosalsi commented 2 days ago

P.S: I just noticed that when I resize my application, there is some resizing also happening in the terminal, but again with a weird margin.

For example, when i use the terminal with default window size, it looks like this image

When i resize, the terminal itself also seems to resize

image

Harzu commented 1 day ago

I experimented with the full_screen example and tried to implement something more complex than a single container.

fn view(&self) -> Element<Event, Theme, iced::Renderer> {
    let stack = stack![
        row![
            self.left(),
            self.right(),
        ]
    ];

    container(stack)
        .height(Length::Fill)
        .padding(0)
        .into()
}

fn left(&self)  -> Element<Event, Theme, iced::Renderer> {
    container(text!("left side"))
        .padding(10)
        .width(Length::Fixed(200.0))
        .height(Length::Fill)
        .into()
}

fn right(&self) -> Element<Event, Theme, iced::Renderer> {
    let column = column![
        container(text("top")).height(Length::Fixed(500.0)),
        self.term_view()
    ];
    column.into()
}

fn term_view(&self) -> Element<Event, Theme, iced::Renderer> {
    container(TerminalView::show(&self.term).map(Event::Terminal))
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

It worked as expected.

Screenshot 2024-10-19 at 23 34 37

How the widget view works.

The widget is essentially a container with a payload that fills the parent dimensions.

pub fn show(term: &'a Terminal) -> Element<'_, Event> {
    container(Self { term })
        .width(Length::Fill)
        .height(Length::Fill)
        .style(|_| term.theme.container_style())
        .into()
}

Resizing is handled by comparing the current dimension with those from widget state within the receiving any of ICED events.

let layout_size = layout.bounds().size();
if state.size != layout_size && self.term.backend.is_some() {
    state.size = layout_size;
    let cmd = Command::ProcessBackendCommand(BackendCommand::Resize(
        Some(layout_size),
        None,
    ));
    shell.publish(Event::CommandReceived(self.term.id, cmd));
}

If they are not equal widget_view sends the Resize event with the new dimension to the application. If the application handles it and forwards it to the terminal.update() method, the terminal backend have to handle it and recalculate number of rows and cols. This method is worked in a lot of cases including the complex (a lot of cases that I tested, of course), so as I can understand the problem could be happen in a several cases:

  1. The event subscription is not set up, but it is not your case as I can see based on your code.
  2. The application does not process the CommandReceived event. It is not your case too.
  3. The parent container that render the terminal view in your application does not change its dimension. This case can happen but if the terminal resizes after manually resizing the window it is not your case too
  4. Incorrect parent dimensions - Ensure that all of parent containers in your terminal component also filling right. since the internal behavior of the widget is to stretch along the parent, you can expect that when the ICED resize the parent component, bounds of terminal_view is also will be changed but only until the first parent, so If you have a complex widget tree it can happen.

f.e

container(
    container(
        container(TerminalView::show(&self.term).map(Event::Terminal))
        .width(Length::Fill)
        .height(Length::Fill)
    )
    .width(Length::Fixed(400.0))
).width(Length::Fill)
.height(Length::Fill)
.into()
Screenshot 2024-10-19 at 23 31 08
leonardosalsi commented 53 minutes ago

I re-checked everything, according to my implementation the terminal is embedded always such that it has to take up its parents dimensions with .width(Length::Fill) and .height(Length::Fill). I will refactor my code nontheless to make sure that everything is set up correctly. If you were not able to reproduce the error under similar enough circumstances, I guess you can close the issue, since the problem must lie in the way I embed the terminal.

Thank you very much still, I got a lot of crucial infos from this thread :)