Raw File
top_bar.rs
/*
ENSnano, a 3d graphical application for DNA nanostructures.
    Copyright (C) 2021  Nicolas Levy <nicolaspierrelevy@gmail.com> and Nicolas Schabanel <nicolas.schabanel@ens-lyon.fr>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
use std::sync::{Arc, Mutex};
use std::thread;

use super::{ApplicationState, UiSize};
use iced::{container, Background, Container};
use iced_native::clipboard::Null as NullClipBoard;
use iced_wgpu::Renderer;
use iced_winit::winit::dpi::LogicalSize;
use iced_winit::{button, Button, Color, Command, Element, Length, Program, Row};

use material_icons::{icon_to_char, Icon as MaterialIcon, FONT as MATERIALFONT};

const ICONFONT: iced::Font = iced::Font::External {
    name: "IconFont",
    bytes: MATERIALFONT,
};

fn icon(icon: MaterialIcon, ui_size: UiSize) -> iced::Text {
    iced::Text::new(format!("{}", icon_to_char(icon)))
        .font(ICONFONT)
        .size(ui_size.icon())
}

use super::{KeepProceed, Requests, SplitMode};

pub struct TopBar {
    button_fit: button::State,
    button_add_file: button::State,
    #[allow(dead_code)]
    button_replace_file: button::State,
    button_save: button::State,
    button_undo: button::State,
    button_redo: button::State,
    button_3d: button::State,
    button_2d: button::State,
    button_split: button::State,
    button_oxdna: button::State,
    button_split_2d: button::State,
    button_help: button::State,
    button_tutorial: button::State,
    button_new_empty_design: button::State,
    requests: Arc<Mutex<Requests>>,
    logical_size: LogicalSize<f64>,
    dialoging: Arc<Mutex<bool>>,
    ui_size: UiSize,
    application_state: ApplicationState,
}

#[derive(Debug, Clone)]
pub enum Message {
    SceneFitRequested,
    FileAddRequested,
    OpenFileButtonPressed,
    #[allow(dead_code)]
    FileReplaceRequested,
    FileSaveRequested(Option<KeepProceed>),
    Resize(LogicalSize<f64>),
    ToggleView(SplitMode),
    UiSizeChanged(UiSize),
    OxDNARequested,
    Split2d,
    NewApplicationState(ApplicationState),
    ForceHelp,
    ShowTutorial,
    Undo,
    Redo,
    ButtonNewEmptyDesignPressed,
}

impl TopBar {
    pub fn new(
        requests: Arc<Mutex<Requests>>,
        logical_size: LogicalSize<f64>,
        dialoging: Arc<Mutex<bool>>,
    ) -> TopBar {
        Self {
            button_fit: Default::default(),
            button_add_file: Default::default(),
            button_replace_file: Default::default(),
            button_save: Default::default(),
            button_undo: Default::default(),
            button_redo: Default::default(),
            button_2d: Default::default(),
            button_3d: Default::default(),
            button_split: Default::default(),
            button_oxdna: Default::default(),
            button_split_2d: Default::default(),
            button_help: Default::default(),
            button_tutorial: Default::default(),
            button_new_empty_design: Default::default(),
            requests,
            logical_size,
            dialoging,
            ui_size: Default::default(),
            application_state: Default::default(),
        }
    }

    pub fn resize(&mut self, logical_size: LogicalSize<f64>) {
        self.logical_size = logical_size;
    }
}

impl Program for TopBar {
    type Renderer = Renderer;
    type Message = Message;
    type Clipboard = NullClipBoard;

    fn update(&mut self, message: Message, _cb: &mut NullClipBoard) -> Command<Message> {
        match message {
            Message::SceneFitRequested => {
                self.requests.lock().expect("fitting_requested").fitting = true;
            }
            Message::OpenFileButtonPressed => {
                crate::save_before_open(self.requests.clone());
            }
            Message::FileAddRequested => {
                if !*self.dialoging.lock().unwrap() {
                    *self.dialoging.lock().unwrap() = true;
                    let requests = self.requests.clone();
                    let dialog = rfd::AsyncFileDialog::new().pick_file();
                    let dialoging = self.dialoging.clone();
                    thread::spawn(move || {
                        let load_op = async move {
                            let file = dialog.await;
                            if let Some(handle) = file {
                                let path_buf: std::path::PathBuf = handle.path().clone().into();
                                requests.lock().unwrap().file_add = Some(path_buf);
                            }
                            *dialoging.lock().unwrap() = false;
                        };
                        futures::executor::block_on(load_op);
                    });
                    /*
                    if cfg!(target_os = "macos") {
                        // do not spawn a new thread on macos
                        let result = match nfd2::open_file_dialog(None, None).expect("oh no") {
                            Response::Okay(file_path) => Some(file_path),
                            Response::OkayMultiple(_) => {
                                println!("Please open only one file");
                                None
                            }
                            Response::Cancel => None,
                        };
                        *self.dialoging.lock().unwrap() = false;
                        if let Some(path) = result {
                            requests.lock().expect("file_opening_request").file_add = Some(path);
                        }
                    } else {
                        let dialoging = self.dialoging.clone();
                        thread::spawn(move || {
                            let result = match nfd2::open_file_dialog(None, None).expect("oh no") {
                                Response::Okay(file_path) => Some(file_path),
                                Response::OkayMultiple(_) => {
                                    println!("Please open only one file");
                                    None
                                }
                                Response::Cancel => None,
                            };
                            *dialoging.lock().unwrap() = false;
                            if let Some(path) = result {
                                requests.lock().expect("file_opening_request").file_add =
                                    Some(path);
                            }
                        });
                    }*/
                }
            }
            Message::FileReplaceRequested => {
                self.requests
                    .lock()
                    .expect("file_opening_request")
                    .file_clear = false;
            }
            Message::FileSaveRequested(keep_proceed) => {
                if !*self.dialoging.lock().unwrap() {
                    *self.dialoging.lock().unwrap() = true;
                    let requests = self.requests.clone();
                    let dialog = rfd::AsyncFileDialog::new().save_file();
                    let dialoging = self.dialoging.clone();
                    thread::spawn(move || {
                        let save_op = async move {
                            let file = dialog.await;
                            if let Some(handle) = file {
                                let mut path_buf: std::path::PathBuf = handle.path().clone().into();
                                let extension = path_buf.extension().clone();
                                if extension.is_none() {
                                    path_buf.set_extension("json");
                                } else if extension.and_then(|e| e.to_str()) != Some("json".into())
                                {
                                    let extension = extension.unwrap();
                                    let new_extension =
                                        format!("{}.json", extension.to_str().unwrap());
                                    path_buf.set_extension(new_extension);
                                }
                                requests.lock().unwrap().file_save = Some((path_buf, keep_proceed));
                            }
                            *dialoging.lock().unwrap() = false;
                        };
                        futures::executor::block_on(save_op);
                    });
                }
            }
            Message::Resize(size) => self.resize(size),
            Message::ToggleView(b) => self.requests.lock().unwrap().toggle_scene = Some(b),
            Message::UiSizeChanged(ui_size) => self.ui_size = ui_size,
            Message::OxDNARequested => self.requests.lock().unwrap().oxdna = true,
            Message::Split2d => self.requests.lock().unwrap().split2d = true,
            Message::NewApplicationState(state) => self.application_state = state,
            Message::Undo => self.requests.lock().unwrap().undo = Some(()),
            Message::Redo => self.requests.lock().unwrap().redo = Some(()),
            Message::ForceHelp => self.requests.lock().unwrap().force_help = Some(()),
            Message::ShowTutorial => self.requests.lock().unwrap().show_tutorial = Some(()),
            Message::ButtonNewEmptyDesignPressed => crate::save_before_new(self.requests.clone()),
        };
        Command::none()
    }

    fn view(&mut self) -> Element<Message, Renderer> {
        let height = self.logical_size.cast::<u16>().height;
        let top_size_info = TopSizeInfo::new(self.ui_size.clone(), height);
        let button_fit = Button::new(
            &mut self.button_fit,
            icon(MaterialIcon::CenterFocusStrong, self.ui_size.clone()),
        )
        .on_press(Message::SceneFitRequested)
        .height(Length::Units(height));

        let button_new_empty_design = bottom_tooltip_icon_btn(
            &mut self.button_new_empty_design,
            MaterialIcon::InsertDriveFile,
            &top_size_info,
            "Load empty design",
            Some(Message::ButtonNewEmptyDesignPressed),
        );

        let button_add_file = bottom_tooltip_icon_btn(
            &mut self.button_add_file,
            MaterialIcon::Folder,
            &top_size_info,
            "Open",
            Some(Message::OpenFileButtonPressed),
        );

        let save_message = Message::FileSaveRequested(None);
        let button_save = bottom_tooltip_icon_btn(
            &mut self.button_save,
            MaterialIcon::Save,
            &top_size_info,
            "Save As..",
            Some(save_message),
        );

        let mut button_undo = Button::new(
            &mut self.button_undo,
            icon(MaterialIcon::Undo, self.ui_size.clone()),
        );
        if self.application_state.can_undo {
            button_undo = button_undo.on_press(Message::Undo)
        }

        let mut button_redo = Button::new(
            &mut self.button_redo,
            icon(MaterialIcon::Redo, self.ui_size.clone()),
        );
        if self.application_state.can_redo {
            button_redo = button_redo.on_press(Message::Redo)
        }

        let button_2d = Button::new(&mut self.button_2d, iced::Text::new("2D"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::ToggleView(SplitMode::Flat));
        let button_3d = Button::new(&mut self.button_3d, iced::Text::new("3D"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::ToggleView(SplitMode::Scene3D));
        let button_split = Button::new(&mut self.button_split, iced::Text::new("3D+2D"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::ToggleView(SplitMode::Both));

        let button_oxdna = Button::new(&mut self.button_oxdna, iced::Text::new("To OxView"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::OxDNARequested);
        let oxdna_tooltip = button_oxdna;

        let button_split_2d = Button::new(&mut self.button_split_2d, iced::Text::new("(Un)split"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::Split2d);

        let button_help = Button::new(&mut self.button_help, iced::Text::new("Help"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::ForceHelp);

        let button_tutorial = Button::new(&mut self.button_tutorial, iced::Text::new("Tutorials"))
            .height(Length::Units(self.ui_size.button()))
            .on_press(Message::ShowTutorial);

        let buttons = Row::new()
            .width(Length::Fill)
            .height(Length::Units(height))
            .push(button_new_empty_design)
            .push(button_add_file)
            .push(button_save)
            .push(oxdna_tooltip)
            .push(iced::Space::with_width(Length::Units(10)))
            .push(button_3d)
            .push(button_2d)
            .push(button_split)
            .push(button_split_2d)
            .push(iced::Space::with_width(Length::Units(10)))
            .push(button_fit)
            .push(iced::Space::with_width(Length::Units(10)))
            .push(button_undo)
            .push(button_redo)
            .push(iced::Space::with_width(Length::Units(10)))
            .push(button_help)
            .push(iced::Space::with_width(Length::Units(2)))
            .push(button_tutorial)
            .push(
                iced::Text::new("\u{e91c}")
                    .width(Length::Fill)
                    .horizontal_alignment(iced::HorizontalAlignment::Right)
                    .vertical_alignment(iced::VerticalAlignment::Center),
            )
            .push(iced::Space::with_width(Length::Units(10)));

        Container::new(buttons)
            .width(Length::Units(self.logical_size.width as u16))
            .style(TopBarStyle)
            .into()
    }
}

struct TopBarStyle;
impl container::StyleSheet for TopBarStyle {
    fn style(&self) -> container::Style {
        container::Style {
            background: Some(Background::Color(BACKGROUND)),
            text_color: Some(Color::WHITE),
            ..container::Style::default()
        }
    }
}

pub const BACKGROUND: Color = Color::from_rgb(
    0x36 as f32 / 255.0,
    0x39 as f32 / 255.0,
    0x3F as f32 / 255.0,
);

#[derive(Clone)]
struct TopSizeInfo {
    ui_size: UiSize,
    height: iced::Length,
}

impl TopSizeInfo {
    fn new(ui_size: UiSize, height: u16) -> Self {
        Self {
            ui_size,
            height: iced::Length::Units(height),
        }
    }
}

#[allow(unused_imports)]
use iced::tooltip::Position as ToolTipPosition;
#[allow(unused_imports)]
use iced::Tooltip;
fn bottom_tooltip_icon_btn<'a, M: 'a + Clone>(
    state: &'a mut button::State,
    icon_char: MaterialIcon,
    size: &TopSizeInfo,
    _tooltip_text: impl ToString,
    on_press: Option<M>,
) -> Button<'a, M, Renderer> {
    let mut button = Button::new(state, icon(icon_char, size.ui_size.clone())).height(size.height);
    if let Some(on_press) = on_press {
        button = button.on_press(on_press);
    }
    button
    //Tooltip::new(button, tooltip_text, ToolTipPosition::Bottom).style(ToolTipStyle)
}

struct ToolTipStyle;
impl iced::container::StyleSheet for ToolTipStyle {
    fn style(&self) -> iced::container::Style {
        iced::container::Style {
            text_color: Some(iced::Color::BLACK),
            ..Default::default()
        }
    }
}
back to top