├── .gitignore ├── LICENSE ├── README.md ├── draw └── text.go ├── go.mod ├── go.sum ├── input.go ├── internal └── xflag │ └── xflag.go ├── kawa.go ├── layers.go ├── menu.go ├── mode.go ├── output.go ├── render.go ├── server.go ├── statusbar.go ├── style.go ├── surface.go └── view.go /.gitignore: -------------------------------------------------------------------------------- 1 | cpu.prof 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DeedleFake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | kawa 2 | ==== 3 | 4 | kawa is planned to be a Wayland compositor with an interface inspired by, though not directly copying, [Plan 9's rio](https://en.wikipedia.org/wiki/Rio_(windowing_system)) window manager. It isn't yet, however, so feel free to either send a pull request or come back later. 5 | 6 | Planned Features 7 | ---------------- 8 | 9 | - [X] Minimal interface besides windows, but not completely blank like rio. There should be, for example, a status bar with the current time and other useful global pieces of info. 10 | - [X] Ability to maximize windows. The status bar will still display, allowing it to be right-clicked to access the window management menu. 11 | - [ ] Ability to access the global menus from anywhere by holding a key (Super?) and clicking. 12 | - [ ] Window overview similar to GNOME shell's. 13 | - [ ] ~~Window starting system similar to rio's with terminal takeover, but with more capability for handling multi-window clients. This could be tricky, however, and will heavily depend on how far Wayland can be stretched to handle something like this.~~ This has been ditched. It isn't feasible, makes little sense on Linux, and does bizarre things with a lot of programs. Maybe later, but probably not. 14 | - [ ] An exit feature. Maybe something in the status bar? It shouldn't be too easy to do accidentally, obviously. 15 | - [ ] Support for fullscreen apps, such as games. 16 | - [X] Auto-focus of windows. 17 | 18 | Wishful Thinking 19 | ---------------- 20 | 21 | - [ ] Touchscreen support. I'm not entirely sure how this would work, but since rio's design is heavily mouse-oriented, if it _does_ work it could be quite nice. 22 | - [ ] Theming support. 23 | - [X] When a window is maximized, maybe it automatically enters a tiled mode and is always underneath non-maximized windows. ~~I'm not sure how feasible this is.~~ Quite feasible indeed, it turns out, thanks to Wayland giving 100% of final say on positioning and sizing to the compositor. 24 | 25 | Building and Installing 26 | ----------------------- 27 | 28 | **Warning**: This project is _not_ ready for production usage. In particular, there is currently an issue with the initialization that causes a machine attempting to run it as a proper compositor to completely lock up all input. **Do not use this as a regular compositor yet.** It should be fine to run it as a Wayland client inside of another compositor, however, if you'd like to test it. 29 | 30 | ### Dependencies 31 | 32 | * wlroots v0.15 33 | 34 | ### Installation 35 | 36 | Installing kawa can be done via [the `go` tool](https://pkg.go.dev/cmd/go): 37 | 38 | ```bash 39 | $ go install deedles.dev/kawa@latest 40 | ``` 41 | 42 | ### Compilation 43 | 44 | If you would like to compile kawa without installing the resulting binary, use the following commands: 45 | 46 | ```bash 47 | $ git clone https://github.com/DeedleFake/kawa 48 | $ cd kawa 49 | $ go build 50 | ``` 51 | 52 | Prior Art 53 | --------- 54 | 55 | * [wio](https://gitlab.com/Rubo/wio) 56 | * [wio+](https://notabug.org/Leon_Plickat/wio-plus) -------------------------------------------------------------------------------- /draw/text.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "deedles.dev/wlr" 8 | "golang.org/x/image/font" 9 | "golang.org/x/image/font/gofont/gomono" 10 | "golang.org/x/image/font/opentype" 11 | "golang.org/x/image/font/sfnt" 12 | "golang.org/x/image/math/fixed" 13 | ) 14 | 15 | var ( 16 | fontOptions = opentype.FaceOptions{ 17 | Size: 14, 18 | DPI: 72, 19 | } 20 | 21 | gomonoFont *sfnt.Font 22 | gomonoFace font.Face 23 | ) 24 | 25 | func init() { 26 | var err error 27 | gomonoFont, err = opentype.Parse(gomono.TTF) 28 | if err != nil { 29 | panic(fmt.Errorf("parse font: %w", err)) 30 | } 31 | 32 | gomonoFace, err = opentype.NewFace(gomonoFont, &fontOptions) 33 | if err != nil { 34 | panic(fmt.Errorf("create font face: %w", err)) 35 | } 36 | } 37 | 38 | func CreateTextTexture(r wlr.Renderer, src image.Image, str string) wlr.Texture { 39 | fdraw := font.Drawer{ 40 | Src: src, 41 | Face: gomonoFace, 42 | Dot: fixed.P(0, int(fontOptions.Size)), 43 | } 44 | 45 | extents, _ := fdraw.BoundString(str) 46 | buf := image.NewNRGBA(image.Rect( 47 | 0, 48 | 0, 49 | (extents.Max.X - extents.Min.X).Floor(), 50 | int(fontOptions.Size), 51 | )) 52 | fdraw.Dst = buf 53 | fdraw.DrawString(str) 54 | 55 | return wlr.TextureFromImage(r, buf) 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module deedles.dev/kawa 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | deedles.dev/wlr v0.0.0-20250321002055-d5699d447b5c 7 | deedles.dev/ximage v0.0.0-20250321002604-9fc93f3c0d3c 8 | deedles.dev/xiter v0.2.1 9 | golang.org/x/image v0.25.0 10 | ) 11 | 12 | require golang.org/x/text v0.23.0 // indirect 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | deedles.dev/wlr v0.0.0-20250321002055-d5699d447b5c h1:Wmtlie1l10c2cPMpXO/k5jneH3n97HmwEdtCbk6d3ow= 2 | deedles.dev/wlr v0.0.0-20250321002055-d5699d447b5c/go.mod h1:CAiheor7eTJwm80MoSHAR9iIJtnpnDE2VGTUw30OiFc= 3 | deedles.dev/ximage v0.0.0-20250321002604-9fc93f3c0d3c h1:tMFXQECCS2B6Os36yfhJ98aNhroQde5brv2vSf0JRPQ= 4 | deedles.dev/ximage v0.0.0-20250321002604-9fc93f3c0d3c/go.mod h1:YnxyYpjiUD7yhS14uOWihIJ+GaB6wZFeOk3RwR8p4SM= 5 | deedles.dev/xiter v0.2.1 h1:yyyfo1sDwARp5lyMvILBJpEI28sIFN0TNYagAdBUa+s= 6 | deedles.dev/xiter v0.2.1/go.mod h1:59997UHUsKAy/8bHUClTfeXdyuLZ6z/+yF++vIpxfx8= 7 | golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= 8 | golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= 9 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 10 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 11 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "deedles.dev/wlr" 8 | "deedles.dev/wlr/xkb" 9 | "deedles.dev/ximage/geom" 10 | ) 11 | 12 | type CursorMover interface { 13 | CursorMoved(*Server, time.Time) 14 | } 15 | 16 | type CursorButtonPresser interface { 17 | CursorButtonPressed(*Server, wlr.Pointer, wlr.CursorButton, time.Time) 18 | } 19 | 20 | type CursorButtonReleaser interface { 21 | CursorButtonReleased(*Server, wlr.Pointer, wlr.CursorButton, time.Time) 22 | } 23 | 24 | type CursorRequester interface { 25 | RequestCursor(*Server, wlr.Surface, int, int) 26 | } 27 | 28 | type Keyboard struct { 29 | Device wlr.Keyboard 30 | 31 | onModifiersListener wlr.Listener 32 | onKeyListener wlr.Listener 33 | } 34 | 35 | func (server *Server) onNewInput(device wlr.InputDevice) { 36 | switch device.Type() { 37 | case wlr.InputDeviceTypeKeyboard: 38 | server.addKeyboard(device.Keyboard()) 39 | case wlr.InputDeviceTypePointer: 40 | server.addPointer(device.Pointer()) 41 | } 42 | } 43 | 44 | func (server *Server) onKeyboardModifiers(kb *Keyboard) { 45 | server.seat.SetKeyboard(kb.Device) 46 | server.seat.KeyboardNotifyModifiers(kb.Device.Modifiers()) 47 | } 48 | 49 | func (server *Server) onKeyboardKey(kb *Keyboard, code uint32, update bool, state wlr.KeyState, t time.Time) { 50 | switch state { 51 | case wlr.KeyStatePressed: 52 | server.onKeyboardKeyPressed(kb, code, update, t) 53 | case wlr.KeyStateReleased: 54 | server.onKeyboardKeyReleased(kb, code, update, t) 55 | } 56 | } 57 | 58 | func (server *Server) onKeyboardKeyPressed(kb *Keyboard, code uint32, update bool, t time.Time) { 59 | if server.handleKeyboardShortcut(kb, code, t) { 60 | return 61 | } 62 | 63 | server.seat.SetKeyboard(kb.Device) 64 | server.seat.KeyboardNotifyKey(t, code, wlr.KeyStatePressed) 65 | } 66 | 67 | func (server *Server) onKeyboardKeyReleased(kb *Keyboard, code uint32, update bool, t time.Time) { 68 | server.seat.SetKeyboard(kb.Device) 69 | server.seat.KeyboardNotifyKey(t, code, wlr.KeyStateReleased) 70 | } 71 | 72 | func (server *Server) onCursorMotion(dev wlr.Pointer, t time.Time, dx, dy float64) { 73 | server.cursor.Move(dev.Base(), dx, dy) 74 | 75 | m, ok := server.inputMode.(CursorMover) 76 | if ok { 77 | m.CursorMoved(server, t) 78 | } 79 | } 80 | 81 | func (server *Server) onCursorMotionAbsolute(dev wlr.Pointer, t time.Time, x, y float64) { 82 | server.cursor.WarpAbsolute(dev.Base(), x, y) 83 | 84 | m, ok := server.inputMode.(CursorMover) 85 | if ok { 86 | m.CursorMoved(server, t) 87 | } 88 | } 89 | 90 | func (server *Server) onCursorButton(dev wlr.Pointer, t time.Time, b wlr.CursorButton, state wlr.ButtonState) { 91 | switch state { 92 | case wlr.ButtonPressed: 93 | m, ok := server.inputMode.(CursorButtonPresser) 94 | if ok { 95 | m.CursorButtonPressed(server, dev, b, t) 96 | } 97 | case wlr.ButtonReleased: 98 | m, ok := server.inputMode.(CursorButtonReleaser) 99 | if ok { 100 | m.CursorButtonReleased(server, dev, b, t) 101 | } 102 | } 103 | } 104 | 105 | func (server *Server) onCursorAxis(dev wlr.Pointer, t time.Time, source wlr.AxisSource, orient wlr.AxisOrientation, delta float64, deltaDiscrete int32) { 106 | server.seat.PointerNotifyAxis(t, orient, delta, deltaDiscrete, source) 107 | } 108 | 109 | func (server *Server) onCursorFrame() { 110 | server.seat.PointerNotifyFrame() 111 | } 112 | 113 | func (server *Server) onRequestCursor(client wlr.SeatClient, surface wlr.Surface, serial uint32, hotspotX, hotspotY int32) { 114 | m, ok := server.inputMode.(CursorRequester) 115 | if !ok { 116 | return 117 | } 118 | 119 | focused := server.seat.PointerState().FocusedClient() 120 | if focused == client { 121 | m.RequestCursor(server, surface, int(hotspotX), int(hotspotY)) 122 | } 123 | } 124 | 125 | func (server *Server) addKeyboard(dev wlr.Keyboard) { 126 | kb := Keyboard{ 127 | Device: dev, 128 | } 129 | 130 | rules := xkb.RuleNames{ 131 | Rules: os.Getenv("XKB_DEFAULT_RULES"), 132 | Model: os.Getenv("XKB_DEFAULT_MODEL"), 133 | Layout: os.Getenv("XKB_DEFAULT_LAYOUT"), 134 | Variant: os.Getenv("XKB_DEFAULT_VARIANT"), 135 | Options: os.Getenv("XKB_DEFAULT_OPTIONS"), 136 | } 137 | 138 | ctx := xkb.NewContext(xkb.ContextNoFlags) 139 | defer ctx.Unref() 140 | 141 | keymap := xkb.NewKeymapFromNames(ctx, &rules, xkb.KeymapCompileNoFlags) 142 | defer keymap.Unref() 143 | 144 | kb.Device.SetKeymap(keymap) 145 | kb.Device.SetRepeatInfo(25, 600) 146 | 147 | kb.onModifiersListener = kb.Device.OnModifiers(func(k wlr.Keyboard) { 148 | server.onKeyboardModifiers(&kb) 149 | }) 150 | kb.onKeyListener = kb.Device.OnKey(func(k wlr.Keyboard, t time.Time, code uint32, update bool, state wlr.KeyState) { 151 | server.onKeyboardKey(&kb, code, update, state, t) 152 | }) 153 | 154 | server.seat.SetKeyboard(dev) 155 | server.keyboards = append(server.keyboards, &kb) 156 | 157 | server.seat.SetCapabilities(server.seat.Capabilities() | wlr.SeatCapabilityKeyboard) 158 | } 159 | 160 | func (server *Server) addPointer(dev wlr.Pointer) { 161 | server.cursor.AttachInputDevice(dev.Base()) 162 | server.seat.SetCapabilities(server.seat.Capabilities() | wlr.SeatCapabilityPointer) 163 | server.setCursor("left_ptr") 164 | 165 | server.pointers = append(server.pointers, dev) 166 | } 167 | 168 | func (server *Server) setCursor(name string) { 169 | if name == "" { 170 | return 171 | } 172 | 173 | if server.xwayland.Valid() { 174 | server.xwayland.SetCursor(server.cursorMgr.GetXCursor(name, 1).Image(0)) 175 | } 176 | server.cursor.SetXCursor(server.cursorMgr, name) 177 | } 178 | 179 | func (server *Server) handleKeyboardShortcut(kb *Keyboard, code uint32, t time.Time) bool { 180 | // TODO 181 | return false 182 | } 183 | 184 | func (server *Server) cursorCoords() geom.Point[float64] { 185 | return geom.Pt(server.cursor.X(), server.cursor.Y()) 186 | } 187 | -------------------------------------------------------------------------------- /internal/xflag/xflag.go: -------------------------------------------------------------------------------- 1 | package xflag 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | ) 7 | 8 | func Flag[T flag.Value](name string, value T, usage string) T { 9 | flag.Var(value, name, usage) 10 | return value 11 | } 12 | 13 | type stringsFlag []string 14 | 15 | func (s stringsFlag) String() string { 16 | return strings.Join(s, ",") 17 | } 18 | 19 | func (s *stringsFlag) Set(v string) error { 20 | *s = strings.Split(v, ",") 21 | return nil 22 | } 23 | 24 | func StringsFlag(name string, value []string, usage string) *[]string { 25 | return (*[]string)(Flag(name, (*stringsFlag)(&value), usage)) 26 | } 27 | -------------------------------------------------------------------------------- /kawa.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "iter" 8 | "log" 9 | "net/http" 10 | _ "net/http/pprof" 11 | "os" 12 | "slices" 13 | "strconv" 14 | "strings" 15 | 16 | _ "image/gif" 17 | _ "image/jpeg" 18 | _ "image/png" 19 | 20 | "deedles.dev/kawa/internal/xflag" 21 | "deedles.dev/wlr" 22 | "deedles.dev/ximage/geom" 23 | "deedles.dev/xiter" 24 | ) 25 | 26 | // parseTransform parses an OutputTransform from a string. 27 | func parseTransform(str string) (wlr.OutputTransform, error) { 28 | switch str { 29 | case "normal", "0": 30 | return wlr.OutputTransformNormal, nil 31 | case "90": 32 | return wlr.OutputTransform90, nil 33 | case "180": 34 | return wlr.OutputTransform180, nil 35 | case "270": 36 | return wlr.OutputTransform270, nil 37 | case "flipped": 38 | return wlr.OutputTransformFlipped, nil 39 | case "flipped-90": 40 | return wlr.OutputTransformFlipped90, nil 41 | case "flipped-180": 42 | return wlr.OutputTransformFlipped180, nil 43 | case "flipped-270": 44 | return wlr.OutputTransformFlipped270, nil 45 | default: 46 | return 0, fmt.Errorf("invalid transform: %q", str) 47 | } 48 | } 49 | 50 | // parseOutputConfigs parses an OutputConfig from a string. 51 | func parseOutputConfigs(outputConfigs string) iter.Seq[OutputConfig] { 52 | return func(yield func(OutputConfig) bool) { 53 | if outputConfigs == "" { 54 | return 55 | } 56 | 57 | // TODO: Handle errors. 58 | for config := range xiter.StringSplit(outputConfigs, ",") { 59 | parts := strings.Split(config, ":") 60 | c := OutputConfig{Name: parts[0]} 61 | c.X, _ = strconv.Atoi(parts[1]) 62 | c.Y, _ = strconv.Atoi(parts[2]) 63 | if len(parts) >= 5 { 64 | c.Width, _ = strconv.Atoi(parts[3]) 65 | c.Height, _ = strconv.Atoi(parts[4]) 66 | } 67 | if len(parts) >= 6 { 68 | scale, _ := strconv.ParseFloat(parts[5], 32) 69 | c.Scale = float32(scale) 70 | } 71 | if len(parts) >= 7 { 72 | c.Transform, _ = parseTransform(parts[6]) 73 | } 74 | 75 | if !yield(c) { 76 | return 77 | } 78 | } 79 | } 80 | } 81 | 82 | // init initializes the boilerplate necessary to get wlroots up and 83 | // running, as well as a few other pieces of initialization. 84 | func (server *Server) init() error { 85 | server.newViews = make(map[int]*geom.Rect[float64]) 86 | 87 | server.display = wlr.CreateDisplay() 88 | 89 | server.backend = wlr.AutocreateBackend(server.display) 90 | if !server.backend.Valid() { 91 | return errors.New("failed to create backend") 92 | } 93 | 94 | server.renderer = wlr.AutocreateRenderer(server.backend) 95 | server.renderer.InitWLSHM(server.display) 96 | 97 | server.allocator = wlr.AutocreateAllocator(server.backend, server.renderer) 98 | if !server.allocator.Valid() { 99 | return errors.New("failed to create allocator") 100 | } 101 | 102 | server.compositor = wlr.CreateCompositor(server.display, 5, server.renderer) 103 | 104 | wlr.CreateDRM(server.display, server.renderer) 105 | wlr.CreateDataDeviceManager(server.display) 106 | wlr.CreateLinuxDMABufV1WithRenderer(server.display, 1, server.renderer) 107 | wlr.CreateExportDMABufV1(server.display) 108 | wlr.CreateScreencopyManagerV1(server.display) 109 | wlr.CreateDataControlManagerV1(server.display) 110 | wlr.CreatePrimarySelectionV1DeviceManager(server.display) 111 | wlr.CreateSubcompositor(server.display) 112 | 113 | wlr.CreateGammaControlManagerV1(server.display) 114 | 115 | server.onNewOutputListener = server.backend.OnNewOutput(server.onNewOutput) 116 | 117 | server.outputLayout = wlr.CreateOutputLayout() 118 | wlr.CreateXDGOutputManagerV1(server.display, server.outputLayout) 119 | 120 | server.cursor = wlr.CreateCursor() 121 | server.cursor.AttachOutputLayout(server.outputLayout) 122 | server.cursorMgr = wlr.CreateXCursorManager("", 24) 123 | server.cursorMgr.Load(1) 124 | 125 | for _, c := range server.OutputConfigs { 126 | server.cursorMgr.Load(float64(c.Scale)) 127 | } 128 | 129 | server.onCursorMotionListener = server.cursor.OnMotion(server.onCursorMotion) 130 | server.onCursorMotionAbsoluteListener = server.cursor.OnMotionAbsolute(server.onCursorMotionAbsolute) 131 | server.onCursorButtonListener = server.cursor.OnButton(server.onCursorButton) 132 | server.onCursorAxisListener = server.cursor.OnAxis(server.onCursorAxis) 133 | server.onCursorFrameListener = server.cursor.OnFrame(server.onCursorFrame) 134 | 135 | server.onNewInputListener = server.backend.OnNewInput(server.onNewInput) 136 | 137 | server.seat = wlr.CreateSeat(server.display, "seat0") 138 | server.onRequestCursorListener = server.seat.OnRequestSetCursor(server.onRequestCursor) 139 | 140 | server.xdgShell = wlr.CreateXDGShell(server.display, 3) 141 | server.onNewXDGSurfaceListener = server.xdgShell.OnNewSurface(server.onNewXDGSurface) 142 | 143 | server.layerShell = wlr.CreateLayerShellV1(server.display, 4) 144 | server.onNewLayerSurfaceListener = server.layerShell.OnNewSurface(server.onNewLayerSurface) 145 | 146 | server.decorationManager = wlr.CreateServerDecorationManager(server.display) 147 | server.decorationManager.SetDefaultMode(wlr.ServerDecorationManagerModeServer) 148 | server.onNewDecorationListener = server.decorationManager.OnNewDecoration(server.onNewDecoration) 149 | 150 | server.xdgDecorationManager = wlr.CreateXDGDecorationManagerV1(server.display) 151 | server.onNewToplevelDecorationListener = server.xdgDecorationManager.OnNewToplevelDecoration(server.onNewToplevelDecoration) 152 | 153 | server.initUI() 154 | 155 | server.startNormal() 156 | 157 | return nil 158 | } 159 | 160 | // run runs the server's main loop. 161 | func (server *Server) run() error { 162 | defer server.Release() 163 | 164 | server.xwayland = wlr.CreateXwayland(server.display, server.compositor, false) 165 | server.onNewXwaylandSurfaceListener = server.xwayland.OnNewSurface(server.onNewXwaylandSurface) 166 | 167 | socket, err := server.display.AddSocketAuto() 168 | if err != nil { 169 | return err 170 | } 171 | 172 | err = server.backend.Start() 173 | if err != nil { 174 | return err 175 | } 176 | 177 | os.Setenv("WAYLAND_DISPLAY", socket) 178 | wlr.Log(wlr.Info, "Running Wayland compositor on WAYLAND_DISPLAY=%v", socket) 179 | 180 | if server.xwayland.Valid() { 181 | os.Setenv("DISPLAY", server.xwayland.Server().DisplayName()) 182 | wlr.Log(wlr.Info, "Running Xwayland on DISPLAY=%v", server.xwayland.Server().DisplayName()) 183 | } 184 | 185 | server.display.Run() 186 | 187 | return nil 188 | } 189 | 190 | func main() { 191 | if addr, ok := os.LookupEnv("PPROF_ADDR"); ok { 192 | go func() { log.Println(http.ListenAndServe(addr, nil)) }() 193 | } 194 | 195 | wlr.InitLog(wlr.Debug, nil) 196 | 197 | terms := xflag.StringsFlag("terms", []string{"sakura", "alacritty"}, "preferentially ordered list of terminals for new windows to use") 198 | bg := flag.String("bg", "", "background image") 199 | bgScale := flag.String("bgscale", "stretch", "background image scaling method (stretch, center, fit, fill)") 200 | outputConfigs := flag.String("out", "", "output configs (name:x:y[:width:height][:scale][:transform])") 201 | flag.Parse() 202 | 203 | outputConfigsParsed := parseOutputConfigs(*outputConfigs) 204 | server := Server{ 205 | Terms: *terms, 206 | OutputConfigs: slices.Collect(outputConfigsParsed), 207 | } 208 | 209 | err := server.init() 210 | if err != nil { 211 | wlr.Log(wlr.Error, "init server: %v", err) 212 | os.Exit(1) 213 | } 214 | 215 | if *bg != "" { 216 | server.loadBG(*bg) 217 | switch *bgScale { 218 | case "stretch": 219 | server.bgScale = scaleStretch 220 | case "center": 221 | server.bgScale = scaleCenter 222 | case "fit": 223 | server.bgScale = scaleFit 224 | case "fill": 225 | server.bgScale = scaleFill 226 | default: 227 | wlr.Log(wlr.Error, "unknown scaling method: %q", *bgScale) 228 | } 229 | } 230 | 231 | err = server.run() 232 | if err != nil { 233 | wlr.Log(wlr.Error, "run server: %v", err) 234 | os.Exit(1) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /layers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | 6 | "deedles.dev/wlr" 7 | ) 8 | 9 | type LayerSurface struct { 10 | LayerSurface wlr.LayerSurfaceV1 11 | Geo image.Rectangle 12 | 13 | onDestroyListener wlr.Listener 14 | onMapListener wlr.Listener 15 | onSurfaceCommitListener wlr.Listener 16 | onOutputDestroyListener wlr.Listener 17 | } 18 | 19 | func (server *Server) onNewLayerSurface(surface wlr.LayerSurfaceV1) { 20 | panic("Not implemented.") 21 | } 22 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "iter" 6 | "slices" 7 | 8 | "deedles.dev/kawa/draw" 9 | "deedles.dev/wlr" 10 | "deedles.dev/ximage/geom" 11 | ) 12 | 13 | var ( 14 | menuItemInset = geom.Pt(WindowBorder, WindowBorder) 15 | ) 16 | 17 | type Menu struct { 18 | items []*MenuItem 19 | bounds []geom.Rect[float64] 20 | prev *MenuItem 21 | } 22 | 23 | func NewMenu(items ...*MenuItem) *Menu { 24 | return NewMenuFromSeq(slices.Values(items), len(items)) 25 | } 26 | 27 | func NewMenuFromSeq(items iter.Seq[*MenuItem], numitems int) *Menu { 28 | m := Menu{ 29 | items: make([]*MenuItem, 0, numitems), 30 | bounds: make([]geom.Rect[float64], 0, numitems), 31 | } 32 | for item := range items { 33 | m.add(item) 34 | } 35 | m.updateBounds(false) 36 | return &m 37 | } 38 | 39 | func (m *Menu) Items() iter.Seq2[*MenuItem, geom.Rect[float64]] { 40 | return func(yield func(*MenuItem, geom.Rect[float64]) bool) { 41 | for i, item := range m.items { 42 | if !yield(item, m.bounds[i]) { 43 | return 44 | } 45 | } 46 | } 47 | } 48 | 49 | func (m *Menu) Len() int { 50 | return len(m.items) 51 | } 52 | 53 | func (m *Menu) updateBounds(shrink bool) { 54 | if shrink { 55 | for i := range m.bounds { 56 | m.bounds[i] = geom.Rect[float64]{ 57 | Max: geom.PConv[float64](m.items[i].Size().Add(menuItemInset)), 58 | } 59 | } 60 | } 61 | 62 | geom.ArrangeVerticalStack(m.bounds) 63 | } 64 | 65 | func (m *Menu) Item(i int) *MenuItem { 66 | return m.items[i] 67 | } 68 | 69 | func (m *Menu) Bounds() (b geom.Rect[float64]) { 70 | return geom.Rect[float64]{ 71 | Min: m.bounds[0].Min, 72 | Max: m.bounds[len(m.bounds)-1].Max, 73 | } 74 | } 75 | 76 | func (m *Menu) Select(item *MenuItem) { 77 | i := slices.Index(m.items, item) 78 | if i < 0 { 79 | return 80 | } 81 | 82 | item.OnSelect() 83 | m.prev = item 84 | } 85 | 86 | func (m *Menu) Prev() *MenuItem { 87 | i := slices.Index(m.items, m.prev) 88 | if i < 0 { 89 | return nil 90 | } 91 | return m.prev 92 | } 93 | 94 | func (m *Menu) ItemAt(p geom.Point[float64]) *MenuItem { 95 | for i, ib := range m.bounds { 96 | if p.In(ib) { 97 | return m.items[i] 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | func (m *Menu) ItemBounds(item *MenuItem) geom.Rect[float64] { 104 | i := slices.Index(m.items, item) 105 | if i < 0 { 106 | return geom.Rect[float64]{} 107 | } 108 | return m.bounds[i] 109 | } 110 | 111 | func (m *Menu) add(item *MenuItem) { 112 | m.items = append(m.items, item) 113 | m.bounds = append(m.bounds, geom.Rect[float64]{ 114 | Max: geom.PConv[float64](item.Size().Add(menuItemInset)), 115 | }) 116 | } 117 | 118 | func (m *Menu) Add(item *MenuItem) { 119 | m.add(item) 120 | m.updateBounds(false) 121 | } 122 | 123 | func (m *Menu) Remove(item *MenuItem) { 124 | i := slices.Index(m.items, item) 125 | m.items = slices.Delete(m.items, i, i+1) 126 | m.bounds = slices.Delete(m.bounds, i, i+1) 127 | m.updateBounds(true) 128 | } 129 | 130 | type MenuItem struct { 131 | OnSelect func() 132 | 133 | active wlr.Texture 134 | inactive wlr.Texture 135 | } 136 | 137 | func NewMenuItem(active, inactive wlr.Texture) *MenuItem { 138 | if (active.Width() != inactive.Width()) || (active.Height() != inactive.Height()) { 139 | panic("active and inactive sizes do no match") 140 | } 141 | 142 | return &MenuItem{ 143 | active: active, 144 | inactive: inactive, 145 | } 146 | } 147 | 148 | func NewTextMenuItem(renderer wlr.Renderer, text string) *MenuItem { 149 | return NewMenuItem( 150 | draw.CreateTextTexture(renderer, image.White, text), 151 | draw.CreateTextTexture(renderer, image.Black, text), 152 | ) 153 | } 154 | 155 | func (item *MenuItem) Size() geom.Point[int] { 156 | return geom.Rt(0, 0, item.active.Width(), item.active.Height()). 157 | Union(geom.Rt(0, 0, item.inactive.Width(), item.inactive.Height())). 158 | Size() 159 | } 160 | 161 | func (item *MenuItem) Release() { 162 | item.active.Destroy() 163 | item.inactive.Destroy() 164 | } 165 | -------------------------------------------------------------------------------- /mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "slices" 6 | "time" 7 | 8 | "deedles.dev/wlr" 9 | "deedles.dev/ximage/geom" 10 | ) 11 | 12 | type InputMode any 13 | 14 | type inputModeNormal struct { 15 | inView bool 16 | prevEdges wlr.Edges 17 | } 18 | 19 | func (server *Server) startNormal() { 20 | server.setCursor("left_ptr") 21 | server.inputMode = &inputModeNormal{} 22 | } 23 | 24 | func (m *inputModeNormal) CursorMoved(server *Server, t time.Time) { 25 | cc := server.cursorCoords() 26 | 27 | view, edges, surface, sp := server.viewAt(nil, cc) 28 | if edges != m.prevEdges { 29 | cursor := interactCursor 30 | if !server.isViewTiled(view) { 31 | cursor = edgeCursors[edges] 32 | m.prevEdges = edges 33 | } 34 | server.setCursor(cursor) 35 | } 36 | if (view == nil) && m.inView { 37 | server.setCursor("left_ptr") 38 | } 39 | m.inView = view != nil 40 | if !surface.Valid() { 41 | server.seat.PointerNotifyClearFocus() 42 | return 43 | } 44 | 45 | server.seat.PointerNotifyEnter(surface, sp.X, sp.Y) 46 | server.seat.PointerNotifyMotion(t, sp.X, sp.Y) 47 | } 48 | 49 | func (m *inputModeNormal) CursorButtonPressed(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 50 | cc := server.cursorCoords() 51 | 52 | forceMenu := server.seat.GetKeyboard().GetModifiers()&wlr.KeyboardModifierLogo != 0 53 | if !forceMenu { 54 | out := server.outputAt(cc) 55 | forceMenu = (out == server.statusBar.Output()) && (cc.Y <= StatusBarHeight) 56 | } 57 | if forceMenu { 58 | switch b { 59 | case wlr.BtnLeft: 60 | server.startMenu(server.systemMenu, b) 61 | case wlr.BtnRight: 62 | server.startMenu(server.mainMenu, b) 63 | } 64 | return 65 | } 66 | 67 | view, edges, surface, _ := server.viewAt(nil, cc) 68 | if view == nil { 69 | switch b { 70 | case wlr.BtnRight: 71 | server.startMenu(server.mainMenu, b) 72 | } 73 | return 74 | } 75 | 76 | server.focusView(view, surface) 77 | 78 | switch edges { 79 | case wlr.EdgeNone: 80 | server.seat.PointerNotifyButton(t, b, wlr.ButtonPressed) 81 | default: 82 | switch b { 83 | case wlr.BtnLeft: 84 | if !server.isViewTiled(view) { 85 | server.startBorderResize(view, edges) 86 | } 87 | case wlr.BtnRight: 88 | server.startMove(view) 89 | } 90 | } 91 | } 92 | 93 | func (m *inputModeNormal) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 94 | server.seat.PointerNotifyButton(t, b, wlr.ButtonReleased) 95 | } 96 | 97 | func (m *inputModeNormal) RequestCursor(server *Server, s wlr.Surface, x, y int) { 98 | server.cursor.SetSurface(s, int32(x), int32(y)) 99 | } 100 | 101 | type inputModeMove struct { 102 | view *View 103 | off geom.Point[float64] 104 | } 105 | 106 | func (server *Server) startMove(view *View) { 107 | server.setCursor("grabbing") 108 | server.focusView(view, view.Surface()) 109 | 110 | cc := server.cursorCoords() 111 | server.inputMode = &inputModeMove{ 112 | view: view, 113 | off: cc.Sub(view.Coords), 114 | } 115 | } 116 | 117 | func (m *inputModeMove) CursorMoved(server *Server, t time.Time) { 118 | cc := server.cursorCoords() 119 | 120 | if server.isViewTiled(m.view) { 121 | i, _, _, _ := server.viewIndexAt(nil, server.tiled, cc) 122 | if i >= 0 { 123 | vi := slices.Index(server.tiled, m.view) 124 | server.tiled[i], server.tiled[vi] = server.tiled[vi], server.tiled[i] 125 | server.layoutTiles(nil) 126 | } 127 | return 128 | } 129 | 130 | to := cc.Sub(m.off) 131 | 132 | //out := server.outputAt(cc) 133 | //if out != nil { 134 | // sbb := server.statusBar.Bounds() 135 | // sbb.Max.Y += WindowBorder 136 | // if cc.In(sbb) { 137 | // to.Y = m.view.Coords.Y 138 | // } 139 | //} 140 | 141 | server.moveViewTo(nil, m.view, to) 142 | } 143 | 144 | func (m *inputModeMove) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 145 | server.startNormal() 146 | } 147 | 148 | func (m *inputModeMove) TargetView() *View { 149 | return m.view 150 | } 151 | 152 | type inputModeBorderResize struct { 153 | view *View 154 | edges wlr.Edges 155 | cur geom.Rect[float64] 156 | } 157 | 158 | func (server *Server) startBorderResize(view *View, edges wlr.Edges) { 159 | from := view.Bounds() 160 | server.startBorderResizeFrom(view, edges, from) 161 | } 162 | 163 | func (server *Server) startBorderResizeFrom(view *View, edges wlr.Edges, from geom.Rect[float64]) { 164 | view.SetResizing(true) 165 | server.focusView(view, view.Surface()) 166 | server.inputMode = &inputModeBorderResize{ 167 | view: view, 168 | edges: edges, 169 | cur: from, 170 | } 171 | } 172 | 173 | func (m *inputModeBorderResize) CursorMoved(server *Server, t time.Time) { 174 | cc := server.cursorCoords() 175 | 176 | min := geom.Pt( 177 | math.Max(MinWidth, m.view.MinWidth()), 178 | math.Max(MinHeight, m.view.MinHeight()), 179 | ) 180 | 181 | if m.edges&wlr.EdgeTop != 0 { 182 | m.cur.Min.Y = cc.Y 183 | if m.cur.Dy() < min.Y { 184 | m.cur.Min.Y = m.cur.Max.Y - min.Y 185 | } 186 | } 187 | if m.edges&wlr.EdgeBottom != 0 { 188 | m.cur.Max.Y = cc.Y 189 | if m.cur.Dy() < min.Y { 190 | m.cur.Max.Y = m.cur.Min.Y + min.Y 191 | } 192 | } 193 | if m.edges&wlr.EdgeLeft != 0 { 194 | m.cur.Min.X = cc.X 195 | if m.cur.Dx() < min.X { 196 | m.cur.Min.X = m.cur.Max.X - min.X 197 | } 198 | } 199 | if m.edges&wlr.EdgeRight != 0 { 200 | m.cur.Max.X = cc.X 201 | if m.cur.Dx() < min.X { 202 | m.cur.Max.X = m.cur.Min.X + min.X 203 | } 204 | } 205 | 206 | if cc.X < m.cur.Min.X { 207 | m.edges |= wlr.EdgeLeft 208 | m.edges &^= wlr.EdgeRight 209 | server.setCursor(edgeCursors[m.edges]) 210 | } 211 | if cc.X > m.cur.Max.X { 212 | m.edges |= wlr.EdgeRight 213 | m.edges &^= wlr.EdgeLeft 214 | server.setCursor(edgeCursors[m.edges]) 215 | } 216 | if cc.Y < m.cur.Min.Y { 217 | m.edges |= wlr.EdgeTop 218 | m.edges &^= wlr.EdgeBottom 219 | server.setCursor(edgeCursors[m.edges]) 220 | } 221 | if cc.Y > m.cur.Max.Y { 222 | m.edges |= wlr.EdgeBottom 223 | m.edges &^= wlr.EdgeTop 224 | server.setCursor(edgeCursors[m.edges]) 225 | } 226 | 227 | server.resizeViewTo(nil, m.view, m.cur) 228 | } 229 | 230 | func (m *inputModeBorderResize) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 231 | m.view.SetResizing(false) 232 | server.startNormal() 233 | } 234 | 235 | func (m *inputModeBorderResize) TargetView() *View { 236 | return m.view 237 | } 238 | 239 | type inputModeMenu struct { 240 | m *Menu 241 | p geom.Point[float64] 242 | sel *MenuItem 243 | btn wlr.CursorButton 244 | } 245 | 246 | func (server *Server) startMenu(m *Menu, btn wlr.CursorButton) { 247 | cc := server.cursorCoords() 248 | ob := server.outputBounds(server.outputAt(cc)).Inset(2 * WindowBorder) 249 | 250 | ib := m.ItemBounds(server.mainMenu.Prev()) 251 | if ib.IsZero() { 252 | ib = m.ItemBounds(m.Item(0)) 253 | } 254 | mb := m.Bounds().Sub(ib.Center()).Add(cc) 255 | mb = mb.ClosestIn(ob) 256 | 257 | mode := inputModeMenu{ 258 | m: m, 259 | p: mb.Min, 260 | btn: btn, 261 | } 262 | mode.CursorMoved(server, time.Now()) 263 | server.inputMode = &mode 264 | } 265 | 266 | func (m *inputModeMenu) CursorMoved(server *Server, t time.Time) { 267 | cc := server.cursorCoords().Sub(m.p) 268 | m.sel = m.m.ItemAt(cc) 269 | } 270 | 271 | func (m *inputModeMenu) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 272 | if b != m.btn { 273 | return 274 | } 275 | 276 | server.startNormal() 277 | m.m.Select(m.sel) 278 | } 279 | 280 | func (m *inputModeMenu) Frame(server *Server, out *Output) { 281 | server.renderMenu(out, m.m, m.p, m.sel) 282 | } 283 | 284 | type inputModeSelectView struct { 285 | startBtn wlr.CursorButton 286 | then func(*View) 287 | } 288 | 289 | func (server *Server) startSelectView(b wlr.CursorButton, then func(*View)) { 290 | server.setCursor("hand1") 291 | server.inputMode = &inputModeSelectView{ 292 | startBtn: b, 293 | then: then, 294 | } 295 | } 296 | 297 | func (m *inputModeSelectView) CursorButtonPressed(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 298 | if b != m.startBtn { 299 | server.startNormal() 300 | return 301 | } 302 | 303 | cc := server.cursorCoords() 304 | view, _, _, _ := server.viewAt(nil, cc) 305 | if view != nil { 306 | m.then(view) 307 | return 308 | } 309 | server.startNormal() 310 | } 311 | 312 | type inputModeResize struct { 313 | view *View 314 | s geom.Point[float64] 315 | resizing bool 316 | } 317 | 318 | func (server *Server) startResize(view *View) { 319 | server.setCursor("top_left_corner") 320 | server.inputMode = &inputModeResize{ 321 | view: view, 322 | } 323 | } 324 | 325 | func (m *inputModeResize) CursorMoved(server *Server, t time.Time) { 326 | if !m.resizing { 327 | return 328 | } 329 | 330 | cc := server.cursorCoords() 331 | r := geom.Rect[float64]{Min: m.s, Max: cc}.Canon() 332 | if r.Dx() < math.Max(MinWidth, m.view.MinWidth()) { 333 | return 334 | } 335 | if r.Dy() < math.Max(MinHeight, m.view.MinHeight()) { 336 | return 337 | } 338 | 339 | if server.isViewTiled(m.view) { 340 | server.untileView(m.view, false) 341 | } 342 | 343 | server.startBorderResizeFrom(m.view, wlr.EdgeNone, r) 344 | } 345 | 346 | func (m *inputModeResize) CursorButtonPressed(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 347 | if b != wlr.BtnRight { 348 | server.startNormal() 349 | return 350 | } 351 | 352 | m.s = server.cursorCoords() 353 | m.resizing = true 354 | } 355 | 356 | func (m *inputModeResize) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 357 | if !m.resizing { 358 | return 359 | } 360 | 361 | server.startNormal() 362 | } 363 | 364 | func (m *inputModeResize) Frame(server *Server, out *Output) { 365 | if !m.resizing { 366 | return 367 | } 368 | 369 | cc := server.cursorCoords() 370 | r := geom.Rect[float64]{Min: m.s, Max: cc} 371 | server.renderSelectionBox(out, r) 372 | } 373 | 374 | func (m *inputModeResize) TargetView() *View { 375 | return m.view 376 | } 377 | 378 | type inputModeNew struct { 379 | n geom.Rect[float64] 380 | dragging bool 381 | started bool 382 | } 383 | 384 | func (server *Server) startNew() { 385 | server.setCursor("top_left_corner") 386 | server.inputMode = &inputModeNew{} 387 | } 388 | 389 | func (m *inputModeNew) CursorMoved(server *Server, t time.Time) { 390 | if !m.dragging { 391 | return 392 | } 393 | 394 | cc := server.cursorCoords() 395 | m.n.Max = cc 396 | 397 | if math.Abs(cc.X-float64(m.n.Min.X)) < MinWidth { 398 | return 399 | } 400 | if math.Abs(cc.Y-float64(m.n.Min.Y)) < MinHeight { 401 | return 402 | } 403 | 404 | if !m.started { 405 | server.exec(&m.n) 406 | m.started = true 407 | } 408 | } 409 | 410 | func (m *inputModeNew) CursorButtonPressed(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 411 | if b != wlr.BtnRight { 412 | server.startNormal() 413 | return 414 | } 415 | 416 | m.n.Min = server.cursorCoords() 417 | m.n.Max = m.n.Min 418 | m.dragging = true 419 | } 420 | 421 | func (m *inputModeNew) CursorButtonReleased(server *Server, dev wlr.Pointer, b wlr.CursorButton, t time.Time) { 422 | if !m.dragging { 423 | return 424 | } 425 | 426 | server.startNormal() 427 | } 428 | 429 | func (m *inputModeNew) Frame(server *Server, out *Output) { 430 | if !m.dragging || m.started { 431 | return 432 | } 433 | 434 | server.renderSelectionBox(out, m.n) 435 | } 436 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "deedles.dev/wlr" 5 | "deedles.dev/ximage/geom" 6 | ) 7 | 8 | type Output struct { 9 | Output wlr.Output 10 | Layers [4][]LayerSurface 11 | 12 | onFrameListener wlr.Listener 13 | } 14 | 15 | type OutputConfig struct { 16 | Name string 17 | X, Y int 18 | Width, Height int 19 | Scale float32 20 | Transform wlr.OutputTransform 21 | } 22 | 23 | func (server *Server) outputAt(p geom.Point[float64]) *Output { 24 | wout := server.outputLayout.OutputAt(p.X, p.Y) 25 | for _, out := range server.outputs { 26 | if out.Output == wout { 27 | return out 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func (server *Server) outputBounds(out *Output) geom.Rect[float64] { 34 | x, y := server.outputLayout.OutputCoords(out.Output) 35 | return geom.Rt(0, 0, float64(out.Output.Width()), float64(out.Output.Height())).Add(geom.Pt(x, y)) 36 | } 37 | 38 | func (server *Server) outputTilingBounds(out *Output) geom.Rect[float64] { 39 | b := server.outputBounds(out) 40 | if out == server.statusBar.Output() { 41 | return b.Pad(StatusBarHeight, 0, 0, 0) 42 | } 43 | return b 44 | } 45 | 46 | func (server *Server) statusBarBounds() geom.Rect[float64] { 47 | b := server.outputBounds(server.statusBar.Output()) 48 | b.Max.Y = b.Min.Y + StatusBarHeight 49 | return b 50 | } 51 | 52 | func (server *Server) onNewOutput(wout wlr.Output) { 53 | out := Output{ 54 | Output: wout, 55 | } 56 | out.onFrameListener = wout.OnFrame(func(wout wlr.Output) { 57 | server.onFrame(&out) 58 | }) 59 | server.addOutput(&out) 60 | 61 | if server.statusBar == nil { 62 | server.statusBar = NewStatusBar(&out) 63 | } 64 | 65 | wout.InitRender(server.allocator, server.renderer) 66 | wout.Commit() 67 | wout.CreateGlobal() 68 | } 69 | 70 | func (server *Server) addOutput(out *Output) { 71 | server.outputs = append(server.outputs, out) 72 | 73 | for _, config := range server.OutputConfigs { 74 | if config.Name != out.Output.Name() { 75 | continue 76 | } 77 | 78 | server.configureOutput(out, &config) 79 | return 80 | } 81 | 82 | server.configureOutput(out, nil) 83 | } 84 | 85 | func (server *Server) configureOutput(out *Output, config *OutputConfig) { 86 | server.setOutputMode(out, config) 87 | server.layoutOutput(out, config) 88 | out.Output.Enable(true) 89 | 90 | if config == nil { 91 | return 92 | } 93 | 94 | if config.Scale != 0 { 95 | out.Output.SetScale(config.Scale) 96 | } 97 | 98 | if config.Transform != 0 { 99 | out.Output.SetTransform(config.Transform) 100 | } 101 | } 102 | 103 | func (server *Server) layoutOutput(out *Output, config *OutputConfig) { 104 | if (config == nil) || (config.X == -1) && (config.Y == -1) { 105 | server.outputLayout.AddAuto(out.Output) 106 | return 107 | } 108 | 109 | server.outputLayout.Add(out.Output, config.X, config.Y) 110 | } 111 | 112 | func (server *Server) setOutputMode(out *Output, config *OutputConfig) { 113 | if (config == nil) || (config.Width == 0) || (config.Height == 0) { 114 | return 115 | } 116 | 117 | modes := out.Output.Modes() 118 | for mode := range modes { 119 | if (mode.Width() == int32(config.Width)) && (mode.Height() == int32(config.Height)) { 120 | out.Output.SetMode(mode) 121 | return 122 | } 123 | } 124 | 125 | mode := out.Output.PreferredMode() 126 | if mode.Valid() { 127 | out.Output.SetMode(mode) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "time" 7 | 8 | "deedles.dev/wlr" 9 | "deedles.dev/ximage/geom" 10 | ) 11 | 12 | type Framer interface { 13 | Frame(*Server, *Output) 14 | } 15 | 16 | func (server *Server) onFrame(out *Output) { 17 | _, err := out.Output.AttachRender() 18 | if err != nil { 19 | wlr.Log(wlr.Error, "output attach render: %v", err) 20 | return 21 | } 22 | defer out.Output.Commit() 23 | 24 | server.renderer.Begin(out.Output, out.Output.Width(), out.Output.Height()) 25 | defer server.renderer.End() 26 | 27 | server.renderer.Clear(ColorBackground) 28 | server.renderBG(out) 29 | server.renderLayer(out, wlr.LayerShellV1LayerBackground) 30 | server.renderLayer(out, wlr.LayerShellV1LayerBottom) 31 | server.renderViews(out) 32 | server.renderNewViews(out) 33 | server.renderLayer(out, wlr.LayerShellV1LayerTop) 34 | server.renderLayer(out, wlr.LayerShellV1LayerOverlay) 35 | if server.statusBar.Output() == out { 36 | server.renderStatusBar() 37 | } 38 | server.renderMode(out) 39 | server.renderCursor(out) 40 | } 41 | 42 | func (server *Server) renderBG(out *Output) { 43 | if !server.bg.Valid() { 44 | return 45 | } 46 | 47 | to := server.outputTilingBounds(out) 48 | r := geom.RConv[float64](geom.Rt(0, 0, server.bg.Width(), server.bg.Height())) 49 | 50 | m := wlr.ProjectBoxMatrix( 51 | server.bgScale(to, r).ImageRect(), 52 | wlr.OutputTransformNormal, 53 | 0, 54 | out.Output.TransformMatrix(), 55 | ) 56 | server.renderer.RenderTextureWithMatrix(server.bg, m, 1) 57 | } 58 | 59 | func (server *Server) renderViews(out *Output) { 60 | for _, view := range server.tiled { 61 | if !view.Mapped() { 62 | continue 63 | } 64 | 65 | server.renderView(out, view) 66 | } 67 | 68 | for _, view := range server.views { 69 | if !view.Mapped() { 70 | continue 71 | } 72 | 73 | server.renderView(out, view) 74 | } 75 | } 76 | 77 | func (server *Server) renderView(out *Output, view *View) { 78 | if !view.CSD { 79 | server.renderViewBorder(out, view) 80 | } 81 | server.renderViewSurfaces(out, view) 82 | } 83 | 84 | func (server *Server) renderViewBorder(out *Output, view *View) { 85 | color := ColorInactiveBorder 86 | if view.Activated() { 87 | color = ColorActiveBorder 88 | } 89 | if server.targetView() == view { 90 | color = ColorSelectionBox 91 | } 92 | 93 | r := view.Bounds().Inset(-WindowBorder) 94 | server.renderRectBorder(out, geom.RConv[float64](r), color) 95 | } 96 | 97 | func (server *Server) renderViewSurfaces(out *Output, view *View) { 98 | for s := range view.Surfaces() { 99 | p := geom.Pt(s.X, s.Y) 100 | server.renderSurface(out, s.Surface, geom.PConv[int](view.Coords).Add(p)) 101 | } 102 | } 103 | 104 | func (server *Server) renderNewViews(out *Output) { 105 | for _, nv := range server.newViews { 106 | server.renderSelectionBox(out, *nv) 107 | } 108 | } 109 | 110 | func (server *Server) renderLayer(out *Output, layer wlr.LayerShellV1Layer) { 111 | // TODO 112 | } 113 | 114 | func (server *Server) renderRectBorder(out *Output, r geom.Rect[float64], color color.Color) { 115 | server.renderer.RenderRect(geom.Rt(0, 0, WindowBorder, r.Dy()).Add(r.Min).ImageRect(), color, out.Output.TransformMatrix()) 116 | server.renderer.RenderRect(geom.Rt(0, 0, WindowBorder, r.Dy()).Add(geom.Pt(r.Max.X-WindowBorder, r.Min.Y)).ImageRect(), color, out.Output.TransformMatrix()) 117 | server.renderer.RenderRect(geom.Rt(0, 0, r.Dx(), WindowBorder).Add(r.Min).ImageRect(), color, out.Output.TransformMatrix()) 118 | server.renderer.RenderRect(geom.Rt(0, 0, r.Dx(), WindowBorder).Add(geom.Pt(r.Min.X, r.Max.Y-WindowBorder)).ImageRect(), color, out.Output.TransformMatrix()) 119 | } 120 | 121 | func (server *Server) renderSelectionBox(out *Output, r geom.Rect[float64]) { 122 | r = r.Canon() 123 | server.renderRectBorder(out, r, ColorSelectionBox) 124 | server.renderer.RenderRect(r.Inset(WindowBorder).ImageRect(), ColorSelectionBackground, out.Output.TransformMatrix()) 125 | } 126 | 127 | func (server *Server) renderSurface(out *Output, s wlr.Surface, p geom.Point[int]) { 128 | texture := s.GetTexture() 129 | if !texture.Valid() { 130 | wlr.Log(wlr.Error, "invalid texture for surface") 131 | return 132 | } 133 | 134 | r := surfaceBounds(s).Add(geom.PConv[int](p)) 135 | tr := s.Current().Transform().Invert() 136 | m := wlr.ProjectBoxMatrix(r.ImageRect(), tr, 0, out.Output.TransformMatrix()) 137 | 138 | server.renderer.RenderTextureWithMatrix(texture, m, 1) 139 | s.SendFrameDone(time.Now()) 140 | } 141 | 142 | func (server *Server) renderStatusBar() { 143 | out := server.statusBar.Output() 144 | tm := out.Output.TransformMatrix() 145 | 146 | b := server.statusBarBounds() 147 | server.renderer.RenderRect(b.ImageRect(), ColorMenuBorder, tm) 148 | 149 | if title := server.statusBar.Title(); title.Valid() { 150 | tb := geom.Rt(0, 0, float64(title.Width()), float64(title.Height())) 151 | tb = geom.Align(b, tb, geom.EdgeLeft) 152 | tb = tb.Add(geom.Pt[float64](WindowBorder, 0)) 153 | m := wlr.ProjectBoxMatrix(tb.ImageRect(), wlr.OutputTransformNormal, 0, tm) 154 | server.renderer.RenderTextureWithMatrix(title, m, 1) 155 | } 156 | } 157 | 158 | func (server *Server) renderMode(out *Output) { 159 | m, ok := server.inputMode.(Framer) 160 | if !ok { 161 | return 162 | } 163 | 164 | m.Frame(server, out) 165 | } 166 | 167 | func (server *Server) renderCursor(out *Output) { 168 | out.Output.RenderSoftwareCursors(image.Rectangle{}) 169 | } 170 | 171 | func (server *Server) renderMenu(out *Output, m *Menu, p geom.Point[float64], sel *MenuItem) { 172 | r := m.Bounds().Add(p) 173 | server.renderer.RenderRect(r.Inset(-WindowBorder/2).ImageRect(), ColorMenuBorder, out.Output.TransformMatrix()) 174 | server.renderer.RenderRect(r.ImageRect(), ColorMenuUnselected, out.Output.TransformMatrix()) 175 | 176 | for item, bounds := range m.Items() { 177 | ar := bounds.Add(p) 178 | tr := geom.Rt(0, 0, float64(item.active.Width()), float64(item.active.Height())).CenterAt(ar.Center()) 179 | 180 | t := item.inactive 181 | if item == sel { 182 | t = item.active 183 | server.renderer.RenderRect(ar.ImageRect(), ColorMenuSelected, out.Output.TransformMatrix()) 184 | } 185 | 186 | matrix := wlr.ProjectBoxMatrix(tr.ImageRect(), wlr.OutputTransformNormal, 0, out.Output.TransformMatrix()) 187 | server.renderer.RenderTextureWithMatrix(t, matrix, 1) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "deedles.dev/wlr" 10 | "deedles.dev/ximage/geom" 11 | ) 12 | 13 | var ( 14 | mainMenuText = []string{ 15 | "New", 16 | "Resize", 17 | "Tile", 18 | "Move", 19 | "Close", 20 | "Hide", 21 | } 22 | 23 | systemMenuText = []string{ 24 | "Log Out", 25 | } 26 | ) 27 | 28 | type Server struct { 29 | Terms []string 30 | OutputConfigs []OutputConfig 31 | 32 | display wlr.Display 33 | 34 | allocator wlr.Allocator 35 | backend wlr.Backend 36 | compositor wlr.Compositor 37 | cursor wlr.Cursor 38 | outputLayout wlr.OutputLayout 39 | renderer wlr.Renderer 40 | seat wlr.Seat 41 | cursorMgr wlr.XCursorManager 42 | xdgShell wlr.XDGShell 43 | layerShell wlr.LayerShellV1 44 | xwayland wlr.Xwayland 45 | decorationManager wlr.ServerDecorationManager 46 | xdgDecorationManager wlr.XDGDecorationManagerV1 47 | 48 | outputs []*Output 49 | //inputs []wlr.InputDevice 50 | pointers []wlr.Pointer 51 | keyboards []*Keyboard 52 | views []*View 53 | tiled []*View 54 | hidden []*View 55 | newViews map[int]*geom.Rect[float64] 56 | 57 | bg wlr.Texture 58 | bgScale scaleFunc 59 | 60 | mainMenu *Menu 61 | systemMenu *Menu 62 | 63 | statusBar *StatusBar 64 | 65 | inputMode InputMode 66 | 67 | onNewOutputListener wlr.Listener 68 | onNewInputListener wlr.Listener 69 | onCursorMotionListener wlr.Listener 70 | onCursorMotionAbsoluteListener wlr.Listener 71 | onCursorButtonListener wlr.Listener 72 | onCursorAxisListener wlr.Listener 73 | onCursorFrameListener wlr.Listener 74 | onRequestCursorListener wlr.Listener 75 | onNewXDGSurfaceListener wlr.Listener 76 | onNewXwaylandSurfaceListener wlr.Listener 77 | onNewLayerSurfaceListener wlr.Listener 78 | onNewDecorationListener wlr.Listener 79 | onNewToplevelDecorationListener wlr.Listener 80 | } 81 | 82 | func (server *Server) Release() { 83 | server.onNewOutputListener.Destroy() 84 | server.onNewInputListener.Destroy() 85 | server.onCursorMotionListener.Destroy() 86 | server.onCursorMotionAbsoluteListener.Destroy() 87 | server.onCursorButtonListener.Destroy() 88 | server.onCursorAxisListener.Destroy() 89 | server.onCursorFrameListener.Destroy() 90 | server.onRequestCursorListener.Destroy() 91 | server.onNewXDGSurfaceListener.Destroy() 92 | server.onNewXwaylandSurfaceListener.Destroy() 93 | server.onNewLayerSurfaceListener.Destroy() 94 | server.onNewDecorationListener.Destroy() 95 | server.onNewToplevelDecorationListener.Destroy() 96 | } 97 | 98 | func (server *Server) Shutdown() { 99 | server.display.Terminate() 100 | } 101 | 102 | func (server *Server) loadBG(path string) { 103 | file, err := os.Open(path) 104 | if err != nil { 105 | wlr.Log(wlr.Error, "load %q as background: %v", path, err) 106 | return 107 | } 108 | defer file.Close() 109 | 110 | img, _, err := image.Decode(file) 111 | if err != nil { 112 | wlr.Log(wlr.Error, "decode %q as background: %v", path, err) 113 | return 114 | } 115 | 116 | if server.bg.Valid() { 117 | server.bg.Destroy() 118 | } 119 | server.bg = wlr.TextureFromImage(server.renderer, img) 120 | wlr.Log(wlr.Info, "loaded %q as background", path) 121 | } 122 | 123 | func (server *Server) exec(to *geom.Rect[float64]) { 124 | for _, term := range server.Terms { 125 | args := strings.Fields(term) 126 | cmd := exec.Command(args[0], args[1:]...) // TODO: Context support? 127 | err := cmd.Start() 128 | if err != nil { 129 | wlr.Log(wlr.Error, "start new with %q: %v", term, err) 130 | continue 131 | } 132 | 133 | server.newViews[cmd.Process.Pid] = to 134 | return 135 | } 136 | 137 | wlr.Log(wlr.Error, "no valid terminals found for new window") 138 | } 139 | 140 | func (server *Server) initUI() { 141 | server.initMainMenu() 142 | server.initSystemMenu() 143 | } 144 | 145 | func (server *Server) initMainMenu() { 146 | cbs := []func(){ 147 | server.onMainMenuNew, 148 | server.onMainMenuResize, 149 | server.onMainMenuTile, 150 | server.onMainMenuMove, 151 | server.onMainMenuClose, 152 | server.onMainMenuHide, 153 | } 154 | 155 | items := func(yield func(*MenuItem) bool) { 156 | for i, text := range mainMenuText { 157 | item := NewTextMenuItem(server.renderer, text) 158 | item.OnSelect = cbs[i] 159 | if !yield(item) { 160 | return 161 | } 162 | } 163 | } 164 | 165 | server.mainMenu = NewMenuFromSeq(items, len(mainMenuText)) 166 | } 167 | 168 | func (server *Server) onMainMenuNew() { 169 | server.startNew() 170 | } 171 | 172 | func (server *Server) onMainMenuResize() { 173 | server.startSelectView(wlr.BtnRight, func(view *View) { 174 | server.startResize(view) 175 | }) 176 | } 177 | 178 | func (server *Server) onMainMenuTile() { 179 | server.startSelectView(wlr.BtnRight, func(view *View) { 180 | server.toggleViewTiling(view) 181 | server.startNormal() 182 | }) 183 | } 184 | 185 | func (server *Server) onMainMenuMove() { 186 | server.startSelectView(wlr.BtnRight, func(view *View) { 187 | server.startMove(view) 188 | }) 189 | } 190 | 191 | func (server *Server) onMainMenuClose() { 192 | server.startSelectView(wlr.BtnRight, func(view *View) { 193 | server.closeView(view) 194 | server.startNormal() 195 | }) 196 | } 197 | 198 | func (server *Server) onMainMenuHide() { 199 | server.startSelectView(wlr.BtnRight, func(view *View) { 200 | server.hideView(view) 201 | server.startNormal() 202 | }) 203 | } 204 | 205 | func (server *Server) initSystemMenu() { 206 | cbs := []func(){ 207 | server.onSystemMenuLogOut, 208 | } 209 | 210 | items := func(yield func(*MenuItem) bool) { 211 | for i, text := range systemMenuText { 212 | item := NewTextMenuItem(server.renderer, text) 213 | item.OnSelect = cbs[i] 214 | if !yield(item) { 215 | return 216 | } 217 | } 218 | } 219 | 220 | server.systemMenu = NewMenuFromSeq(items, len(systemMenuText)) 221 | } 222 | 223 | func (server *Server) onSystemMenuLogOut() { 224 | server.Shutdown() 225 | } 226 | -------------------------------------------------------------------------------- /statusbar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | 6 | "deedles.dev/kawa/draw" 7 | "deedles.dev/wlr" 8 | ) 9 | 10 | type StatusBar struct { 11 | out *Output 12 | title wlr.Texture 13 | } 14 | 15 | func NewStatusBar(out *Output) *StatusBar { 16 | return &StatusBar{ 17 | out: out, 18 | } 19 | } 20 | 21 | func (s *StatusBar) SetTitle(r wlr.Renderer, str string) { 22 | if s.title.Valid() { 23 | s.title.Destroy() 24 | s.title = wlr.Texture{} 25 | } 26 | if str == "" { 27 | return 28 | } 29 | 30 | s.title = draw.CreateTextTexture(r, image.White, str) 31 | } 32 | 33 | func (s *StatusBar) Title() wlr.Texture { 34 | return s.title 35 | } 36 | 37 | func (s *StatusBar) Output() *Output { 38 | return s.out 39 | } 40 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "deedles.dev/ximage/geom" 7 | ) 8 | 9 | const ( 10 | MinWidth = 128 11 | MinHeight = 24 12 | 13 | WindowBorder = 5 14 | StatusBarHeight = 5 * WindowBorder 15 | ) 16 | 17 | var ( 18 | ColorBackground = color.NRGBA{0x77, 0x77, 0x77, 0xFF} 19 | ColorSelectionBox = color.NRGBA{0xFF, 0x0, 0x0, 0xFF} 20 | ColorSelectionBackground = color.NRGBA{0xFF, 0xFF, 0xFF, 0xFF / 100} 21 | ColorActiveBorder = color.NRGBA{0x50, 0xA1, 0xAD, 0xFF} 22 | ColorInactiveBorder = color.NRGBA{0x9C, 0xE9, 0xE9, 0xFF} 23 | ColorMenuSelected = color.NRGBA{0x3D, 0x7D, 0x42, 0xFF} 24 | ColorMenuUnselected = color.NRGBA{0xEB, 0xFF, 0xEC, 0xFF} 25 | ColorMenuBorder = color.NRGBA{0x78, 0xAD, 0x84, 0xFF} 26 | ColorSurface = color.NRGBA{0xEE, 0xEE, 0xEE, 0xFF} 27 | ) 28 | 29 | var ( 30 | DefaultRestore = geom.Rt[float64](0, 0, 640, 480).Add(geom.Pt[float64](10, 10)) 31 | ) 32 | 33 | type scaleFunc func(out, r geom.Rect[float64]) geom.Rect[float64] 34 | 35 | func scaleStretch(out, r geom.Rect[float64]) geom.Rect[float64] { 36 | return out 37 | } 38 | 39 | func scaleCenter(out, r geom.Rect[float64]) geom.Rect[float64] { 40 | return r.CenterAt(out.Center()) 41 | } 42 | 43 | func scaleFit(out, r geom.Rect[float64]) geom.Rect[float64] { 44 | if (r.Dx() < out.Dx()) && (r.Dy() < out.Dy()) { 45 | return r 46 | } 47 | return scaleFill(out, r) 48 | } 49 | 50 | func scaleFill(out, r geom.Rect[float64]) geom.Rect[float64] { 51 | return scaleCenter(out, r.FitTo(out.Size())) 52 | } 53 | 54 | func scaleTile(out, r geom.Rect[float64]) geom.Rect[float64] { 55 | // TODO 56 | return scaleCenter(out, r) 57 | } 58 | -------------------------------------------------------------------------------- /surface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "iter" 6 | 7 | "deedles.dev/wlr" 8 | "deedles.dev/ximage/geom" 9 | ) 10 | 11 | type ViewSurface interface { 12 | io.Closer 13 | 14 | Title() string 15 | PID() int 16 | Surface() wlr.Surface 17 | SetResizing(bool) 18 | SetMinimized(bool) 19 | SetMaximized(bool) 20 | 21 | Resize(w, h int) 22 | Geometry() geom.Rect[int] 23 | MinWidth() float64 24 | MinHeight() float64 25 | 26 | Mapped() bool 27 | Activated() bool 28 | SetActivated(bool) 29 | 30 | Surfaces() iter.Seq[wlr.IterSurface] 31 | SurfaceAt(geom.Point[float64]) (s wlr.Surface, sp geom.Point[float64], ok bool) 32 | HasSurface(wlr.Surface) bool 33 | } 34 | 35 | type viewSurfaceXDG struct { 36 | s wlr.XDGSurface 37 | } 38 | 39 | func (s *viewSurfaceXDG) PID() int { 40 | client := s.s.Resource().GetClient() 41 | pid, _, _ := client.GetCredentials() 42 | return pid 43 | } 44 | 45 | func (s *viewSurfaceXDG) HasSurface(surface wlr.Surface) (has bool) { 46 | return s.s.HasSurface(surface) 47 | } 48 | 49 | func (s *viewSurfaceXDG) Close() error { 50 | s.s.Toplevel().SendClose() 51 | return nil 52 | } 53 | 54 | func (s *viewSurfaceXDG) Title() string { 55 | return s.s.Toplevel().Title() 56 | } 57 | 58 | func (s *viewSurfaceXDG) Resize(w, h int) { 59 | s.s.Toplevel().SetSize(int32(w), int32(h)) 60 | } 61 | 62 | func (s *viewSurfaceXDG) SetResizing(resizing bool) { 63 | s.s.Toplevel().SetResizing(resizing) 64 | } 65 | 66 | func (s *viewSurfaceXDG) SetMinimized(m bool) { 67 | // Apparently XDG clients can't be minimized. Huh. 68 | } 69 | 70 | func (s *viewSurfaceXDG) SetMaximized(m bool) { 71 | s.s.Toplevel().SetMaximized(m) 72 | } 73 | 74 | func (s *viewSurfaceXDG) Geometry() geom.Rect[int] { 75 | return geom.FromImageRect(s.s.GetGeometry()) 76 | } 77 | 78 | func (s *viewSurfaceXDG) MinWidth() float64 { 79 | return float64(s.s.Toplevel().Current().MinWidth()) 80 | } 81 | 82 | func (s *viewSurfaceXDG) MinHeight() float64 { 83 | return float64(s.s.Toplevel().Current().MinHeight()) 84 | } 85 | 86 | func (s *viewSurfaceXDG) Surface() wlr.Surface { 87 | return s.s.Surface() 88 | } 89 | 90 | func (s *viewSurfaceXDG) Mapped() bool { 91 | return s.s.Surface().Mapped() 92 | } 93 | 94 | func (s *viewSurfaceXDG) SetActivated(a bool) { 95 | s.s.Toplevel().SetActivated(a) 96 | } 97 | 98 | func (s *viewSurfaceXDG) Activated() bool { 99 | return s.s.Toplevel().Current().Activated() 100 | } 101 | 102 | func (s *viewSurfaceXDG) Surfaces() iter.Seq[wlr.IterSurface] { 103 | return s.s.Surfaces() 104 | } 105 | 106 | func (s *viewSurfaceXDG) SurfaceAt(p geom.Point[float64]) (surface wlr.Surface, sp geom.Point[float64], ok bool) { 107 | surface, sx, sy, ok := s.s.SurfaceAt(p.X, p.Y) 108 | return surface, geom.Pt(sx, sy), ok 109 | } 110 | 111 | type viewSurfaceXwayland struct { 112 | s wlr.XwaylandSurface 113 | activated bool 114 | } 115 | 116 | func (s *viewSurfaceXwayland) PID() int { 117 | return -1 // There doesn't seem to be a way to get this... 118 | } 119 | 120 | func (s *viewSurfaceXwayland) HasSurface(surface wlr.Surface) (has bool) { 121 | return s.s.Surface().HasSurface(surface) 122 | } 123 | 124 | func (s *viewSurfaceXwayland) Close() error { 125 | s.s.Close() 126 | return nil 127 | } 128 | 129 | func (s *viewSurfaceXwayland) Title() string { 130 | return s.s.Title() 131 | } 132 | 133 | func (s *viewSurfaceXwayland) Resize(w, h int) { 134 | s.s.Configure(0, 0, uint16(w), uint16(h)) 135 | } 136 | 137 | func (s *viewSurfaceXwayland) SetResizing(resizing bool) { 138 | // Doesn't make sense for Xwayland clients, it seems. 139 | } 140 | 141 | func (s *viewSurfaceXwayland) SetMinimized(m bool) { 142 | s.s.SetMinimized(m) 143 | } 144 | 145 | func (s *viewSurfaceXwayland) SetMaximized(m bool) { 146 | s.s.SetMaximized(m) 147 | } 148 | 149 | func (s *viewSurfaceXwayland) Geometry() geom.Rect[int] { 150 | return geom.Rt(0, 0, s.s.Width(), s.s.Height()) 151 | } 152 | 153 | func (s *viewSurfaceXwayland) MinWidth() float64 { 154 | return MinWidth 155 | } 156 | 157 | func (s *viewSurfaceXwayland) MinHeight() float64 { 158 | return MinHeight 159 | } 160 | 161 | func (s *viewSurfaceXwayland) Surface() wlr.Surface { 162 | return s.s.Surface() 163 | } 164 | 165 | func (s *viewSurfaceXwayland) Mapped() bool { 166 | return s.s.Surface().Mapped() 167 | } 168 | 169 | func (s *viewSurfaceXwayland) SetActivated(a bool) { 170 | s.s.Activate(a) 171 | s.activated = a 172 | } 173 | 174 | func (s *viewSurfaceXwayland) Activated() bool { 175 | return s.activated 176 | } 177 | 178 | func (s *viewSurfaceXwayland) Surfaces() iter.Seq[wlr.IterSurface] { 179 | return s.s.Surface().Surfaces() 180 | } 181 | 182 | func (s *viewSurfaceXwayland) SurfaceAt(p geom.Point[float64]) (surface wlr.Surface, sp geom.Point[float64], ok bool) { 183 | surface, sx, sy, ok := s.s.Surface().SurfaceAt(p.X, p.Y) 184 | return surface, geom.Pt(sx, sy), ok 185 | } 186 | -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | 7 | "deedles.dev/wlr" 8 | "deedles.dev/ximage/geom" 9 | "deedles.dev/xiter" 10 | ) 11 | 12 | type ViewTargeter interface { 13 | TargetView() *View 14 | } 15 | 16 | var edgeCursors = [...]string{ 17 | wlr.EdgeNone: "", 18 | wlr.EdgeTop: "top_side", 19 | wlr.EdgeLeft: "left_side", 20 | wlr.EdgeRight: "right_side", 21 | wlr.EdgeBottom: "bottom_side", 22 | wlr.EdgeTop | wlr.EdgeLeft: "top_left_corner", 23 | wlr.EdgeTop | wlr.EdgeRight: "top_right_corner", 24 | wlr.EdgeBottom | wlr.EdgeLeft: "bottom_left_corner", 25 | wlr.EdgeBottom | wlr.EdgeRight: "bottom_right_corner", 26 | } 27 | 28 | const ( 29 | moveCursor = "move" 30 | interactCursor = "hand" 31 | ) 32 | 33 | type View struct { 34 | ViewSurface 35 | Coords geom.Point[float64] 36 | Restore geom.Rect[float64] 37 | CSD bool 38 | 39 | popups []*Popup 40 | 41 | onMapListener wlr.Listener 42 | onDestroyListener wlr.Listener 43 | onRequestMoveListener wlr.Listener 44 | onRequestResizeListener wlr.Listener 45 | onRequestMinimizeListener wlr.Listener 46 | onRequestMaximizeListener wlr.Listener 47 | onSetTitleListener wlr.Listener 48 | } 49 | 50 | func (view *View) Release() { 51 | view.onDestroyListener.Destroy() 52 | view.onMapListener.Destroy() 53 | view.onRequestMoveListener.Destroy() 54 | view.onRequestResizeListener.Destroy() 55 | view.onRequestMinimizeListener.Destroy() 56 | view.onRequestMaximizeListener.Destroy() 57 | view.onSetTitleListener.Destroy() 58 | } 59 | 60 | func (view *View) Bounds() geom.Rect[float64] { 61 | return geom.RConv[float64](view.Geometry()).Add(view.Coords) 62 | } 63 | 64 | func (view *View) addPopup(surface wlr.XDGSurface) { 65 | p := Popup{ 66 | Surface: surface, 67 | } 68 | p.onDestroyListener = surface.OnDestroy(func(s wlr.XDGSurface) { 69 | view.onDestroyPopup(&p) 70 | }) 71 | 72 | view.popups = append(view.popups, &p) 73 | } 74 | 75 | func (view *View) onDestroyPopup(p *Popup) { 76 | p.Release() 77 | 78 | i := slices.Index(view.popups, p) 79 | view.popups = slices.Delete(view.popups, i, i+1) 80 | } 81 | 82 | func (view *View) isPopupSurface(surface wlr.Surface) (ok bool) { 83 | for _, p := range view.popups { 84 | for s := range p.Surface.Surfaces() { 85 | if s.Surface == surface { 86 | return true 87 | } 88 | } 89 | } 90 | return false 91 | } 92 | 93 | func surfaceBounds(s wlr.Surface) geom.Rect[int] { 94 | c := s.Current() 95 | return geom.Rt(0, 0, c.Width(), c.Height()) 96 | } 97 | 98 | type Popup struct { 99 | Surface wlr.XDGSurface 100 | 101 | onDestroyListener wlr.Listener 102 | } 103 | 104 | func (p *Popup) Release() { 105 | p.onDestroyListener.Destroy() 106 | } 107 | 108 | func (server *Server) targetView() *View { 109 | m, ok := server.inputMode.(ViewTargeter) 110 | if !ok { 111 | return nil 112 | } 113 | 114 | return m.TargetView() 115 | } 116 | 117 | func (server *Server) viewAt(out *Output, p geom.Point[float64]) (*View, wlr.Edges, wlr.Surface, geom.Point[float64]) { 118 | if out == nil { 119 | out = server.outputAt(p) 120 | } 121 | 122 | i, edges, surface, sp := server.viewIndexAt(out, server.views, p) 123 | if i >= 0 { 124 | return server.views[i], edges, surface, sp 125 | } 126 | 127 | i, edges, surface, sp = server.viewIndexAt(out, server.tiled, p) 128 | if i >= 0 { 129 | return server.tiled[i], edges, surface, sp 130 | } 131 | 132 | return nil, wlr.EdgeNone, wlr.Surface{}, geom.Point[float64]{} 133 | } 134 | 135 | func (server *Server) viewIndexAt(out *Output, views []*View, p geom.Point[float64]) (int, wlr.Edges, wlr.Surface, geom.Point[float64]) { 136 | for i := len(views) - 1; i >= 0; i-- { 137 | view := views[i] 138 | if !view.Mapped() { 139 | continue 140 | } 141 | 142 | edges, surface, sp, ok := server.isViewAt(out, view, p) 143 | if ok { 144 | return i, edges, surface, sp 145 | } 146 | } 147 | 148 | return -1, 0, wlr.Surface{}, geom.Point[float64]{} 149 | } 150 | 151 | func (server *Server) isViewAt(out *Output, view *View, p geom.Point[float64]) (edges wlr.Edges, s wlr.Surface, sp geom.Point[float64], ok bool) { 152 | surface, sp, ok := view.SurfaceAt(p.Sub(view.Coords)) 153 | if ok { 154 | return wlr.EdgeNone, surface, sp, true 155 | } 156 | 157 | // Don't bother checking the borders if there aren't any. 158 | if view.CSD { 159 | return 0, wlr.Surface{}, geom.Point[float64]{}, false 160 | } 161 | 162 | r := view.Bounds() 163 | if !p.In(r.Inset(-WindowBorder)) { 164 | return 0, wlr.Surface{}, geom.Point[float64]{}, false 165 | } 166 | 167 | left := geom.Rt(r.Min.X-WindowBorder, r.Min.Y, r.Max.X, r.Max.Y) 168 | if p.In(left) { 169 | return wlr.EdgeLeft, wlr.Surface{}, geom.Point[float64]{}, true 170 | } 171 | 172 | top := geom.Rt(r.Min.X, r.Min.Y-WindowBorder, r.Max.X, r.Max.Y) 173 | if p.In(top) { 174 | return wlr.EdgeTop, wlr.Surface{}, geom.Point[float64]{}, true 175 | } 176 | 177 | right := geom.Rt(r.Min.X, r.Min.Y, r.Max.X+WindowBorder, r.Max.Y) 178 | if p.In(right) { 179 | return wlr.EdgeRight, wlr.Surface{}, geom.Point[float64]{}, true 180 | } 181 | 182 | bottom := geom.Rt(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y+WindowBorder) 183 | if p.In(bottom) { 184 | return wlr.EdgeBottom, wlr.Surface{}, geom.Point[float64]{}, true 185 | } 186 | 187 | if (p.X < r.Min.X) && (p.Y < r.Min.Y) { 188 | return wlr.EdgeTop | wlr.EdgeLeft, wlr.Surface{}, geom.Point[float64]{}, true 189 | } 190 | if (p.X >= r.Max.X) && (p.Y < r.Min.Y) { 191 | return wlr.EdgeTop | wlr.EdgeRight, wlr.Surface{}, geom.Point[float64]{}, true 192 | } 193 | if (p.X < r.Min.X) && (p.Y >= r.Max.Y) { 194 | return wlr.EdgeBottom | wlr.EdgeLeft, wlr.Surface{}, geom.Point[float64]{}, true 195 | } 196 | if (p.X >= r.Max.X) && (p.Y >= r.Max.Y) { 197 | return wlr.EdgeBottom | wlr.EdgeRight, wlr.Surface{}, geom.Point[float64]{}, true 198 | } 199 | 200 | // Where else could it possibly be if it gets to here? 201 | panic(fmt.Errorf("this should not have happened\np = %+v\nr = %+v", p, r)) 202 | } 203 | 204 | func (server *Server) onNewXwaylandSurface(surface wlr.XwaylandSurface) { 205 | view := View{ 206 | CSD: false, 207 | ViewSurface: &viewSurfaceXwayland{s: surface}, 208 | } 209 | view.onDestroyListener = surface.OnDestroy(func(s wlr.XwaylandSurface) { 210 | server.onDestroyView(&view) 211 | }) 212 | view.onMapListener = surface.Surface().OnMap(func(s wlr.Surface) { 213 | server.onMapView(&view) 214 | }) 215 | view.onRequestMoveListener = surface.OnRequestMove(func(s wlr.XwaylandSurface) { 216 | server.startMove(&view) 217 | }) 218 | view.onRequestResizeListener = surface.OnRequestResize(func(s wlr.XwaylandSurface, edges wlr.Edges) { 219 | if !server.isViewTiled(&view) { 220 | server.startBorderResize(&view, edges) 221 | } 222 | }) 223 | view.onRequestMinimizeListener = surface.OnRequestMinimize(func(s wlr.XwaylandSurface) { 224 | server.hideView(&view) 225 | }) 226 | view.onRequestMaximizeListener = surface.OnRequestMaximize(func(s wlr.XwaylandSurface) { 227 | server.toggleViewTiling(&view) 228 | }) 229 | view.onSetTitleListener = surface.OnSetTitle(func(s wlr.XwaylandSurface, title string) { 230 | server.updateTitles() 231 | }) 232 | 233 | server.addView(&view) 234 | } 235 | 236 | func (server *Server) onNewXDGSurface(surface wlr.XDGSurface) { 237 | switch surface.Role() { 238 | case wlr.XDGSurfaceRoleToplevel: 239 | server.addXDGToplevel(surface) 240 | case wlr.XDGSurfaceRolePopup: 241 | server.addXDGPopup(surface) 242 | case wlr.XDGSurfaceRoleNone: 243 | // TODO 244 | } 245 | } 246 | 247 | func (server *Server) addXDGPopup(surface wlr.XDGSurface) { 248 | parent := server.viewForSurface(surface.Popup().Parent()) 249 | if parent == nil { 250 | wlr.Log(wlr.Debug, "parent of popup could not be found") 251 | return 252 | } 253 | 254 | parent.addPopup(surface) 255 | } 256 | 257 | func (server *Server) addXDGToplevel(surface wlr.XDGSurface) { 258 | view := View{ 259 | CSD: true, 260 | ViewSurface: &viewSurfaceXDG{s: surface}, 261 | } 262 | view.onDestroyListener = surface.OnDestroy(func(s wlr.XDGSurface) { 263 | server.onDestroyView(&view) 264 | }) 265 | view.onMapListener = surface.Surface().OnMap(func(s wlr.Surface) { 266 | server.onMapView(&view) 267 | }) 268 | view.onRequestMoveListener = surface.Toplevel().OnRequestMove(func(t wlr.XDGToplevel, client wlr.SeatClient, serial uint32) { 269 | server.startMove(&view) 270 | }) 271 | view.onRequestResizeListener = surface.Toplevel().OnRequestResize(func(t wlr.XDGToplevel, client wlr.SeatClient, serial uint32, edges wlr.Edges) { 272 | if !server.isViewTiled(&view) { 273 | server.startBorderResize(&view, edges) 274 | } 275 | }) 276 | view.onRequestMinimizeListener = surface.Toplevel().OnRequestMinimize(func(t wlr.XDGToplevel) { 277 | server.hideView(&view) 278 | }) 279 | view.onRequestMaximizeListener = surface.Toplevel().OnRequestMaximize(func(t wlr.XDGToplevel) { 280 | server.toggleViewTiling(&view) 281 | }) 282 | view.onSetTitleListener = surface.Toplevel().OnSetTitle(func(t wlr.XDGToplevel, title string) { 283 | server.updateTitles() 284 | }) 285 | 286 | server.addView(&view) 287 | } 288 | 289 | func (server *Server) onDestroyView(view *View) { 290 | view.Release() 291 | 292 | i := slices.Index(server.views, view) 293 | if i >= 0 { 294 | server.views = slices.Delete(server.views, i, i+1) 295 | } 296 | i = slices.Index(server.tiled, view) 297 | if i >= 0 { 298 | server.tiled = slices.Delete(server.tiled, i, i+1) 299 | server.layoutTiles(nil) 300 | } 301 | 302 | server.updateTitles() 303 | allviews := xiter.Concat(slices.Values(server.tiled), slices.Values(server.views)) 304 | if n, ok := xiter.Drain(allviews); ok { 305 | server.focusView(n, n.Surface()) 306 | } 307 | } 308 | 309 | func (server *Server) onMapView(view *View) { 310 | pid := view.PID() 311 | 312 | nv, ok := server.newViews[pid] 313 | if ok { 314 | delete(server.newViews, pid) 315 | server.startBorderResizeFrom(view, wlr.EdgeNone, *nv) 316 | return 317 | } 318 | 319 | out := server.outputAt(server.cursorCoords()) 320 | if out == nil { 321 | if len(server.outputs) == 0 { 322 | return 323 | } 324 | out = server.outputs[0] 325 | } 326 | 327 | server.centerViewOnOutput(out, view) 328 | } 329 | 330 | func (server *Server) addView(view *View) { 331 | server.views = append(server.views, view) 332 | 333 | nv, ok := server.newViews[view.PID()] 334 | if ok { 335 | server.resizeViewTo(nil, view, *nv) 336 | } 337 | } 338 | 339 | func (server *Server) centerViewOnOutput(out *Output, view *View) { 340 | ob := server.outputBounds(out) 341 | vb := view.Bounds() 342 | p := vb.CenterAt(ob.Center()) 343 | 344 | server.moveViewTo(out, view, p.Min) 345 | } 346 | 347 | func (server *Server) moveViewTo(out *Output, view *View, p geom.Point[float64]) { 348 | if out == nil { 349 | out = server.outputAt(p) 350 | } 351 | 352 | view.Coords = p 353 | 354 | if out != nil { 355 | view.Surface().SendEnter(out.Output) 356 | } 357 | } 358 | 359 | func (server *Server) resizeViewTo(out *Output, view *View, r geom.Rect[float64]) { 360 | if out == nil { 361 | out = server.outputAt(r.Min) 362 | } 363 | 364 | vb := view.Bounds() 365 | off := view.Coords.Sub(vb.Min) 366 | r = r.Add(off).Canon() 367 | 368 | view.Coords = r.Min 369 | view.Resize(int(r.Dx()), int(r.Dy())) 370 | 371 | if out != nil { 372 | view.Surface().SendEnter(out.Output) 373 | } 374 | } 375 | 376 | func (server *Server) focusView(view *View, s wlr.Surface) { 377 | if !s.Valid() { 378 | if !view.Mapped() { 379 | return 380 | } 381 | s = view.Surface() 382 | } 383 | 384 | pv := server.focusedView() 385 | if pv == view { 386 | return 387 | } 388 | if pv != nil { 389 | pv.SetActivated(false) 390 | } 391 | 392 | k := server.seat.GetKeyboard() 393 | server.seat.KeyboardNotifyEnter(s, k.Keycodes(), k.Modifiers()) 394 | 395 | view.SetActivated(true) 396 | server.bringViewToFront(view) 397 | 398 | server.updateTitles() 399 | } 400 | 401 | func (server *Server) focusedView() *View { 402 | s := server.seat.KeyboardState().FocusedSurface() 403 | return server.viewForSurface(s) 404 | } 405 | 406 | func (server *Server) viewForSurface(s wlr.Surface) *View { 407 | for _, view := range server.views { 408 | if view.HasSurface(s) { 409 | return view 410 | } 411 | } 412 | for _, view := range server.tiled { 413 | if view.HasSurface(s) { 414 | return view 415 | } 416 | } 417 | for _, view := range server.hidden { 418 | if view.HasSurface(s) { 419 | return view 420 | } 421 | } 422 | 423 | return nil 424 | } 425 | 426 | func (server *Server) bringViewToFront(view *View) { 427 | if server.isViewTiled(view) { 428 | return 429 | } 430 | 431 | i := slices.Index(server.views, view) 432 | server.views = slices.Delete(server.views, i, i+1) 433 | server.views = append(server.views, view) 434 | } 435 | 436 | func (server *Server) hideView(view *View) { 437 | // TODO: Remember whether or not a hidden view was tiled. 438 | if server.isViewTiled(view) { 439 | server.untileView(view, true) 440 | } 441 | i := slices.Index(server.views, view) 442 | if i >= 0 { 443 | server.views = slices.Delete(server.views, i, i+1) 444 | } 445 | 446 | server.hidden = append(server.hidden, view) 447 | view.SetMinimized(true) 448 | 449 | item := NewTextMenuItem(server.renderer, view.Title()) 450 | item.OnSelect = func() { 451 | server.unhideView(view) 452 | } 453 | server.mainMenu.Add(item) 454 | } 455 | 456 | func (server *Server) unhideView(view *View) { 457 | i := slices.Index(server.hidden, view) 458 | server.hidden = slices.Delete(server.hidden, i, i+1) 459 | 460 | mi := server.mainMenu.Item(len(mainMenuText) + i) 461 | server.mainMenu.Remove(mi) 462 | mi.Release() 463 | 464 | server.views = append(server.views, view) 465 | server.focusView(view, view.Surface()) 466 | view.SetMinimized(false) 467 | } 468 | 469 | func (server *Server) toggleViewTiling(view *View) { 470 | if server.isViewTiled(view) { 471 | server.untileView(view, true) 472 | return 473 | } 474 | server.tileView(view) 475 | } 476 | 477 | func (server *Server) tileView(view *View) { 478 | if !view.Mapped() { 479 | return 480 | } 481 | 482 | i := slices.Index(server.views, view) 483 | server.views = slices.Delete(server.views, i, i+1) 484 | server.tiled = append(server.tiled, view) 485 | 486 | view.Restore = DefaultRestore 487 | if s := view.Surface(); s.Valid() { 488 | view.Restore = view.Bounds() 489 | } 490 | view.SetMaximized(true) // TODO: Fix the race condition between this and resizing the view. 491 | 492 | server.layoutTiles(nil) 493 | server.focusView(view, view.Surface()) 494 | } 495 | 496 | func (server *Server) untileView(view *View, restore bool) { 497 | i := slices.Index(server.tiled, view) 498 | server.tiled = slices.Delete(server.tiled, i, i+1) 499 | server.views = append(server.views, view) 500 | 501 | server.layoutTiles(nil) 502 | server.focusView(view, view.Surface()) 503 | 504 | view.SetMaximized(false) 505 | if restore && !view.Restore.IsZero() { 506 | server.resizeViewTo(nil, view, view.Restore) 507 | } 508 | } 509 | 510 | func (server *Server) layoutTiles(out *Output) { 511 | if len(server.tiled) == 0 { 512 | return 513 | } 514 | 515 | if out == nil { 516 | out = server.outputs[0] 517 | } 518 | 519 | or := server.outputTilingBounds(out) 520 | tiles := geom.TiledRows(len(server.tiled), or, 4) 521 | for i, tile := range xiter.Enumerate(tiles) { 522 | tile = tile.Inset(3 * WindowBorder) 523 | server.resizeViewTo(out, server.tiled[i], tile) 524 | } 525 | } 526 | 527 | func (server *Server) isViewTiled(view *View) bool { 528 | return slices.Contains(server.tiled, view) 529 | } 530 | 531 | func (server *Server) closeView(view *View) { 532 | view.Close() 533 | } 534 | 535 | func (server *Server) onNewDecoration(dm wlr.ServerDecorationManager, d wlr.ServerDecoration) { 536 | var view *View 537 | for s := range d.Surface().Surfaces() { 538 | if view == nil { 539 | view = server.viewForSurface(s.Surface) 540 | } 541 | } 542 | if view == nil { 543 | return 544 | } 545 | 546 | view.CSD = d.Mode() != wlr.ServerDecorationManagerModeServer 547 | 548 | var onModeListener, onDestroyListener wlr.Listener 549 | onModeListener = d.OnMode(func(d wlr.ServerDecoration) { 550 | view.CSD = d.Mode() != wlr.ServerDecorationManagerModeServer 551 | }) 552 | onDestroyListener = d.OnDestroy(func(d wlr.ServerDecoration) { 553 | onModeListener.Destroy() 554 | onDestroyListener.Destroy() 555 | }) 556 | } 557 | 558 | func (server *Server) onNewToplevelDecoration(dm wlr.XDGDecorationManagerV1, d wlr.XDGToplevelDecorationV1) { 559 | var view *View 560 | for s := range d.Toplevel().Base().Surfaces() { 561 | if view == nil { 562 | view = server.viewForSurface(s.Surface) 563 | } 564 | } 565 | if view == nil { 566 | // If there's no view, there's probably no point. 567 | return 568 | } 569 | 570 | view.CSD = false 571 | d.SetMode(wlr.XDGToplevelDecorationV1ModeServerSide) 572 | 573 | var onDestroyListener wlr.Listener 574 | onDestroyListener = d.OnDestroy(func(d wlr.XDGToplevelDecorationV1) { 575 | onDestroyListener.Destroy() 576 | }) 577 | } 578 | 579 | func (server *Server) updateTitles() { 580 | // Not the best way to do this, perhaps... 581 | for _, view := range server.hidden { 582 | item := server.mainMenu.Item(len(mainMenuText)) 583 | item.Release() 584 | 585 | n := NewTextMenuItem(server.renderer, view.Title()) 586 | n.OnSelect = item.OnSelect 587 | 588 | server.mainMenu.Remove(item) 589 | item.Release() 590 | server.mainMenu.Add(n) 591 | } 592 | 593 | var focusedTitle string 594 | if fv := server.focusedView(); fv != nil { 595 | focusedTitle = fv.Title() 596 | } 597 | server.statusBar.SetTitle(server.renderer, focusedTitle) 598 | } 599 | --------------------------------------------------------------------------------