Add support for setting theme and default font

This commit is contained in:
Jeremy Soller 2023-11-03 11:02:25 -06:00
parent ee2dea71e6
commit 6c0e104314
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
7 changed files with 177 additions and 55 deletions

10
Cargo.lock generated
View file

@ -851,7 +851,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-text" name = "cosmic-text"
version = "0.10.0" version = "0.10.0"
source = "git+https://github.com/pop-os/cosmic-text?branch=vi-editor#e62fea5efddb20fd1bc518e0d733a86f6858fa73" source = "git+https://github.com/pop-os/cosmic-text?branch=vi-editor#f8da72f7a3cbbb4d0807eb089ff10c602b38baed"
dependencies = [ dependencies = [
"fontdb 0.15.0", "fontdb 0.15.0",
"libm", "libm",
@ -5544,18 +5544,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.23" version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107" checksum = "092cd76b01a033a9965b9097da258689d9e17c69ded5dcf41bca001dd20ebc6d"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.23" version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d" checksum = "a13a20a7c6a90e2034bcc65495799da92efcec6a8dd4f3fcb6f7a48988637ead"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -19,7 +19,7 @@ rust-embed = "6.3.0"
[dependencies.cosmic-text] [dependencies.cosmic-text]
git = "https://github.com/pop-os/cosmic-text" git = "https://github.com/pop-os/cosmic-text"
branch = "vi-editor" branch = "vi-editor"
features = ["syntect", "two-face", "vi"] features = ["two-face", "vi"]
#path = "../cosmic-text" #path = "../cosmic-text"
[dependencies.libcosmic] [dependencies.libcosmic]

View file

@ -13,6 +13,12 @@ line-count = Lines
## Settings ## Settings
settings = Settings settings = Settings
## Appearance
appearance = Appearance
theme = Theme
default-font = Default font
default-font-size = Default font size
### Keyboard shortcuts ### Keyboard shortcuts
keyboard-shortcuts = Keyboard shortcuts keyboard-shortcuts = Keyboard shortcuts
enable-vim-bindings = Enable Vim bindings enable-vim-bindings = Enable Vim bindings

View file

@ -3,6 +3,10 @@ use std::{collections::HashMap, fmt};
use crate::{ContextPage, Message}; use crate::{ContextPage, Message};
const DEFAULT_FONT_SIZE: f32 = 14.0;
const DEFAULT_SYNTAX_THEME_DARK: &'static str = "base16-eighties.dark";
const DEFAULT_SYNTAX_THEME_LIGHT: &'static str = "base16-ocean.light";
// Makes key binding definitions simpler // Makes key binding definitions simpler
const CTRL: Modifiers = Modifiers::CTRL; const CTRL: Modifiers = Modifiers::CTRL;
const ALT: Modifiers = Modifiers::ALT; const ALT: Modifiers = Modifiers::ALT;
@ -70,6 +74,9 @@ impl fmt::Display for KeyBind {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Config { pub struct Config {
pub font_size: f32,
pub syntax_theme_dark: String,
pub syntax_theme_light: String,
pub vim_bindings: bool, pub vim_bindings: bool,
pub word_wrap: bool, pub word_wrap: bool,
pub keybinds: HashMap<KeyBind, Message>, pub keybinds: HashMap<KeyBind, Message>,
@ -79,9 +86,26 @@ impl Config {
//TODO: load from cosmic-config //TODO: load from cosmic-config
pub fn load() -> Self { pub fn load() -> Self {
Self { Self {
font_size: DEFAULT_FONT_SIZE,
syntax_theme_dark: DEFAULT_SYNTAX_THEME_DARK.to_string(),
syntax_theme_light: DEFAULT_SYNTAX_THEME_LIGHT.to_string(),
vim_bindings: false, vim_bindings: false,
word_wrap: false, word_wrap: false,
keybinds: KeyBind::load(), keybinds: KeyBind::load(),
} }
} }
// Calculate line height from font size
pub fn line_height(&self) -> f32 {
(self.font_size * 1.4).ceil()
}
// Get current syntax theme based on dark mode
pub fn syntax_theme(&self, dark: bool) -> &str {
if dark {
&self.syntax_theme_dark
} else {
&self.syntax_theme_light
}
}
} }

View file

@ -6,13 +6,13 @@ use cosmic::{
iced::{ iced::{
clipboard, event, keyboard, subscription, clipboard, event, keyboard, subscription,
widget::{row, text}, widget::{row, text},
window, Alignment, Length, Limits, window, Alignment, Length,
}, },
style, style,
widget::{self, button, icon, nav_bar, segmented_button, view_switcher}, widget::{self, button, icon, nav_bar, segmented_button, view_switcher},
ApplicationExt, Element, ApplicationExt, Element,
}; };
use cosmic_text::{Edit, FontSystem, SwashCache, SyntaxSystem, ViMode}; use cosmic_text::{Edit, Family, FontSystem, SwashCache, SyntaxSystem, ViMode};
use std::{ use std::{
env, fs, env, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -65,6 +65,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
pub enum Message { pub enum Message {
Cut, Cut,
Copy, Copy,
DefaultFont(usize),
KeyBind(KeyBind), KeyBind(KeyBind),
NewFile, NewFile,
NewWindow, NewWindow,
@ -76,6 +77,7 @@ pub enum Message {
PasteValue(String), PasteValue(String),
Quit, Quit,
Save, Save,
SyntaxTheme(usize, bool),
TabActivate(segmented_button::Entity), TabActivate(segmented_button::Entity),
TabClose(segmented_button::Entity), TabClose(segmented_button::Entity),
Todo, Todo,
@ -104,6 +106,8 @@ pub struct App {
nav_model: segmented_button::SingleSelectModel, nav_model: segmented_button::SingleSelectModel,
tab_model: segmented_button::SingleSelectModel, tab_model: segmented_button::SingleSelectModel,
config: Config, config: Config,
font_names: Vec<String>,
theme_names: Vec<String>,
context_page: ContextPage, context_page: ContextPage,
} }
@ -208,8 +212,7 @@ impl App {
} }
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) { pub fn open_tab(&mut self, path_opt: Option<PathBuf>) {
let mut tab = Tab::new(); let mut tab = Tab::new(&self.config);
tab.set_config(&self.config);
if let Some(path) = path_opt { if let Some(path) = path_opt {
tab.open(path); tab.open(path);
} }
@ -324,11 +327,37 @@ impl cosmic::Application for App {
/// Creates the application, and optionally emits command on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, _flags: Self::Flags) -> (Self, Command<Self::Message>) { fn init(core: Core, _flags: Self::Flags) -> (Self, Command<Self::Message>) {
let font_names = {
let mut font_names = Vec::new();
let font_system = FONT_SYSTEM.lock().unwrap();
//TODO: do not repeat, used in Tab::new
let attrs = cosmic_text::Attrs::new().family(Family::Monospace);
for face in font_system.db().faces() {
if attrs.matches(face) && face.monospaced {
//TODO: get localized name if possible
let font_name = face
.families
.get(0)
.map_or_else(|| face.post_script_name.to_string(), |x| x.0.to_string());
font_names.push(font_name);
}
}
font_names.sort();
font_names
};
let mut theme_names = Vec::with_capacity(SYNTAX_SYSTEM.theme_set.themes.len());
for (theme_name, _theme) in SYNTAX_SYSTEM.theme_set.themes.iter() {
theme_names.push(theme_name.to_string());
}
let mut app = App { let mut app = App {
core, core,
nav_model: nav_bar::Model::builder().build(), nav_model: nav_bar::Model::builder().build(),
tab_model: segmented_button::Model::builder().build(), tab_model: segmented_button::Model::builder().build(),
config: Config::load(), config: Config::load(),
font_names,
theme_names,
context_page: ContextPage::Settings, context_page: ContextPage::Settings,
}; };
@ -444,6 +473,27 @@ impl cosmic::Application for App {
} }
None => {} None => {}
}, },
Message::DefaultFont(index) => {
match self.font_names.get(index) {
Some(font_name) => {
let mut font_system = FONT_SYSTEM.lock().unwrap();
font_system.db_mut().set_monospace_family(font_name);
// This does a complete reset of shaping data!
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
let mut editor = tab.editor.lock().unwrap();
for line in editor.buffer_mut().lines.iter_mut() {
line.reset();
}
}
}
}
None => {
log::warn!("failed to find font with index {}", index);
}
}
}
Message::KeyBind(key_bind) => { Message::KeyBind(key_bind) => {
for (config_key_bind, config_message) in self.config.keybinds.iter() { for (config_key_bind, config_message) in self.config.keybinds.iter() {
if config_key_bind == &key_bind { if config_key_bind == &key_bind {
@ -459,7 +509,7 @@ impl cosmic::Application for App {
//TODO: support multi-window in winit //TODO: support multi-window in winit
match env::current_exe() { match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() { Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(child) => {} Ok(_child) => {}
Err(err) => { Err(err) => {
log::error!("failed to execute {:?}: {}", exe, err); log::error!("failed to execute {:?}: {}", exe, err);
} }
@ -506,15 +556,13 @@ impl cosmic::Application for App {
None => message::none(), None => message::none(),
}); });
} }
Message::PasteValue(value) => { Message::PasteValue(value) => match self.active_tab() {
match self.active_tab() { Some(tab) => {
Some(tab) => { let mut editor = tab.editor.lock().unwrap();
let mut editor = tab.editor.lock().unwrap(); editor.insert_string(&value, None);
editor.insert_string(&value, None);
}
None => {}
} }
} None => {}
},
Message::Quit => { Message::Quit => {
//TODO: prompt for save? //TODO: prompt for save?
return window::close(); return window::close();
@ -540,6 +588,19 @@ impl cosmic::Application for App {
self.tab_model.text_set(self.tab_model.active(), title); self.tab_model.text_set(self.tab_model.active(), title);
} }
} }
Message::SyntaxTheme(index, dark) => match self.theme_names.get(index) {
Some(theme_name) => {
if dark {
self.config.syntax_theme_dark = theme_name.to_string();
} else {
self.config.syntax_theme_light = theme_name.to_string();
}
self.update_config();
}
None => {
log::warn!("failed to find syntax theme with index {}", index);
}
},
Message::TabActivate(entity) => { Message::TabActivate(entity) => {
self.tab_model.activate(entity); self.tab_model.activate(entity);
return self.update_tab(); return self.update_tab();
@ -643,14 +704,50 @@ impl cosmic::Application for App {
.into() .into()
} }
ContextPage::Settings => { ContextPage::Settings => {
widget::settings::view_column(vec![widget::settings::view_section(fl!( let dark = cosmic::theme::is_dark();
"keyboard-shortcuts" let current_theme_name = if dark {
)) &self.config.syntax_theme_dark
.add( } else {
widget::settings::item::builder(fl!("enable-vim-bindings")) &self.config.syntax_theme_light
.toggler(self.config.vim_bindings, Message::VimBindings), };
) let theme_selected = self
.into()]) .theme_names
.iter()
.position(|theme_name| theme_name == current_theme_name);
let font_selected = {
let font_system = FONT_SYSTEM.lock().unwrap();
let current_font_name = font_system.db().family_name(&Family::Monospace);
self.font_names
.iter()
.position(|font_name| font_name == current_font_name)
};
widget::settings::view_column(vec![
widget::settings::view_section(fl!("appearance"))
.add(widget::settings::item::builder(fl!("theme")).control(
widget::dropdown(&self.theme_names, theme_selected, move |index| {
Message::SyntaxTheme(index, dark)
}),
))
.add(
widget::settings::item::builder(fl!("default-font")).control(
widget::dropdown(&self.font_names, font_selected, |index| {
Message::DefaultFont(index)
}),
),
)
.add(
widget::settings::item::builder(fl!("default-font-size")).control(
widget::dropdown(&["TODO"], Some(0), |_index| Message::Todo),
),
)
.into(),
widget::settings::view_section(fl!("keyboard-shortcuts"))
.add(
widget::settings::item::builder(fl!("enable-vim-bindings"))
.toggler(self.config.vim_bindings, Message::VimBindings),
)
.into(),
])
.into() .into()
} }
}) })
@ -718,7 +815,7 @@ impl cosmic::Application for App {
} }
fn subscription(&self) -> subscription::Subscription<Message> { fn subscription(&self) -> subscription::Subscription<Message> {
subscription::events_with(|event, status| match event { subscription::events_with(|event, _status| match event {
event::Event::Keyboard(keyboard::Event::KeyPressed { event::Event::Keyboard(keyboard::Event::KeyPressed {
modifiers, modifiers,
key_code, key_code,

View file

@ -3,16 +3,7 @@
use cosmic_text::{Attrs, Buffer, Edit, Metrics, SyntaxEditor, ViEditor, Wrap}; use cosmic_text::{Attrs, Buffer, Edit, Metrics, SyntaxEditor, ViEditor, Wrap};
use std::{fs, path::PathBuf, sync::Mutex}; use std::{fs, path::PathBuf, sync::Mutex};
use crate::{fl, text_box, Config, FONT_SYSTEM, SYNTAX_SYSTEM}; use crate::{fl, Config, FONT_SYSTEM, SYNTAX_SYSTEM};
static FONT_SIZES: &'static [Metrics] = &[
Metrics::new(10.0, 14.0), // Caption
Metrics::new(14.0, 20.0), // Body
Metrics::new(20.0, 28.0), // Title 4
Metrics::new(24.0, 32.0), // Title 3
Metrics::new(28.0, 36.0), // Title 2
Metrics::new(32.0, 44.0), // Title 1
];
pub struct Tab { pub struct Tab {
pub path_opt: Option<PathBuf>, pub path_opt: Option<PathBuf>,
@ -21,24 +12,30 @@ pub struct Tab {
} }
impl Tab { impl Tab {
pub fn new() -> Self { pub fn new(config: &Config) -> Self {
//TODO: do not repeat, used in App::init
let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace); let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace);
let editor = SyntaxEditor::new( let editor = SyntaxEditor::new(
Buffer::new(&mut FONT_SYSTEM.lock().unwrap(), FONT_SIZES[1 /* Body */]), Buffer::new(
&mut FONT_SYSTEM.lock().unwrap(),
Metrics::new(config.font_size, config.line_height()),
),
&SYNTAX_SYSTEM, &SYNTAX_SYSTEM,
text_box::Appearance::dark().syntax_theme, config.syntax_theme(cosmic::theme::is_dark()),
) )
.unwrap(); .unwrap();
let mut editor = ViEditor::new(editor); let mut tab = Self {
editor.set_passthrough(true);
Self {
path_opt: None, path_opt: None,
attrs, attrs,
editor: Mutex::new(editor), editor: Mutex::new(ViEditor::new(editor)),
} };
// Update any other config settings
tab.set_config(config);
tab
} }
pub fn set_config(&mut self, config: &Config) { pub fn set_config(&mut self, config: &Config) {
@ -51,6 +48,12 @@ impl Tab {
} else { } else {
Wrap::None Wrap::None
}); });
//TODO: dynamically discover light/dark changes
editor.update_theme(if cosmic::theme::is_dark() {
&config.syntax_theme_dark
} else {
&config.syntax_theme_light
});
} }
pub fn open(&mut self, path: PathBuf) { pub fn open(&mut self, path: PathBuf) {

View file

@ -25,7 +25,6 @@ use crate::{FONT_SYSTEM, SWASH_CACHE};
pub struct Appearance { pub struct Appearance {
pub background_color: Option<Color>, pub background_color: Option<Color>,
pub text_color: Color, pub text_color: Color,
pub syntax_theme: &'static str,
} }
impl Appearance { impl Appearance {
@ -33,7 +32,6 @@ impl Appearance {
Self { Self {
background_color: Some(Color::from_rgb8(0x34, 0x34, 0x34)), background_color: Some(Color::from_rgb8(0x34, 0x34, 0x34)),
text_color: Color::from_rgb8(0xFF, 0xFF, 0xFF), text_color: Color::from_rgb8(0xFF, 0xFF, 0xFF),
syntax_theme: "base16-eighties.dark",
} }
} }
@ -41,7 +39,6 @@ impl Appearance {
Self { Self {
background_color: Some(Color::from_rgb8(0xFC, 0xFC, 0xFC)), background_color: Some(Color::from_rgb8(0xFC, 0xFC, 0xFC)),
text_color: Color::from_rgb8(0x00, 0x00, 0x00), text_color: Color::from_rgb8(0x00, 0x00, 0x00),
syntax_theme: "base16-ocean.light",
} }
} }
} }
@ -63,7 +60,6 @@ impl StyleSheet for Theme {
pub struct TextBox<'a> { pub struct TextBox<'a> {
editor: &'a Mutex<ViEditor<'static>>, editor: &'a Mutex<ViEditor<'static>>,
padding: Padding, padding: Padding,
line_numbers: bool,
} }
impl<'a> TextBox<'a> { impl<'a> TextBox<'a> {
@ -71,7 +67,6 @@ impl<'a> TextBox<'a> {
Self { Self {
editor, editor,
padding: Padding::new(0.0), padding: Padding::new(0.0),
line_numbers: true,
} }
} }
@ -267,9 +262,6 @@ where
let mut font_system = FONT_SYSTEM.lock().unwrap(); let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system); let mut editor = editor.borrow_with(&mut font_system);
// Set theme
editor.update_theme(appearance.syntax_theme);
// Set metrics and size // Set metrics and size
editor.buffer_mut().set_metrics_and_size( editor.buffer_mut().set_metrics_and_size(
//TODO: get from config //TODO: get from config