Merge pull request #74 from Ottatop/improve_api

Improve api and fix stuff
This commit is contained in:
Ottatop 2023-09-09 21:02:49 -05:00 committed by GitHub
commit b719ad4a3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 332 additions and 177 deletions

View file

@ -31,6 +31,7 @@ shellexpand = "3.1.0"
toml = "0.7.7"
anyhow = { version = "1.0.75", features = ["backtrace"] }
clap = { version = "4.4.2", features = ["derive"] }
xkbcommon = "0.6.0"
[features]
default = ["egl", "winit", "udev", "xwayland"]

View file

@ -18,7 +18,6 @@ require("pinnacle").setup(function(pinnacle)
local output = pinnacle.output -- Output management
-- Every key supported by xkbcommon.
-- Support for just putting in a string of a key is intended.
local keys = input.keys
---@type Modifier
@ -85,10 +84,12 @@ require("pinnacle").setup(function(pinnacle)
-- Tags ---------------------------------------------------------------------------
local tags = { "1", "2", "3", "4", "5" }
output.connect_for_all(function(op)
-- Add tags 1, 2, 3, 4 and 5 on all monitors, and toggle tag 1 active by default
op:add_tags("1", "2", "3", "4", "5")
op:add_tags(tags)
-- Same as tag.add(op, "1", "2", "3", "4", "5")
tag.toggle({ name = "1", output = op })
@ -112,8 +113,11 @@ require("pinnacle").setup(function(pinnacle)
-- })
end)
---@type Layout[]
local layouts = {
-- Layout cycling
-- Create a layout cycler to cycle your tag layouts. This will store which layout each tag has
-- and change to the next or previous one in the array when the respective function is called.
local layout_cycler = tag.layout_cycler({
"MasterStack",
"Dwindle",
"Spiral",
@ -121,118 +125,29 @@ require("pinnacle").setup(function(pinnacle)
"CornerTopRight",
"CornerBottomLeft",
"CornerBottomRight",
}
local indices = {}
})
-- Layout cycling
-- Yes, this is overly complicated and yes, I'll cook up a way to make it less so.
input.keybind({ mod_key }, keys.space, function()
local tags = output.get_focused():tags()
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or 1])
if indices[name] == nil then
indices[name] = 2
else
if indices[name] + 1 > #layouts then
indices[name] = 1
else
indices[name] = indices[name] + 1
end
end
break
end
end
end)
input.keybind({ mod_key, "Shift" }, keys.space, function()
local tags = output.get_focused():tags()
for _, tg in pairs(tags) do
if tg:active() then
local name = tg:name()
if name == nil then
return
end
tg:set_layout(layouts[indices[name] or #layouts])
if indices[name] == nil then
indices[name] = #layouts - 1
else
if indices[name] - 1 < 1 then
indices[name] = #layouts
else
indices[name] = indices[name] - 1
end
end
break
end
end
end)
input.keybind({ mod_key }, keys.space, layout_cycler.next)
input.keybind({ mod_key, "Shift" }, keys.space, layout_cycler.prev)
input.keybind({ mod_key }, keys.KEY_1, function()
tag.switch_to("1")
end)
input.keybind({ mod_key }, keys.KEY_2, function()
tag.switch_to("2")
end)
input.keybind({ mod_key }, keys.KEY_3, function()
tag.switch_to("3")
end)
input.keybind({ mod_key }, keys.KEY_4, function()
tag.switch_to("4")
end)
input.keybind({ mod_key }, keys.KEY_5, function()
tag.switch_to("5")
end)
-- Tag manipulation
input.keybind({ mod_key, "Shift" }, keys.KEY_1, function()
tag.toggle("1")
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_2, function()
tag.toggle("2")
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_3, function()
tag.toggle("3")
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_4, function()
tag.toggle("4")
end)
input.keybind({ mod_key, "Shift" }, keys.KEY_5, function()
tag.toggle("5")
end)
-- I check for nil this way because I don't want stylua to take up like 80 lines on `if win ~= nil`
input.keybind({ mod_key, "Alt" }, keys.KEY_1, function()
local _ = window.get_focused() and window:get_focused():move_to_tag("1")
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_2, function()
local _ = window.get_focused() and window:get_focused():move_to_tag("2")
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_3, function()
local _ = window.get_focused() and window:get_focused():move_to_tag("3")
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_4, function()
local _ = window.get_focused() and window:get_focused():move_to_tag("4")
end)
input.keybind({ mod_key, "Alt" }, keys.KEY_5, function()
local _ = window.get_focused() and window:get_focused():move_to_tag("5")
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_1, function()
local _ = window.get_focused() and window.get_focused():toggle_tag("1")
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_2, function()
local _ = window.get_focused() and window.get_focused():toggle_tag("2")
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_3, function()
local _ = window.get_focused() and window.get_focused():toggle_tag("3")
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_4, function()
local _ = window.get_focused() and window.get_focused():toggle_tag("4")
end)
input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_5, function()
local _ = window.get_focused() and window.get_focused():toggle_tag("5")
end)
for _, tag_name in pairs(tags) do
-- mod_key + 1-5 switches tags
input.keybind({ mod_key }, tag_name, function()
tag.switch_to(tag_name)
end)
-- mod_key + Shift + 1-5 toggles tags
input.keybind({ mod_key, "Shift" }, tag_name, function()
tag.toggle(tag_name)
end)
-- mod_key + Alt + 1-5 moves windows to tags
input.keybind({ mod_key, "Alt" }, tag_name, function()
local _ = window.get_focused() and window:get_focused():move_to_tag(tag_name)
end)
-- mod_key + Shift + Alt + 1-5 toggles tags on windows
input.keybind({ mod_key, "Shift", "Alt" }, tag_name, function()
local _ = window.get_focused() and window.get_focused():toggle_tag(tag_name)
end)
end
end)

