OSK integration

This commit is contained in:
Jeremy Soller 2026-09-16 10:46:57 -06:00
parent db39ee661d
commit ce726d8470
6 changed files with 120 additions and 28 deletions

24
Cargo.lock generated
View file

@ -1336,6 +1336,7 @@ dependencies = [
"cosmic-dbus-networkmanager", "cosmic-dbus-networkmanager",
"cosmic-greeter-config", "cosmic-greeter-config",
"cosmic-greeter-daemon", "cosmic-greeter-daemon",
"cosmic-osk-config",
"cosmic-protocols", "cosmic-protocols",
"cosmic-randr-shell", "cosmic-randr-shell",
"cosmic-settings-a11y-manager-subscription", "cosmic-settings-a11y-manager-subscription",
@ -1409,6 +1410,15 @@ dependencies = [
"zbus_systemd", "zbus_systemd",
] ]
[[package]]
name = "cosmic-osk-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-osk#d7b66a24890d03e38dd8962d956bc802d4a41067"
dependencies = [
"cosmic-config",
"serde",
]
[[package]] [[package]]
name = "cosmic-protocols" name = "cosmic-protocols"
version = "0.2.0" version = "0.2.0"
@ -1891,7 +1901,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2086,7 +2096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -4166,7 +4176,7 @@ version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -4807,7 +4817,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -6156,7 +6166,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -6745,7 +6755,7 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -7831,7 +7841,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View file

