v0.2.0
clean architecture refactor, performance, resilience, DX/UX architecture: - 13 crates with proper domain/application/infrastructure layers - domain crate: newtypes, ports (Plugin, AppLauncher), constants - kernel: pure orchestrator - shared UI state machine (k-launcher-ui-core) - merged plugin-api into domain as ports module - granular file structure (no monolithic lib.rs) - all tests extracted to tests/ directories features: - frecency boost in search results - empty query shows top frecent apps - append-only frecency log with configurable compaction - config-driven styling (all colors, sizes, debounce) - configurable terminal emulator, external plugin timeout - log rotation with max_log_files - loading indicator, descriptive placeholder text - graceful shutdown via iced::exit() + Plugin::shutdown() - --version flag, panic hook, signal handling (SIGINT/SIGTERM) - SpawnInTerminal in external plugin protocol performance: - ~1500 -> ~50 heap allocs per keystroke - reused Matcher, Pattern, char buffer across entries - Arc<str> for shared result fields - pre-filter before fuzzy matching - partial sort for top frecent IDs - cached lowercase names in entries resilience: - parking_lot (no mutex poisoning) - thiserror hierarchy (PluginError, ConfigError, AppError) - all silent error swallowing replaced with tracing::warn - config parse errors logged quality: - named constants (no magic strings/numbers) - named types (no anonymous tuples) - Rgba newtype with validation - domain newtype validation (debug_assert non-empty) - man page, LICENSE (MIT), PKGBUILD, example config - plugin development guide updated - make check (fmt + clippy + test), make dev (RUST_LOG=debug) style: format code for better readability in tests and function signatures fix: update build_entries function signature to ignore frecency parameter fix(review): bugs, arch violations, design smells P1 bugs: - unix_launcher: shell_split respects quoted args (was split_whitespace) - plugin-host: 5s timeout on external plugin search - ui: handle engine init panic, wire error state - ui-egui: read window config instead of always using defaults - plugin-url: use OpenPath action instead of SpawnProcess+xdg-open Architecture: - remove WindowConfig (mirror of WindowCfg); use WindowCfg directly - remove on_select closure from SearchResult (domain leakage) - remove LaunchAction::Custom; add Plugin::on_selected + SearchEngine::on_selected - apps: record frecency via on_selected instead of embedded closure Design smells: - frecency: extract decay_factor helper, write outside mutex - apps: remove cfg(test) cache_path hack; add new_for_test ctor - apps: stable ResultId using name+exec to prevent collision - files: stable ResultId using full path instead of index - plugin-host: remove k-launcher-os-bridge dep (WindowConfig gone) Update iced dependency in Cargo.toml to disable default features and add additional ones feat(app): enhance engine initialization with EngineHandle and update run function signature feat: production hardening (panic isolation, file logging, apps cache) - Kernel::search wraps each plugin in catch_unwind; panics are logged and return [] - init_logging() adds daily rolling file at ~/.local/share/k-launcher/logs/ - AppsPlugin caches entries to ~/.cache/k-launcher/apps.bin via bincode; stale-while-revalidate on subsequent launches - 57 tests pass refactor: remove client module and associated show command logic fix(app): format code for clarity in update function chore: update .gitignore and enhance README with compositor setup instructions chore(docs): remove unused screenshot file feature/prod-ready (#1) Reviewed-on: #1 fix(calc): remove ambiguous log alias, use ln/log2/log10 explicitly fix(calc): fix log/ln naming, cache math context, strengthen sin(pi) test feat(calc): add math functions (sqrt, sin, cos, etc.) and pi/e constants refactor(calc): rename preprocess, extend underscore test assertions feat(calc): strip underscore digit separators feat: update dependencies for improved compatibility and performance feat: add plugin-url for URL handling and open in browser functionality feat: add support for external plugins and enhance plugin management feat: add Makefile for build, run, and installation commands feat: add required features for k-launcher-egui and update dependencies feat: update README and add documentation for installation, configuration, usage, and plugin development feat: enhance configuration management and UI styling, remove unused theme module feat: add k-launcher-config crate for configuration management and integrate with existing components feat: add k-launcher-ui-egui crate for enhanced UI - Introduced a new crate `k-launcher-ui-egui` to provide a graphical user interface using eframe and egui. - Updated the workspace configuration in `Cargo.toml` to include the new crate. - Implemented the main application logic in `src/app.rs`, handling search functionality and user interactions. - Created a library entry point in `src/lib.rs` to expose the `run` function for launching the UI. - Modified the `k-launcher` crate to include a new binary target for the egui-based launcher. - Added a new main file `src/main_egui.rs` to initialize and run the egui UI with the existing kernel and launcher components. feat: implement OS bridge and enhance app launcher functionality feat: add FilesPlugin for file searching and integrate into KLauncher feat: implement frecency tracking for app usage and enhance search functionality feat: add CmdPlugin for executing terminal commands and update workspace configuration refactor: update dependencies and improve keyboard event handling in KLauncherApp refactor: simplify theme usage and enhance AppsPlugin structure feat: restructure k-launcher workspace and add core functionality - Updated Cargo.toml to include a new k-launcher crate and reorganized workspace members. - Introduced a README.md file detailing the project philosophy, architecture, and technical specifications. - Implemented a new Kernel struct in k-launcher-kernel for managing plugins and search functionality. - Created a Plugin trait for plugins to implement, allowing for asynchronous search operations. - Developed k-launcher-ui with an Iced-based UI for user interaction, including search input and result display. - Added AppsPlugin and CalcPlugin to handle application launching and basic calculations, respectively. - Established a theme module for UI styling, focusing on an Aero aesthetic. - Removed unnecessary main.rs files from plugin crates, streamlining the project structure. Initialize k-launcher project structure with multiple crates and basic configurations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "k-launcher-ui-egui"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
@@ -10,6 +10,9 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
eframe = { version = "0.31", default-features = false, features = ["default_fonts", "wayland", "x11", "glow"] }
|
||||
egui = "0.31"
|
||||
k-launcher-config = { path = "../k-launcher-config" }
|
||||
k-launcher-kernel = { path = "../k-launcher-kernel" }
|
||||
k-launcher-config = { workspace = true }
|
||||
k-launcher-domain = { workspace = true }
|
||||
k-launcher-kernel = { workspace = true }
|
||||
k-launcher-ui-core = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -1,169 +1,153 @@
|
||||
use std::sync::{Arc, mpsc};
|
||||
|
||||
use egui::{Color32, Key, ViewportCommand};
|
||||
use k_launcher_kernel::{AppLauncher, SearchEngine, SearchResult};
|
||||
use egui::ViewportCommand;
|
||||
use k_launcher_config::AppearanceCfg;
|
||||
use k_launcher_domain::AppLauncher;
|
||||
use k_launcher_domain::SearchResult;
|
||||
use k_launcher_kernel::Kernel;
|
||||
use k_launcher_ui_core::{Action, Effect, LauncherState};
|
||||
|
||||
const BG: Color32 = Color32::from_rgba_premultiplied(20, 20, 30, 230);
|
||||
const BORDER_COLOR: Color32 = Color32::from_rgb(229, 125, 33);
|
||||
const SELECTED_BG: Color32 = Color32::from_rgba_premultiplied(0, 100, 140, 180);
|
||||
const DIM_TEXT: Color32 = Color32::from_rgb(180, 185, 200);
|
||||
use crate::input::{InputAction, process_input};
|
||||
use crate::render;
|
||||
use crate::style;
|
||||
|
||||
pub struct KLauncherApp {
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
query: String,
|
||||
results: Vec<SearchResult>,
|
||||
selected: usize,
|
||||
pub(crate) inner: LauncherState,
|
||||
rt: tokio::runtime::Handle,
|
||||
result_tx: mpsc::SyncSender<Vec<SearchResult>>,
|
||||
result_rx: mpsc::Receiver<Vec<SearchResult>>,
|
||||
pub(crate) result_rx: mpsc::Receiver<Vec<SearchResult>>,
|
||||
}
|
||||
|
||||
impl KLauncherApp {
|
||||
fn new(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
engine: Arc<Kernel>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
rt: tokio::runtime::Handle,
|
||||
cfg: AppearanceCfg,
|
||||
) -> Self {
|
||||
let (result_tx, result_rx) = mpsc::sync_channel(4);
|
||||
Self {
|
||||
engine,
|
||||
launcher,
|
||||
query: String::new(),
|
||||
results: vec![],
|
||||
selected: 0,
|
||||
const RESULT_CHANNEL_CAPACITY: usize = 4;
|
||||
let (result_tx, result_rx) = mpsc::sync_channel(RESULT_CHANNEL_CAPACITY);
|
||||
let mut inner = LauncherState::new(launcher, cfg, 0);
|
||||
|
||||
let effect = inner.handle(Action::EngineReady(engine));
|
||||
|
||||
let app = Self {
|
||||
inner,
|
||||
rt,
|
||||
result_tx,
|
||||
result_rx,
|
||||
}
|
||||
};
|
||||
|
||||
app.execute_effect(effect);
|
||||
app
|
||||
}
|
||||
|
||||
fn trigger_search(&self, query: String) {
|
||||
let engine = self.engine.clone();
|
||||
let Some(engine) = self.inner.engine().cloned() else {
|
||||
return;
|
||||
};
|
||||
let tx = self.result_tx.clone();
|
||||
self.rt.spawn(async move {
|
||||
let results = engine.search(&query).await;
|
||||
let _ = tx.send(results);
|
||||
if let Err(e) = tx.send(results) {
|
||||
tracing::warn!("search result channel closed: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_search_results(&mut self) {
|
||||
if let Ok(results) = self.result_rx.try_recv() {
|
||||
self.inner.handle(Action::ResultsReady {
|
||||
epoch: self.inner.search_epoch(),
|
||||
results,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_effect(&self, effect: Effect) {
|
||||
match effect {
|
||||
Effect::TriggerSearch(q) => self.trigger_search(q),
|
||||
Effect::SearchAfterDebounce { query, .. } => self.trigger_search(query),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_action(&mut self, action: Action, ctx: &egui::Context) {
|
||||
let effect = self.inner.handle(action);
|
||||
match effect {
|
||||
Effect::LaunchAndExit(action) => {
|
||||
self.inner.launcher().execute(&action);
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
Effect::Exit => {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
other => self.execute_effect(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_panel(&mut self, ctx: &egui::Context) {
|
||||
let cfg = self.inner.cfg().clone();
|
||||
egui::CentralPanel::default()
|
||||
.frame(style::outer_frame(&cfg))
|
||||
.show(ctx, |ui| {
|
||||
let query = self.inner.query().to_string();
|
||||
let mut query_buf = query;
|
||||
let response = render::render_search_bar(ui, &mut query_buf, &cfg);
|
||||
|
||||
if response.changed() {
|
||||
self.handle_action(Action::QueryChanged(query_buf), ctx);
|
||||
}
|
||||
|
||||
response.request_focus();
|
||||
ui.add_space(8.0);
|
||||
|
||||
if self.inner.is_loading() {
|
||||
render::render_loading_state(ui, &cfg);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.inner.results().is_empty() && !self.inner.query().is_empty() {
|
||||
render::render_empty_state(ui, &cfg);
|
||||
return;
|
||||
}
|
||||
|
||||
render::render_result_list(ui, self.inner.results(), self.inner.selected(), &cfg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for KLauncherApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if let Ok(results) = self.result_rx.try_recv() {
|
||||
self.results = results;
|
||||
}
|
||||
self.poll_search_results();
|
||||
|
||||
let mut close = false;
|
||||
let mut launch_selected = false;
|
||||
|
||||
ctx.input(|i| {
|
||||
if i.key_pressed(Key::Escape) {
|
||||
close = true;
|
||||
}
|
||||
if i.key_pressed(Key::Enter) {
|
||||
launch_selected = true;
|
||||
}
|
||||
if i.key_pressed(Key::ArrowDown) {
|
||||
let len = self.results.len();
|
||||
if len > 0 {
|
||||
self.selected = (self.selected + 1).min(len - 1);
|
||||
}
|
||||
}
|
||||
if i.key_pressed(Key::ArrowUp) && self.selected > 0 {
|
||||
self.selected -= 1;
|
||||
}
|
||||
});
|
||||
|
||||
if close {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
return;
|
||||
}
|
||||
|
||||
if launch_selected {
|
||||
if let Some(result) = self.results.get(self.selected) {
|
||||
self.engine.on_selected(&result.id);
|
||||
self.launcher.execute(&result.action);
|
||||
}
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
return;
|
||||
}
|
||||
|
||||
let frame = egui::Frame::new()
|
||||
.fill(BG)
|
||||
.stroke(egui::Stroke::new(1.0, BORDER_COLOR))
|
||||
.inner_margin(egui::Margin::same(12))
|
||||
.corner_radius(egui::CornerRadius::same(8));
|
||||
|
||||
egui::CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
let response = ui.add_sized(
|
||||
[ui.available_width(), 36.0],
|
||||
egui::TextEdit::singleline(&mut self.query)
|
||||
.hint_text("Search...")
|
||||
.font(egui::TextStyle::Heading),
|
||||
);
|
||||
|
||||
if response.changed() {
|
||||
self.selected = 0;
|
||||
self.trigger_search(self.query.clone());
|
||||
}
|
||||
|
||||
response.request_focus();
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
if self.results.is_empty() && !self.query.is_empty() {
|
||||
ui.add_space(20.0);
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.colored_label(DIM_TEXT, "No results");
|
||||
});
|
||||
match process_input(ctx) {
|
||||
InputAction::Close => {
|
||||
self.handle_action(Action::Exit, ctx);
|
||||
return;
|
||||
}
|
||||
InputAction::LaunchSelected => {
|
||||
self.handle_action(Action::LaunchSelected, ctx);
|
||||
return;
|
||||
}
|
||||
InputAction::MoveDown => {
|
||||
self.inner.handle(Action::MoveDown);
|
||||
}
|
||||
InputAction::MoveUp => {
|
||||
self.inner.handle(Action::MoveUp);
|
||||
}
|
||||
InputAction::None => {}
|
||||
}
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
for (i, result) in self.results.iter().enumerate() {
|
||||
let is_selected = i == self.selected;
|
||||
let bg = if is_selected {
|
||||
SELECTED_BG
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
|
||||
let row_frame = egui::Frame::new()
|
||||
.fill(bg)
|
||||
.inner_margin(egui::Margin {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
})
|
||||
.corner_radius(egui::CornerRadius::same(4));
|
||||
|
||||
row_frame.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(result.title.as_str());
|
||||
if let Some(desc) = &result.description {
|
||||
ui.colored_label(DIM_TEXT, desc);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
self.render_panel(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
engine: Arc<Kernel>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
window_cfg: &k_launcher_config::WindowCfg,
|
||||
appearance_cfg: AppearanceCfg,
|
||||
) -> Result<(), eframe::Error> {
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
let handle = rt.handle().clone();
|
||||
@@ -179,8 +163,15 @@ pub fn run(
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"K-Launcher",
|
||||
k_launcher_domain::constants::APP_TITLE,
|
||||
options,
|
||||
Box::new(move |_cc| Ok(Box::new(KLauncherApp::new(engine, launcher, handle)))),
|
||||
Box::new(move |_cc| {
|
||||
Ok(Box::new(KLauncherApp::new(
|
||||
engine,
|
||||
launcher,
|
||||
handle,
|
||||
appearance_cfg,
|
||||
)))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
25
crates/k-launcher-ui-egui/src/input.rs
Normal file
25
crates/k-launcher-ui-egui/src/input.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use egui::Key;
|
||||
|
||||
pub enum InputAction {
|
||||
Close,
|
||||
LaunchSelected,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn process_input(ctx: &egui::Context) -> InputAction {
|
||||
ctx.input(|i| {
|
||||
if i.key_pressed(Key::Escape) {
|
||||
InputAction::Close
|
||||
} else if i.key_pressed(Key::Enter) {
|
||||
InputAction::LaunchSelected
|
||||
} else if i.key_pressed(Key::ArrowDown) {
|
||||
InputAction::MoveDown
|
||||
} else if i.key_pressed(Key::ArrowUp) {
|
||||
InputAction::MoveUp
|
||||
} else {
|
||||
InputAction::None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
mod app;
|
||||
mod input;
|
||||
mod render;
|
||||
mod style;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use k_launcher_kernel::{AppLauncher, SearchEngine};
|
||||
use k_launcher_config::AppearanceCfg;
|
||||
use k_launcher_domain::AppLauncher;
|
||||
use k_launcher_kernel::Kernel;
|
||||
|
||||
pub fn run(
|
||||
engine: Arc<dyn SearchEngine>,
|
||||
engine: Arc<Kernel>,
|
||||
launcher: Arc<dyn AppLauncher>,
|
||||
window_cfg: &k_launcher_config::WindowCfg,
|
||||
) -> Result<(), eframe::Error> {
|
||||
app::run(engine, launcher, window_cfg)
|
||||
appearance_cfg: AppearanceCfg,
|
||||
) -> Result<(), String> {
|
||||
app::run(engine, launcher, window_cfg, appearance_cfg).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
58
crates/k-launcher-ui-egui/src/render.rs
Normal file
58
crates/k-launcher-ui-egui/src/render.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use egui::Ui;
|
||||
use k_launcher_config::AppearanceCfg;
|
||||
use k_launcher_domain::SearchResult;
|
||||
|
||||
use crate::style::{self, ROW_SPACING, SEARCH_BAR_HEIGHT, to_color32};
|
||||
|
||||
pub fn render_search_bar(ui: &mut Ui, query: &mut String, cfg: &AppearanceCfg) -> egui::Response {
|
||||
ui.add_sized(
|
||||
[ui.available_width(), SEARCH_BAR_HEIGHT],
|
||||
egui::TextEdit::singleline(query)
|
||||
.hint_text(&cfg.placeholder)
|
||||
.font(egui::TextStyle::Heading),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_loading_state(ui: &mut Ui, cfg: &AppearanceCfg) {
|
||||
ui.add_space(20.0);
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.colored_label(to_color32(&cfg.no_results_rgba), "Loading...");
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_empty_state(ui: &mut Ui, cfg: &AppearanceCfg) {
|
||||
ui.add_space(20.0);
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.colored_label(to_color32(&cfg.no_results_rgba), "No results");
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_result_list(
|
||||
ui: &mut Ui,
|
||||
results: &[SearchResult],
|
||||
selected: usize,
|
||||
cfg: &AppearanceCfg,
|
||||
) {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
render_result_row(ui, result, i == selected, cfg);
|
||||
ui.add_space(ROW_SPACING);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_result_row(ui: &mut Ui, result: &SearchResult, is_selected: bool, cfg: &AppearanceCfg) {
|
||||
style::result_row_frame(is_selected, cfg).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(result.title.as_str());
|
||||
if let Some(desc) = &result.description {
|
||||
ui.colored_label(to_color32(&cfg.description_rgba), desc.as_ref());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
39
crates/k-launcher-ui-egui/src/style.rs
Normal file
39
crates/k-launcher-ui-egui/src/style.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use egui::{Color32, CornerRadius, Frame, Margin, Stroke};
|
||||
use k_launcher_config::AppearanceCfg;
|
||||
|
||||
pub const SEARCH_BAR_HEIGHT: f32 = 36.0;
|
||||
pub const CONTENT_MARGIN: i8 = 12;
|
||||
pub const ROW_SPACING: f32 = 2.0;
|
||||
|
||||
const ROW_PADDING_VERTICAL: i8 = 6;
|
||||
const ROW_PADDING_HORIZONTAL: i8 = 8;
|
||||
|
||||
pub(crate) fn to_color32(c: &k_launcher_config::Rgba) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(c.red_u8(), c.green_u8(), c.blue_u8(), c.alpha_byte())
|
||||
}
|
||||
|
||||
pub fn outer_frame(cfg: &AppearanceCfg) -> Frame {
|
||||
Frame::new()
|
||||
.fill(to_color32(&cfg.background_rgba))
|
||||
.stroke(Stroke::new(cfg.border_width, to_color32(&cfg.border_rgba)))
|
||||
.inner_margin(Margin::same(CONTENT_MARGIN))
|
||||
.corner_radius(CornerRadius::same(cfg.border_radius as u8))
|
||||
}
|
||||
|
||||
pub fn result_row_frame(is_selected: bool, cfg: &AppearanceCfg) -> Frame {
|
||||
let bg = if is_selected {
|
||||
to_color32(&cfg.selected_row_rgba)
|
||||
} else {
|
||||
to_color32(&cfg.unselected_row_rgba)
|
||||
};
|
||||
|
||||
Frame::new()
|
||||
.fill(bg)
|
||||
.inner_margin(Margin {
|
||||
left: ROW_PADDING_HORIZONTAL,
|
||||
right: ROW_PADDING_HORIZONTAL,
|
||||
top: ROW_PADDING_VERTICAL,
|
||||
bottom: ROW_PADDING_VERTICAL,
|
||||
})
|
||||
.corner_radius(CornerRadius::same(cfg.row_radius as u8))
|
||||
}
|
||||
Reference in New Issue
Block a user