View file

@ -1,12 +1,37 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---Input management.
---
---This module provides utilities to set keybinds.
---@class InputModule
local input_module = {
--- A table with every key provided by xkbcommon.
keys = require("keys"),
}
---Set a keybind. If called with an already existing keybind, it gets replaced.
---
---You must provide three arguments:
---
--- - `modifiers`: An array of `Modifier`s. If you don't want any, provide an empty table.
--- - `key`: The key that will trigger `action`. You can provide three types of key:
--- - Something from the `Keys` table in `input.keys`, which lists every xkbcommon key. The naming pattern is the xkbcommon key without the `KEY_` prefix, unless that would make it start with a number or the reserved lua keyword `function`, in which case the `KEY_` prefix is included.
--- - A single character representing your key. This can be something like "g", "$", "~", "1", and so on.
--- - A string of the key's name. This is the name of the xkbcommon key without the `KEY_` prefix.
--- - `action`: The function that will be run when the keybind is pressed.
---
---It is important to note that `"a"` is different than `"A"`. Similarly, `keys.a` is different than `keys.A`.
---Usually, it's best to use the non-modified key to prevent confusion and unintended behavior.
---
---```lua
---input.keybind({ "Shift" }, "a", function() end) -- This is preferred
---input.keybind({ "Shift" }, "A", function() end) -- over this
---
--- -- And in fact, this keybind won't work at all because it expects no modifiers,
--- -- but you can't get "A" without using `Shift`.
---input.keybind({}, "A", function() end)
---```
---
---### Example
---
---```lua
@ -15,15 +40,24 @@ local input_module = {
--- process.spawn("Alacritty")
---end)
---```
---@param key Keys The key for the keybind.
---@param key Keys|string The key for the keybind.
---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger.
---@param action fun() What to do.
function input_module.keybind(modifiers, key, action)
table.insert(CallbackTable, action)
local k = {}
if type(key) == "string" then
k.String = key
else
k.Int = key
end
SendMsg({
SetKeybind = {
modifiers = modifiers,
key = key,
key = k,
callback_id = #CallbackTable,
},
})

