Compare commits

...

39 Commits

Author SHA1 Message Date
LGUG2Z
40c55dec39 chore(release): v0.1.31 2024-12-13 16:48:35 -08:00
LGUG2Z
5cc2d9d469 chore(deps): cargo update 2024-12-13 16:05:20 -08:00
LGUG2Z
91b255280a ci(github): bump winget-releaser from v2 to main 2024-12-13 16:04:10 -08:00
LGUG2Z
9bd1073a83 perf(bar): add icon cache
This commit adds an icon cache which is indexed by executable name to
avoid unnecessary calls to windows_icons::get_icon_by_process_id, which
is known to start failing after the komorebi-bar process has been
running for a certain (unknown) period of time.
2024-12-10 16:23:37 -08:00
LGUG2Z
53c1990442 docs(schema): update all json schemas 2024-12-09 17:04:18 -08:00
Csaba
9d6173ecbb feat(bar): only collect enabled widgets 2024-12-09 15:11:25 -08:00
Csaba
830da89529 feat(bar): network widget - added show_default_interface and use enable to toggle the whole widget 2024-12-09 15:11:25 -08:00
Csaba
f59d7a51f1 feat(bar): indicate clickable widgets 2024-12-09 15:11:25 -08:00
Csaba
1470c63cfe feat(bar): 5 new grouping styles for shadow and glow 2024-12-09 15:11:25 -08:00
Csaba
64382b18c1 fix(bar): only indicate focused window on stack 2024-12-09 15:11:25 -08:00
alex-ds13
26f90cc9ee fix(borders): floating window z-order handling
This commit makes it so a floating window only has the floating border
when it is focused, if not it has the `Unfocused` border. It also makes
the 'focused_container' have the `Unfocused` border when it is not the
foreground window, for example when we have a floating window focused
instead.

This commit also changes the border's `window_kind` so that the stored
borders actually have that value so we can check it later (This value
wasn't being updated).

This commit also makes it so we properly invalidate the borders in the
situations discussed above (for example when changing focus to/from a
floating window we need the floating window border to update its ZOrder
as well as the previously focused window).

Lastly this commit, changes the `WM_PAINT` code part of the border so
that it now sets the position of border so that the border's ZOrder
updates to it's tracking window ZOrder.
2024-12-09 14:53:14 -08:00
alex-ds13
192af6751b fix(border): stop removing borders on wrong monitors 2024-12-09 07:57:53 -08:00
LGUG2Z
4f306e5bfd docs(schema): update all json schemas 2024-12-07 16:43:28 -08:00
LGUG2Z
ede0b23bb4 feat(borders): track window movements + animations
This commit introduces a number of changes to the border manager module
to enable borders to track the movements of windows as they are being
animated.

As part of these changes, the code paths for borders to track user
movement of windows have also been overhauled.

The biggest conceptual change introduced here is borrowed from
@lukeyou05's work on tacky-borders, where the primary event listener of
the komorebi process now forwards EVENT_OBJECT_LOCATIONCHANGE and
EVENT_OBJECT_DESTROY messages from application windows directly on to
their borders.

These events are handled directly in the border window callbacks,
outside of the main border manager module event processing loop.

In order to handle these events more performantly in the border window
callbacks, a number of state trackers have been added to the Border
struct.

When handling EVENT_OBJECT_NAMECHANGE, these values are read directly
from the struct, whereas when handling WM_PAINT, which is sent by the
system whenever we invalidate a border window, we update the state
values on the Border structs from the various atomic configuration
variables in the mod.rs file.

Another trick I borrowed from tacky-borders is to store a pointer to the
Border object alongside a border window whenever it is created with
CreateWindowExW, which can be accessed within the callback as
GWLP_USERDATA.

There is some unfortunate introduction of unsafe code to make this
happen, but the callback uses null checks to exit the callback early to
ensure (to the best of my ability) that there are no pointer
dereferencing issues once we start making border changes in the context
of the callback.

There are a few other Direct2D related optimizations throughout this
commit, mainly avoiding the recreation of objects like brush properties
and brushes.

Finally, the border_z_order option is now deprecated as the border
window is now tracking the z-ordering of the application window it is
associated with by default - this should resolve a whole host of subtle
border z-ordering issues, especially when dragging windows around using
the mouse.

This work would not have been possible without the guidance of
@lukeyou05, so if you like this feature, please make sure you thank him
too!
2024-12-07 12:10:07 -08:00
LGUG2Z
e6b5b78857 feat(cli): add kill cmd
This commit adds a new komorebic command, "kill", to kill background
processes that may be started by "komorebic start", without terminating
the main komorebi process.

This is useful when iterating on changes to external components like the
bar which may require restarts.
2024-12-07 11:14:25 -08:00
LGUG2Z
440d78e8f4 feat(cli): add support for starting/stopping masir
This commit adds support to the start and stop commands for starting and
stopping masir.
2024-12-07 11:09:38 -08:00
LGUG2Z
280ca0ffcd perf(cli): validate komorebi proc earlier on start
This is a small change to the start command which moves the check for
the komorebi processes to come a little bit earlier.

This small change will make running commands like "komorebic start
--bar" around 3s faster when komorebi is already running.
2024-12-07 10:57:50 -08:00
LGUG2Z
f227bd0fef fix(cli): handle spaces in bar config paths
More PowerShell script username-with-spaces path handling shenanigans.

re #1161
2024-12-07 10:51:11 -08:00
LGUG2Z
3781c8ea41 fix(borders): update on resuming from suspend
This commit is a small fix to ensure that the new Direct2D borders get
redrawn properly after the operating system resumes from a suspended
state.
2024-12-02 16:35:11 -08:00
LGUG2Z
33800903f7 chore(deps): cargo update 2024-12-02 16:33:19 -08:00
CtByte
bb31e7155d feat(bar): komorebi widget visual changes
The visual changes include:

* the focused_window section is now indicating the active window in a stack and has hover effect.
* custom icons for all the layouts, including `paused`, `floating`, `monocle` states.
* custom layout/state picker with configurable options.
* display format configuration for the layouts (Icon/Text/IconAndText)
* display format configuration for the focused_window section (Icon/Text/IconAndText)
* display format configuration for the workspaces section (Icon/Text/IconAndText)
2024-12-02 16:12:51 -08:00
LGUG2Z
40b32332ae fix(wm): track all hwnds in known_hwnds 2024-11-30 15:41:57 -08:00
alex-ds13
8743cdd292 fix(wm): cross-monitor ops for floating windows
This commit fixes an issue where when trying to move floating windows or
windows on a floating workspace across boundaries to another monitor
using the `move_container_in_direction` it wouldn't move the floating
windows physically, although it moved them internally on komorebi,
resulting in weird and wrong behavior.

This commit creates a new method on `Monitor` to
`add_container_with_direction` which takes a move direction and then
uses the same logic that was previously on the
`move_container_in_direction` function.

It changes the `move_container_to_monitor` function to take an optional
move direction which if it is some will have this function call the new
method `add_container_with_direction` instead of just `add_container`.

Lastly the `move_container_in_direction` function now when it realizes
the move will be across monitors simply calls the
`move_container_to_monitor` with the direction that was initially given
to it.

These changes require that all callers of `move_container_to_monitor`
add an direction option, instead of passing `None` on all of them, a new
helper function was created, named `direction_from_monitor_idx` which
calculates the direction a move will have from the currently focused
monitor and the target monitor return `None` if they are the same or
returning `Some(direction)` if not. This way now all commands that call
a move across monitor will use the logic to check from the direction if
it should add the container on front or end.

With these changes now all the code related to moving a window across
monitors using a command should be on one place only making sure that in
the future any change required only needs to be done on one place,
instead of having to do it on `move_container_to_monitor` and
`move_container_in_direction` as before!

This commit is an interactive squashed rebase of the following commits
from PR #1154:

8f4bc101bc
fix(wm): move floats in direction across monitors

This commit fixes an issue where when trying to move floating windows or
windows on a floating workspace across boundaries to another monitor
using the `move_container_in_direction` it wouldn't move the floating
windows physically, although it moved them internally on komorebi,
resulting in weird and wrong behavior.

This commit creates a new method on `Monitor` to
`add_container_with_direction` which takes a move direction and then
uses the same logic that was previously on the
`move_container_in_direction` function.

It changes the `move_container_to_monitor` function to take an optional
move direction which if it is some will have this function call the new
method `add_container_with_direction` instead of just `add_container`.
Lastly the `move_container_in_direction` function now when it realizes
the move will be across monitors simply calls the
`move_container_to_monitor` with the direction that was initially given
to it.

These changes require that all callers of `move_container_to_monitor`
add an direction option, instead of passing `None` on all of them, a new
helper function was created, named `direction_from_monitor_idx` which
calculates the direction a move will have from the currently focused
monitor and the target monitor return `None` if they are the same or
returning `Some(direction)` if not. This way now all commands that call
a move across monitor will use the logic to check from the direction if
it should add the container on front or end.

3b20e4b2fe
refactor(wm): use helper function on move to workspace

Use the same `add_container_with_direction` function on
`move_container_to_workspace` as it is being used on
`move_container_to_monitor` or `move_container_in_direction`.

This way we bring parity between all methods and make it easier to
change the way a container is added on a monitor workspace when taking
the move direction into consideration.
2024-11-30 10:57:15 -08:00
alex-ds13
01367f59e2 fix(client): add write-timeout to prevent blocking 2024-11-29 08:11:36 -08:00
alex-ds13
e6ddccc5f6 fix(wm): move ops on floating workspaces
83f222fe84
* fix(wm): correctly define moves across monitors

Moves within the same workspace were being considered as moves across
monitors when the workspace was floating (not tiled).

This commit fixes this by changing the way we first define if a move was
across monitor or not.

We now search for the moved window on all workspaces and check if its
monitor index is different from the target monitor index (the monitor
where the move ended).

a02694348e
* fix(wm): ignore moves/resizes on floating workspaces

This commit makes sure that moves or resizes within a floating workspace
(i.e. not tiled) will be ignored, unless the move is across monitors.
We don't care about the positions or sizes of windows within a floating
workspace!

4bf24f81e0
* fix(wm): avoid workspace load on cross monitor moves

This commit replaces the `window_manager.focus_workspace` call with a
`monitor.focus_workspace` which doesn't load the workspace. There is no
need to load the workspace when moving windows across monitors since
those workspaces will already be loaded, we simply need to update them.
Loading the workspace would cause some issues as well, like when moving
a window to a floating workspace which already contained a window that
matched some `floating_windows` rules was always putting the
"floating_window" on top of the window we just moved with a bunch of
focus flickering. This is fixed with this commit.

cb53f463ae
* fix(wm): avoid workspace load on command move across monitor

If the move happens between the already focused workspaces of two
monitors we shouldn't load the workspace, since it is already loaded and
it will cause changes on focused windows, which might result on the
window we just moved not being focused.
2024-11-29 02:57:32 -08:00
LGUG2Z
b22ec90438 refactor(clippy): apply rust 1.83.0 lints 2024-11-28 12:12:46 -08:00
CtByte
46b81e4372 feat(bar): floating center area for widgets
* Added a new floating area at the center of the bar
* Optional center widgets config, fixed spacing on the center widget
* Turning transparency on by default
2024-11-26 20:14:01 -08:00
alex-ds13
6f00c527a4 fix(wm): cross-monitor max floating window moves
When moving maximized floating windows across monitors they were
magically disappearing!

The window would be on the correct place, with the correct coordinates
and size, its styles wouldn't change it would still have the `VISIBLE`
style, however the window was invisible.

If we used the system move to try to move it sometimes we would be able
to see a bar on the top of the monitor and if we moved the window with
the keyboard on the direction of another monitor then the window would
start showing up on that monitor... So it was visible on that monitor
but not on the one we just moved it into.

After some investigation I decided to atribute that behavior to magic,
since I couldn't find any other plausible explanation, if someone knows
about this please tell me, I too would like to learn the ways of this
dark mysteries from the deep of the Windows OS.

On a serious note, this commit creates a workaround for this by simply
unmaximazing the window first (it's not restore, it doesn't change the
size) then it moves the window (if animations are enabled it proceeds to
wait for the animation to finish...), then it maximizes the window
again.
2024-11-26 14:34:57 -08:00
alex-ds13
3ad4090df8 fix(wm): resize float windows moved across monitors
Previously when moving floating windows across monitors we would keep
the size of the window as it was. For most cases this would be ok.

However for users with monitors with completely different sizes this
could result on a window that would fill across monitors when moving
from the bigger monitor to the smaller monitor.

This commit, attempts to resize the windows proportionally to the
monitors' sizes.

There is currently a slight issue with some apps (so far I've only
noticed it on 'Wezterm'...) where if the DPIs across monitors are
different they don't seem to fully get the OS DPI change completely, but
it seems that setting the `Wezterm` compatibility high DPI scaling
override to "System" on the app's executable properties, fixes the
issue.

Since this is only 1 app (so far...) and only when the scales between
monitors are different I decided to commit this anyway.

This will do more good than harm, since in the cases it was misbehaving
with 'Wezterm' the result would be a wrongly resized window that is
still completely visible on the target monitor anyway and the override
fix seems to be good so far.
2024-11-26 14:34:17 -08:00
thearturca
449ccac645 refactor(animation): new animations engine
This commit is comprised of the following interactively rebased commits
from PR #1002 by @thearturca.

1a184a4442
refactor(animation): move animations to its own mod

First step for more rusty version animations. The goal is to make
animations more generic so its easier to add new animations to komorebi!

d3ac6b72c2
refactor(animation): reduce mutex calls on `ANIMATION_STYLE`

8a42b738fe
refactor(animation): introduce `Lerp` trait

e449861c10
refactor(animation): generalized ANIMATION_MANAGER

Instead of a isize key for the ANIMATION_MANAGER HashMap, now we use a
String key. For window move animation, the key would be
`window_move:{hwnd}`.

This allows us to use single manager for more types of animations.

67b2a7a284
feat(animation): introduce `AnimationPrefix` enum

8290f143a6
feat(animation): introduce `RenderDispatcher` trait

2400d757fe
feat(animation): implement window transparency animation

This commit also fixes graceful shutdown of animations by disabling them
before exit and wait for all remaining animations for 20 seconds.

44189d8382
refactor(animation): move generation of `animation key` to `RenderDispatcher`

e502cb3ffb
refactor(animation): rename `animation` mod to `engine`

Linter was upset about this:
> error: module has the same name as its containing module

369107f5e0
feat(config): adds per animation configuration options

Originally static config only allowed global config for animations.

Since this refactor introduces the abilty to add more type of
animations, this change allows us to configure `enabled`, `duration` and
`style` state per animation type.

Now each of them take either the raw value or a JSON object where keys
are the animation types and values are desired config value. Also adds
support for per animation configuration for komorebic commands.
2024-11-25 20:42:56 -08:00
dependabot[bot]
1d00196240 chore(deps): bump serde from 1.0.214 to 1.0.215
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.214 to 1.0.215.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.214...v1.0.215)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:34 -08:00
dependabot[bot]
639ebd0b3d chore(deps): bump serde_json from 1.0.132 to 1.0.133
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.132 to 1.0.133.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.132...v1.0.133)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:27 -08:00
dependabot[bot]
e22eafbc8e chore(deps): bump clap from 4.5.20 to 4.5.21
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.20...clap_complete-v4.5.21)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:27:19 -08:00
dependabot[bot]
46c2ad512b chore(deps): bump rustls from 0.23.16 to 0.23.18
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.16 to 0.23.18.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.16...v/0.23.18)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 15:26:59 -08:00
LGUG2Z
3de96609bb feat(bar): support floating window title updates 2024-11-25 15:08:08 -08:00
LGUG2Z
9c09284b0f fix(subscriptions): add override for title updates 2024-11-25 14:48:36 -08:00
CtByte
b14c0d07a2 fix(bar): correct network widget icon colour 2024-11-23 13:54:36 -08:00
alex-ds13
3615451f41 fix(wm): disallow focusing other windows when there is a maximized_window 2024-11-21 12:06:54 -08:00
alex-ds13
6893f39dd9 fix(wm): focus maximized windows when moving focus across monitors
There was an issue where if you changed focus across monitors and the
target monitor had a maximized window it would focus one of the
containers beneath it instead. And if there were no containers it
wouldn't focus anything, but instead keep focus on the previous monitor.
This commit fixes that issue.
2024-11-21 12:06:54 -08:00
52 changed files with 4306 additions and 1805 deletions

