Skip to content

Moving boxes with translate ​

Moving boxes with translate.

translate moves a box as it is drawn and clicked, after layout, so nothing around it moves, and its percentages are of the box's own size. The cards lift a cell when hovered or focused, through a transition. Each task slides out of its list once done, an animation whose end, onanimationend, removes it.

Try it: hover or Tab through the cards and click one, then click the tasks, or focus one and press Enter.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Moving boxes with translate.
//
// `translate` moves a box as it is drawn and clicked, after layout, so nothing
// around it moves, and its percentages are of the box's own size. The cards
// lift a cell when hovered or focused, through a transition. Each task slides
// out of its list once done, an animation whose end, `onanimationend`, removes
// it.
//
// Try it: hover or Tab through the cards and click one, then click the tasks,
// or focus one and press Enter.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

using namespace rtxui;

struct Task {
  std::string id;
  std::string name;
  bool leaving = false;
  // Bound state is compared each frame to find what changed.
  bool operator==(const Task&) const = default;
};

// What a template reads of a Task: {task.id}, {task.name} and {task.state}.
void Fields(const Task& task, FieldSink& out) {
  out("id", task.id);
  out("name", task.name);
  out("state", task.leaving ? "leaving" : "");
}

std::vector<Task> AllTasks() {
  return {
      {"1", "Write the release notes"},
      {"2", "Answer the open issues"},
      {"3", "Update the screenshots"},
      {"4", "Tag the release"},
  };
}

class TranslateDemo : public Component<TranslateDemo> {
 public:
  std::string picked = "nothing yet";
  std::vector<Task> tasks = AllTasks();

  void Pick(std::string card) { picked = std::move(card); }

  // Starts the task sliding out; Remove() runs once it is out of sight.
  void Done(std::string id) {
    for (Task& task : tasks) {
      if (task.id == id) {
        task.leaving = true;
      }
    }
  }
  void Remove(std::string id) {
    std::erase_if(tasks, [&](const Task& task) { return task.id == id; });
  }
  void Reset() { tasks = AllTasks(); }
  bool AllDone() const { return tasks.empty(); }

  TranslateDemo() {
    Bind(picked);
    Bind(tasks);
    Bind(Pick);
    Bind(Done);
    Bind(Remove);
    Bind(Reset);
    Bind(AllDone);
  }

  std::string_view view = R"html(
    <div class="page">
      <h1>Translate</h1>

      <p class="label">Cards lift when hovered or focused:</p>
      <div class="cards">
        <div class="tile" tabindex="0" onclick="Pick(Files)">Files</div>
        <div class="tile" tabindex="0" onclick="Pick(Search)">Search</div>
        <div class="tile" tabindex="0" onclick="Pick(Settings)">Settings</div>
      </div>
      <p class="label">Picked: {picked}</p>

      <p class="label">Tasks slide out once done:</p>
      <div class="tasks">
        <for each="{tasks}" as="task" key="{task.id}">
          <div class="task {task.state}" tabindex="0"
               onclick="Done({task.id})" onanimationend="Remove({task.id})">
            ☐ {task.name}
          </div>
        </for>
        <div class="all-done" if="{AllDone}">
          All done. <button onclick="Reset">Reset</button>
        </div>
      </div>
    </div>

    <style>
      self {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        color: rgb(230, 237, 243);
      }
      .page {
        border: tall;
        border-color: rgb(48, 54, 61);
        background-color: rgb(22, 27, 34);
        padding: 1 3;
        width: 52;
      }
      h1 {
        color: rgb(88, 166, 255);
      }
      .label {
        color: rgb(139, 148, 158);
      }

      /* Room above the cards for them to lift into. */
      .cards {
        display: flex;
        gap: 2;
        margin-top: 1;
      }
      .tile {
        width: 14;
        padding: 0 1;
        border: tall;
        border-color: rgb(48, 54, 61);
        background-color: rgb(13, 17, 23);
        transition: translate 150ms ease-out, border-color 150ms;
      }
      .tile:hover, .tile:focus {
        translate: 0 -1;
        border-color: rgb(88, 166, 255);
      }

      /* Clipped, so a task leaving goes out of sight at the list's edge. */
      .tasks {
        overflow: hidden;
      }
      .task {
        padding: 0 1;
        background-color: rgb(13, 17, 23);
      }
      .task:focus {
        background-color: rgb(22, 45, 77);
        color: rgb(88, 166, 255);
      }
      @keyframes leave {
        to {
          translate: calc(100% + 2);
        }
      }
      .task.leaving {
        animation: leave 300ms ease-in forwards;
      }
      .all-done {
        color: rgb(63, 185, 80);
      }
    </style>
  )html";
};

int main() {
  auto app = Ref<TranslateDemo>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}