Support splitting of terminals using a Pane Grid

This commit is contained in:
Mattias Eriksson 2024-01-17 16:09:56 +01:00
parent 7f37ede453
commit 1940f2c056
5 changed files with 572 additions and 266 deletions

View file

@ -48,3 +48,6 @@ menu-settings = Settings...
# Context menu
show-headerbar = Show header bar
split-horizontal = Split Horizontal
split-vertical = Split Vertical
pane-toggle-maximize = Toggle Pane Maximized

View file

@ -17,7 +17,7 @@ use cosmic::{
window, Alignment, Event, Length, Padding, Point,
},
style,
widget::{self, button, segmented_button},
widget::{self, button, container, pane_grid, segmented_button, PaneGrid},
Application, ApplicationExt, Element,
};
use cosmic_text::{fontdb::FaceInfo, Family, Stretch, Weight};
@ -41,7 +41,7 @@ mod localize;
use menu::menu_bar;
mod menu;
use terminal::{Terminal, TerminalScroll};
use terminal::{Terminal, TerminalPaneGrid, TerminalScroll};
mod terminal;
use terminal_box::terminal_box;
@ -167,6 +167,9 @@ pub enum Action {
Settings,
ShowHeaderBar(bool),
TabNew,
PaneSplitHorizontal,
PaneSplitVertical,
PaneToggleMaximized,
}
impl Action {
@ -178,6 +181,9 @@ impl Action {
Action::Settings => Message::ToggleContextPage(ContextPage::Settings),
Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar),
Action::TabNew => Message::TabNew,
Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical),
Action::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal),
Action::PaneToggleMaximized => Message::PaneToggleMaximized,
}
}
}
@ -199,6 +205,12 @@ pub enum Message {
FindNext,
FindPrevious,
FindSearchValueChanged(String),
PaneClicked(pane_grid::Pane),
PaneSplit(pane_grid::Axis),
PaneToggleMaximized,
PaneFocusAdjacent(pane_grid::Direction),
PaneDragged(pane_grid::DragEvent),
PaneResized(pane_grid::ResizeEvent),
Modifiers(Modifiers),
Paste(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String),
@ -212,8 +224,8 @@ pub enum Message {
TabContextAction(segmented_button::Entity, Action),
TabContextMenu(segmented_button::Entity, Option<Point>),
TabNew,
TermEvent(segmented_button::Entity, TermEvent),
TermEventTx(mpsc::Sender<(segmented_button::Entity, TermEvent)>),
TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent),
TermEventTx(mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>),
ToggleContextPage(ContextPage),
ShowAdvancedFontSettings(bool),
WindowClose,
@ -239,7 +251,7 @@ impl ContextPage {
/// The [`App`] stores application-specific state.
pub struct App {
core: Core,
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
pane_model: TerminalPaneGrid,
config_handler: Option<cosmic_config::Config>,
config: Config,
app_themes: Vec<String>,
@ -259,11 +271,11 @@ pub struct App {
theme_names: Vec<String>,
themes: HashMap<String, TermColors>,
context_page: ContextPage,
terminal_id: widget::Id,
terminal_ids: HashMap<pane_grid::Pane, widget::Id>,
find: bool,
find_search_id: widget::Id,
find_search_value: String,
term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>,
term_event_tx_opt: Option<mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>>,
startup_options: Option<tty::Options>,
term_config: TermConfig,
show_advanced_font_settings: bool,
@ -273,11 +285,14 @@ pub struct App {
impl App {
fn update_config(&mut self) -> Command<Message> {
//TODO: provide iterator over data
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.set_config(&self.config, &self.themes, self.zoom_adj);
let panes: Vec<_> = self.pane_model.panes.iter().collect();
for (_pane, tab_model) in panes {
let entities: Vec<_> = tab_model.iter().collect();
for entity in entities {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.set_config(&self.config, &self.themes, self.zoom_adj);
}
}
}
@ -303,22 +318,33 @@ impl App {
Command::none()
} else if self.find {
widget::text_input::focus(self.find_search_id.clone())
} else if let Some(terminal_id) = self.terminal_ids.get(&self.pane_model.focus).cloned() {
widget::text_input::focus(terminal_id)
} else {
widget::text_input::focus(self.terminal_id.clone())
Command::none()
}
}
// Call this any time the tab changes
fn update_title(&mut self) -> Command<Message> {
let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) {
Some(tab_title) => (
tab_title.to_string(),
format!("{tab_title} — COSMIC Terminal"),
),
None => (String::new(), "COSMIC Terminal".to_string()),
};
self.set_header_title(header_title);
Command::batch([self.set_window_title(window_title), self.update_focus()])
fn update_title(&mut self, pane: Option<pane_grid::Pane>) -> Command<Message> {
let pane = pane.unwrap_or(self.pane_model.focus);
if let Some(tab_model) = self.pane_model.panes.get(pane) {
let (header_title, window_title) = match tab_model.text(tab_model.active()) {
Some(tab_title) => (
tab_title.to_string(),
format!("{tab_title} — COSMIC Terminal"),
),
None => (String::new(), "COSMIC Terminal".to_string()),
};
self.set_header_title(header_title);
Command::batch([self.set_window_title(window_title), self.update_focus()])
} else {
log::error!("Failed to get the specific pane");
Command::batch([
self.set_window_title("COSMIC Terminal".to_string()),
self.update_focus(),
])
}
}
fn set_curr_font_weights_and_stretches(&mut self) {
@ -693,10 +719,13 @@ impl Application for App {
let themes = terminal_theme::terminal_themes();
let mut theme_names: Vec<_> = themes.keys().map(|x| x.clone()).collect();
theme_names.sort();
let pane_model = TerminalPaneGrid::new(segmented_button::ModelBuilder::default().build());
let mut terminal_ids = HashMap::new();
terminal_ids.insert(pane_model.focus, widget::Id::unique());
let mut app = App {
core,
tab_model: segmented_button::ModelBuilder::default().build(),
pane_model,
config_handler: flags.config_handler,
config: flags.config,
app_themes,
@ -716,7 +745,7 @@ impl Application for App {
theme_names,
themes,
context_page: ContextPage::Settings,
terminal_id: widget::Id::unique(),
terminal_ids,
find: false,
find_search_id: widget::Id::unique(),
find_search_value: String::new(),
@ -728,7 +757,7 @@ impl Application for App {
};
app.set_curr_font_weights_and_stretches();
let command = app.update_title();
let command = app.update_title(None);
(app, command)
}
@ -763,13 +792,17 @@ impl Application for App {
}
}
Message::Copy(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let term = terminal.term.lock();
if let Some(text) = term.selection_to_string() {
return clipboard::write(text);
if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let term = terminal.term.lock();
if let Some(text) = term.selection_to_string() {
return clipboard::write(text);
}
}
} else {
log::warn!("Failed to get focused pane");
}
}
Message::DefaultFont(index) => {
@ -781,14 +814,16 @@ impl Application for App {
let mut font_system = font_system().write().unwrap();
font_system.raw().db_mut().set_monospace_family(font_name);
}
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(terminal) =
self.tab_model.data::<Mutex<Terminal>>(entity)
{
let mut terminal = terminal.lock().unwrap();
terminal.update_cell_size();
let panes: Vec<_> = self.pane_model.panes.iter().collect();
for (_pane, tab_model) in panes {
let entities: Vec<_> = tab_model.iter().collect();
for entity in entities {
if let Some(terminal) =
tab_model.data::<Mutex<Terminal>>(entity)
{
let mut terminal = terminal.lock().unwrap();
terminal.update_cell_size();
}
}
}
@ -868,10 +903,12 @@ impl Application for App {
}
Message::FindNext => {
if !self.find_search_value.is_empty() {
let entity = self.tab_model.active();
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.search(&self.find_search_value, true);
if let Some(tab_model) = self.pane_model.active() {
let entity = tab_model.active();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.search(&self.find_search_value, true);
}
}
}
@ -880,10 +917,12 @@ impl Application for App {
}
Message::FindPrevious => {
if !self.find_search_value.is_empty() {
let entity = self.tab_model.active();
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.search(&self.find_search_value, false);
if let Some(tab_model) = self.pane_model.active() {
let entity = tab_model.active();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.search(&self.find_search_value, false);
}
}
}
@ -896,6 +935,93 @@ impl Application for App {
Message::Modifiers(modifiers) => {
self.modifiers = modifiers;
}
Message::PaneClicked(pane) => {
self.pane_model.focus = pane;
return self.update_title(Some(pane));
}
Message::PaneSplit(axis) => {
let result = self.pane_model.panes.split(
axis,
self.pane_model.focus,
segmented_button::ModelBuilder::default().build(),
);
if let Some((pane, _)) = result {
self.terminal_ids.insert(pane, widget::Id::unique());
self.pane_model.focus = pane;
match &self.term_event_tx_opt {
Some(term_event_tx) => match self.themes.get(self.config.syntax_theme()) {
Some(colors) => {
let current_pane = self.pane_model.focus;
if let Some(tab_model) = self.pane_model.active_mut() {
let entity = tab_model
.insert()
.text("New Terminal")
.closable()
.activate()
.id();
// Use the startup options, or defaults
let options = self.startup_options.take().unwrap_or_default();
let mut terminal = Terminal::new(
current_pane,
entity,
term_event_tx.clone(),
self.term_config.clone(),
options,
&self.config,
*colors,
);
terminal.set_config(&self.config, &self.themes, self.zoom_adj);
tab_model
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
} else {
log::error!("Found no active pane");
}
}
None => {
log::error!(
"failed to find terminal theme {:?}",
self.config.syntax_theme()
);
//TODO: fall back to known good theme
}
},
None => {
log::warn!("tried to create new tab before having event channel");
}
}
self.pane_model.panes_created += 1;
return self.update_title(Some(pane));
}
}
Message::PaneToggleMaximized => {
if self.pane_model.panes.maximized().is_some() {
self.pane_model.panes.restore();
} else {
self.pane_model.panes.maximize(self.pane_model.focus);
}
return self.update_focus();
}
Message::PaneFocusAdjacent(direction) => {
if let Some(adjacent) = self
.pane_model
.panes
.adjacent(self.pane_model.focus, direction)
{
self.pane_model.focus = adjacent;
return Command::batch([
self.update_focus(),
self.update_title(Some(adjacent)),
]);
}
}
Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => {
self.pane_model.panes.resize(split, ratio);
}
Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => {
self.pane_model.panes.drop(pane, target);
}
Message::PaneDragged(_) => {}
Message::Paste(entity_opt) => {
return clipboard::read(move |value_opt| match value_opt {
Some(value) => message::app(Message::PasteValue(entity_opt, value)),
@ -903,17 +1029,21 @@ impl Application for App {
});
}
Message::PasteValue(entity_opt, value) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
terminal.paste(value);
if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
terminal.paste(value);
}
}
}
Message::SelectAll(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.select_all();
if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.select_all();
}
}
}
Message::ShowHeaderBar(show_headerbar) => {
@ -945,78 +1075,96 @@ impl Application for App {
}
},
Message::TabActivate(entity) => {
self.tab_model.activate(entity);
return self.update_title();
if let Some(tab_model) = self.pane_model.active_mut() {
tab_model.activate(entity);
}
return self.update_title(None);
}
Message::TabClose(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab_model) = self.pane_model.active_mut() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
// Activate closest item
if let Some(position) = self.tab_model.position(entity) {
if position > 0 {
self.tab_model.activate_position(position - 1);
} else {
self.tab_model.activate_position(position + 1);
// Activate closest item
if let Some(position) = tab_model.position(entity) {
if position > 0 {
tab_model.activate_position(position - 1);
} else {
tab_model.activate_position(position + 1);
}
}
// Remove item
tab_model.remove(entity);
// If that was the last tab, close window
if tab_model.iter().next().is_none() {
if let Some((_state, sibling)) =
self.pane_model.panes.close(self.pane_model.focus)
{
self.pane_model.focus = sibling;
} else {
return window::close(window::Id::MAIN);
}
}
}
// Remove item
self.tab_model.remove(entity);
// If that was the last tab, close window
if self.tab_model.iter().next().is_none() {
return window::close(window::Id::MAIN);
}
return self.update_title();
return self.update_title(None);
}
Message::TabContextAction(entity, action) => {
match self.tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
// Close context menu
{
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = None;
if let Some(tab_model) = self.pane_model.active() {
match tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
// Close context menu
{
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = None;
}
// Run action's message
return self.update(action.message(entity));
}
// Run action's message
return self.update(action.message(entity));
_ => {}
}
_ => {}
}
}
Message::TabContextMenu(entity, position_opt) => {
match self.tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
// Update context menu position
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = position_opt;
if let Some(tab_model) = self.pane_model.active() {
match tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
// Update context menu position
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = position_opt;
}
_ => {}
}
_ => {}
}
}
Message::TabNew => match &self.term_event_tx_opt {
Some(term_event_tx) => match self.themes.get(self.config.syntax_theme()) {
Some(colors) => {
let entity = self
.tab_model
.insert()
.text("New Terminal")
.closable()
.activate()
.id();
// Use the startup options, or defaults
let options = self.startup_options.take().unwrap_or_default();
let mut terminal = Terminal::new(
entity,
term_event_tx.clone(),
self.term_config.clone(),
options,
&self.config,
colors.clone(),
);
terminal.set_config(&self.config, &self.themes, self.zoom_adj);
self.tab_model
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
let current_pane = self.pane_model.focus;
if let Some(tab_model) = self.pane_model.active_mut() {
let entity = tab_model
.insert()
.text("New Terminal")
.closable()
.activate()
.id();
// Use the startup options, or defaults
let options = self.startup_options.take().unwrap_or_default();
let mut terminal = Terminal::new(
current_pane,
entity,
term_event_tx.clone(),
self.term_config.clone(),
options,
&self.config,
*colors,
);
terminal.set_config(&self.config, &self.themes, self.zoom_adj);
tab_model.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
} else {
log::error!("Found no active pane");
}
}
None => {
log::error!(
@ -1030,52 +1178,66 @@ impl Application for App {
log::warn!("tried to create new tab before having event channel");
}
},
Message::TermEvent(entity, event) => match event {
TermEvent::Bell => {
//TODO: audible or visible bell options?
}
TermEvent::ColorRequest(index, f) => {
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let rgb = terminal.colors()[index].unwrap_or_default();
let text = f(rgb);
terminal.input_no_scroll(text.into_bytes());
Message::TermEvent(pane, entity, event) => {
match event {
TermEvent::Bell => {
//TODO: audible or visible bell options?
}
TermEvent::ColorRequest(index, f) => {
if let Some(tab_model) = self.pane_model.panes.get(pane) {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let rgb = terminal.colors()[index].unwrap_or_default();
let text = f(rgb);
terminal.input_no_scroll(text.into_bytes());
}
}
}
TermEvent::Exit => {
return self.update(Message::TabClose(Some(entity)));
}
TermEvent::PtyWrite(text) => {
if let Some(tab_model) = self.pane_model.panes.get(pane) {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
terminal.input_no_scroll(text.into_bytes());
}
}
}
TermEvent::ResetTitle => {
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
tab_model.text_set(entity, "New Terminal");
}
return self.update_title(Some(pane));
}
TermEvent::TextAreaSizeRequest(f) => {
if let Some(tab_model) = self.pane_model.panes.get(pane) {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let text = f(terminal.size().into());
terminal.input_no_scroll(text.into_bytes());
}
}
}
TermEvent::Title(title) => {
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
tab_model.text_set(entity, title);
}
return self.update_title(Some(pane));
}
TermEvent::MouseCursorDirty | TermEvent::Wakeup => {
if let Some(tab_model) = self.pane_model.panes.get(pane) {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.needs_update = true;
}
}
}
_ => {
log::warn!("TODO: {:?}", event);
}
}
TermEvent::Exit => {
return self.update(Message::TabClose(Some(entity)));
}
TermEvent::PtyWrite(text) => {
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
terminal.input_no_scroll(text.into_bytes());
}
}
TermEvent::ResetTitle => {
self.tab_model.text_set(entity, "New Terminal");
return self.update_title();
}
TermEvent::TextAreaSizeRequest(f) => {
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let terminal = terminal.lock().unwrap();
let text = f(terminal.size().into());
terminal.input_no_scroll(text.into_bytes());
}
}
TermEvent::Title(title) => {
self.tab_model.text_set(entity, title);
return self.update_title();
}
TermEvent::MouseCursorDirty | TermEvent::Wakeup => {
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.needs_update = true;
}
}
_ => {
log::warn!("TODO: {:?}", event);
}
},
}
Message::TermEventTx(term_event_tx) => {
self.term_event_tx_opt = Some(term_event_tx);
}
@ -1139,114 +1301,128 @@ impl Application for App {
/// Creates a view after each update.
fn view(&self) -> Element<Self::Message> {
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
let pane_grid = PaneGrid::new(&self.pane_model.panes, |pane, tab_model, _is_maximized| {
let mut tab_column = widget::column::with_capacity(1);
let mut tab_column = widget::column::with_capacity(1);
if self.tab_model.iter().count() > 1 {
tab_column = tab_column.push(
widget::container(
widget::view_switcher::horizontal(&self.tab_model)
.button_height(32)
.button_spacing(space_xxs)
.on_activate(Message::TabActivate)
.on_close(|entity| Message::TabClose(Some(entity))),
)
.style(style::Container::Background)
.width(Length::Fill),
);
}
let entity = self.tab_model.active();
match self.tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
let terminal_box = terminal_box(terminal)
.id(self.terminal_id.clone())
.on_context_menu(move |position_opt| {
Message::TabContextMenu(entity, position_opt)
});
let context_menu = {
let terminal = terminal.lock().unwrap();
terminal.context_menu
};
let tab_element: Element<'_, Message> = match context_menu {
Some(position) => widget::popover(
terminal_box.context_menu(position),
menu::context_menu(&self.config, entity),
if tab_model.iter().count() > 1 {
tab_column = tab_column.push(
widget::container(
widget::view_switcher::horizontal(tab_model)
.button_height(32)
.button_spacing(space_xxs)
.on_activate(Message::TabActivate)
.on_close(|entity| Message::TabClose(Some(entity))),
)
.position(position)
.into(),
None => terminal_box.into(),
};
tab_column = tab_column.push(tab_element);
.style(style::Container::Background)
.width(Length::Fill),
);
}
None => {
//TODO
}
}
if self.find {
let find_input =
widget::text_input::text_input(fl!("find-placeholder"), &self.find_search_value)
.id(self.find_search_id.clone())
.on_input(Message::FindSearchValueChanged)
// This is inverted for ease of use, usually in terminals you want to search
// upwards, which is FindPrevious
.on_submit(if self.modifiers.contains(Modifiers::SHIFT) {
Message::FindNext
} else {
Message::FindPrevious
})
.width(Length::Fixed(320.0))
.trailing_icon(
button(icon_cache_get("edit-clear-symbolic", 16))
.on_press(Message::FindSearchValueChanged(String::new()))
.style(style::Button::Icon)
.into(),
let entity = tab_model.active();
let terminal_id = self
.terminal_ids
.get(&pane)
.cloned()
.unwrap_or_else(widget::Id::unique);
match tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => {
let terminal_box = terminal_box(terminal).id(terminal_id).on_context_menu(
move |position_opt| Message::TabContextMenu(entity, position_opt),
);
let find_widget = widget::row::with_children(vec![
find_input.into(),
widget::tooltip(
button(icon_cache_get("go-up-symbolic", 16))
.on_press(Message::FindPrevious)
.padding(space_xxs)
.style(style::Button::Icon),
fl!("find-previous"),
widget::tooltip::Position::Top,
let context_menu = {
let terminal = terminal.lock().unwrap();
terminal.context_menu
};
let tab_element: Element<'_, Message> = match context_menu {
Some(position) => widget::popover(
terminal_box.context_menu(position),
menu::context_menu(&self.config, entity),
)
.position(position)
.into(),
None => terminal_box.into(),
};
tab_column = tab_column.push(tab_element);
}
None => {
//TODO
}
}
if self.find {
let find_input = widget::text_input::text_input(
fl!("find-placeholder"),
&self.find_search_value,
)
.into(),
widget::tooltip(
button(icon_cache_get("go-down-symbolic", 16))
.on_press(Message::FindNext)
.padding(space_xxs)
.style(style::Button::Icon),
fl!("find-next"),
widget::tooltip::Position::Top,
)
.into(),
widget::horizontal_space(Length::Fill).into(),
button(icon_cache_get("window-close-symbolic", 16))
.on_press(Message::Find(false))
.padding(space_xxs)
.style(style::Button::Icon)
.id(self.find_search_id.clone())
.on_input(Message::FindSearchValueChanged)
// This is inverted for ease of use, usually in terminals you want to search
// upwards, which is FindPrevious
.on_submit(if self.modifiers.contains(Modifiers::SHIFT) {
Message::FindNext
} else {
Message::FindPrevious
})
.width(Length::Fixed(320.0))
.trailing_icon(
button(icon_cache_get("edit-clear-symbolic", 16))
.on_press(Message::FindSearchValueChanged(String::new()))
.style(style::Button::Icon)
.into(),
);
let find_widget = widget::row::with_children(vec![
find_input.into(),
widget::tooltip(
button(icon_cache_get("go-up-symbolic", 16))
.on_press(Message::FindPrevious)
.padding(space_xxs)
.style(style::Button::Icon),
fl!("find-previous"),
widget::tooltip::Position::Top,
)
.into(),
])
.align_items(Alignment::Center)
widget::tooltip(
button(icon_cache_get("go-down-symbolic", 16))
.on_press(Message::FindNext)
.padding(space_xxs)
.style(style::Button::Icon),
fl!("find-next"),
widget::tooltip::Position::Top,
)
.into(),
widget::horizontal_space(Length::Fill).into(),
button(icon_cache_get("window-close-symbolic", 16))
.on_press(Message::Find(false))
.padding(space_xxs)
.style(style::Button::Icon)
.into(),
])
.align_items(Alignment::Center)
.padding(space_xxs)
.spacing(space_xxs);
tab_column = tab_column.push(
widget::cosmic_container::container(find_widget)
.layer(cosmic_theme::Layer::Primary),
);
}
pane_grid::Content::new(tab_column)
})
.width(Length::Fill)
.height(Length::Fill)
.spacing(space_xxs)
.on_click(Message::PaneClicked)
.on_resize(space_xxs, Message::PaneResized)
.on_drag(Message::PaneDragged);
container(pane_grid)
.width(Length::Fill)
.height(Length::Fill)
.padding(space_xxs)
.spacing(space_xxs);
tab_column = tab_column.push(
widget::cosmic_container::container(find_widget)
.layer(cosmic_theme::Layer::Primary),
);
}
let content: Element<_> = tab_column.into();
// Uncomment to debug layout:
//content.explain(cosmic::iced::Color::WHITE)
content
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
@ -1296,6 +1472,76 @@ impl Application for App {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::R,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::ALT {
Some(Message::PaneSplit(pane_grid::Axis::Vertical))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::D,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::ALT {
Some(Message::PaneSplit(pane_grid::Axis::Horizontal))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::X,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
Some(Message::PaneToggleMaximized)
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::Left,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
Some(Message::PaneFocusAdjacent(pane_grid::Direction::Left))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::Right,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
Some(Message::PaneFocusAdjacent(pane_grid::Direction::Right))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::Up,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
Some(Message::PaneFocusAdjacent(pane_grid::Direction::Up))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::Down,
modifiers,
}) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
Some(Message::PaneFocusAdjacent(pane_grid::Direction::Down))
} else {
None
}
}
Event::Keyboard(KeyEvent::KeyPressed {
key_code: KeyCode::V,
modifiers,
@ -1354,9 +1600,9 @@ impl Application for App {
// Create first terminal tab
output.send(Message::TabNew).await.unwrap();
while let Some((entity, event)) = event_rx.recv().await {
while let Some((pane, entity, event)) = event_rx.recv().await {
output
.send(Message::TermEvent(entity, event))
.send(Message::TermEvent(pane, entity, event))
.await
.unwrap();
}

View file

@ -54,6 +54,10 @@ pub fn context_menu<'a>(config: &Config, entity: segmented_button::Entity) -> El
menu_action(fl!("paste"), Action::Paste),
menu_action(fl!("select-all"), Action::SelectAll),
horizontal_rule(1),
menu_action(fl!("split-horizontal"), Action::PaneSplitHorizontal),
menu_action(fl!("split-vertical"), Action::PaneSplitVertical),
menu_action(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
horizontal_rule(1),
menu_action(fl!("new-tab"), Action::TabNew),
menu_action(fl!("settings"), Action::Settings),
menu_checkbox(

View file

@ -15,7 +15,10 @@ use alacritty_terminal::{
vte::ansi::{Color, NamedColor, Rgb},
Term,
};
use cosmic::{iced::advanced::graphics::text::font_system, widget::segmented_button};
use cosmic::{
iced::advanced::graphics::text::font_system,
widget::{pane_grid, segmented_button},
};
use cosmic_text::{
Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Wrap,
};
@ -67,14 +70,15 @@ impl From<Size> for WindowSize {
#[derive(Clone)]
pub struct EventProxy(
pane_grid::Pane,
segmented_button::Entity,
mpsc::Sender<(segmented_button::Entity, Event)>,
mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, Event)>,
);
impl EventListener for EventProxy {
fn send_event(&self, event: Event) {
//TODO: handle error
let _ = self.1.blocking_send((self.0, event));
let _ = self.2.blocking_send((self.0, self.1, event));
}
}
@ -132,6 +136,33 @@ fn linear_color(color: cosmic_text::Color) -> cosmic_text::Color {
)
}
type TabModel = segmented_button::Model<segmented_button::SingleSelect>;
pub struct TerminalPaneGrid {
pub panes: pane_grid::State<TabModel>,
pub panes_created: usize,
pub focus: pane_grid::Pane,
}
impl TerminalPaneGrid {
pub fn new(model: TabModel) -> Self {
let (panes, pane) = pane_grid::State::new(model);
let mut terminal_ids = HashMap::new();
terminal_ids.insert(pane, cosmic::widget::Id::unique());
Self {
panes,
panes_created: 1,
focus: pane,
}
}
pub fn active(&self) -> Option<&TabModel> {
self.panes.get(self.focus)
}
pub fn active_mut(&mut self) -> Option<&mut TabModel> {
self.panes.get_mut(self.focus)
}
}
pub struct Terminal {
default_attrs: Attrs<'static>,
buffer: Arc<Buffer>,
@ -151,8 +182,9 @@ pub struct Terminal {
impl Terminal {
//TODO: error handling
pub fn new(
pane: pane_grid::Pane,
entity: segmented_button::Entity,
event_tx: mpsc::Sender<(segmented_button::Entity, Event)>,
event_tx: mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, Event)>,
config: Config,
options: Options,
app_config: &AppConfig,
@ -191,7 +223,7 @@ impl Terminal {
cell_width,
cell_height,
};
let event_proxy = EventProxy(entity, event_tx);
let event_proxy = EventProxy(pane, entity, event_tx);
let term = Arc::new(FairMutex::new(Term::new(
config,
&size,

View file

@ -251,6 +251,17 @@ where
// Render default background
{
let background_color = cosmic_text::Color(terminal.default_attrs().metadata as u32);
//TODO: get shaded background color from theme
let mut shade: f32 = 1.0;
if !state.is_focused {
shade = 0.90;
}
let background = Color::new(
background_color.r() as f32 * shade / 255.0,
background_color.g() as f32 * shade / 255.0,
background_color.b() as f32 * shade / 255.0,
background_color.a() as f32 * shade / 255.0,
);
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
@ -261,12 +272,7 @@ where
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Color::new(
background_color.r() as f32 / 255.0,
background_color.g() as f32 / 255.0,
background_color.b() as f32 / 255.0,
background_color.a() as f32 / 255.0,
),
background,
);
}
@ -711,6 +717,21 @@ where
(true, _, _, _) => {
// Ignore super
}
(false, true, true, false) => {
// Handle ctrl-alt for non-control characters
// Or should I try to minimize this to only
// catch control sequences that conflicts with
// keykodes for Split
// if character != '\u{4}' && character != '\u{12}' {
// is there any valid case for control characters with modifers
// ctrl-alt?
if !character.is_control() {
let mut buf = [0, 0, 0, 0];
let str = character.encode_utf8(&mut buf);
terminal.input_scroll(str.as_bytes().to_vec());
status = Status::Captured;
}
}
(false, true, _, false) => {
// Handle ctrl for control characters (Ctrl-A to Ctrl-Z)
if character.is_control() {