View File

@@ -199,7 +199,7 @@ jobs:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
- uses: vedantmgoyal2009/winget-releaser@main
with:
identifier: LGUG2Z.komorebi
token: ${{ secrets.WINGET_TOKEN }}

500
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -367,7 +367,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.31"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.31/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.31/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -31,7 +31,6 @@ serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

View File

@@ -18,6 +18,8 @@ use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::Align2;
use eframe::egui::Area;
use eframe::egui::CentralPanel;
use eframe::egui::Color32;
use eframe::egui::Context;
@@ -26,6 +28,7 @@ use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Id;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::Rgba;
@@ -50,6 +53,7 @@ pub struct Komobar {
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
@@ -290,6 +294,16 @@ impl Komobar {
}
}
if let Some(center_widgets) = &config.center_widgets {
for (idx, widget_config) in center_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Alignment::Center);
}
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
@@ -301,12 +315,23 @@ impl Komobar {
let mut left_widgets = config
.left_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut center_widgets = match &config.center_widgets {
Some(center_widgets) => center_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>(),
None => vec![],
};
let mut right_widgets = config
.right_widgets
.iter()
.filter(|config| config.enabled())
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
@@ -329,6 +354,7 @@ impl Komobar {
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Alignment::Left => left_widgets[idx] = boxed,
Alignment::Center => center_widgets[idx] = boxed,
Alignment::Right => right_widgets[idx] = boxed,
}
}
@@ -336,6 +362,7 @@ impl Komobar {
right_widgets.reverse();
self.left_widgets = left_widgets;
self.center_widgets = center_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
@@ -354,6 +381,7 @@ impl Komobar {
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
left_widgets: vec![],
center_widgets: vec![],
right_widgets: vec![],
rx_gui,
rx_config,
@@ -495,7 +523,27 @@ impl eframe::App for Komobar {
w.render(ctx, ui, &mut render_conf);
}
});
})
});
if !self.center_widgets.is_empty() {
// Floating center widgets
Area::new(Id::new("center_panel"))
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
.show(ctx, |ui| {
Frame::none().show(ui, |ui| {
ui.horizontal_centered(|ui| {
let mut render_conf = *render_config;
render_conf.alignment = Some(Alignment::Center);
render_config.apply_on_alignment(ui, |ui| {
for w in &mut self.center_widgets {
w.render(ctx, ui, &mut render_conf);
}
});
});
});
});
}
})
})
});
@@ -505,5 +553,6 @@ impl eframe::App for Komobar {
#[derive(Copy, Clone)]
pub enum Alignment {
Left,
Center,
Right,
}

View File

@@ -30,37 +30,18 @@ pub struct BatteryConfig {
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
manager: Manager::new().unwrap(),
last_state: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
state: BatteryState::Discharging,
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}

View File

@@ -37,6 +37,8 @@ pub struct KomobarConfig {
pub grouping: Option<Grouping>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
@@ -185,3 +187,13 @@ pub enum LabelPrefix {
/// Show an icon and text
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum DisplayFormat {
/// Show only icon
Icon,
/// Show only text
Text,
/// Show both icon and text
IconAndText,
}

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -30,17 +30,18 @@ pub struct CpuConfig {
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
system: System::new_with_specifics(
RefreshKind::default().without_memory().without_processes(),
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -99,13 +100,9 @@ impl BarWidget for Cpu {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -119,13 +119,14 @@ impl BarWidget for Date {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false),
)
})
.clicked()
{
self.format.next()

View File

@@ -1,38 +1,45 @@
use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::komorebi_layout::KomorebiLayout;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::ICON_CACHE;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::text::LayoutJob;
use eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Margin;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::Container;
use komorebi_client::NotificationEvent;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::Window;
use komorebi_client::Workspace;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
@@ -55,20 +62,28 @@ pub struct KomorebiWorkspacesConfig {
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
pub show_icon: Option<bool>,
/// Display format of the currently focused window
pub display: Option<DisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -102,12 +117,12 @@ impl From<&KomorebiConfig> for Komorebi {
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: (vec![], vec![], 0),
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
stack_accent: None,
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
})),
workspaces: value.workspaces,
layout: value.layout,
layout: value.layout.clone(),
focused_window: value.focused_window,
configuration_switcher,
}
@@ -130,120 +145,153 @@ impl BarWidget for Komorebi {
if self.workspaces.enable {
let mut update = None;
// NOTE: There should always be at least one workspace if the bar is connected to komorebi.
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, should_show)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if *should_show
&& ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
if !komorebi_notification_state.workspaces.is_empty() {
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, container_information)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
update = Some(ws.to_string());
let mut proceed = true;
if SelectableFrame::new(
komorebi_notification_state.selected_workspace.eq(ws),
)
.show(ui, |ui| {
let mut has_icon = false;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.is_err()
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
let icons: Vec<_> =
container_information.icons.iter().flatten().collect();
if !icons.is_empty() {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
for icon in icons {
ui.add(
Image::from(&img_to_texture(ctx, icon))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
);
if !has_icon {
has_icon = true;
}
}
});
}
}
// draw a custom icon when there is no app icon
if match format {
DisplayFormat::Icon => !has_icon,
_ => false,
} {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let (response, painter) =
ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover());
let stroke =
Stroke::new(1.0, ctx.style().visuals.selection.stroke.color);
let mut rect = response.rect;
let rounding = Rounding::same(rect.width() * 0.1);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
response.on_hover_text(ws.to_string())
} else if match format {
DisplayFormat::Icon => has_icon,
_ => false,
} {
ui.response().on_hover_text(ws.to_string())
} else {
ui.add(Label::new(ws.to_string()).selectable(false))
}
})
.clicked()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
update = Some(ws.to_string());
let mut proceed = true;
if proceed
&& komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusWorkspaceNumber"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(
&SocketMessage::RetileWithResizeDimensions,
)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
if proceed
&& komorebi_client::send_message(
&SocketMessage::FocusMonitorWorkspaceNumber(
komorebi_notification_state.monitor_index,
i,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusWorkspaceNumber"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
proceed = false;
}
if proceed
&& komorebi_client::send_message(
&SocketMessage::RetileWithResizeDimensions,
)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
}
}
}
}
});
});
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
}
if let Some(layout) = self.layout {
if layout.enable {
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(komorebi_notification_state.layout.to_string())
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
match komorebi_notification_state.layout {
KomorebiLayout::Default(_) => {
if komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: CycleLayout"
);
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling)
.is_err()
{
tracing::error!(
"could not send message to komorebi: ToggleTiling"
);
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause)
.is_err()
{
tracing::error!(
"could not send message to komorebi: TogglePause"
);
}
}
KomorebiLayout::Custom => {}
}
}
});
if let Some(layout_config) = &self.layout {
if layout_config.enable {
let workspace_idx: Option<usize> = komorebi_notification_state
.workspaces
.iter()
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
komorebi_notification_state.layout.show(
ctx,
ui,
config,
layout_config,
workspace_idx,
);
}
}
@@ -252,9 +300,10 @@ impl BarWidget for Komorebi {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file() {
config.apply_on_widget(true, ui,|ui|{
if ui
.add(Label::new(name).selectable(false).sense(Sense::click()))
config.apply_on_widget(false, ui,|ui|{
if SelectableFrame::new(false).show(ui, |ui|{
ui.add(Label::new(name).selectable(false))
})
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
@@ -307,112 +356,104 @@ impl BarWidget for Komorebi {
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state.focused_container_information.0;
let titles = &komorebi_notification_state
.focused_container_information
.titles;
if !titles.is_empty() {
config.apply_on_widget(true, ui, |ui| {
let icons = &komorebi_notification_state.focused_container_information.1;
let focused_window_idx =
komorebi_notification_state.focused_container_information.2;
config.apply_on_widget(false, ui, |ui| {
let icons = &komorebi_notification_state
.focused_container_information
.icons;
let focused_window_idx = komorebi_notification_state
.focused_container_information
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
let len = iter.len();
for (i, (title, icon)) in iter.enumerate() {
if focused_window.show_icon {
if let Some(img) = icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
let selected = i == focused_window_idx && len != 1;
if SelectableFrame::new(selected)
.show(ui, |ui| {
// handle legacy setting
let format = focused_window.display.unwrap_or(
if focused_window.show_icon.unwrap_or(false) {
DisplayFormat::IconAndText
} else {
DisplayFormat::Text
},
);
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
{
if let Some(img) = icon {
Frame::none()
.inner_margin(Margin::same(
ui.style().spacing.button_padding.y,
))
.show(ui, |ui| {
let response = ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.shrink_to_fit(),
);
if let DisplayFormat::Icon = format {
response.on_hover_text(title);
}
});
}
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
{
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
})
.clicked()
{
if selected {
return;
}
}
if i == focused_window_idx {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let layout_job = LayoutJob::simple(
title.to_string(),
font_id.clone(),
komorebi_notification_state
.stack_accent
.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
if titles.len() > 1 {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
);
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
if komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(false),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(
&SocketMessage::FocusStackWindow(i),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(
i,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(
&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
@@ -432,9 +473,9 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<(String, bool)>,
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
pub selected_workspace: String,
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
pub focused_container_information: KomorebiNotificationStateContainerInformation,
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
@@ -443,25 +484,6 @@ pub struct KomorebiNotificationState {
pub monitor_index: usize,
}
#[derive(Copy, Clone, Debug)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Floating,
Paused,
Custom,
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiNotificationState {
pub fn update_from_config(&mut self, config: &Self) {
self.hide_empty_workspaces = config.hide_empty_workspaces;
@@ -526,64 +548,142 @@ impl KomorebiNotificationState {
true
};
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
should_show,
));
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
ws.into(),
));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
if !*monitor.workspaces()[focused_workspace_idx].tile() {
if monitor.workspaces()[focused_workspace_idx]
.monocle_container()
.is_some()
{
self.layout = KomorebiLayout::Monocle;
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
}
if notification.state.is_paused {
} else if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
} else {
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => {
KomorebiLayout::Default(*layout)
}
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
}
if let Some(container) =
monitor.workspaces()[focused_workspace_idx].monocle_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else if let Some(container) =
monitor.workspaces()[focused_workspace_idx].focused_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else {
self.focused_container_information.0.clear();
self.focused_container_information.1.clear();
self.focused_container_information.2 = 0;
}
self.focused_container_information =
(&monitor.workspaces()[focused_workspace_idx]).into();
}
}
}
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationStateContainerInformation {
pub titles: Vec<String>,
pub icons: Vec<Option<RgbaImage>>,
pub focused_window_idx: usize,
}
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
fn from(value: &Workspace) -> Self {
let mut container_info = Self::EMPTY;
if let Some(container) = value.monocle_container() {
container_info = container.into();
} else if let Some(container) = value.focused_container() {
container_info = container.into();
}
for floating_window in value.floating_windows() {
if floating_window.is_focused() {
container_info = floating_window.into();
}
}
container_info
}
}
impl From<&Container> for KomorebiNotificationStateContainerInformation {
fn from(value: &Container) -> Self {
let windows = value.windows().iter().collect::<Vec<_>>();
let mut icons = vec![];
for window in windows {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let exe = window.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
}
Self {
titles: value
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
icons,
focused_window_idx: value.focused_window_idx(),
}
}
}
impl From<&Window> for KomorebiNotificationStateContainerInformation {
fn from(value: &Window) -> Self {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let mut icons = vec![];
let exe = value.exe().unwrap_or_default();
match icon_cache.get(&exe) {
None => {
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
update_cache = true;
}
Some(icon) => {
icons.push(Some(icon.clone()));
}
}
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
}
}
Self {
titles: vec![value.title().unwrap_or_default()],
icons,
focused_window_idx: 0,
}
}
}
impl KomorebiNotificationStateContainerInformation {
pub const EMPTY: Self = Self {
titles: vec![],
icons: vec![],
focused_window_idx: 0,
};
}

View File

@@ -0,0 +1,311 @@
use crate::config::DisplayFormat;
use crate::komorebi::KomorebiLayoutConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use eframe::egui::vec2;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Label;
use eframe::egui::Rounding;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Monocle,
Floating,
Paused,
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiLayout {
fn is_default(&mut self) -> bool {
matches!(self, KomorebiLayout::Default(_))
}
fn on_click(
&mut self,
show_options: &bool,
monitor_idx: usize,
workspace_idx: Option<usize>,
) -> bool {
if self.is_default() {
!show_options
} else {
self.on_click_option(monitor_idx, workspace_idx);
false
}
}
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx {
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
// paint custom icons for the layout
let size = Vec2::splat(font_id.size);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let color = ctx.style().visuals.selection.stroke.color;
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let rounding = Rounding::same(rect.width() * 0.1);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke);
match self {
KomorebiLayout::Default(layout) => match layout {
komorebi_client::DefaultLayout::BSP => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Columns => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Rows => {
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
}
komorebi_client::DefaultLayout::VerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
}
komorebi_client::DefaultLayout::RightMainVerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(r, 0.0), c], stroke);
}
komorebi_client::DefaultLayout::HorizontalStack => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c, c + vec2(0.0, r)], stroke);
}
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Grid => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.5);
rect_left.set_height(rect.height() * 0.5);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.1 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.35 + stroke.width,
rect.width() * 0.35 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_stroke(rect_right, rounding, stroke);
}
KomorebiLayout::Paused => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.25);
rect_left.set_height(rect.height() * 0.8);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.2 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.55 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_filled(rect_right, rounding, color);
}
KomorebiLayout::Custom => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
}
}
}
pub fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
render_config: &mut RenderConfig,
layout_config: &KomorebiLayoutConfig,
workspace_idx: Option<usize>,
) {
let monitor_idx = render_config.monitor_idx;
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
if !self.is_default() {
show_options = false;
}
render_config.apply_on_widget(false, ui, |ui| {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
ui.add(Label::new(self.to_string()).selectable(false));
}
})
.on_hover_text(self.to_string());
if layout_frame.clicked() {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options {
if let Some(workspace_idx) = workspace_idx {
Frame::none().show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::HorizontalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
if SelectableFrame::new(self == layout_option)
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
}
});
RenderConfig::store_show_komorebi_layout_options(show_options);
}
}

