Add GUI font configuration controls
This commit is contained in:
@@ -3,6 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use crate::buffers::{HexBuffer, PlotBuffer, TextBuffer};
|
use crate::buffers::{HexBuffer, PlotBuffer, TextBuffer};
|
||||||
use crate::panels::{config, console, hex_view, plot_view, sidebar};
|
use crate::panels::{config, console, hex_view, plot_view, sidebar};
|
||||||
|
use crate::ui_fonts::{self, FontCandidate, FontChoice, UiFontSettings};
|
||||||
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
use egui::{Color32, Layout, Panel, Pos2, Rect, TextEdit, UiBuilder};
|
||||||
use xserial_client::SessionManager;
|
use xserial_client::SessionManager;
|
||||||
use xserial_client::config::SessionConfig;
|
use xserial_client::config::SessionConfig;
|
||||||
@@ -69,6 +70,11 @@ pub struct XserialApp {
|
|||||||
tabs: Vec<SessionTab>,
|
tabs: Vec<SessionTab>,
|
||||||
active: usize,
|
active: usize,
|
||||||
display: DisplayOptions,
|
display: DisplayOptions,
|
||||||
|
font_settings_open: bool,
|
||||||
|
font_candidates: Vec<FontCandidate>,
|
||||||
|
font_settings: UiFontSettings,
|
||||||
|
primary_font_search: String,
|
||||||
|
fallback_font_search: String,
|
||||||
config_open: bool,
|
config_open: bool,
|
||||||
config_target: Option<u64>,
|
config_target: Option<u64>,
|
||||||
config_form: config::ConfigForm,
|
config_form: config::ConfigForm,
|
||||||
@@ -83,6 +89,7 @@ impl XserialApp {
|
|||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (event_tx, event_rx) = mpsc::channel();
|
let (event_tx, event_rx) = mpsc::channel();
|
||||||
|
let repaint_ctx = ctx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
@@ -90,7 +97,7 @@ impl XserialApp {
|
|||||||
if event_tx.send(event).is_err() {
|
if event_tx.send(event).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ctx.request_repaint();
|
repaint_ctx.request_repaint();
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||||
@@ -98,6 +105,10 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let font_candidates = ui_fonts::discover_font_candidates();
|
||||||
|
let font_settings = UiFontSettings::default();
|
||||||
|
ui_fonts::apply_font_settings(&ctx, &font_settings, &font_candidates);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
manager,
|
manager,
|
||||||
tabs: Vec::new(),
|
tabs: Vec::new(),
|
||||||
@@ -107,6 +118,11 @@ impl XserialApp {
|
|||||||
show_direction: true,
|
show_direction: true,
|
||||||
show_pipeline: true,
|
show_pipeline: true,
|
||||||
},
|
},
|
||||||
|
font_settings_open: false,
|
||||||
|
font_candidates,
|
||||||
|
font_settings,
|
||||||
|
primary_font_search: String::new(),
|
||||||
|
fallback_font_search: String::new(),
|
||||||
config_open: false,
|
config_open: false,
|
||||||
config_target: None,
|
config_target: None,
|
||||||
config_form: config::ConfigForm::default(),
|
config_form: config::ConfigForm::default(),
|
||||||
@@ -371,6 +387,178 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||||
|
Panel::top("top_bar").show_inside(ui, |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.heading("xserial");
|
||||||
|
ui.separator();
|
||||||
|
ui.label(format!(
|
||||||
|
"Fonts: {} + {} {:.1} pt",
|
||||||
|
ui_fonts::font_choice_label(
|
||||||
|
&self.font_settings.primary_choice,
|
||||||
|
&self.font_candidates
|
||||||
|
),
|
||||||
|
ui_fonts::font_choice_label(
|
||||||
|
&self.font_settings.fallback_choice,
|
||||||
|
&self.font_candidates
|
||||||
|
),
|
||||||
|
self.font_settings.ui_font_size
|
||||||
|
));
|
||||||
|
if ui.button("UI Settings").clicked() {
|
||||||
|
self.font_settings_open = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_font_settings_window(&mut self, ctx: &egui::Context) {
|
||||||
|
if !self.font_settings_open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open = self.font_settings_open;
|
||||||
|
let mut changed = false;
|
||||||
|
egui::Window::new("UI Settings")
|
||||||
|
.open(&mut open)
|
||||||
|
.default_width(520.0)
|
||||||
|
.resizable(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading("Fonts");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Primary:");
|
||||||
|
ui.monospace(ui_fonts::font_choice_label(
|
||||||
|
&self.font_settings.primary_choice,
|
||||||
|
&self.font_candidates,
|
||||||
|
));
|
||||||
|
ui.label("Fallback:");
|
||||||
|
ui.monospace(ui_fonts::font_choice_label(
|
||||||
|
&self.font_settings.fallback_choice,
|
||||||
|
&self.font_candidates,
|
||||||
|
));
|
||||||
|
if ui.button("Refresh").clicked() {
|
||||||
|
self.font_candidates = ui_fonts::discover_font_candidates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.small("Primary font is tried first. Fallback font is used when the primary font lacks a glyph.");
|
||||||
|
ui.add_space(6.0);
|
||||||
|
render_font_selector(
|
||||||
|
ui,
|
||||||
|
"Primary font",
|
||||||
|
"primary_font_choice",
|
||||||
|
&mut self.font_settings.primary_choice,
|
||||||
|
&mut self.primary_font_search,
|
||||||
|
&self.font_candidates,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
180.0,
|
||||||
|
&mut changed,
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
render_font_selector(
|
||||||
|
ui,
|
||||||
|
"Fallback font",
|
||||||
|
"fallback_font_choice",
|
||||||
|
&mut self.font_settings.fallback_choice,
|
||||||
|
&mut self.fallback_font_search,
|
||||||
|
&self.font_candidates,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
140.0,
|
||||||
|
&mut changed,
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Sizes");
|
||||||
|
ui.label("UI font size");
|
||||||
|
changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::Slider::new(&mut self.font_settings.ui_font_size, 10.0..=28.0)
|
||||||
|
.suffix(" pt"),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
ui.label("Monospace font size");
|
||||||
|
changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::Slider::new(
|
||||||
|
&mut self.font_settings.monospace_font_size,
|
||||||
|
10.0..=28.0,
|
||||||
|
)
|
||||||
|
.suffix(" pt"),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
ui.label("Heading size");
|
||||||
|
changed |= ui
|
||||||
|
.add(
|
||||||
|
egui::Slider::new(&mut self.font_settings.heading_font_size, 14.0..=40.0)
|
||||||
|
.suffix(" pt"),
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Preview");
|
||||||
|
ui.label("The quick brown fox jumps over the lazy dog.");
|
||||||
|
ui.label("中文预览:串口、网络、绘图、十六进制、会话管理。");
|
||||||
|
ui.monospace("Monospace preview: 0123456789 ABCDEF deadbeef");
|
||||||
|
});
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
ui_fonts::apply_font_settings(ctx, &self.font_settings, &self.font_candidates);
|
||||||
|
}
|
||||||
|
self.font_settings_open = open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_font_selector(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
title: &str,
|
||||||
|
id_prefix: &str,
|
||||||
|
choice: &mut FontChoice,
|
||||||
|
search: &mut String,
|
||||||
|
candidates: &[FontCandidate],
|
||||||
|
allow_auto: bool,
|
||||||
|
allow_default: bool,
|
||||||
|
max_height: f32,
|
||||||
|
changed: &mut bool,
|
||||||
|
) {
|
||||||
|
ui.label(title);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Search:");
|
||||||
|
ui.text_edit_singleline(search);
|
||||||
|
});
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
if allow_auto {
|
||||||
|
*changed |= ui
|
||||||
|
.selectable_value(choice, FontChoice::Auto, "Auto")
|
||||||
|
.changed();
|
||||||
|
}
|
||||||
|
if allow_default {
|
||||||
|
*changed |= ui
|
||||||
|
.selectable_value(choice, FontChoice::Default, "Default")
|
||||||
|
.changed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_salt(format!("{id_prefix}_scroll"))
|
||||||
|
.max_height(max_height)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let needle = search.trim().to_lowercase();
|
||||||
|
for candidate in candidates.iter().filter(|candidate| {
|
||||||
|
needle.is_empty()
|
||||||
|
|| candidate.label.to_lowercase().contains(&needle)
|
||||||
|
|| candidate.family.to_lowercase().contains(&needle)
|
||||||
|
|| candidate.style.to_lowercase().contains(&needle)
|
||||||
|
|| candidate.path.to_lowercase().contains(&needle)
|
||||||
|
}) {
|
||||||
|
let label = if candidate.likely_cjk {
|
||||||
|
format!("{} [CJK]", candidate.label)
|
||||||
|
} else {
|
||||||
|
candidate.label.clone()
|
||||||
|
};
|
||||||
|
let response = ui.selectable_value(choice, FontChoice::System(candidate.id.clone()), label);
|
||||||
|
*changed |= response.changed();
|
||||||
|
response.on_hover_text(&candidate.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for XserialApp {
|
impl eframe::App for XserialApp {
|
||||||
@@ -381,6 +569,8 @@ impl eframe::App for XserialApp {
|
|||||||
if self.wants_live_plot_repaint() {
|
if self.wants_live_plot_repaint() {
|
||||||
ui.ctx().request_repaint_after(Duration::from_millis(16));
|
ui.ctx().request_repaint_after(Duration::from_millis(16));
|
||||||
}
|
}
|
||||||
|
self.render_top_bar(ui);
|
||||||
|
self.render_font_settings_window(ui.ctx());
|
||||||
self.render_config_window(ui.ctx());
|
self.render_config_window(ui.ctx());
|
||||||
self.render_sidebar(ui);
|
self.render_sidebar(ui);
|
||||||
self.render_main_panel(ui);
|
self.render_main_panel(ui);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod buffers;
|
mod buffers;
|
||||||
mod panels;
|
mod panels;
|
||||||
|
mod ui_fonts;
|
||||||
|
|
||||||
use xserial_client::SessionManager;
|
use xserial_client::SessionManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
||||||
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||||
use xserial_core::protocol::plot::PlotFormat;
|
use xserial_core::protocol::plot::PlotFormat;
|
||||||
|
use xserial_core::protocol::text::TextEncoding;
|
||||||
use xserial_core::transport::TransportConfig;
|
use xserial_core::transport::TransportConfig;
|
||||||
use xserial_core::transport::serial::{
|
use xserial_core::transport::serial::{
|
||||||
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
||||||
@@ -30,6 +31,7 @@ pub struct PipelineForm {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub framer: FramerChoice,
|
pub framer: FramerChoice,
|
||||||
pub decoder: DecoderChoice,
|
pub decoder: DecoderChoice,
|
||||||
|
pub text_encoding: TextEncoding,
|
||||||
pub plot_channels: usize,
|
pub plot_channels: usize,
|
||||||
pub plot_format: PlotFormat,
|
pub plot_format: PlotFormat,
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ impl Default for ConfigForm {
|
|||||||
name: "text".into(),
|
name: "text".into(),
|
||||||
framer: FramerChoice::Line,
|
framer: FramerChoice::Line,
|
||||||
decoder: DecoderChoice::Text,
|
decoder: DecoderChoice::Text,
|
||||||
|
text_encoding: TextEncoding::Utf8,
|
||||||
plot_channels: 1,
|
plot_channels: 1,
|
||||||
plot_format: PlotFormat::Interleaved,
|
plot_format: PlotFormat::Interleaved,
|
||||||
}],
|
}],
|
||||||
@@ -148,6 +151,11 @@ impl ConfigForm {
|
|||||||
DecoderConfig::Plot { channels, .. } => *channels,
|
DecoderConfig::Plot { channels, .. } => *channels,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
|
let text_encoding = match &pipeline.decoder {
|
||||||
|
DecoderConfig::Text { encoding } => *encoding,
|
||||||
|
DecoderConfig::MixedTextPlot { encoding } => *encoding,
|
||||||
|
_ => TextEncoding::Utf8,
|
||||||
|
};
|
||||||
let plot_format = match &pipeline.decoder {
|
let plot_format = match &pipeline.decoder {
|
||||||
DecoderConfig::Plot { format, .. } => *format,
|
DecoderConfig::Plot { format, .. } => *format,
|
||||||
_ => PlotFormat::Interleaved,
|
_ => PlotFormat::Interleaved,
|
||||||
@@ -156,6 +164,7 @@ impl ConfigForm {
|
|||||||
name: pipeline.name.clone(),
|
name: pipeline.name.clone(),
|
||||||
framer,
|
framer,
|
||||||
decoder,
|
decoder,
|
||||||
|
text_encoding,
|
||||||
plot_channels,
|
plot_channels,
|
||||||
plot_format,
|
plot_format,
|
||||||
}
|
}
|
||||||
@@ -209,7 +218,7 @@ impl ConfigForm {
|
|||||||
},
|
},
|
||||||
decoder: match &pipeline.decoder {
|
decoder: match &pipeline.decoder {
|
||||||
DecoderChoice::Text => DecoderConfig::Text {
|
DecoderChoice::Text => DecoderConfig::Text {
|
||||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
encoding: pipeline.text_encoding,
|
||||||
},
|
},
|
||||||
DecoderChoice::Hex => DecoderConfig::Hex {
|
DecoderChoice::Hex => DecoderConfig::Hex {
|
||||||
uppercase: false,
|
uppercase: false,
|
||||||
@@ -224,7 +233,7 @@ impl ConfigForm {
|
|||||||
format: pipeline.plot_format,
|
format: pipeline.plot_format,
|
||||||
},
|
},
|
||||||
DecoderChoice::MixedTextPlot => DecoderConfig::MixedTextPlot {
|
DecoderChoice::MixedTextPlot => DecoderConfig::MixedTextPlot {
|
||||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
encoding: pipeline.text_encoding,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -529,6 +538,21 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(pipeline.decoder, DecoderChoice::Text) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Text encoding:");
|
||||||
|
render_text_encoding_combo(ui, &mut pipeline.text_encoding, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(pipeline.decoder, DecoderChoice::MixedTextPlot) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Mixed text encoding:");
|
||||||
|
render_text_encoding_combo(ui, &mut pipeline.text_encoding, i);
|
||||||
|
});
|
||||||
|
ui.small("Plot sample type, endian, format, and channels come from the mixed packet header.");
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(pipeline.decoder, DecoderChoice::MixedTextPlot)
|
if matches!(pipeline.decoder, DecoderChoice::MixedTextPlot)
|
||||||
&& !matches!(pipeline.framer, FramerChoice::MixedTextPlot)
|
&& !matches!(pipeline.framer, FramerChoice::MixedTextPlot)
|
||||||
{
|
{
|
||||||
@@ -548,6 +572,7 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
|||||||
name: format!("p{}", form.pipelines.len()),
|
name: format!("p{}", form.pipelines.len()),
|
||||||
framer: FramerChoice::Line,
|
framer: FramerChoice::Line,
|
||||||
decoder: DecoderChoice::Text,
|
decoder: DecoderChoice::Text,
|
||||||
|
text_encoding: TextEncoding::Utf8,
|
||||||
plot_channels: 1,
|
plot_channels: 1,
|
||||||
plot_format: PlotFormat::Interleaved,
|
plot_format: PlotFormat::Interleaved,
|
||||||
});
|
});
|
||||||
@@ -573,3 +598,17 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_text_encoding_combo(ui: &mut Ui, encoding: &mut TextEncoding, index: usize) {
|
||||||
|
ComboBox::from_id_salt(format!("text_encoding{}", index))
|
||||||
|
.selected_text(match encoding {
|
||||||
|
TextEncoding::Utf8 => "UTF-8",
|
||||||
|
TextEncoding::Latin1 => "Latin1",
|
||||||
|
TextEncoding::Ascii => "ASCII",
|
||||||
|
})
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
ui.selectable_value(encoding, TextEncoding::Utf8, "UTF-8");
|
||||||
|
ui.selectable_value(encoding, TextEncoding::Latin1, "Latin1");
|
||||||
|
ui.selectable_value(encoding, TextEncoding::Ascii, "ASCII");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
300
crates/xserial-gui/src/ui_fonts.rs
Normal file
300
crates/xserial-gui/src/ui_fonts.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use eframe::egui::{self, FontData, FontDefinitions, FontFamily, FontId, Style, TextStyle};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FontChoice {
|
||||||
|
Auto,
|
||||||
|
Default,
|
||||||
|
System(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FontCandidate {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub family: String,
|
||||||
|
pub style: String,
|
||||||
|
pub path: String,
|
||||||
|
pub likely_cjk: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct UiFontSettings {
|
||||||
|
pub primary_choice: FontChoice,
|
||||||
|
pub fallback_choice: FontChoice,
|
||||||
|
pub ui_font_size: f32,
|
||||||
|
pub monospace_font_size: f32,
|
||||||
|
pub heading_font_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiFontSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
primary_choice: FontChoice::Auto,
|
||||||
|
fallback_choice: FontChoice::Default,
|
||||||
|
ui_font_size: 14.0,
|
||||||
|
monospace_font_size: 14.0,
|
||||||
|
heading_font_size: 20.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discover_font_candidates() -> Vec<FontCandidate> {
|
||||||
|
discover_via_fc_list().unwrap_or_else(discover_via_fallback_paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_font_settings(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
settings: &UiFontSettings,
|
||||||
|
candidates: &[FontCandidate],
|
||||||
|
) {
|
||||||
|
let mut fonts = FontDefinitions::default();
|
||||||
|
let mut inserted = Vec::new();
|
||||||
|
|
||||||
|
for choice in [&settings.primary_choice, &settings.fallback_choice] {
|
||||||
|
if let Some((font_name, font_bytes)) = load_selected_font(choice, candidates, &inserted) {
|
||||||
|
fonts
|
||||||
|
.font_data
|
||||||
|
.insert(font_name.clone(), FontData::from_owned(font_bytes).into());
|
||||||
|
inserted.push(font_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inserted.is_empty() {
|
||||||
|
if let Some(family) = fonts.families.get_mut(&FontFamily::Proportional) {
|
||||||
|
for font_name in inserted.iter().rev() {
|
||||||
|
family.insert(0, font_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(family) = fonts.families.get_mut(&FontFamily::Monospace) {
|
||||||
|
for font_name in inserted.iter().rev() {
|
||||||
|
family.insert(0, font_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!(fonts = ?inserted, "Applied egui font chain");
|
||||||
|
} else {
|
||||||
|
if settings.primary_choice != FontChoice::Default
|
||||||
|
|| settings.fallback_choice != FontChoice::Default
|
||||||
|
{
|
||||||
|
warn!("No configured fonts could be loaded; falling back to egui default fonts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
|
||||||
|
let mut style = (*ctx.global_style()).clone();
|
||||||
|
apply_font_size(&mut style, settings);
|
||||||
|
ctx.set_global_style(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_choice_label(choice: &FontChoice, candidates: &[FontCandidate]) -> String {
|
||||||
|
match choice {
|
||||||
|
FontChoice::Auto => String::from("Auto"),
|
||||||
|
FontChoice::Default => String::from("Default"),
|
||||||
|
FontChoice::System(id) => candidates
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| candidate.id == *id)
|
||||||
|
.map(|candidate| candidate.label.clone())
|
||||||
|
.unwrap_or_else(|| id.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_selected_font(
|
||||||
|
choice: &FontChoice,
|
||||||
|
candidates: &[FontCandidate],
|
||||||
|
already_loaded: &[String],
|
||||||
|
) -> Option<(String, Vec<u8>)> {
|
||||||
|
match choice {
|
||||||
|
FontChoice::Default => None,
|
||||||
|
FontChoice::Auto => {
|
||||||
|
load_auto_font(candidates, already_loaded)
|
||||||
|
}
|
||||||
|
FontChoice::System(id) => {
|
||||||
|
let candidate = candidates.iter().find(|candidate| &candidate.id == id)?;
|
||||||
|
if already_loaded.iter().any(|loaded| loaded == &candidate.id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match fs::read(&candidate.path) {
|
||||||
|
Ok(bytes) => Some((candidate.id.clone(), bytes)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(path = %candidate.path, error = %err, "Failed to load selected font");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_auto_font(
|
||||||
|
candidates: &[FontCandidate],
|
||||||
|
already_loaded: &[String],
|
||||||
|
) -> Option<(String, Vec<u8>)> {
|
||||||
|
for candidate in candidates.iter().filter(|candidate| candidate.likely_cjk) {
|
||||||
|
if already_loaded.iter().any(|loaded| loaded == &candidate.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match fs::read(&candidate.path) {
|
||||||
|
Ok(bytes) => return Some((candidate.id.clone(), bytes)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(path = %candidate.path, error = %err, "Failed to load preferred CJK font");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for candidate in candidates {
|
||||||
|
if already_loaded.iter().any(|loaded| loaded == &candidate.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match fs::read(&candidate.path) {
|
||||||
|
Ok(bytes) => return Some((candidate.id.clone(), bytes)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(path = %candidate.path, error = %err, "Failed to load candidate font");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_font_size(style: &mut Style, settings: &UiFontSettings) {
|
||||||
|
style
|
||||||
|
.text_styles
|
||||||
|
.insert(TextStyle::Body, FontId::proportional(settings.ui_font_size));
|
||||||
|
style
|
||||||
|
.text_styles
|
||||||
|
.insert(TextStyle::Button, FontId::proportional(settings.ui_font_size));
|
||||||
|
style.text_styles.insert(
|
||||||
|
TextStyle::Monospace,
|
||||||
|
FontId::monospace(settings.monospace_font_size),
|
||||||
|
);
|
||||||
|
style
|
||||||
|
.text_styles
|
||||||
|
.insert(TextStyle::Small, FontId::proportional((settings.ui_font_size - 2.0).max(8.0)));
|
||||||
|
style
|
||||||
|
.text_styles
|
||||||
|
.insert(TextStyle::Heading, FontId::proportional(settings.heading_font_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_via_fc_list() -> Option<Vec<FontCandidate>> {
|
||||||
|
let output = Command::new("fc-list")
|
||||||
|
.args([":", "file", "family", "style"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let (path, rest) = line.split_once(": ")?;
|
||||||
|
let (family_part, style_part) = rest.split_once(":style=").unwrap_or((rest, ""));
|
||||||
|
let family = family_part
|
||||||
|
.split(',')
|
||||||
|
.find(|name| !name.trim().is_empty())
|
||||||
|
.unwrap_or(family_part)
|
||||||
|
.trim()
|
||||||
|
.to_owned();
|
||||||
|
let style = style_part
|
||||||
|
.split(',')
|
||||||
|
.find(|name| !name.trim().is_empty())
|
||||||
|
.unwrap_or(style_part)
|
||||||
|
.trim()
|
||||||
|
.to_owned();
|
||||||
|
let path = path.trim();
|
||||||
|
if !Path::new(path).is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stem = Path::new(path)
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("font");
|
||||||
|
let id = sanitize_id(&format!("{family}_{style}_{stem}"));
|
||||||
|
let label = if style.is_empty() || style == "Regular" {
|
||||||
|
family.clone()
|
||||||
|
} else {
|
||||||
|
format!("{family} ({style})")
|
||||||
|
};
|
||||||
|
candidates.push(FontCandidate {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
family: family.clone(),
|
||||||
|
style: style.clone(),
|
||||||
|
path: path.to_owned(),
|
||||||
|
likely_cjk: is_likely_cjk(&family, &style, path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.path.cmp(&b.path)));
|
||||||
|
candidates.dedup_by(|a, b| a.path == b.path);
|
||||||
|
(!candidates.is_empty()).then_some(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_via_fallback_paths() -> Vec<FontCandidate> {
|
||||||
|
let fallback_paths = [
|
||||||
|
"/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/google-noto-sans-cjk-vf-fonts/NotoSansCJK-VF.ttc",
|
||||||
|
"/usr/share/fonts/google-droid-sans-fonts/DroidSansFallbackFull.ttf",
|
||||||
|
];
|
||||||
|
|
||||||
|
fallback_paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
fs::metadata(path).ok().map(|_| {
|
||||||
|
let stem = Path::new(path)
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("font")
|
||||||
|
.to_owned();
|
||||||
|
FontCandidate {
|
||||||
|
id: sanitize_id(&stem),
|
||||||
|
label: stem.clone(),
|
||||||
|
family: stem.clone(),
|
||||||
|
style: String::from("Regular"),
|
||||||
|
path: path.to_owned(),
|
||||||
|
likely_cjk: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_id(input: &str) -> String {
|
||||||
|
input
|
||||||
|
.chars()
|
||||||
|
.map(|ch| {
|
||||||
|
if ch.is_ascii_alphanumeric() {
|
||||||
|
ch.to_ascii_lowercase()
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_likely_cjk(family: &str, style: &str, path: &str) -> bool {
|
||||||
|
let haystack = format!("{family} {style} {path}").to_lowercase();
|
||||||
|
[
|
||||||
|
"cjk",
|
||||||
|
"noto sans sc",
|
||||||
|
"noto sans tc",
|
||||||
|
"noto sans jp",
|
||||||
|
"noto sans kr",
|
||||||
|
"source han",
|
||||||
|
"sarasa",
|
||||||
|
"wenquanyi",
|
||||||
|
"fallback",
|
||||||
|
"han",
|
||||||
|
"hei",
|
||||||
|
"kai",
|
||||||
|
"song",
|
||||||
|
"gothic",
|
||||||
|
"mincho",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|needle| haystack.contains(needle))
|
||||||
|
}
|
||||||
@@ -241,8 +241,8 @@ def main():
|
|||||||
for index, value in enumerate(values)
|
for index, value in enumerate(values)
|
||||||
)
|
)
|
||||||
line = (
|
line = (
|
||||||
f"t={now - started_at:7.3f}s "
|
f"时间={now - started_at:7.3f}s "
|
||||||
f"sample={sample_index:7d} {joined}\n"
|
f"样本={sample_index:7d} {joined}\n"
|
||||||
)
|
)
|
||||||
conn.sendall(line.encode("utf-8"))
|
conn.sendall(line.encode("utf-8"))
|
||||||
text_count += 1
|
text_count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user