View file

@ -3,7 +3,7 @@
---@meta _
---@class _Msg
---@field SetKeybind { key: Keys, modifiers: Modifier[], callback_id: integer }?
---@field SetKeybind { key: { Int: Keys?, String: string? }, modifiers: Modifier[], callback_id: integer }?
---@field SetMousebind { button: integer }?
--Windows
---@field CloseWindow { window_id: WindowId }?

View file

@ -2,6 +2,9 @@
---@diagnostic disable: redefined-local
---Process management.
---
---This module provides utilities to spawn processes and capture their output.
---@class ProcessModule
local process_module = {}

View file

@ -1,2 +1,2 @@
indent_type = "Spaces"
column_width = 80
column_width = 120

View file

@ -168,14 +168,9 @@ end
---local op = output.get_by_name("DP-1")
---
---tag.toggle("1") -- Toggle tag 1 on the focused output
---tag.toggle({ "1" }) -- Same as above
---
---tag.toggle({ "1", "DP-1" }) -- Toggle tag 1 on DP-1
---tag.toggle({ "1", op }) -- Same as above
---
--- -- Verbose versions of the two above
---tag.toggle({ name = "1", output = "DP-1" })
---tag.toggle({ name = "1", output = op })
---tag.toggle({ name = "1", output = "DP-1" }) -- Toggle tag 1 on "DP-1"
---tag.toggle({ name = "1", output = op }) -- Same as above
---
--- -- Using a tag object
---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1"
@ -205,14 +200,9 @@ end
---local op = output.get_by_name("DP-1")
---
---tag.switch_to("1") -- Switch to tag 1 on the focused output
---tag.switch_to({ "1" }) -- Same as above
---
---tag.switch_to({ "1", "DP-1" }) -- Switch to tag 1 on DP-1
---tag.switch_to({ "1", op }) -- Same as above
---
--- -- Verbose versions of the two above
---tag.switch_to({ name = "1", output = "DP-1" })
---tag.switch_to({ name = "1", output = op })
---tag.switch_to({ name = "1", output = "DP-1" }) -- Switch to tag 1 on "DP-1"
---tag.switch_to({ name = "1", output = op }) -- Same as above
---
--- -- Using a tag object
---local t = tag.get_by_name("1")[1] -- `t` is the first tag with the name "1"
@ -472,4 +462,113 @@ function tag_module.output(t)
return require("output").get_for_tag(t)
end
---@class LayoutCycler
---@field next fun(output: (Output|OutputName)?) Change the first active tag on `output` to its next layout. If `output` is empty, the focused output is used.
---@field prev fun(output: (Output|OutputName)?) Change the first active tag on `output` to its previous layout. If `output` is empty, the focused output is used.
---Given an array of layouts, this will create two functions; one will cycle forward the layout
---for the provided tag, and one will cycle backward.
---
--- ### Example
---```lua
---local layout_cycler = tag.layout_cycler({ "Dwindle", "Spiral", "MasterStack" })
---
---layout_cycler.next() -- Go to the next layout on the first tag of the focused output
---layout_cycler.prev() -- Go to the previous layout on the first tag of the focused output
---
---layout_cycler.next("DP-1") -- Do the above but on "DP-1" instead
---layout_cycler.prev(output.get_by_name("DP-1")) -- With an output object
---```
---@param layouts Layout[] The available layouts.
---@return LayoutCycler layout_cycler A table with the functions `next` and `prev`, which will cycle layouts for the given tag.
function tag_module.layout_cycler(layouts)
local indices = {}
-- Return empty functions if layouts is empty
if #layouts == 0 then
return {
next = function(_) end,
prev = function(_) end,
}
end
return {
---@param output (Output|OutputName)?
next = function(output)
if type(output) == "string" then
output = require("output").get_by_name(output)
end
output = output or require("output").get_focused()
if output == nil then
return
end
local tags = output:tags()
for _, tg in pairs(tags) do
if tg:active() then
local id = tg:id()
if id == nil then
return
end
if #layouts == 1 then
indices[id] = 1
elseif indices[id] == nil then
indices[id] = 2
else
if indices[id] + 1 > #layouts then
indices[id] = 1
else
indices[id] = indices[id] + 1
end
end
tg:set_layout(layouts[indices[id]])
break
end
end
end,
---@param output (Output|OutputName)?
prev = function(output)
if type(output) == "string" then
output = require("output").get_by_name(output)
end
output = output or require("output").get_focused()
if output == nil then
return
end
local tags = output:tags()
for _, tg in pairs(tags) do
if tg:active() then
local id = tg:id()
if id == nil then
return
end
if #layouts == 1 then
indices[id] = 1
elseif indices[id] == nil then
indices[id] = #layouts - 1
else
if indices[id] - 1 < 1 then
indices[id] = #layouts
else
indices[id] = indices[id] - 1
end
end
tg:set_layout(layouts[indices[id]])
break
end
end
end,
}
end
return tag_module