View File

@@ -4,10 +4,12 @@ mod config;
mod cpu;
mod date;
mod komorebi;
mod komorebi_layout;
mod media;
mod memory;
mod network;
mod render;
mod selected_frame;
mod storage;
mod time;
mod ui;
@@ -22,9 +24,11 @@ use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::collections::HashMap;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
@@ -32,6 +36,8 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
@@ -51,6 +57,9 @@ pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
@@ -269,7 +278,7 @@ fn main() -> color_eyre::Result<()> {
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
.with_transparent(config.transparency_alpha.is_some())
.with_transparent(true)
.with_taskbar(false);
let native_options = eframe::NativeOptions {

View File

@@ -1,4 +1,5 @@
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
@@ -6,7 +7,6 @@ use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -102,21 +102,20 @@ impl BarWidget for Media {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
)
})
.clicked()
{
self.toggle();

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -30,17 +30,18 @@ pub struct MemoryConfig {
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
system: System::new_with_specifics(
RefreshKind::default().without_cpu().without_processes(),
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -102,13 +103,9 @@ impl BarWidget for Memory {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) =

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -27,6 +27,8 @@ pub struct NetworkConfig {
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
@@ -37,130 +39,40 @@ pub struct NetworkConfig {
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(match prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(match prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::IconAndText => format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
})
}
}
}
}
}
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
@@ -174,214 +86,264 @@ impl Network {
}
}
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::IconAndText => {
format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
)
}
})
));
}
if self.show_total_activity {
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1),
Self::to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
}
}
}
self.last_state_network_activity.clone_from(&outputs);
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
outputs
(activity, total_activity)
}
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s | ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"{: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("{} | ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s | ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"UP: {: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},
};
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
let icon_format =
TextFormat::simple(font_id.clone(), ctx.style().visuals.selection.stroke.color);
let text_format = TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color());
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
// icon
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
icon_format.font_id.clone(),
icon_format.color,
100.0,
);
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
// text
layout_job.append(
&text_down,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
// icon
layout_job.append(
&match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
0.0,
icon_format.clone(),
);
// text
layout_job.append(
&text_up,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
Label::new(layout_job).selectable(false)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
outputs
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(output).selectable(false));
});
}
}
if self.show_network_activity {
for output in self.network_activity() {
config.apply_on_widget(true, ui, |ui| {
ui.add(Label::new(output).selectable(false));
});
}
}
if self.enable {
self.default_interface();
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
if self.show_total_activity {
for reading in total_activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
if self.show_activity {
for reading in activity {
config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading));
});
}
});
}
}
if self.show_default_interface {
self.default_interface();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
});
}
}
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct NetworkReading {
pub format: NetworkReadingFormat,
pub received_text: String,
pub transmitted_text: String,
}
impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
NetworkReading {
format,
received_text: received,
transmitted_text: transmitted,
}
}
}
@@ -404,22 +366,3 @@ impl fmt::Display for DataUnit {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

View File

@@ -11,6 +11,10 @@ use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
@@ -27,6 +31,8 @@ pub enum Grouping {
#[derive(Copy, Clone)]
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
/// Spacing between widgets
pub spacing: f32,
/// Sets how widgets are grouped
@@ -48,6 +54,7 @@ pub trait RenderExt {
impl RenderExt for &KomobarConfig {
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
RenderConfig {
monitor_idx: self.monitor.index,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
@@ -59,8 +66,17 @@ impl RenderExt for &KomobarConfig {
}
impl RenderConfig {
pub fn load_show_komorebi_layout_options() -> bool {
SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0
}
pub fn store_show_komorebi_layout_options(show: bool) {
SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst);
}
pub fn new() -> Self {
Self {
monitor_idx: 0,
spacing: 0.0,
grouping: Grouping::None,
background_color: Color32::BLACK,
@@ -162,12 +178,60 @@ impl RenderConfig {
Some(style) => match style {
// new styles can be added if needed here
GroupingStyle::Default => Shadow::NONE,
GroupingStyle::DefaultWithShadow => Shadow {
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
blur: 4.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
blur: 4.0,
offset: Vec2::new(0.0, 0.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 3.0,
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
blur: 3.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui
.style()
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
blur: 3.0,
offset: Vec2::new(0.0, 0.0),
spread: 2.0,
color: ui
.style()
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
blur: 0.0,
offset: Vec2::new(1.0, 1.0),
spread: 2.0,
color: ui
.style()
.visuals
.selection
.stroke
.color
.try_apply_alpha(config.transparency_alpha),
},
},
None => Shadow::NONE,
})
@@ -190,6 +254,7 @@ impl RenderConfig {
left: match self.alignment {
Some(align) => match align {
Alignment::Left => spacing,
Alignment::Center => spacing,
Alignment::Right => 0.0,
},
None => 0.0,
@@ -197,6 +262,7 @@ impl RenderConfig {
right: match self.alignment {
Some(align) => match align {
Alignment::Left => 0.0,
Alignment::Center => 0.0,
Alignment::Right => spacing,
},
None => 0.0,
@@ -221,9 +287,20 @@ pub struct GroupingConfig {
pub enum GroupingStyle {
#[serde(alias = "CtByte")]
Default,
/// A black shadow is added under the default group
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
#[serde(alias = "CtByteWithShadow")]
DefaultWithShadow,
#[serde(alias = "DefaultWithShadow")]
DefaultWithShadowB4O1S3,
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
DefaultWithShadowB4O0S3,
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
DefaultWithShadowB0O1S3,
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
DefaultWithGlowB3O1S2,
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
DefaultWithGlowB3O0S2,
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
DefaultWithGlowB0O1S2,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]

View File

@@ -0,0 +1,55 @@
use eframe::egui::Frame;
use eframe::egui::Margin;
use eframe::egui::Response;
use eframe::egui::Sense;
use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content
pub struct SelectableFrame {
selected: bool,
}
impl SelectableFrame {
pub fn new(selected: bool) -> Self {
Self { selected }
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self;
Frame::none()
.show(ui, |ui| {
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
if ui.is_rect_visible(response.rect) {
let inner_margin = Margin::symmetric(
ui.style().spacing.button_padding.x,
ui.style().spacing.button_padding.y,
);
if selected
|| response.hovered()
|| response.highlighted()
|| response.has_focus()
{
let visuals = ui.style().interact_selectable(&response, selected);
Frame::none()
.stroke(visuals.bg_stroke)
.rounding(visuals.rounding)
.fill(visuals.bg_fill)
.inner_margin(inner_margin)
.show(ui, add_contents);
} else {
Frame::none()
.inner_margin(inner_margin)
.show(ui, add_contents);
}
}
response
})
.inner
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
}
}

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -107,13 +107,9 @@ impl BarWidget for Storage {
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe")

View File

@@ -1,11 +1,11 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
@@ -110,13 +110,9 @@ impl BarWidget for Time {
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
config.apply_on_widget(true, ui, |ui| {
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
self.format.toggle()

View File

@@ -54,4 +54,35 @@ impl WidgetConfig {
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
}
}
pub fn enabled(&self) -> bool {
match self {
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.enable
|| (if let Some(layout) = &config.layout {
layout.enable
} else {
false
})
|| (if let Some(focused_window) = &config.focused_window {
focused_window.enable
} else {
false
})
|| (if let Some(configuration_switcher) = &config.configuration_switcher {
configuration_switcher.enable
} else {
false
})
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
}
}
}

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
@@ -54,6 +55,7 @@ use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::Shutdown;
use std::time::Duration;
pub use uds_windows::UnixListener;
use uds_windows::UnixStream;
@@ -62,6 +64,7 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
@@ -69,6 +72,8 @@ pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_read_timeout(Some(Duration::from_secs(1)))?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
stream.shutdown(Shutdown::Write)?;

View File

@@ -42,7 +42,6 @@ tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
uds_windows = { workspace = true }
which = { workspace = true }
widestring = "1"
win32-display-data = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }

View File

@@ -0,0 +1,115 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use super::prefix::AnimationPrefix;
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<String, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, animation_key: &str) -> bool {
if let Some(animation_state) = self.animations.get(animation_key) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, animation_key: &str) {
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
return;
}
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, animation_key: &str) {
if let Some(animation_state) = self.animations.get_mut(animation_key) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(animation_key);
}
}
}
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
self.animations
.keys()
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
.count()
}
pub fn count(&self) -> usize {
self.animations.len()
}
}

View File

@@ -0,0 +1,122 @@
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct AnimationEngine;
impl AnimationEngine {
pub fn wait_for_all_animations() {
let max_duration = Duration::from_secs(20);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().count() > 0 {
if spent_duration.elapsed() >= max_duration {
break;
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
));
}
}
/// Returns true if the animation needs to continue
pub fn cancel(animation_key: &str) -> bool {
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
let max_duration = Duration::from_secs(5);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(animation_key);
}
std::thread::sleep(Duration::from_millis(250 / 2));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
ANIMATION_MANAGER.lock().end_cancel(animation_key);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration,
) -> Result<()> {
std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
let should_animate = Self::cancel(animation_key.as_str());
if !should_animate {
return Ok(());
}
}
render_dispatcher.pre_render()?;
ANIMATION_MANAGER.lock().start(animation_key.as_str());
let target_frame_time =
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER
.lock()
.is_cancelled(animation_key.as_str())
{
// cancel animation
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress =
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_dispatcher.render(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(animation_key.as_str());
// limit progress to 1.0 if animation took longer
if progress != 1.0 {
progress = 1.0;
// process animation for 1.0 to set target position
render_dispatcher.render(progress).ok();
}
render_dispatcher.post_render()
});
Ok(())
}
}

View File

@@ -0,0 +1,42 @@
use crate::core::Rect;
use crate::AnimationStyle;
use super::style::apply_ease_func;
pub trait Lerp<T = Self> {
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
}
impl Lerp for i32 {
#[allow(clippy::cast_possible_truncation)]
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
let time = apply_ease_func(time, style);
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
}
}
impl Lerp for f64 {
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
let time = apply_ease_func(time, style);
(end - self).mul_add(time, self)
}
}
impl Lerp for u8 {
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
(self as f64).lerp(end as f64, time, style) as u8
}
}
impl Lerp for Rect {
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
Rect {
left: self.left.lerp(end.left, time, style),
top: self.top.lerp(end.top, time, style),
right: self.right.lerp(end.right, time, style),
bottom: self.bottom.lerp(end.bottom, time, style),
}
}
}