@ -18,6 +18,7 @@ cosmic-comp-config.workspace = true
cosmic-config = { workspace = true, features = ["calloop", "macro"] } cosmic-config = { workspace = true, features = ["calloop", "macro"] }
cosmic-greeter-config.workspace = true cosmic-greeter-config.workspace = true
cosmic-greeter-daemon = { path = "daemon" } cosmic-greeter-daemon = { path = "daemon" }
cosmic-osk-config = { git = "https://github.com/pop-os/cosmic-osk" }
dirs = "6" dirs = "6"
libcosmic = { workspace = true, features = [ libcosmic = { workspace = true, features = [
"autosize", "autosize",

View file

@ -9,6 +9,7 @@ caps-lock = Caps Lock is active.
enter-user = Enter name manually... enter-user = Enter name manually...
type-username = Username: type-username = Username:
keyboard-layout = Keyboard layout keyboard-layout = Keyboard layout
on-screen-keyboard = On-screen keyboard
restart = Restart restart = Restart
restart-now = Restart now? restart-now = Restart now?
restart-timeout = The system will restart automatically restart-timeout = The system will restart automatically

View file

@ -10,6 +10,7 @@ use cosmic::iced::{self, Rectangle, Size, Subscription};
use cosmic::surface::corner_radius::rounded_rect_strips; use cosmic::surface::corner_radius::rounded_rect_strips;
use cosmic::widget::rectangle_tracker::{RectangleUpdate, rectangle_tracker_subscription}; use cosmic::widget::rectangle_tracker::{RectangleUpdate, rectangle_tracker_subscription};
use cosmic::widget::{self, RectangleTracker}; use cosmic::widget::{self, RectangleTracker};
use cosmic_config::CosmicConfigEntry;
use cosmic_greeter_daemon::{BgSource, UserData}; use cosmic_greeter_daemon::{BgSource, UserData};
use cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1; use cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@ -19,6 +20,7 @@ use wayland_client::{Connection, Proxy};
use crate::keyboard_layout_wayland; use crate::keyboard_layout_wayland;
pub const MAX_WIDTH: f32 = 866.;
pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.; pub const DEFAULT_MENU_ITEM_HEIGHT: f32 = 36.;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
@ -28,6 +30,18 @@ pub struct ActiveLayout {
pub variant: String, pub variant: String,
} }
impl ActiveLayout {
pub fn name(&self) -> String {
let mut name = self.layout.clone();
if !self.variant.is_empty() {
name.push_str(" (");
name.push_str(&self.variant);
name.push_str(")");
}
name
}
}
pub struct Common<M> { pub struct Common<M> {
pub wayland_connection: Option<Connection>, pub wayland_connection: Option<Connection>,
pub keyboard_layout: Option<ZcosmicKeyboardLayoutV1>, pub keyboard_layout: Option<ZcosmicKeyboardLayoutV1>,
@ -67,6 +81,7 @@ pub enum Message {
Focus(SurfaceId), Focus(SurfaceId),
Key(Modifiers, Key, Option<SmolStr>), Key(Modifiers, Key, Option<SmolStr>),
NetworkIcon(Option<&'static str>), NetworkIcon(Option<&'static str>),
OnScreenKeyboard,
SubsurfaceOpened(SurfaceId), SubsurfaceOpened(SurfaceId),
OutputEvent(OutputEvent, WlOutput), OutputEvent(OutputEvent, WlOutput),
PowerInfo(Option<(f64, bool, bool)>), PowerInfo(Option<(f64, bool, bool)>),
@ -276,6 +291,26 @@ impl<M: From<Message> + Send + 'static> Common<M> {
self.network_icon_opt = self.network_icon_opt =
network_icon_opt.map(|name| widget::icon::from_name(name).into()); network_icon_opt.map(|name| widget::icon::from_name(name).into());
} }
Message::OnScreenKeyboard => {
use cosmic_osk_config::Config;
match Config::handler() {
Ok(handler) => {
let mut config = match Config::get_entry(&handler) {
Ok(config) => config,
Err((errs, config)) => {
tracing::warn!("failed to parse OSK config: {:?}", errs);
config
}
};
if let Err(err) = config.set_always_shown(&handler, !config.always_shown) {
tracing::error!("failed to set OSK always_shown config: {}", err);
}
}
Err(err) => {
tracing::error!("failed to create OSK config handler: {}", err);
}
}
}
Message::OutputEvent(output_event, output) => { Message::OutputEvent(output_event, output) => {
if self.wayland_connection.is_none() { if self.wayland_connection.is_none() {
if let Some(backend) = output.backend().upgrade() { if let Some(backend) = output.backend().upgrade() {

View file

@ -11,6 +11,7 @@ use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity
use cosmic::cosmic_config::{self, ConfigSet}; use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::cosmic_theme::{self, CosmicPalette}; use cosmic::cosmic_theme::{self, CosmicPalette};
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env}; use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::event::listen_with; use cosmic::iced::event::listen_with;
use cosmic::iced::event::wayland::OutputEvent; use cosmic::iced::event::wayland::OutputEvent;
use cosmic::iced::futures::SinkExt; use cosmic::iced::futures::SinkExt;
@ -41,7 +42,7 @@ use std::num::NonZeroU32;
use std::process::Stdio; use std::process::Stdio;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fs, io, process}; use std::{fs, io, process, thread};
use tokio::process::Child; use tokio::process::Child;
use tokio::time; use tokio::time;
use tracing::metadata::LevelFilter; use tracing::metadata::LevelFilter;
@ -52,7 +53,7 @@ use wayland_client::Proxy;
use wayland_client::protocol::wl_output::WlOutput; use wayland_client::protocol::wl_output::WlOutput;
use zbus::{Connection, proxy}; use zbus::{Connection, proxy};
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT}; use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT, MAX_WIDTH};
use crate::fl; use crate::fl;
static USERNAME_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("username-id")); static USERNAME_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("username-id"));
@ -280,6 +281,15 @@ pub fn main() -> Result<(), Box<dyn Error>> {
sessions sessions
}; };
match process::Command::new("cosmic-osk").spawn() {
Ok(mut child) => {
thread::spawn(move || child.wait().unwrap());
}
Err(err) => {
tracing::warn!("failed to spawn cosmic-osk: {}", err);
}
}
let flags = Flags { let flags = Flags {
user_icons: user_datas user_icons: user_datas
.iter_mut() .iter_mut()
@ -482,9 +492,9 @@ impl App {
.window_size .window_size
.get(&id) .get(&id)
.map(|s| s.width) .map(|s| s.width)
.unwrap_or(800.); .unwrap_or(MAX_WIDTH);
let menu_width = if window_width > 800. { let menu_width = if window_width > MAX_WIDTH {
800. MAX_WIDTH
} else { } else {
window_width window_width
}; };
@ -584,9 +594,19 @@ impl App {
}; };
let mut input_button = widget::popover( let mut input_button = widget::popover(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic")) widget::container(
widget::button::custom(
widget::text(
self.common.active_layouts[self.common.current_keyboard_layout].name(),
)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.height(16)
.center(),
)
.padding(12.0) .padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Keyboard)), .on_press(Message::DropdownToggle(Dropdown::Keyboard)),
)
.max_width(92),
) )
.position(widget::popover::Position::Bottom); .position(widget::popover::Position::Bottom);
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) { if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
@ -702,6 +722,13 @@ impl App {
text(fl!("accessibility")), text(fl!("accessibility")),
widget::tooltip::Position::Top widget::tooltip::Position::Top
), ),
widget::tooltip(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::Common(common::Message::OnScreenKeyboard)),
text(fl!("on-screen-keyboard")),
widget::tooltip::Position::Top,
),
widget::tooltip( widget::tooltip(
input_button, input_button,
text(fl!("keyboard-layout")), text(fl!("keyboard-layout")),
@ -965,7 +992,7 @@ impl App {
appearance appearance
}, },
))) )))
.width(Length::Fixed(800.0)); .width(Length::Fixed(MAX_WIDTH));
let menu = if let Some(t) = self.common.rectangle_tracker.as_ref() { let menu = if let Some(t) = self.common.rectangle_tracker.as_ref() {
Element::from(t.container((id, false), menu)) Element::from(t.container((id, false), menu))
} else { } else {
@ -1253,10 +1280,10 @@ impl cosmic::Application for App {
let unwrapped_size = size let unwrapped_size = size
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080))) .map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
.unwrap_or((1920, 1080)); .unwrap_or((1920, 1080));
let (loc, sub_size) = if unwrapped_size.0 > 800 { let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
( (
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.), Point::new((unwrapped_size.0 as f32 - MAX_WIDTH) / 2., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.), Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
) )
} else { } else {
( (
@ -1872,8 +1899,8 @@ impl cosmic::Application for App {
tracing::error!("Failed to find subsurface menu id"); tracing::error!("Failed to find subsurface menu id");
return Task::none(); return Task::none();
}; };
let loc = if size.width > 800. { let loc = if size.width > MAX_WIDTH {
Point::new(size.width / 2. - 400., 32.) Point::new((size.width - MAX_WIDTH) / 2., 32.)
} else { } else {
Point::new(0., 32.) Point::new(0., 32.)
}; };

View file

@ -4,6 +4,7 @@
use color_eyre::eyre::WrapErr; use color_eyre::eyre::WrapErr;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity; use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::event::wayland::{OutputEvent, SessionLockEvent}; use cosmic::iced::event::wayland::{OutputEvent, SessionLockEvent};
use cosmic::iced::futures::{self, SinkExt}; use cosmic::iced::futures::{self, SinkExt};
use cosmic::iced::platform_specific::shell::wayland::commands::session_lock::{ use cosmic::iced::platform_specific::shell::wayland::commands::session_lock::{
@ -34,7 +35,7 @@ use tracing_subscriber::{EnvFilter, fmt};
use wayland_client::Proxy; use wayland_client::Proxy;
use wayland_client::protocol::wl_output::WlOutput; use wayland_client::protocol::wl_output::WlOutput;
use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT}; use crate::common::{self, Common, DEFAULT_MENU_ITEM_HEIGHT, MAX_WIDTH};
use crate::fl; use crate::fl;
fn lockfile_opt() -> Option<PathBuf> { fn lockfile_opt() -> Option<PathBuf> {
@ -326,9 +327,9 @@ impl App {
.window_size .window_size
.get(&surface_id) .get(&surface_id)
.map(|s| s.width) .map(|s| s.width)
.unwrap_or(800.); .unwrap_or(MAX_WIDTH);
let menu_width = if window_width > 800. { let menu_width = if window_width > MAX_WIDTH {
800. MAX_WIDTH
} else { } else {
window_width window_width
}; };
@ -417,9 +418,19 @@ impl App {
}; };
let mut input_button = widget::popover( let mut input_button = widget::popover(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic")) widget::container(
widget::button::custom(
widget::text(
self.common.active_layouts[self.common.current_keyboard_layout].name(),
)
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.height(16)
.center(),
)
.padding(12.0) .padding(12.0)
.on_press(Message::DropdownToggle(Dropdown::Keyboard)), .on_press(Message::DropdownToggle(Dropdown::Keyboard)),
)
.max_width(92),
) )
.position(widget::popover::Position::Bottom); .position(widget::popover::Position::Bottom);
if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) { if matches!(self.dropdown_opt, Some(Dropdown::Keyboard)) {
@ -443,6 +454,13 @@ impl App {
.padding(12.0) .padding(12.0)
.on_press(Message::None), .on_press(Message::None),
*/ */
widget::tooltip(
widget::button::custom(widget::icon::from_name("input-keyboard-symbolic"))
.padding(12.0)
.on_press(Message::Common(common::Message::OnScreenKeyboard)),
widget::text(fl!("on-screen-keyboard")),
widget::tooltip::Position::Top,
),
widget::tooltip( widget::tooltip(
input_button, input_button,
widget::text(fl!("keyboard-layout")), widget::text(fl!("keyboard-layout")),
@ -783,10 +801,10 @@ impl cosmic::Application for App {
let unwrapped_size = size let unwrapped_size = size
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080))) .map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
.unwrap_or((1920, 1080)); .unwrap_or((1920, 1080));
let (loc, sub_size) = if unwrapped_size.0 > 800 { let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
( (
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.), Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.), Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
) )
} else { } else {
( (
@ -864,10 +882,10 @@ impl cosmic::Application for App {
let unwrapped_size = size let unwrapped_size = size
.map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080))) .map(|s| (s.0.unwrap_or(1920), s.1.unwrap_or(1080)))
.unwrap_or((1920, 1080)); .unwrap_or((1920, 1080));
let (loc, sub_size) = if unwrapped_size.0 > 800 { let (loc, sub_size) = if unwrapped_size.0 as f32 > MAX_WIDTH {
( (
Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.), Point::new(unwrapped_size.0 as f32 / 2. - 400., 32.),
Size::new(800., unwrapped_size.1 as f32 - 32.), Size::new(MAX_WIDTH, unwrapped_size.1 as f32 - 32.),
) )
} else { } else {
(Point::ORIGIN, Size::new(1920., 1080.)) (Point::ORIGIN, Size::new(1920., 1080.))