iced_aw icon indicating copy to clipboard operation
iced_aw copied to clipboard

Tabs panic under certain circumstances

Open andrew-voidoverride opened this issue 10 months ago • 4 comments

thread 'main' panicked at C:\Users\Andrew\.cargo\registry\src\index.crates.io-6f17d22bba15001f\iced_aw-0.12.0\src\widget\tabs.rs:355:39:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I managed to hack around it by changing:

if let Some(element) = self.tabs.get(self.tab_bar.get_active_tab_idx()) {
  element.as_widget().layout(
    &mut tree.children[1].children[self.tab_bar.get_active_tab_idx()],
    renderer,
    &tab_content_limits,
  )

to

if let (Some(element), Some(child)) = (self.tabs.get(self.tab_bar.get_active_tab_idx()), tree.children.get_mut(1)) {
element.as_widget().layout(
  &mut child.children[self.tab_bar.get_active_tab_idx()],
  renderer,
  &tab_content_limits,
)

and by changing Tabs::diff() to:

fn diff(&self, tree: &mut Tree) {
  if tree.children.len() != 2 {
    tree.children = self.children();
  }

  if let Some(tabs) = tree.children.get_mut(1) {
    tabs.diff_children(&self.tabs);
  }
}

andrew-voidoverride avatar Feb 16 '25 03:02 andrew-voidoverride

Hi,

Which version of iced_aw are you using ? It reminds me of an issue that should have been patched with the last release (0.12) If it's not the case I'll try to investigate that.

Ultraxime avatar Feb 16 '25 10:02 Ultraxime

in the error message it does say 0.12. It is odd the thing was not yet fix. Can we see a minimal breaking Example of this?

genusistimelord avatar Feb 16 '25 15:02 genusistimelord

My bad. Maybe it was a similar issue with the menu bar. I'll try to look into it if I have the time. But as @genusistimelord said, a minimal exemple of the bug would be good.

Ultraxime avatar Feb 16 '25 16:02 Ultraxime

I was able to create a minimal example. You can trigger the panic by switcing to the "No Tabs" view, then back to the "Tabs" view:

main.rs:

use std::{path::PathBuf, sync::Arc};

use iced::{border, futures::{channel::mpsc, SinkExt, Stream, StreamExt}, stream::channel, widget::{button, center, column, container, mouse_area, opaque, stack, text}, Element, Length::{self}, Padding, Subscription, Task, Theme};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GUIState { 
    NoTabs,
    Tabs(TabId),
} 

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GUIConfig { 
    pub state: GUIState
}

impl GUIConfig {
    pub fn path() -> PathBuf {
        std::env::current_dir().unwrap().join("config.json")
    }

    pub fn read(config_path: &PathBuf) -> Result<Self, GUIError> {
        Self::ensure_exists(config_path)?;
        let config_str = std::fs::read_to_string(config_path)?;
        Ok(serde_json::from_str::<Self>(&config_str)?)
    }
    
    pub fn write(&self, config_path: &PathBuf) -> Result<(), GUIError> {
        std::fs::create_dir_all(config_path.parent().unwrap())?;
        let config_str = serde_json::to_string_pretty(self).unwrap();
        Ok(std::fs::write(config_path, config_str)?)
    }
    
    pub fn ensure_exists(config_path: &PathBuf) -> Result<(), GUIError> {
        if !config_path.try_exists()? {
            GUIConfig { state: GUIState::Tabs(TabId::One) }.write(config_path)?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone)]
pub struct NoTabs { } 

impl NoTabs {
    pub fn new() -> Self {
        Self { }  
    }

    pub fn update(&mut self, msg: NoTabsMessage) -> Task<GUIMessage> {
        match msg {
            NoTabsMessage::SwitchViewButtonPressed => {
                let mut gui_config = GUIConfig::read(&GUIConfig::path()).unwrap();
                gui_config.state = GUIState::Tabs(TabId::One);
                let _ = gui_config.write(&GUIConfig::path()).unwrap();
                Task::none()
            },
        }
    }    

    pub fn view(&self) -> Element<NoTabsMessage> {
        let underlay = stack![
            center(column![
                text("Background Text".to_owned())
            ])
        ];

        fn background_style(theme: &Theme) -> container::Style {
            let palette = theme.extended_palette();
            let mut background = palette.background.weak.color;
            background.a = 0.67;
        
            container::Style {
                background: Some(background.into()),
                ..container::Style::default()
            }
        }
        
        fn overlay_style(theme: &Theme) -> container::Style {
            container::Style {
                background: Some(theme.extended_palette().background.strong.color.into()),
                border: border::rounded(10),
                ..container::Style::default()
            }
        }

        let modal = opaque(
            mouse_area(opaque(
                container(
                    container(
                        button("Switch View").on_press(NoTabsMessage::SwitchViewButtonPressed)
                    )
                    .style(overlay_style)
                )
                .style(background_style)
                .padding(Padding::new(50.0))
                .height(Length::Fill)
                .width(Length::Fill)
            ))
        );

        stack![center(underlay).padding(20)]
            .push(modal)
            .into()        
    }
}

#[derive(Debug, Clone)]
pub struct Tabs {
    selected_tab: TabId
 }

impl Tabs {
    pub fn new(selected_tab: TabId) -> (Self, Task<GUIMessage>) {
        (
            Self { selected_tab },
            Task::batch(vec![
                Task::none(),
                Task::none(),
            ])
        )  
    }

    pub fn update(&mut self, msg: TabsMessage) -> Task<GUIMessage> {
        match msg {
            TabsMessage::TabSelected(tab_id) => { 
                self.selected_tab = tab_id;
                Task::none()
            },
            TabsMessage::SwitchViewButtonPressed => {
                let mut gui_config = GUIConfig::read(&GUIConfig::path()).unwrap();
                gui_config.state = GUIState::NoTabs;
                let _ = gui_config.write(&GUIConfig::path()).unwrap();
                Task::none()
            },
        }
    }    

    pub fn view(&self) -> Element<TabsMessage> {
        let tabs: Element<_> = iced_aw::Tabs::new(TabsMessage::TabSelected)
            .push(TabId::One,   iced_aw::TabLabel::Text("Tab One".to_owned()),   button("Switch View").on_press(TabsMessage::SwitchViewButtonPressed))
            .push(TabId::Two,   iced_aw::TabLabel::Text("Tab Two".to_owned()),   button("Switch View").on_press(TabsMessage::SwitchViewButtonPressed))
            .push(TabId::Three, iced_aw::TabLabel::Text("Tab Three".to_owned()), button("Switch View").on_press(TabsMessage::SwitchViewButtonPressed))
            .set_active_tab(&self.selected_tab)
            .into();

        stack![tabs].into()
    }
}

#[derive(Clone)]
pub enum View {
    NoTabs(NoTabs),
    Tabs(Tabs),
}

#[derive(Debug, Clone)]
pub enum ViewMessage {
    NoTabs(NoTabsMessage),
    Tabs(TabsMessage),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TabId { 
    One,
    Two,
    Three,
}

#[derive(Debug, Clone)]
pub enum TabsMessage { 
    TabSelected(TabId),
    SwitchViewButtonPressed,
}

#[derive(Debug, Clone)]

pub enum NoTabsMessage {
    SwitchViewButtonPressed,
}

#[derive(Debug, Clone)]
pub enum GUIMessage {
    ConfigEvent(ConfigEvent),
    Refresh,
    View(ViewMessage),
}

#[derive(Debug, thiserror::Error)]
pub enum GUIError {
    #[error(transparent)]
    GUIError(#[from] iced::Error),
    #[error(transparent)]
    IOError(#[from] std::io::Error),
    #[error(transparent)]
    SerdeJsonError(#[from] serde_json::Error),
}

struct GUI {
    pub active_view: View,
}

impl GUI {
    fn new() -> (Self, Task<GUIMessage>) {
        let mut new = Self {
            active_view: View::NoTabs(NoTabs::new())
        };
        
        let tasks = new.init();
        
        (new, tasks)
    } 

    fn init(&mut self) -> Task<GUIMessage> {
        let gui_config = GUIConfig::read(&GUIConfig::path()).unwrap();
        let (active_view, tasks) = match gui_config.state {
            GUIState::NoTabs => (View::NoTabs(NoTabs::new()), Task::none()),
            GUIState::Tabs(tab_id) => {
                let (tabs, tasks) = Tabs::new(tab_id);
                (View::Tabs(tabs), tasks)
            },
        };

        self.active_view = active_view;
        tasks     
    }

    fn title(&self) -> String { "GUI".to_owned() }

    fn update(&mut self, msg: GUIMessage) -> Task<GUIMessage> {
        match msg {
            GUIMessage::ConfigEvent(msg) => match msg {
                ConfigEvent::NotifyError(error) => {
                    eprintln!("{:?}", error);
                    Task::none()
                },
                ConfigEvent::Updated => Task::done(GUIMessage::Refresh),
            },
            GUIMessage::Refresh => self.init(),
            GUIMessage::View(msg) => match msg {
                ViewMessage::NoTabs(msg) => {
                    let View::NoTabs(no_tabs) = &mut self.active_view else {
                        return Task::none();
                    };
                    
                    no_tabs.update(msg)
                },
                ViewMessage::Tabs(msg) => {
                    let View::Tabs(tabs) = &mut self.active_view else {
                        return Task::none();
                    };
                    
                    tabs.update(msg)
                },
            },
        }   
    }

    fn view(&self) -> Element<GUIMessage> {
        match &self.active_view {
            View::NoTabs(no_tabs) => no_tabs.view().map(|msg| GUIMessage::View(ViewMessage::NoTabs(msg))),
            View::Tabs(tabs) => tabs.view().map(|msg| GUIMessage::View(ViewMessage::Tabs(msg))),
        }
    }

    fn subscription(&self) -> Subscription<GUIMessage> {
        Subscription::run(watch_config).map(GUIMessage::ConfigEvent)   
    }
}

#[derive(Debug, Clone)]
pub enum ConfigEvent {
    NotifyError(Arc<notify::Error>),
    Updated,
}

pub fn watch_config() -> impl Stream<Item = ConfigEvent> {
    channel(4, move |mut output| async move {
        let (mut notify_tx, mut notify_rx) = mpsc::channel(4);
        let mut config_watcher = RecommendedWatcher::new(
            move |res| { iced::futures::executor::block_on( async {
                    let _ = notify_tx.send(res).await;
                })
            }, Config::default()
        ).unwrap();
        let _ = config_watcher.watch(&GUIConfig::path(), RecursiveMode::NonRecursive);

        loop { match notify_rx.select_next_some().await {
            Ok(_) => {
                let _ = output.send(ConfigEvent::Updated).await;
            },
            Err(err) =>{
                let _ = output.send(ConfigEvent::NotifyError(err.into())).await;
            },
        }}
    })
}

fn main() -> Result<(), GUIError> {
    GUIConfig::ensure_exists(&GUIConfig::path())?;

    Ok(iced::application(GUI::title, GUI::update, GUI::view)
        .subscription(GUI::subscription)
        .run_with(GUI::new)?)
}

Cargo.toml:

[package]
name = "tabs-panic"
version = "0.1.0"
edition = "2021"

[dependencies]
iced = { version = "0.13.1", features = ["advanced", "debug", "lazy", "svg", "tokio"]}
iced_aw = { version = "0.12.0", default-features = false, features = ["tabs"] }
notify = { version = "7.0.0", default-features = false, features = ["macos_kqueue", "serde"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
thiserror = "2.0.11"

andrew-voidoverride avatar Feb 16 '25 17:02 andrew-voidoverride