View File

@@ -0,0 +1,54 @@
use crate::animation::animation_manager::AnimationManager;
use crate::core::animation::AnimationStyle;
use lazy_static::lazy_static;
use prefix::AnimationPrefix;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use parking_lot::Mutex;
pub use engine::AnimationEngine;
pub mod animation_manager;
pub mod engine;
pub mod lerp;
pub mod prefix;
pub mod render_dispatcher;
pub use render_dispatcher::RenderDispatcher;
pub mod style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),
Global(T),
}
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
lazy_static! {
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);

View File

@@ -0,0 +1,31 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AnimationPrefix {
Movement,
Transparency,
}
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{}:{}", prefix, key)
}

View File

@@ -0,0 +1,8 @@
use color_eyre::Result;
pub trait RenderDispatcher {
fn get_animation_key(&self) -> String;
fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> Result<()>;
}

View File

@@ -1,22 +1,6 @@
use crate::core::AnimationStyle;
use crate::core::Rect;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::f64::consts::PI;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_MANAGER;
use crate::ANIMATION_STYLE;
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
pub trait Ease {
fn evaluate(t: f64) -> f64;
@@ -370,9 +354,8 @@ impl Ease for EaseInOutBounce {
}
}
}
fn apply_ease_func(t: f64) -> f64 {
let style = *ANIMATION_STYLE.lock();
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
match style {
AnimationStyle::Linear => Linear::evaluate(t),
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
@@ -406,112 +389,3 @@ fn apply_ease_func(t: f64) -> f64 {
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Animation {
pub hwnd: isize,
}
impl Animation {
pub fn new(hwnd: isize) -> Self {
Self { hwnd }
}
/// Returns true if the animation needs to continue
pub fn cancel(&mut self) -> bool {
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
return true;
}
// should be more than 0
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
let max_duration = Duration::from_secs(1);
let spent_duration = Instant::now();
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
if spent_duration.elapsed() >= max_duration {
ANIMATION_MANAGER.lock().end(self.hwnd);
}
std::thread::sleep(Duration::from_millis(
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
));
}
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
latest_cancel_idx == cancel_idx
}
#[allow(clippy::cast_possible_truncation)]
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
let time = apply_ease_func(t);
f64::from(end - start)
.mul_add(time, f64::from(start))
.round() as i32
}
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
Rect {
left: Self::lerp(start_rect.left, end_rect.left, t),
top: Self::lerp(start_rect.top, end_rect.top, t),
right: Self::lerp(start_rect.right, end_rect.right, t),
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
}
}
#[allow(clippy::cast_precision_loss)]
pub fn animate(
&mut self,
duration: Duration,
mut render_callback: impl FnMut(f64) -> Result<()>,
) -> Result<()> {
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
let should_animate = self.cancel();
if !should_animate {
return Ok(());
}
}
ANIMATION_MANAGER.lock().start(self.hwnd);
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
let mut progress = 0.0;
let animation_start = Instant::now();
// start animation
while progress < 1.0 {
// check if animation is cancelled
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
// cancel animation
ANIMATION_MANAGER.lock().cancel(self.hwnd);
return Ok(());
}
let frame_start = Instant::now();
// calculate progress
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
render_callback(progress).ok();
// sleep until next frame
let frame_time_elapsed = frame_start.elapsed();
if frame_time_elapsed < target_frame_time {
std::thread::sleep(target_frame_time - frame_time_elapsed);
}
}
ANIMATION_MANAGER.lock().end(self.hwnd);
// limit progress to 1.0 if animation took longer
if progress > 1.0 {
progress = 1.0;
}
// process animation for 1.0 to set target position
render_callback(progress)
}
}

View File

@@ -1,108 +0,0 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy)]
struct AnimationState {
pub in_progress: bool,
pub cancel_idx_counter: usize,
pub pending_cancel_count: usize,
}
#[derive(Debug)]
pub struct AnimationManager {
animations: HashMap<isize, AnimationState>,
}
impl Default for AnimationManager {
fn default() -> Self {
Self::new()
}
}
impl AnimationManager {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
}
}
pub fn is_cancelled(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.pending_cancel_count > 0
} else {
false
}
}
pub fn in_progress(&self, hwnd: isize) -> bool {
if let Some(animation_state) = self.animations.get(&hwnd) {
animation_state.in_progress
} else {
false
}
}
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count += 1;
animation_state.cancel_idx_counter += 1;
// return cancel idx
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.cancel_idx_counter
} else {
0
}
}
pub fn end_cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.pending_cancel_count -= 1;
}
}
pub fn cancel(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
}
}
pub fn start(&mut self, hwnd: isize) {
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
e.insert(AnimationState {
in_progress: true,
cancel_idx_counter: 0,
pending_cancel_count: 0,
});
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
return;
}
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = true;
}
}
pub fn end(&mut self, hwnd: isize) {
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
animation_state.in_progress = false;
if animation_state.pending_cancel_count == 0 {
self.animations.remove(&hwnd);
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
}
}
}
}

View File

