summaryrefslogtreecommitdiff
path: root/Services
diff options
context:
space:
mode:
Diffstat (limited to 'Services')
-rw-r--r--Services/AudioServer/ASClientConnection.cpp161
-rw-r--r--Services/AudioServer/ASClientConnection.h69
-rw-r--r--Services/AudioServer/ASMixer.cpp152
-rw-r--r--Services/AudioServer/ASMixer.h139
-rw-r--r--Services/AudioServer/AudioClient.ipc5
-rw-r--r--Services/AudioServer/AudioServer.ipc21
-rw-r--r--Services/AudioServer/Makefile24
-rw-r--r--Services/AudioServer/main.cpp61
-rw-r--r--Services/DHCPClient/DHCPv4.cpp59
-rw-r--r--Services/DHCPClient/DHCPv4.h300
-rw-r--r--Services/DHCPClient/DHCPv4Client.cpp283
-rw-r--r--Services/DHCPClient/DHCPv4Client.h80
-rw-r--r--Services/DHCPClient/Makefile10
-rw-r--r--Services/DHCPClient/main.cpp101
-rw-r--r--Services/LookupServer/DNSAnswer.cpp46
-rw-r--r--Services/LookupServer/DNSAnswer.h51
-rw-r--r--Services/LookupServer/DNSPacket.h116
-rw-r--r--Services/LookupServer/DNSQuestion.h59
-rw-r--r--Services/LookupServer/DNSRequest.cpp98
-rw-r--r--Services/LookupServer/DNSRequest.h65
-rw-r--r--Services/LookupServer/DNSResponse.cpp147
-rw-r--r--Services/LookupServer/DNSResponse.h78
-rw-r--r--Services/LookupServer/LookupServer.cpp259
-rw-r--r--Services/LookupServer/LookupServer.h56
-rw-r--r--Services/LookupServer/Makefile12
-rw-r--r--Services/LookupServer/main.cpp53
-rw-r--r--Services/Makefile3
-rw-r--r--Services/NotificationServer/ClientConnection.cpp63
-rw-r--r--Services/NotificationServer/ClientConnection.h49
-rw-r--r--Services/NotificationServer/Makefile24
-rw-r--r--Services/NotificationServer/NotificationClient.ipc4
-rw-r--r--Services/NotificationServer/NotificationServer.ipc7
-rw-r--r--Services/NotificationServer/NotificationWindow.cpp124
-rw-r--r--Services/NotificationServer/NotificationWindow.h46
-rw-r--r--Services/NotificationServer/main.cpp71
-rw-r--r--Services/ProtocolServer/Download.cpp92
-rw-r--r--Services/ProtocolServer/Download.h70
-rw-r--r--Services/ProtocolServer/HttpDownload.cpp60
-rw-r--r--Services/ProtocolServer/HttpDownload.h45
-rw-r--r--Services/ProtocolServer/HttpProtocol.cpp50
-rw-r--r--Services/ProtocolServer/HttpProtocol.h37
-rw-r--r--Services/ProtocolServer/HttpsDownload.cpp60
-rw-r--r--Services/ProtocolServer/HttpsDownload.h45
-rw-r--r--Services/ProtocolServer/HttpsProtocol.cpp50
-rw-r--r--Services/ProtocolServer/HttpsProtocol.h37
-rw-r--r--Services/ProtocolServer/Makefile25
-rw-r--r--Services/ProtocolServer/PSClientConnection.cpp112
-rw-r--r--Services/ProtocolServer/PSClientConnection.h55
-rw-r--r--Services/ProtocolServer/Protocol.cpp49
-rw-r--r--Services/ProtocolServer/Protocol.h49
-rw-r--r--Services/ProtocolServer/ProtocolClient.ipc6
-rw-r--r--Services/ProtocolServer/ProtocolServer.ipc15
-rw-r--r--Services/ProtocolServer/main.cpp71
-rw-r--r--Services/SystemServer/Makefile13
-rw-r--r--Services/SystemServer/Service.cpp365
-rw-r--r--Services/SystemServer/Service.h90
-rw-r--r--Services/SystemServer/main.cpp139
-rw-r--r--Services/TTYServer/Makefile6
-rw-r--r--Services/TTYServer/main.cpp68
-rw-r--r--Services/TelnetServer/Client.cpp188
-rw-r--r--Services/TelnetServer/Client.h69
-rw-r--r--Services/TelnetServer/Command.h82
-rw-r--r--Services/TelnetServer/Makefile10
-rw-r--r--Services/TelnetServer/Parser.cpp89
-rw-r--r--Services/TelnetServer/Parser.h59
-rw-r--r--Services/TelnetServer/main.cpp177
-rw-r--r--Services/WebServer/Client.cpp230
-rw-r--r--Services/WebServer/Client.h54
-rw-r--r--Services/WebServer/Makefile9
-rw-r--r--Services/WebServer/main.cpp81
-rw-r--r--Services/WindowServer/AppletManager.cpp135
-rw-r--r--Services/WindowServer/AppletManager.h56
-rw-r--r--Services/WindowServer/Button.cpp115
-rw-r--r--Services/WindowServer/Button.h70
-rw-r--r--Services/WindowServer/ClientConnection.cpp833
-rw-r--r--Services/WindowServer/ClientConnection.h148
-rw-r--r--Services/WindowServer/Clipboard.cpp78
-rw-r--r--Services/WindowServer/Clipboard.h62
-rw-r--r--Services/WindowServer/Compositor.cpp507
-rw-r--r--Services/WindowServer/Compositor.h108
-rw-r--r--Services/WindowServer/Cursor.cpp77
-rw-r--r--Services/WindowServer/Cursor.h65
-rw-r--r--Services/WindowServer/Event.h157
-rw-r--r--Services/WindowServer/EventLoop.cpp157
-rw-r--r--Services/WindowServer/EventLoop.h57
-rw-r--r--Services/WindowServer/Makefile38
-rw-r--r--Services/WindowServer/Menu.cpp570
-rw-r--r--Services/WindowServer/Menu.h158
-rw-r--r--Services/WindowServer/MenuBar.cpp55
-rw-r--r--Services/WindowServer/MenuBar.h61
-rw-r--r--Services/WindowServer/MenuItem.cpp96
-rw-r--r--Services/WindowServer/MenuItem.h99
-rw-r--r--Services/WindowServer/MenuManager.cpp404
-rw-r--r--Services/WindowServer/MenuManager.h123
-rw-r--r--Services/WindowServer/Screen.cpp171
-rw-r--r--Services/WindowServer/Screen.h86
-rw-r--r--Services/WindowServer/Window.cpp524
-rw-r--r--Services/WindowServer/Window.h286
-rw-r--r--Services/WindowServer/WindowClient.ipc40
-rw-r--r--Services/WindowServer/WindowFrame.cpp414
-rw-r--r--Services/WindowServer/WindowFrame.h74
-rw-r--r--Services/WindowServer/WindowManager.cpp1317
-rw-r--r--Services/WindowServer/WindowManager.h416
-rw-r--r--Services/WindowServer/WindowServer.ipc97
-rw-r--r--Services/WindowServer/WindowSwitcher.cpp256
-rw-r--r--Services/WindowServer/WindowSwitcher.h84
-rw-r--r--Services/WindowServer/WindowType.h41
-rw-r--r--Services/WindowServer/main.cpp117
108 files changed, 13705 insertions, 0 deletions
diff --git a/Services/AudioServer/ASClientConnection.cpp b/Services/AudioServer/ASClientConnection.cpp
new file mode 100644
index 0000000000..a1d4b766a0
--- /dev/null
+++ b/Services/AudioServer/ASClientConnection.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ASClientConnection.h"
+#include "ASMixer.h"
+#include "AudioClientEndpoint.h"
+#include <AK/SharedBuffer.h>
+#include <LibAudio/Buffer.h>
+#include <LibCore/EventLoop.h>
+#include <errno.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+static HashMap<int, RefPtr<ASClientConnection>> s_connections;
+
+void ASClientConnection::for_each(Function<void(ASClientConnection&)> callback)
+{
+ NonnullRefPtrVector<ASClientConnection> connections;
+ for (auto& it : s_connections)
+ connections.append(*it.value);
+ for (auto& connection : connections)
+ callback(connection);
+}
+
+ASClientConnection::ASClientConnection(Core::LocalSocket& client_socket, int client_id, ASMixer& mixer)
+ : IPC::ClientConnection<AudioServerEndpoint>(*this, client_socket, client_id)
+ , m_mixer(mixer)
+{
+ s_connections.set(client_id, *this);
+}
+
+ASClientConnection::~ASClientConnection()
+{
+}
+
+void ASClientConnection::die()
+{
+ s_connections.remove(client_id());
+}
+
+void ASClientConnection::did_finish_playing_buffer(Badge<ASBufferQueue>, int buffer_id)
+{
+ post_message(Messages::AudioClient::FinishedPlayingBuffer(buffer_id));
+}
+
+void ASClientConnection::did_change_muted_state(Badge<ASMixer>, bool muted)
+{
+ post_message(Messages::AudioClient::MutedStateChanged(muted));
+}
+
+OwnPtr<Messages::AudioServer::GreetResponse> ASClientConnection::handle(const Messages::AudioServer::Greet&)
+{
+ return make<Messages::AudioServer::GreetResponse>(client_id());
+}
+
+OwnPtr<Messages::AudioServer::GetMainMixVolumeResponse> ASClientConnection::handle(const Messages::AudioServer::GetMainMixVolume&)
+{
+ return make<Messages::AudioServer::GetMainMixVolumeResponse>(m_mixer.main_volume());
+}
+
+OwnPtr<Messages::AudioServer::SetMainMixVolumeResponse> ASClientConnection::handle(const Messages::AudioServer::SetMainMixVolume& message)
+{
+ m_mixer.set_main_volume(message.volume());
+ return make<Messages::AudioServer::SetMainMixVolumeResponse>();
+}
+
+OwnPtr<Messages::AudioServer::EnqueueBufferResponse> ASClientConnection::handle(const Messages::AudioServer::EnqueueBuffer& message)
+{
+ auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.buffer_id());
+ if (!shared_buffer) {
+ // FIXME: The shared buffer should have been retrieved for us already.
+ // We don't want to do IPC error checking at this layer.
+ ASSERT_NOT_REACHED();
+ }
+
+ if (!m_queue)
+ m_queue = m_mixer.create_queue(*this);
+
+ if (m_queue->is_full())
+ return make<Messages::AudioServer::EnqueueBufferResponse>(false);
+
+ m_queue->enqueue(Audio::Buffer::create_with_shared_buffer(*shared_buffer, message.sample_count()));
+ return make<Messages::AudioServer::EnqueueBufferResponse>(true);
+}
+
+OwnPtr<Messages::AudioServer::GetRemainingSamplesResponse> ASClientConnection::handle(const Messages::AudioServer::GetRemainingSamples&)
+{
+ int remaining = 0;
+ if (m_queue)
+ remaining = m_queue->get_remaining_samples();
+
+ return make<Messages::AudioServer::GetRemainingSamplesResponse>(remaining);
+}
+
+OwnPtr<Messages::AudioServer::GetPlayedSamplesResponse> ASClientConnection::handle(const Messages::AudioServer::GetPlayedSamples&)
+{
+ int played = 0;
+ if (m_queue)
+ played = m_queue->get_played_samples();
+
+ return make<Messages::AudioServer::GetPlayedSamplesResponse>(played);
+}
+
+OwnPtr<Messages::AudioServer::SetPausedResponse> ASClientConnection::handle(const Messages::AudioServer::SetPaused& message)
+{
+ if (m_queue)
+ m_queue->set_paused(message.paused());
+ return make<Messages::AudioServer::SetPausedResponse>();
+}
+
+OwnPtr<Messages::AudioServer::ClearBufferResponse> ASClientConnection::handle(const Messages::AudioServer::ClearBuffer& message)
+{
+ if (m_queue)
+ m_queue->clear(message.paused());
+ return make<Messages::AudioServer::ClearBufferResponse>();
+}
+
+OwnPtr<Messages::AudioServer::GetPlayingBufferResponse> ASClientConnection::handle(const Messages::AudioServer::GetPlayingBuffer&)
+{
+ int id = -1;
+ if (m_queue)
+ id = m_queue->get_playing_buffer();
+ return make<Messages::AudioServer::GetPlayingBufferResponse>(id);
+}
+
+OwnPtr<Messages::AudioServer::GetMutedResponse> ASClientConnection::handle(const Messages::AudioServer::GetMuted&)
+{
+ return make<Messages::AudioServer::GetMutedResponse>(m_mixer.is_muted());
+}
+
+OwnPtr<Messages::AudioServer::SetMutedResponse> ASClientConnection::handle(const Messages::AudioServer::SetMuted& message)
+{
+ m_mixer.set_muted(message.muted());
+ return make<Messages::AudioServer::SetMutedResponse>();
+}
diff --git a/Services/AudioServer/ASClientConnection.h b/Services/AudioServer/ASClientConnection.h
new file mode 100644
index 0000000000..c76c51eb36
--- /dev/null
+++ b/Services/AudioServer/ASClientConnection.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashMap.h>
+#include <AudioServer/AudioServerEndpoint.h>
+#include <LibIPC/ClientConnection.h>
+
+namespace Audio {
+class Buffer;
+}
+
+class ASBufferQueue;
+class ASMixer;
+
+class ASClientConnection final : public IPC::ClientConnection<AudioServerEndpoint>
+ , public AudioServerEndpoint {
+ C_OBJECT(ASClientConnection)
+public:
+ explicit ASClientConnection(Core::LocalSocket&, int client_id, ASMixer& mixer);
+ ~ASClientConnection() override;
+
+ void did_finish_playing_buffer(Badge<ASBufferQueue>, int buffer_id);
+ void did_change_muted_state(Badge<ASMixer>, bool muted);
+
+ virtual void die() override;
+
+ static void for_each(Function<void(ASClientConnection&)>);
+
+private:
+ virtual OwnPtr<Messages::AudioServer::GreetResponse> handle(const Messages::AudioServer::Greet&) override;
+ virtual OwnPtr<Messages::AudioServer::GetMainMixVolumeResponse> handle(const Messages::AudioServer::GetMainMixVolume&) override;
+ virtual OwnPtr<Messages::AudioServer::SetMainMixVolumeResponse> handle(const Messages::AudioServer::SetMainMixVolume&) override;
+ virtual OwnPtr<Messages::AudioServer::EnqueueBufferResponse> handle(const Messages::AudioServer::EnqueueBuffer&) override;
+ virtual OwnPtr<Messages::AudioServer::GetRemainingSamplesResponse> handle(const Messages::AudioServer::GetRemainingSamples&) override;
+ virtual OwnPtr<Messages::AudioServer::GetPlayedSamplesResponse> handle(const Messages::AudioServer::GetPlayedSamples&) override;
+ virtual OwnPtr<Messages::AudioServer::SetPausedResponse> handle(const Messages::AudioServer::SetPaused&) override;
+ virtual OwnPtr<Messages::AudioServer::ClearBufferResponse> handle(const Messages::AudioServer::ClearBuffer&) override;
+ virtual OwnPtr<Messages::AudioServer::GetPlayingBufferResponse> handle(const Messages::AudioServer::GetPlayingBuffer&) override;
+ virtual OwnPtr<Messages::AudioServer::GetMutedResponse> handle(const Messages::AudioServer::GetMuted&) override;
+ virtual OwnPtr<Messages::AudioServer::SetMutedResponse> handle(const Messages::AudioServer::SetMuted&) override;
+
+ ASMixer& m_mixer;
+ RefPtr<ASBufferQueue> m_queue;
+};
diff --git a/Services/AudioServer/ASMixer.cpp b/Services/AudioServer/ASMixer.cpp
new file mode 100644
index 0000000000..3d9ff00cc5
--- /dev/null
+++ b/Services/AudioServer/ASMixer.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/BufferStream.h>
+#include <AK/NumericLimits.h>
+#include <AudioServer/ASClientConnection.h>
+#include <AudioServer/ASMixer.h>
+#include <pthread.h>
+
+ASMixer::ASMixer()
+ : m_device(Core::File::construct("/dev/audio", this))
+ , m_sound_thread(
+ [this] {
+ mix();
+ return 0;
+ },
+ "AudioServer[mixer]")
+{
+ if (!m_device->open(Core::IODevice::WriteOnly)) {
+ dbgprintf("Can't open audio device: %s\n", m_device->error_string());
+ return;
+ }
+
+ pthread_mutex_init(&m_pending_mutex, nullptr);
+ pthread_cond_init(&m_pending_cond, nullptr);
+
+ m_zero_filled_buffer = (u8*)malloc(4096);
+ bzero(m_zero_filled_buffer, 4096);
+ m_sound_thread.start();
+}
+
+ASMixer::~ASMixer()
+{
+}
+
+NonnullRefPtr<ASBufferQueue> ASMixer::create_queue(ASClientConnection& client)
+{
+ auto queue = adopt(*new ASBufferQueue(client));
+ pthread_mutex_lock(&m_pending_mutex);
+ m_pending_mixing.append(*queue);
+ pthread_cond_signal(&m_pending_cond);
+ pthread_mutex_unlock(&m_pending_mutex);
+ return queue;
+}
+
+void ASMixer::mix()
+{
+ decltype(m_pending_mixing) active_mix_queues;
+
+ for (;;) {
+ if (active_mix_queues.is_empty()) {
+ pthread_mutex_lock(&m_pending_mutex);
+ pthread_cond_wait(&m_pending_cond, &m_pending_mutex);
+ active_mix_queues.append(move(m_pending_mixing));
+ pthread_mutex_unlock(&m_pending_mutex);
+ }
+
+ active_mix_queues.remove_all_matching([&](auto& entry) { return !entry->client(); });
+
+ Audio::Sample mixed_buffer[1024];
+ auto mixed_buffer_length = (int)(sizeof(mixed_buffer) / sizeof(Audio::Sample));
+
+ // Mix the buffers together into the output
+ for (auto& queue : active_mix_queues) {
+ if (!queue->client()) {
+ queue->clear();
+ continue;
+ }
+
+ for (int i = 0; i < mixed_buffer_length; ++i) {
+ auto& mixed_sample = mixed_buffer[i];
+ Audio::Sample sample;
+ if (!queue->get_next_sample(sample))
+ break;
+ mixed_sample += sample;
+ }
+ }
+
+ bool muted = m_muted;
+
+ // output the mixed stuff to the device
+ u8 raw_buffer[4096];
+ auto buffer = ByteBuffer::wrap(muted ? m_zero_filled_buffer : raw_buffer, sizeof(raw_buffer));
+
+ BufferStream stream(buffer);
+ if (!muted) {
+ for (int i = 0; i < mixed_buffer_length; ++i) {
+ auto& mixed_sample = mixed_buffer[i];
+
+ mixed_sample.scale(m_main_volume);
+ mixed_sample.clip();
+
+ i16 out_sample;
+ out_sample = mixed_sample.left * NumericLimits<i16>::max();
+ stream << out_sample;
+
+ ASSERT(!stream.at_end()); // we should have enough space for both channels in one buffer!
+ out_sample = mixed_sample.right * NumericLimits<i16>::max();
+ stream << out_sample;
+ }
+ }
+
+ if (stream.offset() != 0) {
+ buffer.trim(stream.offset());
+ }
+ m_device->write(buffer);
+ }
+}
+
+void ASMixer::set_muted(bool muted)
+{
+ if (m_muted == muted)
+ return;
+ m_muted = muted;
+ ASClientConnection::for_each([muted](ASClientConnection& client) {
+ client.did_change_muted_state({}, muted);
+ });
+}
+
+ASBufferQueue::ASBufferQueue(ASClientConnection& client)
+ : m_client(client.make_weak_ptr())
+{
+}
+
+void ASBufferQueue::enqueue(NonnullRefPtr<Audio::Buffer>&& buffer)
+{
+ m_remaining_samples += buffer->sample_count();
+ m_queue.enqueue(move(buffer));
+}
diff --git a/Services/AudioServer/ASMixer.h b/Services/AudioServer/ASMixer.h
new file mode 100644
index 0000000000..e5ed5874b2
--- /dev/null
+++ b/Services/AudioServer/ASMixer.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ASClientConnection.h"
+#include <AK/Badge.h>
+#include <AK/ByteBuffer.h>
+#include <AK/NonnullRefPtrVector.h>
+#include <AK/Queue.h>
+#include <AK/RefCounted.h>
+#include <AK/WeakPtr.h>
+#include <LibAudio/Buffer.h>
+#include <LibCore/File.h>
+#include <LibThread/Lock.h>
+#include <LibThread/Thread.h>
+
+class ASClientConnection;
+
+class ASBufferQueue : public RefCounted<ASBufferQueue> {
+public:
+ explicit ASBufferQueue(ASClientConnection&);
+ ~ASBufferQueue() {}
+
+ bool is_full() const { return m_queue.size() >= 3; }
+ void enqueue(NonnullRefPtr<Audio::Buffer>&&);
+
+ bool get_next_sample(Audio::Sample& sample)
+ {
+ if (m_paused)
+ return false;
+
+ while (!m_current && !m_queue.is_empty())
+ m_current = m_queue.dequeue();
+
+ if (!m_current)
+ return false;
+
+ sample = m_current->samples()[m_position++];
+ --m_remaining_samples;
+ ++m_played_samples;
+
+ if (m_position >= m_current->sample_count()) {
+ m_client->did_finish_playing_buffer({}, m_current->shbuf_id());
+ m_current = nullptr;
+ m_position = 0;
+ }
+ return true;
+ }
+
+ ASClientConnection* client() { return m_client.ptr(); }
+
+ void clear(bool paused = false)
+ {
+ m_queue.clear();
+ m_position = 0;
+ m_remaining_samples = 0;
+ m_played_samples = 0;
+ m_current = nullptr;
+ m_paused = paused;
+ }
+
+ void set_paused(bool paused)
+ {
+ m_paused = paused;
+ }
+
+ int get_remaining_samples() const { return m_remaining_samples; }
+ int get_played_samples() const { return m_played_samples; }
+ int get_playing_buffer() const
+ {
+ if (m_current)
+ return m_current->shbuf_id();
+ return -1;
+ }
+
+private:
+ RefPtr<Audio::Buffer> m_current;
+ Queue<NonnullRefPtr<Audio::Buffer>> m_queue;
+ int m_position { 0 };
+ int m_remaining_samples { 0 };
+ int m_played_samples { 0 };
+ bool m_paused { false };
+ WeakPtr<ASClientConnection> m_client;
+};
+
+class ASMixer : public Core::Object {
+ C_OBJECT(ASMixer)
+public:
+ ASMixer();
+ virtual ~ASMixer() override;
+
+ NonnullRefPtr<ASBufferQueue> create_queue(ASClientConnection&);
+
+ int main_volume() const { return m_main_volume; }
+ void set_main_volume(int volume) { m_main_volume = volume; }
+
+ bool is_muted() const { return m_muted; }
+ void set_muted(bool);
+
+private:
+ Vector<NonnullRefPtr<ASBufferQueue>> m_pending_mixing;
+ pthread_mutex_t m_pending_mutex;
+ pthread_cond_t m_pending_cond;
+
+ RefPtr<Core::File> m_device;
+
+ LibThread::Thread m_sound_thread;
+
+ bool m_muted { false };
+ int m_main_volume { 100 };
+
+ u8* m_zero_filled_buffer { nullptr };
+
+ void mix();
+};
diff --git a/Services/AudioServer/AudioClient.ipc b/Services/AudioServer/AudioClient.ipc
new file mode 100644
index 0000000000..12aa2d0b92
--- /dev/null
+++ b/Services/AudioServer/AudioClient.ipc
@@ -0,0 +1,5 @@
+endpoint AudioClient = 82
+{
+ FinishedPlayingBuffer(i32 buffer_id) =|
+ MutedStateChanged(bool muted) =|
+}
diff --git a/Services/AudioServer/AudioServer.ipc b/Services/AudioServer/AudioServer.ipc
new file mode 100644
index 0000000000..b48dcd7e46
--- /dev/null
+++ b/Services/AudioServer/AudioServer.ipc
@@ -0,0 +1,21 @@
+endpoint AudioServer = 85
+{
+ // Basic protocol
+ Greet() => (i32 client_id)
+
+ // Mixer functions
+ SetMuted(bool muted) => ()
+ GetMuted() => (bool muted)
+ GetMainMixVolume() => (i32 volume)
+ SetMainMixVolume(i32 volume) => ()
+
+ // Buffer playback
+ EnqueueBuffer(i32 buffer_id, int sample_count) => (bool success)
+ SetPaused(bool paused) => ()
+ ClearBuffer(bool paused) => ()
+
+ //Buffer information
+ GetRemainingSamples() => (int remaining_samples)
+ GetPlayedSamples() => (int played_samples)
+ GetPlayingBuffer() => (i32 buffer_id)
+}
diff --git a/Services/AudioServer/Makefile b/Services/AudioServer/Makefile
new file mode 100644
index 0000000000..7b6b3dd031
--- /dev/null
+++ b/Services/AudioServer/Makefile
@@ -0,0 +1,24 @@
+OBJS = \
+ main.o \
+ ASMixer.o \
+ ASClientConnection.o
+
+PROGRAM = AudioServer
+
+LIB_DEPS = Core IPC Thread Pthread
+
+EXTRA_CLEAN = AudioServerEndpoint.h AudioClientEndpoint.h
+
+*.cpp: AudioServerEndpoint.h AudioClientEndpoint.h
+
+AudioServerEndpoint.h: AudioServer.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+AudioClientEndpoint.h: AudioClient.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+install:
+ mkdir -p $(SERENITY_BASE_DIR)/Root/usr/include/AudioServer/
+ cp *.h $(SERENITY_BASE_DIR)/Root/usr/include/AudioServer/
+
+include ../../Makefile.common
diff --git a/Services/AudioServer/main.cpp b/Services/AudioServer/main.cpp
new file mode 100644
index 0000000000..5907dd77c6
--- /dev/null
+++ b/Services/AudioServer/main.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ASMixer.h"
+#include <LibCore/File.h>
+#include <LibCore/LocalServer.h>
+
+int main(int, char**)
+{
+ if (pledge("stdio thread shared_buffer accept rpath wpath cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ Core::EventLoop event_loop;
+ ASMixer mixer;
+
+ auto server = Core::LocalServer::construct();
+ bool ok = server->take_over_from_system_server();
+ ASSERT(ok);
+ server->on_ready_to_accept = [&] {
+ auto client_socket = server->accept();
+ if (!client_socket) {
+ dbg() << "AudioServer: accept failed.";
+ return;
+ }
+ static int s_next_client_id = 0;
+ int client_id = ++s_next_client_id;
+ IPC::new_client_connection<ASClientConnection>(*client_socket, client_id, mixer);
+ };
+
+ if (pledge("stdio thread shared_buffer accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ return event_loop.exec();
+}
diff --git a/Services/DHCPClient/DHCPv4.cpp b/Services/DHCPClient/DHCPv4.cpp
new file mode 100644
index 0000000000..36430f4eec
--- /dev/null
+++ b/Services/DHCPClient/DHCPv4.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DHCPv4.h"
+
+//#define DHCPV4_DEBUG
+
+ParsedDHCPv4Options DHCPv4Packet::parse_options() const
+{
+ ParsedDHCPv4Options options;
+ for (size_t index = 4; index < DHCPV4_OPTION_FIELD_MAX_LENGTH; ++index) {
+ auto opt_name = *(const DHCPOption*)&m_options[index];
+ switch (opt_name) {
+ case DHCPOption::Pad:
+ continue;
+ case DHCPOption::End:
+ goto escape;
+ default:
+ ++index;
+ auto length = m_options[index];
+ if ((size_t)length > DHCPV4_OPTION_FIELD_MAX_LENGTH - index) {
+ dbg() << "Bogus option length " << length << " assuming forgotten END";
+ break;
+ }
+#ifdef DHCPV4_DEBUG
+ dbg() << "DHCP Option " << (u8)opt_name << " with length " << length;
+#endif
+ ++index;
+ options.options.set(opt_name, { length, &m_options[index] });
+ index += length - 1;
+ break;
+ }
+ }
+escape:;
+ return options;
+}
diff --git a/Services/DHCPClient/DHCPv4.h b/Services/DHCPClient/DHCPv4.h
new file mode 100644
index 0000000000..07c5787f8a
--- /dev/null
+++ b/Services/DHCPClient/DHCPv4.h
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Assertions.h>
+#include <AK/ByteBuffer.h>
+#include <AK/HashMap.h>
+#include <AK/IPv4Address.h>
+#include <AK/MACAddress.h>
+#include <AK/NetworkOrdered.h>
+#include <AK/StringBuilder.h>
+#include <AK/StringView.h>
+#include <AK/Traits.h>
+#include <AK/Types.h>
+#include <string.h>
+
+enum class DHCPv4Flags : u16 {
+ Broadcast = 1,
+ /* everything else is reserved and must be zero */
+};
+
+enum class DHCPv4Op : u8 {
+ BootRequest = 1,
+ BootReply = 2
+};
+
+enum class DHCPOption : u8 {
+ // BOOTP
+ Pad = 0,
+ SubnetMask,
+ TimeOffset,
+ Router,
+ TimeServer,
+ NameServer,
+ DomainNameServer,
+ LogServer,
+ CookieServer,
+ LPRServer,
+ ImpressServer,
+ ResourceLocationServer,
+ HostName,
+ BootFileSize,
+ MeritDumpFile,
+ DomainName,
+ SwapServer,
+ RootPath,
+ ExtensionsPath,
+ IPForwardingEnableDisable,
+ NonLocalSourceRoutingEnableDisable,
+ PolicyFilter,
+ MaximumDatagramReassemblySize,
+ DefaultIPTTL,
+ PathMTUAgingTimeout,
+ PathMTUPlateauTable,
+ InterfaceMTU,
+ AllSubnetsAreLocal,
+ BroadcastAddress,
+ PerformMaskDiscovery,
+ MaskSupplier,
+ PerformRouterDiscovery,
+ RouterSolicitationAddress,
+ StaticRoute,
+ TrailerEncapsulation,
+ ARPCacheTimeout,
+ EthernetEncapsulation,
+ TCPDefaultTTL,
+ TCPKeepaliveInterval,
+ TCPKeepaliveGarbage,
+ NetworkInformationServiceDomain,
+ NetworkInformationServers,
+ NetworkTimeProtocolServers,
+ VendorSpecificInformation,
+ NetBIOSOverTCPIPNameServer,
+ NetBIOSOverTCPIPDatagramDistributionServer,
+ NetBIOSOverTCPIPNodeType,
+ NetBIOSOverTCPIPScope,
+ XWindowSystemFontServer, // wow
+ XWindowSystemDisplayManager,
+ // DHCP
+ RequestedIPAddress = 50,
+ IPAddressLeaseTime,
+ OptionOverload,
+ DHCPMessageType,
+ ServerIdentifier,
+ ParameterRequestList,
+ Message,
+ MaximumDHCPMessageSize,
+ RenewalT1Time,
+ RenewalT2Time,
+ ClassIdentifier,
+ ClientIdentifier,
+ End = 255
+};
+
+enum class DHCPMessageType : u8 {
+ DHCPDiscover = 1,
+ DHCPOffer,
+ DHCPRequest,
+ DHCPDecline,
+ DHCPAck,
+ DHCPNak,
+ DHCPRelease,
+};
+
+template <>
+struct AK::Traits<DHCPOption> : public AK::GenericTraits<DHCPOption> {
+ static constexpr bool is_trivial() { return true; }
+ static unsigned hash(DHCPOption u) { return int_hash((u8)u); }
+};
+
+struct ParsedDHCPv4Options {
+ template <typename T>
+ Optional<const T> get(DHCPOption option_name) const
+ {
+ auto option = options.get(option_name);
+ if (!option.has_value()) {
+ return {};
+ }
+ auto& value = option.value();
+ if (value.length != sizeof(T))
+ return {};
+ return *(const T*)value.value;
+ }
+
+ template <typename T>
+ Vector<T> get_many(DHCPOption option_name, size_t max_number) const
+ {
+ Vector<T> values;
+
+ auto option = options.get(option_name);
+ if (!option.has_value()) {
+ return {};
+ }
+ auto& value = option.value();
+ if (value.length < sizeof(T))
+ return {};
+
+ for (size_t i = 0; i < max_number; ++i) {
+ auto offset = i * sizeof(T);
+ if (offset >= value.length)
+ break;
+ values.append(*(T*)((u8*)const_cast<void*>(value.value) + offset));
+ }
+
+ return values;
+ }
+
+ String to_string() const
+ {
+ StringBuilder builder;
+ builder.append("DHCP Options (");
+ builder.appendf("%d", options.size());
+ builder.append(" entries)\n");
+ for (auto& opt : options) {
+ builder.appendf("\toption %d (%d bytes):", (u8)opt.key, (u8)opt.value.length);
+ for (auto i = 0; i < opt.value.length; ++i)
+ builder.appendf(" %u ", ((const u8*)opt.value.value)[i]);
+ builder.append('\n');
+ }
+ return builder.build();
+ }
+
+ struct DHCPOptionValue {
+ u8 length;
+ const void* value;
+ };
+
+ HashMap<DHCPOption, DHCPOptionValue> options;
+};
+
+constexpr auto DHCPV4_OPTION_FIELD_MAX_LENGTH = 312;
+
+class [[gnu::packed]] DHCPv4Packet
+{
+public:
+ u8 op() const { return m_op; }
+ void set_op(DHCPv4Op op) { m_op = (u8)op; }
+
+ u8 htype() const { return m_htype; }
+ void set_htype(u8 htype) { m_htype = htype; }
+
+ u8 hlen() const { return m_hlen; }
+ void set_hlen(u8 hlen) { m_hlen = hlen; }
+
+ u8 hops() const { return m_hops; }
+ void set_hops(u8 hops) { m_hops = hops; }
+
+ u32 xid() const { return m_xid; }
+ void set_xid(u32 xid) { m_xid = xid; }
+
+ u16 secs() const { return m_secs; }
+ void set_secs(u16 secs) { m_secs = secs; }
+
+ u16 flags() const { return m_flags; }
+ void set_flags(DHCPv4Flags flags) { m_flags = (u16)flags; }
+
+ const IPv4Address& ciaddr() const { return m_ciaddr; }
+ const IPv4Address& yiaddr() const { return m_yiaddr; }
+ const IPv4Address& siaddr() const { return m_siaddr; }
+ const IPv4Address& giaddr() const { return m_giaddr; }
+
+ IPv4Address& ciaddr() { return m_ciaddr; }
+ IPv4Address& yiaddr() { return m_yiaddr; }
+ IPv4Address& siaddr() { return m_siaddr; }
+ IPv4Address& giaddr() { return m_giaddr; }
+
+ u8* options() { return m_options; }
+ ParsedDHCPv4Options parse_options() const;
+
+ const MACAddress& chaddr() const { return *(const MACAddress*)&m_chaddr[0]; }
+ void set_chaddr(const MACAddress& mac) { *(MACAddress*)&m_chaddr[0] = mac; }
+
+ StringView sname() const { return { (const char*)&m_sname[0] }; }
+ StringView file() const { return { (const char*)&m_file[0] }; }
+
+private:
+ NetworkOrdered<u8> m_op;
+ NetworkOrdered<u8> m_htype;
+ NetworkOrdered<u8> m_hlen;
+ NetworkOrdered<u8> m_hops;
+ NetworkOrdered<u32> m_xid;
+ NetworkOrdered<u16> m_secs;
+ NetworkOrdered<u16> m_flags;
+ IPv4Address m_ciaddr;
+ IPv4Address m_yiaddr;
+ IPv4Address m_siaddr;
+ IPv4Address m_giaddr;
+ u8 m_chaddr[16]; // 10 bytes of padding at the end
+ u8 m_sname[64] { 0 };
+ u8 m_file[128] { 0 };
+ u8 m_options[DHCPV4_OPTION_FIELD_MAX_LENGTH] { 0 }; // variable, less than 312 bytes
+};
+
+class DHCPv4PacketBuilder {
+public:
+ DHCPv4PacketBuilder()
+ : m_buffer(ByteBuffer::create_zeroed(sizeof(DHCPv4Packet)))
+ {
+ auto* options = peek().options();
+ // set the magic DHCP cookie value
+ options[0] = 99;
+ options[1] = 130;
+ options[2] = 83;
+ options[3] = 99;
+ }
+
+ void add_option(DHCPOption option, u8 length, const void* data)
+ {
+ ASSERT(m_can_add);
+ // we need enough space to fit the option value, its length, and its data
+ ASSERT(next_option_offset + length + 2 < DHCPV4_OPTION_FIELD_MAX_LENGTH);
+
+ auto* options = peek().options();
+ options[next_option_offset++] = (u8)option;
+ memcpy(options + next_option_offset, &length, 1);
+ next_option_offset++;
+ memcpy(options + next_option_offset, data, length);
+ next_option_offset += length;
+ }
+
+ void set_message_type(DHCPMessageType type) { add_option(DHCPOption::DHCPMessageType, 1, &type); }
+
+ DHCPv4Packet& peek() { return *(DHCPv4Packet*)m_buffer.data(); }
+ DHCPv4Packet& build()
+ {
+ add_option(DHCPOption::End, 0, nullptr);
+ m_can_add = false;
+ return *(DHCPv4Packet*)m_buffer.data();
+ }
+ size_t size() const { return m_buffer.size(); }
+
+private:
+ ByteBuffer m_buffer;
+ size_t next_option_offset { 4 };
+ bool m_can_add { true };
+};
diff --git a/Services/DHCPClient/DHCPv4Client.cpp b/Services/DHCPClient/DHCPv4Client.cpp
new file mode 100644
index 0000000000..918a20b93f
--- /dev/null
+++ b/Services/DHCPClient/DHCPv4Client.cpp
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DHCPv4Client.h"
+#include <AK/ByteBuffer.h>
+#include <AK/Function.h>
+#include <LibCore/SocketAddress.h>
+#include <LibCore/Timer.h>
+#include <stdio.h>
+
+//#define DHCPV4CLIENT_DEBUG
+
+static void send(const InterfaceDescriptor& iface, const DHCPv4Packet& packet, Core::Object*)
+{
+ int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (fd < 0) {
+ dbg() << "ERROR: socket :: " << strerror(errno);
+ return;
+ }
+
+ if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.m_ifname.characters(), IFNAMSIZ) < 0) {
+ dbg() << "ERROR: setsockopt(SO_BINDTODEVICE) :: " << strerror(errno);
+ return;
+ }
+
+ sockaddr_in dst;
+ memset(&dst, 0, sizeof(dst));
+ dst.sin_family = AF_INET;
+ dst.sin_port = htons(67);
+ dst.sin_addr.s_addr = IPv4Address { 255, 255, 255, 255 }.to_u32();
+ memset(&dst.sin_zero, 0, sizeof(dst.sin_zero));
+
+ auto rc = sendto(fd, &packet, sizeof(packet), 0, (sockaddr*)&dst, sizeof(dst));
+ if (rc < 0) {
+ dbg() << "sendto failed with " << strerror(errno);
+ // FIXME: what do we do here?
+ }
+}
+
+static void set_params(const InterfaceDescriptor& iface, const IPv4Address& ipv4_addr, const IPv4Address& netmask, const IPv4Address& gateway)
+{
+ int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
+ if (fd < 0) {
+ dbg() << "ERROR: socket :: " << strerror(errno);
+ return;
+ }
+
+ struct ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, iface.m_ifname.characters(), IFNAMSIZ);
+
+ // set the IP address
+ ifr.ifr_addr.sa_family = AF_INET;
+ ((sockaddr_in&)ifr.ifr_addr).sin_addr.s_addr = ipv4_addr.to_in_addr_t();
+
+ if (ioctl(fd, SIOCSIFADDR, &ifr) < 0) {
+ dbg() << "ERROR: ioctl(SIOCSIFADDR) :: " << strerror(errno);
+ }
+
+ // set the network mask
+ ((sockaddr_in&)ifr.ifr_netmask).sin_addr.s_addr = netmask.to_in_addr_t();
+
+ if (ioctl(fd, SIOCSIFNETMASK, &ifr) < 0) {
+ dbg() << "ERROR: ioctl(SIOCSIFNETMASK) :: " << strerror(errno);
+ }
+
+ // set the default gateway
+ struct rtentry rt;
+ memset(&rt, 0, sizeof(rt));
+
+ rt.rt_dev = const_cast<char*>(iface.m_ifname.characters());
+ rt.rt_gateway.sa_family = AF_INET;
+ ((sockaddr_in&)rt.rt_gateway).sin_addr.s_addr = gateway.to_in_addr_t();
+ rt.rt_flags = RTF_UP | RTF_GATEWAY;
+
+ if (ioctl(fd, SIOCADDRT, &rt) < 0) {
+ dbg() << "Error: ioctl(SIOCADDRT) :: " << strerror(errno);
+ }
+}
+
+DHCPv4Client::DHCPv4Client(Vector<InterfaceDescriptor> ifnames)
+ : m_ifnames(ifnames)
+{
+ m_server = Core::UDPServer::construct(this);
+ m_server->on_ready_to_receive = [this] {
+ auto buffer = m_server->receive(sizeof(DHCPv4Packet));
+ dbg() << "Received " << buffer.size() << " bytes";
+ if (buffer.size() != sizeof(DHCPv4Packet)) {
+ dbg() << "we expected " << sizeof(DHCPv4Packet) << " bytes, this is a bad packet";
+ return;
+ }
+ auto& packet = *(DHCPv4Packet*)buffer.data();
+ process_incoming(packet);
+ };
+
+ if (!m_server->bind({}, 68)) {
+ dbg() << "The server we just created somehow came already bound, refusing to continue";
+ ASSERT_NOT_REACHED();
+ }
+
+ for (auto& iface : m_ifnames)
+ dhcp_discover(iface);
+}
+
+DHCPv4Client::~DHCPv4Client()
+{
+}
+
+void DHCPv4Client::handle_offer(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
+{
+ dbg() << "We were offered " << packet.yiaddr().to_string() << " for " << options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(0);
+ auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
+ if (!transaction) {
+ dbg() << "we're not looking for " << packet.xid();
+ return;
+ }
+ if (transaction->has_ip)
+ return;
+ if (transaction->accepted_offer) {
+ // we've accepted someone's offer, but they haven't given us an ack
+ // TODO: maybe record this offer?
+ return;
+ }
+ // TAKE IT...
+ transaction->offered_lease_time = options.get<u32>(DHCPOption::IPAddressLeaseTime).value();
+ dhcp_request(*transaction, packet);
+}
+
+void DHCPv4Client::handle_ack(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
+{
+#ifdef DHCPV4CLIENT_DEBUG
+ dbg() << "The DHCP server handed us " << packet.yiaddr().to_string();
+ dbg() << "Here are the options: " << options.to_string();
+#endif
+ auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
+ if (!transaction) {
+ dbg() << "we're not looking for " << packet.xid();
+ return;
+ }
+ transaction->has_ip = true;
+ auto& interface = transaction->interface;
+ auto new_ip = packet.yiaddr();
+ auto lease_time = convert_between_host_and_network(options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(transaction->offered_lease_time));
+ // set a timer for the duration of the lease, we shall renew if needed
+ Core::Timer::create_single_shot(
+ lease_time * 1000,
+ [this, transaction, interface = InterfaceDescriptor { interface }, new_ip] {
+ transaction->accepted_offer = false;
+ transaction->has_ip = false;
+ dhcp_discover(interface, new_ip);
+ },
+ this);
+ set_params(transaction->interface, new_ip, options.get<IPv4Address>(DHCPOption::SubnetMask).value(), options.get_many<IPv4Address>(DHCPOption::Router, 1).first());
+}
+
+void DHCPv4Client::handle_nak(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
+{
+ dbg() << "The DHCP server told us to go chase our own tail about " << packet.yiaddr().to_string();
+ dbg() << "Here are the options: " << options.to_string();
+ // make another request a bit later :shrug:
+ auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
+ if (!transaction) {
+ dbg() << "we're not looking for " << packet.xid();
+ return;
+ }
+ transaction->accepted_offer = false;
+ transaction->has_ip = false;
+ auto& iface = transaction->interface;
+ Core::Timer::create_single_shot(
+ 10000,
+ [this, iface = InterfaceDescriptor { iface }] {
+ dhcp_discover(iface);
+ },
+ this);
+}
+
+void DHCPv4Client::process_incoming(const DHCPv4Packet& packet)
+{
+ auto options = packet.parse_options();
+#ifdef DHCPV4CLIENT_DEBUG
+ dbg() << "Here are the options: " << options.to_string();
+#endif
+ auto value = options.get<DHCPMessageType>(DHCPOption::DHCPMessageType).value();
+ switch (value) {
+ case DHCPMessageType::DHCPOffer:
+ handle_offer(packet, options);
+ break;
+ case DHCPMessageType::DHCPAck:
+ handle_ack(packet, options);
+ break;
+ case DHCPMessageType::DHCPNak:
+ handle_nak(packet, options);
+ break;
+ case DHCPMessageType::DHCPDiscover:
+ case DHCPMessageType::DHCPRequest:
+ case DHCPMessageType::DHCPRelease:
+ // These are not for us
+ // we're just getting them because there are other people on our subnet
+ // broadcasting stuff
+ break;
+ case DHCPMessageType::DHCPDecline:
+ default:
+ dbg() << "I dunno what to do with this " << (u8)value;
+ ASSERT_NOT_REACHED();
+ break;
+ }
+}
+
+void DHCPv4Client::dhcp_discover(const InterfaceDescriptor& iface, IPv4Address previous)
+{
+ auto transaction_id = rand();
+#ifdef DHCPV4CLIENT_DEBUG
+ dbg() << "Trying to lease an IP for " << iface.m_ifname << " with ID " << transaction_id;
+ if (!previous.is_zero())
+ dbg() << "going to request the server to hand us " << previous.to_string();
+#endif
+ DHCPv4PacketBuilder builder;
+
+ DHCPv4Packet& packet = builder.peek();
+ packet.set_op(DHCPv4Op::BootRequest);
+ packet.set_htype(1); // 10mb ethernet
+ packet.set_hlen(sizeof(MACAddress));
+ packet.set_xid(transaction_id);
+ packet.set_flags(DHCPv4Flags::Broadcast);
+ packet.ciaddr() = previous;
+ packet.set_chaddr(iface.m_mac_address);
+ packet.set_secs(65535); // we lie
+
+ // set packet options
+ builder.set_message_type(DHCPMessageType::DHCPDiscover);
+ auto& dhcp_packet = builder.build();
+
+ // broadcast the discover request
+ send(iface, dhcp_packet, this);
+ m_ongoing_transactions.set(transaction_id, make<DHCPv4Transaction>(iface));
+}
+
+void DHCPv4Client::dhcp_request(DHCPv4Transaction& transaction, const DHCPv4Packet& offer)
+{
+ auto& iface = transaction.interface;
+ dbg() << "Leasing the IP " << offer.yiaddr().to_string() << " for adapter " << iface.m_ifname;
+ DHCPv4PacketBuilder builder;
+
+ DHCPv4Packet& packet = builder.peek();
+ packet.set_op(DHCPv4Op::BootRequest);
+ packet.set_htype(1); // 10mb ethernet
+ packet.set_hlen(sizeof(MACAddress));
+ packet.set_xid(offer.xid());
+ packet.set_flags(DHCPv4Flags::Broadcast);
+ packet.set_chaddr(iface.m_mac_address);
+ packet.set_secs(65535); // we lie
+
+ // set packet options
+ builder.set_message_type(DHCPMessageType::DHCPRequest);
+ auto& dhcp_packet = builder.build();
+
+ // broadcast the "request" request
+ send(iface, dhcp_packet, this);
+ transaction.accepted_offer = true;
+}
diff --git a/Services/DHCPClient/DHCPv4Client.h b/Services/DHCPClient/DHCPv4Client.h
new file mode 100644
index 0000000000..c45f08f0d1
--- /dev/null
+++ b/Services/DHCPClient/DHCPv4Client.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "DHCPv4.h"
+#include <AK/FlyString.h>
+#include <AK/HashMap.h>
+#include <AK/OwnPtr.h>
+#include <AK/Vector.h>
+#include <LibCore/UDPServer.h>
+#include <LibCore/UDPSocket.h>
+#include <net/if.h>
+#include <net/route.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+struct InterfaceDescriptor {
+ FlyString m_ifname;
+ MACAddress m_mac_address;
+};
+
+struct DHCPv4Transaction {
+ DHCPv4Transaction(InterfaceDescriptor ifname)
+ : interface(ifname)
+ {
+ }
+
+ InterfaceDescriptor interface;
+ bool accepted_offer { false };
+ bool has_ip { false };
+ u32 offered_lease_time { 0 };
+};
+
+class DHCPv4Client final : public Core::Object {
+ C_OBJECT(DHCPv4Client)
+
+public:
+ explicit DHCPv4Client(Vector<InterfaceDescriptor> ifnames);
+ virtual ~DHCPv4Client() override;
+
+ void dhcp_discover(const InterfaceDescriptor& ifname, IPv4Address previous = IPv4Address { 0, 0, 0, 0 });
+ void dhcp_request(DHCPv4Transaction& transaction, const DHCPv4Packet& packet);
+
+ void process_incoming(const DHCPv4Packet& packet);
+
+ bool id_is_registered(u32 id) { return m_ongoing_transactions.contains(id); }
+
+private:
+ HashMap<u32, OwnPtr<DHCPv4Transaction>> m_ongoing_transactions;
+ Vector<InterfaceDescriptor> m_ifnames;
+ RefPtr<Core::UDPServer> m_server;
+
+ void handle_offer(const DHCPv4Packet&, const ParsedDHCPv4Options&);
+ void handle_ack(const DHCPv4Packet&, const ParsedDHCPv4Options&);
+ void handle_nak(const DHCPv4Packet&, const ParsedDHCPv4Options&);
+};
diff --git a/Services/DHCPClient/Makefile b/Services/DHCPClient/Makefile
new file mode 100644
index 0000000000..ce7c27b86d
--- /dev/null
+++ b/Services/DHCPClient/Makefile
@@ -0,0 +1,10 @@
+OBJS = \
+ DHCPv4.o \
+ DHCPv4Client.o \
+ main.o
+
+PROGRAM = DHCPClient
+
+LIB_DEPS = Core
+
+include ../../Makefile.common
diff --git a/Services/DHCPClient/main.cpp b/Services/DHCPClient/main.cpp
new file mode 100644
index 0000000000..b65b63eb46
--- /dev/null
+++ b/Services/DHCPClient/main.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DHCPv4Client.h"
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/String.h>
+#include <AK/Types.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/File.h>
+#include <LibCore/LocalServer.h>
+#include <stdio.h>
+#include <string.h>
+
+static u8 mac_part(const Vector<String>& parts, size_t index)
+{
+ auto chars = parts.at(index).characters();
+ return (chars[0] - '0') * 16 + (chars[1] - '0');
+}
+
+static MACAddress mac_from_string(const String& str)
+{
+ auto chunks = str.split(':');
+ ASSERT(chunks.size() == 6); // should we...worry about this?
+ return {
+ mac_part(chunks, 0), mac_part(chunks, 1), mac_part(chunks, 2),
+ mac_part(chunks, 3), mac_part(chunks, 4), mac_part(chunks, 5)
+ };
+}
+
+int main(int argc, char** argv)
+{
+ (void)argc;
+ (void)argv;
+
+ if (pledge("stdio unix inet cpath rpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ Core::EventLoop event_loop;
+
+ if (unveil("/proc/net/", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ auto file = Core::File::construct("/proc/net/adapters");
+ if (!file->open(Core::IODevice::ReadOnly)) {
+ fprintf(stderr, "Error: %s\n", file->error_string());
+ return 1;
+ }
+
+ auto file_contents = file->read_all();
+ auto json = JsonValue::from_string(file_contents).as_array();
+ Vector<InterfaceDescriptor> ifnames;
+ json.for_each([&ifnames](auto& value) {
+ auto if_object = value.as_object();
+
+ if (if_object.get("class_name").to_string() == "LoopbackAdapter")
+ return;
+
+ auto name = if_object.get("name").to_string();
+ auto mac = if_object.get("mac_address").to_string();
+ ifnames.append({ name, mac_from_string(mac) });
+ });
+
+ auto client = DHCPv4Client::construct(move(ifnames));
+
+ if (pledge("stdio inet", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ return event_loop.exec();
+}
diff --git a/Services/LookupServer/DNSAnswer.cpp b/Services/LookupServer/DNSAnswer.cpp
new file mode 100644
index 0000000000..775c23784e
--- /dev/null
+++ b/Services/LookupServer/DNSAnswer.cpp
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DNSAnswer.h"
+#include <time.h>
+
+DNSAnswer::DNSAnswer(const String& name, u16 type, u16 class_code, u32 ttl, const String& record_data)
+ : m_name(name)
+ , m_type(type)
+ , m_class_code(class_code)
+ , m_ttl(ttl)
+ , m_record_data(record_data)
+{
+ auto now = time(nullptr);
+ m_expiration_time = now + m_ttl;
+ if (m_expiration_time < now)
+ m_expiration_time = 0;
+}
+
+bool DNSAnswer::has_expired() const
+{
+ return time(nullptr) >= m_expiration_time;
+}
diff --git a/Services/LookupServer/DNSAnswer.h b/Services/LookupServer/DNSAnswer.h
new file mode 100644
index 0000000000..794603dd30
--- /dev/null
+++ b/Services/LookupServer/DNSAnswer.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Types.h>
+
+class DNSAnswer {
+public:
+ DNSAnswer(const String& name, u16 type, u16 class_code, u32 ttl, const String& record_data);
+
+ const String& name() const { return m_name; }
+ u16 type() const { return m_type; }
+ u16 class_code() const { return m_class_code; }
+ u32 ttl() const { return m_ttl; }
+ const String& record_data() const { return m_record_data; }
+
+ bool has_expired() const;
+
+private:
+ String m_name;
+ u16 m_type { 0 };
+ u16 m_class_code { 0 };
+ u32 m_ttl { 0 };
+ time_t m_expiration_time { 0 };
+ String m_record_data;
+};
diff --git a/Services/LookupServer/DNSPacket.h b/Services/LookupServer/DNSPacket.h
new file mode 100644
index 0000000000..832de9b7bb
--- /dev/null
+++ b/Services/LookupServer/DNSPacket.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/NetworkOrdered.h>
+#include <AK/Types.h>
+
+class [[gnu::packed]] DNSPacket
+{
+public:
+ DNSPacket()
+ : m_recursion_desired(false)
+ , m_truncated(false)
+ , m_authoritative_answer(false)
+ , m_opcode(0)
+ , m_query_or_response(false)
+ , m_response_code(0)
+ , m_checking_disabled(false)
+ , m_authenticated_data(false)
+ , m_zero(false)
+ , m_recursion_available(false)
+ {
+ }
+
+ u16 id() const { return m_id; }
+ void set_id(u16 w) { m_id = w; }
+
+ bool recursion_desired() const { return m_recursion_desired; }
+ void set_recursion_desired(bool b) { m_recursion_desired = b; }
+
+ bool is_truncated() const { return m_truncated; }
+ void set_truncated(bool b) { m_truncated = b; }
+
+ bool is_authoritative_answer() const { return m_authoritative_answer; }
+ void set_authoritative_answer(bool b) { m_authoritative_answer = b; }
+
+ u8 opcode() const { return m_opcode; }
+ void set_opcode(u8 b) { m_opcode = b; }
+
+ bool is_query() const { return !m_query_or_response; }
+ bool is_response() const { return m_query_or_response; }
+ void set_is_query() { m_query_or_response = false; }
+ void set_is_response() { m_query_or_response = true; }
+
+ u8 response_code() const { return m_response_code; }
+ void set_response_code(u8 b) { m_response_code = b; }
+
+ bool checking_disabled() const { return m_checking_disabled; }
+ void set_checking_disabled(bool b) { m_checking_disabled = b; }
+
+ bool is_authenticated_data() const { return m_authenticated_data; }
+ void set_authenticated_data(bool b) { m_authenticated_data = b; }
+
+ bool is_recursion_available() const { return m_recursion_available; }
+ void set_recursion_available(bool b) { m_recursion_available = b; }
+
+ u16 question_count() const { return m_question_count; }
+ void set_question_count(u16 w) { m_question_count = w; }
+
+ u16 answer_count() const { return m_answer_count; }
+ void set_answer_count(u16 w) { m_answer_count = w; }
+
+ u16 authority_count() const { return m_authority_count; }
+ void set_authority_count(u16 w) { m_authority_count = w; }
+
+ u16 additional_count() const { return m_additional_count; }
+ void set_additional_count(u16 w) { m_additional_count = w; }
+
+ void* payload() { return this + 1; }
+ const void* payload() const { return this + 1; }
+
+private:
+ NetworkOrdered<u16> m_id;
+
+ bool m_recursion_desired : 1;
+ bool m_truncated : 1;
+ bool m_authoritative_answer : 1;
+ u8 m_opcode : 4;
+ bool m_query_or_response : 1;
+ u8 m_response_code : 4;
+ bool m_checking_disabled : 1;
+ bool m_authenticated_data : 1;
+ bool m_zero : 1;
+ bool m_recursion_available : 1;
+
+ NetworkOrdered<u16> m_question_count;
+ NetworkOrdered<u16> m_answer_count;
+ NetworkOrdered<u16> m_authority_count;
+ NetworkOrdered<u16> m_additional_count;
+};
+
+static_assert(sizeof(DNSPacket) == 12);
diff --git a/Services/LookupServer/DNSQuestion.h b/Services/LookupServer/DNSQuestion.h
new file mode 100644
index 0000000000..ec696905e6
--- /dev/null
+++ b/Services/LookupServer/DNSQuestion.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Types.h>
+
+class DNSQuestion {
+public:
+ DNSQuestion(const String& name, u16 record_type, u16 class_code)
+ : m_name(name)
+ , m_record_type(record_type)
+ , m_class_code(class_code)
+ {
+ }
+
+ u16 record_type() const { return m_record_type; }
+ u16 class_code() const { return m_class_code; }
+ const String& name() const { return m_name; }
+
+ bool operator==(const DNSQuestion& other) const
+ {
+ return m_name == other.m_name && m_record_type == other.m_record_type && m_class_code == other.m_class_code;
+ }
+
+ bool operator!=(const DNSQuestion& other) const
+ {
+ return !(*this == other);
+ }
+
+private:
+ String m_name;
+ u16 m_record_type { 0 };
+ u16 m_class_code { 0 };
+};
diff --git a/Services/LookupServer/DNSRequest.cpp b/Services/LookupServer/DNSRequest.cpp
new file mode 100644
index 0000000000..79d6ff5aec
--- /dev/null
+++ b/Services/LookupServer/DNSRequest.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DNSRequest.h"
+#include "DNSPacket.h"
+#include <AK/BufferStream.h>
+#include <AK/StringBuilder.h>
+#include <arpa/inet.h>
+#include <ctype.h>
+#include <stdlib.h>
+
+#define C_IN 1
+
+DNSRequest::DNSRequest()
+ : m_id(arc4random_uniform(UINT16_MAX))
+{
+}
+
+void DNSRequest::add_question(const String& name, u16 record_type, ShouldRandomizeCase should_randomize_case)
+{
+ ASSERT(m_questions.size() <= UINT16_MAX);
+
+ if (name.is_empty())
+ return;
+
+ StringBuilder builder;
+ for (size_t i = 0; i < name.length(); ++i) {
+ u8 ch = name[i];
+ if (should_randomize_case == ShouldRandomizeCase::Yes) {
+ // Randomize the 0x20 bit in every ASCII character.
+ if (isalpha(ch)) {
+ if (arc4random_uniform(2))
+ ch |= 0x20;
+ else
+ ch &= ~0x20;
+ }
+ }
+ builder.append(ch);
+ }
+
+ if (name[name.length() - 1] != '.')
+ builder.append('.');
+
+ m_questions.empend(builder.to_string(), record_type, C_IN);
+}
+
+ByteBuffer DNSRequest::to_byte_buffer() const
+{
+ DNSPacket request_header;
+ request_header.set_id(m_id);
+ request_header.set_is_query();
+ request_header.set_opcode(0);
+ request_header.set_truncated(false);
+ request_header.set_recursion_desired(true);
+ request_header.set_question_count(m_questions.size());
+
+ auto buffer = ByteBuffer::create_uninitialized(m_questions.size() * 4096);
+ BufferStream stream(buffer);
+
+ stream << ByteBuffer::wrap(&request_header, sizeof(request_header));
+
+ for (auto& question : m_questions) {
+ auto parts = question.name().split('.');
+ for (auto& part : parts) {
+ stream << (u8)part.length();
+ stream << part;
+ }
+ stream << '\0';
+ stream << htons(question.record_type());
+ stream << htons(question.class_code());
+ }
+ stream.snip();
+
+ return buffer;
+}
diff --git a/Services/LookupServer/DNSRequest.h b/Services/LookupServer/DNSRequest.h
new file mode 100644
index 0000000000..2b55dd75fb
--- /dev/null
+++ b/Services/LookupServer/DNSRequest.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "DNSQuestion.h"
+#include <AK/Types.h>
+#include <AK/Vector.h>
+
+#define T_A 1
+#define T_NS 2
+#define T_CNAME 5
+#define T_SOA 6
+#define T_PTR 12
+#define T_MX 15
+
+enum class ShouldRandomizeCase {
+ No = 0,
+ Yes
+};
+
+class DNSRequest {
+public:
+ DNSRequest();
+
+ void add_question(const String& name, u16 record_type, ShouldRandomizeCase);
+
+ const Vector<DNSQuestion>& questions() const { return m_questions; }
+
+ u16 question_count() const
+ {
+ ASSERT(m_questions.size() < UINT16_MAX);
+ return m_questions.size();
+ }
+
+ u16 id() const { return m_id; }
+ ByteBuffer to_byte_buffer() const;
+
+private:
+ u16 m_id { 0 };
+ Vector<DNSQuestion> m_questions;
+};
diff --git a/Services/LookupServer/DNSResponse.cpp b/Services/LookupServer/DNSResponse.cpp
new file mode 100644
index 0000000000..614e58db2b
--- /dev/null
+++ b/Services/LookupServer/DNSResponse.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DNSResponse.h"
+#include "DNSPacket.h"
+#include "DNSRequest.h"
+#include <AK/IPv4Address.h>
+#include <AK/StringBuilder.h>
+
+static String parse_dns_name(const u8* data, size_t& offset, size_t max_offset, size_t recursion_level = 0);
+
+class [[gnu::packed]] DNSRecordWithoutName
+{
+public:
+ DNSRecordWithoutName() {}
+
+ u16 type() const { return m_type; }
+ u16 record_class() const { return m_class; }
+ u32 ttl() const { return m_ttl; }
+ u16 data_length() const { return m_data_length; }
+
+ void* data() { return this + 1; }
+ const void* data() const { return this + 1; }
+
+private:
+ NetworkOrdered<u16> m_type;
+ NetworkOrdered<u16> m_class;
+ NetworkOrdered<u32> m_ttl;
+ NetworkOrdered<u16> m_data_length;
+};
+
+static_assert(sizeof(DNSRecordWithoutName) == 10);
+
+Optional<DNSResponse> DNSResponse::from_raw_response(const u8* raw_data, size_t raw_size)
+{
+ if (raw_size < sizeof(DNSPacket)) {
+ dbg() << "DNS response not large enough (" << raw_size << " out of " << sizeof(DNSPacket) << ") to be a DNS packet.";
+ return {};
+ }
+
+ auto& response_header = *(const DNSPacket*)(raw_data);
+ dbgprintf("Got response (ID: %u)\n", response_header.id());
+ dbgprintf(" Question count: %u\n", response_header.question_count());
+ dbgprintf(" Answer count: %u\n", response_header.answer_count());
+ dbgprintf(" Authority count: %u\n", response_header.authority_count());
+ dbgprintf("Additional count: %u\n", response_header.additional_count());
+
+ DNSResponse response;
+ response.m_id = response_header.id();
+ response.m_code = response_header.response_code();
+
+ if (response.code() != DNSResponse::Code::NOERROR)
+ return response;
+
+ size_t offset = sizeof(DNSPacket);
+
+ for (u16 i = 0; i < response_header.question_count(); ++i) {
+ auto name = parse_dns_name(raw_data, offset, raw_size);
+ struct RawDNSAnswerQuestion {
+ NetworkOrdered<u16> record_type;
+ NetworkOrdered<u16> class_code;
+ };
+ auto& record_and_class = *(const RawDNSAnswerQuestion*)&raw_data[offset];
+ response.m_questions.empend(name, record_and_class.record_type, record_and_class.class_code);
+ auto& question = response.m_questions.last();
+ offset += 4;
+ dbg() << "Question #" << i << ": _" << question.name() << "_ type: " << question.record_type() << ", class: " << question.class_code();
+ }
+
+ for (u16 i = 0; i < response_header.answer_count(); ++i) {
+ auto name = parse_dns_name(raw_data, offset, raw_size);
+
+ auto& record = *(const DNSRecordWithoutName*)(&raw_data[offset]);
+
+ String data;
+
+ offset += sizeof(DNSRecordWithoutName);
+ if (record.type() == T_PTR) {
+ size_t dummy_offset = offset;
+ data = parse_dns_name(raw_data, dummy_offset, raw_size);
+ } else if (record.type() == T_A) {
+ auto ipv4_address = IPv4Address((const u8*)record.data());
+ data = ipv4_address.to_string();
+ } else {
+ // FIXME: Parse some other record types perhaps?
+ dbg() << " data=(unimplemented record type " << record.type() << ")";
+ }
+ dbg() << "Answer #" << i << ": name=_" << name << "_, type=" << record.type() << ", ttl=" << record.ttl() << ", length=" << record.data_length() << ", data=_" << data << "_";
+ response.m_answers.empend(name, record.type(), record.record_class(), record.ttl(), data);
+ offset += record.data_length();
+ }
+
+ return response;
+}
+
+String parse_dns_name(const u8* data, size_t& offset, size_t max_offset, size_t recursion_level)
+{
+ if (recursion_level > 4)
+ return {};
+ Vector<char, 128> buf;
+ while (offset < max_offset) {
+ u8 ch = data[offset];
+ if (ch == '\0') {
+ ++offset;
+ break;
+ }
+ if ((ch & 0xc0) == 0xc0) {
+ if ((offset + 1) >= max_offset)
+ return {};
+ size_t dummy = (ch & 0x3f) << 8 | data[offset + 1];
+ offset += 2;
+ StringBuilder builder;
+ builder.append(buf.data(), buf.size());
+ auto okay = parse_dns_name(data, dummy, max_offset, recursion_level + 1);
+ builder.append(okay);
+ return builder.to_string();
+ }
+ for (size_t i = 0; i < ch; ++i)
+ buf.append(data[offset + i + 1]);
+ buf.append('.');
+ offset += ch + 1;
+ }
+ return String::copy(buf);
+}
diff --git a/Services/LookupServer/DNSResponse.h b/Services/LookupServer/DNSResponse.h
new file mode 100644
index 0000000000..0301c12075
--- /dev/null
+++ b/Services/LookupServer/DNSResponse.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#pragma once
+
+#include "DNSAnswer.h"
+#include "DNSQuestion.h"
+#include <AK/Types.h>
+#include <AK/Vector.h>
+#include <AK/Optional.h>
+
+class DNSResponse {
+public:
+ static Optional<DNSResponse> from_raw_response(const u8*, size_t);
+
+ u16 id() const { return m_id; }
+ const Vector<DNSQuestion>& questions() const { return m_questions; }
+ const Vector<DNSAnswer>& answers() const { return m_answers; }
+
+ u16 question_count() const
+ {
+ ASSERT(m_questions.size() <= UINT16_MAX);
+ return m_questions.size();
+ }
+
+ u16 answer_count() const
+ {
+ ASSERT(m_answers.size() <= UINT16_MAX);
+ return m_answers.size();
+ }
+
+ enum class Code : u8 {
+ NOERROR = 0,
+ FORMERR = 1,
+ SERVFAIL = 2,
+ NXDOMAIN = 3,
+ NOTIMP = 4,
+ REFUSED = 5,
+ YXDOMAIN = 6,
+ XRRSET = 7,
+ NOTAUTH = 8,
+ NOTZONE = 9,
+ };
+
+ Code code() const { return (Code)m_code; }
+
+private:
+ DNSResponse() {}
+
+ u16 m_id { 0 };
+ u8 m_code { 0 };
+ Vector<DNSQuestion> m_questions;
+ Vector<DNSAnswer> m_answers;
+};
diff --git a/Services/LookupServer/LookupServer.cpp b/Services/LookupServer/LookupServer.cpp
new file mode 100644
index 0000000000..fe89bd97fc
--- /dev/null
+++ b/Services/LookupServer/LookupServer.cpp
@@ -0,0 +1,259 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LookupServer.h"
+#include "DNSRequest.h"
+#include "DNSResponse.h"
+#include <AK/HashMap.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/File.h>
+#include <LibCore/LocalServer.h>
+#include <LibCore/LocalSocket.h>
+#include <LibCore/UDPSocket.h>
+#include <stdio.h>
+#include <unistd.h>
+
+LookupServer::LookupServer()
+{
+ auto config = Core::ConfigFile::get_for_system("LookupServer");
+ dbg() << "Using network config file at " << config->file_name();
+ m_nameserver = config->read_entry("DNS", "Nameserver", "1.1.1.1");
+
+ load_etc_hosts();
+
+ m_local_server = Core::LocalServer::construct(this);
+ m_local_server->on_ready_to_accept = [this]() {
+ auto socket = m_local_server->accept();
+ socket->on_ready_to_read = [this, socket]() {
+ service_client(socket);
+ RefPtr<Core::LocalSocket> keeper = socket;
+ const_cast<Core::LocalSocket&>(*socket).on_ready_to_read = [] {};
+ };
+ };
+ bool ok = m_local_server->take_over_from_system_server();
+ ASSERT(ok);
+}
+
+void LookupServer::load_etc_hosts()
+{
+ auto file = Core::File::construct("/etc/hosts");
+ if (!file->open(Core::IODevice::ReadOnly))
+ return;
+ while (!file->eof()) {
+ auto line = file->read_line(1024);
+ if (line.is_empty())
+ break;
+ auto str_line = String((const char*)line.data(), line.size() - 1, Chomp);
+ auto fields = str_line.split('\t');
+
+ auto sections = fields[0].split('.');
+ IPv4Address addr {
+ (u8)atoi(sections[0].characters()),
+ (u8)atoi(sections[1].characters()),
+ (u8)atoi(sections[2].characters()),
+ (u8)atoi(sections[3].characters()),
+ };
+
+ auto name = fields[1];
+ m_etc_hosts.set(name, addr.to_string());
+
+ IPv4Address reverse_addr {
+ (u8)atoi(sections[3].characters()),
+ (u8)atoi(sections[2].characters()),
+ (u8)atoi(sections[1].characters()),
+ (u8)atoi(sections[0].characters()),
+ };
+ StringBuilder builder;
+ builder.append(reverse_addr.to_string());
+ builder.append(".in-addr.arpa");
+ m_etc_hosts.set(builder.to_string(), name);
+ }
+}
+
+void LookupServer::service_client(RefPtr<Core::LocalSocket> socket)
+{
+ u8 client_buffer[1024];
+ int nrecv = socket->read(client_buffer, sizeof(client_buffer) - 1);
+ if (nrecv < 0) {
+ perror("read");
+ return;
+ }
+
+ client_buffer[nrecv] = '\0';
+
+ char lookup_type = client_buffer[0];
+ if (lookup_type != 'L' && lookup_type != 'R') {
+ dbg() << "Invalid lookup_type " << lookup_type;
+ return;
+ }
+ auto hostname = String((const char*)client_buffer + 1, nrecv - 1, Chomp);
+ dbg() << "Got request for '" << hostname << "' (using IP " << m_nameserver << ")";
+
+ Vector<String> responses;
+
+ if (auto known_host = m_etc_hosts.get(hostname); known_host.has_value()) {
+ responses.append(known_host.value());
+ } else if (!hostname.is_empty()) {
+ bool did_timeout;
+ int retries = 3;
+ do {
+ did_timeout = false;
+ if (lookup_type == 'L')
+ responses = lookup(hostname, did_timeout, T_A);
+ else if (lookup_type == 'R')
+ responses = lookup(hostname, did_timeout, T_PTR);
+ if (!did_timeout)
+ break;
+ } while (--retries);
+ if (did_timeout) {
+ fprintf(stderr, "LookupServer: Out of retries :(\n");
+ return;
+ }
+ }
+
+ if (responses.is_empty()) {
+ int nsent = socket->write("Not found.\n");
+ if (nsent < 0)
+ perror("write");
+ return;
+ }
+ for (auto& response : responses) {
+ auto line = String::format("%s\n", response.characters());
+ int nsent = socket->write(line);
+ if (nsent < 0) {
+ perror("write");
+ break;
+ }
+ }
+}
+
+Vector<String> LookupServer::lookup(const String& hostname, bool& did_timeout, unsigned short record_type, ShouldRandomizeCase should_randomize_case)
+{
+ if (auto it = m_lookup_cache.find(hostname); it != m_lookup_cache.end()) {
+ auto& cached_lookup = it->value;
+ if (cached_lookup.question.record_type() == record_type) {
+ Vector<String> responses;
+ for (auto& cached_answer : cached_lookup.answers) {
+ dbg() << "Cache hit: " << hostname << " -> " << cached_answer.record_data() << ", expired: " << cached_answer.has_expired();
+ if (!cached_answer.has_expired()) {
+ responses.append(cached_answer.record_data());
+ }
+ }
+ if (!responses.is_empty())
+ return responses;
+ }
+ m_lookup_cache.remove(it);
+ }
+
+ DNSRequest request;
+ request.add_question(hostname, record_type, should_randomize_case);
+
+ auto buffer = request.to_byte_buffer();
+
+ auto udp_socket = Core::UDPSocket::construct();
+ udp_socket->set_blocking(true);
+
+ struct timeval timeout {
+ 1, 0
+ };
+
+ int rc = setsockopt(udp_socket->fd(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ if (rc < 0) {
+ perror("setsockopt(SOL_SOCKET, SO_RCVTIMEO)");
+ return {};
+ }
+
+ if (!udp_socket->connect(m_nameserver, 53))
+ return {};
+
+ if (!udp_socket->write(buffer))
+ return {};
+
+ u8 response_buffer[4096];
+ int nrecv = udp_socket->read(response_buffer, sizeof(response_buffer));
+ if (nrecv == 0)
+ return {};
+
+ auto o_response = DNSResponse::from_raw_response(response_buffer, nrecv);
+ if (!o_response.has_value())
+ return {};
+
+ auto& response = o_response.value();
+
+ if (response.id() != request.id()) {
+ dbgprintf("LookupServer: ID mismatch (%u vs %u) :(\n", response.id(), request.id());
+ return {};
+ }
+
+ if (response.code() == DNSResponse::Code::REFUSED) {
+ if (should_randomize_case == ShouldRandomizeCase::Yes) {
+ // Retry with 0x20 case randomization turned off.
+ return lookup(hostname, did_timeout, record_type, ShouldRandomizeCase::No);
+ }
+ return {};
+ }
+
+ if (response.question_count() != request.question_count()) {
+ dbgprintf("LookupServer: Question count (%u vs %u) :(\n", response.question_count(), request.question_count());
+ return {};
+ }
+
+ for (size_t i = 0; i < request.question_count(); ++i) {
+ auto& request_question = request.questions()[i];
+ auto& response_question = response.questions()[i];
+ if (request_question != response_question) {
+ dbg() << "Request and response questions do not match";
+ dbg() << " Request: {_" << request_question.name() << "_, " << request_question.record_type() << ", " << request_question.class_code() << "}";
+ dbg() << " Response: {_" << response_question.name() << "_, " << response_question.record_type() << ", " << response_question.class_code() << "}";
+ return {};
+ }
+ }
+
+ if (response.answer_count() < 1) {
+ dbgprintf("LookupServer: Not enough answers (%u) :(\n", response.answer_count());
+ return {};
+ }
+
+ Vector<String, 8> responses;
+ Vector<DNSAnswer, 8> cacheable_answers;
+ for (auto& answer : response.answers()) {
+ if (answer.type() != T_A)
+ continue;
+ responses.append(answer.record_data());
+ if (!answer.has_expired())
+ cacheable_answers.append(answer);
+ }
+
+ if (!cacheable_answers.is_empty()) {
+ if (m_lookup_cache.size() >= 256)
+ m_lookup_cache.remove(m_lookup_cache.begin());
+ m_lookup_cache.set(hostname, { request.questions()[0], move(cacheable_answers) });
+ }
+ return responses;
+}
diff --git a/Services/LookupServer/LookupServer.h b/Services/LookupServer/LookupServer.h
new file mode 100644
index 0000000000..4666e0904b
--- /dev/null
+++ b/Services/LookupServer/LookupServer.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "DNSRequest.h"
+#include "DNSResponse.h"
+#include <AK/HashMap.h>
+#include <LibCore/Object.h>
+
+class DNSAnswer;
+
+class LookupServer final : public Core::Object {
+ C_OBJECT(LookupServer)
+
+public:
+ LookupServer();
+
+private:
+ void load_etc_hosts();
+ void service_client(RefPtr<Core::LocalSocket>);
+ Vector<String> lookup(const String& hostname, bool& did_timeout, unsigned short record_type, ShouldRandomizeCase = ShouldRandomizeCase::Yes);
+
+ struct CachedLookup {
+ DNSQuestion question;
+ Vector<DNSAnswer> answers;
+ };
+
+ RefPtr<Core::LocalServer> m_local_server;
+ String m_nameserver;
+ HashMap<String, String> m_etc_hosts;
+ HashMap<String, CachedLookup> m_lookup_cache;
+};
diff --git a/Services/LookupServer/Makefile b/Services/LookupServer/Makefile
new file mode 100644
index 0000000000..42a4e1930b
--- /dev/null
+++ b/Services/LookupServer/Makefile
@@ -0,0 +1,12 @@
+OBJS = \
+ LookupServer.o \
+ DNSRequest.o \
+ DNSResponse.o \
+ DNSAnswer.o \
+ main.o
+
+PROGRAM = LookupServer
+
+LIB_DEPS = Core
+
+include ../../Makefile.common
diff --git a/Services/LookupServer/main.cpp b/Services/LookupServer/main.cpp
new file mode 100644
index 0000000000..d8f40e114b
--- /dev/null
+++ b/Services/LookupServer/main.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LookupServer.h"
+#include <LibCore/EventLoop.h>
+#include <LibCore/LocalServer.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ (void)argc;
+ (void)argv;
+
+ if (pledge("stdio accept unix inet cpath rpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ Core::EventLoop event_loop;
+ LookupServer server;
+
+ if (pledge("stdio accept inet", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ return event_loop.exec();
+}
diff --git a/Services/Makefile b/Services/Makefile
new file mode 100644
index 0000000000..0025ae2061
--- /dev/null
+++ b/Services/Makefile
@@ -0,0 +1,3 @@
+SUBDIRS := $(patsubst %/Makefile,%/,$(wildcard */Makefile))
+
+include ../Makefile.subdir
diff --git a/Services/NotificationServer/ClientConnection.cpp b/Services/NotificationServer/ClientConnection.cpp
new file mode 100644
index 0000000000..9b8064de03
--- /dev/null
+++ b/Services/NotificationServer/ClientConnection.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ClientConnection.h"
+#include "NotificationClientEndpoint.h"
+#include "NotificationWindow.h"
+#include <AK/HashMap.h>
+
+namespace NotificationServer {
+
+static HashMap<int, RefPtr<ClientConnection>> s_connections;
+
+ClientConnection::ClientConnection(Core::LocalSocket& client_socket, int client_id)
+ : IPC::ClientConnection<NotificationServerEndpoint>(*this, client_socket, client_id)
+{
+ s_connections.set(client_id, *this);
+}
+
+ClientConnection::~ClientConnection()
+{
+}
+
+void ClientConnection::die()
+{
+ s_connections.remove(client_id());
+}
+
+OwnPtr<Messages::NotificationServer::GreetResponse> ClientConnection::handle(const Messages::NotificationServer::Greet&)
+{
+ return make<Messages::NotificationServer::GreetResponse>(client_id());
+}
+
+OwnPtr<Messages::NotificationServer::ShowNotificationResponse> ClientConnection::handle(const Messages::NotificationServer::ShowNotification& message)
+{
+ auto window = NotificationWindow::construct(message.text(), message.title(), message.icon());
+ window->show();
+ return make<Messages::NotificationServer::ShowNotificationResponse>();
+}
+
+}
diff --git a/Services/NotificationServer/ClientConnection.h b/Services/NotificationServer/ClientConnection.h
new file mode 100644
index 0000000000..bec07fb127
--- /dev/null
+++ b/Services/NotificationServer/ClientConnection.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibIPC/ClientConnection.h>
+#include <NotificationServer/NotificationServerEndpoint.h>
+
+namespace NotificationServer {
+
+class ClientConnection final : public IPC::ClientConnection<NotificationServerEndpoint>
+ , public NotificationServerEndpoint {
+ C_OBJECT(ClientConnection)
+public:
+ ~ClientConnection() override;
+
+ virtual void die() override;
+
+private:
+ explicit ClientConnection(Core::LocalSocket&, int client_id);
+
+ virtual OwnPtr<Messages::NotificationServer::GreetResponse> handle(const Messages::NotificationServer::Greet&) override;
+ virtual OwnPtr<Messages::NotificationServer::ShowNotificationResponse> handle(const Messages::NotificationServer::ShowNotification&) override;
+};
+
+}
diff --git a/Services/NotificationServer/Makefile b/Services/NotificationServer/Makefile
new file mode 100644
index 0000000000..36a1e41652
--- /dev/null
+++ b/Services/NotificationServer/Makefile
@@ -0,0 +1,24 @@
+OBJS = \
+ main.o \
+ ClientConnection.o \
+ NotificationWindow.o
+
+PROGRAM = NotificationServer
+
+LIB_DEPS = GUI Gfx Core IPC
+
+EXTRA_CLEAN = NotificationServerEndpoint.h NotificationClientEndpoint.h
+
+*.cpp: NotificationServerEndpoint.h NotificationClientEndpoint.h
+
+NotificationServerEndpoint.h: NotificationServer.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+NotificationClientEndpoint.h: NotificationClient.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+install:
+ mkdir -p $(SERENITY_BASE_DIR)/Root/usr/include/NotificationServer/
+ cp *.h $(SERENITY_BASE_DIR)/Root/usr/include/NotificationServer/
+
+include ../../Makefile.common
diff --git a/Services/NotificationServer/NotificationClient.ipc b/Services/NotificationServer/NotificationClient.ipc
new file mode 100644
index 0000000000..fc85168f76
--- /dev/null
+++ b/Services/NotificationServer/NotificationClient.ipc
@@ -0,0 +1,4 @@
+endpoint NotificationClient = 92
+{
+ Dummy() =|
+}
diff --git a/Services/NotificationServer/NotificationServer.ipc b/Services/NotificationServer/NotificationServer.ipc
new file mode 100644
index 0000000000..f1ac5e13df
--- /dev/null
+++ b/Services/NotificationServer/NotificationServer.ipc
@@ -0,0 +1,7 @@
+endpoint NotificationServer = 95
+{
+ // Basic protocol
+ Greet() => (i32 client_id)
+
+ ShowNotification(String text, String title, Gfx::ShareableBitmap icon) => ()
+}
diff --git a/Services/NotificationServer/NotificationWindow.cpp b/Services/NotificationServer/NotificationWindow.cpp
new file mode 100644
index 0000000000..96b0c2652d
--- /dev/null
+++ b/Services/NotificationServer/NotificationWindow.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "NotificationWindow.h"
+#include <AK/Vector.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Desktop.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/ShareableBitmap.h>
+
+namespace NotificationServer {
+
+static Vector<RefPtr<NotificationWindow>> s_windows;
+
+void update_notification_window_locations()
+{
+ Gfx::Rect last_window_rect;
+ for (auto& window : s_windows) {
+ Gfx::Point new_window_location;
+ if (last_window_rect.is_null())
+ new_window_location = GUI::Desktop::the().rect().top_right().translated(-window->rect().width() - 24, 26);
+ else
+ new_window_location = last_window_rect.bottom_left().translated(0, 10);
+ if (window->rect().location() != new_window_location) {
+ window->move_to(new_window_location);
+ window->set_original_rect(window->rect());
+ }
+ last_window_rect = window->rect();
+ }
+}
+
+NotificationWindow::NotificationWindow(const String& text, const String& title, const Gfx::ShareableBitmap& icon)
+{
+ s_windows.append(this);
+
+ set_window_type(GUI::WindowType::Notification);
+ set_resizable(false);
+ set_minimizable(false);
+
+ Gfx::Rect lowest_notification_rect_on_screen;
+ for (auto& window : s_windows) {
+ if (window->m_original_rect.y() > lowest_notification_rect_on_screen.y())
+ lowest_notification_rect_on_screen = window->m_original_rect;
+ }
+
+ Gfx::Rect rect;
+ rect.set_width(220);
+ rect.set_height(40);
+ rect.set_location(GUI::Desktop::the().rect().top_right().translated(-rect.width() - 24, 26));
+
+ if (!lowest_notification_rect_on_screen.is_null())
+ rect.set_location(lowest_notification_rect_on_screen.bottom_left().translated(0, 10));
+
+ set_rect(rect);
+
+ m_original_rect = rect;
+
+ auto& widget = set_main_widget<GUI::Widget>();
+ widget.set_fill_with_background_color(true);
+
+ widget.set_layout<GUI::HorizontalBoxLayout>();
+ widget.layout()->set_margins({ 8, 8, 8, 8 });
+ widget.layout()->set_spacing(6);
+
+ if (icon.is_valid()) {
+ auto& icon_label = widget.add<GUI::Label>();
+ icon_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed);
+ icon_label.set_preferred_size(32, 32);
+ icon_label.set_icon(icon.bitmap());
+ }
+
+ auto& left_container = widget.add<GUI::Widget>();
+ left_container.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& title_label = left_container.add<GUI::Label>(title);
+ title_label.set_font(Gfx::Font::default_bold_font());
+ title_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ auto& text_label = left_container.add<GUI::Label>(text);
+ text_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& right_container = widget.add<GUI::Widget>();
+ right_container.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill);
+ right_container.set_preferred_size(36, 0);
+ right_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ on_close_request = [this] {
+ s_windows.remove_first_matching([this](auto& entry) { return entry == this; });
+ update_notification_window_locations();
+ return CloseRequestDecision::Close;
+ };
+}
+
+NotificationWindow::~NotificationWindow()
+{
+}
+
+}
diff --git a/Services/NotificationServer/NotificationWindow.h b/Services/NotificationServer/NotificationWindow.h
new file mode 100644
index 0000000000..3519af0752
--- /dev/null
+++ b/Services/NotificationServer/NotificationWindow.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Window.h>
+
+namespace NotificationServer {
+
+class NotificationWindow final : public GUI::Window {
+ C_OBJECT(NotificationWindow);
+
+public:
+ virtual ~NotificationWindow() override;
+ void set_original_rect(Gfx::Rect original_rect) { m_original_rect = original_rect; };
+
+private:
+ NotificationWindow(const String& text, const String& title, const Gfx::ShareableBitmap&);
+
+ Gfx::Rect m_original_rect;
+};
+
+}
diff --git a/Services/NotificationServer/main.cpp b/Services/NotificationServer/main.cpp
new file mode 100644
index 0000000000..f26aeec567
--- /dev/null
+++ b/Services/NotificationServer/main.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ClientConnection.h"
+#include <LibCore/EventLoop.h>
+#include <LibCore/LocalServer.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/WindowServerConnection.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept rpath wpath cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ GUI::Application app(argc, argv);
+ auto server = Core::LocalServer::construct();
+
+ bool ok = server->take_over_from_system_server();
+ ASSERT(ok);
+ server->on_ready_to_accept = [&] {
+ auto client_socket = server->accept();
+ if (!client_socket) {
+ dbg() << "NotificationServer: accept failed.";
+ return;
+ }
+ static int s_next_client_id = 0;
+ int client_id = ++s_next_client_id;
+ IPC::new_client_connection<NotificationServer::ClientConnection>(*client_socket, client_id);
+ };
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ if (pledge("stdio shared_buffer accept rpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ return app.exec();
+}
diff --git a/Services/ProtocolServer/Download.cpp b/Services/ProtocolServer/Download.cpp
new file mode 100644
index 0000000000..e4bbce082b
--- /dev/null
+++ b/Services/ProtocolServer/Download.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Badge.h>
+#include <ProtocolServer/Download.h>
+#include <ProtocolServer/PSClientConnection.h>
+
+// FIXME: What about rollover?
+static i32 s_next_id = 1;
+
+static HashMap<i32, RefPtr<Download>>& all_downloads()
+{
+ static HashMap<i32, RefPtr<Download>> map;
+ return map;
+}
+
+Download* Download::find_by_id(i32 id)
+{
+ return const_cast<Download*>(all_downloads().get(id).value_or(nullptr));
+}
+
+Download::Download(PSClientConnection& client)
+ : m_id(s_next_id++)
+ , m_client(client.make_weak_ptr())
+{
+ all_downloads().set(m_id, this);
+}
+
+Download::~Download()
+{
+}
+
+void Download::stop()
+{
+ all_downloads().remove(m_id);
+}
+
+void Download::set_payload(const ByteBuffer& payload)
+{
+ m_payload = payload;
+ m_total_size = payload.size();
+}
+
+void Download::set_response_headers(const HashMap<String, String>& response_headers)
+{
+ m_response_headers = response_headers;
+}
+
+void Download::did_finish(bool success)
+{
+ if (!m_client) {
+ dbg() << "Download::did_finish() after the client already disconnected.";
+ return;
+ }
+ m_client->did_finish_download({}, *this, success);
+ all_downloads().remove(m_id);
+}
+
+void Download::did_progress(Optional<u32> total_size, u32 downloaded_size)
+{
+ if (!m_client) {
+ // FIXME: We should also abort the download in this situation, I guess!
+ dbg() << "Download::did_progress() after the client already disconnected.";
+ return;
+ }
+ m_total_size = total_size;
+ m_downloaded_size = downloaded_size;
+ m_client->did_progress_download({}, *this);
+}
diff --git a/Services/ProtocolServer/Download.h b/Services/ProtocolServer/Download.h
new file mode 100644
index 0000000000..5609cd15b4
--- /dev/null
+++ b/Services/ProtocolServer/Download.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <AK/HashMap.h>
+#include <AK/Optional.h>
+#include <AK/RefCounted.h>
+#include <AK/URL.h>
+#include <AK/WeakPtr.h>
+
+class PSClientConnection;
+
+class Download : public RefCounted<Download> {
+public:
+ virtual ~Download();
+
+ static Download* find_by_id(i32);
+
+ i32 id() const { return m_id; }
+ URL url() const { return m_url; }
+
+ Optional<u32> total_size() const { return m_total_size; }
+ size_t downloaded_size() const { return m_downloaded_size; }
+ const ByteBuffer& payload() const { return m_payload; }
+ const HashMap<String, String>& response_headers() const { return m_response_headers; }
+
+ void stop();
+
+protected:
+ explicit Download(PSClientConnection&);
+
+ void did_finish(bool success);
+ void did_progress(Optional<u32> total_size, u32 downloaded_size);
+ void set_payload(const ByteBuffer&);
+ void set_response_headers(const HashMap<String, String>&);
+
+private:
+ i32 m_id;
+ URL m_url;
+ Optional<u32> m_total_size {};
+ size_t m_downloaded_size { 0 };
+ ByteBuffer m_payload;
+ HashMap<String, String> m_response_headers;
+ WeakPtr<PSClientConnection> m_client;
+};
diff --git a/Services/ProtocolServer/HttpDownload.cpp b/Services/ProtocolServer/HttpDownload.cpp
new file mode 100644
index 0000000000..5ffa30362b
--- /dev/null
+++ b/Services/ProtocolServer/HttpDownload.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibHTTP/HttpJob.h>
+#include <LibHTTP/HttpResponse.h>
+#include <ProtocolServer/HttpDownload.h>
+
+HttpDownload::HttpDownload(PSClientConnection& client, NonnullRefPtr<HTTP::HttpJob>&& job)
+ : Download(client)
+ , m_job(job)
+{
+ m_job->on_finish = [this](bool success) {
+ if (auto* response = m_job->response()) {
+ set_payload(response->payload());
+ set_response_headers(response->headers());
+ }
+
+ // if we didn't know the total size, pretend that the download finished successfully
+ // and set the total size to the downloaded size
+ if (!total_size().has_value())
+ did_progress(downloaded_size(), downloaded_size());
+
+ did_finish(success);
+ };
+ m_job->on_progress = [this](Optional<u32> total, u32 current) {
+ did_progress(total, current);
+ };
+}
+
+HttpDownload::~HttpDownload()
+{
+}
+
+NonnullRefPtr<HttpDownload> HttpDownload::create_with_job(Badge<HttpProtocol>, PSClientConnection& client, NonnullRefPtr<HTTP::HttpJob>&& job)
+{
+ return adopt(*new HttpDownload(client, move(job)));
+}
diff --git a/Services/ProtocolServer/HttpDownload.h b/Services/ProtocolServer/HttpDownload.h
new file mode 100644
index 0000000000..364fe6aef2
--- /dev/null
+++ b/Services/ProtocolServer/HttpDownload.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Badge.h>
+#include <LibCore/Forward.h>
+#include <LibHTTP/HttpJob.h>
+#include <ProtocolServer/Download.h>
+
+class HttpProtocol;
+
+class HttpDownload final : public Download {
+public:
+ virtual ~HttpDownload() override;
+ static NonnullRefPtr<HttpDownload> create_with_job(Badge<HttpProtocol>, PSClientConnection&, NonnullRefPtr<HTTP::HttpJob>&&);
+
+private:
+ explicit HttpDownload(PSClientConnection&, NonnullRefPtr<HTTP::HttpJob>&&);
+
+ NonnullRefPtr<HTTP::HttpJob> m_job;
+};
diff --git a/Services/ProtocolServer/HttpProtocol.cpp b/Services/ProtocolServer/HttpProtocol.cpp
new file mode 100644
index 0000000000..1513a32e66
--- /dev/null
+++ b/Services/ProtocolServer/HttpProtocol.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibHTTP/HttpJob.h>
+#include <LibHTTP/HttpRequest.h>
+#include <ProtocolServer/HttpDownload.h>
+#include <ProtocolServer/HttpProtocol.h>
+
+HttpProtocol::HttpProtocol()
+ : Protocol("http")
+{
+}
+
+HttpProtocol::~HttpProtocol()
+{
+}
+
+RefPtr<Download> HttpProtocol::start_download(PSClientConnection& client, const URL& url)
+{
+ HTTP::HttpRequest request;
+ request.set_method(HTTP::HttpRequest::Method::GET);
+ request.set_url(url);
+ auto job = request.schedule();
+ if (!job)
+ return nullptr;
+ return HttpDownload::create_with_job({}, client, (HTTP::HttpJob&)*job);
+}
diff --git a/Services/ProtocolServer/HttpProtocol.h b/Services/ProtocolServer/HttpProtocol.h
new file mode 100644
index 0000000000..ea6c15d2a4
--- /dev/null
+++ b/Services/ProtocolServer/HttpProtocol.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <ProtocolServer/Protocol.h>
+
+class HttpProtocol final : public Protocol {
+public:
+ HttpProtocol();
+ virtual ~HttpProtocol() override;
+
+ virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) override;
+};
diff --git a/Services/ProtocolServer/HttpsDownload.cpp b/Services/ProtocolServer/HttpsDownload.cpp
new file mode 100644
index 0000000000..8e068c2b04
--- /dev/null
+++ b/Services/ProtocolServer/HttpsDownload.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibHTTP/HttpResponse.h>
+#include <LibHTTP/HttpsJob.h>
+#include <ProtocolServer/HttpsDownload.h>
+
+HttpsDownload::HttpsDownload(PSClientConnection& client, NonnullRefPtr<HTTP::HttpsJob>&& job)
+ : Download(client)
+ , m_job(job)
+{
+ m_job->on_finish = [this](bool success) {
+ if (auto* response = m_job->response()) {
+ set_payload(response->payload());
+ set_response_headers(response->headers());
+ }
+
+ // if we didn't know the total size, pretend that the download finished successfully
+ // and set the total size to the downloaded size
+ if (!total_size().has_value())
+ did_progress(downloaded_size(), downloaded_size());
+
+ did_finish(success);
+ };
+ m_job->on_progress = [this](Optional<u32> total, u32 current) {
+ did_progress(total, current);
+ };
+}
+
+HttpsDownload::~HttpsDownload()
+{
+}
+
+NonnullRefPtr<HttpsDownload> HttpsDownload::create_with_job(Badge<HttpsProtocol>, PSClientConnection& client, NonnullRefPtr<HTTP::HttpsJob>&& job)
+{
+ return adopt(*new HttpsDownload(client, move(job)));
+}
diff --git a/Services/ProtocolServer/HttpsDownload.h b/Services/ProtocolServer/HttpsDownload.h
new file mode 100644
index 0000000000..a6c75bc4d7
--- /dev/null
+++ b/Services/ProtocolServer/HttpsDownload.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Badge.h>
+#include <LibCore/Forward.h>
+#include <LibHTTP/HttpsJob.h>
+#include <ProtocolServer/Download.h>
+
+class HttpsProtocol;
+
+class HttpsDownload final : public Download {
+public:
+ virtual ~HttpsDownload() override;
+ static NonnullRefPtr<HttpsDownload> create_with_job(Badge<HttpsProtocol>, PSClientConnection&, NonnullRefPtr<HTTP::HttpsJob>&&);
+
+private:
+ explicit HttpsDownload(PSClientConnection&, NonnullRefPtr<HTTP::HttpsJob>&&);
+
+ NonnullRefPtr<HTTP::HttpsJob> m_job;
+};
diff --git a/Services/ProtocolServer/HttpsProtocol.cpp b/Services/ProtocolServer/HttpsProtocol.cpp
new file mode 100644
index 0000000000..07020affe5
--- /dev/null
+++ b/Services/ProtocolServer/HttpsProtocol.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibHTTP/HttpRequest.h>
+#include <LibHTTP/HttpsJob.h>
+#include <ProtocolServer/HttpsDownload.h>
+#include <ProtocolServer/HttpsProtocol.h>
+
+HttpsProtocol::HttpsProtocol()
+ : Protocol("https")
+{
+}
+
+HttpsProtocol::~HttpsProtocol()
+{
+}
+
+RefPtr<Download> HttpsProtocol::start_download(PSClientConnection& client, const URL& url)
+{
+ HTTP::HttpRequest request;
+ request.set_method(HTTP::HttpRequest::Method::GET);
+ request.set_url(url);
+ auto job = HTTP::HttpsJob::construct(request);
+ auto download = HttpsDownload::create_with_job({}, client, (HTTP::HttpsJob&)*job);
+ job->start();
+ return download;
+}
diff --git a/Services/ProtocolServer/HttpsProtocol.h b/Services/ProtocolServer/HttpsProtocol.h
new file mode 100644
index 0000000000..e446178751
--- /dev/null
+++ b/Services/ProtocolServer/HttpsProtocol.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2018-2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <ProtocolServer/Protocol.h>
+
+class HttpsProtocol final : public Protocol {
+public:
+ HttpsProtocol();
+ virtual ~HttpsProtocol() override;
+
+ virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) override;
+};
diff --git a/Services/ProtocolServer/Makefile b/Services/ProtocolServer/Makefile
new file mode 100644
index 0000000000..fa39255c53
--- /dev/null
+++ b/Services/ProtocolServer/Makefile
@@ -0,0 +1,25 @@
+OBJS = \
+ PSClientConnection.o \
+ Protocol.o \
+ Download.o \
+ HttpProtocol.o \
+ HttpDownload.o \
+ HttpsProtocol.o \
+ HttpsDownload.o \
+ main.o
+
+PROGRAM = ProtocolServer
+
+LIB_DEPS = HTTP TLS Crypto Core IPC
+
+EXTRA_CLEAN = ProtocolServerEndpoint.h ProtocolClientEndpoint.h
+
+*.cpp: ProtocolServerEndpoint.h ProtocolClientEndpoint.h
+
+ProtocolServerEndpoint.h: ProtocolServer.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+ProtocolClientEndpoint.h: ProtocolClient.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+include ../../Makefile.common
diff --git a/Services/ProtocolServer/PSClientConnection.cpp b/Services/ProtocolServer/PSClientConnection.cpp
new file mode 100644
index 0000000000..1d07fdb8b2
--- /dev/null
+++ b/Services/ProtocolServer/PSClientConnection.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Badge.h>
+#include <AK/SharedBuffer.h>
+#include <ProtocolServer/Download.h>
+#include <ProtocolServer/PSClientConnection.h>
+#include <ProtocolServer/Protocol.h>
+#include <ProtocolServer/ProtocolClientEndpoint.h>
+
+static HashMap<int, RefPtr<PSClientConnection>> s_connections;
+
+PSClientConnection::PSClientConnection(Core::LocalSocket& socket, int client_id)
+ : IPC::ClientConnection<ProtocolServerEndpoint>(*this, socket, client_id)
+{
+ s_connections.set(client_id, *this);
+}
+
+PSClientConnection::~PSClientConnection()
+{
+}
+
+void PSClientConnection::die()
+{
+ s_connections.remove(client_id());
+}
+
+OwnPtr<Messages::ProtocolServer::IsSupportedProtocolResponse> PSClientConnection::handle(const Messages::ProtocolServer::IsSupportedProtocol& message)
+{
+ bool supported = Protocol::find_by_name(message.protocol().to_lowercase());
+ return make<Messages::ProtocolServer::IsSupportedProtocolResponse>(supported);
+}
+
+OwnPtr<Messages::ProtocolServer::StartDownloadResponse> PSClientConnection::handle(const Messages::ProtocolServer::StartDownload& message)
+{
+ URL url(message.url());
+ if (!url.is_valid())
+ return make<Messages::ProtocolServer::StartDownloadResponse>(-1);
+ auto* protocol = Protocol::find_by_name(url.protocol());
+ if (!protocol)
+ return make<Messages::ProtocolServer::StartDownloadResponse>(-1);
+ auto download = protocol->start_download(*this, url);
+ return make<Messages::ProtocolServer::StartDownloadResponse>(download->id());
+}
+
+OwnPtr<Messages::ProtocolServer::StopDownloadResponse> PSClientConnection::handle(const Messages::ProtocolServer::StopDownload& message)
+{
+ auto* download = Download::find_by_id(message.download_id());
+ bool success = false;
+ if (download) {
+ download->stop();
+ success = true;
+ }
+ return make<Messages::ProtocolServer::StopDownloadResponse>(success);
+}
+
+void PSClientConnection::did_finish_download(Badge<Download>, Download& download, bool success)
+{
+ RefPtr<SharedBuffer> buffer;
+ if (success && download.payload().size() > 0 && !download.payload().is_null()) {
+ buffer = SharedBuffer::create_with_size(download.payload().size());
+ memcpy(buffer->data(), download.payload().data(), download.payload().size());
+ buffer->seal();
+ buffer->share_with(client_pid());
+ m_shared_buffers.set(buffer->shbuf_id(), buffer);
+ }
+ ASSERT(download.total_size().has_value());
+
+ IPC::Dictionary response_headers;
+ for (auto& it : download.response_headers())
+ response_headers.add(it.key, it.value);
+ post_message(Messages::ProtocolClient::DownloadFinished(download.id(), success, download.total_size().value(), buffer ? buffer->shbuf_id() : -1, response_headers));
+}
+
+void PSClientConnection::did_progress_download(Badge<Download>, Download& download)
+{
+ post_message(Messages::ProtocolClient::DownloadProgress(download.id(), download.total_size(), download.downloaded_size()));
+}
+
+OwnPtr<Messages::ProtocolServer::GreetResponse> PSClientConnection::handle(const Messages::ProtocolServer::Greet&)
+{
+ return make<Messages::ProtocolServer::GreetResponse>(client_id());
+}
+
+OwnPtr<Messages::ProtocolServer::DisownSharedBufferResponse> PSClientConnection::handle(const Messages::ProtocolServer::DisownSharedBuffer& message)
+{
+ m_shared_buffers.remove(message.shbuf_id());
+ return make<Messages::ProtocolServer::DisownSharedBufferResponse>();
+}
diff --git a/Services/ProtocolServer/PSClientConnection.h b/Services/ProtocolServer/PSClientConnection.h
new file mode 100644
index 0000000000..56a7c63c0c
--- /dev/null
+++ b/Services/ProtocolServer/PSClientConnection.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashMap.h>
+#include <LibIPC/ClientConnection.h>
+#include <ProtocolServer/ProtocolServerEndpoint.h>
+
+class Download;
+
+class PSClientConnection final : public IPC::ClientConnection<ProtocolServerEndpoint>
+ , public ProtocolServerEndpoint {
+ C_OBJECT(PSClientConnection)
+public:
+ explicit PSClientConnection(Core::LocalSocket&, int client_id);
+ ~PSClientConnection() override;
+
+ virtual void die() override;
+
+ void did_finish_download(Badge<Download>, Download&, bool success);
+ void did_progress_download(Badge<Download>, Download&);
+
+private:
+ virtual OwnPtr<Messages::ProtocolServer::GreetResponse> handle(const Messages::ProtocolServer::Greet&) override;
+ virtual OwnPtr<Messages::ProtocolServer::IsSupportedProtocolResponse> handle(const Messages::ProtocolServer::IsSupportedProtocol&) override;
+ virtual OwnPtr<Messages::ProtocolServer::StartDownloadResponse> handle(const Messages::ProtocolServer::StartDownload&) override;
+ virtual OwnPtr<Messages::ProtocolServer::StopDownloadResponse> handle(const Messages::ProtocolServer::StopDownload&) override;
+ virtual OwnPtr<Messages::ProtocolServer::DisownSharedBufferResponse> handle(const Messages::ProtocolServer::DisownSharedBuffer&) override;
+
+ HashMap<i32, RefPtr<AK::SharedBuffer>> m_shared_buffers;
+};
diff --git a/Services/ProtocolServer/Protocol.cpp b/Services/ProtocolServer/Protocol.cpp
new file mode 100644
index 0000000000..bf53895eaa
--- /dev/null
+++ b/Services/ProtocolServer/Protocol.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/HashMap.h>
+#include <ProtocolServer/Protocol.h>
+
+static HashMap<String, Protocol*>& all_protocols()
+{
+ static HashMap<String, Protocol*> map;
+ return map;
+}
+
+Protocol* Protocol::find_by_name(const String& name)
+{
+ return all_protocols().get(name).value_or(nullptr);
+}
+
+Protocol::Protocol(const String& name)
+{
+ all_protocols().set(name, this);
+}
+
+Protocol::~Protocol()
+{
+ ASSERT_NOT_REACHED();
+}
diff --git a/Services/ProtocolServer/Protocol.h b/Services/ProtocolServer/Protocol.h
new file mode 100644
index 0000000000..19bd38f517
--- /dev/null
+++ b/Services/ProtocolServer/Protocol.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/RefPtr.h>
+#include <AK/URL.h>
+
+class Download;
+class PSClientConnection;
+
+class Protocol {
+public:
+ virtual ~Protocol();
+
+ const String& name() const { return m_name; }
+ virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) = 0;
+
+ static Protocol* find_by_name(const String&);
+
+protected:
+ explicit Protocol(const String& name);
+
+private:
+ String m_name;
+};
diff --git a/Services/ProtocolServer/ProtocolClient.ipc b/Services/ProtocolServer/ProtocolClient.ipc
new file mode 100644
index 0000000000..575b3ddd2b
--- /dev/null
+++ b/Services/ProtocolServer/ProtocolClient.ipc
@@ -0,0 +1,6 @@
+endpoint ProtocolClient = 13
+{
+ // Download notifications
+ DownloadProgress(i32 download_id, Optional<u32> total_size, u32 downloaded_size) =|
+ DownloadFinished(i32 download_id, bool success, u32 total_size, i32 shbuf_id, IPC::Dictionary response_headers) =|
+}
diff --git a/Services/ProtocolServer/ProtocolServer.ipc b/Services/ProtocolServer/ProtocolServer.ipc
new file mode 100644
index 0000000000..74ec138ab5
--- /dev/null
+++ b/Services/ProtocolServer/ProtocolServer.ipc
@@ -0,0 +1,15 @@
+endpoint ProtocolServer = 9
+{
+ // Basic protocol
+ Greet() => (i32 client_id)
+
+ // FIXME: It would be nice if the kernel provided a way to avoid this
+ DisownSharedBuffer(i32 shbuf_id) => ()
+
+ // Test if a specific protocol is supported, e.g "http"
+ IsSupportedProtocol(String protocol) => (bool supported)
+
+ // Download API
+ StartDownload(String url) => (i32 download_id)
+ StopDownload(i32 download_id) => (bool success)
+}
diff --git a/Services/ProtocolServer/main.cpp b/Services/ProtocolServer/main.cpp
new file mode 100644
index 0000000000..9a3da67b78
--- /dev/null
+++ b/Services/ProtocolServer/main.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibCore/EventLoop.h>
+#include <LibCore/LocalServer.h>
+#include <LibIPC/ClientConnection.h>
+#include <ProtocolServer/HttpProtocol.h>
+#include <ProtocolServer/HttpsProtocol.h>
+#include <ProtocolServer/PSClientConnection.h>
+
+int main(int, char**)
+{
+ if (pledge("stdio inet shared_buffer accept unix rpath cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+ Core::EventLoop event_loop;
+ // FIXME: Establish a connection to LookupServer and then drop "unix"?
+ if (pledge("stdio inet shared_buffer accept unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+ if (unveil("/tmp/portal/lookup", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ (void)*new HttpProtocol;
+ (void)*new HttpsProtocol;
+ auto server = Core::LocalServer::construct();
+ bool ok = server->take_over_from_system_server();
+ ASSERT(ok);
+ server->on_ready_to_accept = [&] {
+ auto client_socket = server->accept();
+ if (!client_socket) {
+ dbg() << "ProtocolServer: accept failed.";
+ return;
+ }
+ static int s_next_client_id = 0;
+ int client_id = ++s_next_client_id;
+ IPC::new_client_connection<PSClientConnection>(*client_socket, client_id);
+ };
+ return event_loop.exec();
+}
diff --git a/Services/SystemServer/Makefile b/Services/SystemServer/Makefile
new file mode 100644
index 0000000000..0a29c53463
--- /dev/null
+++ b/Services/SystemServer/Makefile
@@ -0,0 +1,13 @@
+OBJS = \
+ Service.o \
+ main.o
+
+PROGRAM = SystemServer
+
+LIB_DEPS = Core
+
+install:
+ mkdir -p ../../Root/usr/include/SystemServer/
+ cp *.h ../../Root/usr/include/SystemServer/
+
+include ../../Makefile.common
diff --git a/Services/SystemServer/Service.cpp b/Services/SystemServer/Service.cpp
new file mode 100644
index 0000000000..1f917ba1e6
--- /dev/null
+++ b/Services/SystemServer/Service.cpp
@@ -0,0 +1,365 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Service.h"
+#include <AK/HashMap.h>
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/LocalSocket.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <libgen.h>
+#include <pwd.h>
+#include <sched.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+struct UidAndGids {
+ uid_t uid;
+ gid_t gid;
+ Vector<gid_t> extra_gids;
+};
+
+static HashMap<String, UidAndGids>* s_user_map;
+static HashMap<pid_t, Service*> s_service_map;
+
+void Service::resolve_user()
+{
+ if (s_user_map == nullptr) {
+ s_user_map = new HashMap<String, UidAndGids>;
+ for (struct passwd* passwd = getpwent(); passwd; passwd = getpwent()) {
+ Vector<gid_t> extra_gids;
+ for (struct group* group = getgrent(); group; group = getgrent()) {
+ for (size_t m = 0; group->gr_mem[m]; ++m) {
+ if (!strcmp(group->gr_mem[m], passwd->pw_name))
+ extra_gids.append(group->gr_gid);
+ }
+ }
+ endgrent();
+ s_user_map->set(passwd->pw_name, { passwd->pw_uid, passwd->pw_gid, move(extra_gids) });
+ }
+ endpwent();
+ }
+
+ auto user = s_user_map->get(m_user);
+ if (!user.has_value()) {
+ dbg() << "Failed to resolve user name " << m_user;
+ ASSERT_NOT_REACHED();
+ }
+ m_uid = user.value().uid;
+ m_gid = user.value().gid;
+ m_extra_gids = user.value().extra_gids;
+}
+
+Service* Service::find_by_pid(pid_t pid)
+{
+ auto it = s_service_map.find(pid);
+ if (it == s_service_map.end())
+ return nullptr;
+ return (*it).value;
+}
+
+static int ensure_parent_directories(const char* path)
+{
+ ASSERT(path[0] == '/');
+
+ char* parent_buffer = strdup(path);
+ const char* parent = dirname(parent_buffer);
+
+ int rc = 0;
+ while (true) {
+ int rc = mkdir(parent, 0755);
+
+ if (rc == 0)
+ break;
+
+ if (errno != ENOENT)
+ break;
+
+ ensure_parent_directories(parent);
+ };
+
+ free(parent_buffer);
+ return rc;
+}
+
+void Service::setup_socket()
+{
+ ASSERT(!m_socket_path.is_null());
+ ASSERT(m_socket_fd == -1);
+
+ ensure_parent_directories(m_socket_path.characters());
+
+ // Note: we use SOCK_CLOEXEC here to make sure we don't leak every socket to
+ // all the clients. We'll make the one we do need to pass down !CLOEXEC later
+ // after forking off the process.
+ m_socket_fd = socket(AF_LOCAL, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ if (m_socket_fd < 0) {
+ perror("socket");
+ ASSERT_NOT_REACHED();
+ }
+
+ if (fchown(m_socket_fd, m_uid, m_gid) < 0) {
+ perror("fchown");
+ ASSERT_NOT_REACHED();
+ }
+
+ if (fchmod(m_socket_fd, m_socket_permissions) < 0) {
+ perror("fchmod");
+ ASSERT_NOT_REACHED();
+ }
+
+ auto socket_address = Core::SocketAddress::local(m_socket_path);
+ auto un = socket_address.to_sockaddr_un();
+ int rc = bind(m_socket_fd, (const sockaddr*)&un, sizeof(un));
+ if (rc < 0) {
+ perror("bind");
+ ASSERT_NOT_REACHED();
+ }
+
+ rc = listen(m_socket_fd, 16);
+ if (rc < 0) {
+ perror("listen");
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void Service::setup_notifier()
+{
+ ASSERT(m_lazy);
+ ASSERT(m_socket_fd >= 0);
+ ASSERT(!m_socket_notifier);
+
+ m_socket_notifier = Core::Notifier::construct(m_socket_fd, Core::Notifier::Event::Read, this);
+ m_socket_notifier->on_ready_to_read = [this] {
+ dbg() << "Ready to read on behalf of " << name();
+ remove_child(*m_socket_notifier);
+ m_socket_notifier = nullptr;
+ spawn();
+ };
+}
+
+void Service::activate()
+{
+ ASSERT(m_pid < 0);
+
+ if (m_lazy)
+ setup_notifier();
+ else
+ spawn();
+}
+
+void Service::spawn()
+{
+ dbg() << "Spawning " << name();
+
+ m_run_timer.start();
+ m_pid = fork();
+
+ if (m_pid < 0) {
+ perror("fork");
+ ASSERT_NOT_REACHED();
+ } else if (m_pid == 0) {
+ // We are the child.
+
+ if (!m_working_directory.is_null()) {
+ if (chdir(m_working_directory.characters()) < 0) {
+ perror("chdir");
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ struct sched_param p;
+ p.sched_priority = m_priority;
+ int rc = sched_setparam(0, &p);
+ if (rc < 0) {
+ perror("sched_setparam");
+ ASSERT_NOT_REACHED();
+ }
+
+ if (!m_stdio_file_path.is_null()) {
+ close(STDIN_FILENO);
+ int fd = open_with_path_length(m_stdio_file_path.characters(), m_stdio_file_path.length(), O_RDWR, 0);
+ ASSERT(fd <= 0);
+ if (fd < 0) {
+ perror("open");
+ ASSERT_NOT_REACHED();
+ }
+ dup2(STDIN_FILENO, STDOUT_FILENO);
+ dup2(STDIN_FILENO, STDERR_FILENO);
+
+ if (isatty(STDIN_FILENO)) {
+ ioctl(STDIN_FILENO, TIOCSCTTY);
+ }
+ } else {
+ if (isatty(STDIN_FILENO)) {
+ ioctl(STDIN_FILENO, TIOCNOTTY);
+ }
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+
+ int fd = open("/dev/null", O_RDWR);
+ ASSERT(fd == STDIN_FILENO);
+ dup2(STDIN_FILENO, STDOUT_FILENO);
+ dup2(STDIN_FILENO, STDERR_FILENO);
+ }
+
+ if (!m_socket_path.is_null()) {
+ ASSERT(m_socket_fd > 2);
+ dup2(m_socket_fd, 3);
+ // The new descriptor is !CLOEXEC here.
+ // This is true even if m_socket_fd == 3.
+ setenv("SOCKET_TAKEOVER", "1", true);
+ }
+
+ if (!m_user.is_null()) {
+ if (setgid(m_gid) < 0 || setgroups(m_extra_gids.size(), m_extra_gids.data()) < 0 || setuid(m_uid) < 0) {
+ dbgprintf("Failed to drop privileges (GID=%u, UID=%u)\n", m_gid, m_uid);
+ exit(1);
+ }
+ }
+
+ char* argv[m_extra_arguments.size() + 2];
+ argv[0] = const_cast<char*>(m_executable_path.characters());
+ for (size_t i = 0; i < m_extra_arguments.size(); i++)
+ argv[i + 1] = const_cast<char*>(m_extra_arguments[i].characters());
+ argv[m_extra_arguments.size() + 1] = nullptr;
+
+ rc = execv(argv[0], argv);
+ perror("exec");
+ ASSERT_NOT_REACHED();
+ } else {
+ // We are the parent.
+ s_service_map.set(m_pid, this);
+ }
+}
+
+void Service::did_exit(int exit_code)
+{
+ ASSERT(m_pid > 0);
+
+ dbg() << "Service " << name() << " has exited with exit code " << exit_code;
+
+ s_service_map.remove(m_pid);
+ m_pid = -1;
+
+ if (!m_keep_alive)
+ return;
+
+ int run_time_in_msec = m_run_timer.elapsed();
+ bool exited_successfully = exit_code == 0;
+
+ if (!exited_successfully && run_time_in_msec < 1000) {
+ switch (m_restart_attempts) {
+ case 0:
+ dbg() << "Trying again";
+ break;
+ case 1:
+ dbg() << "Third time's a charm?";
+ break;
+ default:
+ dbg() << "Giving up on " << name() << ". Good luck!";
+ return;
+ }
+ m_restart_attempts++;
+ }
+
+ activate();
+}
+
+Service::Service(const Core::ConfigFile& config, const StringView& name)
+ : Core::Object(nullptr)
+{
+ ASSERT(config.has_group(name));
+
+ set_name(name);
+ m_executable_path = config.read_entry(name, "Executable", String::format("/bin/%s", this->name().characters()));
+ m_extra_arguments = config.read_entry(name, "Arguments", "").split(' ');
+ m_stdio_file_path = config.read_entry(name, "StdIO");
+
+ String prio = config.read_entry(name, "Priority");
+ if (prio == "low")
+ m_priority = 10;
+ else if (prio == "normal" || prio.is_null())
+ m_priority = 30;
+ else if (prio == "high")
+ m_priority = 50;
+ else
+ ASSERT_NOT_REACHED();
+
+ m_keep_alive = config.read_bool_entry(name, "KeepAlive");
+ m_lazy = config.read_bool_entry(name, "Lazy");
+
+ m_user = config.read_entry(name, "User");
+ if (!m_user.is_null())
+ resolve_user();
+
+ m_socket_path = config.read_entry(name, "Socket");
+ if (!m_socket_path.is_null()) {
+ auto socket_permissions_string = config.read_entry(name, "SocketPermissions", "0600");
+ m_socket_permissions = strtol(socket_permissions_string.characters(), nullptr, 8) & 04777;
+ setup_socket();
+ }
+
+ m_working_directory = config.read_entry(name, "WorkingDirectory");
+}
+
+void Service::save_to(JsonObject& json)
+{
+ Core::Object::save_to(json);
+
+ json.set("executable_path", m_executable_path);
+
+ // FIXME: This crashes Inspector.
+ /*
+ JsonArray extra_args;
+ for (String& arg : m_extra_arguments)
+ extra_args.append(arg);
+ json.set("extra_arguments", move(extra_args));
+ */
+
+ json.set("stdio_file_path", m_stdio_file_path);
+ json.set("priority", m_priority);
+ json.set("keep_alive", m_keep_alive);
+ json.set("socket_path", m_socket_path);
+ json.set("socket_permissions", m_socket_permissions);
+ json.set("lazy", m_lazy);
+ json.set("user", m_user);
+ json.set("uid", m_uid);
+ json.set("gid", m_gid);
+
+ if (m_pid > 0)
+ json.set("pid", m_pid);
+ else
+ json.set("pid", nullptr);
+
+ json.set("restart_attempts", m_restart_attempts);
+ json.set("working_directory", m_working_directory);
+}
diff --git a/Services/SystemServer/Service.h b/Services/SystemServer/Service.h
new file mode 100644
index 0000000000..de408ec04d
--- /dev/null
+++ b/Services/SystemServer/Service.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <LibCore/ElapsedTimer.h>
+#include <LibCore/Notifier.h>
+#include <LibCore/Object.h>
+
+class Service final : public Core::Object {
+ C_OBJECT(Service)
+
+public:
+ void activate();
+ void did_exit(int exit_code);
+
+ static Service* find_by_pid(pid_t);
+
+ void save_to(AK::JsonObject&) override;
+
+private:
+ Service(const Core::ConfigFile&, const StringView& name);
+
+ void spawn();
+
+ // Path to the executable. By default this is /bin/{m_name}.
+ String m_executable_path;
+ // Extra arguments, starting from argv[1], to pass when exec'ing.
+ Vector<String> m_extra_arguments;
+ // File path to open as stdio fds.
+ String m_stdio_file_path;
+ int m_priority { 1 };
+ // Whether we should re-launch it if it exits.
+ bool m_keep_alive { false };
+ // Path to the socket to create and listen on on behalf of this service.
+ String m_socket_path;
+ // File system permissions for the socket.
+ mode_t m_socket_permissions { 0 };
+ // Whether we should only spawn this service once somebody connects to the socket.
+ bool m_lazy;
+ // The name of the user we should run this service as.
+ String m_user;
+ uid_t m_uid { 0 };
+ gid_t m_gid { 0 };
+ Vector<gid_t> m_extra_gids;
+
+ // PID of the running instance of this service.
+ pid_t m_pid { -1 };
+ // An open fd to the socket.
+ int m_socket_fd { -1 };
+ RefPtr<Core::Notifier> m_socket_notifier;
+
+ // Timer since we last spawned the service.
+ Core::ElapsedTimer m_run_timer;
+ // How many times we have tried to restart this service, only counting those
+ // times where it has exited unsuccessfully and too quickly.
+ int m_restart_attempts { 0 };
+
+ // The working directory in which to spawn the service
+ String m_working_directory;
+
+ void resolve_user();
+ void setup_socket();
+ void setup_notifier();
+};
diff --git a/Services/SystemServer/main.cpp b/Services/SystemServer/main.cpp
new file mode 100644
index 0000000000..0d49d0d937
--- /dev/null
+++ b/Services/SystemServer/main.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Service.h"
+#include <AK/Assertions.h>
+#include <AK/ByteBuffer.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/Event.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/File.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static void sigchld_handler(int)
+{
+ int status = 0;
+ pid_t pid = waitpid(-1, &status, WNOHANG);
+ if (!pid)
+ return;
+
+ dbg() << "Reaped child with pid " << pid;
+ Service* service = Service::find_by_pid(pid);
+ if (service == nullptr) {
+ dbg() << "There was no service with this pid, what is going on?";
+ return;
+ }
+
+ // Call service->did_exit(status) some time soon.
+ // We wouldn't want to run the complex logic, such
+ // as possibly spawning the service again, from the
+ // signal handler, so defer it.
+ Core::EventLoop::main().post_event(*service, make<Core::DeferredInvocationEvent>([=](auto&) {
+ service->did_exit(status);
+ }));
+ Core::EventLoop::wake();
+}
+
+static void check_for_test_mode()
+{
+ auto f = Core::File::construct("/proc/cmdline");
+ if (!f->open(Core::IODevice::ReadOnly)) {
+ dbg() << "Failed to read command line: " << f->error_string();
+ ASSERT(false);
+ }
+ const String cmd = String::copy(f->read_all());
+ dbg() << "Read command line: " << cmd;
+ if (cmd.matches("*testmode=1*")) {
+ // Eventually, we should run a test binary and wait for it to finish
+ // before shutting down. But this is good enough for now.
+ dbg() << "Waiting for testmode shutdown...";
+ sleep(5);
+ dbg() << "Shutting down due to testmode...";
+ if (fork() == 0) {
+ execl("/bin/shutdown", "/bin/shutdown", "-n", nullptr);
+ ASSERT_NOT_REACHED();
+ }
+ } else {
+ dbg() << "Continuing normally";
+ }
+}
+
+static void mount_all_filesystems()
+{
+ dbg() << "Spawning mount -a to mount all filesystems.";
+ pid_t pid = fork();
+
+ if (pid < 0) {
+ perror("fork");
+ ASSERT_NOT_REACHED();
+ } else if (pid == 0) {
+ execl("/bin/mount", "mount", "-a", nullptr);
+ perror("exec");
+ ASSERT_NOT_REACHED();
+ } else {
+ wait(nullptr);
+ }
+}
+
+int main(int, char**)
+{
+ if (pledge("stdio proc exec tty accept unix rpath wpath cpath chown fattr id", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ mount_all_filesystems();
+
+ struct sigaction sa = {
+ .sa_handler = sigchld_handler,
+ .sa_mask = 0,
+ .sa_flags = SA_RESTART
+ };
+ sigaction(SIGCHLD, &sa, nullptr);
+
+ Core::EventLoop event_loop;
+
+ // Read our config and instantiate services.
+ // This takes care of setting up sockets.
+ NonnullRefPtrVector<Service> services;
+ auto config = Core::ConfigFile::get_for_system("SystemServer");
+ for (auto name : config->groups())
+ services.append(Service::construct(*config, name));
+
+ // After we've set them all up, activate them!
+ for (auto& service : services)
+ service.activate();
+
+ // This won't return if we're in test mode.
+ check_for_test_mode();
+
+ return event_loop.exec();
+}
diff --git a/Services/TTYServer/Makefile b/Services/TTYServer/Makefile
new file mode 100644
index 0000000000..d1eb798683
--- /dev/null
+++ b/Services/TTYServer/Makefile
@@ -0,0 +1,6 @@
+OBJS = \
+ main.o
+
+PROGRAM = TTYServer
+
+include ../../Makefile.common
diff --git a/Services/TTYServer/main.cpp b/Services/TTYServer/main.cpp
new file mode 100644
index 0000000000..79e43ca84b
--- /dev/null
+++ b/Services/TTYServer/main.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Assertions.h>
+#include <AK/LogStream.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio tty proc exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/bin/Shell", "x") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ if (argc < 2)
+ return -1;
+
+ dbgprintf("Starting console server on %s\n", argv[1]);
+
+ while (true) {
+ dbgprintf("Running shell on %s\n", argv[1]);
+
+ auto child = fork();
+ if (!child) {
+ int rc = execl("/bin/Shell", "Shell", nullptr);
+ ASSERT(rc < 0);
+ perror("execl");
+ exit(127);
+ } else {
+ int wstatus;
+ waitpid(child, &wstatus, 0);
+ dbgprintf("Shell on %s exited with code %d\n", argv[1], WEXITSTATUS(wstatus));
+ }
+ }
+}
diff --git a/Services/TelnetServer/Client.cpp b/Services/TelnetServer/Client.cpp
new file mode 100644
index 0000000000..e983025df6
--- /dev/null
+++ b/Services/TelnetServer/Client.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Client.h"
+#include <AK/BufferStream.h>
+#include <AK/ByteBuffer.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/StringView.h>
+#include <AK/Types.h>
+#include <LibCore/Notifier.h>
+#include <LibCore/TCPSocket.h>
+#include <stdio.h>
+#include <unistd.h>
+
+Client::Client(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd)
+ : m_id(id)
+ , m_socket(move(socket))
+ , m_ptm_fd(ptm_fd)
+ , m_ptm_notifier(Core::Notifier::construct(ptm_fd, Core::Notifier::Read))
+{
+ m_socket->on_ready_to_read = [this] { drain_socket(); };
+ m_ptm_notifier->on_ready_to_read = [this] { drain_pty(); };
+ m_parser.on_command = [this](const Command& command) { handle_command(command); };
+ m_parser.on_data = [this](const StringView& data) { handle_data(data); };
+ m_parser.on_error = [this]() { handle_error(); };
+ send_commands({
+ { CMD_WILL, SUB_SUPPRESS_GO_AHEAD },
+ { CMD_WILL, SUB_ECHO },
+ { CMD_DO, SUB_SUPPRESS_GO_AHEAD },
+ { CMD_DONT, SUB_ECHO },
+ });
+}
+
+void Client::drain_socket()
+{
+ NonnullRefPtr<Client> protect(*this);
+ while (m_socket->can_read()) {
+ auto buf = m_socket->read(1024);
+
+ m_parser.write(buf);
+
+ if (m_socket->eof()) {
+ quit();
+ break;
+ }
+ }
+}
+
+void Client::drain_pty()
+{
+ u8 buffer[BUFSIZ];
+ ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer));
+ if (nread < 0) {
+ perror("read(ptm)");
+ quit();
+ return;
+ }
+ if (nread == 0) {
+ quit();
+ return;
+ }
+ send_data(StringView(buffer, (size_t)nread));
+}
+
+void Client::handle_data(const StringView& data)
+{
+ write(m_ptm_fd, data.characters_without_null_termination(), data.length());
+}
+
+void Client::handle_command(const Command& command)
+{
+ switch (command.command) {
+ case CMD_DO:
+ // no response - we've already advertised our options, and none of
+ // them can be disabled (or re-enabled) after connecting.
+ break;
+ case CMD_DONT:
+ // no response - we only "support" two options (echo and suppres
+ // go-ahead), and both of them are always enabled.
+ break;
+ case CMD_WILL:
+ switch (command.subcommand) {
+ case SUB_ECHO:
+ // we always want to be the ones in control of the output. tell
+ // the client to disable local echo.
+ send_command({ CMD_DONT, SUB_ECHO });
+ break;
+ case SUB_SUPPRESS_GO_AHEAD:
+ send_command({ CMD_DO, SUB_SUPPRESS_GO_AHEAD });
+ break;
+ default:
+ // don't respond to unknown commands
+ break;
+ }
+ break;
+ case CMD_WONT:
+ // no response - we don't care about anything the client says they
+ // won't do.
+ break;
+ }
+}
+
+void Client::handle_error()
+{
+ quit();
+}
+
+void Client::send_data(StringView data)
+{
+ bool fast = true;
+ for (size_t i = 0; i < data.length(); i++) {
+ u8 c = data[i];
+ if (c == '\n' || c == 0xff)
+ fast = false;
+ }
+
+ if (fast) {
+ m_socket->write(data);
+ return;
+ }
+
+ StringBuilder builder;
+ for (size_t i = 0; i < data.length(); i++) {
+ u8 c = data[i];
+
+ switch (c) {
+ case '\n':
+ builder.append("\r\n");
+ break;
+ case IAC:
+ builder.append("\xff\xff");
+ break;
+ default:
+ builder.append(c);
+ break;
+ }
+ }
+
+ m_socket->write(builder.to_string());
+}
+
+void Client::send_command(Command command)
+{
+ send_commands({ command });
+}
+
+void Client::send_commands(Vector<Command> commands)
+{
+ auto buffer = ByteBuffer::create_uninitialized(commands.size() * 3);
+ BufferStream stream(buffer);
+ for (auto& command : commands)
+ stream << (u8)IAC << command.command << command.subcommand;
+ stream.snip();
+ m_socket->write(buffer.data(), buffer.size());
+}
+
+void Client::quit()
+{
+ m_ptm_notifier->set_enabled(false);
+ close(m_ptm_fd);
+ m_socket->close();
+ if (on_exit)
+ on_exit();
+}
diff --git a/Services/TelnetServer/Client.h b/Services/TelnetServer/Client.h
new file mode 100644
index 0000000000..fe345d38e3
--- /dev/null
+++ b/Services/TelnetServer/Client.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/StringView.h>
+#include <AK/Types.h>
+#include <LibCore/Notifier.h>
+#include <LibCore/TCPSocket.h>
+
+#include "Command.h"
+#include "Parser.h"
+
+class Client : public RefCounted<Client> {
+public:
+ static NonnullRefPtr<Client> create(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd)
+ {
+ return adopt(*new Client(id, move(socket), ptm_fd));
+ }
+
+ Function<void()> on_exit;
+
+protected:
+ Client(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd);
+
+ void drain_socket();
+ void drain_pty();
+ void handle_data(const StringView&);
+ void handle_command(const Command& command);
+ void handle_error();
+ void send_data(StringView str);
+ void send_command(Command command);
+ void send_commands(Vector<Command> commands);
+ void quit();
+
+private:
+ // client id
+ int m_id { 0 };
+ // client resources
+ RefPtr<Core::TCPSocket> m_socket;
+ Parser m_parser;
+ // pty resources
+ int m_ptm_fd { -1 };
+ RefPtr<Core::Notifier> m_ptm_notifier;
+};
diff --git a/Services/TelnetServer/Command.h b/Services/TelnetServer/Command.h
new file mode 100644
index 0000000000..ac696c2111
--- /dev/null
+++ b/Services/TelnetServer/Command.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Types.h>
+
+#define CMD_WILL 0xfb
+#define CMD_WONT 0xfc
+#define CMD_DO 0xfd
+#define CMD_DONT 0xfe
+#define SUB_ECHO 0x01
+#define SUB_SUPPRESS_GO_AHEAD 0x03
+
+struct Command {
+ u8 command;
+ u8 subcommand;
+
+ String to_string() const
+ {
+ StringBuilder builder;
+
+ switch (command) {
+ case CMD_WILL:
+ builder.append("WILL");
+ break;
+ case CMD_WONT:
+ builder.append("WONT");
+ break;
+ case CMD_DO:
+ builder.append("DO");
+ break;
+ case CMD_DONT:
+ builder.append("DONT");
+ break;
+ default:
+ builder.append(String::format("UNKNOWN<%02x>", command));
+ break;
+ }
+
+ builder.append(" ");
+
+ switch (subcommand) {
+ case SUB_ECHO:
+ builder.append("ECHO");
+ break;
+ case SUB_SUPPRESS_GO_AHEAD:
+ builder.append("SUPPRESS_GO_AHEAD");
+ break;
+ default:
+ builder.append(String::format("UNKNOWN<%02x>"));
+ break;
+ }
+
+ return builder.to_string();
+ };
+};
diff --git a/Services/TelnetServer/Makefile b/Services/TelnetServer/Makefile
new file mode 100644
index 0000000000..bfee24f38c
--- /dev/null
+++ b/Services/TelnetServer/Makefile
@@ -0,0 +1,10 @@
+OBJS = \
+ Client.o \
+ Parser.o \
+ main.o
+
+PROGRAM = TelnetServer
+
+LIB_DEPS = Core
+
+include ../../Makefile.common
diff --git a/Services/TelnetServer/Parser.cpp b/Services/TelnetServer/Parser.cpp
new file mode 100644
index 0000000000..9fc1aabcd1
--- /dev/null
+++ b/Services/TelnetServer/Parser.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Function.h>
+#include <AK/String.h>
+#include <AK/Types.h>
+
+#include "Parser.h"
+
+void Parser::write(const StringView& data)
+{
+ for (size_t i = 0; i < data.length(); i++) {
+ u8 ch = data[i];
+
+ switch (m_state) {
+ case State::Free:
+ switch (ch) {
+ case IAC:
+ m_state = State::ReadCommand;
+ break;
+ case '\r':
+ if (on_data)
+ on_data("\n");
+ break;
+ default:
+ if (on_data)
+ on_data(StringView(&ch, 1));
+ break;
+ }
+ break;
+ case State::ReadCommand:
+ switch (ch) {
+ case IAC: {
+ m_state = State::Free;
+ if (on_data)
+ on_data("\xff");
+ break;
+ }
+ case CMD_WILL:
+ case CMD_WONT:
+ case CMD_DO:
+ case CMD_DONT:
+ m_command = ch;
+ m_state = State::ReadSubcommand;
+ break;
+ default:
+ m_state = State::Error;
+ if (on_error)
+ on_error();
+ break;
+ }
+ break;
+ case State::ReadSubcommand: {
+ auto command = m_command;
+ m_command = 0;
+ m_state = State::Free;
+ if (on_command)
+ on_command({ command, ch });
+ break;
+ }
+ case State::Error:
+ // ignore everything
+ break;
+ }
+ }
+}
diff --git a/Services/TelnetServer/Parser.h b/Services/TelnetServer/Parser.h
new file mode 100644
index 0000000000..64688881df
--- /dev/null
+++ b/Services/TelnetServer/Parser.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/String.h>
+#include <AK/StringView.h>
+#include <AK/Types.h>
+
+#include "Command.h"
+
+#define IAC 0xff
+
+class Parser {
+public:
+ Function<void(const Command&)> on_command;
+ Function<void(const StringView&)> on_data;
+ Function<void()> on_error;
+
+ void write(const StringView&);
+
+protected:
+ enum State {
+ Free,
+ ReadCommand,
+ ReadSubcommand,
+ Error,
+ };
+
+ void write(const String& str);
+
+private:
+ State m_state { State::Free };
+ u8 m_command { 0 };
+};
diff --git a/Services/TelnetServer/main.cpp b/Services/TelnetServer/main.cpp
new file mode 100644
index 0000000000..6acb900b61
--- /dev/null
+++ b/Services/TelnetServer/main.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Client.h"
+#include <AK/BufferStream.h>
+#include <AK/ByteBuffer.h>
+#include <AK/HashMap.h>
+#include <AK/IPv4Address.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Types.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/TCPServer.h>
+#include <LibCore/TCPSocket.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+
+static void run_command(int ptm_fd, String command)
+{
+ pid_t pid = fork();
+ if (pid == 0) {
+ const char* tty_name = ptsname(ptm_fd);
+ if (!tty_name) {
+ perror("ptsname");
+ exit(1);
+ }
+ close(ptm_fd);
+ int pts_fd = open(tty_name, O_RDWR);
+ if (pts_fd < 0) {
+ perror("open");
+ exit(1);
+ }
+
+ // NOTE: It's okay if this fails.
+ (void)ioctl(0, TIOCNOTTY);
+
+ close(0);
+ close(1);
+ close(2);
+
+ int rc = dup2(pts_fd, 0);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = dup2(pts_fd, 1);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = dup2(pts_fd, 2);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = close(pts_fd);
+ if (rc < 0) {
+ perror("close");
+ exit(1);
+ }
+ rc = ioctl(0, TIOCSCTTY);
+ if (rc < 0) {
+ perror("ioctl(TIOCSCTTY)");
+ exit(1);
+ }
+ const char* args[4] = { "/bin/Shell", nullptr, nullptr, nullptr };
+ if (!command.is_empty()) {
+ args[1] = "-c";
+ args[2] = command.characters();
+ }
+ const char* envs[] = { "TERM=xterm", "PATH=/bin:/usr/bin:/usr/local/bin", nullptr };
+ rc = execve("/bin/Shell", const_cast<char**>(args), const_cast<char**>(envs));
+ if (rc < 0) {
+ perror("execve");
+ exit(1);
+ }
+ ASSERT_NOT_REACHED();
+ }
+}
+
+int main(int argc, char** argv)
+{
+ Core::EventLoop event_loop;
+ auto server = Core::TCPServer::construct();
+
+ int opt;
+ u16 port = 23;
+ const char* command = "";
+ while ((opt = getopt(argc, argv, "p:c:")) != -1) {
+ switch (opt) {
+ case 'p':
+ port = atoi(optarg);
+ break;
+ case 'c':
+ command = optarg;
+ break;
+ default:
+ fprintf(stderr, "Usage: %s [-p port] [-c command]", argv[0]);
+ exit(1);
+ }
+ }
+
+ if (!server->listen({}, port)) {
+ perror("listen");
+ exit(1);
+ }
+
+ HashMap<int, NonnullRefPtr<Client>> clients;
+ int next_id = 0;
+
+ server->on_ready_to_accept = [&next_id, &clients, &server, command] {
+ int id = next_id++;
+
+ auto client_socket = server->accept();
+ if (!client_socket) {
+ perror("accept");
+ return;
+ }
+
+ int ptm_fd = posix_openpt(O_RDWR);
+ if (ptm_fd < 0) {
+ perror("posix_openpt");
+ client_socket->close();
+ return;
+ }
+ if (grantpt(ptm_fd) < 0) {
+ perror("grantpt");
+ client_socket->close();
+ return;
+ }
+ if (unlockpt(ptm_fd) < 0) {
+ perror("unlockpt");
+ client_socket->close();
+ return;
+ }
+
+ run_command(ptm_fd, command);
+
+ auto client = Client::create(id, move(client_socket), ptm_fd);
+ client->on_exit = [&clients, id] { clients.remove(id); };
+ clients.set(id, client);
+ };
+
+ int rc = event_loop.exec();
+ if (rc != 0) {
+ fprintf(stderr, "event loop exited badly; rc=%d", rc);
+ exit(1);
+ }
+
+ return 0;
+}
diff --git a/Services/WebServer/Client.cpp b/Services/WebServer/Client.cpp
new file mode 100644
index 0000000000..ae9aedc907
--- /dev/null
+++ b/Services/WebServer/Client.cpp
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Client.h"
+#include <AK/FileSystemPath.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/DateTime.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <LibHTTP/HttpRequest.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+namespace WebServer {
+
+Client::Client(NonnullRefPtr<Core::TCPSocket> socket, Core::Object* parent)
+ : Core::Object(parent)
+ , m_socket(socket)
+{
+}
+
+void Client::die()
+{
+ remove_from_parent();
+}
+
+void Client::start()
+{
+ m_socket->on_ready_to_read = [this] {
+ auto raw_request = m_socket->read_all();
+ if (raw_request.is_null()) {
+ die();
+ return;
+ }
+
+ dbg() << "Got raw request: '" << String::copy(raw_request) << "'";
+
+ handle_request(move(raw_request));
+ die();
+ };
+}
+
+void Client::handle_request(ByteBuffer raw_request)
+{
+ auto request_or_error = HTTP::HttpRequest::from_raw_request(raw_request);
+ if (!request_or_error.has_value())
+ return;
+ auto& request = request_or_error.value();
+
+ dbg() << "Got HTTP request: " << request.method_name() << " " << request.resource();
+ for (auto& header : request.headers()) {
+ dbg() << " " << header.name << " => " << header.value;
+ }
+
+ if (request.method() != HTTP::HttpRequest::Method::GET) {
+ send_error_response(403, "Forbidden, bro!", request);
+ return;
+ }
+
+ auto requested_path = canonicalized_path(request.resource());
+ dbg() << "Canonical requested path: '" << requested_path << "'";
+
+ StringBuilder path_builder;
+ path_builder.append("/www/");
+ path_builder.append(requested_path);
+ auto real_path = path_builder.to_string();
+
+ if (Core::File::is_directory(real_path)) {
+
+ if (!request.resource().ends_with("/")) {
+ StringBuilder red;
+
+ red.append(requested_path);
+ red.append("/");
+
+ send_redirect(red.to_string(), request);
+ return;
+ }
+
+ StringBuilder index_html_path_builder;
+ index_html_path_builder.append(real_path);
+ index_html_path_builder.append("/index.html");
+ auto index_html_path = index_html_path_builder.to_string();
+ if (!Core::File::exists(index_html_path)) {
+ handle_directory_listing(requested_path, real_path, request);
+ return;
+ }
+ real_path = index_html_path;
+ }
+
+ auto file = Core::File::construct(real_path);
+ if (!file->open(Core::File::ReadOnly)) {
+ send_error_response(404, "Not found, bro!", request);
+ return;
+ }
+
+ send_response(file->read_all(), request);
+}
+
+void Client::send_response(StringView response, const HTTP::HttpRequest& request)
+{
+ StringBuilder builder;
+ builder.append("HTTP/1.0 200 OK\r\n");
+ builder.append("Server: WebServer (SerenityOS)\r\n");
+ builder.append("Content-Type: text/html\r\n");
+ builder.append("\r\n");
+
+ m_socket->write(builder.to_string());
+ m_socket->write(response);
+
+ log_response(200, request);
+}
+
+void Client::send_redirect(StringView redirect_path, const HTTP::HttpRequest& request)
+{
+ StringBuilder builder;
+ builder.append("HTTP/1.0 301 Moved Permanently\r\n");
+ builder.append("Location: ");
+ builder.append(redirect_path);
+ builder.append("\r\n");
+ builder.append("\r\n");
+
+ m_socket->write(builder.to_string());
+
+ log_response(301, request);
+}
+
+void Client::handle_directory_listing(const String& requested_path, const String& real_path, const HTTP::HttpRequest& request)
+{
+ StringBuilder builder;
+
+ builder.append("<!DOCTYPE html>\n");
+ builder.append("<html>\n");
+ builder.append("<head><title>Index of ");
+ builder.append(escape_html_entities(requested_path));
+ builder.append("</title></head>\n");
+ builder.append("<body>\n");
+ builder.append("<h1>Index of ");
+ builder.append(escape_html_entities(requested_path));
+ builder.append("</h1>\n");
+ builder.append("<hr>\n");
+ builder.append("<pre>\n");
+
+ Core::DirIterator dt(real_path);
+ while (dt.has_next()) {
+ auto name = dt.next_path();
+ builder.append("<a href=\"");
+ // FIXME: urlencode
+ builder.append(name);
+ builder.append("\">");
+ builder.append(escape_html_entities(name));
+ builder.append("</a>");
+ for (size_t i = 0; i < (40 - name.length()); ++i)
+ builder.append(' ');
+
+ StringBuilder path_builder;
+ path_builder.append(real_path);
+ path_builder.append('/');
+ path_builder.append(name);
+ struct stat st;
+ memset(&st, 0, sizeof(st));
+ int rc = stat(path_builder.to_string().characters(), &st);
+ if (rc < 0) {
+ perror("stat");
+ }
+ builder.appendf(" %10d", st.st_size);
+ builder.appendf(" ");
+ builder.append(Core::DateTime::from_timestamp(st.st_mtime).to_string());
+ builder.append("\n");
+ }
+
+ builder.append("</pre>\n");
+ builder.append("<hr>\n");
+ builder.append("<i>Generated by WebServer (SerenityOS)</i>\n");
+ builder.append("</body>\n");
+ builder.append("</html>\n");
+
+ send_response(builder.to_string(), request);
+}
+
+void Client::send_error_response(unsigned code, const StringView& message, const HTTP::HttpRequest& request)
+{
+ StringBuilder builder;
+ builder.appendf("HTTP/1.0 %u ", code);
+ builder.append(message);
+ builder.append("\r\n\r\n");
+ builder.append("<!DOCTYPE html><html><body><h1>");
+ builder.appendf("%u ", code);
+ builder.append(message);
+ builder.append("</h1></body></html>");
+ m_socket->write(builder.to_string());
+
+ log_response(code, request);
+}
+
+void Client::log_response(unsigned code, const HTTP::HttpRequest& request)
+{
+ printf("%s :: %03u :: %s %s\n",
+ Core::DateTime::now().to_string().characters(),
+ code,
+ request.method_name().characters(),
+ request.resource().characters());
+}
+
+}
diff --git a/Services/WebServer/Client.h b/Services/WebServer/Client.h
new file mode 100644
index 0000000000..35cdb4ab1c
--- /dev/null
+++ b/Services/WebServer/Client.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/Object.h>
+#include <LibCore/TCPSocket.h>
+#include <LibHTTP/Forward.h>
+
+namespace WebServer {
+
+class Client final : public Core::Object {
+ C_OBJECT(Client);
+public:
+ void start();
+
+private:
+ Client(NonnullRefPtr<Core::TCPSocket>, Core::Object* parent);
+
+ void handle_request(ByteBuffer);
+ void send_response(StringView, const HTTP::HttpRequest&);
+ void send_redirect(StringView redirect, const HTTP::HttpRequest& request);
+ void send_error_response(unsigned code, const StringView& message, const HTTP::HttpRequest&);
+ void die();
+ void log_response(unsigned code, const HTTP::HttpRequest&);
+ void handle_directory_listing(const String& requested_path, const String& real_path, const HTTP::HttpRequest&);
+
+ NonnullRefPtr<Core::TCPSocket> m_socket;
+};
+
+}
diff --git a/Services/WebServer/Makefile b/Services/WebServer/Makefile
new file mode 100644
index 0000000000..ddc99a85ae
--- /dev/null
+++ b/Services/WebServer/Makefile
@@ -0,0 +1,9 @@
+OBJS = \
+ Client.o \
+ main.o
+
+PROGRAM = WebServer
+
+LIB_DEPS = HTTP Core
+
+include ../../Makefile.common
diff --git a/Services/WebServer/main.cpp b/Services/WebServer/main.cpp
new file mode 100644
index 0000000000..7e218b8bb8
--- /dev/null
+++ b/Services/WebServer/main.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Client.h"
+#include <LibCore/ArgsParser.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/TCPServer.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int main(int argc, char** argv)
+{
+ u16 default_port = 8000;
+
+ int port = default_port;
+
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(port, "Port to listen on", "port", Core::ArgsParser::Required::No);
+ args_parser.parse(argc, argv);
+
+ if ((u16)port != port) {
+ printf("Warning: invalid port number: %d\n", port);
+ port = default_port;
+ }
+
+ if (pledge("stdio accept rpath inet unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ Core::EventLoop loop;
+
+ auto server = Core::TCPServer::construct();
+
+ server->on_ready_to_accept = [&] {
+ auto client_socket = server->accept();
+ ASSERT(client_socket);
+ auto client = WebServer::Client::construct(client_socket.release_nonnull(), server);
+ client->start();
+ };
+
+ server->listen({}, port);
+ printf("Listening on 0.0.0.0:%d\n", port);
+
+ if (unveil("/www", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ if (pledge("stdio accept rpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ return loop.exec();
+}
diff --git a/Services/WindowServer/AppletManager.cpp b/Services/WindowServer/AppletManager.cpp
new file mode 100644
index 0000000000..855ff3bcd3
--- /dev/null
+++ b/Services/WindowServer/AppletManager.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2020-2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AppletManager.h"
+#include <AK/QuickSort.h>
+#include <LibGfx/Painter.h>
+#include <WindowServer/MenuManager.h>
+
+namespace WindowServer {
+
+static AppletManager* s_the;
+Vector<String> order_vector;
+
+AppletManager::AppletManager()
+{
+ s_the = this;
+
+ auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini");
+ auto order = wm_config->read_entry("Applet", "Order");
+ order_vector = order.split(',');
+}
+
+AppletManager::~AppletManager()
+{
+}
+
+AppletManager& AppletManager::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+void AppletManager::event(Core::Event& event)
+{
+ auto& mouse_event = static_cast<MouseEvent&>(event);
+
+ for (auto& applet : m_applets) {
+ if (!applet)
+ continue;
+ if (!applet->rect_in_menubar().contains(mouse_event.position()))
+ continue;
+ auto local_event = mouse_event.translated(-applet->rect_in_menubar().location());
+ applet->event(local_event);
+ }
+}
+
+void AppletManager::add_applet(Window& applet)
+{
+ m_applets.append(applet.make_weak_ptr());
+
+ // Prune any dead weak pointers from the applet list.
+ m_applets.remove_all_matching([](auto& entry) {
+ return entry.is_null();
+ });
+
+ quick_sort(m_applets, [](auto& a, auto& b) {
+ auto index_a = order_vector.find_first_index(a->title());
+ auto index_b = order_vector.find_first_index(b->title());
+ return index_a.value_or(0) > index_b.value_or(0);
+ });
+
+ calculate_applet_rects(MenuManager::the().window());
+}
+
+void AppletManager::calculate_applet_rects(Window& window)
+{
+ auto menubar_rect = window.rect();
+ int right_edge_x = menubar_rect.width() - 4;
+ for (auto& existing_applet : m_applets) {
+
+ Gfx::Rect new_applet_rect(right_edge_x - existing_applet->size().width(), 0, existing_applet->size().width(), existing_applet->size().height());
+ Gfx::Rect dummy_menubar_rect(0, 0, 0, 18);
+ new_applet_rect.center_vertically_within(dummy_menubar_rect);
+
+ existing_applet->set_rect_in_menubar(new_applet_rect);
+ right_edge_x = existing_applet->rect_in_menubar().x() - 4;
+ }
+}
+
+void AppletManager::remove_applet(Window& applet)
+{
+ m_applets.remove_first_matching([&](auto& entry) {
+ return &applet == entry.ptr();
+ });
+}
+
+void AppletManager::draw()
+{
+ for (auto& applet : m_applets) {
+ if (!applet)
+ continue;
+ draw_applet(*applet);
+ }
+}
+
+void AppletManager::draw_applet(const Window& applet)
+{
+ if (!applet.backing_store())
+ return;
+
+ Gfx::Painter painter(*MenuManager::the().window().backing_store());
+ painter.fill_rect(applet.rect_in_menubar(), WindowManager::the().palette().window());
+ painter.blit(applet.rect_in_menubar().location(), *applet.backing_store(), applet.backing_store()->rect());
+}
+
+void AppletManager::invalidate_applet(const Window& applet, const Gfx::Rect& rect)
+{
+ draw_applet(applet);
+ MenuManager::the().window().invalidate(rect.translated(applet.rect_in_menubar().location()));
+}
+
+}
diff --git a/Services/WindowServer/AppletManager.h b/Services/WindowServer/AppletManager.h
new file mode 100644
index 0000000000..4619fd0030
--- /dev/null
+++ b/Services/WindowServer/AppletManager.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2020-2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <WindowServer/Window.h>
+#include <WindowServer/WindowManager.h>
+
+namespace WindowServer {
+
+class AppletManager : public Core::Object {
+ C_OBJECT(AppletManager)
+public:
+ AppletManager();
+ ~AppletManager();
+
+ static AppletManager& the();
+
+ virtual void event(Core::Event&) override;
+
+ void add_applet(Window& applet);
+ void remove_applet(Window& applet);
+ void draw();
+ void invalidate_applet(const Window& applet, const Gfx::Rect& rect);
+ void calculate_applet_rects(Window& window);
+
+private:
+ void draw_applet(const Window& applet);
+
+ Vector<WeakPtr<Window>> m_applets;
+};
+
+}
diff --git a/Services/WindowServer/Button.cpp b/Services/WindowServer/Button.cpp
new file mode 100644
index 0000000000..95037d4bc8
--- /dev/null
+++ b/Services/WindowServer/Button.cpp
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGfx/CharacterBitmap.h>
+#include <LibGfx/Painter.h>
+#include <LibGfx/StylePainter.h>
+#include <WindowServer/Button.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/WindowManager.h>
+
+namespace WindowServer {
+
+Button::Button(WindowFrame& frame, NonnullRefPtr<Gfx::CharacterBitmap>&& bitmap, Function<void(Button&)>&& on_click_handler)
+ : on_click(move(on_click_handler))
+ , m_frame(frame)
+ , m_bitmap(move(bitmap))
+{
+}
+
+Button::~Button()
+{
+}
+
+void Button::paint(Gfx::Painter& painter)
+{
+ auto palette = WindowManager::the().palette();
+ Gfx::PainterStateSaver saver(painter);
+ painter.translate(relative_rect().location());
+ Gfx::StylePainter::paint_button(painter, rect(), palette, Gfx::ButtonStyle::Normal, m_pressed, m_hovered);
+ auto x_location = rect().center();
+ x_location.move_by(-(m_bitmap->width() / 2), -(m_bitmap->height() / 2));
+ if (m_pressed)
+ x_location.move_by(1, 1);
+ painter.draw_bitmap(x_location, *m_bitmap, palette.button_text());
+}
+
+void Button::on_mouse_event(const MouseEvent& event)
+{
+ auto& wm = WindowManager::the();
+
+ if (event.type() == Event::MouseDown && event.button() == MouseButton::Left) {
+ m_pressed = true;
+ wm.set_cursor_tracking_button(this);
+ wm.invalidate(screen_rect());
+ return;
+ }
+
+ if (event.type() == Event::MouseUp && event.button() == MouseButton::Left) {
+ if (wm.cursor_tracking_button() != this)
+ return;
+ wm.set_cursor_tracking_button(nullptr);
+ bool old_pressed = m_pressed;
+ m_pressed = false;
+ if (rect().contains(event.position())) {
+ if (on_click)
+ on_click(*this);
+ }
+ if (old_pressed != m_pressed) {
+ // Would like to compute:
+ // m_hovered = rect_after_action().contains(event.position());
+ // However, we don't know that rect yet. We can make an educated
+ // guess which also looks okay even when wrong:
+ m_hovered = false;
+ wm.invalidate(screen_rect());
+ }
+ return;
+ }
+
+ if (event.type() == Event::MouseMove) {
+ bool old_hovered = m_hovered;
+ m_hovered = rect().contains(event.position());
+ wm.set_hovered_button(m_hovered ? this : nullptr);
+ if (old_hovered != m_hovered)
+ wm.invalidate(screen_rect());
+ }
+
+ if (event.type() == Event::MouseMove && event.buttons() & (unsigned)MouseButton::Left) {
+ if (wm.cursor_tracking_button() != this)
+ return;
+ bool old_pressed = m_pressed;
+ m_pressed = m_hovered;
+ if (old_pressed != m_pressed)
+ wm.invalidate(screen_rect());
+ }
+}
+
+Gfx::Rect Button::screen_rect() const
+{
+ return m_relative_rect.translated(m_frame.rect().location());
+}
+
+}
diff --git a/Services/WindowServer/Button.h b/Services/WindowServer/Button.h
new file mode 100644
index 0000000000..555a5d6f9e
--- /dev/null
+++ b/Services/WindowServer/Button.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/Weakable.h>
+#include <LibGfx/Rect.h>
+#include <LibGfx/Forward.h>
+
+namespace WindowServer {
+
+class MouseEvent;
+class WindowFrame;
+
+class Button : public Weakable<Button> {
+public:
+ Button(WindowFrame&, NonnullRefPtr<Gfx::CharacterBitmap>&&, Function<void(Button&)>&& on_click_handler);
+ ~Button();
+
+ Gfx::Rect relative_rect() const { return m_relative_rect; }
+ void set_relative_rect(const Gfx::Rect& rect) { m_relative_rect = rect; }
+
+ Gfx::Rect rect() const { return { {}, m_relative_rect.size() }; }
+ Gfx::Rect screen_rect() const;
+
+ void paint(Gfx::Painter&);
+
+ void on_mouse_event(const MouseEvent&);
+
+ Function<void(Button&)> on_click;
+
+ bool is_visible() const { return m_visible; }
+
+ void set_bitmap(const Gfx::CharacterBitmap& bitmap) { m_bitmap = bitmap; }
+
+private:
+ WindowFrame& m_frame;
+ Gfx::Rect m_relative_rect;
+ NonnullRefPtr<Gfx::CharacterBitmap> m_bitmap;
+ bool m_pressed { false };
+ bool m_visible { true };
+ bool m_hovered { false };
+};
+
+}
diff --git a/Services/WindowServer/ClientConnection.cpp b/Services/WindowServer/ClientConnection.cpp
new file mode 100644
index 0000000000..7f128689fb
--- /dev/null
+++ b/Services/WindowServer/ClientConnection.cpp
@@ -0,0 +1,833 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Badge.h>
+#include <AK/SharedBuffer.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/SystemTheme.h>
+#include <WindowServer/AppletManager.h>
+#include <WindowServer/ClientConnection.h>
+#include <WindowServer/Clipboard.h>
+#include <WindowServer/Compositor.h>
+#include <WindowServer/EventLoop.h>
+#include <WindowServer/Menu.h>
+#include <WindowServer/MenuBar.h>
+#include <WindowServer/MenuItem.h>
+#include <WindowServer/Screen.h>
+#include <WindowServer/Window.h>
+#include <WindowServer/WindowClientEndpoint.h>
+#include <WindowServer/WindowManager.h>
+#include <WindowServer/WindowSwitcher.h>
+#include <errno.h>
+#include <serenity.h>
+#include <stdio.h>
+#include <unistd.h>
+
+namespace WindowServer {
+
+HashMap<int, NonnullRefPtr<ClientConnection>>* s_connections;
+
+static Gfx::Rect normalize_window_rect(Gfx::Rect rect, WindowType window_type)
+{
+ auto min_size = 1;
+ if (window_type == WindowType::Normal)
+ min_size = 50;
+ Gfx::Rect normalized_rect = { rect.x(), rect.y(), max(rect.width(), min_size), max(rect.height(), min_size) };
+ return normalized_rect;
+}
+
+void ClientConnection::for_each_client(Function<void(ClientConnection&)> callback)
+{
+ if (!s_connections)
+ return;
+ for (auto& it : *s_connections) {
+ callback(*it.value);
+ }
+}
+
+ClientConnection* ClientConnection::from_client_id(int client_id)
+{
+ if (!s_connections)
+ return nullptr;
+ auto it = s_connections->find(client_id);
+ if (it == s_connections->end())
+ return nullptr;
+ return (*it).value.ptr();
+}
+
+ClientConnection::ClientConnection(Core::LocalSocket& client_socket, int client_id)
+ : IPC::ClientConnection<WindowServerEndpoint>(*this, client_socket, client_id)
+{
+ if (!s_connections)
+ s_connections = new HashMap<int, NonnullRefPtr<ClientConnection>>;
+ s_connections->set(client_id, *this);
+}
+
+ClientConnection::~ClientConnection()
+{
+ if (m_has_display_link)
+ Compositor::the().decrement_display_link_count({});
+
+ MenuManager::the().close_all_menus_from_client({}, *this);
+ auto windows = move(m_windows);
+ for (auto& window : windows) {
+ window.value->detach_client({});
+ if (window.value->type() == WindowType::MenuApplet)
+ AppletManager::the().remove_applet(window.value);
+ }
+}
+
+void ClientConnection::die()
+{
+ deferred_invoke([this](auto&) {
+ s_connections->remove(client_id());
+ });
+}
+
+void ClientConnection::notify_about_new_screen_rect(const Gfx::Rect& rect)
+{
+ post_message(Messages::WindowClient::ScreenRectChanged(rect));
+}
+
+void ClientConnection::notify_about_clipboard_contents_changed()
+{
+ post_message(Messages::WindowClient::ClipboardContentsChanged(Clipboard::the().data_type()));
+}
+
+OwnPtr<Messages::WindowServer::CreateMenubarResponse> ClientConnection::handle(const Messages::WindowServer::CreateMenubar&)
+{
+ int menubar_id = m_next_menubar_id++;
+ auto menubar = make<MenuBar>(*this, menubar_id);
+ m_menubars.set(menubar_id, move(menubar));
+ return make<Messages::WindowServer::CreateMenubarResponse>(menubar_id);
+}
+
+OwnPtr<Messages::WindowServer::DestroyMenubarResponse> ClientConnection::handle(const Messages::WindowServer::DestroyMenubar& message)
+{
+ int menubar_id = message.menubar_id();
+ auto it = m_menubars.find(menubar_id);
+ if (it == m_menubars.end()) {
+ did_misbehave("DestroyMenubar: Bad menubar ID");
+ return nullptr;
+ }
+ auto& menubar = *(*it).value;
+ MenuManager::the().close_menubar(menubar);
+ m_menubars.remove(it);
+ return make<Messages::WindowServer::DestroyMenubarResponse>();
+}
+
+OwnPtr<Messages::WindowServer::CreateMenuResponse> ClientConnection::handle(const Messages::WindowServer::CreateMenu& message)
+{
+ int menu_id = m_next_menu_id++;
+ auto menu = Menu::construct(this, menu_id, message.menu_title());
+ m_menus.set(menu_id, move(menu));
+ return make<Messages::WindowServer::CreateMenuResponse>(menu_id);
+}
+
+OwnPtr<Messages::WindowServer::DestroyMenuResponse> ClientConnection::handle(const Messages::WindowServer::DestroyMenu& message)
+{
+ int menu_id = message.menu_id();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ did_misbehave("DestroyMenu: Bad menu ID");
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ menu.close();
+ m_menus.remove(it);
+ remove_child(menu);
+ return make<Messages::WindowServer::DestroyMenuResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> ClientConnection::handle(const Messages::WindowServer::SetApplicationMenubar& message)
+{
+ int menubar_id = message.menubar_id();
+ auto it = m_menubars.find(menubar_id);
+ if (it == m_menubars.end()) {
+ did_misbehave("SetApplicationMenubar: Bad menubar ID");
+ return nullptr;
+ }
+ auto& menubar = *(*it).value;
+ m_app_menubar = menubar.make_weak_ptr();
+ WindowManager::the().notify_client_changed_app_menubar(*this);
+ return make<Messages::WindowServer::SetApplicationMenubarResponse>();
+}
+
+OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuToMenubar& message)
+{
+ int menubar_id = message.menubar_id();
+ int menu_id = message.menu_id();
+ auto it = m_menubars.find(menubar_id);
+ auto jt = m_menus.find(menu_id);
+ if (it == m_menubars.end()) {
+ did_misbehave("AddMenuToMenubar: Bad menubar ID");
+ return nullptr;
+ }
+ if (jt == m_menus.end()) {
+ did_misbehave("AddMenuToMenubar: Bad menu ID");
+ return nullptr;
+ }
+ auto& menubar = *(*it).value;
+ auto& menu = *(*jt).value;
+ menubar.add_menu(menu);
+ return make<Messages::WindowServer::AddMenuToMenubarResponse>();
+}
+
+OwnPtr<Messages::WindowServer::AddMenuItemResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuItem& message)
+{
+ int menu_id = message.menu_id();
+ unsigned identifier = message.identifier();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ dbg() << "AddMenuItem: Bad menu ID: " << menu_id;
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ auto menu_item = make<MenuItem>(menu, identifier, message.text(), message.shortcut(), message.enabled(), message.checkable(), message.checked());
+ if (message.icon_buffer_id() != -1) {
+ auto icon_buffer = SharedBuffer::create_from_shbuf_id(message.icon_buffer_id());
+ if (!icon_buffer)
+ return nullptr;
+ // FIXME: Verify that the icon buffer can accomodate a 16x16 bitmap view.
+ auto shared_icon = Gfx::Bitmap::create_with_shared_buffer(Gfx::BitmapFormat::RGBA32, icon_buffer.release_nonnull(), { 16, 16 });
+ menu_item->set_icon(shared_icon);
+ }
+ menu_item->set_submenu_id(message.submenu_id());
+ menu_item->set_exclusive(message.exclusive());
+ menu.add_item(move(menu_item));
+ return make<Messages::WindowServer::AddMenuItemResponse>();
+}
+
+OwnPtr<Messages::WindowServer::PopupMenuResponse> ClientConnection::handle(const Messages::WindowServer::PopupMenu& message)
+{
+ int menu_id = message.menu_id();
+ auto position = message.screen_position();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ did_misbehave("PopupMenu: Bad menu ID");
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ menu.popup(position);
+ return make<Messages::WindowServer::PopupMenuResponse>();
+}
+
+OwnPtr<Messages::WindowServer::DismissMenuResponse> ClientConnection::handle(const Messages::WindowServer::DismissMenu& message)
+{
+ int menu_id = message.menu_id();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ did_misbehave("DismissMenu: Bad menu ID");
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ menu.close();
+ return make<Messages::WindowServer::DismissMenuResponse>();
+}
+
+OwnPtr<Messages::WindowServer::UpdateMenuItemResponse> ClientConnection::handle(const Messages::WindowServer::UpdateMenuItem& message)
+{
+ int menu_id = message.menu_id();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ did_misbehave("UpdateMenuItem: Bad menu ID");
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ auto* menu_item = menu.item_with_identifier(message.identifier());
+ if (!menu_item) {
+ did_misbehave("UpdateMenuItem: Bad menu item identifier");
+ return nullptr;
+ }
+ menu_item->set_text(message.text());
+ menu_item->set_shortcut_text(message.shortcut());
+ menu_item->set_enabled(message.enabled());
+ menu_item->set_checkable(message.checkable());
+ if (message.checkable())
+ menu_item->set_checked(message.checked());
+ return make<Messages::WindowServer::UpdateMenuItemResponse>();
+}
+
+OwnPtr<Messages::WindowServer::AddMenuSeparatorResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuSeparator& message)
+{
+ int menu_id = message.menu_id();
+ auto it = m_menus.find(menu_id);
+ if (it == m_menus.end()) {
+ did_misbehave("AddMenuSeparator: Bad menu ID");
+ return nullptr;
+ }
+ auto& menu = *(*it).value;
+ menu.add_item(make<MenuItem>(menu, MenuItem::Separator));
+ return make<Messages::WindowServer::AddMenuSeparatorResponse>();
+}
+
+OwnPtr<Messages::WindowServer::MoveWindowToFrontResponse> ClientConnection::handle(const Messages::WindowServer::MoveWindowToFront& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("MoveWindowToFront: Bad window ID");
+ return nullptr;
+ }
+ WindowManager::the().move_to_front_and_make_active(*(*it).value);
+ return make<Messages::WindowServer::MoveWindowToFrontResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetFullscreenResponse> ClientConnection::handle(const Messages::WindowServer::SetFullscreen& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetFullscreen: Bad window ID");
+ return nullptr;
+ }
+ it->value->set_fullscreen(message.fullscreen());
+ return make<Messages::WindowServer::SetFullscreenResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetWindowOpacityResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowOpacity& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowOpacity: Bad window ID");
+ return nullptr;
+ }
+ it->value->set_opacity(message.opacity());
+ return make<Messages::WindowServer::SetWindowOpacityResponse>();
+}
+
+void ClientConnection::handle(const Messages::WindowServer::AsyncSetWallpaper& message)
+{
+ Compositor::the().set_wallpaper(message.path(), [&](bool success) {
+ post_message(Messages::WindowClient::AsyncSetWallpaperFinished(success));
+ });
+}
+
+OwnPtr<Messages::WindowServer::SetBackgroundColorResponse> ClientConnection::handle(const Messages::WindowServer::SetBackgroundColor& message)
+{
+ Compositor::the().set_background_color(message.background_color());
+ return make<Messages::WindowServer::SetBackgroundColorResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetWallpaperModeResponse> ClientConnection::handle(const Messages::WindowServer::SetWallpaperMode& message)
+{
+ Compositor::the().set_wallpaper_mode(message.mode());
+ return make<Messages::WindowServer::SetWallpaperModeResponse>();
+}
+
+OwnPtr<Messages::WindowServer::GetWallpaperResponse> ClientConnection::handle(const Messages::WindowServer::GetWallpaper&)
+{
+ return make<Messages::WindowServer::GetWallpaperResponse>(Compositor::the().wallpaper_path());
+}
+
+OwnPtr<Messages::WindowServer::SetResolutionResponse> ClientConnection::handle(const Messages::WindowServer::SetResolution& message)
+{
+ return make<Messages::WindowServer::SetResolutionResponse>(WindowManager::the().set_resolution(message.resolution().width(), message.resolution().height()), WindowManager::the().resolution());
+}
+
+OwnPtr<Messages::WindowServer::SetWindowTitleResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowTitle& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowTitle: Bad window ID");
+ return nullptr;
+ }
+ it->value->set_title(message.title());
+ return make<Messages::WindowServer::SetWindowTitleResponse>();
+}
+
+OwnPtr<Messages::WindowServer::GetWindowTitleResponse> ClientConnection::handle(const Messages::WindowServer::GetWindowTitle& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("GetWindowTitle: Bad window ID");
+ return nullptr;
+ }
+ return make<Messages::WindowServer::GetWindowTitleResponse>(it->value->title());
+}
+
+OwnPtr<Messages::WindowServer::SetWindowIconBitmapResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowIconBitmap& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowIconBitmap: Bad window ID");
+ return nullptr;
+ }
+ auto& window = *(*it).value;
+
+ if (message.icon().is_valid()) {
+ window.set_icon(*message.icon().bitmap());
+ } else {
+ window.set_default_icon();
+ }
+
+ window.frame().invalidate_title_bar();
+ WindowManager::the().tell_wm_listeners_window_icon_changed(window);
+ return make<Messages::WindowServer::SetWindowIconBitmapResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetWindowRectResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowRect& message)
+{
+ int window_id = message.window_id();
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowRect: Bad window ID");
+ return nullptr;
+ }
+ auto& window = *(*it).value;
+ if (window.is_fullscreen()) {
+ dbg() << "ClientConnection: Ignoring SetWindowRect request for fullscreen window";
+ return nullptr;
+ }
+ auto normalized_rect = normalize_window_rect(message.rect(), window.type());
+ window.set_rect(normalized_rect);
+ window.request_update(normalized_rect);
+ return make<Messages::WindowServer::SetWindowRectResponse>(normalized_rect);
+}
+
+OwnPtr<Messages::WindowServer::GetWindowRectResponse> ClientConnection::handle(const Messages::WindowServer::GetWindowRect& message)
+{
+ int window_id = message.window_id();
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end()) {
+ did_misbehave("GetWindowRect: Bad window ID");
+ return nullptr;
+ }
+ return make<Messages::WindowServer::GetWindowRectResponse>(it->value->rect());
+}
+
+OwnPtr<Messages::WindowServer::SetClipboardContentsResponse> ClientConnection::handle(const Messages::WindowServer::SetClipboardContents& message)
+{
+ auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.shbuf_id());
+ if (!shared_buffer) {
+ did_misbehave("SetClipboardContents: Bad shared buffer ID");
+ return nullptr;
+ }
+ Clipboard::the().set_data(*shared_buffer, message.content_size(), message.content_type());
+ return make<Messages::WindowServer::SetClipboardContentsResponse>();
+}
+
+OwnPtr<Messages::WindowServer::GetClipboardContentsResponse> ClientConnection::handle(const Messages::WindowServer::GetClipboardContents&)
+{
+ auto& clipboard = Clipboard::the();
+
+ i32 shbuf_id = -1;
+ if (clipboard.size()) {
+ // FIXME: Optimize case where an app is copy/pasting within itself.
+ // We can just reuse the SharedBuffer then, since it will have the same peer PID.
+ // It would be even nicer if a SharedBuffer could have an arbitrary number of clients..
+ RefPtr<SharedBuffer> shared_buffer = SharedBuffer::create_with_size(clipboard.size());
+ ASSERT(shared_buffer);
+ memcpy(shared_buffer->data(), clipboard.data(), clipboard.size());
+ shared_buffer->seal();
+ shared_buffer->share_with(client_pid());
+ shbuf_id = shared_buffer->shbuf_id();
+
+ // FIXME: This is a workaround for the fact that SharedBuffers will go away if neither side is retaining them.
+ // After we respond to GetClipboardContents, we have to wait for the client to ref the buffer on his side.
+ m_last_sent_clipboard_content = move(shared_buffer);
+ }
+ return make<Messages::WindowServer::GetClipboardContentsResponse>(shbuf_id, clipboard.size(), clipboard.data_type());
+}
+
+Window* ClientConnection::window_from_id(i32 window_id)
+{
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end())
+ return nullptr;
+ return it->value.ptr();
+}
+
+OwnPtr<Messages::WindowServer::CreateWindowResponse> ClientConnection::handle(const Messages::WindowServer::CreateWindow& message)
+{
+ int window_id = m_next_window_id++;
+ auto window = Window::construct(*this, (WindowType)message.type(), window_id, message.modal(), message.minimizable(), message.frameless(), message.resizable(), message.fullscreen());
+
+ if (message.parent_window_id()) {
+ auto* parent_window = window_from_id(message.parent_window_id());
+ if (!parent_window) {
+ did_misbehave("CreateWindow with bad parent_window_id");
+ return nullptr;
+ }
+ if (parent_window->window_id() == window_id) {
+ did_misbehave("CreateWindow trying to make a window with itself as parent");
+ return nullptr;
+ }
+ window->set_parent_window(*parent_window);
+ }
+
+ window->set_has_alpha_channel(message.has_alpha_channel());
+ window->set_title(message.title());
+ if (!message.fullscreen()) {
+ auto normalized_rect = normalize_window_rect(message.rect(), window->type());
+ window->set_rect(normalized_rect);
+ }
+ if (window->type() == WindowType::Desktop) {
+ window->set_rect(WindowManager::the().desktop_rect());
+ window->recalculate_rect();
+ }
+ window->set_opacity(message.opacity());
+ window->set_size_increment(message.size_increment());
+ window->set_base_size(message.base_size());
+ window->invalidate();
+ if (window->type() == WindowType::MenuApplet)
+ AppletManager::the().add_applet(*window);
+ m_windows.set(window_id, move(window));
+ return make<Messages::WindowServer::CreateWindowResponse>(window_id);
+}
+
+void ClientConnection::destroy_window(Window& window, Vector<i32>& destroyed_window_ids)
+{
+ for (auto& child_window : window.child_windows()) {
+ if (!child_window)
+ continue;
+ ASSERT(child_window->window_id() != window.window_id());
+ destroy_window(*child_window, destroyed_window_ids);
+ }
+
+ destroyed_window_ids.append(window.window_id());
+
+ if (window.type() == WindowType::MenuApplet)
+ AppletManager::the().remove_applet(window);
+
+ WindowManager::the().invalidate(window);
+ remove_child(window);
+ m_windows.remove(window.window_id());
+}
+
+OwnPtr<Messages::WindowServer::DestroyWindowResponse> ClientConnection::handle(const Messages::WindowServer::DestroyWindow& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("DestroyWindow: Bad window ID");
+ return nullptr;
+ }
+ auto& window = *(*it).value;
+ Vector<i32> destroyed_window_ids;
+ destroy_window(window, destroyed_window_ids);
+ return make<Messages::WindowServer::DestroyWindowResponse>(destroyed_window_ids);
+}
+
+void ClientConnection::post_paint_message(Window& window, bool ignore_occlusion)
+{
+ auto rect_set = window.take_pending_paint_rects();
+ if (window.is_minimized() || (!ignore_occlusion && window.is_occluded()))
+ return;
+
+ post_message(Messages::WindowClient::Paint(window.window_id(), window.size(), rect_set.rects()));
+}
+
+void ClientConnection::handle(const Messages::WindowServer::InvalidateRect& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("InvalidateRect: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ for (size_t i = 0; i < message.rects().size(); ++i)
+ window.request_update(message.rects()[i].intersected({ {}, window.size() }), message.ignore_occlusion());
+}
+
+void ClientConnection::handle(const Messages::WindowServer::DidFinishPainting& message)
+{
+ int window_id = message.window_id();
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end()) {
+ did_misbehave("DidFinishPainting: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ for (auto& rect : message.rects())
+ WindowManager::the().invalidate(window, rect);
+
+ WindowSwitcher::the().refresh_if_needed();
+}
+
+OwnPtr<Messages::WindowServer::SetWindowBackingStoreResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowBackingStore& message)
+{
+ int window_id = message.window_id();
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowBackingStore: Bad window ID");
+ return nullptr;
+ }
+ auto& window = *(*it).value;
+ if (window.last_backing_store() && window.last_backing_store()->shbuf_id() == message.shbuf_id()) {
+ window.swap_backing_stores();
+ } else {
+ auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.shbuf_id());
+ if (!shared_buffer)
+ return make<Messages::WindowServer::SetWindowBackingStoreResponse>();
+ auto backing_store = Gfx::Bitmap::create_with_shared_buffer(
+ message.has_alpha_channel() ? Gfx::BitmapFormat::RGBA32 : Gfx::BitmapFormat::RGB32,
+ *shared_buffer,
+ message.size());
+ window.set_backing_store(move(backing_store));
+ }
+
+ if (message.flush_immediately())
+ window.invalidate();
+
+ return make<Messages::WindowServer::SetWindowBackingStoreResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetGlobalCursorTrackingResponse> ClientConnection::handle(const Messages::WindowServer::SetGlobalCursorTracking& message)
+{
+ int window_id = message.window_id();
+ auto it = m_windows.find(window_id);
+ if (it == m_windows.end()) {
+ did_misbehave("SetGlobalCursorTracking: Bad window ID");
+ return nullptr;
+ }
+ it->value->set_global_cursor_tracking_enabled(message.enabled());
+ return make<Messages::WindowServer::SetGlobalCursorTrackingResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetWindowOverrideCursorResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowOverrideCursor& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowOverrideCursor: Bad window ID");
+ return nullptr;
+ }
+ auto& window = *(*it).value;
+ window.set_override_cursor(Cursor::create((StandardCursor)message.cursor_type()));
+ return make<Messages::WindowServer::SetWindowOverrideCursorResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetWindowHasAlphaChannelResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowHasAlphaChannel& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowHasAlphaChannel: Bad window ID");
+ return nullptr;
+ }
+ it->value->set_has_alpha_channel(message.has_alpha_channel());
+ return make<Messages::WindowServer::SetWindowHasAlphaChannelResponse>();
+}
+
+void ClientConnection::handle(const Messages::WindowServer::WM_SetActiveWindow& message)
+{
+ auto* client = ClientConnection::from_client_id(message.client_id());
+ if (!client) {
+ did_misbehave("WM_SetActiveWindow: Bad client ID");
+ return;
+ }
+ auto it = client->m_windows.find(message.window_id());
+ if (it == client->m_windows.end()) {
+ did_misbehave("WM_SetActiveWindow: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ window.set_minimized(false);
+ WindowManager::the().move_to_front_and_make_active(window);
+}
+
+void ClientConnection::handle(const Messages::WindowServer::WM_PopupWindowMenu& message)
+{
+ auto* client = ClientConnection::from_client_id(message.client_id());
+ if (!client) {
+ did_misbehave("WM_PopupWindowMenu: Bad client ID");
+ return;
+ }
+ auto it = client->m_windows.find(message.window_id());
+ if (it == client->m_windows.end()) {
+ did_misbehave("WM_PopupWindowMenu: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ window.popup_window_menu(message.screen_position());
+}
+
+void ClientConnection::handle(const Messages::WindowServer::WM_StartWindowResize& request)
+{
+ auto* client = ClientConnection::from_client_id(request.client_id());
+ if (!client) {
+ did_misbehave("WM_StartWindowResize: Bad client ID");
+ return;
+ }
+ auto it = client->m_windows.find(request.window_id());
+ if (it == client->m_windows.end()) {
+ did_misbehave("WM_StartWindowResize: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ // FIXME: We are cheating a bit here by using the current cursor location and hard-coding the left button.
+ // Maybe the client should be allowed to specify what initiated this request?
+ WindowManager::the().start_window_resize(window, Screen::the().cursor_location(), MouseButton::Left);
+}
+
+void ClientConnection::handle(const Messages::WindowServer::WM_SetWindowMinimized& message)
+{
+ auto* client = ClientConnection::from_client_id(message.client_id());
+ if (!client) {
+ did_misbehave("WM_SetWindowMinimized: Bad client ID");
+ return;
+ }
+ auto it = client->m_windows.find(message.window_id());
+ if (it == client->m_windows.end()) {
+ did_misbehave("WM_SetWindowMinimized: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ window.set_minimized(message.minimized());
+}
+
+OwnPtr<Messages::WindowServer::GreetResponse> ClientConnection::handle(const Messages::WindowServer::Greet&)
+{
+ return make<Messages::WindowServer::GreetResponse>(client_id(), Screen::the().rect(), Gfx::current_system_theme_buffer_id());
+}
+
+bool ClientConnection::is_showing_modal_window() const
+{
+ for (auto& it : m_windows) {
+ auto& window = *it.value;
+ if (window.is_visible() && window.is_modal())
+ return true;
+ }
+ return false;
+}
+
+void ClientConnection::handle(const Messages::WindowServer::WM_SetWindowTaskbarRect& message)
+{
+ auto* client = ClientConnection::from_client_id(message.client_id());
+ if (!client) {
+ did_misbehave("WM_SetWindowTaskbarRect: Bad client ID");
+ return;
+ }
+ auto it = client->m_windows.find(message.window_id());
+ if (it == client->m_windows.end()) {
+ did_misbehave("WM_SetWindowTaskbarRect: Bad window ID");
+ return;
+ }
+ auto& window = *(*it).value;
+ window.set_taskbar_rect(message.rect());
+}
+
+OwnPtr<Messages::WindowServer::StartDragResponse> ClientConnection::handle(const Messages::WindowServer::StartDrag& message)
+{
+ auto& wm = WindowManager::the();
+ if (wm.dnd_client())
+ return make<Messages::WindowServer::StartDragResponse>(false);
+
+ RefPtr<Gfx::Bitmap> bitmap;
+ if (message.bitmap_id() != -1) {
+ auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.bitmap_id());
+ ssize_t size_in_bytes = message.bitmap_size().area() * sizeof(Gfx::RGBA32);
+ if (size_in_bytes > shared_buffer->size()) {
+ did_misbehave("SetAppletBackingStore: Shared buffer is too small for applet size");
+ return nullptr;
+ }
+ bitmap = Gfx::Bitmap::create_with_shared_buffer(Gfx::BitmapFormat::RGBA32, *shared_buffer, message.bitmap_size());
+ }
+
+ wm.start_dnd_drag(*this, message.text(), bitmap, message.data_type(), message.data());
+ return make<Messages::WindowServer::StartDragResponse>(true);
+}
+
+OwnPtr<Messages::WindowServer::SetSystemMenuResponse> ClientConnection::handle(const Messages::WindowServer::SetSystemMenu& message)
+{
+ auto it = m_menus.find(message.menu_id());
+ if (it == m_menus.end()) {
+ did_misbehave("SetSystemMenu called with invalid menu ID");
+ return nullptr;
+ }
+
+ auto& menu = it->value;
+ MenuManager::the().set_system_menu(menu);
+ return make<Messages::WindowServer::SetSystemMenuResponse>();
+}
+
+OwnPtr<Messages::WindowServer::SetSystemThemeResponse> ClientConnection::handle(const Messages::WindowServer::SetSystemTheme& message)
+{
+ bool success = WindowManager::the().update_theme(message.theme_path(), message.theme_name());
+ return make<Messages::WindowServer::SetSystemThemeResponse>(success);
+}
+
+OwnPtr<Messages::WindowServer::GetSystemThemeResponse> ClientConnection::handle(const Messages::WindowServer::GetSystemTheme&)
+{
+ auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini");
+ auto name = wm_config->read_entry("Theme", "Name");
+ return make<Messages::WindowServer::GetSystemThemeResponse>(name);
+}
+
+void ClientConnection::boost()
+{
+ // FIXME: Re-enable this when we have a solution for boosting.
+#if 0
+ if (set_process_boost(client_pid(), 10) < 0)
+ perror("boost: set_process_boost");
+#endif
+}
+
+void ClientConnection::deboost()
+{
+ // FIXME: Re-enable this when we have a solution for boosting.
+#if 0
+ if (set_process_boost(client_pid(), 0) < 0)
+ perror("deboost: set_process_boost");
+#endif
+}
+
+OwnPtr<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowBaseSizeAndSizeIncrement& message)
+{
+ auto it = m_windows.find(message.window_id());
+ if (it == m_windows.end()) {
+ did_misbehave("SetWindowBaseSizeAndSizeIncrementResponse: Bad window ID");
+ return nullptr;
+ }
+
+ auto& window = *it->value;
+ window.set_base_size(message.base_size());
+ window.set_size_increment(message.size_increment());
+
+ return make<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse>();
+}
+
+void ClientConnection::handle(const Messages::WindowServer::EnableDisplayLink&)
+{
+ if (m_has_display_link)
+ return;
+ m_has_display_link = true;
+ Compositor::the().increment_display_link_count({});
+}
+
+void ClientConnection::handle(const Messages::WindowServer::DisableDisplayLink&)
+{
+ if (!m_has_display_link)
+ return;
+ m_has_display_link = false;
+ Compositor::the().decrement_display_link_count({});
+}
+
+void ClientConnection::notify_display_link(Badge<Compositor>)
+{
+ if (!m_has_display_link)
+ return;
+
+ post_message(Messages::WindowClient::DisplayLinkNotification());
+}
+
+}
diff --git a/Services/WindowServer/ClientConnection.h b/Services/WindowServer/ClientConnection.h
new file mode 100644
index 0000000000..92c2f2fa08
--- /dev/null
+++ b/Services/WindowServer/ClientConnection.h
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Badge.h>
+#include <AK/Function.h>
+#include <AK/HashMap.h>
+#include <AK/OwnPtr.h>
+#include <AK/WeakPtr.h>
+#include <LibCore/Object.h>
+#include <LibGfx/Bitmap.h>
+#include <LibIPC/ClientConnection.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/WindowServerEndpoint.h>
+
+namespace WindowServer {
+
+class Compositor;
+class Window;
+class Menu;
+class MenuBar;
+
+class ClientConnection final
+ : public IPC::ClientConnection<WindowServerEndpoint>
+ , public WindowServerEndpoint {
+ C_OBJECT(ClientConnection)
+public:
+ ~ClientConnection() override;
+ virtual void die() override;
+
+ void boost();
+ void deboost();
+
+ static ClientConnection* from_client_id(int client_id);
+ static void for_each_client(Function<void(ClientConnection&)>);
+
+ MenuBar* app_menubar() { return m_app_menubar.ptr(); }
+
+ bool is_showing_modal_window() const;
+
+ void notify_about_new_screen_rect(const Gfx::Rect&);
+ void notify_about_clipboard_contents_changed();
+ void post_paint_message(Window&, bool ignore_occlusion = false);
+
+ Menu* find_menu_by_id(int menu_id)
+ {
+ auto menu = m_menus.get(menu_id);
+ if (!menu.has_value())
+ return nullptr;
+ return const_cast<Menu*>(menu.value().ptr());
+ }
+
+ void notify_display_link(Badge<Compositor>);
+
+private:
+ explicit ClientConnection(Core::LocalSocket&, int client_id);
+
+ void destroy_window(Window&, Vector<i32>& destroyed_window_ids);
+
+ virtual OwnPtr<Messages::WindowServer::GreetResponse> handle(const Messages::WindowServer::Greet&) override;
+ virtual OwnPtr<Messages::WindowServer::CreateMenubarResponse> handle(const Messages::WindowServer::CreateMenubar&) override;
+ virtual OwnPtr<Messages::WindowServer::DestroyMenubarResponse> handle(const Messages::WindowServer::DestroyMenubar&) override;
+ virtual OwnPtr<Messages::WindowServer::CreateMenuResponse> handle(const Messages::WindowServer::CreateMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::DestroyMenuResponse> handle(const Messages::WindowServer::DestroyMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> handle(const Messages::WindowServer::AddMenuToMenubar&) override;
+ virtual OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> handle(const Messages::WindowServer::SetApplicationMenubar&) override;
+ virtual OwnPtr<Messages::WindowServer::AddMenuItemResponse> handle(const Messages::WindowServer::AddMenuItem&) override;
+ virtual OwnPtr<Messages::WindowServer::AddMenuSeparatorResponse> handle(const Messages::WindowServer::AddMenuSeparator&) override;
+ virtual OwnPtr<Messages::WindowServer::UpdateMenuItemResponse> handle(const Messages::WindowServer::UpdateMenuItem&) override;
+ virtual OwnPtr<Messages::WindowServer::CreateWindowResponse> handle(const Messages::WindowServer::CreateWindow&) override;
+ virtual OwnPtr<Messages::WindowServer::DestroyWindowResponse> handle(const Messages::WindowServer::DestroyWindow&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowTitleResponse> handle(const Messages::WindowServer::SetWindowTitle&) override;
+ virtual OwnPtr<Messages::WindowServer::GetWindowTitleResponse> handle(const Messages::WindowServer::GetWindowTitle&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowRectResponse> handle(const Messages::WindowServer::SetWindowRect&) override;
+ virtual OwnPtr<Messages::WindowServer::GetWindowRectResponse> handle(const Messages::WindowServer::GetWindowRect&) override;
+ virtual void handle(const Messages::WindowServer::InvalidateRect&) override;
+ virtual void handle(const Messages::WindowServer::DidFinishPainting&) override;
+ virtual OwnPtr<Messages::WindowServer::SetGlobalCursorTrackingResponse> handle(const Messages::WindowServer::SetGlobalCursorTracking&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowOpacityResponse> handle(const Messages::WindowServer::SetWindowOpacity&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowBackingStoreResponse> handle(const Messages::WindowServer::SetWindowBackingStore&) override;
+ virtual OwnPtr<Messages::WindowServer::GetClipboardContentsResponse> handle(const Messages::WindowServer::GetClipboardContents&) override;
+ virtual OwnPtr<Messages::WindowServer::SetClipboardContentsResponse> handle(const Messages::WindowServer::SetClipboardContents&) override;
+ virtual void handle(const Messages::WindowServer::WM_SetActiveWindow&) override;
+ virtual void handle(const Messages::WindowServer::WM_SetWindowMinimized&) override;
+ virtual void handle(const Messages::WindowServer::WM_StartWindowResize&) override;
+ virtual void handle(const Messages::WindowServer::WM_PopupWindowMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowHasAlphaChannelResponse> handle(const Messages::WindowServer::SetWindowHasAlphaChannel&) override;
+ virtual OwnPtr<Messages::WindowServer::MoveWindowToFrontResponse> handle(const Messages::WindowServer::MoveWindowToFront&) override;
+ virtual OwnPtr<Messages::WindowServer::SetFullscreenResponse> handle(const Messages::WindowServer::SetFullscreen&) override;
+ virtual void handle(const Messages::WindowServer::AsyncSetWallpaper&) override;
+ virtual OwnPtr<Messages::WindowServer::SetBackgroundColorResponse> handle(const Messages::WindowServer::SetBackgroundColor&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWallpaperModeResponse> handle(const Messages::WindowServer::SetWallpaperMode&) override;
+ virtual OwnPtr<Messages::WindowServer::GetWallpaperResponse> handle(const Messages::WindowServer::GetWallpaper&) override;
+ virtual OwnPtr<Messages::WindowServer::SetResolutionResponse> handle(const Messages::WindowServer::SetResolution&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowOverrideCursorResponse> handle(const Messages::WindowServer::SetWindowOverrideCursor&) override;
+ virtual OwnPtr<Messages::WindowServer::PopupMenuResponse> handle(const Messages::WindowServer::PopupMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::DismissMenuResponse> handle(const Messages::WindowServer::DismissMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowIconBitmapResponse> handle(const Messages::WindowServer::SetWindowIconBitmap&) override;
+ virtual void handle(const Messages::WindowServer::WM_SetWindowTaskbarRect&) override;
+ virtual OwnPtr<Messages::WindowServer::StartDragResponse> handle(const Messages::WindowServer::StartDrag&) override;
+ virtual OwnPtr<Messages::WindowServer::SetSystemMenuResponse> handle(const Messages::WindowServer::SetSystemMenu&) override;
+ virtual OwnPtr<Messages::WindowServer::SetSystemThemeResponse> handle(const Messages::WindowServer::SetSystemTheme&) override;
+ virtual OwnPtr<Messages::WindowServer::GetSystemThemeResponse> handle(const Messages::WindowServer::GetSystemTheme&) override;
+ virtual OwnPtr<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse> handle(const Messages::WindowServer::SetWindowBaseSizeAndSizeIncrement&) override;
+ virtual void handle(const Messages::WindowServer::EnableDisplayLink&) override;
+ virtual void handle(const Messages::WindowServer::DisableDisplayLink&) override;
+
+ Window* window_from_id(i32 window_id);
+
+ HashMap<int, NonnullRefPtr<Window>> m_windows;
+ HashMap<int, NonnullOwnPtr<MenuBar>> m_menubars;
+ HashMap<int, NonnullRefPtr<Menu>> m_menus;
+ WeakPtr<MenuBar> m_app_menubar;
+
+ int m_next_menubar_id { 10000 };
+ int m_next_menu_id { 20000 };
+ int m_next_window_id { 1982 };
+
+ bool m_has_display_link { false };
+
+ RefPtr<SharedBuffer> m_last_sent_clipboard_content;
+};
+
+}
diff --git a/Services/WindowServer/Clipboard.cpp b/Services/WindowServer/Clipboard.cpp
new file mode 100644
index 0000000000..4d9db8b4bc
--- /dev/null
+++ b/Services/WindowServer/Clipboard.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <WindowServer/Clipboard.h>
+
+namespace WindowServer {
+
+Clipboard& Clipboard::the()
+{
+ static Clipboard* s_the;
+ if (!s_the)
+ s_the = new Clipboard;
+ return *s_the;
+}
+
+Clipboard::Clipboard()
+{
+}
+
+Clipboard::~Clipboard()
+{
+}
+
+const u8* Clipboard::data() const
+{
+ if (!m_shared_buffer)
+ return nullptr;
+ return (const u8*)m_shared_buffer->data();
+}
+
+int Clipboard::size() const
+{
+ if (!m_shared_buffer)
+ return 0;
+ return m_contents_size;
+}
+
+void Clipboard::clear()
+{
+ m_shared_buffer = nullptr;
+ m_contents_size = 0;
+}
+
+void Clipboard::set_data(NonnullRefPtr<SharedBuffer>&& data, int contents_size, const String& data_type)
+{
+ dbg() << "Clipboard::set_data <- [" << data_type << "] " << data->data() << " (" << contents_size << " bytes)";
+ m_shared_buffer = move(data);
+ m_contents_size = contents_size;
+ m_data_type = data_type;
+
+ if (on_content_change)
+ on_content_change();
+}
+
+}
diff --git a/Services/WindowServer/Clipboard.h b/Services/WindowServer/Clipboard.h
new file mode 100644
index 0000000000..6cb9140234
--- /dev/null
+++ b/Services/WindowServer/Clipboard.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/SharedBuffer.h>
+#include <AK/String.h>
+
+namespace WindowServer {
+
+class Clipboard {
+public:
+ static Clipboard& the();
+ ~Clipboard();
+
+ bool has_data() const
+ {
+ return m_shared_buffer;
+ }
+
+ const String& data_type() const { return m_data_type; }
+ const u8* data() const;
+ int size() const;
+
+ void clear();
+ void set_data(NonnullRefPtr<SharedBuffer>&&, int contents_size, const String& data_type);
+
+ Function<void()> on_content_change;
+
+private:
+ Clipboard();
+
+ String m_data_type;
+ RefPtr<SharedBuffer> m_shared_buffer;
+ int m_contents_size { 0 };
+};
+
+}
diff --git a/Services/WindowServer/Compositor.cpp b/Services/WindowServer/Compositor.cpp
new file mode 100644
index 0000000000..57f87178df
--- /dev/null
+++ b/Services/WindowServer/Compositor.cpp
@@ -0,0 +1,507 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Compositor.h"
+#include "ClientConnection.h"
+#include "Event.h"
+#include "EventLoop.h"
+#include "Screen.h"
+#include "Window.h"
+#include "WindowManager.h"
+#include <AK/Memory.h>
+#include <LibCore/Timer.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Painter.h>
+#include <LibThread/BackgroundAction.h>
+
+namespace WindowServer {
+
+Compositor& Compositor::the()
+{
+ static Compositor s_the;
+ return s_the;
+}
+
+WallpaperMode mode_to_enum(const String& name)
+{
+ if (name == "simple")
+ return WallpaperMode::Simple;
+ if (name == "tile")
+ return WallpaperMode::Tile;
+ if (name == "center")
+ return WallpaperMode::Center;
+ if (name == "scaled")
+ return WallpaperMode::Scaled;
+ return WallpaperMode::Simple;
+}
+
+Compositor::Compositor()
+{
+ m_display_link_notify_timer = add<Core::Timer>(
+ 1000 / 60, [this] {
+ notify_display_links();
+ });
+ m_display_link_notify_timer->stop();
+
+ m_compose_timer = Core::Timer::create_single_shot(
+ 1000 / 60,
+ [this] {
+ compose();
+ },
+ this);
+
+ m_immediate_compose_timer = Core::Timer::create_single_shot(
+ 0,
+ [this] {
+ compose();
+ },
+ this);
+
+ m_screen_can_set_buffer = Screen::the().can_set_buffer();
+ init_bitmaps();
+}
+
+void Compositor::init_bitmaps()
+{
+ auto& screen = Screen::the();
+ auto size = screen.size();
+
+ m_front_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGB32, size, screen.pitch(), screen.scanline(0));
+
+ if (m_screen_can_set_buffer)
+ m_back_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGB32, size, screen.pitch(), screen.scanline(size.height()));
+ else
+ m_back_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, size);
+
+ m_front_painter = make<Gfx::Painter>(*m_front_bitmap);
+ m_back_painter = make<Gfx::Painter>(*m_back_bitmap);
+
+ m_buffers_are_flipped = false;
+
+ invalidate();
+}
+
+void Compositor::compose()
+{
+ auto& wm = WindowManager::the();
+ if (m_wallpaper_mode == WallpaperMode::Unchecked)
+ m_wallpaper_mode = mode_to_enum(wm.wm_config()->read_entry("Background", "Mode", "simple"));
+ auto& ws = Screen::the();
+
+ auto dirty_rects = move(m_dirty_rects);
+
+ if (dirty_rects.size() == 0) {
+ // nothing dirtied since the last compose pass.
+ return;
+ }
+
+ dirty_rects.add(Gfx::Rect::intersection(m_last_geometry_label_rect, Screen::the().rect()));
+ dirty_rects.add(Gfx::Rect::intersection(m_last_cursor_rect, Screen::the().rect()));
+ dirty_rects.add(Gfx::Rect::intersection(m_last_dnd_rect, Screen::the().rect()));
+ dirty_rects.add(Gfx::Rect::intersection(current_cursor_rect(), Screen::the().rect()));
+
+ auto any_dirty_rect_intersects_window = [&dirty_rects](const Window& window) {
+ auto window_frame_rect = window.frame().rect();
+ for (auto& dirty_rect : dirty_rects.rects()) {
+ if (dirty_rect.intersects(window_frame_rect))
+ return true;
+ }
+ return false;
+ };
+
+ Color background_color = wm.palette().desktop_background();
+ String background_color_entry = wm.wm_config()->read_entry("Background", "Color", "");
+ if (!background_color_entry.is_empty()) {
+ background_color = Color::from_string(background_color_entry).value_or(background_color);
+ }
+
+ // Paint the wallpaper.
+ for (auto& dirty_rect : dirty_rects.rects()) {
+ if (wm.any_opaque_window_contains_rect(dirty_rect))
+ continue;
+ // FIXME: If the wallpaper is opaque, no need to fill with color!
+ m_back_painter->fill_rect(dirty_rect, background_color);
+ if (m_wallpaper) {
+ if (m_wallpaper_mode == WallpaperMode::Simple) {
+ m_back_painter->blit(dirty_rect.location(), *m_wallpaper, dirty_rect);
+ } else if (m_wallpaper_mode == WallpaperMode::Center) {
+ Gfx::Point offset { ws.size().width() / 2 - m_wallpaper->size().width() / 2,
+ ws.size().height() / 2 - m_wallpaper->size().height() / 2 };
+ m_back_painter->blit_offset(dirty_rect.location(), *m_wallpaper,
+ dirty_rect, offset);
+ } else if (m_wallpaper_mode == WallpaperMode::Tile) {
+ m_back_painter->draw_tiled_bitmap(dirty_rect, *m_wallpaper);
+ } else if (m_wallpaper_mode == WallpaperMode::Scaled) {
+ float hscale = (float)m_wallpaper->size().width() / (float)ws.size().width();
+ float vscale = (float)m_wallpaper->size().height() / (float)ws.size().height();
+
+ m_back_painter->blit_scaled(dirty_rect, *m_wallpaper, dirty_rect, hscale, vscale);
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+ }
+ }
+
+ auto compose_window = [&](Window& window) -> IterationDecision {
+ if (!any_dirty_rect_intersects_window(window))
+ return IterationDecision::Continue;
+ Gfx::PainterStateSaver saver(*m_back_painter);
+ m_back_painter->add_clip_rect(window.frame().rect());
+ RefPtr<Gfx::Bitmap> backing_store = window.backing_store();
+ for (auto& dirty_rect : dirty_rects.rects()) {
+ if (wm.any_opaque_window_above_this_one_contains_rect(window, dirty_rect))
+ continue;
+ Gfx::PainterStateSaver saver(*m_back_painter);
+ m_back_painter->add_clip_rect(dirty_rect);
+ if (!backing_store)
+ m_back_painter->fill_rect(dirty_rect, wm.palette().window());
+ if (!window.is_fullscreen())
+ window.frame().paint(*m_back_painter);
+ if (!backing_store)
+ continue;
+
+ // Decide where we would paint this window's backing store.
+ // This is subtly different from widow.rect(), because window
+ // size may be different from its backing store size. This
+ // happens when the window has been resized and the client
+ // has not yet attached a new backing store. In this case,
+ // we want to try to blit the backing store at the same place
+ // it was previously, and fill the rest of the window with its
+ // background color.
+ Gfx::Rect backing_rect;
+ backing_rect.set_size(backing_store->size());
+ switch (WindowManager::the().resize_direction_of_window(window)) {
+ case ResizeDirection::None:
+ case ResizeDirection::Right:
+ case ResizeDirection::Down:
+ case ResizeDirection::DownRight:
+ backing_rect.set_location(window.rect().location());
+ break;
+ case ResizeDirection::Left:
+ case ResizeDirection::Up:
+ case ResizeDirection::UpLeft:
+ backing_rect.set_right_without_resize(window.rect().right());
+ backing_rect.set_bottom_without_resize(window.rect().bottom());
+ break;
+ case ResizeDirection::UpRight:
+ backing_rect.set_left(window.rect().left());
+ backing_rect.set_bottom_without_resize(window.rect().bottom());
+ break;
+ case ResizeDirection::DownLeft:
+ backing_rect.set_right_without_resize(window.rect().right());
+ backing_rect.set_top(window.rect().top());
+ break;
+ }
+
+ Gfx::Rect dirty_rect_in_backing_coordinates = dirty_rect
+ .intersected(window.rect())
+ .intersected(backing_rect)
+ .translated(-backing_rect.location());
+
+ if (dirty_rect_in_backing_coordinates.is_empty())
+ continue;
+ auto dst = backing_rect.location().translated(dirty_rect_in_backing_coordinates.location());
+
+ m_back_painter->blit(dst, *backing_store, dirty_rect_in_backing_coordinates, window.opacity());
+ for (auto background_rect : window.rect().shatter(backing_rect))
+ m_back_painter->fill_rect(background_rect, wm.palette().window());
+ }
+ return IterationDecision::Continue;
+ };
+
+ // Paint the window stack.
+ if (auto* fullscreen_window = wm.active_fullscreen_window()) {
+ compose_window(*fullscreen_window);
+ } else {
+ wm.for_each_visible_window_from_back_to_front([&](Window& window) {
+ return compose_window(window);
+ });
+
+ draw_geometry_label();
+ }
+
+ run_animations();
+
+ draw_cursor();
+
+ if (m_flash_flush) {
+ for (auto& rect : dirty_rects.rects())
+ m_front_painter->fill_rect(rect, Color::Yellow);
+ }
+
+ if (m_screen_can_set_buffer)
+ flip_buffers();
+
+ for (auto& r : dirty_rects.rects())
+ flush(r);
+}
+
+void Compositor::flush(const Gfx::Rect& a_rect)
+{
+ auto rect = Gfx::Rect::intersection(a_rect, Screen::the().rect());
+
+ Gfx::RGBA32* front_ptr = m_front_bitmap->scanline(rect.y()) + rect.x();
+ Gfx::RGBA32* back_ptr = m_back_bitmap->scanline(rect.y()) + rect.x();
+ size_t pitch = m_back_bitmap->pitch();
+
+ // NOTE: The meaning of a flush depends on whether we can flip buffers or not.
+ //
+ // If flipping is supported, flushing means that we've flipped, and now we
+ // copy the changed bits from the front buffer to the back buffer, to keep
+ // them in sync.
+ //
+ // If flipping is not supported, flushing means that we copy the changed
+ // rects from the backing bitmap to the display framebuffer.
+
+ Gfx::RGBA32* to_ptr;
+ const Gfx::RGBA32* from_ptr;
+
+ if (m_screen_can_set_buffer) {
+ to_ptr = back_ptr;
+ from_ptr = front_ptr;
+ } else {
+ to_ptr = front_ptr;
+ from_ptr = back_ptr;
+ }
+
+ for (int y = 0; y < rect.height(); ++y) {
+ fast_u32_copy(to_ptr, from_ptr, rect.width());
+ from_ptr = (const Gfx::RGBA32*)((const u8*)from_ptr + pitch);
+ to_ptr = (Gfx::RGBA32*)((u8*)to_ptr + pitch);
+ }
+}
+
+void Compositor::invalidate()
+{
+ m_dirty_rects.clear_with_capacity();
+ invalidate(Screen::the().rect());
+}
+
+void Compositor::invalidate(const Gfx::Rect& a_rect)
+{
+ auto rect = Gfx::Rect::intersection(a_rect, Screen::the().rect());
+ if (rect.is_empty())
+ return;
+
+ m_dirty_rects.add(rect);
+
+ // We delay composition by a timer interval, but to not affect latency too
+ // much, if a pending compose is not already scheduled, we also schedule an
+ // immediate compose the next spin of the event loop.
+ if (!m_compose_timer->is_active()) {
+ m_compose_timer->start();
+ m_immediate_compose_timer->start();
+ }
+}
+
+bool Compositor::set_background_color(const String& background_color)
+{
+ auto& wm = WindowManager::the();
+ wm.wm_config()->write_entry("Background", "Color", background_color);
+ bool ret_val = wm.wm_config()->sync();
+
+ if (ret_val)
+ Compositor::invalidate();
+
+ return ret_val;
+}
+
+bool Compositor::set_wallpaper_mode(const String& mode)
+{
+ auto& wm = WindowManager::the();
+ wm.wm_config()->write_entry("Background", "Mode", mode);
+ bool ret_val = wm.wm_config()->sync();
+
+ if (ret_val) {
+ m_wallpaper_mode = mode_to_enum(mode);
+ Compositor::invalidate();
+ }
+
+ return ret_val;
+}
+
+bool Compositor::set_wallpaper(const String& path, Function<void(bool)>&& callback)
+{
+ LibThread::BackgroundAction<RefPtr<Gfx::Bitmap>>::create(
+ [path] {
+ return Gfx::Bitmap::load_from_file(path);
+ },
+
+ [this, path, callback = move(callback)](RefPtr<Gfx::Bitmap> bitmap) {
+ m_wallpaper_path = path;
+ m_wallpaper = move(bitmap);
+ invalidate();
+ callback(true);
+ });
+ return true;
+}
+
+void Compositor::flip_buffers()
+{
+ ASSERT(m_screen_can_set_buffer);
+ swap(m_front_bitmap, m_back_bitmap);
+ swap(m_front_painter, m_back_painter);
+ Screen::the().set_buffer(m_buffers_are_flipped ? 0 : 1);
+ m_buffers_are_flipped = !m_buffers_are_flipped;
+}
+
+void Compositor::run_animations()
+{
+ static const int minimize_animation_steps = 10;
+
+ WindowManager::the().for_each_window([&](Window& window) {
+ if (window.in_minimize_animation()) {
+ int animation_index = window.minimize_animation_index();
+
+ auto from_rect = window.is_minimized() ? window.frame().rect() : window.taskbar_rect();
+ auto to_rect = window.is_minimized() ? window.taskbar_rect() : window.frame().rect();
+
+ float x_delta_per_step = (float)(from_rect.x() - to_rect.x()) / minimize_animation_steps;
+ float y_delta_per_step = (float)(from_rect.y() - to_rect.y()) / minimize_animation_steps;
+ float width_delta_per_step = (float)(from_rect.width() - to_rect.width()) / minimize_animation_steps;
+ float height_delta_per_step = (float)(from_rect.height() - to_rect.height()) / minimize_animation_steps;
+
+ Gfx::Rect rect {
+ from_rect.x() - (int)(x_delta_per_step * animation_index),
+ from_rect.y() - (int)(y_delta_per_step * animation_index),
+ from_rect.width() - (int)(width_delta_per_step * animation_index),
+ from_rect.height() - (int)(height_delta_per_step * animation_index)
+ };
+
+#ifdef MINIMIZE_ANIMATION_DEBUG
+ dbg() << "Minimize animation from " << from_rect << " to " << to_rect << " frame# " << animation_index << " " << rect;
+#endif
+
+ m_back_painter->draw_rect(rect, Color::White);
+
+ window.step_minimize_animation();
+ if (window.minimize_animation_index() >= minimize_animation_steps)
+ window.end_minimize_animation();
+
+ invalidate(rect);
+ }
+ return IterationDecision::Continue;
+ });
+}
+
+bool Compositor::set_resolution(int desired_width, int desired_height)
+{
+ auto screen_rect = Screen::the().rect();
+ if (screen_rect.width() == desired_width && screen_rect.height() == desired_height)
+ return true;
+
+ // Make sure it's impossible to set an invalid resolution
+ ASSERT(desired_width >= 640 && desired_height >= 480);
+ bool success = Screen::the().set_resolution(desired_width, desired_height);
+ init_bitmaps();
+ compose();
+ return success;
+}
+
+Gfx::Rect Compositor::current_cursor_rect() const
+{
+ auto& wm = WindowManager::the();
+ return { Screen::the().cursor_location().translated(-wm.active_cursor().hotspot()), wm.active_cursor().size() };
+}
+
+void Compositor::invalidate_cursor()
+{
+ auto& wm = WindowManager::the();
+ if (wm.dnd_client())
+ invalidate(wm.dnd_rect());
+ invalidate(current_cursor_rect());
+}
+
+void Compositor::draw_geometry_label()
+{
+ auto& wm = WindowManager::the();
+ auto* window_being_moved_or_resized = wm.m_move_window ? wm.m_move_window.ptr() : (wm.m_resize_window ? wm.m_resize_window.ptr() : nullptr);
+ if (!window_being_moved_or_resized) {
+ m_last_geometry_label_rect = {};
+ return;
+ }
+ auto geometry_string = window_being_moved_or_resized->rect().to_string();
+ if (!window_being_moved_or_resized->size_increment().is_null()) {
+ int width_steps = (window_being_moved_or_resized->width() - window_being_moved_or_resized->base_size().width()) / window_being_moved_or_resized->size_increment().width();
+ int height_steps = (window_being_moved_or_resized->height() - window_being_moved_or_resized->base_size().height()) / window_being_moved_or_resized->size_increment().height();
+ geometry_string = String::format("%s (%dx%d)", geometry_string.characters(), width_steps, height_steps);
+ }
+ auto geometry_label_rect = Gfx::Rect { 0, 0, wm.font().width(geometry_string) + 16, wm.font().glyph_height() + 10 };
+ geometry_label_rect.center_within(window_being_moved_or_resized->rect());
+ m_back_painter->fill_rect(geometry_label_rect, wm.palette().window());
+ m_back_painter->draw_rect(geometry_label_rect, wm.palette().threed_shadow2());
+ m_back_painter->draw_text(geometry_label_rect, geometry_string, Gfx::TextAlignment::Center, wm.palette().window_text());
+ m_last_geometry_label_rect = geometry_label_rect;
+}
+
+void Compositor::draw_cursor()
+{
+ auto& wm = WindowManager::the();
+ Gfx::Rect cursor_rect = current_cursor_rect();
+ m_back_painter->blit(cursor_rect.location(), wm.active_cursor().bitmap(), wm.active_cursor().rect());
+
+ if (wm.dnd_client()) {
+ auto dnd_rect = wm.dnd_rect();
+ m_back_painter->fill_rect(dnd_rect, wm.palette().selection().with_alpha(200));
+ if (!wm.dnd_text().is_empty()) {
+ auto text_rect = dnd_rect;
+ if (wm.dnd_bitmap())
+ text_rect.move_by(wm.dnd_bitmap()->width(), 0);
+ m_back_painter->draw_text(text_rect, wm.dnd_text(), Gfx::TextAlignment::CenterLeft, wm.palette().selection_text());
+ }
+ if (wm.dnd_bitmap()) {
+ m_back_painter->blit(dnd_rect.top_left(), *wm.dnd_bitmap(), wm.dnd_bitmap()->rect());
+ }
+ m_last_dnd_rect = dnd_rect;
+ } else {
+ m_last_dnd_rect = {};
+ }
+ m_last_cursor_rect = cursor_rect;
+}
+
+void Compositor::notify_display_links()
+{
+ ClientConnection::for_each_client([](auto& client) {
+ client.notify_display_link({});
+ });
+}
+
+void Compositor::increment_display_link_count(Badge<ClientConnection>)
+{
+ ++m_display_link_count;
+ if (m_display_link_count == 1)
+ m_display_link_notify_timer->start();
+}
+
+void Compositor::decrement_display_link_count(Badge<ClientConnection>)
+{
+ ASSERT(m_display_link_count);
+ --m_display_link_count;
+ if (!m_display_link_count)
+ m_display_link_notify_timer->stop();
+}
+
+}
diff --git a/Services/WindowServer/Compositor.h b/Services/WindowServer/Compositor.h
new file mode 100644
index 0000000000..99778d786c
--- /dev/null
+++ b/Services/WindowServer/Compositor.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/OwnPtr.h>
+#include <AK/RefPtr.h>
+#include <LibCore/Object.h>
+#include <LibGfx/DisjointRectSet.h>
+#include <LibGfx/Forward.h>
+
+namespace WindowServer {
+
+class ClientConnection;
+class Cursor;
+
+enum class WallpaperMode {
+ Simple,
+ Tile,
+ Center,
+ Scaled,
+ Unchecked
+};
+
+class Compositor final : public Core::Object {
+ C_OBJECT(Compositor)
+public:
+ static Compositor& the();
+
+ void compose();
+ void invalidate();
+ void invalidate(const Gfx::Rect&);
+
+ bool set_resolution(int desired_width, int desired_height);
+
+ bool set_background_color(const String& background_color);
+
+ bool set_wallpaper_mode(const String& mode);
+
+ bool set_wallpaper(const String& path, Function<void(bool)>&& callback);
+ String wallpaper_path() const { return m_wallpaper_path; }
+
+ void invalidate_cursor();
+ Gfx::Rect current_cursor_rect() const;
+
+ void increment_display_link_count(Badge<ClientConnection>);
+ void decrement_display_link_count(Badge<ClientConnection>);
+
+private:
+ Compositor();
+ void init_bitmaps();
+ void flip_buffers();
+ void flush(const Gfx::Rect&);
+ void draw_cursor();
+ void draw_geometry_label();
+ void draw_menubar();
+ void run_animations();
+ void notify_display_links();
+
+ RefPtr<Core::Timer> m_compose_timer;
+ RefPtr<Core::Timer> m_immediate_compose_timer;
+ bool m_flash_flush { false };
+ bool m_buffers_are_flipped { false };
+ bool m_screen_can_set_buffer { false };
+
+ RefPtr<Gfx::Bitmap> m_front_bitmap;
+ RefPtr<Gfx::Bitmap> m_back_bitmap;
+ OwnPtr<Gfx::Painter> m_back_painter;
+ OwnPtr<Gfx::Painter> m_front_painter;
+
+ Gfx::DisjointRectSet m_dirty_rects;
+
+ Gfx::Rect m_last_cursor_rect;
+ Gfx::Rect m_last_dnd_rect;
+ Gfx::Rect m_last_geometry_label_rect;
+
+ String m_wallpaper_path;
+ WallpaperMode m_wallpaper_mode { WallpaperMode::Unchecked };
+ RefPtr<Gfx::Bitmap> m_wallpaper;
+
+ RefPtr<Core::Timer> m_display_link_notify_timer;
+ size_t m_display_link_count { 0 };
+};
+
+}
diff --git a/Services/WindowServer/Cursor.cpp b/Services/WindowServer/Cursor.cpp
new file mode 100644
index 0000000000..42e431f481
--- /dev/null
+++ b/Services/WindowServer/Cursor.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <WindowServer/Cursor.h>
+#include <WindowServer/WindowManager.h>
+
+namespace WindowServer {
+
+Cursor::Cursor(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::Point& hotspot)
+ : m_bitmap(move(bitmap))
+ , m_hotspot(hotspot)
+{
+}
+
+Cursor::~Cursor()
+{
+}
+
+NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap)
+{
+ return adopt(*new Cursor(move(bitmap), bitmap->rect().center()));
+}
+
+NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::Point& hotspot)
+{
+ return adopt(*new Cursor(move(bitmap), hotspot));
+}
+
+RefPtr<Cursor> Cursor::create(StandardCursor standard_cursor)
+{
+ switch (standard_cursor) {
+ case StandardCursor::None:
+ return nullptr;
+ case StandardCursor::Arrow:
+ return WindowManager::the().arrow_cursor();
+ case StandardCursor::IBeam:
+ return WindowManager::the().i_beam_cursor();
+ case StandardCursor::ResizeHorizontal:
+ return WindowManager::the().resize_horizontally_cursor();
+ case StandardCursor::ResizeVertical:
+ return WindowManager::the().resize_vertically_cursor();
+ case StandardCursor::ResizeDiagonalTLBR:
+ return WindowManager::the().resize_diagonally_tlbr_cursor();
+ case StandardCursor::ResizeDiagonalBLTR:
+ return WindowManager::the().resize_diagonally_bltr_cursor();
+ case StandardCursor::Hand:
+ return WindowManager::the().hand_cursor();
+ case StandardCursor::Drag:
+ return WindowManager::the().drag_cursor();
+ }
+ ASSERT_NOT_REACHED();
+}
+
+}
diff --git a/Services/WindowServer/Cursor.h b/Services/WindowServer/Cursor.h
new file mode 100644
index 0000000000..5173bd86a1
--- /dev/null
+++ b/Services/WindowServer/Cursor.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGfx/Bitmap.h>
+
+namespace WindowServer {
+
+enum class StandardCursor {
+ None = 0,
+ Arrow,
+ IBeam,
+ ResizeHorizontal,
+ ResizeVertical,
+ ResizeDiagonalTLBR,
+ ResizeDiagonalBLTR,
+ Hand,
+ Drag,
+};
+
+class Cursor : public RefCounted<Cursor> {
+public:
+ static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::Point& hotspot);
+ static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&);
+ static RefPtr<Cursor> create(StandardCursor);
+ ~Cursor();
+
+ Gfx::Point hotspot() const { return m_hotspot; }
+ const Gfx::Bitmap& bitmap() const { return *m_bitmap; }
+
+ Gfx::Rect rect() const { return m_bitmap->rect(); }
+ Gfx::Size size() const { return m_bitmap->size(); }
+
+private:
+ Cursor(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::Point&);
+
+ RefPtr<Gfx::Bitmap> m_bitmap;
+ Gfx::Point m_hotspot;
+};
+
+}
diff --git a/Services/WindowServer/Event.h b/Services/WindowServer/Event.h
new file mode 100644
index 0000000000..48def26519
--- /dev/null
+++ b/Services/WindowServer/Event.h
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <Kernel/KeyCode.h>
+#include <LibCore/Event.h>
+#include <LibGfx/Rect.h>
+#include <WindowServer/Cursor.h>
+#include <WindowServer/WindowType.h>
+
+namespace WindowServer {
+
+class Event : public Core::Event {
+public:
+ enum Type {
+ Invalid = 3000,
+ MouseMove,
+ MouseDown,
+ MouseDoubleClick,
+ MouseUp,
+ MouseWheel,
+ WindowEntered,
+ WindowLeft,
+ KeyDown,
+ KeyUp,
+ WindowActivated,
+ WindowDeactivated,
+ WindowCloseRequest,
+ WindowResized,
+ };
+
+ Event() {}
+ explicit Event(Type type)
+ : Core::Event(type)
+ {
+ }
+ virtual ~Event() {}
+
+ bool is_mouse_event() const { return type() == MouseMove || type() == MouseDown || type() == MouseDoubleClick || type() == MouseUp || type() == MouseWheel; }
+ bool is_key_event() const { return type() == KeyUp || type() == KeyDown; }
+};
+
+enum class MouseButton : u8 {
+ None = 0,
+ Left = 1,
+ Right = 2,
+ Middle = 4,
+ Back = 8,
+ Forward = 16,
+};
+
+class KeyEvent final : public Event {
+public:
+ KeyEvent(Type type, int key, char character, u8 modifiers)
+ : Event(type)
+ , m_key(key)
+ , m_character(character)
+ , m_modifiers(modifiers)
+ {
+ }
+
+ int key() const { return m_key; }
+ bool ctrl() const { return m_modifiers & Mod_Ctrl; }
+ bool alt() const { return m_modifiers & Mod_Alt; }
+ bool shift() const { return m_modifiers & Mod_Shift; }
+ bool logo() const { return m_modifiers & Mod_Logo; }
+ u8 modifiers() const { return m_modifiers; }
+ char character() const { return m_character; }
+
+private:
+ friend class EventLoop;
+ friend class Screen;
+ int m_key { 0 };
+ char m_character { 0 };
+ u8 m_modifiers { 0 };
+};
+
+class MouseEvent final : public Event {
+public:
+ MouseEvent(Type type, const Gfx::Point& position, unsigned buttons, MouseButton button, unsigned modifiers, int wheel_delta = 0)
+ : Event(type)
+ , m_position(position)
+ , m_buttons(buttons)
+ , m_button(button)
+ , m_modifiers(modifiers)
+ , m_wheel_delta(wheel_delta)
+ {
+ }
+
+ Gfx::Point position() const { return m_position; }
+ int x() const { return m_position.x(); }
+ int y() const { return m_position.y(); }
+ MouseButton button() const { return m_button; }
+ unsigned buttons() const { return m_buttons; }
+ unsigned modifiers() const { return m_modifiers; }
+ int wheel_delta() const { return m_wheel_delta; }
+ bool is_drag() const { return m_drag; }
+ const String& drag_data_type() const { return m_drag_data_type; }
+
+ void set_drag(bool b) { m_drag = b; }
+ void set_drag_data_type(const String& drag_data_type) { m_drag_data_type = drag_data_type; }
+
+ MouseEvent translated(const Gfx::Point& delta) const { return MouseEvent((Type)type(), m_position.translated(delta), m_buttons, m_button, m_modifiers, m_wheel_delta); }
+
+private:
+ Gfx::Point m_position;
+ unsigned m_buttons { 0 };
+ MouseButton m_button { MouseButton::None };
+ unsigned m_modifiers { 0 };
+ int m_wheel_delta { 0 };
+ bool m_drag { false };
+ String m_drag_data_type;
+};
+
+class ResizeEvent final : public Event {
+public:
+ ResizeEvent(const Gfx::Rect& old_rect, const Gfx::Rect& rect)
+ : Event(Event::WindowResized)
+ , m_old_rect(old_rect)
+ , m_rect(rect)
+ {
+ }
+
+ Gfx::Rect old_rect() const { return m_old_rect; }
+ Gfx::Rect rect() const { return m_rect; }
+
+private:
+ Gfx::Rect m_old_rect;
+ Gfx::Rect m_rect;
+};
+
+}
diff --git a/Services/WindowServer/EventLoop.cpp b/Services/WindowServer/EventLoop.cpp
new file mode 100644
index 0000000000..078794d6f3
--- /dev/null
+++ b/Services/WindowServer/EventLoop.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Clipboard.h"
+#include <Kernel/KeyCode.h>
+#include <Kernel/MousePacket.h>
+#include <LibCore/LocalSocket.h>
+#include <LibCore/Object.h>
+#include <WindowServer/ClientConnection.h>
+#include <WindowServer/Cursor.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/EventLoop.h>
+#include <WindowServer/Screen.h>
+#include <WindowServer/WindowManager.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+//#define WSMESSAGELOOP_DEBUG
+
+namespace WindowServer {
+
+EventLoop::EventLoop()
+ : m_server(Core::LocalServer::construct())
+{
+ m_keyboard_fd = open("/dev/keyboard", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ m_mouse_fd = open("/dev/mouse", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+
+ bool ok = m_server->take_over_from_system_server();
+ ASSERT(ok);
+
+ m_server->on_ready_to_accept = [this] {
+ auto client_socket = m_server->accept();
+ if (!client_socket) {
+ dbg() << "WindowServer: accept failed.";
+ return;
+ }
+ static int s_next_client_id = 0;
+ int client_id = ++s_next_client_id;
+ IPC::new_client_connection<ClientConnection>(*client_socket, client_id);
+ };
+
+ ASSERT(m_keyboard_fd >= 0);
+ ASSERT(m_mouse_fd >= 0);
+
+ m_keyboard_notifier = Core::Notifier::construct(m_keyboard_fd, Core::Notifier::Read);
+ m_keyboard_notifier->on_ready_to_read = [this] { drain_keyboard(); };
+
+ m_mouse_notifier = Core::Notifier::construct(m_mouse_fd, Core::Notifier::Read);
+ m_mouse_notifier->on_ready_to_read = [this] { drain_mouse(); };
+
+ Clipboard::the().on_content_change = [&] {
+ ClientConnection::for_each_client([&](auto& client) {
+ client.notify_about_clipboard_contents_changed();
+ });
+ };
+}
+
+EventLoop::~EventLoop()
+{
+}
+
+void EventLoop::drain_mouse()
+{
+ auto& screen = Screen::the();
+ MousePacket state;
+ state.buttons = screen.mouse_button_state();
+ unsigned buttons = state.buttons;
+ MousePacket packets[32];
+
+ ssize_t nread = read(m_mouse_fd, &packets, sizeof(packets));
+ if (nread < 0) {
+ perror("EventLoop::drain_mouse read");
+ return;
+ }
+ size_t npackets = nread / sizeof(MousePacket);
+ if (!npackets)
+ return;
+ for (size_t i = 0; i < npackets; ++i) {
+ auto& packet = packets[i];
+#ifdef WSMESSAGELOOP_DEBUG
+ dbgprintf("EventLoop: Mouse X %d, Y %d, Z %d, relative %d\n", packet.x, packet.y, packet.z, packet.is_relative);
+#endif
+ buttons = packet.buttons;
+
+ state.is_relative = packet.is_relative;
+ if (packet.is_relative) {
+ state.x += packet.x;
+ state.y -= packet.y;
+ state.z += packet.z;
+ } else {
+ state.x = packet.x;
+ state.y = packet.y;
+ state.z += packet.z;
+ }
+
+ if (buttons != state.buttons) {
+ state.buttons = buttons;
+#ifdef WSMESSAGELOOP_DEBUG
+ dbgprintf("EventLoop: Mouse Button Event\n");
+#endif
+ screen.on_receive_mouse_data(state);
+ if (state.is_relative) {
+ state.x = 0;
+ state.y = 0;
+ state.z = 0;
+ }
+ }
+ }
+ if (state.is_relative && (state.x || state.y || state.z))
+ screen.on_receive_mouse_data(state);
+ if (!state.is_relative)
+ screen.on_receive_mouse_data(state);
+}
+
+void EventLoop::drain_keyboard()
+{
+ auto& screen = Screen::the();
+ for (;;) {
+ ::KeyEvent event;
+ ssize_t nread = read(m_keyboard_fd, (u8*)&event, sizeof(::KeyEvent));
+ if (nread == 0)
+ break;
+ ASSERT(nread == sizeof(::KeyEvent));
+ screen.on_receive_keyboard_data(event);
+ }
+}
+
+}
diff --git a/Services/WindowServer/EventLoop.h b/Services/WindowServer/EventLoop.h
new file mode 100644
index 0000000000..b476b5e639
--- /dev/null
+++ b/Services/WindowServer/EventLoop.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/LocalServer.h>
+#include <LibCore/Notifier.h>
+
+namespace WindowServer {
+
+class ClientConnection;
+
+class EventLoop {
+public:
+ EventLoop();
+ virtual ~EventLoop();
+
+ int exec() { return m_event_loop.exec(); }
+
+private:
+ void drain_mouse();
+ void drain_keyboard();
+
+ Core::EventLoop m_event_loop;
+ int m_keyboard_fd { -1 };
+ RefPtr<Core::Notifier> m_keyboard_notifier;
+ int m_mouse_fd { -1 };
+ RefPtr<Core::Notifier> m_mouse_notifier;
+ RefPtr<Core::LocalServer> m_server;
+};
+
+}
diff --git a/Services/WindowServer/Makefile b/Services/WindowServer/Makefile
new file mode 100644
index 0000000000..064e2bd0f2
--- /dev/null
+++ b/Services/WindowServer/Makefile
@@ -0,0 +1,38 @@
+OBJS = \
+ AppletManager.o \
+ Button.o \
+ ClientConnection.o \
+ Clipboard.o \
+ Compositor.o \
+ Cursor.o \
+ EventLoop.o \
+ Menu.o \
+ MenuBar.o \
+ MenuItem.o \
+ MenuManager.o \
+ Screen.o \
+ Window.o \
+ WindowFrame.o \
+ WindowManager.o \
+ WindowSwitcher.o \
+ main.o
+
+PROGRAM = WindowServer
+
+LIB_DEPS = Gfx Core Thread Pthread IPC
+
+*.cpp: WindowServerEndpoint.h WindowClientEndpoint.h
+
+WindowServerEndpoint.h: WindowServer.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+WindowClientEndpoint.h: WindowClient.ipc | IPCCOMPILER
+ @echo "IPC $<"; $(IPCCOMPILER) $< > $@
+
+EXTRA_CLEAN = WindowServerEndpoint.h WindowClientEndpoint.h
+
+install:
+ mkdir -p ../../Root/usr/include/WindowServer/
+ cp *.h ../../Root/usr/include/WindowServer/
+
+include ../../Makefile.common
diff --git a/Services/WindowServer/Menu.cpp b/Services/WindowServer/Menu.cpp
new file mode 100644
index 0000000000..07c460b9b2
--- /dev/null
+++ b/Services/WindowServer/Menu.cpp
@@ -0,0 +1,570 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Menu.h"
+#include "Event.h"
+#include "EventLoop.h"
+#include "MenuItem.h"
+#include "MenuManager.h"
+#include "Screen.h"
+#include "Window.h"
+#include "WindowManager.h"
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/CharacterBitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Painter.h>
+#include <LibGfx/StylePainter.h>
+#include <LibGfx/Triangle.h>
+#include <WindowServer/ClientConnection.h>
+#include <WindowServer/WindowClientEndpoint.h>
+
+namespace WindowServer {
+
+Menu::Menu(ClientConnection* client, int menu_id, const String& name)
+ : Core::Object(client)
+ , m_client(client)
+ , m_menu_id(menu_id)
+ , m_name(move(name))
+{
+}
+
+Menu::~Menu()
+{
+}
+
+void Menu::set_title_font(const Gfx::Font& font)
+{
+ m_title_font = &font;
+}
+
+const Gfx::Font& Menu::title_font() const
+{
+ return *m_title_font;
+}
+
+const Gfx::Font& Menu::font() const
+{
+ return Gfx::Font::default_font();
+}
+
+static const char* s_checked_bitmap_data = {
+ " "
+ " # "
+ " ## "
+ " ### "
+ " ## ### "
+ " ##### "
+ " ### "
+ " # "
+ " "
+};
+
+static const char* s_submenu_arrow_bitmap_data = {
+ " "
+ " # "
+ " ## "
+ " ### "
+ " #### "
+ " ### "
+ " ## "
+ " # "
+ " "
+};
+
+static Gfx::CharacterBitmap* s_checked_bitmap;
+static const int s_checked_bitmap_width = 9;
+static const int s_checked_bitmap_height = 9;
+static const int s_submenu_arrow_bitmap_width = 9;
+static const int s_submenu_arrow_bitmap_height = 9;
+static const int s_item_icon_width = 16;
+static const int s_stripe_width = 23;
+
+int Menu::content_width() const
+{
+ int widest_text = 0;
+ int widest_shortcut = 0;
+ for (auto& item : m_items) {
+ if (item.type() != MenuItem::Text)
+ continue;
+ int text_width = font().width(item.text());
+ if (!item.shortcut_text().is_empty()) {
+ int shortcut_width = font().width(item.shortcut_text());
+ widest_shortcut = max(shortcut_width, widest_shortcut);
+ }
+ widest_text = max(widest_text, text_width);
+ }
+
+ int widest_item = widest_text + s_stripe_width;
+ if (widest_shortcut)
+ widest_item += padding_between_text_and_shortcut() + widest_shortcut;
+
+ return max(widest_item, rect_in_menubar().width()) + horizontal_padding() + frame_thickness() * 2;
+}
+
+void Menu::redraw()
+{
+ if (!menu_window())
+ return;
+ draw();
+ menu_window()->invalidate();
+}
+
+Window& Menu::ensure_menu_window()
+{
+ if (m_menu_window)
+ return *m_menu_window;
+
+ int width = this->content_width();
+
+ Gfx::Point next_item_location(frame_thickness(), frame_thickness());
+ for (auto& item : m_items) {
+ int height = 0;
+ if (item.type() == MenuItem::Text)
+ height = item_height();
+ else if (item.type() == MenuItem::Separator)
+ height = 8;
+ item.set_rect({ next_item_location, { width - frame_thickness() * 2, height } });
+ next_item_location.move_by(0, height);
+ }
+
+ int window_height_available = Screen::the().height() - MenuManager::the().menubar_rect().height() - frame_thickness() * 2;
+ int max_window_height = (window_height_available / item_height()) * item_height() + frame_thickness() * 2;
+ int content_height = m_items.is_empty() ? 0 : (m_items.last().rect().bottom() + 1) + frame_thickness();
+ int window_height = min(max_window_height, content_height);
+ if (window_height < content_height) {
+ m_scrollable = true;
+ m_max_scroll_offset = item_count() - window_height / item_height() + 2;
+ }
+
+ auto window = Window::construct(*this, WindowType::Menu);
+ window->set_rect(0, 0, width, window_height);
+ m_menu_window = move(window);
+ draw();
+
+ return *m_menu_window;
+}
+
+int Menu::visible_item_count() const
+{
+ if (!is_scrollable())
+ return m_items.size();
+ ASSERT(m_menu_window);
+ // Make space for up/down arrow indicators
+ return m_menu_window->height() / item_height() - 2;
+}
+
+void Menu::draw()
+{
+ auto palette = WindowManager::the().palette();
+ m_theme_index_at_last_paint = MenuManager::the().theme_index();
+
+ ASSERT(menu_window());
+ ASSERT(menu_window()->backing_store());
+ Gfx::Painter painter(*menu_window()->backing_store());
+
+ Gfx::Rect rect { {}, menu_window()->size() };
+ Gfx::StylePainter::paint_window_frame(painter, rect, palette);
+ painter.fill_rect(rect.shrunken(6, 6), palette.menu_base());
+ int width = this->content_width();
+
+ if (!s_checked_bitmap)
+ s_checked_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref();
+
+ bool has_checkable_items = false;
+ bool has_items_with_icon = false;
+ for (auto& item : m_items) {
+ has_checkable_items = has_checkable_items | item.is_checkable();
+ has_items_with_icon = has_items_with_icon | !!item.icon();
+ }
+
+ Gfx::Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, menu_window()->height() - frame_thickness() * 2 };
+ painter.fill_rect(stripe_rect, palette.menu_stripe());
+ painter.draw_line(stripe_rect.top_right(), stripe_rect.bottom_right(), palette.menu_stripe().darkened());
+
+ int visible_item_count = this->visible_item_count();
+
+ if (is_scrollable()) {
+ bool can_go_up = m_scroll_offset > 0;
+ bool can_go_down = m_scroll_offset < m_max_scroll_offset;
+ Gfx::Rect up_indicator_rect { frame_thickness(), frame_thickness(), content_width(), item_height() };
+ painter.draw_text(up_indicator_rect, "\xc3\xb6", Gfx::TextAlignment::Center, can_go_up ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
+ Gfx::Rect down_indicator_rect { frame_thickness(), menu_window()->height() - item_height() - frame_thickness(), content_width(), item_height() };
+ painter.draw_text(down_indicator_rect, "\xc3\xb7", Gfx::TextAlignment::Center, can_go_down ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
+ }
+
+ for (int i = 0; i < visible_item_count; ++i) {
+ auto& item = m_items.at(m_scroll_offset + i);
+ if (item.type() == MenuItem::Text) {
+ Color text_color = palette.menu_base_text();
+ if (&item == hovered_item() && item.is_enabled()) {
+ painter.fill_rect(item.rect(), palette.menu_selection());
+ painter.draw_rect(item.rect(), palette.menu_selection().darkened());
+ text_color = palette.menu_selection_text();
+ } else if (!item.is_enabled()) {
+ text_color = Color::MidGray;
+ }
+ Gfx::Rect text_rect = item.rect().translated(stripe_rect.width() + 6, 0);
+ if (item.is_checkable()) {
+ if (item.is_exclusive()) {
+ Gfx::Rect radio_rect { item.rect().x() + 5, 0, 12, 12 };
+ radio_rect.center_vertically_within(text_rect);
+ Gfx::StylePainter::paint_radio_button(painter, radio_rect, palette, item.is_checked(), false);
+ } else {
+ Gfx::Rect checkmark_rect { item.rect().x() + 7, 0, s_checked_bitmap_width, s_checked_bitmap_height };
+ checkmark_rect.center_vertically_within(text_rect);
+ Gfx::Rect checkbox_rect = checkmark_rect.inflated(4, 4);
+ painter.fill_rect(checkbox_rect, palette.base());
+ Gfx::StylePainter::paint_frame(painter, checkbox_rect, palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2);
+ if (item.is_checked()) {
+ painter.draw_bitmap(checkmark_rect.location(), *s_checked_bitmap, palette.button_text());
+ }
+ }
+ } else if (item.icon()) {
+ Gfx::Rect icon_rect { item.rect().x() + 3, 0, s_item_icon_width, s_item_icon_width };
+ icon_rect.center_vertically_within(text_rect);
+ painter.blit(icon_rect.location(), *item.icon(), item.icon()->rect());
+ }
+ painter.draw_text(text_rect, item.text(), Gfx::TextAlignment::CenterLeft, text_color);
+ if (!item.shortcut_text().is_empty()) {
+ painter.draw_text(item.rect().translated(-right_padding(), 0), item.shortcut_text(), Gfx::TextAlignment::CenterRight, text_color);
+ }
+ if (item.is_submenu()) {
+ static auto& submenu_arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_submenu_arrow_bitmap_data, s_submenu_arrow_bitmap_width, s_submenu_arrow_bitmap_height).leak_ref();
+ Gfx::Rect submenu_arrow_rect {
+ item.rect().right() - s_submenu_arrow_bitmap_width - 2,
+ 0,
+ s_submenu_arrow_bitmap_width,
+ s_submenu_arrow_bitmap_height
+ };
+ submenu_arrow_rect.center_vertically_within(item.rect());
+ painter.draw_bitmap(submenu_arrow_rect.location(), submenu_arrow_bitmap, text_color);
+ }
+ } else if (item.type() == MenuItem::Separator) {
+ Gfx::Point p1(item.rect().translated(stripe_rect.width() + 4, 0).x(), item.rect().center().y() - 1);
+ Gfx::Point p2(width - 7, item.rect().center().y() - 1);
+ painter.draw_line(p1, p2, palette.threed_shadow1());
+ painter.draw_line(p1.translated(0, 1), p2.translated(0, 1), palette.threed_highlight());
+ }
+ }
+}
+
+MenuItem* Menu::hovered_item() const
+{
+ if (m_hovered_item_index == -1)
+ return nullptr;
+ return const_cast<MenuItem*>(&item(m_hovered_item_index));
+}
+
+void Menu::update_for_new_hovered_item()
+{
+ if (hovered_item() && hovered_item()->is_submenu()) {
+ MenuManager::the().close_everyone_not_in_lineage(*hovered_item()->submenu());
+ hovered_item()->submenu()->popup(hovered_item()->rect().top_right().translated(menu_window()->rect().location()), true);
+ } else {
+ MenuManager::the().close_everyone_not_in_lineage(*this);
+ MenuManager::the().set_current_menu(this);
+ menu_window()->set_visible(true);
+ }
+ redraw();
+}
+
+void Menu::open_hovered_item()
+{
+ ASSERT(menu_window());
+ ASSERT(menu_window()->is_visible());
+ if (!hovered_item())
+ return;
+ if (hovered_item()->is_enabled())
+ did_activate(*hovered_item());
+ clear_hovered_item();
+}
+
+void Menu::descend_into_submenu_at_hovered_item()
+{
+ ASSERT(hovered_item());
+ ASSERT(hovered_item()->is_submenu());
+ auto submenu = hovered_item()->submenu();
+ submenu->m_hovered_item_index = 0;
+ ASSERT(submenu->hovered_item()->type() != MenuItem::Separator);
+ submenu->update_for_new_hovered_item();
+ m_in_submenu = true;
+}
+
+void Menu::handle_mouse_move_event(const MouseEvent& mouse_event)
+{
+ ASSERT(menu_window());
+ if (hovered_item() && hovered_item()->is_submenu()) {
+
+ auto item = *hovered_item();
+ auto submenu_top_left = item.rect().location() + Gfx::Point { item.rect().width(), 0 };
+ auto submenu_bottom_left = submenu_top_left + Gfx::Point { 0, item.submenu()->menu_window()->height() };
+
+ auto safe_hover_triangle = Gfx::Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left };
+ m_last_position_in_hover = mouse_event.position();
+
+ // Don't update the hovered item if mouse is moving towards a submenu
+ if (safe_hover_triangle.contains(mouse_event.position()))
+ return;
+ }
+
+ int index = item_index_at(mouse_event.position());
+ if (m_hovered_item_index == index)
+ return;
+ m_hovered_item_index = index;
+
+ // FIXME: Tell parent menu (if it exists) that it is currently in a submenu
+ m_in_submenu = false;
+ update_for_new_hovered_item();
+ return;
+}
+
+void Menu::event(Core::Event& event)
+{
+ if (event.type() == Event::MouseMove) {
+ handle_mouse_move_event(static_cast<const MouseEvent&>(event));
+ return;
+ }
+
+ if (event.type() == Event::MouseUp) {
+ open_hovered_item();
+ return;
+ }
+
+ if (event.type() == Event::MouseWheel && is_scrollable()) {
+ ASSERT(menu_window());
+ auto& mouse_event = static_cast<const MouseEvent&>(event);
+ m_scroll_offset += mouse_event.wheel_delta();
+ m_scroll_offset = clamp(m_scroll_offset, 0, m_max_scroll_offset);
+
+ int index = item_index_at(mouse_event.position());
+ if (m_hovered_item_index == index)
+ return;
+
+ m_hovered_item_index = index;
+ update_for_new_hovered_item();
+ return;
+ }
+
+ if (event.type() == Event::KeyDown) {
+ auto key = static_cast<KeyEvent&>(event).key();
+
+ if (!(key == Key_Up || key == Key_Down || key == Key_Left || key == Key_Right || key == Key_Return))
+ return;
+
+ ASSERT(menu_window());
+ ASSERT(menu_window()->is_visible());
+
+ // Default to the first item on key press if one has not been selected yet
+ if (!hovered_item()) {
+ m_hovered_item_index = 0;
+ update_for_new_hovered_item();
+ return;
+ }
+
+ // Pass the event for the submenu that we are currently in to handle
+ if (m_in_submenu && key != Key_Left) {
+ ASSERT(hovered_item()->is_submenu());
+ hovered_item()->submenu()->dispatch_event(event);
+ return;
+ }
+
+ if (key == Key_Return) {
+ if (!hovered_item()->is_enabled())
+ return;
+ if (hovered_item()->is_submenu())
+ descend_into_submenu_at_hovered_item();
+ else
+ open_hovered_item();
+ return;
+ }
+
+ if (key == Key_Up) {
+ ASSERT(m_items.at(0).type() != MenuItem::Separator);
+
+ if (is_scrollable() && m_hovered_item_index == 0)
+ return;
+
+ auto original_index = m_hovered_item_index;
+ do {
+ if (m_hovered_item_index == 0)
+ m_hovered_item_index = m_items.size() - 1;
+ else
+ --m_hovered_item_index;
+ if (m_hovered_item_index == original_index)
+ return;
+ } while (hovered_item()->type() == MenuItem::Separator || !hovered_item()->is_enabled());
+
+ ASSERT(m_hovered_item_index >= 0 && m_hovered_item_index <= static_cast<int>(m_items.size()) - 1);
+
+ if (is_scrollable() && m_hovered_item_index < m_scroll_offset)
+ --m_scroll_offset;
+
+ update_for_new_hovered_item();
+ return;
+ }
+
+ if (key == Key_Down) {
+ ASSERT(m_items.at(0).type() != MenuItem::Separator);
+
+ if (is_scrollable() && m_hovered_item_index == static_cast<int>(m_items.size()) - 1)
+ return;
+
+ auto original_index = m_hovered_item_index;
+ do {
+ if (m_hovered_item_index == static_cast<int>(m_items.size()) - 1)
+ m_hovered_item_index = 0;
+ else
+ ++m_hovered_item_index;
+ if (m_hovered_item_index == original_index)
+ return;
+ } while (hovered_item()->type() == MenuItem::Separator || !hovered_item()->is_enabled());
+
+ ASSERT(m_hovered_item_index >= 0 && m_hovered_item_index <= static_cast<int>(m_items.size()) - 1);
+
+ if (is_scrollable() && m_hovered_item_index >= (m_scroll_offset + visible_item_count()))
+ ++m_scroll_offset;
+
+ update_for_new_hovered_item();
+ return;
+ }
+
+ if (key == Key_Left) {
+ if (!m_in_submenu)
+ return;
+
+ ASSERT(hovered_item()->is_submenu());
+ hovered_item()->submenu()->clear_hovered_item();
+ m_in_submenu = false;
+ return;
+ }
+
+ if (key == Key_Right) {
+ if (hovered_item()->is_enabled() && hovered_item()->is_submenu())
+ descend_into_submenu_at_hovered_item();
+ return;
+ }
+ }
+ Core::Object::event(event);
+}
+
+void Menu::clear_hovered_item()
+{
+ if (!hovered_item())
+ return;
+ m_hovered_item_index = -1;
+ m_in_submenu = false;
+ redraw();
+}
+
+void Menu::did_activate(MenuItem& item)
+{
+ if (item.type() == MenuItem::Type::Separator)
+ return;
+
+ if (on_item_activation)
+ on_item_activation(item);
+
+ MenuManager::the().close_bar();
+
+ if (m_client)
+ m_client->post_message(Messages::WindowClient::MenuItemActivated(m_menu_id, item.identifier()));
+}
+
+MenuItem* Menu::item_with_identifier(unsigned identifer)
+{
+ for (auto& item : m_items) {
+ if (item.identifier() == identifer)
+ return &item;
+ }
+ return nullptr;
+}
+
+int Menu::item_index_at(const Gfx::Point& position)
+{
+ int i = 0;
+ for (auto& item : m_items) {
+ if (item.rect().contains(position))
+ return i;
+ ++i;
+ }
+ return -1;
+}
+
+void Menu::close()
+{
+ MenuManager::the().close_menu_and_descendants(*this);
+}
+
+void Menu::redraw_if_theme_changed()
+{
+ if (m_theme_index_at_last_paint != MenuManager::the().theme_index())
+ redraw();
+}
+
+void Menu::popup(const Gfx::Point& position, bool is_submenu)
+{
+ if (is_empty()) {
+ dbg() << "Menu: Empty menu popup";
+ return;
+ }
+
+ auto& window = ensure_menu_window();
+ redraw_if_theme_changed();
+
+ const int margin = 30;
+ Gfx::Point adjusted_pos = position;
+
+ if (adjusted_pos.x() + window.width() >= Screen::the().width() - margin) {
+ adjusted_pos = adjusted_pos.translated(-window.width(), 0);
+ }
+ if (adjusted_pos.y() + window.height() >= Screen::the().height() - margin) {
+ adjusted_pos = adjusted_pos.translated(0, -window.height());
+ }
+
+ if (adjusted_pos.y() < MenuManager::the().menubar_rect().height())
+ adjusted_pos.set_y(MenuManager::the().menubar_rect().height());
+
+ window.move_to(adjusted_pos);
+ window.set_visible(true);
+ MenuManager::the().set_current_menu(this, is_submenu);
+}
+
+bool Menu::is_menu_ancestor_of(const Menu& other) const
+{
+ for (auto& item : m_items) {
+ if (!item.is_submenu())
+ continue;
+ auto& submenu = *const_cast<MenuItem&>(item).submenu();
+ if (&submenu == &other)
+ return true;
+ if (submenu.is_menu_ancestor_of(other))
+ return true;
+ }
+ return false;
+}
+
+}
diff --git a/Services/WindowServer/Menu.h b/Services/WindowServer/Menu.h
new file mode 100644
index 0000000000..de9325616c
--- /dev/null
+++ b/Services/WindowServer/Menu.h
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/String.h>
+#include <AK/WeakPtr.h>
+#include <LibCore/Object.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Rect.h>
+#include <WindowServer/Cursor.h>
+#include <WindowServer/MenuItem.h>
+#include <WindowServer/Window.h>
+
+namespace WindowServer {
+
+class ClientConnection;
+class MenuBar;
+class Event;
+
+class Menu final : public Core::Object {
+ C_OBJECT(Menu)
+public:
+ Menu(ClientConnection*, int menu_id, const String& name);
+ virtual ~Menu() override;
+
+ ClientConnection* client() { return m_client; }
+ const ClientConnection* client() const { return m_client; }
+ int menu_id() const { return m_menu_id; }
+
+ MenuBar* menubar() { return m_menubar; }
+ const MenuBar* menubar() const { return m_menubar; }
+ void set_menubar(MenuBar* menubar) { m_menubar = menubar; }
+
+ bool is_empty() const { return m_items.is_empty(); }
+ int item_count() const { return m_items.size(); }
+ const MenuItem& item(int index) const { return m_items.at(index); }
+ MenuItem& item(int index) { return m_items.at(index); }
+
+ void add_item(NonnullOwnPtr<MenuItem>&& item) { m_items.append(move(item)); }
+
+ String name() const { return m_name; }
+
+ template<typename Callback>
+ void for_each_item(Callback callback) const
+ {
+ for (auto& item : m_items)
+ callback(item);
+ }
+
+ Gfx::Rect text_rect_in_menubar() const { return m_text_rect_in_menubar; }
+ void set_text_rect_in_menubar(const Gfx::Rect& rect) { m_text_rect_in_menubar = rect; }
+
+ Gfx::Rect rect_in_menubar() const { return m_rect_in_menubar; }
+ void set_rect_in_menubar(const Gfx::Rect& rect) { m_rect_in_menubar = rect; }
+
+ Window* menu_window() { return m_menu_window.ptr(); }
+ Window& ensure_menu_window();
+
+ Window* window_menu_of() { return m_window_menu_of; }
+ void set_window_menu_of(Window& window) { m_window_menu_of = window.make_weak_ptr(); }
+ bool is_window_menu_open() { return m_is_window_menu_open; }
+ void set_window_menu_open(bool is_open) { m_is_window_menu_open = is_open; }
+
+ int content_width() const;
+
+ int item_height() const { return 20; }
+ int frame_thickness() const { return 3; }
+ int horizontal_padding() const { return left_padding() + right_padding(); }
+ int left_padding() const { return 14; }
+ int right_padding() const { return 14; }
+
+ void draw();
+ const Gfx::Font& font() const;
+ const Gfx::Font& title_font() const;
+ void set_title_font(const Gfx::Font& font);
+
+ MenuItem* item_with_identifier(unsigned);
+ void redraw();
+
+ MenuItem* hovered_item() const;
+ void clear_hovered_item();
+
+ Function<void(MenuItem&)> on_item_activation;
+
+ void close();
+
+ void popup(const Gfx::Point&, bool is_submenu = false);
+
+ bool is_menu_ancestor_of(const Menu&) const;
+
+ void redraw_if_theme_changed();
+
+ bool is_scrollable() const { return m_scrollable; }
+ int scroll_offset() const { return m_scroll_offset; }
+
+private:
+ virtual void event(Core::Event&) override;
+
+ RefPtr<Gfx::Font> m_title_font { &Gfx::Font::default_font() };
+
+ void handle_mouse_move_event(const MouseEvent&);
+ int visible_item_count() const;
+
+ int item_index_at(const Gfx::Point&);
+ int padding_between_text_and_shortcut() const { return 50; }
+ void did_activate(MenuItem&);
+ void open_hovered_item();
+ void update_for_new_hovered_item();
+ void descend_into_submenu_at_hovered_item();
+
+ ClientConnection* m_client { nullptr };
+ int m_menu_id { 0 };
+ String m_name;
+ Gfx::Rect m_rect_in_menubar;
+ Gfx::Rect m_text_rect_in_menubar;
+ MenuBar* m_menubar { nullptr };
+ NonnullOwnPtrVector<MenuItem> m_items;
+ RefPtr<Window> m_menu_window;
+
+ WeakPtr<Window> m_window_menu_of;
+ bool m_is_window_menu_open = { false };
+ Gfx::Point m_last_position_in_hover;
+ int m_theme_index_at_last_paint { -1 };
+ int m_hovered_item_index { -1 };
+ bool m_in_submenu { false };
+
+ bool m_scrollable { false };
+ int m_scroll_offset { 0 };
+ int m_max_scroll_offset { 0 };
+};
+
+}
diff --git a/Services/WindowServer/MenuBar.cpp b/Services/WindowServer/MenuBar.cpp
new file mode 100644
index 0000000000..27dbdcbebf
--- /dev/null
+++ b/Services/WindowServer/MenuBar.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MenuBar.h"
+#include "Menu.h"
+#include "MenuItem.h"
+#include <LibGfx/Bitmap.h>
+
+namespace WindowServer {
+
+MenuBar::MenuBar(ClientConnection& client, int menubar_id)
+ : m_client(client)
+ , m_menubar_id(menubar_id)
+{
+}
+
+MenuBar::~MenuBar()
+{
+}
+
+void MenuBar::add_menu(Menu& menu)
+{
+ menu.set_menubar(this);
+
+ // NOTE: We assume that the first menu is the App menu, which has a bold font.
+ if (m_menus.is_empty())
+ menu.set_title_font(Gfx::Font::default_bold_font());
+
+ m_menus.append(&menu);
+}
+
+}
diff --git a/Services/WindowServer/MenuBar.h b/Services/WindowServer/MenuBar.h
new file mode 100644
index 0000000000..31dff9ac0f
--- /dev/null
+++ b/Services/WindowServer/MenuBar.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Menu.h"
+#include <AK/Vector.h>
+#include <AK/WeakPtr.h>
+#include <AK/Weakable.h>
+
+namespace WindowServer {
+
+class MenuBar : public Weakable<MenuBar> {
+public:
+ MenuBar(ClientConnection& client, int menubar_id);
+ ~MenuBar();
+
+ ClientConnection& client() { return m_client; }
+ const ClientConnection& client() const { return m_client; }
+ int menubar_id() const { return m_menubar_id; }
+ void add_menu(Menu&);
+
+ template<typename Callback>
+ void for_each_menu(Callback callback)
+ {
+ for (auto& menu : m_menus) {
+ if (callback(*menu) == IterationDecision::Break)
+ return;
+ }
+ }
+
+private:
+ ClientConnection& m_client;
+ int m_menubar_id { 0 };
+ Vector<Menu*> m_menus;
+};
+
+}
diff --git a/Services/WindowServer/MenuItem.cpp b/Services/WindowServer/MenuItem.cpp
new file mode 100644
index 0000000000..eba593c136
--- /dev/null
+++ b/Services/WindowServer/MenuItem.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MenuItem.h"
+#include "ClientConnection.h"
+#include "Menu.h"
+#include "WindowManager.h"
+#include <LibGfx/Bitmap.h>
+
+namespace WindowServer {
+
+MenuItem::MenuItem(Menu& menu, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked, const Gfx::Bitmap* icon)
+ : m_menu(menu)
+ , m_type(Text)
+ , m_enabled(enabled)
+ , m_checkable(checkable)
+ , m_checked(checked)
+ , m_identifier(identifier)
+ , m_text(text)
+ , m_shortcut_text(shortcut_text)
+ , m_icon(icon)
+{
+}
+
+MenuItem::MenuItem(Menu& menu, Type type)
+ : m_menu(menu)
+ , m_type(type)
+{
+}
+
+MenuItem::~MenuItem()
+{
+}
+
+void MenuItem::set_enabled(bool enabled)
+{
+ if (m_enabled == enabled)
+ return;
+ m_enabled = enabled;
+ m_menu.redraw();
+}
+
+void MenuItem::set_checked(bool checked)
+{
+ if (m_checked == checked)
+ return;
+ m_checked = checked;
+ m_menu.redraw();
+}
+
+Menu* MenuItem::submenu()
+{
+ ASSERT(is_submenu());
+ ASSERT(m_menu.client());
+ return m_menu.client()->find_menu_by_id(m_submenu_id);
+}
+
+Gfx::Rect MenuItem::rect() const
+{
+ if (!m_menu.is_scrollable())
+ return m_rect;
+ return m_rect.translated(0, m_menu.item_height() - (m_menu.scroll_offset() * m_menu.item_height()));
+}
+
+void MenuItem::set_icon(const Gfx::Bitmap* icon)
+{
+ if (m_icon == icon)
+ return;
+ m_icon = icon;
+ m_menu.redraw();
+}
+
+}
diff --git a/Services/WindowServer/MenuItem.h b/Services/WindowServer/MenuItem.h
new file mode 100644
index 0000000000..3f297adb43
--- /dev/null
+++ b/Services/WindowServer/MenuItem.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/String.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Rect.h>
+
+namespace WindowServer {
+
+class Menu;
+
+class MenuItem {
+public:
+ enum Type {
+ None,
+ Text,
+ Separator,
+ };
+
+ MenuItem(Menu&, unsigned identifier, const String& text, const String& shortcut_text = {}, bool enabled = true, bool checkable = false, bool checked = false, const Gfx::Bitmap* icon = nullptr);
+ MenuItem(Menu&, Type);
+ ~MenuItem();
+
+ Type type() const { return m_type; }
+
+ bool is_enabled() const { return m_enabled; }
+ void set_enabled(bool);
+
+ bool is_checkable() const { return m_checkable; }
+ void set_checkable(bool checkable) { m_checkable = checkable; }
+
+ bool is_checked() const { return m_checked; }
+ void set_checked(bool);
+
+ String text() const { return m_text; }
+ void set_text(const String& text) { m_text = text; }
+
+ String shortcut_text() const { return m_shortcut_text; }
+ void set_shortcut_text(const String& text) { m_shortcut_text = text; }
+
+ void set_rect(const Gfx::Rect& rect) { m_rect = rect; }
+ Gfx::Rect rect() const;
+
+ unsigned identifier() const { return m_identifier; }
+
+ const Gfx::Bitmap* icon() const { return m_icon; }
+ void set_icon(const Gfx::Bitmap*);
+
+ bool is_submenu() const { return m_submenu_id != -1; }
+ int submenu_id() const { return m_submenu_id; }
+ void set_submenu_id(int submenu_id) { m_submenu_id = submenu_id; }
+
+ Menu* submenu();
+
+ bool is_exclusive() const { return m_exclusive; }
+ void set_exclusive(bool exclusive) { m_exclusive = exclusive; }
+
+private:
+ Menu& m_menu;
+ Type m_type { None };
+ bool m_enabled { true };
+ bool m_checkable { false };
+ bool m_checked { false };
+ unsigned m_identifier { 0 };
+ String m_text;
+ String m_shortcut_text;
+ Gfx::Rect m_rect;
+ RefPtr<Gfx::Bitmap> m_icon;
+ int m_submenu_id { -1 };
+ bool m_exclusive { false };
+};
+
+}
diff --git a/Services/WindowServer/MenuManager.cpp b/Services/WindowServer/MenuManager.cpp
new file mode 100644
index 0000000000..2470d4f1fa
--- /dev/null
+++ b/Services/WindowServer/MenuManager.cpp
@@ -0,0 +1,404 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Badge.h>
+#include <AK/FileSystemPath.h>
+#include <AK/QuickSort.h>
+#include <LibCore/DirIterator.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Painter.h>
+#include <WindowServer/AppletManager.h>
+#include <WindowServer/MenuManager.h>
+#include <WindowServer/Screen.h>
+#include <WindowServer/WindowManager.h>
+#include <unistd.h>
+
+//#define DEBUG_MENUS
+
+namespace WindowServer {
+
+static MenuManager* s_the;
+
+MenuManager& MenuManager::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+MenuManager::MenuManager()
+{
+ s_the = this;
+ m_needs_window_resize = true;
+
+ // NOTE: This ensures that the system menu has the correct dimensions.
+ set_current_menubar(nullptr);
+
+ m_window = Window::construct(*this, WindowType::Menubar);
+ m_window->set_rect(menubar_rect());
+}
+
+MenuManager::~MenuManager()
+{
+}
+
+bool MenuManager::is_open(const Menu& menu) const
+{
+ for (size_t i = 0; i < m_open_menu_stack.size(); ++i) {
+ if (&menu == m_open_menu_stack[i].ptr())
+ return true;
+ }
+ return false;
+}
+
+void MenuManager::draw()
+{
+ auto& wm = WindowManager::the();
+ auto palette = wm.palette();
+ auto menubar_rect = this->menubar_rect();
+
+ if (m_needs_window_resize) {
+ m_window->set_rect(menubar_rect);
+ AppletManager::the().calculate_applet_rects(window());
+ m_needs_window_resize = false;
+ }
+
+ Gfx::Painter painter(*window().backing_store());
+
+ painter.fill_rect(menubar_rect, palette.window());
+ painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, palette.threed_shadow1());
+
+ for_each_active_menubar_menu([&](Menu& menu) {
+ Color text_color = palette.window_text();
+ if (is_open(menu)) {
+ painter.fill_rect(menu.rect_in_menubar(), palette.menu_selection());
+ painter.draw_rect(menu.rect_in_menubar(), palette.menu_selection().darkened());
+ text_color = palette.menu_selection_text();
+ }
+ painter.draw_text(
+ menu.text_rect_in_menubar(),
+ menu.name(),
+ menu.title_font(),
+ Gfx::TextAlignment::CenterLeft,
+ text_color);
+ return IterationDecision::Continue;
+ });
+
+ AppletManager::the().draw();
+}
+
+void MenuManager::refresh()
+{
+ if (!m_window)
+ return;
+ draw();
+ window().invalidate();
+}
+
+void MenuManager::event(Core::Event& event)
+{
+ if (static_cast<Event&>(event).is_mouse_event()) {
+ handle_mouse_event(static_cast<MouseEvent&>(event));
+ return;
+ }
+
+ if (static_cast<Event&>(event).is_key_event()) {
+ auto& key_event = static_cast<const KeyEvent&>(event);
+
+ if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) {
+ close_everyone();
+ return;
+ }
+
+ if (event.type() == Event::KeyDown) {
+ for_each_active_menubar_menu([&](Menu& menu) {
+ if (is_open(menu))
+ menu.dispatch_event(event);
+ return IterationDecision::Continue;
+ });
+ }
+ }
+
+ return Core::Object::event(event);
+}
+
+void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
+{
+ auto* active_window = WindowManager::the().active_window();
+ bool handled_menubar_event = false;
+ for_each_active_menubar_menu([&](Menu& menu) {
+ if (menu.rect_in_menubar().contains(mouse_event.position())) {
+ handled_menubar_event = &menu == m_system_menu || !active_window || !active_window->is_modal();
+ if (handled_menubar_event)
+ handle_menu_mouse_event(menu, mouse_event);
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+ });
+ if (handled_menubar_event)
+ return;
+
+ if (has_open_menu()) {
+ auto* topmost_menu = m_open_menu_stack.last().ptr();
+ ASSERT(topmost_menu);
+ auto* window = topmost_menu->menu_window();
+ if (!window) {
+ dbg() << "MenuManager::handle_mouse_event: No menu window";
+ return;
+ }
+ ASSERT(window->is_visible());
+
+ bool event_is_inside_current_menu = window->rect().contains(mouse_event.position());
+ if (event_is_inside_current_menu) {
+ WindowManager::the().set_hovered_window(window);
+ auto translated_event = mouse_event.translated(-window->position());
+ WindowManager::the().deliver_mouse_event(*window, translated_event);
+ return;
+ }
+
+ if (topmost_menu->hovered_item())
+ topmost_menu->clear_hovered_item();
+ if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) {
+ auto* window_menu_of = topmost_menu->window_menu_of();
+ if (window_menu_of) {
+ bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position());
+ if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) {
+ topmost_menu->set_window_menu_open(true);
+ return;
+ }
+ }
+
+ if (mouse_event.type() == Event::MouseDown) {
+ close_bar();
+ topmost_menu->set_window_menu_open(false);
+ }
+ }
+
+ if (mouse_event.type() == Event::MouseMove) {
+ for (auto& menu : m_open_menu_stack) {
+ if (!menu)
+ continue;
+ if (!menu->menu_window()->rect().contains(mouse_event.position()))
+ continue;
+ WindowManager::the().set_hovered_window(menu->menu_window());
+ auto translated_event = mouse_event.translated(-menu->menu_window()->position());
+ WindowManager::the().deliver_mouse_event(*menu->menu_window(), translated_event);
+ break;
+ }
+ }
+ return;
+ }
+
+ AppletManager::the().dispatch_event(static_cast<Event&>(mouse_event));
+}
+
+void MenuManager::handle_menu_mouse_event(Menu& menu, const MouseEvent& event)
+{
+ bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove
+ && has_open_menu()
+ && (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == m_system_menu.ptr());
+ bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Left;
+ bool should_open_menu = &menu != m_current_menu && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
+
+ if (is_mousedown_with_left_button)
+ m_bar_open = !m_bar_open;
+
+ if (should_open_menu && m_bar_open) {
+ open_menu(menu);
+ return;
+ }
+
+ if (!m_bar_open)
+ close_everyone();
+}
+
+void MenuManager::set_needs_window_resize()
+{
+ m_needs_window_resize = true;
+}
+
+void MenuManager::close_all_menus_from_client(Badge<ClientConnection>, ClientConnection& client)
+{
+ if (!has_open_menu())
+ return;
+ if (m_open_menu_stack.first()->client() != &client)
+ return;
+ close_everyone();
+}
+
+void MenuManager::close_everyone()
+{
+ for (auto& menu : m_open_menu_stack) {
+ if (menu && menu->menu_window())
+ menu->menu_window()->set_visible(false);
+ menu->clear_hovered_item();
+ }
+ m_open_menu_stack.clear();
+ m_current_menu = nullptr;
+ refresh();
+}
+
+void MenuManager::close_everyone_not_in_lineage(Menu& menu)
+{
+ Vector<Menu*> menus_to_close;
+ for (auto& open_menu : m_open_menu_stack) {
+ if (!open_menu)
+ continue;
+ if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
+ continue;
+ menus_to_close.append(open_menu);
+ }
+ close_menus(menus_to_close);
+}
+
+void MenuManager::close_menus(const Vector<Menu*>& menus)
+{
+ for (auto& menu : menus) {
+ if (menu == m_current_menu)
+ m_current_menu = nullptr;
+ if (menu->menu_window())
+ menu->menu_window()->set_visible(false);
+ menu->clear_hovered_item();
+ m_open_menu_stack.remove_first_matching([&](auto& entry) {
+ return entry == menu;
+ });
+ }
+ refresh();
+}
+
+static void collect_menu_subtree(Menu& menu, Vector<Menu*>& menus)
+{
+ menus.append(&menu);
+ for (int i = 0; i < menu.item_count(); ++i) {
+ auto& item = menu.item(i);
+ if (!item.is_submenu())
+ continue;
+ collect_menu_subtree(*const_cast<MenuItem&>(item).submenu(), menus);
+ }
+}
+
+void MenuManager::close_menu_and_descendants(Menu& menu)
+{
+ Vector<Menu*> menus_to_close;
+ collect_menu_subtree(menu, menus_to_close);
+ close_menus(menus_to_close);
+}
+
+void MenuManager::toggle_menu(Menu& menu)
+{
+ if (is_open(menu)) {
+ close_menu_and_descendants(menu);
+ return;
+ }
+ open_menu(menu);
+}
+
+void MenuManager::open_menu(Menu& menu)
+{
+ if (is_open(menu))
+ return;
+ if (!menu.is_empty()) {
+ menu.redraw_if_theme_changed();
+ auto& menu_window = menu.ensure_menu_window();
+ menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 });
+ menu_window.set_visible(true);
+ }
+ set_current_menu(&menu);
+ refresh();
+}
+
+void MenuManager::set_current_menu(Menu* menu, bool is_submenu)
+{
+ if (menu == m_current_menu)
+ return;
+
+ if (!is_submenu) {
+ if (menu)
+ close_everyone_not_in_lineage(*menu);
+ else
+ close_everyone();
+ }
+
+ if (!menu) {
+ m_current_menu = nullptr;
+ return;
+ }
+
+ m_current_menu = menu->make_weak_ptr();
+ if (m_open_menu_stack.find([menu](auto& other) { return menu == other.ptr(); }).is_end())
+ m_open_menu_stack.append(menu->make_weak_ptr());
+}
+
+void MenuManager::close_bar()
+{
+ close_everyone();
+ m_bar_open = false;
+}
+
+Gfx::Rect MenuManager::menubar_rect() const
+{
+ return { 0, 0, Screen::the().rect().width(), 18 };
+}
+
+void MenuManager::set_current_menubar(MenuBar* menubar)
+{
+ if (menubar)
+ m_current_menubar = menubar->make_weak_ptr();
+ else
+ m_current_menubar = nullptr;
+#ifdef DEBUG_MENUS
+ dbg() << "[WM] Current menubar is now " << menubar;
+#endif
+ Gfx::Point next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 };
+ for_each_active_menubar_menu([&](Menu& menu) {
+ int text_width = menu.title_font().width(menu.name());
+ menu.set_rect_in_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect().height() - 1 });
+ menu.set_text_rect_in_menubar({ next_menu_location, { text_width, menubar_rect().height() } });
+ next_menu_location.move_by(menu.rect_in_menubar().width(), 0);
+ return IterationDecision::Continue;
+ });
+ refresh();
+}
+
+void MenuManager::close_menubar(MenuBar& menubar)
+{
+ if (current_menubar() == &menubar)
+ set_current_menubar(nullptr);
+}
+
+void MenuManager::set_system_menu(Menu& menu)
+{
+ m_system_menu = menu.make_weak_ptr();
+ set_current_menubar(m_current_menubar);
+}
+
+void MenuManager::did_change_theme()
+{
+ ++m_theme_index;
+ refresh();
+}
+
+}
diff --git a/Services/WindowServer/MenuManager.h b/Services/WindowServer/MenuManager.h
new file mode 100644
index 0000000000..f4d5d5aad8
--- /dev/null
+++ b/Services/WindowServer/MenuManager.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Menu.h"
+#include "MenuBar.h"
+#include "Window.h"
+#include <AK/HashMap.h>
+#include <LibCore/Object.h>
+
+namespace WindowServer {
+
+class MenuManager final : public Core::Object {
+ C_OBJECT(MenuManager)
+public:
+ static MenuManager& the();
+
+ MenuManager();
+ virtual ~MenuManager() override;
+
+ void refresh();
+
+ bool is_open(const Menu&) const;
+ bool has_open_menu() const { return !m_open_menu_stack.is_empty(); }
+
+ Gfx::Rect menubar_rect() const;
+ static int menubar_menu_margin() { return 16; }
+
+ void set_needs_window_resize();
+
+ Menu* current_menu() { return m_current_menu.ptr(); }
+ void set_current_menu(Menu*, bool is_submenu = false);
+ void open_menu(Menu&);
+ void toggle_menu(Menu&);
+
+ MenuBar* current_menubar() { return m_current_menubar.ptr(); }
+ void set_current_menubar(MenuBar*);
+ void close_menubar(MenuBar&);
+
+ void close_bar();
+ void close_everyone();
+ void close_everyone_not_in_lineage(Menu&);
+ void close_menu_and_descendants(Menu&);
+
+ void close_all_menus_from_client(Badge<ClientConnection>, ClientConnection&);
+
+ void toggle_system_menu()
+ {
+ if (m_system_menu)
+ toggle_menu(*m_system_menu);
+ }
+
+ Menu* system_menu() { return m_system_menu; }
+ void set_system_menu(Menu&);
+
+ int theme_index() const { return m_theme_index; }
+
+ Window& window() { return *m_window; }
+
+ template<typename Callback>
+ void for_each_active_menubar_menu(Callback callback)
+ {
+ if (system_menu()) {
+ if (callback(*system_menu()) == IterationDecision::Break)
+ return;
+ }
+ if (m_current_menubar)
+ m_current_menubar->for_each_menu(callback);
+ }
+
+ void did_change_theme();
+
+private:
+ void close_menus(const Vector<Menu*>&);
+
+ const Window& window() const { return *m_window; }
+
+ virtual void event(Core::Event&) override;
+ void handle_mouse_event(MouseEvent&);
+ void handle_menu_mouse_event(Menu&, const MouseEvent&);
+
+ void draw();
+
+ RefPtr<Window> m_window;
+
+ WeakPtr<Menu> m_current_menu;
+ Vector<WeakPtr<Menu>> m_open_menu_stack;
+
+ WeakPtr<Menu> m_system_menu;
+
+ bool m_needs_window_resize { false };
+ bool m_bar_open { false };
+
+ int m_theme_index { 0 };
+
+ WeakPtr<MenuBar> m_current_menubar;
+};
+
+}
diff --git a/Services/WindowServer/Screen.cpp b/Services/WindowServer/Screen.cpp
new file mode 100644
index 0000000000..3f0fbb04fd
--- /dev/null
+++ b/Services/WindowServer/Screen.cpp
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Screen.h"
+#include "Compositor.h"
+#include "Event.h"
+#include "EventLoop.h"
+#include "WindowManager.h"
+#include <Kernel/FB.h>
+#include <Kernel/MousePacket.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+namespace WindowServer {
+
+static Screen* s_the;
+
+Screen& Screen::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+Screen::Screen(unsigned desired_width, unsigned desired_height)
+{
+ ASSERT(!s_the);
+ s_the = this;
+ m_framebuffer_fd = open("/dev/fb0", O_RDWR | O_CLOEXEC);
+ if (m_framebuffer_fd < 0) {
+ perror("failed to open /dev/fb0");
+ ASSERT_NOT_REACHED();
+ }
+
+ if (fb_set_buffer(m_framebuffer_fd, 0) == 0) {
+ m_can_set_buffer = true;
+ }
+
+ set_resolution(desired_width, desired_height);
+ m_cursor_location = rect().center();
+}
+
+Screen::~Screen()
+{
+}
+
+bool Screen::set_resolution(int width, int height)
+{
+ FBResolution resolution { 0, (unsigned)width, (unsigned)height };
+ int rc = fb_set_resolution(m_framebuffer_fd, &resolution);
+#ifdef WSSCREEN_DEBUG
+ dbg() << "fb_set_resolution() - return code " << rc;
+#endif
+ if (rc == 0) {
+ on_change_resolution(resolution.pitch, resolution.width, resolution.height);
+ return true;
+ }
+ if (rc == -1) {
+ dbg() << "Invalid resolution " << width << "x" << height;
+ on_change_resolution(resolution.pitch, resolution.width, resolution.height);
+ return false;
+ }
+ ASSERT_NOT_REACHED();
+}
+
+void Screen::on_change_resolution(int pitch, int width, int height)
+{
+ if (m_framebuffer) {
+ size_t previous_size_in_bytes = m_size_in_bytes;
+ int rc = munmap(m_framebuffer, previous_size_in_bytes);
+ ASSERT(rc == 0);
+ }
+
+ int rc = fb_get_size_in_bytes(m_framebuffer_fd, &m_size_in_bytes);
+ ASSERT(rc == 0);
+
+ m_framebuffer = (Gfx::RGBA32*)mmap(nullptr, m_size_in_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, m_framebuffer_fd, 0);
+ ASSERT(m_framebuffer && m_framebuffer != (void*)-1);
+
+ m_pitch = pitch;
+ m_width = width;
+ m_height = height;
+
+ m_cursor_location.constrain(rect());
+}
+
+void Screen::set_buffer(int index)
+{
+ ASSERT(m_can_set_buffer);
+ int rc = fb_set_buffer(m_framebuffer_fd, index);
+ ASSERT(rc == 0);
+}
+
+void Screen::on_receive_mouse_data(const MousePacket& packet)
+{
+ auto prev_location = m_cursor_location;
+ if (packet.is_relative) {
+ m_cursor_location.move_by(packet.x, packet.y);
+#ifdef WSSCREEN_DEBUG
+ dbgprintf("Screen: New Relative mouse point @ X %d, Y %d\n", m_cursor_location.x(), m_cursor_location.y());
+#endif
+ } else {
+ m_cursor_location = { packet.x * m_width / 0xffff, packet.y * m_height / 0xffff };
+#ifdef WSSCREEN_DEBUG
+ dbgprintf("Screen: New Absolute mouse point @ X %d, Y %d\n", m_cursor_location.x(), m_cursor_location.y());
+#endif
+ }
+
+ m_cursor_location.constrain(rect());
+
+ unsigned buttons = packet.buttons;
+ unsigned prev_buttons = m_mouse_button_state;
+ m_mouse_button_state = buttons;
+ unsigned changed_buttons = prev_buttons ^ buttons;
+ auto post_mousedown_or_mouseup_if_needed = [&](MouseButton button) {
+ if (!(changed_buttons & (unsigned)button))
+ return;
+ auto message = make<MouseEvent>(buttons & (unsigned)button ? Event::MouseDown : Event::MouseUp, m_cursor_location, buttons, button, m_modifiers);
+ Core::EventLoop::current().post_event(WindowManager::the(), move(message));
+ };
+ post_mousedown_or_mouseup_if_needed(MouseButton::Left);
+ post_mousedown_or_mouseup_if_needed(MouseButton::Right);
+ post_mousedown_or_mouseup_if_needed(MouseButton::Middle);
+ post_mousedown_or_mouseup_if_needed(MouseButton::Back);
+ post_mousedown_or_mouseup_if_needed(MouseButton::Forward);
+ if (m_cursor_location != prev_location) {
+ auto message = make<MouseEvent>(Event::MouseMove, m_cursor_location, buttons, MouseButton::None, m_modifiers);
+ Core::EventLoop::current().post_event(WindowManager::the(), move(message));
+ }
+
+ if (packet.z) {
+ auto message = make<MouseEvent>(Event::MouseWheel, m_cursor_location, buttons, MouseButton::None, m_modifiers, packet.z);
+ Core::EventLoop::current().post_event(WindowManager::the(), move(message));
+ }
+
+ if (m_cursor_location != prev_location)
+ Compositor::the().invalidate_cursor();
+}
+
+void Screen::on_receive_keyboard_data(::KeyEvent kernel_event)
+{
+ m_modifiers = kernel_event.modifiers();
+ auto message = make<KeyEvent>(kernel_event.is_press() ? Event::KeyDown : Event::KeyUp, kernel_event.key, kernel_event.character, kernel_event.modifiers());
+ Core::EventLoop::current().post_event(WindowManager::the(), move(message));
+}
+
+}
diff --git a/Services/WindowServer/Screen.h b/Services/WindowServer/Screen.h
new file mode 100644
index 0000000000..d522ea7b96
--- /dev/null
+++ b/Services/WindowServer/Screen.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <Kernel/KeyCode.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Rect.h>
+#include <LibGfx/Size.h>
+
+struct MousePacket;
+
+namespace WindowServer {
+
+class Screen {
+public:
+ Screen(unsigned width, unsigned height);
+ ~Screen();
+
+ bool set_resolution(int width, int height);
+ bool can_set_buffer() { return m_can_set_buffer; }
+ void set_buffer(int index);
+
+ int width() const { return m_width; }
+ int height() const { return m_height; }
+ size_t pitch() const { return m_pitch; }
+ Gfx::RGBA32* scanline(int y);
+
+ static Screen& the();
+
+ Gfx::Size size() const { return { width(), height() }; }
+ Gfx::Rect rect() const { return { 0, 0, width(), height() }; }
+
+ Gfx::Point cursor_location() const { return m_cursor_location; }
+ unsigned mouse_button_state() const { return m_mouse_button_state; }
+
+ void on_receive_mouse_data(const MousePacket&);
+ void on_receive_keyboard_data(::KeyEvent);
+
+private:
+ void on_change_resolution(int pitch, int width, int height);
+
+ size_t m_size_in_bytes;
+
+ Gfx::RGBA32* m_framebuffer { nullptr };
+ bool m_can_set_buffer { false };
+
+ int m_pitch { 0 };
+ int m_width { 0 };
+ int m_height { 0 };
+ int m_framebuffer_fd { -1 };
+
+ Gfx::Point m_cursor_location;
+ unsigned m_mouse_button_state { 0 };
+ unsigned m_modifiers { 0 };
+};
+
+inline Gfx::RGBA32* Screen::scanline(int y)
+{
+ return reinterpret_cast<Gfx::RGBA32*>(((u8*)m_framebuffer) + (y * m_pitch));
+}
+
+}
diff --git a/Services/WindowServer/Window.cpp b/Services/WindowServer/Window.cpp
new file mode 100644
index 0000000000..4d506e1dc7
--- /dev/null
+++ b/Services/WindowServer/Window.cpp
@@ -0,0 +1,524 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Window.h"
+#include "ClientConnection.h"
+#include "Event.h"
+#include "EventLoop.h"
+#include "Screen.h"
+#include "WindowClientEndpoint.h"
+#include "WindowManager.h"
+#include <AK/Badge.h>
+
+namespace WindowServer {
+
+static String default_window_icon_path()
+{
+ return "/res/icons/16x16/window.png";
+}
+
+static Gfx::Bitmap& default_window_icon()
+{
+ static Gfx::Bitmap* s_icon;
+ if (!s_icon)
+ s_icon = Gfx::Bitmap::load_from_file(default_window_icon_path()).leak_ref();
+ return *s_icon;
+}
+
+static Gfx::Bitmap& minimize_icon()
+{
+ static Gfx::Bitmap* s_icon;
+ if (!s_icon)
+ s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-minimize.png").leak_ref();
+ return *s_icon;
+}
+
+static Gfx::Bitmap& maximize_icon()
+{
+ static Gfx::Bitmap* s_icon;
+ if (!s_icon)
+ s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-maximize.png").leak_ref();
+ return *s_icon;
+}
+
+static Gfx::Bitmap& restore_icon()
+{
+ static Gfx::Bitmap* s_icon;
+ if (!s_icon)
+ s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-restore.png").leak_ref();
+ return *s_icon;
+}
+
+static Gfx::Bitmap& close_icon()
+{
+ static Gfx::Bitmap* s_icon;
+ if (!s_icon)
+ s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-close.png").leak_ref();
+ return *s_icon;
+}
+
+Window::Window(Core::Object& parent, WindowType type)
+ : Core::Object(&parent)
+ , m_type(type)
+ , m_icon(default_window_icon())
+ , m_frame(*this)
+{
+ WindowManager::the().add_window(*this);
+}
+
+Window::Window(ClientConnection& client, WindowType window_type, int window_id, bool modal, bool minimizable, bool frameless, bool resizable, bool fullscreen)
+ : Core::Object(&client)
+ , m_client(&client)
+ , m_type(window_type)
+ , m_modal(modal)
+ , m_minimizable(minimizable)
+ , m_frameless(frameless)
+ , m_resizable(resizable)
+ , m_fullscreen(fullscreen)
+ , m_window_id(window_id)
+ , m_client_id(client.client_id())
+ , m_icon(default_window_icon())
+ , m_frame(*this)
+{
+ // FIXME: This should not be hard-coded here.
+ if (m_type == WindowType::Taskbar) {
+ m_wm_event_mask = WMEventMask::WindowStateChanges | WMEventMask::WindowRemovals | WMEventMask::WindowIconChanges;
+ m_listens_to_wm_events = true;
+ }
+
+ WindowManager::the().add_window(*this);
+}
+
+Window::~Window()
+{
+ // Detach from client at the start of teardown since we don't want
+ // to confuse things by trying to send messages to it.
+ m_client = nullptr;
+
+ WindowManager::the().remove_window(*this);
+}
+
+void Window::set_title(const String& title)
+{
+ if (m_title == title)
+ return;
+ m_title = title;
+ WindowManager::the().notify_title_changed(*this);
+}
+
+void Window::set_rect(const Gfx::Rect& rect)
+{
+ ASSERT(!rect.is_empty());
+ Gfx::Rect old_rect;
+ if (m_rect == rect)
+ return;
+ old_rect = m_rect;
+ m_rect = rect;
+ if (!m_client && (!m_backing_store || old_rect.size() != rect.size())) {
+ m_backing_store = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, m_rect.size());
+ }
+ m_frame.notify_window_rect_changed(old_rect, rect);
+}
+
+void Window::set_rect_without_repaint(const Gfx::Rect& rect)
+{
+ ASSERT(!rect.is_empty());
+ if (m_rect == rect)
+ return;
+ auto old_rect = m_rect;
+ m_rect = rect;
+
+ if (old_rect.size() == m_rect.size()) {
+ auto delta = m_rect.location() - old_rect.location();
+ for (auto& child_window : m_child_windows) {
+ if (child_window)
+ child_window->move_by(delta);
+ }
+ }
+
+ m_frame.notify_window_rect_changed(old_rect, rect);
+}
+
+void Window::handle_mouse_event(const MouseEvent& event)
+{
+ set_automatic_cursor_tracking_enabled(event.buttons() != 0);
+
+ switch (event.type()) {
+ case Event::MouseMove:
+ m_client->post_message(Messages::WindowClient::MouseMove(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta(), event.is_drag(), event.drag_data_type()));
+ break;
+ case Event::MouseDown:
+ m_client->post_message(Messages::WindowClient::MouseDown(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta()));
+ break;
+ case Event::MouseDoubleClick:
+ m_client->post_message(Messages::WindowClient::MouseDoubleClick(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta()));
+ break;
+ case Event::MouseUp:
+ m_client->post_message(Messages::WindowClient::MouseUp(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta()));
+ break;
+ case Event::MouseWheel:
+ m_client->post_message(Messages::WindowClient::MouseWheel(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta()));
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void Window::update_menu_item_text(PopupMenuItem item)
+{
+ if (m_window_menu) {
+ m_window_menu->item((int)item).set_text(item == PopupMenuItem::Minimize ? (m_minimized ? "Unminimize" : "Minimize") : (m_maximized ? "Restore" : "Maximize"));
+ m_window_menu->redraw();
+ }
+}
+
+void Window::update_menu_item_enabled(PopupMenuItem item)
+{
+ if (m_window_menu) {
+ m_window_menu->item((int)item).set_enabled(item == PopupMenuItem::Minimize ? m_minimizable : m_resizable);
+ m_window_menu->redraw();
+ }
+}
+
+void Window::set_minimized(bool minimized)
+{
+ if (m_minimized == minimized)
+ return;
+ if (minimized && !m_minimizable)
+ return;
+ if (is_blocked_by_modal_window())
+ return;
+ m_minimized = minimized;
+ update_menu_item_text(PopupMenuItem::Minimize);
+ start_minimize_animation();
+ if (!minimized)
+ request_update({ {}, size() });
+ invalidate();
+ WindowManager::the().notify_minimization_state_changed(*this);
+}
+
+void Window::set_minimizable(bool minimizable)
+{
+ if (m_minimizable == minimizable)
+ return;
+ m_minimizable = minimizable;
+ update_menu_item_enabled(PopupMenuItem::Minimize);
+ // TODO: Hide/show (or alternatively change enabled state of) window minimize button dynamically depending on value of m_minimizable
+}
+
+void Window::set_opacity(float opacity)
+{
+ if (m_opacity == opacity)
+ return;
+ m_opacity = opacity;
+ WindowManager::the().notify_opacity_changed(*this);
+}
+
+void Window::set_occluded(bool occluded)
+{
+ if (m_occluded == occluded)
+ return;
+ m_occluded = occluded;
+ WindowManager::the().notify_occlusion_state_changed(*this);
+}
+
+void Window::set_maximized(bool maximized)
+{
+ if (m_maximized == maximized)
+ return;
+ if (maximized && !is_resizable())
+ return;
+ if (is_blocked_by_modal_window())
+ return;
+ set_tiled(WindowTileType::None);
+ m_maximized = maximized;
+ update_menu_item_text(PopupMenuItem::Maximize);
+ auto old_rect = m_rect;
+ if (maximized) {
+ m_unmaximized_rect = m_rect;
+ set_rect(WindowManager::the().maximized_window_rect(*this));
+ } else {
+ set_rect(m_unmaximized_rect);
+ }
+ m_frame.did_set_maximized({}, maximized);
+ Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect));
+}
+
+void Window::set_resizable(bool resizable)
+{
+ if (m_resizable == resizable)
+ return;
+ m_resizable = resizable;
+ update_menu_item_enabled(PopupMenuItem::Maximize);
+ // TODO: Hide/show (or alternatively change enabled state of) window maximize button dynamically depending on value of is_resizable()
+}
+
+void Window::event(Core::Event& event)
+{
+ if (!m_client) {
+ ASSERT(parent());
+ event.ignore();
+ return;
+ }
+
+ if (is_blocked_by_modal_window())
+ return;
+
+ if (static_cast<Event&>(event).is_mouse_event())
+ return handle_mouse_event(static_cast<const MouseEvent&>(event));
+
+ switch (event.type()) {
+ case Event::WindowEntered:
+ m_client->post_message(Messages::WindowClient::WindowEntered(m_window_id));
+ break;
+ case Event::WindowLeft:
+ m_client->post_message(Messages::WindowClient::WindowLeft(m_window_id));
+ break;
+ case Event::KeyDown:
+ m_client->post_message(
+ Messages::WindowClient::KeyDown(m_window_id,
+ (u8) static_cast<const KeyEvent&>(event).character(),
+ (u32) static_cast<const KeyEvent&>(event).key(),
+ static_cast<const KeyEvent&>(event).modifiers()));
+ break;
+ case Event::KeyUp:
+ m_client->post_message(
+ Messages::WindowClient::KeyUp(m_window_id,
+ (u8) static_cast<const KeyEvent&>(event).character(),
+ (u32) static_cast<const KeyEvent&>(event).key(),
+ static_cast<const KeyEvent&>(event).modifiers()));
+ break;
+ case Event::WindowActivated:
+ m_client->post_message(Messages::WindowClient::WindowActivated(m_window_id));
+ break;
+ case Event::WindowDeactivated:
+ m_client->post_message(Messages::WindowClient::WindowDeactivated(m_window_id));
+ break;
+ case Event::WindowCloseRequest:
+ m_client->post_message(Messages::WindowClient::WindowCloseRequest(m_window_id));
+ break;
+ case Event::WindowResized:
+ m_client->post_message(
+ Messages::WindowClient::WindowResized(
+ m_window_id,
+ static_cast<const ResizeEvent&>(event).old_rect(),
+ static_cast<const ResizeEvent&>(event).rect()));
+ break;
+ default:
+ break;
+ }
+}
+
+void Window::set_global_cursor_tracking_enabled(bool enabled)
+{
+ m_global_cursor_tracking_enabled = enabled;
+}
+
+void Window::set_visible(bool b)
+{
+ if (m_visible == b)
+ return;
+ m_visible = b;
+ invalidate();
+}
+
+void Window::invalidate()
+{
+ WindowManager::the().invalidate(*this);
+}
+
+void Window::invalidate(const Gfx::Rect& rect)
+{
+ WindowManager::the().invalidate(*this, rect);
+}
+
+bool Window::is_active() const
+{
+ return WindowManager::the().active_window() == this;
+}
+
+bool Window::is_blocked_by_modal_window() const
+{
+ bool is_any_modal = false;
+ const Window* next = this;
+ while (!is_any_modal && next) {
+ is_any_modal = next->is_modal();
+ next = next->parent_window();
+ }
+
+ return !is_any_modal && client() && client()->is_showing_modal_window();
+}
+
+void Window::set_default_icon()
+{
+ m_icon = default_window_icon();
+}
+
+void Window::request_update(const Gfx::Rect& rect, bool ignore_occlusion)
+{
+ if (m_pending_paint_rects.is_empty()) {
+ deferred_invoke([this, ignore_occlusion](auto&) {
+ client()->post_paint_message(*this, ignore_occlusion);
+ });
+ }
+ m_pending_paint_rects.add(rect);
+}
+
+void Window::popup_window_menu(const Gfx::Point& position)
+{
+ if (!m_window_menu) {
+ m_window_menu = Menu::construct(nullptr, -1, "(Window Menu)");
+ m_window_menu->set_window_menu_of(*this);
+
+ auto minimize_item = make<MenuItem>(*m_window_menu, 1, m_minimized ? "Unminimize" : "Minimize");
+ m_window_menu_minimize_item = minimize_item.ptr();
+ m_window_menu->add_item(move(minimize_item));
+
+ auto maximize_item = make<MenuItem>(*m_window_menu, 2, m_maximized ? "Restore" : "Maximize");
+ m_window_menu_maximize_item = maximize_item.ptr();
+ m_window_menu->add_item(move(maximize_item));
+
+ m_window_menu->add_item(make<MenuItem>(*m_window_menu, MenuItem::Type::Separator));
+
+ auto close_item = make<MenuItem>(*m_window_menu, 3, "Close");
+ close_item->set_icon(&close_icon());
+ m_window_menu->add_item(move(close_item));
+
+ m_window_menu->item((int)PopupMenuItem::Minimize).set_enabled(m_minimizable);
+ m_window_menu->item((int)PopupMenuItem::Maximize).set_enabled(m_resizable);
+
+ m_window_menu->on_item_activation = [&](auto& item) {
+ switch (item.identifier()) {
+ case 1:
+ set_minimized(!m_minimized);
+ if (!m_minimized)
+ WindowManager::the().move_to_front_and_make_active(*this);
+ break;
+ case 2:
+ set_maximized(!m_maximized);
+ if (m_minimized)
+ set_minimized(false);
+ WindowManager::the().move_to_front_and_make_active(*this);
+ break;
+ case 3:
+ request_close();
+ break;
+ }
+ };
+ }
+ m_window_menu_minimize_item->set_icon(m_minimized ? nullptr : &minimize_icon());
+ m_window_menu_maximize_item->set_icon(m_maximized ? &restore_icon() : &maximize_icon());
+
+ m_window_menu->popup(position);
+}
+
+void Window::request_close()
+{
+ Event close_request(Event::WindowCloseRequest);
+ event(close_request);
+}
+
+void Window::set_fullscreen(bool fullscreen)
+{
+ if (m_fullscreen == fullscreen)
+ return;
+ m_fullscreen = fullscreen;
+ Gfx::Rect new_window_rect = m_rect;
+ if (m_fullscreen) {
+ m_saved_nonfullscreen_rect = m_rect;
+ new_window_rect = Screen::the().rect();
+ } else if (!m_saved_nonfullscreen_rect.is_empty()) {
+ new_window_rect = m_saved_nonfullscreen_rect;
+ }
+ Core::EventLoop::current().post_event(*this, make<ResizeEvent>(m_rect, new_window_rect));
+ set_rect(new_window_rect);
+}
+
+Gfx::Rect Window::tiled_rect(WindowTileType tiled) const
+{
+ int frame_width = (m_frame.rect().width() - m_rect.width()) / 2;
+ switch (tiled) {
+ case WindowTileType::None:
+ return m_untiled_rect;
+ case WindowTileType::Left:
+ return Gfx::Rect(0,
+ WindowManager::the().maximized_window_rect(*this).y(),
+ Screen::the().width() / 2 - frame_width,
+ WindowManager::the().maximized_window_rect(*this).height());
+ case WindowTileType::Right:
+ return Gfx::Rect(Screen::the().width() / 2 + frame_width,
+ WindowManager::the().maximized_window_rect(*this).y(),
+ Screen::the().width() / 2 - frame_width,
+ WindowManager::the().maximized_window_rect(*this).height());
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void Window::set_tiled(WindowTileType tiled)
+{
+ if (m_tiled == tiled)
+ return;
+
+ m_tiled = tiled;
+ auto old_rect = m_rect;
+ if (tiled != WindowTileType::None)
+ m_untiled_rect = m_rect;
+ set_rect(tiled_rect(tiled));
+ Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect));
+}
+
+void Window::detach_client(Badge<ClientConnection>)
+{
+ m_client = nullptr;
+}
+
+void Window::recalculate_rect()
+{
+ if (!is_resizable())
+ return;
+
+ auto old_rect = m_rect;
+ if (m_tiled != WindowTileType::None)
+ set_rect(tiled_rect(m_tiled));
+ else if (is_maximized())
+ set_rect(WindowManager::the().maximized_window_rect(*this));
+ Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect));
+}
+
+void Window::add_child_window(Window& child_window)
+{
+ m_child_windows.append(child_window.make_weak_ptr());
+}
+
+void Window::set_parent_window(Window& parent_window)
+{
+ ASSERT(!m_parent_window);
+ m_parent_window = parent_window.make_weak_ptr();
+ parent_window.add_child_window(*this);
+}
+
+}
diff --git a/Services/WindowServer/Window.h b/Services/WindowServer/Window.h
new file mode 100644
index 0000000000..09856c96dd
--- /dev/null
+++ b/Services/WindowServer/Window.h
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/InlineLinkedList.h>
+#include <AK/String.h>
+#include <AK/WeakPtr.h>
+#include <LibCore/Object.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/DisjointRectSet.h>
+#include <LibGfx/Rect.h>
+#include <WindowServer/WindowFrame.h>
+#include <WindowServer/WindowType.h>
+
+namespace WindowServer {
+
+class ClientConnection;
+class Cursor;
+class Menu;
+class MenuItem;
+class MouseEvent;
+
+enum WMEventMask {
+ WindowRectChanges = 1 << 0,
+ WindowStateChanges = 1 << 1,
+ WindowIconChanges = 1 << 2,
+ WindowRemovals = 1 << 3,
+};
+
+enum class WindowTileType {
+ None = 0,
+ Left,
+ Right,
+};
+
+enum class PopupMenuItem {
+ Minimize = 0,
+ Maximize,
+};
+
+class Window final : public Core::Object
+ , public InlineLinkedListNode<Window> {
+ C_OBJECT(Window)
+public:
+ Window(ClientConnection&, WindowType, int window_id, bool modal, bool minimizable, bool frameless, bool resizable, bool fullscreen);
+ Window(Core::Object&, WindowType);
+ virtual ~Window() override;
+
+ void popup_window_menu(const Gfx::Point&);
+ void request_close();
+
+ unsigned wm_event_mask() const { return m_wm_event_mask; }
+ void set_wm_event_mask(unsigned mask) { m_wm_event_mask = mask; }
+
+ bool is_minimized() const { return m_minimized; }
+ void set_minimized(bool);
+
+ bool is_minimizable() const { return m_minimizable; }
+ void set_minimizable(bool);
+
+ bool is_resizable() const { return m_resizable && !m_fullscreen; }
+ void set_resizable(bool);
+
+ bool is_maximized() const { return m_maximized; }
+ void set_maximized(bool);
+
+ bool is_fullscreen() const { return m_fullscreen; }
+ void set_fullscreen(bool);
+
+ WindowTileType tiled() const { return m_tiled; }
+ void set_tiled(WindowTileType);
+
+ bool is_occluded() const { return m_occluded; }
+ void set_occluded(bool);
+
+ bool is_movable() const
+ {
+ return m_type == WindowType::Normal;
+ }
+
+ WindowFrame& frame() { return m_frame; }
+ const WindowFrame& frame() const { return m_frame; }
+
+ bool is_blocked_by_modal_window() const;
+
+ bool listens_to_wm_events() const { return m_listens_to_wm_events; }
+
+ ClientConnection* client() { return m_client; }
+ const ClientConnection* client() const { return m_client; }
+
+ WindowType type() const { return m_type; }
+ int window_id() const { return m_window_id; }
+
+ bool is_internal() const { return m_client_id == -1; }
+ i32 client_id() const { return m_client_id; }
+
+ String title() const { return m_title; }
+ void set_title(const String&);
+
+ float opacity() const { return m_opacity; }
+ void set_opacity(float);
+
+ int x() const { return m_rect.x(); }
+ int y() const { return m_rect.y(); }
+ int width() const { return m_rect.width(); }
+ int height() const { return m_rect.height(); }
+
+ bool is_active() const;
+
+ bool is_visible() const { return m_visible; }
+ void set_visible(bool);
+
+ bool is_modal() const { return m_modal; }
+
+ Gfx::Rect rect() const { return m_rect; }
+ void set_rect(const Gfx::Rect&);
+ void set_rect(int x, int y, int width, int height) { set_rect({ x, y, width, height }); }
+ void set_rect_without_repaint(const Gfx::Rect&);
+
+ void set_taskbar_rect(const Gfx::Rect& rect) { m_taskbar_rect = rect; }
+ const Gfx::Rect& taskbar_rect() const { return m_taskbar_rect; }
+
+ void move_to(const Gfx::Point& position) { set_rect({ position, size() }); }
+ void move_to(int x, int y) { move_to({ x, y }); }
+
+ void move_by(const Gfx::Point& delta) { set_position_without_repaint(position().translated(delta)); }
+
+ Gfx::Point position() const { return m_rect.location(); }
+ void set_position(const Gfx::Point& position) { set_rect({ position.x(), position.y(), width(), height() }); }
+ void set_position_without_repaint(const Gfx::Point& position) { set_rect_without_repaint({ position.x(), position.y(), width(), height() }); }
+
+ Gfx::Size size() const { return m_rect.size(); }
+
+ void invalidate();
+ void invalidate(const Gfx::Rect&);
+
+ virtual void event(Core::Event&) override;
+
+ // Only used by WindowType::MenuApplet. Perhaps it could be a Window subclass? I don't know.
+ void set_rect_in_menubar(const Gfx::Rect& rect) { m_rect_in_menubar = rect; }
+ const Gfx::Rect& rect_in_menubar() const { return m_rect_in_menubar; }
+
+ const Gfx::Bitmap* backing_store() const { return m_backing_store.ptr(); }
+ Gfx::Bitmap* backing_store() { return m_backing_store.ptr(); }
+
+ void set_backing_store(RefPtr<Gfx::Bitmap>&& backing_store)
+ {
+ m_last_backing_store = move(m_backing_store);
+ m_backing_store = move(backing_store);
+ }
+
+ void swap_backing_stores()
+ {
+ swap(m_backing_store, m_last_backing_store);
+ }
+
+ Gfx::Bitmap* last_backing_store() { return m_last_backing_store.ptr(); }
+
+ void set_global_cursor_tracking_enabled(bool);
+ void set_automatic_cursor_tracking_enabled(bool enabled) { m_automatic_cursor_tracking_enabled = enabled; }
+ bool global_cursor_tracking() const { return m_global_cursor_tracking_enabled || m_automatic_cursor_tracking_enabled; }
+
+ bool has_alpha_channel() const { return m_has_alpha_channel; }
+ void set_has_alpha_channel(bool value) { m_has_alpha_channel = value; }
+
+ Gfx::Size size_increment() const { return m_size_increment; }
+ void set_size_increment(const Gfx::Size& increment) { m_size_increment = increment; }
+
+ Gfx::Size base_size() const { return m_base_size; }
+ void set_base_size(const Gfx::Size& size) { m_base_size = size; }
+
+ const Gfx::Bitmap& icon() const { return *m_icon; }
+ void set_icon(NonnullRefPtr<Gfx::Bitmap>&& icon) { m_icon = move(icon); }
+
+ void set_default_icon();
+
+ const Cursor* override_cursor() const { return m_override_cursor.ptr(); }
+ void set_override_cursor(RefPtr<Cursor>&& cursor) { m_override_cursor = move(cursor); }
+
+ void request_update(const Gfx::Rect&, bool ignore_occlusion = false);
+ Gfx::DisjointRectSet take_pending_paint_rects() { return move(m_pending_paint_rects); }
+
+ bool in_minimize_animation() const { return m_minimize_animation_step != -1; }
+
+ int minimize_animation_index() const { return m_minimize_animation_step; }
+ void step_minimize_animation() { m_minimize_animation_step += 1; }
+ void start_minimize_animation() { m_minimize_animation_step = 0; }
+ void end_minimize_animation() { m_minimize_animation_step = -1; }
+
+ Gfx::Rect tiled_rect(WindowTileType) const;
+ void recalculate_rect();
+
+ // For InlineLinkedList.
+ // FIXME: Maybe make a ListHashSet and then WindowManager can just use that.
+ Window* m_next { nullptr };
+ Window* m_prev { nullptr };
+
+ void detach_client(Badge<ClientConnection>);
+
+ Window* parent_window() { return m_parent_window; }
+ const Window* parent_window() const { return m_parent_window; }
+
+ void set_parent_window(Window&);
+
+ Vector<WeakPtr<Window>>& child_windows() { return m_child_windows; }
+ const Vector<WeakPtr<Window>>& child_windows() const { return m_child_windows; }
+
+ void set_frameless(bool frameless) { m_frameless = frameless; }
+ bool is_frameless() const { return m_frameless; }
+
+private:
+ void handle_mouse_event(const MouseEvent&);
+ void update_menu_item_text(PopupMenuItem item);
+ void update_menu_item_enabled(PopupMenuItem item);
+ void add_child_window(Window&);
+
+ ClientConnection* m_client { nullptr };
+
+ WeakPtr<Window> m_parent_window;
+ Vector<WeakPtr<Window>> m_child_windows;
+
+ String m_title;
+ Gfx::Rect m_rect;
+ Gfx::Rect m_saved_nonfullscreen_rect;
+ Gfx::Rect m_taskbar_rect;
+ WindowType m_type { WindowType::Normal };
+ bool m_global_cursor_tracking_enabled { false };
+ bool m_automatic_cursor_tracking_enabled { false };
+ bool m_visible { true };
+ bool m_has_alpha_channel { false };
+ bool m_modal { false };
+ bool m_minimizable { false };
+ bool m_frameless { false };
+ bool m_resizable { false };
+ bool m_listens_to_wm_events { false };
+ bool m_minimized { false };
+ bool m_maximized { false };
+ bool m_fullscreen { false };
+ WindowTileType m_tiled { WindowTileType::None };
+ Gfx::Rect m_untiled_rect;
+ bool m_occluded { false };
+ RefPtr<Gfx::Bitmap> m_backing_store;
+ RefPtr<Gfx::Bitmap> m_last_backing_store;
+ int m_window_id { -1 };
+ i32 m_client_id { -1 };
+ float m_opacity { 1 };
+ Gfx::Size m_size_increment;
+ Gfx::Size m_base_size;
+ NonnullRefPtr<Gfx::Bitmap> m_icon;
+ RefPtr<Cursor> m_override_cursor;
+ WindowFrame m_frame;
+ unsigned m_wm_event_mask { 0 };
+ Gfx::DisjointRectSet m_pending_paint_rects;
+ Gfx::Rect m_unmaximized_rect;
+ Gfx::Rect m_rect_in_menubar;
+ RefPtr<Menu> m_window_menu;
+ MenuItem* m_window_menu_minimize_item { nullptr };
+ MenuItem* m_window_menu_maximize_item { nullptr };
+ int m_minimize_animation_step { -1 };
+};
+
+}
diff --git a/Services/WindowServer/WindowClient.ipc b/Services/WindowServer/WindowClient.ipc
new file mode 100644
index 0000000000..3c94ccf89e
--- /dev/null
+++ b/Services/WindowServer/WindowClient.ipc
@@ -0,0 +1,40 @@
+endpoint WindowClient = 4
+{
+ Paint(i32 window_id, Gfx::Size window_size, Vector<Gfx::Rect> rects) =|
+ MouseMove(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta, bool is_drag, String drag_data_type) =|
+ MouseDown(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =|
+ MouseDoubleClick(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =|
+ MouseUp(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =|
+ MouseWheel(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =|
+ WindowEntered(i32 window_id) =|
+ WindowLeft(i32 window_id) =|
+ KeyDown(i32 window_id, u8 character, u32 key, u32 modifiers) =|
+ KeyUp(i32 window_id, u8 character, u32 key, u32 modifiers) =|
+ WindowActivated(i32 window_id) =|
+ WindowDeactivated(i32 window_id) =|
+ WindowStateChanged(i32 window_id, bool minimized, bool occluded) =|
+ WindowCloseRequest(i32 window_id) =|
+ WindowResized(i32 window_id, Gfx::Rect old_rect, Gfx::Rect new_rect) =|
+
+ MenuItemActivated(i32 menu_id, i32 identifier) =|
+
+ ScreenRectChanged(Gfx::Rect rect) =|
+
+ ClipboardContentsChanged(String content_type) =|
+
+ WM_WindowRemoved(i32 wm_id, i32 client_id, i32 window_id) =|
+ WM_WindowStateChanged(i32 wm_id, i32 client_id, i32 window_id, bool is_active, bool is_minimized, bool is_frameless, i32 window_type, String title, Gfx::Rect rect) =|
+ WM_WindowIconBitmapChanged(i32 wm_id, i32 client_id, i32 window_id, i32 icon_buffer_id, Gfx::Size icon_size) =|
+ WM_WindowRectChanged(i32 wm_id, i32 client_id, i32 window_id, Gfx::Rect rect) =|
+
+ AsyncSetWallpaperFinished(bool success) =|
+
+ DragAccepted() =|
+ DragCancelled() =|
+
+ DragDropped(i32 window_id, Gfx::Point mouse_position, String text, String data_type, String data) =|
+
+ UpdateSystemTheme(i32 system_theme_buffer_id) =|
+
+ DisplayLinkNotification() =|
+}
diff --git a/Services/WindowServer/WindowFrame.cpp b/Services/WindowServer/WindowFrame.cpp
new file mode 100644
index 0000000000..2d6b8cad73
--- /dev/null
+++ b/Services/WindowServer/WindowFrame.cpp
@@ -0,0 +1,414 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Badge.h>
+#include <LibGfx/CharacterBitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Painter.h>
+#include <LibGfx/StylePainter.h>
+#include <WindowServer/Button.h>
+#include <WindowServer/Compositor.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/Window.h>
+#include <WindowServer/WindowFrame.h>
+#include <WindowServer/WindowManager.h>
+
+namespace WindowServer {
+
+static const int window_titlebar_height = 19;
+
+static const char* s_close_button_bitmap_data = {
+ "## ##"
+ "### ###"
+ " ###### "
+ " #### "
+ " #### "
+ " ###### "
+ "### ###"
+ "## ##"
+ " "
+};
+
+static Gfx::CharacterBitmap* s_close_button_bitmap;
+static const int s_close_button_bitmap_width = 8;
+static const int s_close_button_bitmap_height = 9;
+
+static const char* s_minimize_button_bitmap_data = {
+ " "
+ " "
+ " "
+ " ###### "
+ " #### "
+ " ## "
+ " "
+ " "
+ " "
+};
+
+static Gfx::CharacterBitmap* s_minimize_button_bitmap;
+static const int s_minimize_button_bitmap_width = 8;
+static const int s_minimize_button_bitmap_height = 9;
+
+static const char* s_maximize_button_bitmap_data = {
+ " "
+ " "
+ " "
+ " ## "
+ " #### "
+ " ###### "
+ " "
+ " "
+ " "
+};
+
+static Gfx::CharacterBitmap* s_maximize_button_bitmap;
+static const int s_maximize_button_bitmap_width = 8;
+static const int s_maximize_button_bitmap_height = 9;
+
+static const char* s_unmaximize_button_bitmap_data = {
+ " "
+ " ## "
+ " #### "
+ " ###### "
+ " "
+ " ###### "
+ " #### "
+ " ## "
+ " "
+};
+
+static Gfx::CharacterBitmap* s_unmaximize_button_bitmap;
+static const int s_unmaximize_button_bitmap_width = 8;
+static const int s_unmaximize_button_bitmap_height = 9;
+
+WindowFrame::WindowFrame(Window& window)
+ : m_window(window)
+{
+ if (!s_close_button_bitmap)
+ s_close_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_close_button_bitmap_data, s_close_button_bitmap_width, s_close_button_bitmap_height).leak_ref();
+
+ if (!s_minimize_button_bitmap)
+ s_minimize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_minimize_button_bitmap_data, s_minimize_button_bitmap_width, s_minimize_button_bitmap_height).leak_ref();
+
+ if (!s_maximize_button_bitmap)
+ s_maximize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_maximize_button_bitmap_data, s_maximize_button_bitmap_width, s_maximize_button_bitmap_height).leak_ref();
+
+ if (!s_unmaximize_button_bitmap)
+ s_unmaximize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_unmaximize_button_bitmap_data, s_unmaximize_button_bitmap_width, s_unmaximize_button_bitmap_height).leak_ref();
+
+ m_buttons.append(make<Button>(*this, *s_close_button_bitmap, [this](auto&) {
+ m_window.request_close();
+ }));
+
+ if (window.is_resizable()) {
+ auto button = make<Button>(*this, *s_maximize_button_bitmap, [this](auto&) {
+ m_window.set_maximized(!m_window.is_maximized());
+ });
+ m_maximize_button = button.ptr();
+ m_buttons.append(move(button));
+ }
+
+ if (window.is_minimizable()) {
+ auto button = make<Button>(*this, *s_minimize_button_bitmap, [this](auto&) {
+ m_window.set_minimized(true);
+ });
+ m_minimize_button = button.ptr();
+ m_buttons.append(move(button));
+ }
+}
+
+WindowFrame::~WindowFrame()
+{
+}
+
+void WindowFrame::did_set_maximized(Badge<Window>, bool maximized)
+{
+ ASSERT(m_maximize_button);
+ m_maximize_button->set_bitmap(maximized ? *s_unmaximize_button_bitmap : *s_maximize_button_bitmap);
+}
+
+Gfx::Rect WindowFrame::title_bar_rect() const
+{
+ if (m_window.type() == WindowType::Notification)
+ return { m_window.width() + 3, 3, window_titlebar_height, m_window.height() };
+ return { 4, 4, m_window.width(), window_titlebar_height };
+}
+
+Gfx::Rect WindowFrame::title_bar_icon_rect() const
+{
+ auto titlebar_rect = title_bar_rect();
+ return {
+ titlebar_rect.x() + 1,
+ titlebar_rect.y() + 2,
+ 16,
+ titlebar_rect.height(),
+ };
+}
+
+Gfx::Rect WindowFrame::title_bar_text_rect() const
+{
+ auto titlebar_rect = title_bar_rect();
+ auto titlebar_icon_rect = title_bar_icon_rect();
+ return {
+ titlebar_rect.x() + 2 + titlebar_icon_rect.width() + 2,
+ titlebar_rect.y(),
+ titlebar_rect.width() - 4 - titlebar_icon_rect.width() - 2,
+ titlebar_rect.height()
+ };
+}
+
+WindowFrame::FrameColors WindowFrame::compute_frame_colors() const
+{
+ auto& wm = WindowManager::the();
+ auto palette = wm.palette();
+ if (&m_window == wm.m_highlight_window)
+ return { palette.highlight_window_title(), palette.highlight_window_border1(), palette.highlight_window_border2() };
+ if (&m_window == wm.m_move_window)
+ return { palette.moving_window_title(), palette.moving_window_border1(), palette.moving_window_border2() };
+ if (&m_window == wm.m_active_window)
+ return { palette.active_window_title(), palette.active_window_border1(), palette.active_window_border2() };
+ return { palette.inactive_window_title(), palette.inactive_window_border1(), palette.inactive_window_border2() };
+}
+
+void WindowFrame::paint_notification_frame(Gfx::Painter& painter)
+{
+ auto palette = WindowManager::the().palette();
+ Gfx::Rect outer_rect = { {}, rect().size() };
+
+ Gfx::StylePainter::paint_window_frame(painter, outer_rect, palette);
+
+ auto titlebar_rect = title_bar_rect();
+ painter.fill_rect_with_gradient(Gfx::Orientation::Vertical, titlebar_rect, palette.active_window_border1(), palette.active_window_border2());
+
+ int stripe_top = m_buttons.last().relative_rect().bottom() + 4;
+ int stripe_bottom = m_window.height() - 3;
+ if (stripe_top && stripe_bottom && stripe_top < stripe_bottom) {
+ for (int i = 2; i <= window_titlebar_height - 2; i += 2) {
+ painter.draw_line({ titlebar_rect.x() + i, stripe_top }, { titlebar_rect.x() + i, stripe_bottom }, palette.active_window_border1());
+ }
+ }
+}
+
+void WindowFrame::paint_normal_frame(Gfx::Painter& painter)
+{
+ auto palette = WindowManager::the().palette();
+ auto& window = m_window;
+ Gfx::Rect outer_rect = { {}, rect().size() };
+
+ Gfx::StylePainter::paint_window_frame(painter, outer_rect, palette);
+
+ auto titlebar_rect = title_bar_rect();
+ auto titlebar_icon_rect = title_bar_icon_rect();
+ auto titlebar_inner_rect = title_bar_text_rect();
+ auto titlebar_title_rect = titlebar_inner_rect;
+ titlebar_title_rect.set_width(Gfx::Font::default_bold_font().width(window.title()));
+
+ auto [title_color, border_color, border_color2] = compute_frame_colors();
+
+ auto& wm = WindowManager::the();
+ painter.draw_line(titlebar_rect.bottom_left().translated(0, 1), titlebar_rect.bottom_right().translated(0, 1), palette.button());
+ painter.draw_line(titlebar_rect.bottom_left().translated(0, 2), titlebar_rect.bottom_right().translated(0, 2), palette.button());
+
+ auto leftmost_button_rect = m_buttons.is_empty() ? Gfx::Rect() : m_buttons.last().relative_rect();
+
+ painter.fill_rect_with_gradient(titlebar_rect, border_color, border_color2);
+
+ int stripe_left = titlebar_title_rect.right() + 4;
+ int stripe_right = leftmost_button_rect.left() - 3;
+ if (stripe_left && stripe_right && stripe_left < stripe_right) {
+ for (int i = 2; i <= titlebar_inner_rect.height() - 2; i += 2) {
+ painter.draw_line({ stripe_left, titlebar_inner_rect.y() + i }, { stripe_right, titlebar_inner_rect.y() + i }, border_color);
+ }
+ }
+
+ auto clipped_title_rect = titlebar_title_rect;
+ clipped_title_rect.set_width(stripe_right - clipped_title_rect.x());
+ if (!clipped_title_rect.is_empty()) {
+ painter.draw_text(clipped_title_rect.translated(1, 2), window.title(), wm.window_title_font(), Gfx::TextAlignment::CenterLeft, border_color.darkened(0.4), Gfx::TextElision::Right);
+ // FIXME: The translated(0, 1) wouldn't be necessary if we could center text based on its baseline.
+ painter.draw_text(clipped_title_rect.translated(0, 1), window.title(), wm.window_title_font(), Gfx::TextAlignment::CenterLeft, title_color, Gfx::TextElision::Right);
+ }
+
+ painter.blit(titlebar_icon_rect.location(), window.icon(), window.icon().rect());
+}
+
+void WindowFrame::paint(Gfx::Painter& painter)
+{
+ if (m_window.is_frameless())
+ return;
+
+ Gfx::PainterStateSaver saver(painter);
+ painter.translate(rect().location());
+
+ if (m_window.type() == WindowType::Notification)
+ paint_notification_frame(painter);
+ else if (m_window.type() == WindowType::Normal)
+ paint_normal_frame(painter);
+ else
+ return;
+
+ for (auto& button : m_buttons) {
+ button.paint(painter);
+ }
+}
+
+static Gfx::Rect frame_rect_for_window(Window& window, const Gfx::Rect& rect)
+{
+ if (window.is_frameless())
+ return rect;
+
+ auto type = window.type();
+
+ switch (type) {
+ case WindowType::Normal:
+ return {
+ rect.x() - 4,
+ rect.y() - window_titlebar_height - 6,
+ rect.width() + 8,
+ rect.height() + 10 + window_titlebar_height
+ };
+ case WindowType::Notification:
+ return {
+ rect.x() - 3,
+ rect.y() - 3,
+ rect.width() + 6 + window_titlebar_height,
+ rect.height() + 6
+ };
+ default:
+ return rect;
+ }
+}
+
+static Gfx::Rect frame_rect_for_window(Window& window)
+{
+ return frame_rect_for_window(window, window.rect());
+}
+
+Gfx::Rect WindowFrame::rect() const
+{
+ return frame_rect_for_window(m_window);
+}
+
+void WindowFrame::invalidate_title_bar()
+{
+ WindowManager::the().invalidate(title_bar_rect().translated(rect().location()));
+}
+
+void WindowFrame::notify_window_rect_changed(const Gfx::Rect& old_rect, const Gfx::Rect& new_rect)
+{
+ int window_button_width = 15;
+ int window_button_height = 15;
+ int pos;
+ if (m_window.type() == WindowType::Notification)
+ pos = title_bar_rect().top() + 2;
+ else
+ pos = title_bar_text_rect().right() + 1;
+
+ for (auto& button : m_buttons) {
+ if (m_window.type() == WindowType::Notification) {
+ Gfx::Rect rect { 0, pos, window_button_width, window_button_height };
+ rect.center_horizontally_within(title_bar_rect());
+ button.set_relative_rect(rect);
+ pos += window_button_width;
+ } else {
+ pos -= window_button_width;
+ Gfx::Rect rect { pos, 0, window_button_width, window_button_height };
+ rect.center_vertically_within(title_bar_text_rect());
+ button.set_relative_rect(rect);
+ }
+ }
+
+ auto& wm = WindowManager::the();
+ wm.invalidate(frame_rect_for_window(m_window, old_rect));
+ wm.invalidate(frame_rect_for_window(m_window, new_rect));
+ wm.notify_rect_changed(m_window, old_rect, new_rect);
+}
+
+void WindowFrame::on_mouse_event(const MouseEvent& event)
+{
+ ASSERT(!m_window.is_fullscreen());
+
+ if (m_window.is_blocked_by_modal_window())
+ return;
+
+ auto& wm = WindowManager::the();
+ if (m_window.type() != WindowType::Normal && m_window.type() != WindowType::Notification)
+ return;
+
+ if (m_window.type() == WindowType::Normal && event.type() == Event::MouseDown && (event.button() == MouseButton::Left || event.button() == MouseButton::Right) && title_bar_icon_rect().contains(event.position())) {
+ wm.move_to_front_and_make_active(m_window);
+ m_window.popup_window_menu(title_bar_rect().bottom_left().translated(rect().location()));
+ return;
+ }
+
+ // This is slightly hackish, but expand the title bar rect by two pixels downwards,
+ // so that mouse events between the title bar and window contents don't act like
+ // mouse events on the border.
+ auto adjusted_title_bar_rect = title_bar_rect();
+ adjusted_title_bar_rect.set_height(adjusted_title_bar_rect.height() + 2);
+
+ if (adjusted_title_bar_rect.contains(event.position())) {
+ wm.clear_resize_candidate();
+
+ if (event.type() == Event::MouseDown)
+ wm.move_to_front_and_make_active(m_window);
+
+ for (auto& button : m_buttons) {
+ if (button.relative_rect().contains(event.position()))
+ return button.on_mouse_event(event.translated(-button.relative_rect().location()));
+ }
+ if (event.type() == Event::MouseDown) {
+ if (m_window.type() == WindowType::Normal && event.button() == MouseButton::Right) {
+ m_window.popup_window_menu(event.position().translated(rect().location()));
+ return;
+ }
+ if (m_window.is_movable() && event.button() == MouseButton::Left)
+ wm.start_window_move(m_window, event.translated(rect().location()));
+ }
+ return;
+ }
+
+ if (m_window.is_resizable() && event.type() == Event::MouseMove && event.buttons() == 0) {
+ constexpr ResizeDirection direction_for_hot_area[3][3] = {
+ { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
+ { ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right },
+ { ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight },
+ };
+ Gfx::Rect outer_rect = { {}, rect().size() };
+ ASSERT(outer_rect.contains(event.position()));
+ int window_relative_x = event.x() - outer_rect.x();
+ int window_relative_y = event.y() - outer_rect.y();
+ int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3));
+ int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3));
+ wm.set_resize_candidate(m_window, direction_for_hot_area[hot_area_row][hot_area_column]);
+ Compositor::the().invalidate_cursor();
+ return;
+ }
+
+ if (m_window.is_resizable() && event.type() == Event::MouseDown && event.button() == MouseButton::Left)
+ wm.start_window_resize(m_window, event.translated(rect().location()));
+}
+}
diff --git a/Services/WindowServer/WindowFrame.h b/Services/WindowServer/WindowFrame.h
new file mode 100644
index 0000000000..021edb9d0f
--- /dev/null
+++ b/Services/WindowServer/WindowFrame.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Forward.h>
+#include <AK/NonnullOwnPtrVector.h>
+#include <LibGfx/Forward.h>
+
+namespace WindowServer {
+
+class Button;
+class MouseEvent;
+class Window;
+
+class WindowFrame {
+public:
+ WindowFrame(Window&);
+ ~WindowFrame();
+
+ Gfx::Rect rect() const;
+ void paint(Gfx::Painter&);
+ void on_mouse_event(const MouseEvent&);
+ void notify_window_rect_changed(const Gfx::Rect& old_rect, const Gfx::Rect& new_rect);
+ void invalidate_title_bar();
+
+ Gfx::Rect title_bar_rect() const;
+ Gfx::Rect title_bar_icon_rect() const;
+ Gfx::Rect title_bar_text_rect() const;
+
+ void did_set_maximized(Badge<Window>, bool);
+
+private:
+ void paint_notification_frame(Gfx::Painter&);
+ void paint_normal_frame(Gfx::Painter&);
+
+ struct FrameColors {
+ Color title_color;
+ Color border_color;
+ Color border_color2;
+ };
+
+ FrameColors compute_frame_colors() const;
+
+ Window& m_window;
+ NonnullOwnPtrVector<Button> m_buttons;
+ Button* m_maximize_button { nullptr };
+ Button* m_minimize_button { nullptr };
+};
+
+}
diff --git a/Services/WindowServer/WindowManager.cpp b/Services/WindowServer/WindowManager.cpp
new file mode 100644
index 0000000000..2b168107d1
--- /dev/null
+++ b/Services/WindowServer/WindowManager.cpp
@@ -0,0 +1,1317 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "WindowManager.h"
+#include "Compositor.h"
+#include "EventLoop.h"
+#include "Menu.h"
+#include "MenuBar.h"
+#include "MenuItem.h"
+#include "Screen.h"
+#include "Window.h"
+#include <AK/LogStream.h>
+#include <AK/SharedBuffer.h>
+#include <AK/StdLibExtras.h>
+#include <AK/Vector.h>
+#include <LibGfx/CharacterBitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Painter.h>
+#include <LibGfx/StylePainter.h>
+#include <LibGfx/SystemTheme.h>
+#include <WindowServer/AppletManager.h>
+#include <WindowServer/Button.h>
+#include <WindowServer/ClientConnection.h>
+#include <WindowServer/Cursor.h>
+#include <WindowServer/WindowClientEndpoint.h>
+#include <errno.h>
+#include <serenity.h>
+#include <stdio.h>
+#include <time.h>
+#include <unistd.h>
+
+//#define WINDOWMANAGER_DEBUG
+//#define RESIZE_DEBUG
+//#define MOVE_DEBUG
+//#define DOUBLECLICK_DEBUG
+
+namespace WindowServer {
+
+static WindowManager* s_the;
+
+WindowManager& WindowManager::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+WindowManager::WindowManager(const Gfx::PaletteImpl& palette)
+ : m_palette(palette)
+{
+ s_the = this;
+
+ reload_config(false);
+
+ invalidate();
+ Compositor::the().compose();
+}
+
+WindowManager::~WindowManager()
+{
+}
+
+NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name, const Gfx::Point& hotspot)
+{
+ auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
+ auto gb = Gfx::Bitmap::load_from_file(path);
+ if (gb)
+ return Cursor::create(*gb, hotspot);
+ return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png"));
+}
+
+NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name)
+{
+ auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
+ auto gb = Gfx::Bitmap::load_from_file(path);
+
+ if (gb)
+ return Cursor::create(*gb);
+ return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png"));
+}
+
+void WindowManager::reload_config(bool set_screen)
+{
+ m_wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini");
+
+ m_double_click_speed = m_wm_config->read_num_entry("Input", "DoubleClickSpeed", 250);
+
+ if (set_screen) {
+ set_resolution(m_wm_config->read_num_entry("Screen", "Width", 1920), m_wm_config->read_num_entry("Screen", "Height", 1080));
+ }
+
+ m_arrow_cursor = get_cursor("Arrow", { 2, 2 });
+ m_hand_cursor = get_cursor("Hand", { 8, 4 });
+ m_resize_horizontally_cursor = get_cursor("ResizeH");
+ m_resize_vertically_cursor = get_cursor("ResizeV");
+ m_resize_diagonally_tlbr_cursor = get_cursor("ResizeDTLBR");
+ m_resize_diagonally_bltr_cursor = get_cursor("ResizeDBLTR");
+ m_i_beam_cursor = get_cursor("IBeam");
+ m_disallowed_cursor = get_cursor("Disallowed");
+ m_move_cursor = get_cursor("Move");
+ m_drag_cursor = get_cursor("Drag");
+}
+
+const Gfx::Font& WindowManager::font() const
+{
+ return Gfx::Font::default_font();
+}
+
+const Gfx::Font& WindowManager::window_title_font() const
+{
+ return Gfx::Font::default_bold_font();
+}
+
+bool WindowManager::set_resolution(int width, int height)
+{
+ bool success = Compositor::the().set_resolution(width, height);
+ MenuManager::the().set_needs_window_resize();
+ ClientConnection::for_each_client([&](ClientConnection& client) {
+ client.notify_about_new_screen_rect(Screen::the().rect());
+ });
+ if (success) {
+ for_each_window([](Window& window) {
+ if (window.type() == WindowType::Desktop)
+ window.set_rect(WindowManager::the().desktop_rect());
+ window.recalculate_rect();
+ return IterationDecision::Continue;
+ });
+ }
+ if (m_wm_config) {
+ if (success) {
+ dbg() << "Saving resolution: " << Gfx::Size(width, height) << " to config file at " << m_wm_config->file_name();
+ m_wm_config->write_num_entry("Screen", "Width", width);
+ m_wm_config->write_num_entry("Screen", "Height", height);
+ m_wm_config->sync();
+ } else {
+ dbg() << "Saving fallback resolution: " << resolution() << " to config file at " << m_wm_config->file_name();
+ m_wm_config->write_num_entry("Screen", "Width", resolution().width());
+ m_wm_config->write_num_entry("Screen", "Height", resolution().height());
+ m_wm_config->sync();
+ }
+ }
+ return success;
+}
+
+Gfx::Size WindowManager::resolution() const
+{
+ return Screen::the().size();
+}
+
+void WindowManager::add_window(Window& window)
+{
+ bool is_first_window = m_windows_in_order.is_empty();
+
+ m_windows_in_order.append(&window);
+
+ if (window.is_fullscreen()) {
+ Core::EventLoop::current().post_event(window, make<ResizeEvent>(window.rect(), Screen::the().rect()));
+ window.set_rect(Screen::the().rect());
+ }
+
+ if (window.type() != WindowType::Desktop || is_first_window)
+ set_active_window(&window);
+
+ if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher)
+ m_switcher.refresh();
+
+ recompute_occlusions();
+
+ if (window.listens_to_wm_events()) {
+ for_each_window([&](Window& other_window) {
+ if (&window != &other_window) {
+ tell_wm_listener_about_window(window, other_window);
+ tell_wm_listener_about_window_icon(window, other_window);
+ }
+ return IterationDecision::Continue;
+ });
+ }
+
+ tell_wm_listeners_window_state_changed(window);
+}
+
+void WindowManager::move_to_front_and_make_active(Window& window)
+{
+ if (window.is_blocked_by_modal_window())
+ return;
+
+ if (m_windows_in_order.tail() != &window)
+ invalidate(window);
+ m_windows_in_order.remove(&window);
+ m_windows_in_order.append(&window);
+
+ recompute_occlusions();
+
+ set_active_window(&window);
+
+ if (m_switcher.is_visible()) {
+ m_switcher.refresh();
+ m_switcher.select_window(window);
+ set_highlight_window(&window);
+ }
+
+ for (auto& child_window : window.child_windows()) {
+ if (child_window)
+ move_to_front_and_make_active(*child_window);
+ }
+}
+
+void WindowManager::remove_window(Window& window)
+{
+ invalidate(window);
+ m_windows_in_order.remove(&window);
+ if (window.is_active())
+ pick_new_active_window();
+ if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher)
+ m_switcher.refresh();
+
+ recompute_occlusions();
+
+ for_each_window_listening_to_wm_events([&window](Window& listener) {
+ if (!(listener.wm_event_mask() & WMEventMask::WindowRemovals))
+ return IterationDecision::Continue;
+ if (!window.is_internal())
+ listener.client()->post_message(Messages::WindowClient::WM_WindowRemoved(listener.window_id(), window.client_id(), window.window_id()));
+ return IterationDecision::Continue;
+ });
+}
+
+void WindowManager::tell_wm_listener_about_window(Window& listener, Window& window)
+{
+ if (!(listener.wm_event_mask() & WMEventMask::WindowStateChanges))
+ return;
+ if (window.is_internal())
+ return;
+ listener.client()->post_message(Messages::WindowClient::WM_WindowStateChanged(listener.window_id(), window.client_id(), window.window_id(), window.is_active(), window.is_minimized(), window.is_frameless(), (i32)window.type(), window.title(), window.rect()));
+}
+
+void WindowManager::tell_wm_listener_about_window_rect(Window& listener, Window& window)
+{
+ if (!(listener.wm_event_mask() & WMEventMask::WindowRectChanges))
+ return;
+ if (window.is_internal())
+ return;
+ listener.client()->post_message(Messages::WindowClient::WM_WindowRectChanged(listener.window_id(), window.client_id(), window.window_id(), window.rect()));
+}
+
+void WindowManager::tell_wm_listener_about_window_icon(Window& listener, Window& window)
+{
+ if (!(listener.wm_event_mask() & WMEventMask::WindowIconChanges))
+ return;
+ if (window.is_internal())
+ return;
+ if (window.icon().shbuf_id() == -1)
+ return;
+#ifdef WINDOWMANAGER_DEBUG
+ dbg() << "WindowServer: Sharing icon buffer " << window.icon().shbuf_id() << " with PID " << listener.client()->client_pid();
+#endif
+ if (shbuf_allow_pid(window.icon().shbuf_id(), listener.client()->client_pid()) < 0) {
+ ASSERT_NOT_REACHED();
+ }
+ listener.client()->post_message(Messages::WindowClient::WM_WindowIconBitmapChanged(listener.window_id(), window.client_id(), window.window_id(), window.icon().shbuf_id(), window.icon().size()));
+}
+
+void WindowManager::tell_wm_listeners_window_state_changed(Window& window)
+{
+ for_each_window_listening_to_wm_events([&](Window& listener) {
+ tell_wm_listener_about_window(listener, window);
+ return IterationDecision::Continue;
+ });
+}
+
+void WindowManager::tell_wm_listeners_window_icon_changed(Window& window)
+{
+ for_each_window_listening_to_wm_events([&](Window& listener) {
+ tell_wm_listener_about_window_icon(listener, window);
+ return IterationDecision::Continue;
+ });
+}
+
+void WindowManager::tell_wm_listeners_window_rect_changed(Window& window)
+{
+ for_each_window_listening_to_wm_events([&](Window& listener) {
+ tell_wm_listener_about_window_rect(listener, window);
+ return IterationDecision::Continue;
+ });
+}
+
+void WindowManager::notify_title_changed(Window& window)
+{
+ if (window.type() != WindowType::Normal)
+ return;
+#ifdef WINDOWMANAGER_DEBUG
+ dbg() << "[WM] Window{" << &window << "} title set to \"" << window.title() << '"';
+#endif
+ invalidate(window.frame().rect());
+ if (m_switcher.is_visible())
+ m_switcher.refresh();
+
+ tell_wm_listeners_window_state_changed(window);
+}
+
+void WindowManager::notify_rect_changed(Window& window, const Gfx::Rect& old_rect, const Gfx::Rect& new_rect)
+{
+ UNUSED_PARAM(old_rect);
+ UNUSED_PARAM(new_rect);
+#ifdef RESIZE_DEBUG
+ dbg() << "[WM] Window " << &window << " rect changed " << old_rect << " -> " << new_rect;
+#endif
+ if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher)
+ m_switcher.refresh();
+
+ recompute_occlusions();
+
+ tell_wm_listeners_window_rect_changed(window);
+
+ MenuManager::the().refresh();
+}
+
+void WindowManager::recompute_occlusions()
+{
+ for_each_visible_window_from_back_to_front([&](Window& window) {
+ if (m_switcher.is_visible()) {
+ window.set_occluded(false);
+ } else {
+ if (any_opaque_window_above_this_one_contains_rect(window, window.frame().rect()))
+ window.set_occluded(true);
+ else
+ window.set_occluded(false);
+ }
+ return IterationDecision::Continue;
+ });
+}
+
+void WindowManager::notify_opacity_changed(Window&)
+{
+ recompute_occlusions();
+}
+
+void WindowManager::notify_minimization_state_changed(Window& window)
+{
+ tell_wm_listeners_window_state_changed(window);
+
+ if (window.client())
+ window.client()->post_message(Messages::WindowClient::WindowStateChanged(window.window_id(), window.is_minimized(), window.is_occluded()));
+
+ if (window.is_active() && window.is_minimized())
+ pick_new_active_window();
+}
+
+void WindowManager::notify_occlusion_state_changed(Window& window)
+{
+ if (window.client())
+ window.client()->post_message(Messages::WindowClient::WindowStateChanged(window.window_id(), window.is_minimized(), window.is_occluded()));
+}
+
+void WindowManager::pick_new_active_window()
+{
+ bool new_window_picked = false;
+ for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, [&](Window& candidate) {
+ set_active_window(&candidate);
+ new_window_picked = true;
+ return IterationDecision::Break;
+ });
+ if (!new_window_picked)
+ set_active_window(nullptr);
+}
+
+void WindowManager::start_window_move(Window& window, const MouseEvent& event)
+{
+#ifdef MOVE_DEBUG
+ dbg() << "[WM] Begin moving Window{" << &window << "}";
+#endif
+ move_to_front_and_make_active(window);
+ m_move_window = window.make_weak_ptr();
+ m_move_origin = event.position();
+ m_move_window_origin = window.position();
+ invalidate(window);
+}
+
+void WindowManager::start_window_resize(Window& window, const Gfx::Point& position, MouseButton button)
+{
+ move_to_front_and_make_active(window);
+ constexpr ResizeDirection direction_for_hot_area[3][3] = {
+ { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
+ { ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right },
+ { ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight },
+ };
+ Gfx::Rect outer_rect = window.frame().rect();
+ ASSERT(outer_rect.contains(position));
+ int window_relative_x = position.x() - outer_rect.x();
+ int window_relative_y = position.y() - outer_rect.y();
+ int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3));
+ int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3));
+ m_resize_direction = direction_for_hot_area[hot_area_row][hot_area_column];
+ if (m_resize_direction == ResizeDirection::None) {
+ ASSERT(!m_resize_window);
+ return;
+ }
+
+#ifdef RESIZE_DEBUG
+ dbg() << "[WM] Begin resizing Window{" << &window << "}";
+#endif
+ m_resizing_mouse_button = button;
+ m_resize_window = window.make_weak_ptr();
+ ;
+ m_resize_origin = position;
+ m_resize_window_original_rect = window.rect();
+
+ invalidate(window);
+}
+
+void WindowManager::start_window_resize(Window& window, const MouseEvent& event)
+{
+ start_window_resize(window, event.position(), event.button());
+}
+
+bool WindowManager::process_ongoing_window_move(MouseEvent& event, Window*& hovered_window)
+{
+ if (!m_move_window)
+ return false;
+ if (event.type() == Event::MouseUp && event.button() == MouseButton::Left) {
+#ifdef MOVE_DEBUG
+ dbg() << "[WM] Finish moving Window{" << m_move_window << "}";
+#endif
+
+ invalidate(*m_move_window);
+ if (m_move_window->rect().contains(event.position()))
+ hovered_window = m_move_window;
+ if (m_move_window->is_resizable()) {
+ process_event_for_doubleclick(*m_move_window, event);
+ if (event.type() == Event::MouseDoubleClick) {
+#if defined(DOUBLECLICK_DEBUG)
+ dbg() << "[WM] Click up became doubleclick!";
+#endif
+ m_move_window->set_maximized(!m_move_window->is_maximized());
+ }
+ }
+ m_move_window = nullptr;
+ return true;
+ }
+ if (event.type() == Event::MouseMove) {
+
+#ifdef MOVE_DEBUG
+ dbg() << "[WM] Moving, origin: " << m_move_origin << ", now: " << event.position();
+ if (m_move_window->is_maximized()) {
+ dbg() << " [!] The window is still maximized. Not moving yet.";
+ }
+
+#endif
+
+ const int maximization_deadzone = 2;
+
+ if (m_move_window->is_maximized()) {
+ auto pixels_moved_from_start = event.position().pixels_moved(m_move_origin);
+ // dbg() << "[WM] " << pixels_moved_from_start << " moved since start of window move";
+ if (pixels_moved_from_start > 5) {
+ // dbg() << "[WM] de-maximizing window";
+ m_move_origin = event.position();
+ if (m_move_origin.y() <= maximization_deadzone)
+ return true;
+ auto width_before_resize = m_move_window->width();
+ m_move_window->set_maximized(false);
+ m_move_window->move_to(m_move_origin.x() - (m_move_window->width() * ((float)m_move_origin.x() / width_before_resize)), m_move_origin.y());
+ m_move_window_origin = m_move_window->position();
+ }
+ } else {
+ bool is_resizable = m_move_window->is_resizable();
+ auto pixels_moved_from_start = event.position().pixels_moved(m_move_origin);
+ const int tiling_deadzone = 5;
+
+ if (is_resizable && event.y() <= maximization_deadzone) {
+ m_move_window->set_tiled(WindowTileType::None);
+ m_move_window->set_maximized(true);
+ return true;
+ }
+ if (is_resizable && event.x() <= tiling_deadzone) {
+ m_move_window->set_tiled(WindowTileType::Left);
+ } else if (is_resizable && event.x() >= Screen::the().width() - tiling_deadzone) {
+ m_move_window->set_tiled(WindowTileType::Right);
+ } else if (pixels_moved_from_start > 5 || m_move_window->tiled() == WindowTileType::None) {
+ m_move_window->set_tiled(WindowTileType::None);
+ Gfx::Point pos = m_move_window_origin.translated(event.position() - m_move_origin);
+ m_move_window->set_position_without_repaint(pos);
+ if (m_move_window->rect().contains(event.position()))
+ hovered_window = m_move_window;
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool WindowManager::process_ongoing_window_resize(const MouseEvent& event, Window*& hovered_window)
+{
+ if (!m_resize_window)
+ return false;
+
+ if (event.type() == Event::MouseUp && event.button() == m_resizing_mouse_button) {
+#ifdef RESIZE_DEBUG
+ dbg() << "[WM] Finish resizing Window{" << m_resize_window << "}";
+#endif
+ Core::EventLoop::current().post_event(*m_resize_window, make<ResizeEvent>(m_resize_window->rect(), m_resize_window->rect()));
+ invalidate(*m_resize_window);
+ if (m_resize_window->rect().contains(event.position()))
+ hovered_window = m_resize_window;
+ m_resize_window = nullptr;
+ m_resizing_mouse_button = MouseButton::None;
+ return true;
+ }
+
+ if (event.type() != Event::MouseMove)
+ return false;
+
+ auto old_rect = m_resize_window->rect();
+
+ int diff_x = event.x() - m_resize_origin.x();
+ int diff_y = event.y() - m_resize_origin.y();
+
+ int change_w = 0;
+ int change_h = 0;
+
+ switch (m_resize_direction) {
+ case ResizeDirection::DownRight:
+ change_w = diff_x;
+ change_h = diff_y;
+ break;
+ case ResizeDirection::Right:
+ change_w = diff_x;
+ break;
+ case ResizeDirection::UpRight:
+ change_w = diff_x;
+ change_h = -diff_y;
+ break;
+ case ResizeDirection::Up:
+ change_h = -diff_y;
+ break;
+ case ResizeDirection::UpLeft:
+ change_w = -diff_x;
+ change_h = -diff_y;
+ break;
+ case ResizeDirection::Left:
+ change_w = -diff_x;
+ break;
+ case ResizeDirection::DownLeft:
+ change_w = -diff_x;
+ change_h = diff_y;
+ break;
+ case ResizeDirection::Down:
+ change_h = diff_y;
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ auto new_rect = m_resize_window_original_rect;
+
+ // First, size the new rect.
+ Gfx::Size minimum_size { 50, 50 };
+
+ new_rect.set_width(max(minimum_size.width(), new_rect.width() + change_w));
+ new_rect.set_height(max(minimum_size.height(), new_rect.height() + change_h));
+
+ if (!m_resize_window->size_increment().is_null()) {
+ int horizontal_incs = (new_rect.width() - m_resize_window->base_size().width()) / m_resize_window->size_increment().width();
+ new_rect.set_width(m_resize_window->base_size().width() + horizontal_incs * m_resize_window->size_increment().width());
+ int vertical_incs = (new_rect.height() - m_resize_window->base_size().height()) / m_resize_window->size_increment().height();
+ new_rect.set_height(m_resize_window->base_size().height() + vertical_incs * m_resize_window->size_increment().height());
+ }
+
+ // Second, set its position so that the sides of the window
+ // that end up moving are the same ones as the user is dragging,
+ // no matter which part of the logic above caused us to decide
+ // to resize by this much.
+ switch (m_resize_direction) {
+ case ResizeDirection::DownRight:
+ case ResizeDirection::Right:
+ case ResizeDirection::Down:
+ break;
+ case ResizeDirection::Left:
+ case ResizeDirection::Up:
+ case ResizeDirection::UpLeft:
+ new_rect.set_right_without_resize(m_resize_window_original_rect.right());
+ new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom());
+ break;
+ case ResizeDirection::UpRight:
+ new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom());
+ break;
+ case ResizeDirection::DownLeft:
+ new_rect.set_right_without_resize(m_resize_window_original_rect.right());
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ if (new_rect.contains(event.position()))
+ hovered_window = m_resize_window;
+
+ if (m_resize_window->rect() == new_rect)
+ return true;
+#ifdef RESIZE_DEBUG
+ dbg() << "[WM] Resizing, original: " << m_resize_window_original_rect << ", now: " << new_rect;
+#endif
+ m_resize_window->set_rect(new_rect);
+ Core::EventLoop::current().post_event(*m_resize_window, make<ResizeEvent>(old_rect, new_rect));
+ return true;
+}
+
+bool WindowManager::process_ongoing_drag(MouseEvent& event, Window*& hovered_window)
+{
+ if (!m_dnd_client)
+ return false;
+
+ if (event.type() == Event::MouseMove) {
+ // We didn't let go of the drag yet, see if we should send some drag move events..
+ for_each_visible_window_from_front_to_back([&](Window& window) {
+ if (!window.rect().contains(event.position()))
+ return IterationDecision::Continue;
+ hovered_window = &window;
+ auto translated_event = event.translated(-window.position());
+ translated_event.set_drag(true);
+ translated_event.set_drag_data_type(m_dnd_data_type);
+ deliver_mouse_event(window, translated_event);
+ return IterationDecision::Break;
+ });
+ }
+
+ if (!(event.type() == Event::MouseUp && event.button() == MouseButton::Left))
+ return true;
+
+ hovered_window = nullptr;
+ for_each_visible_window_from_front_to_back([&](auto& window) {
+ if (window.frame().rect().contains(event.position())) {
+ hovered_window = &window;
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+ });
+
+ if (hovered_window) {
+ m_dnd_client->post_message(Messages::WindowClient::DragAccepted());
+ if (hovered_window->client()) {
+ auto translated_event = event.translated(-hovered_window->position());
+ hovered_window->client()->post_message(Messages::WindowClient::DragDropped(hovered_window->window_id(), translated_event.position(), m_dnd_text, m_dnd_data_type, m_dnd_data));
+ }
+ } else {
+ m_dnd_client->post_message(Messages::WindowClient::DragCancelled());
+ }
+
+ end_dnd_drag();
+ return true;
+}
+
+void WindowManager::set_cursor_tracking_button(Button* button)
+{
+ m_cursor_tracking_button = button ? button->make_weak_ptr() : nullptr;
+}
+
+auto WindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) -> ClickMetadata&
+{
+ switch (button) {
+ case MouseButton::Left:
+ return m_left;
+ case MouseButton::Right:
+ return m_right;
+ case MouseButton::Middle:
+ return m_middle;
+ case MouseButton::Back:
+ return m_back;
+ case MouseButton::Forward:
+ return m_forward;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+// #define DOUBLECLICK_DEBUG
+
+void WindowManager::process_event_for_doubleclick(Window& window, MouseEvent& event)
+{
+ // We only care about button presses (because otherwise it's not a doubleclick, duh!)
+ ASSERT(event.type() == Event::MouseUp);
+
+ if (&window != m_double_click_info.m_clicked_window) {
+ // we either haven't clicked anywhere, or we haven't clicked on this
+ // window. set the current click window, and reset the timers.
+#if defined(DOUBLECLICK_DEBUG)
+ dbg() << "Initial mouseup on window " << &window << " (previous was " << m_double_click_info.m_clicked_window << ')';
+#endif
+ m_double_click_info.m_clicked_window = window.make_weak_ptr();
+ m_double_click_info.reset();
+ }
+
+ auto& metadata = m_double_click_info.metadata_for_button(event.button());
+
+ // if the clock is invalid, we haven't clicked with this button on this
+ // window yet, so there's nothing to do.
+ if (!metadata.clock.is_valid()) {
+ metadata.clock.start();
+ } else {
+ int elapsed_since_last_click = metadata.clock.elapsed();
+ metadata.clock.start();
+ if (elapsed_since_last_click < m_double_click_speed) {
+ auto diff = event.position() - metadata.last_position;
+ auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
+ if (distance_travelled_squared > (m_max_distance_for_double_click * m_max_distance_for_double_click)) {
+ // too far; try again
+ metadata.clock.start();
+ } else {
+#if defined(DOUBLECLICK_DEBUG)
+ dbg() << "Transforming MouseUp to MouseDoubleClick (" << elapsed_since_last_click << " < " << m_double_click_speed << ")!";
+#endif
+ event = MouseEvent(Event::MouseDoubleClick, event.position(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta());
+ // invalidate this now we've delivered a doubleclick, otherwise
+ // tripleclick will deliver two doubleclick events (incorrectly).
+ metadata.clock = {};
+ }
+ } else {
+ // too slow; try again
+ metadata.clock.start();
+ }
+ }
+
+ metadata.last_position = event.position();
+}
+
+void WindowManager::deliver_mouse_event(Window& window, MouseEvent& event)
+{
+ window.dispatch_event(event);
+ if (event.type() == Event::MouseUp) {
+ process_event_for_doubleclick(window, event);
+ if (event.type() == Event::MouseDoubleClick)
+ window.dispatch_event(event);
+ }
+}
+
+void WindowManager::process_mouse_event(MouseEvent& event, Window*& hovered_window)
+{
+ hovered_window = nullptr;
+
+ if (process_ongoing_drag(event, hovered_window))
+ return;
+
+ if (process_ongoing_window_move(event, hovered_window))
+ return;
+
+ if (process_ongoing_window_resize(event, hovered_window))
+ return;
+
+ if (m_cursor_tracking_button)
+ return m_cursor_tracking_button->on_mouse_event(event.translated(-m_cursor_tracking_button->screen_rect().location()));
+
+ // This is quite hackish, but it's how the Button hover effect is implemented.
+ if (m_hovered_button && event.type() == Event::MouseMove)
+ m_hovered_button->on_mouse_event(event.translated(-m_hovered_button->screen_rect().location()));
+
+ HashTable<Window*> windows_who_received_mouse_event_due_to_cursor_tracking;
+
+ for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
+ if (!window->global_cursor_tracking())
+ continue;
+ ASSERT(window->is_visible()); // Maybe this should be supported? Idk. Let's catch it and think about it later.
+ ASSERT(!window->is_minimized()); // Maybe this should also be supported? Idk.
+ windows_who_received_mouse_event_due_to_cursor_tracking.set(window);
+ auto translated_event = event.translated(-window->position());
+ deliver_mouse_event(*window, translated_event);
+ }
+
+ // FIXME: Now that the menubar has a dedicated window, is this special-casing really necessary?
+ if (MenuManager::the().has_open_menu() || (!active_window_is_modal() && menubar_rect().contains(event.position()))) {
+ clear_resize_candidate();
+ MenuManager::the().dispatch_event(event);
+ return;
+ }
+
+ Window* event_window_with_frame = nullptr;
+
+ if (m_active_input_window) {
+ // At this point, we have delivered the start of an input sequence to a
+ // client application. We must keep delivering to that client
+ // application until the input sequence is done.
+ //
+ // This prevents e.g. moving on one window out of the bounds starting
+ // a move in that other unrelated window, and other silly shenanigans.
+ if (!windows_who_received_mouse_event_due_to_cursor_tracking.contains(m_active_input_window)) {
+ auto translated_event = event.translated(-m_active_input_window->position());
+ deliver_mouse_event(*m_active_input_window, translated_event);
+ windows_who_received_mouse_event_due_to_cursor_tracking.set(m_active_input_window.ptr());
+ }
+ if (event.type() == Event::MouseUp && event.buttons() == 0) {
+ m_active_input_window = nullptr;
+ }
+
+ for_each_visible_window_from_front_to_back([&](auto& window) {
+ if (window.frame().rect().contains(event.position())) {
+ hovered_window = &window;
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+ });
+ } else {
+ for_each_visible_window_from_front_to_back([&](Window& window) {
+ auto window_frame_rect = window.frame().rect();
+ if (!window_frame_rect.contains(event.position()))
+ return IterationDecision::Continue;
+
+ if (&window != m_resize_candidate.ptr())
+ clear_resize_candidate();
+
+ // First check if we should initiate a move or resize (Logo+LMB or Logo+RMB).
+ // In those cases, the event is swallowed by the window manager.
+ if (window.is_movable()) {
+ if (!window.is_fullscreen() && m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseDown && event.button() == MouseButton::Left) {
+ hovered_window = &window;
+ start_window_move(window, event);
+ m_moved_or_resized_since_logo_keydown = true;
+ return IterationDecision::Break;
+ }
+ if (window.is_resizable() && m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseDown && event.button() == MouseButton::Right && !window.is_blocked_by_modal_window()) {
+ hovered_window = &window;
+ start_window_resize(window, event);
+ m_moved_or_resized_since_logo_keydown = true;
+ return IterationDecision::Break;
+ }
+ }
+
+ if (m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseWheel) {
+ float opacity_change = -event.wheel_delta() * 0.05f;
+ float new_opacity = window.opacity() + opacity_change;
+ if (new_opacity < 0.05f)
+ new_opacity = 0.05f;
+ if (new_opacity > 1.0f)
+ new_opacity = 1.0f;
+ window.set_opacity(new_opacity);
+ window.invalidate();
+ return IterationDecision::Break;
+ }
+
+ // Well okay, let's see if we're hitting the frame or the window inside the frame.
+ if (window.rect().contains(event.position())) {
+ if (event.type() == Event::MouseDown) {
+ if (window.type() == WindowType::Normal)
+ move_to_front_and_make_active(window);
+ else if (window.type() == WindowType::Desktop)
+ set_active_window(&window);
+ }
+
+ hovered_window = &window;
+ if (!window.global_cursor_tracking() && !windows_who_received_mouse_event_due_to_cursor_tracking.contains(&window)) {
+ auto translated_event = event.translated(-window.position());
+ deliver_mouse_event(window, translated_event);
+ if (event.type() == Event::MouseDown) {
+ m_active_input_window = window.make_weak_ptr();
+ }
+ }
+ return IterationDecision::Break;
+ }
+
+ // We are hitting the frame, pass the event along to WindowFrame.
+ window.frame().on_mouse_event(event.translated(-window_frame_rect.location()));
+ event_window_with_frame = &window;
+ return IterationDecision::Break;
+ });
+
+ // Clicked outside of any window
+ if (!hovered_window && !event_window_with_frame && event.type() == Event::MouseDown)
+ set_active_window(nullptr);
+ }
+
+ if (event_window_with_frame != m_resize_candidate.ptr())
+ clear_resize_candidate();
+}
+
+void WindowManager::clear_resize_candidate()
+{
+ if (m_resize_candidate)
+ Compositor::the().invalidate_cursor();
+ m_resize_candidate = nullptr;
+}
+
+bool WindowManager::any_opaque_window_contains_rect(const Gfx::Rect& rect)
+{
+ bool found_containing_window = false;
+ for_each_visible_window_from_back_to_front([&](Window& window) {
+ if (window.is_minimized())
+ return IterationDecision::Continue;
+ if (window.opacity() < 1.0f)
+ return IterationDecision::Continue;
+ if (window.has_alpha_channel()) {
+ // FIXME: Just because the window has an alpha channel doesn't mean it's not opaque.
+ // Maybe there's some way we could know this?
+ return IterationDecision::Continue;
+ }
+ if (window.frame().rect().contains(rect)) {
+ found_containing_window = true;
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+ });
+ return found_containing_window;
+};
+
+bool WindowManager::any_opaque_window_above_this_one_contains_rect(const Window& a_window, const Gfx::Rect& rect)
+{
+ bool found_containing_window = false;
+ bool checking = false;
+ for_each_visible_window_from_back_to_front([&](Window& window) {
+ if (&window == &a_window) {
+ checking = true;
+ return IterationDecision::Continue;
+ }
+ if (!checking)
+ return IterationDecision::Continue;
+ if (!window.is_visible())
+ return IterationDecision::Continue;
+ if (window.is_minimized())
+ return IterationDecision::Continue;
+ if (window.opacity() < 1.0f)
+ return IterationDecision::Continue;
+ if (window.has_alpha_channel())
+ return IterationDecision::Continue;
+ if (window.frame().rect().contains(rect)) {
+ found_containing_window = true;
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+ });
+ return found_containing_window;
+};
+
+Gfx::Rect WindowManager::menubar_rect() const
+{
+ if (active_fullscreen_window())
+ return {};
+ return MenuManager::the().menubar_rect();
+}
+
+Gfx::Rect WindowManager::desktop_rect() const
+{
+ if (active_fullscreen_window())
+ return {};
+ return {
+ 0,
+ menubar_rect().bottom() + 1,
+ Screen::the().width(),
+ Screen::the().height() - menubar_rect().height() - 28
+ };
+}
+
+void WindowManager::event(Core::Event& event)
+{
+ if (static_cast<Event&>(event).is_mouse_event()) {
+ Window* hovered_window = nullptr;
+ process_mouse_event(static_cast<MouseEvent&>(event), hovered_window);
+ set_hovered_window(hovered_window);
+ return;
+ }
+
+ if (static_cast<Event&>(event).is_key_event()) {
+ auto& key_event = static_cast<const KeyEvent&>(event);
+ m_keyboard_modifiers = key_event.modifiers();
+
+ if (key_event.type() == Event::KeyDown && key_event.key() == Key_Escape && m_dnd_client) {
+ m_dnd_client->post_message(Messages::WindowClient::DragCancelled());
+ end_dnd_drag();
+ return;
+ }
+
+ if (key_event.key() == Key_Logo) {
+ if (key_event.type() == Event::KeyUp) {
+ if (!m_moved_or_resized_since_logo_keydown && !m_switcher.is_visible() && !m_move_window && !m_resize_window) {
+ MenuManager::the().toggle_system_menu();
+ return;
+ }
+
+ } else if (key_event.type() == Event::KeyDown) {
+ m_moved_or_resized_since_logo_keydown = false;
+ }
+ }
+
+ if (MenuManager::the().current_menu()) {
+ MenuManager::the().dispatch_event(event);
+ return;
+ }
+
+ if (key_event.type() == Event::KeyDown && ((key_event.modifiers() == Mod_Logo && key_event.key() == Key_Tab) || (key_event.modifiers() == (Mod_Logo | Mod_Shift) && key_event.key() == Key_Tab)))
+ m_switcher.show();
+ if (m_switcher.is_visible()) {
+ m_switcher.on_key_event(key_event);
+ return;
+ }
+
+ if (m_active_window) {
+ if (key_event.type() == Event::KeyDown && key_event.modifiers() == Mod_Logo) {
+ if (key_event.key() == Key_Down) {
+ m_moved_or_resized_since_logo_keydown = true;
+ if (m_active_window->is_resizable() && m_active_window->is_maximized()) {
+ m_active_window->set_maximized(false);
+ return;
+ }
+ if (m_active_window->is_minimizable())
+ m_active_window->set_minimized(true);
+ return;
+ }
+ if (m_active_window->is_resizable()) {
+ if (key_event.key() == Key_Up) {
+ m_moved_or_resized_since_logo_keydown = true;
+ m_active_window->set_maximized(!m_active_window->is_maximized());
+ return;
+ }
+ if (key_event.key() == Key_Left) {
+ m_moved_or_resized_since_logo_keydown = true;
+ if (m_active_window->tiled() != WindowTileType::None) {
+ m_active_window->set_tiled(WindowTileType::None);
+ return;
+ }
+ if (m_active_window->is_maximized())
+ m_active_window->set_maximized(false);
+ m_active_window->set_tiled(WindowTileType::Left);
+ return;
+ }
+ if (key_event.key() == Key_Right) {
+ m_moved_or_resized_since_logo_keydown = true;
+ if (m_active_window->tiled() != WindowTileType::None) {
+ m_active_window->set_tiled(WindowTileType::None);
+ return;
+ }
+ if (m_active_window->is_maximized())
+ m_active_window->set_maximized(false);
+ m_active_window->set_tiled(WindowTileType::Right);
+ return;
+ }
+ }
+ }
+ m_active_window->dispatch_event(event);
+ return;
+ }
+ }
+
+ Core::Object::event(event);
+}
+
+void WindowManager::set_highlight_window(Window* window)
+{
+ if (window == m_highlight_window)
+ return;
+ if (auto* previous_highlight_window = m_highlight_window.ptr())
+ invalidate(*previous_highlight_window);
+ m_highlight_window = window ? window->make_weak_ptr() : nullptr;
+ if (m_highlight_window)
+ invalidate(*m_highlight_window);
+}
+
+static bool window_type_can_become_active(WindowType type)
+{
+ return type == WindowType::Normal || type == WindowType::Desktop;
+}
+
+void WindowManager::set_active_window(Window* window)
+{
+ if (window && window->is_blocked_by_modal_window())
+ return;
+
+ if (window && !window_type_can_become_active(window->type()))
+ return;
+
+ if (window == m_active_window)
+ return;
+
+ auto* previously_active_window = m_active_window.ptr();
+
+ ClientConnection* previously_active_client = nullptr;
+ ClientConnection* active_client = nullptr;
+
+ if (previously_active_window) {
+ previously_active_client = previously_active_window->client();
+ Core::EventLoop::current().post_event(*previously_active_window, make<Event>(Event::WindowDeactivated));
+ invalidate(*previously_active_window);
+ m_active_window = nullptr;
+ m_active_input_window = nullptr;
+ tell_wm_listeners_window_state_changed(*previously_active_window);
+ }
+
+ if (window) {
+ m_active_window = window->make_weak_ptr();
+ active_client = m_active_window->client();
+ Core::EventLoop::current().post_event(*m_active_window, make<Event>(Event::WindowActivated));
+ invalidate(*m_active_window);
+
+ auto* client = window->client();
+ ASSERT(client);
+ MenuManager::the().set_current_menubar(client->app_menubar());
+ tell_wm_listeners_window_state_changed(*m_active_window);
+ } else {
+ MenuManager::the().set_current_menubar(nullptr);
+ }
+
+ if (active_client != previously_active_client) {
+ if (previously_active_client)
+ previously_active_client->deboost();
+ if (active_client)
+ active_client->boost();
+ }
+}
+
+void WindowManager::set_hovered_window(Window* window)
+{
+ if (m_hovered_window == window)
+ return;
+
+ if (m_hovered_window)
+ Core::EventLoop::current().post_event(*m_hovered_window, make<Event>(Event::WindowLeft));
+
+ m_hovered_window = window ? window->make_weak_ptr() : nullptr;
+
+ if (m_hovered_window)
+ Core::EventLoop::current().post_event(*m_hovered_window, make<Event>(Event::WindowEntered));
+}
+
+void WindowManager::invalidate()
+{
+ Compositor::the().invalidate();
+}
+
+void WindowManager::invalidate(const Gfx::Rect& rect)
+{
+ Compositor::the().invalidate(rect);
+}
+
+void WindowManager::invalidate(const Window& window)
+{
+ invalidate(window.frame().rect());
+}
+
+void WindowManager::invalidate(const Window& window, const Gfx::Rect& rect)
+{
+ if (window.type() == WindowType::MenuApplet) {
+ AppletManager::the().invalidate_applet(window, rect);
+ return;
+ }
+
+ if (rect.is_empty()) {
+ invalidate(window);
+ return;
+ }
+ auto outer_rect = window.frame().rect();
+ auto inner_rect = rect;
+ inner_rect.move_by(window.position());
+ // FIXME: This seems slightly wrong; the inner rect shouldn't intersect the border part of the outer rect.
+ inner_rect.intersect(outer_rect);
+ invalidate(inner_rect);
+}
+
+const ClientConnection* WindowManager::active_client() const
+{
+ if (m_active_window)
+ return m_active_window->client();
+ return nullptr;
+}
+
+void WindowManager::notify_client_changed_app_menubar(ClientConnection& client)
+{
+ if (active_client() == &client)
+ MenuManager::the().set_current_menubar(client.app_menubar());
+}
+
+const Cursor& WindowManager::active_cursor() const
+{
+ if (m_dnd_client)
+ return *m_drag_cursor;
+
+ if (m_move_window)
+ return *m_move_cursor;
+
+ if (m_resize_window || m_resize_candidate) {
+ switch (m_resize_direction) {
+ case ResizeDirection::Up:
+ case ResizeDirection::Down:
+ return *m_resize_vertically_cursor;
+ case ResizeDirection::Left:
+ case ResizeDirection::Right:
+ return *m_resize_horizontally_cursor;
+ case ResizeDirection::UpLeft:
+ case ResizeDirection::DownRight:
+ return *m_resize_diagonally_tlbr_cursor;
+ case ResizeDirection::UpRight:
+ case ResizeDirection::DownLeft:
+ return *m_resize_diagonally_bltr_cursor;
+ case ResizeDirection::None:
+ break;
+ }
+ }
+
+ if (m_hovered_window && m_hovered_window->override_cursor())
+ return *m_hovered_window->override_cursor();
+
+ return *m_arrow_cursor;
+}
+
+void WindowManager::set_hovered_button(Button* button)
+{
+ m_hovered_button = button ? button->make_weak_ptr() : nullptr;
+}
+
+void WindowManager::set_resize_candidate(Window& window, ResizeDirection direction)
+{
+ m_resize_candidate = window.make_weak_ptr();
+ m_resize_direction = direction;
+}
+
+ResizeDirection WindowManager::resize_direction_of_window(const Window& window)
+{
+ if (&window != m_resize_window)
+ return ResizeDirection::None;
+ return m_resize_direction;
+}
+
+Gfx::Rect WindowManager::maximized_window_rect(const Window& window) const
+{
+ Gfx::Rect rect = Screen::the().rect();
+
+ // Subtract window title bar (leaving the border)
+ rect.set_y(rect.y() + window.frame().title_bar_rect().height());
+ rect.set_height(rect.height() - window.frame().title_bar_rect().height());
+
+ // Subtract menu bar
+ rect.set_y(rect.y() + menubar_rect().height());
+ rect.set_height(rect.height() - menubar_rect().height());
+
+ // Subtract taskbar window height if present
+ const_cast<WindowManager*>(this)->for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, [&rect](Window& taskbar_window) {
+ rect.set_height(rect.height() - taskbar_window.height());
+ return IterationDecision::Break;
+ });
+
+ return rect;
+}
+
+void WindowManager::start_dnd_drag(ClientConnection& client, const String& text, Gfx::Bitmap* bitmap, const String& data_type, const String& data)
+{
+ ASSERT(!m_dnd_client);
+ m_dnd_client = client.make_weak_ptr();
+ m_dnd_text = text;
+ m_dnd_bitmap = bitmap;
+ m_dnd_data_type = data_type;
+ m_dnd_data = data;
+ Compositor::the().invalidate_cursor();
+ m_active_input_window = nullptr;
+}
+
+void WindowManager::end_dnd_drag()
+{
+ ASSERT(m_dnd_client);
+ Compositor::the().invalidate_cursor();
+ m_dnd_client = nullptr;
+ m_dnd_text = {};
+ m_dnd_bitmap = nullptr;
+}
+
+Gfx::Rect WindowManager::dnd_rect() const
+{
+ int bitmap_width = m_dnd_bitmap ? m_dnd_bitmap->width() : 0;
+ int bitmap_height = m_dnd_bitmap ? m_dnd_bitmap->height() : 0;
+ int width = font().width(m_dnd_text) + bitmap_width;
+ int height = max((int)font().glyph_height(), bitmap_height);
+ auto location = Compositor::the().current_cursor_rect().center().translated(8, 8);
+ return Gfx::Rect(location, { width, height }).inflated(4, 4);
+}
+
+bool WindowManager::update_theme(String theme_path, String theme_name)
+{
+ auto new_theme = Gfx::load_system_theme(theme_path);
+ if (!new_theme)
+ return false;
+ ASSERT(new_theme);
+ Gfx::set_system_theme(*new_theme);
+ m_palette = Gfx::PaletteImpl::create_with_shared_buffer(*new_theme);
+ Compositor::the().set_background_color(palette().desktop_background().to_string());
+ HashTable<ClientConnection*> notified_clients;
+ for_each_window([&](Window& window) {
+ if (window.client()) {
+ if (!notified_clients.contains(window.client())) {
+ window.client()->post_message(Messages::WindowClient::UpdateSystemTheme(Gfx::current_system_theme_buffer_id()));
+ notified_clients.set(window.client());
+ }
+ }
+ return IterationDecision::Continue;
+ });
+ MenuManager::the().did_change_theme();
+ auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini");
+ wm_config->write_entry("Theme", "Name", theme_name);
+ wm_config->sync();
+ invalidate();
+ return true;
+}
+
+}
diff --git a/Services/WindowServer/WindowManager.h b/Services/WindowServer/WindowManager.h
new file mode 100644
index 0000000000..0c7ec8429f
--- /dev/null
+++ b/Services/WindowServer/WindowManager.h
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashMap.h>
+#include <AK/HashTable.h>
+#include <AK/InlineLinkedList.h>
+#include <AK/WeakPtr.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/ElapsedTimer.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/DisjointRectSet.h>
+#include <LibGfx/Painter.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/Rect.h>
+#include <WindowServer/Cursor.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/MenuBar.h>
+#include <WindowServer/MenuManager.h>
+#include <WindowServer/Window.h>
+#include <WindowServer/WindowSwitcher.h>
+#include <WindowServer/WindowType.h>
+
+namespace WindowServer {
+
+class Screen;
+class MouseEvent;
+class Window;
+class ClientConnection;
+class WindowSwitcher;
+class Button;
+
+enum class ResizeDirection {
+ None,
+ Left,
+ UpLeft,
+ Up,
+ UpRight,
+ Right,
+ DownRight,
+ Down,
+ DownLeft
+};
+
+class WindowManager : public Core::Object {
+ C_OBJECT(WindowManager)
+
+ friend class Compositor;
+ friend class WindowFrame;
+ friend class WindowSwitcher;
+
+public:
+ static WindowManager& the();
+
+ explicit WindowManager(const Gfx::PaletteImpl&);
+ virtual ~WindowManager() override;
+
+ Palette palette() const { return Palette(*m_palette); }
+
+ RefPtr<Core::ConfigFile> wm_config() const
+ {
+ return m_wm_config;
+ }
+ void reload_config(bool);
+
+ void add_window(Window&);
+ void remove_window(Window&);
+
+ void notify_title_changed(Window&);
+ void notify_rect_changed(Window&, const Gfx::Rect& oldRect, const Gfx::Rect& newRect);
+ void notify_minimization_state_changed(Window&);
+ void notify_opacity_changed(Window&);
+ void notify_occlusion_state_changed(Window&);
+ void notify_client_changed_app_menubar(ClientConnection&);
+
+ Gfx::Rect maximized_window_rect(const Window&) const;
+
+ ClientConnection* dnd_client() { return m_dnd_client.ptr(); }
+ const String& dnd_text() const { return m_dnd_text; }
+ const String& dnd_data_type() const { return m_dnd_data_type; }
+ const String& dnd_data() const { return m_dnd_data; }
+ const Gfx::Bitmap* dnd_bitmap() const { return m_dnd_bitmap; }
+ Gfx::Rect dnd_rect() const;
+
+ void start_dnd_drag(ClientConnection&, const String& text, Gfx::Bitmap*, const String& data_type, const String& data);
+ void end_dnd_drag();
+
+ Window* active_window() { return m_active_window.ptr(); }
+ const ClientConnection* active_client() const;
+ bool active_window_is_modal() const { return m_active_window && m_active_window->is_modal(); }
+
+ Window* highlight_window() { return m_highlight_window.ptr(); }
+ void set_highlight_window(Window*);
+
+ void move_to_front_and_make_active(Window&);
+
+ Gfx::Rect menubar_rect() const;
+ Gfx::Rect desktop_rect() const;
+
+ const Cursor& active_cursor() const;
+ const Cursor& arrow_cursor() const { return *m_arrow_cursor; }
+ const Cursor& hand_cursor() const { return *m_hand_cursor; }
+ const Cursor& resize_horizontally_cursor() const { return *m_resize_horizontally_cursor; }
+ const Cursor& resize_vertically_cursor() const { return *m_resize_vertically_cursor; }
+ const Cursor& resize_diagonally_tlbr_cursor() const { return *m_resize_diagonally_tlbr_cursor; }
+ const Cursor& resize_diagonally_bltr_cursor() const { return *m_resize_diagonally_bltr_cursor; }
+ const Cursor& i_beam_cursor() const { return *m_i_beam_cursor; }
+ const Cursor& disallowed_cursor() const { return *m_disallowed_cursor; }
+ const Cursor& move_cursor() const { return *m_move_cursor; }
+ const Cursor& drag_cursor() const { return *m_drag_cursor; }
+
+ void invalidate(const Window&);
+ void invalidate(const Window&, const Gfx::Rect&);
+ void invalidate(const Gfx::Rect&);
+ void invalidate();
+ void flush(const Gfx::Rect&);
+
+ const Gfx::Font& font() const;
+ const Gfx::Font& window_title_font() const;
+
+ bool set_resolution(int width, int height);
+ Gfx::Size resolution() const;
+
+ void set_active_window(Window*);
+ void set_hovered_button(Button*);
+
+ Button* cursor_tracking_button() { return m_cursor_tracking_button.ptr(); }
+ void set_cursor_tracking_button(Button*);
+
+ void set_resize_candidate(Window&, ResizeDirection);
+ void clear_resize_candidate();
+ ResizeDirection resize_direction_of_window(const Window&);
+
+ bool any_opaque_window_contains_rect(const Gfx::Rect&);
+ bool any_opaque_window_above_this_one_contains_rect(const Window&, const Gfx::Rect&);
+
+ void tell_wm_listeners_window_state_changed(Window&);
+ void tell_wm_listeners_window_icon_changed(Window&);
+ void tell_wm_listeners_window_rect_changed(Window&);
+
+ void start_window_resize(Window&, const Gfx::Point&, MouseButton);
+ void start_window_resize(Window&, const MouseEvent&);
+
+ const Window* active_fullscreen_window() const { return (m_active_window && m_active_window->is_fullscreen()) ? m_active_window : nullptr; }
+ Window* active_fullscreen_window() { return (m_active_window && m_active_window->is_fullscreen()) ? m_active_window : nullptr; }
+
+ bool update_theme(String theme_path, String theme_name);
+
+ void set_hovered_window(Window*);
+ void deliver_mouse_event(Window& window, MouseEvent& event);
+
+private:
+ NonnullRefPtr<Cursor> get_cursor(const String& name);
+ NonnullRefPtr<Cursor> get_cursor(const String& name, const Gfx::Point& hotspot);
+
+ void process_mouse_event(MouseEvent&, Window*& hovered_window);
+ void process_event_for_doubleclick(Window& window, MouseEvent& event);
+ bool process_ongoing_window_resize(const MouseEvent&, Window*& hovered_window);
+ bool process_ongoing_window_move(MouseEvent&, Window*& hovered_window);
+ bool process_ongoing_drag(MouseEvent&, Window*& hovered_window);
+ void start_window_move(Window&, const MouseEvent&);
+ template<typename Callback>
+ IterationDecision for_each_visible_window_of_type_from_back_to_front(WindowType, Callback, bool ignore_highlight = false);
+ template<typename Callback>
+ IterationDecision for_each_visible_window_of_type_from_front_to_back(WindowType, Callback, bool ignore_highlight = false);
+ template<typename Callback>
+ IterationDecision for_each_visible_window_from_front_to_back(Callback);
+ template<typename Callback>
+ IterationDecision for_each_visible_window_from_back_to_front(Callback);
+ template<typename Callback>
+ void for_each_window_listening_to_wm_events(Callback);
+ template<typename Callback>
+ void for_each_window(Callback);
+ template<typename Callback>
+ IterationDecision for_each_window_of_type_from_front_to_back(WindowType, Callback, bool ignore_highlight = false);
+
+ virtual void event(Core::Event&) override;
+ void paint_window_frame(const Window&);
+ void tell_wm_listener_about_window(Window& listener, Window&);
+ void tell_wm_listener_about_window_icon(Window& listener, Window&);
+ void tell_wm_listener_about_window_rect(Window& listener, Window&);
+ void pick_new_active_window();
+
+ void recompute_occlusions();
+
+ RefPtr<Cursor> m_arrow_cursor;
+ RefPtr<Cursor> m_hand_cursor;
+ RefPtr<Cursor> m_resize_horizontally_cursor;
+ RefPtr<Cursor> m_resize_vertically_cursor;
+ RefPtr<Cursor> m_resize_diagonally_tlbr_cursor;
+ RefPtr<Cursor> m_resize_diagonally_bltr_cursor;
+ RefPtr<Cursor> m_i_beam_cursor;
+ RefPtr<Cursor> m_disallowed_cursor;
+ RefPtr<Cursor> m_move_cursor;
+ RefPtr<Cursor> m_drag_cursor;
+
+ InlineLinkedList<Window> m_windows_in_order;
+
+ struct DoubleClickInfo {
+ struct ClickMetadata {
+ Core::ElapsedTimer clock;
+ Gfx::Point last_position;
+ };
+
+ ClickMetadata& metadata_for_button(MouseButton);
+
+ void reset()
+ {
+ m_left = {};
+ m_right = {};
+ m_middle = {};
+ m_back = {};
+ m_forward = {};
+ }
+
+ WeakPtr<Window> m_clicked_window;
+
+ private:
+ ClickMetadata m_left;
+ ClickMetadata m_right;
+ ClickMetadata m_middle;
+ ClickMetadata m_back;
+ ClickMetadata m_forward;
+ };
+ DoubleClickInfo m_double_click_info;
+ int m_double_click_speed { 0 };
+ int m_max_distance_for_double_click { 4 };
+
+ WeakPtr<Window> m_active_window;
+ WeakPtr<Window> m_hovered_window;
+ WeakPtr<Window> m_highlight_window;
+ WeakPtr<Window> m_active_input_window;
+
+ WeakPtr<Window> m_move_window;
+ Gfx::Point m_move_origin;
+ Gfx::Point m_move_window_origin;
+
+ WeakPtr<Window> m_resize_window;
+ WeakPtr<Window> m_resize_candidate;
+ MouseButton m_resizing_mouse_button { MouseButton::None };
+ Gfx::Rect m_resize_window_original_rect;
+ Gfx::Point m_resize_origin;
+ ResizeDirection m_resize_direction { ResizeDirection::None };
+
+ bool m_moved_or_resized_since_logo_keydown { false };
+
+ u8 m_keyboard_modifiers { 0 };
+
+ WindowSwitcher m_switcher;
+
+ WeakPtr<Button> m_cursor_tracking_button;
+ WeakPtr<Button> m_hovered_button;
+
+ NonnullRefPtr<Gfx::PaletteImpl> m_palette;
+
+ RefPtr<Core::ConfigFile> m_wm_config;
+
+ WeakPtr<ClientConnection> m_dnd_client;
+ String m_dnd_text;
+ String m_dnd_data_type;
+ String m_dnd_data;
+ RefPtr<Gfx::Bitmap> m_dnd_bitmap;
+};
+
+template<typename Callback>
+IterationDecision WindowManager::for_each_visible_window_of_type_from_back_to_front(WindowType type, Callback callback, bool ignore_highlight)
+{
+ bool do_highlight_window_at_end = false;
+ for (auto& window : m_windows_in_order) {
+ if (!window.is_visible())
+ continue;
+ if (window.is_minimized())
+ continue;
+ if (window.type() != type)
+ continue;
+ if (!ignore_highlight && m_highlight_window == &window) {
+ do_highlight_window_at_end = true;
+ continue;
+ }
+ if (callback(window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ if (do_highlight_window_at_end) {
+ if (callback(*m_highlight_window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+}
+
+template<typename Callback>
+IterationDecision WindowManager::for_each_visible_window_from_back_to_front(Callback callback)
+{
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Desktop, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Normal, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Tooltip, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Notification, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Menubar, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_back_to_front(WindowType::Menu, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ return for_each_visible_window_of_type_from_back_to_front(WindowType::WindowSwitcher, callback);
+}
+
+template<typename Callback>
+IterationDecision WindowManager::for_each_visible_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight)
+{
+ if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) {
+ if (callback(*m_highlight_window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+
+ for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
+ if (!window->is_visible())
+ continue;
+ if (window->is_minimized())
+ continue;
+ if (window->type() != type)
+ continue;
+ if (!ignore_highlight && window == m_highlight_window)
+ continue;
+ if (callback(*window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+}
+
+template<typename Callback>
+IterationDecision WindowManager::for_each_visible_window_from_front_to_back(Callback callback)
+{
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::WindowSwitcher, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Menu, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Menubar, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Notification, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Tooltip, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Taskbar, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ if (for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ return for_each_visible_window_of_type_from_front_to_back(WindowType::Desktop, callback);
+}
+
+template<typename Callback>
+void WindowManager::for_each_window_listening_to_wm_events(Callback callback)
+{
+ for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
+ if (!window->listens_to_wm_events())
+ continue;
+ if (callback(*window) == IterationDecision::Break)
+ return;
+ }
+}
+
+template<typename Callback>
+void WindowManager::for_each_window(Callback callback)
+{
+ for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
+ if (callback(*window) == IterationDecision::Break)
+ return;
+ }
+}
+
+template<typename Callback>
+IterationDecision WindowManager::for_each_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight)
+{
+ if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) {
+ if (callback(*m_highlight_window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+
+ for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
+ if (window->type() != type)
+ continue;
+ if (!ignore_highlight && window == m_highlight_window)
+ continue;
+ if (callback(*window) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+}
+
+}
diff --git a/Services/WindowServer/WindowServer.ipc b/Services/WindowServer/WindowServer.ipc
new file mode 100644
index 0000000000..214afa7567
--- /dev/null
+++ b/Services/WindowServer/WindowServer.ipc
@@ -0,0 +1,97 @@
+endpoint WindowServer = 2
+{
+ Greet() => (i32 client_id, Gfx::Rect screen_rect, i32 system_theme_buffer_id)
+
+ CreateMenubar() => (i32 menubar_id)
+ DestroyMenubar(i32 menubar_id) => ()
+
+ CreateMenu(String menu_title) => (i32 menu_id)
+ DestroyMenu(i32 menu_id) => ()
+
+ AddMenuToMenubar(i32 menubar_id, i32 menu_id) => ()
+ SetApplicationMenubar(i32 menubar_id) => ()
+
+ SetSystemMenu(i32 menu_id) => ()
+
+ AddMenuItem(
+ i32 menu_id,
+ i32 identifier,
+ i32 submenu_id,
+ String text,
+ bool enabled,
+ bool checkable,
+ bool checked,
+ String shortcut,
+ i32 icon_buffer_id,
+ bool exclusive) => ()
+
+ AddMenuSeparator(i32 menu_id) => ()
+
+ UpdateMenuItem(i32 menu_id, i32 identifier, i32 submenu_id, String text, bool enabled, bool checkable, bool checked, String shortcut) => ()
+
+ CreateWindow(
+ Gfx::Rect rect,
+ bool has_alpha_channel,
+ bool modal,
+ bool minimizable,
+ bool resizable,
+ bool fullscreen,
+ bool frameless,
+ float opacity,
+ Gfx::Size base_size,
+ Gfx::Size size_increment,
+ i32 type,
+ String title,
+ i32 parent_window_id) => (i32 window_id)
+
+ DestroyWindow(i32 window_id) => (Vector<i32> destroyed_window_ids)
+
+ SetWindowTitle(i32 window_id, String title) => ()
+ GetWindowTitle(i32 window_id) => (String title)
+
+ SetWindowRect(i32 window_id, Gfx::Rect rect) => (Gfx::Rect rect)
+ GetWindowRect(i32 window_id) => (Gfx::Rect rect)
+
+ InvalidateRect(i32 window_id, Vector<Gfx::Rect> rects, bool ignore_occlusion) =|
+ DidFinishPainting(i32 window_id, Vector<Gfx::Rect> rects) =|
+
+ SetGlobalCursorTracking(i32 window_id, bool enabled) => ()
+ SetWindowOpacity(i32 window_id, float opacity) => ()
+
+ SetWindowBackingStore(i32 window_id, i32 bpp, i32 pitch, i32 shbuf_id, bool has_alpha_channel, Gfx::Size size, bool flush_immediately) => ()
+ GetClipboardContents() => (i32 shbuf_id, i32 content_size, String content_type)
+ SetClipboardContents(i32 shbuf_id, i32 content_size, String content_type) => ()
+
+ WM_SetActiveWindow(i32 client_id, i32 window_id) =|
+ WM_SetWindowMinimized(i32 client_id, i32 window_id, bool minimized) =|
+ WM_StartWindowResize(i32 client_id, i32 window_id) =|
+ WM_PopupWindowMenu(i32 client_id, i32 window_id, Gfx::Point screen_position) =|
+ WM_SetWindowTaskbarRect(i32 client_id, i32 window_id, Gfx::Rect rect) =|
+
+ SetWindowHasAlphaChannel(i32 window_id, bool has_alpha_channel) => ()
+ MoveWindowToFront(i32 window_id) => ()
+ SetFullscreen(i32 window_id, bool fullscreen) => ()
+ PopupMenu(i32 menu_id, Gfx::Point screen_position) => ()
+ DismissMenu(i32 menu_id) => ()
+
+ AsyncSetWallpaper(String path) =|
+
+ SetBackgroundColor(String background_color) => ()
+ SetWallpaperMode(String mode) => ()
+
+ SetResolution(Gfx::Size resolution) => (bool success, Gfx::Size resolution)
+ SetWindowIconBitmap(i32 window_id, Gfx::ShareableBitmap icon) => ()
+
+ GetWallpaper() => (String path)
+ SetWindowOverrideCursor(i32 window_id, i32 cursor_type) => ()
+
+ StartDrag(String text, String data_type, String data, i32 bitmap_id, Gfx::Size bitmap_size) => (bool started)
+
+ SetSystemTheme(String theme_path, String theme_name) => (bool success)
+ GetSystemTheme() => (String theme_name)
+
+ SetWindowBaseSizeAndSizeIncrement(i32 window_id, Gfx::Size base_size, Gfx::Size size_increment) => ()
+
+ EnableDisplayLink() =|
+ DisableDisplayLink() =|
+}
diff --git a/Services/WindowServer/WindowSwitcher.cpp b/Services/WindowServer/WindowSwitcher.cpp
new file mode 100644
index 0000000000..fbf53184da
--- /dev/null
+++ b/Services/WindowServer/WindowSwitcher.cpp
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/StylePainter.h>
+#include <WindowServer/Event.h>
+#include <WindowServer/Screen.h>
+#include <WindowServer/WindowManager.h>
+#include <WindowServer/WindowSwitcher.h>
+
+namespace WindowServer {
+
+static WindowSwitcher* s_the;
+
+WindowSwitcher& WindowSwitcher::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+WindowSwitcher::WindowSwitcher()
+{
+ s_the = this;
+}
+
+WindowSwitcher::~WindowSwitcher()
+{
+}
+
+void WindowSwitcher::set_visible(bool visible)
+{
+ if (m_visible == visible)
+ return;
+ m_visible = visible;
+ WindowManager::the().recompute_occlusions();
+ if (m_switcher_window)
+ m_switcher_window->set_visible(visible);
+ if (!m_visible)
+ return;
+ refresh();
+}
+
+Window* WindowSwitcher::selected_window()
+{
+ if (m_selected_index < 0 || m_selected_index >= static_cast<int>(m_windows.size()))
+ return nullptr;
+ return m_windows[m_selected_index].ptr();
+}
+
+void WindowSwitcher::event(Core::Event& event)
+{
+ if (!static_cast<Event&>(event).is_mouse_event())
+ return;
+
+ auto& mouse_event = static_cast<MouseEvent&>(event);
+ int new_hovered_index = -1;
+ for (size_t i = 0; i < m_windows.size(); ++i) {
+ auto item_rect = this->item_rect(i);
+ if (item_rect.contains(mouse_event.position())) {
+ new_hovered_index = i;
+ break;
+ }
+ }
+
+ if (mouse_event.type() == Event::MouseMove) {
+ if (m_hovered_index != new_hovered_index) {
+ m_hovered_index = new_hovered_index;
+ redraw();
+ }
+ }
+
+ if (new_hovered_index == -1)
+ return;
+
+ if (mouse_event.type() == Event::MouseDown)
+ select_window_at_index(new_hovered_index);
+
+ event.accept();
+}
+
+void WindowSwitcher::on_key_event(const KeyEvent& event)
+{
+ if (event.type() == Event::KeyUp) {
+ if (event.key() == Key_Logo) {
+ if (auto* window = selected_window()) {
+ window->set_minimized(false);
+ WindowManager::the().move_to_front_and_make_active(*window);
+ }
+ WindowManager::the().set_highlight_window(nullptr);
+ hide();
+ }
+ return;
+ }
+
+ if (event.key() == Key_LeftShift || event.key() == Key_RightShift)
+ return;
+ if (event.key() != Key_Tab) {
+ WindowManager::the().set_highlight_window(nullptr);
+ hide();
+ return;
+ }
+ ASSERT(!m_windows.is_empty());
+
+ int new_selected_index;
+
+ if (!event.shift()) {
+ new_selected_index = (m_selected_index + 1) % static_cast<int>(m_windows.size());
+ } else {
+ new_selected_index = (m_selected_index - 1) % static_cast<int>(m_windows.size());
+ if (new_selected_index < 0)
+ new_selected_index = static_cast<int>(m_windows.size()) - 1;
+ }
+ ASSERT(new_selected_index < static_cast<int>(m_windows.size()));
+
+ select_window_at_index(new_selected_index);
+}
+
+void WindowSwitcher::select_window(Window& window)
+{
+ for (size_t i = 0; i < m_windows.size(); ++i) {
+ if (m_windows.at(i) == &window) {
+ select_window_at_index(i);
+ return;
+ }
+ }
+}
+
+void WindowSwitcher::select_window_at_index(int index)
+{
+ m_selected_index = index;
+ auto* highlight_window = m_windows.at(index).ptr();
+ ASSERT(highlight_window);
+ WindowManager::the().set_highlight_window(highlight_window);
+ redraw();
+}
+
+void WindowSwitcher::redraw()
+{
+ draw();
+ WindowManager::the().invalidate(m_rect);
+}
+
+Gfx::Rect WindowSwitcher::item_rect(int index) const
+{
+ return {
+ padding(),
+ padding() + index * item_height(),
+ m_rect.width() - padding() * 2,
+ item_height()
+ };
+}
+
+void WindowSwitcher::draw()
+{
+ auto palette = WindowManager::the().palette();
+ Gfx::Painter painter(*m_switcher_window->backing_store());
+ painter.fill_rect({ {}, m_rect.size() }, palette.window());
+ painter.draw_rect({ {}, m_rect.size() }, palette.threed_shadow2());
+ for (size_t index = 0; index < m_windows.size(); ++index) {
+ auto& window = *m_windows.at(index);
+ auto item_rect = this->item_rect(index);
+ Color text_color;
+ Color rect_text_color;
+ if (static_cast<int>(index) == m_selected_index) {
+ painter.fill_rect(item_rect, palette.selection());
+ text_color = palette.selection_text();
+ rect_text_color = palette.threed_shadow1();
+ } else {
+ if (static_cast<int>(index) == m_hovered_index)
+ Gfx::StylePainter::paint_button(painter, item_rect, palette, Gfx::ButtonStyle::CoolBar, false, true);
+ text_color = palette.window_text();
+ rect_text_color = palette.threed_shadow2();
+ }
+ item_rect.shrink(item_padding(), 0);
+ Gfx::Rect thumbnail_rect = { item_rect.location().translated(0, 5), { thumbnail_width(), thumbnail_height() } };
+ if (window.backing_store()) {
+ painter.draw_scaled_bitmap(thumbnail_rect, *window.backing_store(), window.backing_store()->rect());
+ Gfx::StylePainter::paint_frame(painter, thumbnail_rect.inflated(4, 4), palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2);
+ }
+ Gfx::Rect icon_rect = { thumbnail_rect.bottom_right().translated(-window.icon().width(), -window.icon().height()), { window.icon().width(), window.icon().height() } };
+ painter.fill_rect(icon_rect, palette.window());
+ painter.blit(icon_rect.location(), window.icon(), window.icon().rect());
+ painter.draw_text(item_rect.translated(thumbnail_width() + 12, 0), window.title(), WindowManager::the().window_title_font(), Gfx::TextAlignment::CenterLeft, text_color);
+ painter.draw_text(item_rect, window.rect().to_string(), Gfx::TextAlignment::CenterRight, rect_text_color);
+ }
+}
+
+void WindowSwitcher::refresh()
+{
+ auto& wm = WindowManager::the();
+ Window* selected_window = nullptr;
+ if (m_selected_index > 0 && m_windows[m_selected_index])
+ selected_window = m_windows[m_selected_index].ptr();
+ if (!selected_window)
+ selected_window = wm.highlight_window() ? wm.highlight_window() : wm.active_window();
+ m_windows.clear();
+ m_selected_index = 0;
+ int window_count = 0;
+ int longest_title_width = 0;
+ wm.for_each_window_of_type_from_front_to_back(
+ WindowType::Normal, [&](Window& window) {
+ if (window.is_frameless())
+ return IterationDecision::Continue;
+ ++window_count;
+ longest_title_width = max(longest_title_width, wm.font().width(window.title()));
+ if (selected_window == &window)
+ m_selected_index = m_windows.size();
+ m_windows.append(window.make_weak_ptr());
+ return IterationDecision::Continue;
+ },
+ true);
+ if (m_windows.is_empty()) {
+ hide();
+ return;
+ }
+ int space_for_window_rect = 180;
+ m_rect.set_width(thumbnail_width() + longest_title_width + space_for_window_rect + padding() * 2 + item_padding() * 2);
+ m_rect.set_height(window_count * item_height() + padding() * 2);
+ m_rect.center_within(Screen::the().rect());
+ if (!m_switcher_window)
+ m_switcher_window = Window::construct(*this, WindowType::WindowSwitcher);
+ m_switcher_window->set_rect(m_rect);
+ redraw();
+}
+
+void WindowSwitcher::refresh_if_needed()
+{
+ if (m_visible)
+ refresh();
+}
+
+}
diff --git a/Services/WindowServer/WindowSwitcher.h b/Services/WindowServer/WindowSwitcher.h
new file mode 100644
index 0000000000..79fb3336a5
--- /dev/null
+++ b/Services/WindowServer/WindowSwitcher.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+#include <AK/WeakPtr.h>
+#include <LibCore/Object.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Rect.h>
+
+namespace WindowServer {
+
+class KeyEvent;
+class Window;
+
+class WindowSwitcher final : public Core::Object {
+ C_OBJECT(WindowSwitcher)
+public:
+ static WindowSwitcher& the();
+
+ WindowSwitcher();
+ virtual ~WindowSwitcher() override;
+
+ bool is_visible() const { return m_visible; }
+ void set_visible(bool);
+
+ void show() { set_visible(true); }
+ void hide() { set_visible(false); }
+
+ void on_key_event(const KeyEvent&);
+
+ void refresh();
+ void refresh_if_needed();
+
+ void select_window(Window&);
+
+private:
+ int thumbnail_width() const { return 40; }
+ int thumbnail_height() const { return 40; }
+ int item_height() const { return 10 + thumbnail_height(); }
+ int padding() const { return 8; }
+ int item_padding() const { return 8; }
+
+ void draw();
+ void redraw();
+ void select_window_at_index(int index);
+ Gfx::Rect item_rect(int index) const;
+ Window* selected_window();
+
+ virtual void event(Core::Event&) override;
+
+ RefPtr<Window> m_switcher_window;
+ Gfx::Rect m_rect;
+ bool m_visible { false };
+ Vector<WeakPtr<Window>> m_windows;
+ int m_selected_index { 0 };
+ int m_hovered_index { -1 };
+};
+
+}
diff --git a/Services/WindowServer/WindowType.h b/Services/WindowServer/WindowType.h
new file mode 100644
index 0000000000..a3c53007a1
--- /dev/null
+++ b/Services/WindowServer/WindowType.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+// Keep this in sync with GUI::WindowType.
+enum class WindowType {
+ Invalid = 0,
+ Normal,
+ Menu,
+ WindowSwitcher,
+ Taskbar,
+ Tooltip,
+ Menubar,
+ MenuApplet,
+ Notification,
+ Desktop,
+};
diff --git a/Services/WindowServer/main.cpp b/Services/WindowServer/main.cpp
new file mode 100644
index 0000000000..9165a5cf8f
--- /dev/null
+++ b/Services/WindowServer/main.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AppletManager.h"
+#include "Compositor.h"
+#include "EventLoop.h"
+#include "Screen.h"
+#include "WindowManager.h"
+#include <AK/SharedBuffer.h>
+#include <LibCore/ConfigFile.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/SystemTheme.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+
+int main(int, char**)
+{
+ if (pledge("stdio video thread shared_buffer accept rpath wpath cpath unix proc fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp", "cw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc/WindowServer/WindowServer.ini", "rwc") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/dev", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ struct sigaction act;
+ memset(&act, 0, sizeof(act));
+ act.sa_flags = SA_NOCLDWAIT;
+ act.sa_handler = SIG_IGN;
+ int rc = sigaction(SIGCHLD, &act, nullptr);
+ if (rc < 0) {
+ perror("sigaction");
+ return 1;
+ }
+
+ auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini");
+ auto theme_name = wm_config->read_entry("Theme", "Name", "Default");
+
+ auto theme = Gfx::load_system_theme(String::format("/res/themes/%s.ini", theme_name.characters()));
+ ASSERT(theme);
+ Gfx::set_system_theme(*theme);
+ auto palette = Gfx::PaletteImpl::create_with_shared_buffer(*theme);
+
+ WindowServer::EventLoop loop;
+
+ if (pledge("stdio video thread shared_buffer accept rpath wpath cpath proc", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ WindowServer::Screen screen(wm_config->read_num_entry("Screen", "Width", 1024),
+ wm_config->read_num_entry("Screen", "Height", 768));
+ WindowServer::Compositor::the();
+ auto wm = WindowServer::WindowManager::construct(*palette);
+ auto am = WindowServer::AppletManager::construct();
+ auto mm = WindowServer::MenuManager::construct();
+
+ if (unveil("/tmp", "") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/dev", "") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ dbgprintf("Entering WindowServer main loop.\n");
+ loop.exec();
+ ASSERT_NOT_REACHED();
+}