View file

@ -17,11 +17,17 @@ use self::window_rules::{WindowRule, WindowRuleCondition};
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)]
pub struct CallbackId(pub u32);
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub enum KeyIntOrString {
Int(u32),
String(String),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Msg {
// Input
SetKeybind {
key: u32,
key: KeyIntOrString,
modifiers: Vec<Modifier>,
callback_id: CallbackId,
},

View file

@ -72,7 +72,7 @@ use smithay::{
backend::GlobalId, protocol::wl_surface::WlSurface, Display, DisplayHandle,
},
},
utils::{Clock, DeviceFd, Logical, Monotonic, Physical, Point, Rectangle, Transform},
utils::{Clock, DeviceFd, IsAlive, Logical, Monotonic, Physical, Point, Rectangle, Transform},
wayland::{
dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufState},
input_method::{InputMethodHandle, InputMethodSeat},
@ -86,7 +86,9 @@ use smithay_drm_extras::{
use crate::{
api::msg::{Args, OutgoingMsg},
render::{pointer::PointerElement, CustomRenderElements},
state::{take_presentation_feedback, Backend, CalloopData, State, SurfaceDmabufFeedback},
state::{
take_presentation_feedback, Backend, CalloopData, State, SurfaceDmabufFeedback, WithState,
},
window::WindowElement,
};
@ -151,14 +153,10 @@ pub fn run_udev() -> anyhow::Result<()> {
let mut event_loop = EventLoop::try_new().unwrap();
let mut display = Display::new().unwrap();
/*
* Initialize session
*/
// Initialize session
let (session, notifier) = LibSeatSession::new()?;
/*
* Initialize the compositor
*/
// Initialize the compositor
let primary_gpu = if let Ok(var) = std::env::var("ANVIL_DRM_DEVICE") {
DrmNode::from_path(var).expect("Invalid drm device path")
} else {
@ -202,9 +200,7 @@ pub fn run_udev() -> anyhow::Result<()> {
event_loop.handle(),
)?;
/*
* Initialize the udev backend
*/
// Initialize the udev backend
let udev_backend = UdevBackend::new(state.seat.name())?;
// Create DrmNodes from already connected GPUs
@ -245,9 +241,7 @@ pub fn run_udev() -> anyhow::Result<()> {
})
.unwrap();
/*
* Initialize libinput backend
*/
// Initialize libinput backend
let mut libinput_context = Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(
backend.session.clone().into(),
);
@ -256,9 +250,7 @@ pub fn run_udev() -> anyhow::Result<()> {
.unwrap();
let libinput_backend = LibinputInputBackend::new(libinput_context.clone());
/*
* Bind all our objects that get driven by the event loop
*/
// Bind all our objects that get driven by the event loop
let insert_ret = event_loop
.handle()
.insert_source(libinput_backend, move |event, _, data| {
@ -1332,10 +1324,18 @@ impl State {
return;
};
let windows = self
.focus_state
.focus_stack
.iter()
.filter(|win| win.alive())
.cloned()
.collect::<Vec<_>>();
let result = render_surface(
&mut self.cursor_status,
&self.space,
&self.windows,
&windows,
self.dnd_icon.as_ref(),
&self.focus_state.focus_stack,
surface,
@ -1452,6 +1452,31 @@ fn render_surface<'a>(
pointer_location: Point<f64, Logical>,
clock: &Clock<Monotonic>,
) -> Result<bool, SwapBuffersError> {
let pending_win_count = windows
.iter()
.filter(|win| win.alive())
.filter(|win| win.with_state(|state| !state.loc_request_state.is_idle()))
.count() as u32;
tracing::debug!("pending_win_count is {pending_win_count}");
if pending_win_count > 0 {
for win in windows.iter() {
win.send_frame(output, clock.now(), Some(Duration::ZERO), |_, _| {
Some(output.clone())
});
}
surface
.compositor
.queue_frame(None, None, None)
.map_err(Into::<SwapBuffersError>::into)?;
// TODO: still draw the cursor here
return Ok(true);
}
let output_render_elements = crate::render::generate_render_elements(
space,
windows,

View file

@ -12,7 +12,10 @@ use smithay::{
},
winit::{WinitError, WinitEvent, WinitGraphicsBackend},
},
desktop::{layer_map_for_output, utils::send_frames_surface_tree},
desktop::{
layer_map_for_output,
utils::{send_frames_surface_tree, surface_primary_scanout_output},
},
input::pointer::CursorImageStatus,
output::{Output, Subpixel},
reexports::{
@ -32,7 +35,7 @@ use smithay::{
use crate::{
render::pointer::PointerElement,
state::{take_presentation_feedback, Backend, CalloopData, State},
state::{take_presentation_feedback, Backend, CalloopData, State, WithState},
};
use super::BackendData;
@ -232,13 +235,35 @@ pub fn run_winit() -> anyhow::Result<()> {
pointer_element.set_status(state.cursor_status.clone());
if state.pause_rendering {
// TODO: make a pending_windows state, when pending_windows increases,
// | pause rendering.
// | If it goes down, push a frame, then repeat until no pending_windows are left.
let pending_win_count = state
.windows
.iter()
.filter(|win| win.alive())
.filter(|win| win.with_state(|state| !state.loc_request_state.is_idle()))
.count() as u32;
if pending_win_count > 0 {
for win in state.windows.iter() {
win.send_frame(
&output,
state.clock.now(),
Some(Duration::ZERO),
surface_primary_scanout_output,
);
}
state.space.refresh();
state.popup_manager.cleanup();
display
.flush_clients()
.expect("failed to flush client buffers");
// TODO: still draw the cursor here
return TimeoutAction::ToDuration(Duration::from_millis(1));
}
@ -248,9 +273,17 @@ pub fn run_winit() -> anyhow::Result<()> {
state.focus_state.fix_up_focus(&mut state.space);
let windows = state
.focus_state
.focus_stack
.iter()
.filter(|win| win.alive())
.cloned()
.collect::<Vec<_>>();
let output_render_elements = crate::render::generate_render_elements(
&state.space,
&state.windows,
&windows,
state.pointer_location,
&mut state.cursor_status,
state.dnd_icon.as_ref(),

View file

@ -333,7 +333,9 @@ impl XdgShellHandler for State {
if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) {
window.toggle_maximized();
}
// TODO: might need to update_windows here
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
@ -344,6 +346,9 @@ impl XdgShellHandler for State {
if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) {
window.toggle_maximized();
}
let Some(output) = window.output(self) else { return };
self.update_windows(&output);
}
// fn minimize_request(&mut self, surface: ToplevelSurface) {

View file

@ -183,21 +183,30 @@ impl State {
modifier_mask.push(Modifier::Super);
}
let modifier_mask = ModifierMask::from(modifier_mask);
let raw_sym = if keysym.raw_syms().len() == 1 {
keysym.raw_syms()[0]
} else {
keysyms::KEY_NoSymbol
};
let raw_sym = keysym.raw_syms().iter().next();
let mod_sym = keysym.modified_sym();
if let Some(callback_id) = state
let cb_id_mod = state
.input_state
.keybinds
.get(&(modifier_mask, raw_sym))
{
return FilterResult::Intercept(KeyAction::CallCallback(*callback_id));
} else if (modifier_mask, raw_sym) == kill_keybind {
.get(&(modifier_mask, mod_sym));
let cb_id_raw = if let Some(raw_sym) = raw_sym {
state.input_state.keybinds.get(&(modifier_mask, *raw_sym))
} else {
None
};
match (cb_id_mod, cb_id_raw) {
(Some(cb_id), _) | (None, Some(cb_id)) => {
return FilterResult::Intercept(KeyAction::CallCallback(*cb_id));
}
(None, None) => ()
}
if (modifier_mask, mod_sym) == kill_keybind {
return FilterResult::Intercept(KeyAction::Quit);
} else if (modifier_mask, raw_sym) == reload_keybind {
} else if (modifier_mask, mod_sym) == reload_keybind {
return FilterResult::Intercept(KeyAction::ReloadConfig);
} else if let mut vt @ keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12 =
keysym.modified_sym() {

View file

@ -161,24 +161,32 @@ impl State {
//
// This *will* cause everything to freeze for a few frames, but it shouldn't impact
// anything meaningfully.
self.pause_rendering = true;
// self.pause_rendering = true;
// schedule on all idle
self.schedule(
move |_dt| {
// tracing::debug!("Waiting for all to be idle");
tracing::debug!("Waiting for all to be idle");
let all_idle = pending_wins
.iter()
.filter(|(_, win)| win.alive())
.all(|(_, win)| win.with_state(|state| state.loc_request_state.is_idle()));
let num_not_idle = pending_wins
.iter()
.filter(|(_, win)| win.alive())
.filter(|(_, win)| !win.with_state(|state| state.loc_request_state.is_idle()))
.count();
tracing::debug!("{num_not_idle} not idle");
all_idle
},
move |dt| {
for (loc, win) in non_pending_wins {
dt.state.space.map_element(win, loc, false);
}
dt.state.pause_rendering = false;
// dt.state.pause_rendering = false;
},
);
}

View file

@ -138,8 +138,6 @@ pub struct State {
pub xwayland: XWayland,
pub xwm: Option<X11Wm>,
pub xdisplay: Option<u32>,
pub pause_rendering: bool,
}
impl State {
@ -359,8 +357,6 @@ impl State {
xwayland,
xwm: None,
xdisplay: None,
pause_rendering: false,
})
}

View file

@ -9,7 +9,9 @@ use smithay::{
};
use crate::{
api::msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestId, RequestResponse},
api::msg::{
Args, CallbackId, KeyIntOrString, Msg, OutgoingMsg, Request, RequestId, RequestResponse,
},
tag::Tag,
window::WindowElement,
};
@ -25,7 +27,26 @@ impl State {
modifiers,
callback_id,
} => {
tracing::info!("set keybind: {:?}, {}", modifiers, key);
let key = match key {
KeyIntOrString::Int(num) => num,
KeyIntOrString::String(s) => {
if s.chars().count() == 1 {
let Some(ch) = s.chars().next() else { unreachable!() };
let raw = xkbcommon::xkb::Keysym::from_char(ch).raw();
tracing::info!("set keybind: {:?}, {:?} (raw {})", modifiers, ch, raw);
raw
} else {
let raw = xkbcommon::xkb::keysym_from_name(
&s,
xkbcommon::xkb::KEYSYM_NO_FLAGS,
)
.raw();
tracing::info!("set keybind: {:?}, {:?}", modifiers, raw);
raw
}
}
};
self.input_state
.keybinds
.insert((modifiers.into(), key), callback_id);