@@ -5,16 +5,18 @@ use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::RENDER_TARGETS;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use color_eyre::eyre::anyhow;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Foundation::Numerics::Matrix3x2;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::FALSE;
@@ -30,6 +32,8 @@ use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
@@ -43,22 +47,26 @@ use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::BeginPaint;
use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::EndPaint;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SIZE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::PCWSTR;
@@ -89,14 +97,36 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Border {
pub hwnd: isize,
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
pub tracking_hwnd: isize,
pub window_rect: Rect,
pub window_kind: WindowKind,
pub style: BorderStyle,
pub width: i32,
pub offset: i32,
pub brush_properties: D2D1_BRUSH_PROPERTIES,
pub rounded_rect: D2D1_ROUNDED_RECT,
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
}
impl From<isize> for Border {
fn from(value: isize) -> Self {
Self { hwnd: value }
Self {
hwnd: value,
render_target: OnceLock::new(),
tracking_hwnd: 0,
window_rect: Rect::default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
rounded_rect: D2D1_ROUNDED_RECT::default(),
brushes: HashMap::new(),
}
}
}
@@ -105,7 +135,7 @@ impl Border {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str) -> color_eyre::Result<Self> {
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
@@ -121,12 +151,30 @@ impl Border {
let _ = WindowsApi::register_class_w(&window_class);
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
let (border_sender, border_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
std::thread::spawn(move || -> color_eyre::Result<()> {
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
hwnd_sender.send(hwnd)?;
let mut border = Self {
hwnd: 0,
render_target: OnceLock::new(),
tracking_hwnd,
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
window_kind: WindowKind::Unfocused,
style: STYLE.load(),
width: BORDER_WIDTH.load(Ordering::Relaxed),
offset: BORDER_OFFSET.load(Ordering::Relaxed),
brush_properties: Default::default(),
rounded_rect: Default::default(),
brushes: HashMap::new(),
};
let border_pointer = std::ptr::addr_of!(border);
let hwnd =
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
border.hwnd = hwnd;
border_sender.send(border_pointer as isize)?;
let mut msg: MSG = MSG::default();
@@ -145,8 +193,8 @@ impl Border {
Ok(())
});
let hwnd = hwnd_receiver.recv()?;
let border = Self { hwnd };
let border_ref = border_receiver.recv()?;
let border = unsafe { &mut *(border_ref as *mut Border) };
// I have literally no idea, apparently this is to get rid of the black pixels
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
@@ -167,7 +215,7 @@ impl Border {
}
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(hwnd)),
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
};
@@ -188,10 +236,47 @@ impl Border {
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} {
Ok(render_target) => unsafe {
border.brush_properties = *BRUSH_PROPERTIES.deref();
for window_kind in [
WindowKind::Single,
WindowKind::Stack,
WindowKind::Monocle,
WindowKind::Unfocused,
WindowKind::Floating,
] {
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
r: ((color & 0xFF) as f32) / 255.0,
g: (((color >> 8) & 0xFF) as f32) / 255.0,
b: (((color >> 16) & 0xFF) as f32) / 255.0,
a: 1.0,
};
if let Ok(brush) =
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
{
border.brushes.insert(window_kind, brush);
}
}
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
if border.render_target.set(render_target.clone()).is_err() {
return Err(anyhow!("could not store border render target"));
}
border.rounded_rect = {
let radius = 8.0 + border.width as f32 / 2.0;
D2D1_ROUNDED_RECT {
rect: Default::default(),
radiusX: radius,
radiusY: radius,
}
};
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(hwnd, render_target);
Ok(border)
render_targets.insert(border.hwnd, render_target);
Ok(border.clone())
},
Err(error) => Err(error.into()),
}
@@ -203,27 +288,17 @@ impl Border {
WindowsApi::close_window(self.hwnd)
}
pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> {
// Make adjustments to the border
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
let mut rect = *rect;
rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed));
rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed));
rect.add_margin(self.width);
rect.add_padding(-self.offset);
// Update the position of the border if required
// This effectively handles WM_MOVE
// Also if I remove this no borders render at all lol
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
}
// Invalidate the rect to trigger the callback to update colours etc.
if should_invalidate {
self.invalidate();
}
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
Ok(())
}
// this triggers WM_PAINT in the callback below
pub fn invalidate(&self) {
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
}
@@ -236,50 +311,73 @@ impl Border {
) -> LRESULT {
unsafe {
match message {
WM_SIZE | WM_PAINT => {
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
let render_targets = RENDER_TARGETS.lock();
if let Some(render_target) = render_targets.get(&(window.0 as isize)) {
let pixel_size = D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
};
WM_CREATE => {
let mut border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
if border_pointer.is_null() {
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
border_pointer = (*create_struct).lpCreateParams as *mut _;
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
}
let rect = D2D_RECT_F {
LRESULT(0)
}
EVENT_OBJECT_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
// we don't actually want to destroy the window here, just hide it for quicker
// visual feedback to the user; the actual destruction will be handled by the
// core border manager loop
WindowsApi::hide_window(window.0 as isize);
LRESULT(0)
}
EVENT_OBJECT_LOCATIONCHANGE => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
let reference_hwnd = lparam.0;
let old_rect = (*border_pointer).window_rect;
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
(*border_pointer).window_rect = rect;
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
tracing::error!("failed to update border position {error}");
}
if !rect.is_same_size_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.get() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&pixel_size);
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
// Get window kind and color
let window_kind = FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused);
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
r: ((color & 0xFF) as f32) / 255.0,
g: (((color >> 8) & 0xFF) as f32) / 255.0,
b: (((color >> 16) & 0xFF) as f32) / 255.0,
a: 1.0,
};
if let Ok(brush) = render_target
.CreateSolidColorBrush(&color, Some(BRUSH_PROPERTIES.deref()))
{
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match STYLE.load() {
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
@@ -293,31 +391,17 @@ impl Border {
match style {
BorderStyle::Rounded => {
let radius = 8.0 + border_width as f32 / 2.0;
let rounded_rect = D2D1_ROUNDED_RECT {
rect,
radiusX: radius,
radiusY: radius,
};
render_target.DrawRoundedRectangle(
&rounded_rect,
&brush,
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
let rect = D2D_RECT_F {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
};
render_target.DrawRectangle(
&rect,
&brush,
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
@@ -326,17 +410,109 @@ impl Border {
}
let _ = render_target.EndDraw(None, None);
// If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen
// https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234
let _ = BeginPaint(window, &mut PAINTSTRUCT::default());
let _ = EndPaint(window, &PAINTSTRUCT::default());
}
}
}
LRESULT(0)
}
WM_PAINT => {
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
let border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd;
// Update position to update the ZOrder
let border_window_rect = (*border_pointer).window_rect;
tracing::trace!("updating border position");
if let Err(error) =
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
{
tracing::error!("failed to update border position {error}");
}
if let Some(render_target) = (*border_pointer).render_target.get() {
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
// Get window kind and color
(*border_pointer).window_kind = FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
(*border_pointer).style = STYLE.load();
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
}
}
let _ = ValidateRect(window, None);
LRESULT(0)
}
WM_DESTROY => {
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
PostQuitMessage(0);
LRESULT(0)
}

View File

@@ -11,7 +11,7 @@ use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
use border::Border;
pub use border::Border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
@@ -30,18 +30,13 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
use windows::Win32::System::Threading::GetCurrentThread;
use windows::Win32::System::Threading::SetThreadPriority;
use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
lazy_static! {
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
AtomicCell::new(BorderImplementation::Komorebi);
@@ -59,6 +54,7 @@ lazy_static! {
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
Mutex::new(HashMap::new());
@@ -80,6 +76,10 @@ fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn window_border(hwnd: isize) -> Option<Border> {
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
}
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
@@ -122,31 +122,22 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
WindowKind::Stack => STACK.load(Ordering::SeqCst),
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
WindowKind::Stack => STACK.load(Ordering::Relaxed),
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
WindowKind::Floating => FLOATING.load(Ordering::Relaxed),
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
unsafe {
if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)
{
tracing::error!("{error}");
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
}
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
@@ -155,7 +146,6 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
let receiver = event_rx();
event_tx().send(Notification(None))?;
@@ -172,7 +162,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let focused_workspace_idx =
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
let monitors = state.monitors.clone();
let weak_pending_move_op = Arc::downgrade(&state.pending_move_op);
let pending_move_op = *state.pending_move_op;
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
[focused_workspace_idx]
@@ -180,6 +169,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
drop(state);
@@ -249,10 +239,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
should_process_notification = true;
}
// when we switch focus to a floating window
if !should_process_notification
&& floating_window_hwnds.contains(&notification.0.unwrap_or_default())
{
// when we switch focus to/from a floating window
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
// if we switch focus to a floating window
fw == &notification.0.unwrap_or_default() ||
// if there is any floating window with a `WindowKind::Floating` border
// that no longer is the foreground window then we need to update that
// border.
(fw != &foreground_window
&& window_border(*fw)
.map(|b| b.window_kind == WindowKind::Floating)
.unwrap_or_default())
});
if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true;
}
@@ -271,11 +270,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
let mut windows_borders = WINDOWS_BORDERS.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if they are temporarily disabled
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
// Or if the wm is paused
|| is_paused
// Or if we are handling an alt-tab across workspaces
@@ -287,6 +285,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
borders.clear();
windows_borders.clear();
previous_is_paused = is_paused;
continue 'receiver;
@@ -316,10 +315,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut new_border = false;
let border = match borders.entry(monocle.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(monocle.id()) {
if let Ok(border) = Border::create(
monocle.id(),
monocle.focused_window().copied().unwrap_or_default().hwnd,
) {
new_border = true;
entry.insert(border)
} else {
continue 'monitors;
@@ -327,25 +331,33 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(monocle.id().clone(), monitor_idx);
let new_focus_state = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
};
border.window_kind = new_focus_state;
{
let mut focus_state = FOCUS_STATE.lock();
focus_state.insert(
border.hwnd,
if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
WindowKind::Monocle
},
);
focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(
monocle.focused_window().copied().unwrap_or_default().hwnd,
)?;
let reference_hwnd =
monocle.focused_window().copied().unwrap_or_default().hwnd;
border.update(&rect, true)?;
let rect = WindowsApi::window_rect(reference_hwnd)?;
if new_border {
border.set_position(&rect, reference_hwnd)?;
}
border.invalidate();
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
let border_hwnd = border.hwnd;
let mut to_remove = vec![];
@@ -366,9 +378,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
continue 'monitors;
}
let is_maximized = WindowsApi::is_zoomed(
WindowsApi::foreground_window().unwrap_or_default(),
);
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let foreground_monitor_id =
WindowsApi::monitor_from_window(foreground_hwnd);
let is_maximized = foreground_monitor_id == m.id()
&& WindowsApi::is_zoomed(foreground_hwnd);
if is_maximized {
let mut to_remove = vec![];
@@ -414,65 +428,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
for (idx, c) in ws.containers().iter().enumerate() {
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
// Update border when moving or resizing with mouse
if pending_move_op.is_some()
&& idx == ws.focused_container_idx()
&& hwnd == notification_hwnd
{
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
let mut rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
// We create a new variable to track the actual pending move op so
// that the other variable `pending_move_op` still holds the
// pending move info so that when the move ends we know on the next
// notification that the previous pending move and pending move are
// different (because a move just finished) and still handle the
// notification. If otherwise we updated the pending_move_op here
// directly then the next pending move after finish would be the
// same because we had already updated it here.
let mut sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
while sync_pending_move_op.is_some() {
sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, false)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
// Get the border entry for this container from the map or create one
let mut new_border = false;
let border = match borders.entry(c.id().clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id()) {
if let Ok(border) = Border::create(
c.id(),
c.focused_window().copied().unwrap_or_default().hwnd,
) {
new_border = true;
entry.insert(border)
} else {
continue 'monitors;
@@ -480,13 +445,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(c.id().clone(), monitor_idx);
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
|| c.focused_window()
.map(|w| w.hwnd != foreground_window)
.unwrap_or_default()
{
WindowKind::Unfocused
} else if c.windows().len() > 1 {
@@ -494,6 +460,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} else {
WindowKind::Single
};
border.window_kind = new_focus_state;
// Update the focused state for all containers on this workspace
{
@@ -501,66 +468,41 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
}
let rect = WindowsApi::window_rect(
c.focused_window().copied().unwrap_or_default().hwnd,
)?;
let reference_hwnd =
c.focused_window().copied().unwrap_or_default().hwnd;
let rect = WindowsApi::window_rect(reference_hwnd)?;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => last_focus_state != new_focus_state,
};
border.update(&rect, should_invalidate)?;
if new_border {
border.set_position(&rect, reference_hwnd)?;
}
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
}
{
let restore_z_order = Z_ORDER.load();
Z_ORDER.store(ZOrder::TopMost);
'windows: for window in ws.floating_windows() {
let hwnd = window.hwnd;
let notification_hwnd = notification.0.unwrap_or_default();
if pending_move_op.is_some() && hwnd == notification_hwnd {
let mut rect = WindowsApi::window_rect(hwnd)?;
// Check comment above for containers move
let mut sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
while sync_pending_move_op.is_some() {
sync_pending_move_op =
weak_pending_move_op.upgrade().and_then(|p| *p);
let border = match borders.entry(hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&hwnd.to_string())
{
entry.insert(border)
} else {
continue 'monitors;
}
}
};
let new_rect = WindowsApi::window_rect(hwnd)?;
if rect != new_rect {
rect = new_rect;
border.update(&rect, true)?;
}
}
Z_ORDER.store(restore_z_order);
continue 'monitors;
}
for window in ws.floating_windows() {
let mut new_border = false;
let border = match borders.entry(window.hwnd.to_string()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(&window.hwnd.to_string())
if let Ok(border) =
Border::create(&window.hwnd.to_string(), window.hwnd)
{
new_border = true;
entry.insert(border)
} else {
continue 'monitors;
@@ -568,32 +510,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
let mut should_destroy = false;
if let Some(notification_hwnd) = notification.0 {
if notification_hwnd != window.hwnd {
should_destroy = true;
}
}
if WindowsApi::foreground_window().unwrap_or_default()
!= window.hwnd
{
should_destroy = true;
}
if should_destroy {
border.destroy()?;
borders.remove(&window.hwnd.to_string());
borders_monitors.remove(&window.hwnd.to_string());
continue 'windows;
}
#[allow(unused_assignments)]
let mut last_focus_state = None;
let new_focus_state = WindowKind::Floating;
let mut new_focus_state = WindowKind::Unfocused;
if foreground_window == window.hwnd {
new_focus_state = WindowKind::Floating;
}
border.window_kind = new_focus_state;
{
let mut focus_state = FOCUS_STATE.lock();
last_focus_state =
@@ -607,10 +532,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Some(last_focus_state) => last_focus_state != new_focus_state,
};
border.update(&rect, should_invalidate)?;
}
if new_border {
border.set_position(&rect, window.hwnd)?;
}
Z_ORDER.store(restore_z_order);
if should_invalidate {
border.invalidate();
}
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
}
}
}
}

View File

@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
}
}
impl<'a, T> Deref for ComIn<'a, T> {
impl<T> Deref for ComIn<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data

View File

@@ -14,6 +14,7 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::animation::prefix::AnimationPrefix;
use crate::KomorebiTheme;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
@@ -147,10 +148,10 @@ pub enum SocketMessage {
CompleteConfiguration,
AltFocusHack(bool),
Theme(KomorebiTheme),
Animation(bool),
AnimationDuration(u64),
Animation(bool, Option<AnimationPrefix>),
AnimationDuration(u64, Option<AnimationPrefix>),
AnimationFps(u64),
AnimationStyle(AnimationStyle),
AnimationStyle(AnimationStyle, Option<AnimationPrefix>),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
@@ -305,6 +306,8 @@ pub enum BorderImplementation {
ValueEnum,
JsonSchema,
PartialEq,
Eq,
Hash,
)]
pub enum WindowKind {
Single,

View File

@@ -37,6 +37,12 @@ impl From<Rect> for RECT {
}
}
impl Rect {
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
self.right == rhs.right && self.bottom == rhs.bottom
}
}
impl Rect {
/// decrease the size of self by the padding amount.
pub fn add_padding<T>(&mut self, padding: T)

View File

@@ -1,7 +1,6 @@
#![warn(clippy::all)]
pub mod animation;
pub mod animation_manager;
pub mod border_manager;
pub mod com;
#[macro_use]
@@ -47,8 +46,6 @@ use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use animation::*;
pub use animation_manager::*;
pub use colour::*;
pub use core::*;
pub use process_command::*;
@@ -216,12 +213,6 @@ lazy_static! {
)
};
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
Arc::new(Mutex::new(AnimationStyle::Linear));
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
Arc::new(Mutex::new(AnimationManager::new()));
// Use app-specific titlebar removal options where possible
// eg. Windows Terminal, IntelliJ IDEA, Firefox
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
@@ -238,8 +229,6 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
@@ -307,6 +296,8 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
| NotificationEvent::Socket(SocketMessage::Theme(_))
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
);
let notification = &serde_json::to_string(&notification)?;

View File

@@ -17,6 +17,9 @@ use std::time::Duration;
use clap::Parser;
use color_eyre::Result;
use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
@@ -287,7 +290,10 @@ fn main() -> Result<()> {
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
wm.lock().restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;

View File

@@ -139,6 +139,86 @@ impl Monitor {
Ok(())
}
/// Adds a container to this `Monitor` using the move direction to calculate if the container
/// should be added in front of all containers, in the back or in place of the focused
/// container, moving the rest along. The move direction should be from the origin monitor
/// towards the target monitor or from the origin workspace towards the target workspace.
pub fn add_container_with_direction(
&mut self,
container: Container,
workspace_idx: Option<usize>,
direction: OperationDirection,
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no workspace"))?
};
match direction {
OperationDirection::Left => {
// insert the container into the workspace on the monitor at the back (or rightmost position)
// if we are moving across a boundary to the left (back = right side of the target)
match workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.insert_container_at_idx(0, container);
} else {
workspace.add_container_to_back(container);
}
}
_ => {
workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
workspace.add_container_to_back(container);
}
}
}
OperationDirection::Right => {
// insert the container into the workspace on the monitor at the front (or leftmost position)
// if we are moving across a boundary to the right (front = left side of the target)
match workspace.layout() {
Layout::Default(layout) => {
let target_index = layout.leftmost_index(workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.add_container_to_back(container);
} else {
workspace.insert_container_at_idx(target_index, container);
}
}
_ => {
workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
workspace.add_container_to_front(container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the container into the workspace on the monitor at the position
// where the currently focused container on that workspace is
workspace.insert_container_at_idx(workspace.focused_container_idx(), container);
}
};
Ok(())
}
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
if idx < self.workspaces().len() {
return self.workspaces_mut().remove(idx);
@@ -215,54 +295,14 @@ impl Monitor {
Some(workspace) => workspace,
};
match direction {
Some(OperationDirection::Left) => match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.insert_container_at_idx(0, container);
} else {
target_workspace.add_container_to_back(container);
}
}
_ => {
target_workspace.add_container_to_back(container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(container);
}
},
Some(OperationDirection::Right) => match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace.add_container_to_back(container);
} else {
target_workspace
.insert_container_at_idx(target_index, container);
}
}
_ => {
target_workspace.insert_container_at_idx(target_index, container);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(container);
}
},
_ => {
target_workspace.add_container_to_back(container);
}
if let Some(direction) = direction {
self.add_container_with_direction(
container,
Some(target_workspace_idx),
direction,
)?;
} else {
target_workspace.add_container_to_back(container);
}
}

View File

@@ -132,6 +132,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
);
ACTIVE.store(true, Ordering::SeqCst);
border_manager::send_notification(None);
}
continue 'receiver;

View File

@@ -22,6 +22,10 @@ use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use crate::animation::AnimationEngine;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
@@ -40,6 +44,10 @@ use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
@@ -63,10 +71,6 @@ use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
@@ -532,7 +536,8 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, true, None)?;
}
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
}
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
self.swap_focused_monitor(monitor_idx)?;
@@ -544,7 +549,8 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_container_to_monitor(monitor_idx, None, true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
}
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, false, None)?;
@@ -566,7 +572,8 @@ impl WindowManager {
self.move_container_to_workspace(workspace_idx, false, None)?;
}
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
self.move_container_to_monitor(monitor_idx, None, false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
}
SocketMessage::CycleSendContainerToMonitor(direction) => {
let monitor_idx = direction.next_idx(
@@ -575,22 +582,37 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
);
self.move_container_to_monitor(monitor_idx, None, false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
}
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
}
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
}
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
false,
direction,
)?;
}
}
@@ -598,7 +620,13 @@ impl WindowManager {
if let Some((monitor_idx, workspace_idx)) =
self.monitor_workspace_index_by_name(workspace)
{
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
let direction = self.direction_from_monitor_idx(monitor_idx);
self.move_container_to_monitor(
monitor_idx,
Option::from(workspace_idx),
true,
direction,
)?;
}
}
@@ -642,12 +670,10 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
SocketMessage::Retile => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(false)?
}
SocketMessage::RetileWithResizeDimensions => {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
border_manager::destroy_all_borders()?;
self.retile_all(true)?
}
@@ -889,7 +915,11 @@ impl WindowManager {
tracing::info!(
"received stop command, restoring all hidden windows and terminating process"
);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
self.restore_all_windows()?;
AnimationEngine::wait_for_all_animations();
if WindowsApi::focus_follows_mouse()? {
WindowsApi::disable_focus_follows_mouse()?;
@@ -1486,6 +1516,16 @@ impl WindowManager {
}
SocketMessage::Border(enable) => {
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
if !enable {
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
border_manager::destroy_all_borders()?;
}
BorderImplementation::Windows => {
self.remove_all_accents()?;
}
}
}
}
SocketMessage::BorderImplementation(implementation) => {
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
@@ -1532,18 +1572,41 @@ impl WindowManager {
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
}
SocketMessage::Animation(enable) => {
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
}
SocketMessage::AnimationDuration(duration) => {
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
}
SocketMessage::Animation(enable, prefix) => match prefix {
Some(prefix) => {
ANIMATION_ENABLED_PER_ANIMATION
.lock()
.insert(prefix, enable);
}
None => {
ANIMATION_ENABLED_GLOBAL.store(enable, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationDuration(duration, prefix) => match prefix {
Some(prefix) => {
ANIMATION_DURATION_PER_ANIMATION
.lock()
.insert(prefix, duration);
}
None => {
ANIMATION_DURATION_GLOBAL.store(duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
},
SocketMessage::AnimationFps(fps) => {
ANIMATION_FPS.store(fps, Ordering::SeqCst);
}
SocketMessage::AnimationStyle(style) => {
*ANIMATION_STYLE.lock() = style;
}
SocketMessage::AnimationStyle(style, prefix) => match prefix {
Some(prefix) => {
ANIMATION_STYLE_PER_ANIMATION.lock().insert(prefix, style);
}
None => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
},
SocketMessage::ToggleTransparency => {
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);

View File

@@ -485,8 +485,21 @@ impl WindowManager {
// place across a monitor boundary to an empty workspace
.unwrap_or(&Rect::default());
// This will be true if we have moved to an empty workspace on another monitor
let mut moved_across_monitors = old_position == Rect::default();
// This will be true if we have moved to another monitor
let mut moved_across_monitors = false;
for (i, monitors) in self.monitors().iter().enumerate() {
for workspace in monitors.workspaces() {
if workspace.contains_window(window.hwnd) && i != target_monitor_idx {
moved_across_monitors = true;
break;
}
}
if moved_across_monitors {
break;
}
}
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
// If we didn't move to another monitor with an empty workspace, it is
// still possible that we moved to another monitor with a populated workspace
@@ -517,7 +530,9 @@ impl WindowManager {
}
let workspace = self.focused_workspace_mut()?;
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|| moved_across_monitors
{
let resize = Rect {
left: new_position.left - old_position.left,
top: new_position.top - old_position.top,
@@ -570,11 +585,19 @@ impl WindowManager {
// so that we don't have ghost tiles until we force an interaction on
// the origin monitor's focused workspace
self.focus_monitor(origin_monitor_idx)?;
self.focus_workspace(origin_workspace_idx)?;
let origin_monitor = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false, false)?;
self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?;
let target_monitor = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?;
// Make sure to give focus to the moved window again
@@ -686,6 +709,20 @@ impl WindowManager {
known_hwnds.push(window.hwnd);
}
}
for window in workspace.floating_windows() {
known_hwnds.push(window.hwnd);
}
if let Some(window) = workspace.maximized_window() {
known_hwnds.push(window.hwnd);
}
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
known_hwnds.push(window.hwnd);
}
}
}
}

View File

@@ -1,8 +1,16 @@
use crate::animation::PerAnimationPrefixConfig;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::DEFAULT_ANIMATION_FPS;
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::border_manager::Z_ORDER;
use crate::colour::Colour;
use crate::core::BorderImplementation;
use crate::core::StackbarLabel;
@@ -28,10 +36,6 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::workspace::Workspace;
use crate::CrossBoundaryBehaviour;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::ANIMATION_FPS;
use crate::ANIMATION_STYLE;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -263,7 +267,7 @@ pub struct StaticConfig {
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
/// END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
/// Enable or disable mouse follows focus (default: true)
@@ -292,7 +296,7 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
pub border_style: Option<BorderStyle>,
/// Active window border z-order (default: System)
/// DEPRECATED from v0.1.31: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Active window border implementation (default: Komorebi)
@@ -374,11 +378,11 @@ pub struct StaticConfig {
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
enabled: bool,
enabled: PerAnimationPrefixConfig<bool>,
/// Set the animation duration in ms (default: 250)
duration: Option<u64>,
duration: Option<PerAnimationPrefixConfig<u64>>,
/// Set the animation style (default: Linear)
style: Option<AnimationStyle>,
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
/// Set the animation FPS (default: 60)
fps: Option<u64>,
}
@@ -492,7 +496,7 @@ impl StaticConfig {
}
pub fn deprecated(raw: &str) {
let deprecated_options = ["invisible_borders"];
let deprecated_options = ["invisible_borders", "border_z_order"];
let deprecated_variants = vec![
("Hide", "window_hiding_behaviour", "Cloak"),
("Minimize", "window_hiding_behaviour", "Cloak"),
@@ -595,7 +599,7 @@ impl From<&WindowManager> for StaticConfig {
),
transparency_ignore_rules: None,
border_style: Option::from(STYLE.load()),
border_z_order: Option::from(Z_ORDER.load()),
border_z_order: None,
border_implementation: Option::from(IMPLEMENTATION.load()),
default_workspace_padding: Option::from(
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
@@ -654,11 +658,43 @@ impl StaticConfig {
}
if let Some(animations) = &self.animation {
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
let mut animation_style = ANIMATION_STYLE.lock();
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
match &animations.enabled {
PerAnimationPrefixConfig::Prefix(enabled) => {
ANIMATION_ENABLED_PER_ANIMATION.lock().clone_from(enabled);
}
PerAnimationPrefixConfig::Global(enabled) => {
ANIMATION_ENABLED_GLOBAL.store(*enabled, Ordering::SeqCst);
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
}
}
match &animations.style {
Some(PerAnimationPrefixConfig::Prefix(style)) => {
ANIMATION_STYLE_PER_ANIMATION.lock().clone_from(style);
}
Some(PerAnimationPrefixConfig::Global(style)) => {
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
*animation_style = *style;
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
}
None => {}
}
match &animations.duration {
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
ANIMATION_DURATION_PER_ANIMATION.lock().clone_from(duration);
}
Some(PerAnimationPrefixConfig::Global(duration)) => {
ANIMATION_DURATION_GLOBAL.store(*duration, Ordering::SeqCst);
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
}
None => {}
}
ANIMATION_FPS.store(
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
Ordering::SeqCst,
);
}
if let Some(container) = self.default_container_padding {

View File

@@ -1,11 +1,20 @@
use crate::border_manager;
use crate::animation::lerp::Lerp;
use crate::animation::prefix::new_animation_key;
use crate::animation::prefix::AnimationPrefix;
use crate::animation::AnimationEngine;
use crate::animation::RenderDispatcher;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::com::SetCloak;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::windows_api;
use crate::ANIMATIONS_IN_PROGRESS;
use crate::ANIMATION_DURATION;
use crate::ANIMATION_ENABLED;
use crate::AnimationStyle;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use std::collections::HashMap;
@@ -15,6 +24,7 @@ use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use crate::core::config_generation::IdWithIdentifier;
@@ -35,7 +45,6 @@ use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::animation::Animation;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
@@ -58,16 +67,11 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
pub struct Window {
pub hwnd: isize,
#[serde(skip)]
animation: Animation,
}
impl From<isize> for Window {
fn from(value: isize) -> Self {
Self {
hwnd: value,
animation: Animation::new(value),
}
Self { hwnd: value }
}
}
@@ -75,7 +79,6 @@ impl From<HWND> for Window {
fn from(value: HWND) -> Self {
Self {
hwnd: value.0 as isize,
animation: Animation::new(value.0 as isize),
}
}
}
@@ -155,6 +158,144 @@ impl Serialize for Window {
}
}
struct MovementRenderDispatcher {
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
}
impl MovementRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
pub fn new(
hwnd: isize,
start_rect: Rect,
target_rect: Rect,
top: bool,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_rect,
target_rect,
top,
style,
}
}
}
impl RenderDispatcher for MovementRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(self.hwnd, None, false);
Ok(())
}
fn post_render(&self) -> Result<()> {
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
if ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX)
== 0
{
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
focus_manager::send_notification(self.hwnd)
}
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
Ok(())
}
}
struct TransparencyRenderDispatcher {
hwnd: isize,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
is_opaque: bool,
}
impl TransparencyRenderDispatcher {
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
pub fn new(
hwnd: isize,
is_opaque: bool,
start_opacity: u8,
target_opacity: u8,
style: AnimationStyle,
) -> Self {
Self {
hwnd,
start_opacity,
target_opacity,
style,
is_opaque,
}
}
}
impl RenderDispatcher for TransparencyRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> Result<()> {
//transparent
if !self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
fn render(&self, progress: f64) -> Result<()> {
WindowsApi::set_transparent(
self.hwnd,
self.start_opacity
.lerp(self.target_opacity, progress, self.style),
)
}
fn post_render(&self) -> Result<()> {
//opaque
if self.is_opaque {
let window = Window::from(self.hwnd);
let mut ex_style = window.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
window.update_ex_style(&ex_style)?;
}
Ok(())
}
}
impl Window {
pub const fn hwnd(self) -> HWND {
HWND(windows_api::as_ptr!(self.hwnd))
@@ -172,17 +313,59 @@ impl Window {
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
let window_x = current_area.left + corrected_relative_x;
let window_y = current_area.top + corrected_relative_y;
let left = x_diff + window_x;
let top = y_diff + window_y;
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
let new_rect = Rect {
left: x_diff + window_x,
top: y_diff + window_y,
right: current_rect.right,
bottom: current_rect.bottom,
left,
top,
right: corrected_width,
bottom: corrected_height,
};
//TODO: We might need to take into account the differences in DPI for the new_rect, unless
//we can use the xy ratios above to the right/bottom (width/height of window) as well?
self.set_position(&new_rect, true)
let is_maximized = &new_rect == target_area;
if is_maximized {
windows_api::WindowsApi::unmaximize_window(self.hwnd);
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled
.get(&MovementRenderDispatcher::PREFIX)
.is_some_and(|v| *v);
drop(animation_enabled);
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
let anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
self.set_position(&new_rect, true)?;
let hwnd = self.hwnd;
// Wait for the animation to finish before maximizing the window again, otherwise
// we would be maximizing the window on the current monitor anyway
thread::spawn(move || {
let mut new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
while new_anim_count > anim_count && max_wait > 0 {
thread::sleep(Duration::from_millis(10));
new_anim_count = ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX);
max_wait -= 1;
}
windows_api::WindowsApi::maximize_window(hwnd);
});
} else {
self.set_position(&new_rect, true)?;
windows_api::WindowsApi::maximize_window(self.hwnd);
}
} else {
self.set_position(&new_rect, true)?;
}
Ok(())
}
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
@@ -200,53 +383,6 @@ impl Window {
)
}
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
let start_rect = *start_rect;
let target_rect = *target_rect;
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
let mut animation = self.animation;
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
border_manager::send_notification(Some(self.hwnd));
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
let hwnd = self.hwnd;
std::thread::spawn(move || {
animation.animate(duration, |progress: f64| {
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
if progress == 1.0 {
WindowsApi::position_window(hwnd, &new_rect, top)?;
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
focus_manager::send_notification(hwnd)
}
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
.store(false, Ordering::SeqCst);
border_manager::send_notification(Some(hwnd));
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
} else {
// using MoveWindow because it runs faster than SetWindowPos
// so animation have more fps and feel smoother
WindowsApi::move_window(hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(hwnd, None, false);
}
Ok(())
})
});
Ok(())
}
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
let window_rect = WindowsApi::window_rect(self.hwnd)?;
@@ -254,8 +390,27 @@ impl Window {
return Ok(());
}
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
self.animate_position(&window_rect, layout, top)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
if move_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&MovementRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher =
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
AnimationEngine::animate(render_dispatcher, duration)
} else {
WindowsApi::position_window(self.hwnd, layout, top)
}
@@ -363,20 +518,81 @@ impl Window {
Ok(())
}
pub fn is_focused(self) -> bool {
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
}
pub fn transparent(self) -> Result<()> {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
false,
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.insert(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)?;
WindowsApi::set_transparent(
self.hwnd,
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
)
}
}
pub fn opaque(self) -> Result<()> {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
if transparent_enabled.is_some_and(|enabled| *enabled)
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
{
let duration = Duration::from_millis(
*ANIMATION_DURATION_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
);
let style = *ANIMATION_STYLE_PER_ANIMATION
.lock()
.get(&TransparencyRenderDispatcher::PREFIX)
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
let render_dispatcher = TransparencyRenderDispatcher::new(
self.hwnd,
true,
WindowsApi::get_transparent(self.hwnd)
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
255,
style,
);
AnimationEngine::animate(render_dispatcher, duration)
} else {
let mut ex_style = self.ex_style()?;
ex_style.remove(ExtendedWindowStyle::LAYERED);
self.update_ex_style(&ex_style)
}
}
pub fn set_accent(self, colour: u32) -> Result<()> {

View File

@@ -498,6 +498,35 @@ impl WindowManager {
None
}
/// Calculates the direction of a move across monitors given a specific monitor index
pub fn direction_from_monitor_idx(
&self,
target_monitor_idx: usize,
) -> Option<OperationDirection> {
let current_monitor_idx = self.focused_monitor_idx();
if current_monitor_idx == target_monitor_idx {
return None;
}
let current_monitor_size = self.focused_monitor_size().ok()?;
let target_monitor_size = *self.monitors().get(target_monitor_idx)?.size();
if target_monitor_size.left + target_monitor_size.right == current_monitor_size.left {
return Some(OperationDirection::Left);
}
if current_monitor_size.right + current_monitor_size.left == target_monitor_size.left {
return Some(OperationDirection::Right);
}
if target_monitor_size.top + target_monitor_size.bottom == current_monitor_size.top {
return Some(OperationDirection::Up);
}
if current_monitor_size.top + current_monitor_size.bottom == target_monitor_size.top {
return Some(OperationDirection::Down);
}
None
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(self), level = "debug")]
fn add_window_handle_to_move_based_on_workspace_rule(
@@ -1383,6 +1412,7 @@ impl WindowManager {
monitor_idx: usize,
workspace_idx: Option<usize>,
follow: bool,
move_direction: Option<OperationDirection>,
) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -1437,8 +1467,12 @@ impl WindowManager {
.get_mut(monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor"))?;
let mut should_load_workspace = false;
if let Some(workspace_idx) = workspace_idx {
target_monitor.focus_workspace(workspace_idx)?;
if workspace_idx != target_monitor.focused_workspace_idx() {
target_monitor.focus_workspace(workspace_idx)?;
should_load_workspace = true;
}
}
let target_workspace = target_monitor
.focused_workspace_mut()
@@ -1455,7 +1489,11 @@ impl WindowManager {
.map(|w| w.hwnd)
.collect::<Vec<_>>();
target_monitor.add_container(container, workspace_idx)?;
if let Some(direction) = move_direction {
target_monitor.add_container_with_direction(container, workspace_idx, direction)?;
} else {
target_monitor.add_container(container, workspace_idx)?;
}
if let Some(workspace) = target_monitor.focused_workspace() {
if !*workspace.tile() {
@@ -1469,7 +1507,9 @@ impl WindowManager {
bail!("failed to find a window to move");
}
target_monitor.load_focused_workspace(mouse_follows_focus)?;
if should_load_workspace {
target_monitor.load_focused_workspace(mouse_follows_focus)?;
}
target_monitor.update_focused_workspace(offset)?;
// this second one is for DPI changes when the target is another monitor
@@ -1548,13 +1588,14 @@ impl WindowManager {
tracing::info!("focusing container");
let new_idx = if workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let new_idx =
if workspace.maximized_window().is_some() || workspace.monocle_container().is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle = false;
let mut cross_monitor_monocle_or_max = false;
// this is for when we are scrolling across workspaces like PaperWM
if new_idx.is_none()
@@ -1631,14 +1672,24 @@ impl WindowManager {
let mouse_follows_focus = self.mouse_follows_focus;
if let Ok(focused_workspace) = self.focused_workspace_mut() {
if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = focused_workspace.maximized_window() {
window.focus(mouse_follows_focus)?;
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
// Should it really be done?
//
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
// window.hwnd,
// )?)?;
cross_monitor_monocle_or_max = true;
} else if let Some(monocle) = focused_workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(mouse_follows_focus)?;
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
window.hwnd,
)?)?;
cross_monitor_monocle = true;
cross_monitor_monocle_or_max = true;
}
} else {
match direction {
@@ -1675,7 +1726,7 @@ impl WindowManager {
}
}
if !cross_monitor_monocle {
if !cross_monitor_monocle_or_max {
if let Ok(focused_window) = self.focused_window_mut() {
focused_window.focus(self.mouse_follows_focus)?;
}
@@ -1754,15 +1805,13 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
{
// remove the container from the origin monitor workspace
let origin_container = self
.focused_workspace_mut()?
.remove_container_by_idx(origin_container_idx)
.ok_or_else(|| {
anyhow!("could not remove container at given origin index")
})?;
self.focused_workspace_mut()?.focus_previous_container();
// actually move the container to target monitor using the direction
self.move_container_to_monitor(
target_monitor_idx,
None,
true,
Some(direction),
)?;
// focus the target monitor
self.focus_monitor(target_monitor_idx)?;
@@ -1782,79 +1831,6 @@ impl WindowManager {
// get a mutable ref to the focused workspace on the target monitor
let target_workspace = self.focused_workspace_mut()?;
match direction {
OperationDirection::Left => {
// insert the origin container into the focused workspace on the target monitor
// at the back (or rightmost position) if we are moving across a boundary to
// the left (back = right side of the target)
match target_workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
target_workspace.add_container_to_front(origin_container);
}
DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.insert_container_at_idx(0, origin_container);
} else {
target_workspace
.add_container_to_back(origin_container);
}
}
_ => {
target_workspace.add_container_to_back(origin_container);
}
},
Layout::Custom(_) => {
target_workspace.add_container_to_back(origin_container);
}
}
}
OperationDirection::Right => {
// insert the origin container into the focused workspace on the target monitor
// at the front (or leftmost position) if we are moving across a boundary to the
// right (front = left side of the target)
match target_workspace.layout() {
Layout::Default(layout) => {
let target_index =
layout.leftmost_index(target_workspace.containers().len());
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack => {
if target_workspace.containers().len() == 1 {
target_workspace
.add_container_to_back(origin_container);
} else {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
_ => {
target_workspace.insert_container_at_idx(
target_index,
origin_container,
);
}
}
}
Layout::Custom(_) => {
target_workspace.add_container_to_front(origin_container);
}
}
}
OperationDirection::Up | OperationDirection::Down => {
// insert the origin container into the focused workspace on the target monitor
// at the position where the currently focused container on that workspace is
target_workspace.insert_container_at_idx(
target_workspace.focused_container_idx(),
origin_container,
);
}
};
// if there is only one container on the target workspace after the insertion
// it means that there won't be one swapped back, so we have to decrement the
// focused position

View File

@@ -77,6 +77,7 @@ use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
@@ -105,7 +106,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
@@ -128,7 +128,6 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
@@ -153,6 +152,7 @@ macro_rules! as_ptr {
};
}
use crate::border_manager::Border;
pub(crate) use as_ptr;
pub enum WindowsResult<T, E> {
@@ -452,7 +452,13 @@ impl WindowsApi {
}
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
let flags = {
SetWindowPosition::NO_SEND_CHANGING
| SetWindowPosition::NO_ACTIVATE
| SetWindowPosition::NO_REDRAW
| SetWindowPosition::SHOW_WINDOW
};
Self::set_window_pos(
HWND(as_ptr!(hwnd)),
layout,
@@ -1089,10 +1095,14 @@ impl WindowsApi {
.process()
}
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
pub fn create_border_window(
name: PCWSTR,
instance: isize,
border: *const Border,
) -> Result<isize> {
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
name,
name,
WS_POPUP | WS_SYSMENU,
@@ -1103,12 +1113,8 @@ impl WindowsApi {
None,
None,
HINSTANCE(as_ptr!(instance)),
None,
)?;
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
hwnd
Some(border as _),
)?
}
.process()
}
@@ -1127,6 +1133,21 @@ impl WindowsApi {
Ok(())
}
pub fn get_transparent(hwnd: isize) -> Result<u8> {
unsafe {
let mut alpha: u8 = u8::default();
let mut color_ref = COLORREF(-1i32 as u32);
let mut flags = LWA_ALPHA;
GetLayeredWindowAttributes(
HWND(as_ptr!(hwnd)),
Some(std::ptr::addr_of_mut!(color_ref)),
Some(std::ptr::addr_of_mut!(alpha)),
Some(std::ptr::addr_of_mut!(flags)),
)?;
Ok(alpha)
}
}
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
unsafe {
CreateWindowExW(

View File

@@ -1,10 +1,6 @@
use std::collections::VecDeque;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use crate::border_manager;
use crate::container::Container;
use crate::window::RuleDebug;
use crate::window::Window;
@@ -12,6 +8,19 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::winevent_listener;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW;
use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW;
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
@@ -60,6 +69,15 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
true.into()
}
fn has_filtered_style(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 };
let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 };
style & WS_CHILD.0 != 0
|| ex_style & WS_EX_TOOLWINDOW.0 != 0
|| ex_style & WS_EX_NOACTIVATE.0 != 0
}
pub extern "system" fn win_event_hook(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
@@ -69,8 +87,7 @@ pub extern "system" fn win_event_hook(
_id_event_thread: u32,
_dwms_event_time: u32,
) {
// OBJID_WINDOW
if id_object != 0 {
if id_object != OBJID_WINDOW.0 {
return;
}
@@ -81,6 +98,23 @@ pub extern "system" fn win_event_hook(
Err(_) => return,
};
// this forwards the message to the window's border when it moves or is destroyed
// see border_manager/border.rs
if matches!(
winevent,
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
) && !has_filtered_style(hwnd)
{
let border_window = border_manager::window_border(hwnd.0 as isize);
if let Some(border) = border_window {
unsafe {
let _ =
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
}
}
}
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
None => {
tracing::trace!(

View File

@@ -11,6 +11,8 @@ use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_OUTOFCONTEXT;
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_SKIPOWNPROCESS;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_callbacks;
@@ -31,7 +33,7 @@ pub fn start() {
Some(windows_callbacks::win_event_hook),
0,
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
)
};

View File

@@ -724,12 +724,18 @@ struct BorderImplementation {
struct Animation {
#[clap(value_enum)]
boolean_state: BooleanState,
/// Animation type to apply the state to. If not specified, sets global state
#[clap(value_enum, short, long)]
animation_type: Option<komorebi_client::AnimationPrefix>,
}
#[derive(Parser)]
struct AnimationDuration {
/// Desired animation durations in ms
duration: u64,
/// Animation type to apply the duration to. If not specified, sets global duration
#[clap(value_enum, short, long)]
animation_type: Option<komorebi_client::AnimationPrefix>,
}
#[derive(Parser)]
@@ -743,6 +749,9 @@ struct AnimationStyle {
/// Desired ease function for animation
#[clap(value_enum, short, long, default_value = "linear")]
style: komorebi_client::AnimationStyle,
/// Animation type to apply the style to. If not specified, sets global style
#[clap(value_enum, short, long)]
animation_type: Option<komorebi_client::AnimationPrefix>,
}
#[derive(Parser)]
@@ -770,6 +779,9 @@ struct Start {
/// Start komorebi-bar in a background process
#[clap(long)]
bar: bool,
/// Start masir in a background process for focus-follows-mouse
#[clap(long)]
masir: bool,
}
#[derive(Parser)]
@@ -783,6 +795,25 @@ struct Stop {
/// Stop komorebi-bar if it is running as a background process
#[clap(long)]
bar: bool,
/// Stop masir if it is running as a background process
#[clap(long)]
masir: bool,
}
#[derive(Parser)]
struct Kill {
/// Kill whkd if it is running as a background process
#[clap(long)]
whkd: bool,
/// Kill ahk if it is running as a background process
#[clap(long)]
ahk: bool,
/// Kill komorebi-bar if it is running as a background process
#[clap(long)]
bar: bool,
/// Kill masir if it is running as a background process
#[clap(long)]
masir: bool,
}
#[derive(Parser)]
@@ -879,6 +910,9 @@ struct EnableAutostart {
/// Enable autostart of komorebi-bar
#[clap(long)]
bar: bool,
/// Enable autostart of masir
#[clap(long)]
masir: bool,
}
#[derive(Parser)]
@@ -904,6 +938,8 @@ enum SubCommand {
Start(Start),
/// Stop the komorebi.exe process and restore all hidden windows
Stop(Stop),
/// Kill background processes started by komorebic
Kill(Kill),
/// Check komorebi configuration and related files for common errors
Check,
/// Show the path to komorebi.json
@@ -1498,6 +1534,10 @@ fn main() -> Result<()> {
arguments.push_str(" --ahk");
}
if args.bar {
arguments.push_str(" --masir");
}
Command::new("powershell")
.arg("-c")
.arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()")
@@ -1918,6 +1958,10 @@ fn main() -> Result<()> {
bail!("could not find whkd, please make sure it is installed before using the --whkd flag");
}
if arg.masir && which("masir").is_err() {
bail!("could not find masir, please make sure it is installed before using the --masir flag");
}
if arg.ahk && which(&ahk).is_err() {
bail!("could not find autohotkey, please make sure it is installed before using the --ahk flag");
}
@@ -1981,8 +2025,14 @@ fn main() -> Result<()> {
)
};
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
let mut attempts = 0;
let mut running = false;
let mut running = system
.processes_by_name("komorebi.exe".as_ref())
.next()
.is_some();
while !running && attempts <= 2 {
match powershell_script::run(&script) {
@@ -1997,7 +2047,6 @@ fn main() -> Result<()> {
print!("Waiting for komorebi.exe to start...");
std::thread::sleep(Duration::from_secs(3));
let mut system = sysinfo::System::new_all();
system.refresh_processes(ProcessesToUpdate::All);
if system
@@ -2085,7 +2134,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
let mut config = StaticConfig::read(config)?;
if let Some(display_bar_configurations) = &mut config.bar_configurations {
for config_file_path in &mut *display_bar_configurations {
let script = r#"Start-Process 'komorebi-bar' '--config "CONFIGFILE"' -WindowStyle hidden"#
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
match powershell_script::run(&script) {
@@ -2116,6 +2165,23 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
}
}
if arg.masir {
let script = r"
if (!(Get-Process masir -ErrorAction SilentlyContinue))
{
Start-Process masir -WindowStyle hidden
}
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
println!("\nThank you for using komorebi!\n");
println!("# Sponsorship");
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference");
@@ -2184,6 +2250,20 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
}
}
if arg.masir {
let script = r"
Stop-Process -Name:masir -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
if arg.ahk {
let script = r#"
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
@@ -2243,6 +2323,78 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
}
}
}
SubCommand::Kill(arg) => {
if arg.whkd {
let script = r"
Stop-Process -Name:whkd -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
if arg.bar {
let script = r"
Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
if arg.masir {
let script = r"
Stop-Process -Name:masir -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
if arg.ahk {
let script = r#"
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
(Get-CimInstance Win32_Process | Where-Object {
($_.CommandLine -like '*komorebi.ahk"') -and
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
} | Select-Object -First 1) | ForEach-Object {
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
}
} else {
(Get-WmiObject Win32_Process | Where-Object {
($_.CommandLine -like '*komorebi.ahk"') -and
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
} | Select-Object -First 1) | ForEach-Object {
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
}
}
"#;
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
}
SubCommand::IgnoreRule(arg) => {
send_message(&SocketMessage::IgnoreRule(arg.identifier, arg.id))?;
}
@@ -2546,16 +2698,25 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
send_message(&SocketMessage::ToggleTransparency)?;
}
SubCommand::Animation(arg) => {
send_message(&SocketMessage::Animation(arg.boolean_state.into()))?;
send_message(&SocketMessage::Animation(
arg.boolean_state.into(),
arg.animation_type,
))?;
}
SubCommand::AnimationDuration(arg) => {
send_message(&SocketMessage::AnimationDuration(arg.duration))?;
send_message(&SocketMessage::AnimationDuration(
arg.duration,
arg.animation_type,
))?;
}
SubCommand::AnimationFps(arg) => {
send_message(&SocketMessage::AnimationFps(arg.fps))?;
}
SubCommand::AnimationStyle(arg) => {
send_message(&SocketMessage::AnimationStyle(arg.style))?;
send_message(&SocketMessage::AnimationStyle(
arg.style,
arg.animation_type,
))?;
}
SubCommand::ResizeDelta(arg) => {

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,35 @@
"properties": {
"duration": {
"description": "Set the animation duration in ms (default: 250)",
"type": "integer",
"format": "uint64",
"minimum": 0.0
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
]
},
"enabled": {
"description": "Enable or disable animations (default: false)",
"type": "boolean"
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "boolean"
}
},
{
"type": "boolean"
}
]
},
"fps": {
"description": "Set the animation FPS (default: 60)",
@@ -29,38 +51,80 @@
},
"style": {
"description": "Set the animation style (default: Linear)",
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
},
{
"type": "string",
"enum": [
"Linear",
"EaseInSine",
"EaseOutSine",
"EaseInOutSine",
"EaseInQuad",
"EaseOutQuad",
"EaseInOutQuad",
"EaseInCubic",
"EaseInOutCubic",
"EaseInQuart",
"EaseOutQuart",
"EaseInOutQuart",
"EaseInQuint",
"EaseOutQuint",
"EaseInOutQuint",
"EaseInExpo",
"EaseOutExpo",
"EaseInOutExpo",
"EaseInCirc",
"EaseOutCirc",
"EaseInOutCirc",
"EaseInBack",
"EaseOutBack",
"EaseInOutBack",
"EaseInElastic",
"EaseOutElastic",
"EaseInOutElastic",
"EaseInBounce",
"EaseOutBounce",
"EaseInOutBounce"
]
}
]
}
}
@@ -420,7 +484,7 @@
"format": "int32"
},
"border_z_order": {
"description": "Active window border z-order (default: System)",
"description": "DEPRECATED from v0.1.31: no longer required",
"type": "string",
"enum": [
"Top",
@@ -579,7 +643,7 @@
}
},
"focus_follows_mouse": {
"description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)",
"description": "END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead",
"oneOf": [
{
"description": "A custom FFM implementation (slightly more CPU-intensive)",