[review] Introduce new layout code

Message ID gerrit.1572212661000.I3a4cae666327b617d862aaa356f8179f945c6a4e@gnutoolchain-gerrit.osci.io
State New, archived
Headers

Commit Message

Simon Marchi (Code Review) Oct. 27, 2019, 9:44 p.m. UTC
  Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................

Introduce new layout code

This introduces a new approach to window layout for the TUI.  The idea
behind this code is that a layout should be specified in a declarative
way, and the applied by generic code that does not need to know the
specifics of every possible layout.

This patch itself does not change any behavior, because the new layout
engine isn't yet connected to anything.  That is, this merely
introduces the implementation.

This generic approach makes the code more maintainable.  It also
enables some future changes:

* New window types are simpler to add;
* User-specified layouts are possible; and
* Horizontal layouts are more attainable

gdb/ChangeLog
2019-10-27  Tom Tromey  <tom@tromey.com>

	* tui/tui-layout.h (class tui_layout_base)
	(class tui_layout_window, class tui_layout_split): New.
	* tui/tui-layout.c (tui_get_window_by_name)
	(tui_layout_window::clone, tui_layout_window::apply)
	(tui_layout_window::get_sizes, tui_layout_window::add_split)
	(tui_layout_split::add_window, tui_layout_split::clone)
	(tui_layout_split::get_sizes)
	(tui_layout_split::set_weights_from_heights)
	(tui_layout_split::adjust_size, tui_layout_split::apply): New
	functions.

Change-Id: I3a4cae666327b617d862aaa356f8179f945c6a4e
---
M gdb/ChangeLog
M gdb/tui/tui-layout.c
M gdb/tui/tui-layout.h
3 files changed, 423 insertions(+), 0 deletions(-)
  

Comments

Simon Marchi (Code Review) Nov. 14, 2019, 12:02 p.m. UTC | #1
Andrew Burgess has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 2:

(2 comments)

I started looking through and had a few comments.  I still need to finish looking through the layout code though, but I hope to get back to this soon.

| --- /dev/null
| +++ /COMMIT_MSG
| @@ -1,0 +2,19 @@ Parent:     7e625c33 (Remove struct tui_point)
| +Author:     Tom Tromey <tom@tromey.com>
| +AuthorDate: 2019-10-07 18:03:02 -0600
| +Commit:     Tom Tromey <tom@tromey.com>
| +CommitDate: 2019-11-06 06:23:34 -0700
| +
| +Introduce new layout code
| +
| +This introduces a new approach to window layout for the TUI.  The idea
| +behind this code is that a layout should be specified in a declarative
| +way, and the applied by generic code that does not need to know the

PS2, Line 11:

typo: ...and THEN applied...

| +specifics of every possible layout.
| +
| +This patch itself does not change any behavior, because the new layout
| +engine isn't yet connected to anything.  That is, this merely
| +introduces the implementation.
| +
| +This generic approach makes the code more maintainable.  It also
| +enables some future changes:
| +
| --- gdb/tui/tui-layout.c
| +++ gdb/tui/tui-layout.c
| @@ -538,8 +538,19 @@ show_source_or_disasm_and_command (enum tui_layout_type layout_type)
|    TUI_CMD_WIN->resize (cmd_height,
|  		       tui_term_width (),
|  		       0,
|  		       src_height);
|  }
|  
|  
|  
| +static tui_gen_win_info *
| +tui_get_window_by_name (const std::string &name)

PS2, Line 547:

I noticed that all the new functions are missing header comments.  I
can see that some (all?) of the class functions are documented in the
header file, in this case the GDB style seems to be /* See somefile.h.
*/, right?

| +{
| +  if (name == "src")
| +    {
| +      if (tui_win_list[SRC_WIN] == nullptr)
| +	tui_win_list[SRC_WIN] = new tui_source_window ();
| +      return tui_win_list[SRC_WIN];
| +    }
| +  else if (name == "cmd")
| +    {
  
Simon Marchi (Code Review) Nov. 17, 2019, 9:59 p.m. UTC | #2
Tom Tromey has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 3:

(1 comment)

| --- /dev/null
| +++ /COMMIT_MSG
| @@ -1,0 +12,21 @@ way, and then be applied by generic code that does not need to know
| +the specifics of every possible layout.
| +
| +This patch itself does not change any behavior, because the new layout
| +engine isn't yet connected to anything.  That is, this merely
| +introduces the implementation.
| +
| +This generic approach makes the code more maintainable.  It also
| +enables some future changes:
| +
| +* New window types are simpler to add;
| +* User-specified layouts are possible; and
| +* Horizontal layouts are more attainable

PS3, Line 23:

FWIW I have patches to do all of these now.  For "new layout
types", the series adds the ability to define a new TUI
window type from Python.

However, these rely on changes in both of the TUI series that
are pending (or would if I cleaned them up), so they'll have
to wait until these all land.

| +
| +gdb/ChangeLog
| +2019-11-12  Tom Tromey  <tom@tromey.com>
| +
| +	* tui/tui-layout.h (class tui_layout_base)
| +	(class tui_layout_window, class tui_layout_split): New.
| +	* tui/tui-layout.c (tui_get_window_by_name)
| +	(tui_layout_window::clone, tui_layout_window::apply)
| +	(tui_layout_window::get_sizes, tui_layout_window::add_split)
  
Simon Marchi (Code Review) Nov. 18, 2019, 6:25 p.m. UTC | #3
Andrew Burgess has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 3: Code-Review+2

I took a look through the code.  It all seems to make sense and I'd be happy to debug that code if anything came up later.  So I vote LGTM!
  
Simon Marchi (Code Review) Nov. 23, 2019, 12:49 a.m. UTC | #4
Andrew Burgess has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 4:

First, let me say, the new code ... massive improvement on what we have before, what I'm going to describe below certainly doesn't work on HEAD, so maybe you'd prefer to merge what you have and fix other issues later .... but I thought I'd mention what I spotted.

Start GDB running a test program, then `tui enable`.  (Note, I'm in a reasonably tall terminal for this test) You'll have the SRC/CMD window split.  Now make the CMD window bigger with `winheight cmd + ???` replace `???` with whatever number you need, and keep making the cmd window bigger until the SRC window reaches it minimum size, it should be upper board line, one line of source code, lower border line, then the location bar, then the CMD window.

Now `layout prev`.

Yeah, so the previous layout is REGS/ASM/CMD split.  When we switch to the previous layout GDB really needs to decrease the size of the CMD window to make space for new window at its minimum size I think.

Like I said, if you don't think this is worth fixing right now, then just let me know and I'll continue to review as we are right now - the current code already fixes a bunch of issues.
  
Simon Marchi (Code Review) Nov. 24, 2019, 6:37 p.m. UTC | #5
Tom Tromey has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 4:

...
> Yeah, so the previous layout is REGS/ASM/CMD split.  When we switch to the previous layout GDB really needs to decrease the size of the CMD window to make space for new window at its minimum size I think.
> 
> Like I said, if you don't think this is worth fixing right now, then just let me know and I'll continue to review as we are right now - the current code already fixes a bunch of issues.

I intentionally made the new layout work as closely to the old layout as I could.
That is, I didn't specifically try to reproduce bad corner cases, but I did 
implement the preserve-the-command-window-height feature.

I suppose we could try a bit harder to handle these cases.  Like, maybe we could do
the layout, then go back and shrink the command window if the height preservation
branch was taken and if there isn't enough room.

In general, though, I think it's fine to just put some of this burden on the user.
Make a funny layout -- get a funny layout.  Or, make the terminal super small,
maybe you'll see nonsense.
  
Simon Marchi (Code Review) Nov. 26, 2019, 11:50 p.m. UTC | #6
Andrew Burgess has posted comments on this change.

Change URL: https://gnutoolchain-gerrit.osci.io/r/c/binutils-gdb/+/367
......................................................................


Patch Set 4: Code-Review+2

LGTM then.
  

Patch

diff --git a/gdb/ChangeLog b/gdb/ChangeLog
index 2d14a6f..cb3f0dd 100644
--- a/gdb/ChangeLog
+++ b/gdb/ChangeLog
@@ -1,5 +1,18 @@ 
 2019-10-27  Tom Tromey  <tom@tromey.com>
 
+	* tui/tui-layout.h (class tui_layout_base)
+	(class tui_layout_window, class tui_layout_split): New.
+	* tui/tui-layout.c (tui_get_window_by_name)
+	(tui_layout_window::clone, tui_layout_window::apply)
+	(tui_layout_window::get_sizes, tui_layout_window::add_split)
+	(tui_layout_split::add_window, tui_layout_split::clone)
+	(tui_layout_split::get_sizes)
+	(tui_layout_split::set_weights_from_heights)
+	(tui_layout_split::adjust_size, tui_layout_split::apply): New
+	functions.
+
+2019-10-27  Tom Tromey  <tom@tromey.com>
+
 	* tui/tui-wingeneral.c (tui_gen_win_info::make_window): Update.
 	* tui/tui-win.c (tui_adjust_win_heights, tui_resize_all): Update.
 	* tui/tui-layout.c (tui_gen_win_info::resize)
diff --git a/gdb/tui/tui-layout.c b/gdb/tui/tui-layout.c
index 0a7812a..f3004ab 100644
--- a/gdb/tui/tui-layout.c
+++ b/gdb/tui/tui-layout.c
@@ -543,6 +543,284 @@ 
 
 
 
+static tui_gen_win_info *
+tui_get_window_by_name (const std::string &name)
+{
+  if (name == "src")
+    {
+      if (tui_win_list[SRC_WIN] == nullptr)
+	tui_win_list[SRC_WIN] = new tui_source_window ();
+      return tui_win_list[SRC_WIN];
+    }
+  else if (name == "cmd")
+    {
+      if (tui_win_list[CMD_WIN] == nullptr)
+	tui_win_list[CMD_WIN] = new tui_cmd_window ();
+      return tui_win_list[CMD_WIN];
+    }
+  else if (name == "regs")
+    {
+      if (tui_win_list[DATA_WIN] == nullptr)
+	tui_win_list[DATA_WIN] = new tui_data_window ();
+      return tui_win_list[DATA_WIN];
+    }
+  else if (name == "asm")
+    {
+      if (tui_win_list[DISASSEM_WIN] == nullptr)
+	tui_win_list[DISASSEM_WIN] = new tui_disasm_window ();
+      return tui_win_list[DISASSEM_WIN];
+    }
+  else
+    {
+      gdb_assert (name == "locator");
+      return tui_locator_win_info_ptr ();
+    }
+}
+
+std::unique_ptr<tui_layout_base>
+tui_layout_window::clone () const
+{
+  tui_layout_window *result = new tui_layout_window (m_contents.c_str ());
+  return std::unique_ptr<tui_layout_base> (result);
+}
+
+void
+tui_layout_window::apply (int x_, int y_, int width_, int height_)
+{
+  x = x_;
+  y = y_;
+  width = width_;
+  height = height_;
+  if (m_window == nullptr)
+    {
+      /* This can occur in the special case where the command window
+	 already exists, so get_sizes is skipped.  */
+      m_window = tui_get_window_by_name (m_contents);
+    }
+  m_window->resize (height, width, x, y);
+}
+
+void
+tui_layout_window::get_sizes (int *min_height, int *max_height)
+{
+  if (m_window == nullptr)
+    m_window = tui_get_window_by_name (m_contents);
+  *min_height = m_window->min_height ();
+  *max_height = m_window->max_height ();
+}
+
+tui_layout_split *
+tui_layout_split::add_split (int weight)
+{
+  tui_layout_split *result = new tui_layout_split ();
+  split s = {weight, std::unique_ptr<tui_layout_base> (result)};
+  m_splits.push_back (std::move (s));
+  return result;
+}
+
+void
+tui_layout_split::add_window (const char *name, int weight)
+{
+  tui_layout_window *result = new tui_layout_window (name);
+  split s = {weight, std::unique_ptr<tui_layout_base> (result)};
+  m_splits.push_back (std::move (s));
+}
+
+std::unique_ptr<tui_layout_base>
+tui_layout_split::clone () const
+{
+  tui_layout_split *result = new tui_layout_split ();
+  for (const split &item : m_splits)
+    {
+      std::unique_ptr<tui_layout_base> next = item.layout->clone ();
+      split s = {item.weight, std::move (next)};
+      result->m_splits.push_back (std::move (s));
+    }
+  return std::unique_ptr<tui_layout_base> (result);
+}
+
+void
+tui_layout_split::get_sizes (int *min_height, int *max_height)
+{
+  *min_height = 0;
+  *max_height = 0;
+  for (const split &item : m_splits)
+    {
+      int new_min, new_max;
+      item.layout->get_sizes (&new_min, &new_max);
+      *min_height += new_min;
+      *max_height += new_max;
+    }
+}
+
+void
+tui_layout_split::set_weights_from_heights ()
+{
+  for (int i = 0; i < m_splits.size (); ++i)
+    m_splits[i].weight = m_splits[i].layout->height;
+}
+
+bool
+tui_layout_split::adjust_size (const char *name, int new_height)
+{
+  /* Look through the children.  If one is a layout holding the named
+     window, we're done; or if one actually is the named window,
+     update it.  */
+  int found_index = -1;
+  for (int i = 0; i < m_splits.size (); ++i)
+    {
+      if (m_splits[i].layout->adjust_size (name, new_height))
+	return true;
+      const char *win_name = m_splits[i].layout->get_name ();
+      if (win_name != nullptr && strcmp (name, win_name) == 0)
+	{
+	  found_index = i;
+	  break;
+	}
+    }
+
+  if (found_index == -1)
+    return false;
+  if (m_splits[found_index].layout->height == new_height)
+    return true;
+
+  set_weights_from_heights ();
+  int delta = m_splits[found_index].weight - new_height;
+  m_splits[found_index].weight = new_height;
+
+  /* Distribute the "delta" over the next window; but if the next
+     window cannot hold it all, keep going until we either find a
+     window that does, or until we loop all the way around.  */
+  for (int i = 0; delta != 0 && i < m_splits.size () - 1; ++i)
+    {
+      int index = (found_index + 1 + i) % m_splits.size ();
+
+      int new_min, new_max;
+      m_splits[index].layout->get_sizes (&new_min, &new_max);
+
+      if (delta < 0)
+	{
+	  /* The primary window grew, so we are trying to shrink other
+	     windows.  */
+	  int available = m_splits[index].weight - new_min;
+	  int shrink_by = std::min (available, -delta);
+	  m_splits[index].weight -= shrink_by;
+	  delta += shrink_by;
+	}
+      else
+	{
+	  /* The primary window shrank, so we are trying to grow other
+	     windows.  */
+	  int available = new_max - m_splits[index].weight;
+	  int grow_by = std::min (available, delta);
+	  m_splits[index].weight += grow_by;
+	  delta -= grow_by;
+	}
+    }
+
+  if (delta != 0)
+    {
+      warning (_("Invalid window height specified"));
+      /* Effectively undo any modifications made here.  */
+      set_weights_from_heights ();
+    }
+  else
+    {
+      /* Simply re-apply the updated layout.  */
+      apply (x, y, width, height);
+    }
+
+  return true;
+}
+
+void
+tui_layout_split::apply (int x_, int y_, int width_, int height_)
+{
+  x = x_;
+  y = y_;
+  width = width_;
+  height = height_;
+
+  std::vector<int> heights (m_splits.size ());
+  std::vector<int> min_heights (m_splits.size ());
+  std::vector<int> max_heights (m_splits.size ());
+
+  /* Step 1: Find the min and max height of each sub-layout.
+     Fixed-sized layouts are given their desired height, and then the
+     remaining space is distributed among the remaining windows
+     according to the weights given.  */
+  int available_height = height;
+  int last_index = -1;
+  int total_weight = 0;
+  for (int i = 0; i < m_splits.size (); ++i)
+    {
+      if (!m_applied
+	  && strcmp (m_splits[i].layout->get_name (), "cmd") == 0
+	  && TUI_CMD_WIN != nullptr)
+	{
+	  /* If this layout has never been applied, then it means the
+	     user just changed the layout.  In this situation, it's
+	     desirable to keep the size of the command window the
+	     same.  Setting the min and max heights this way ensures
+	     that the resizing step, below, does the right thing with
+	     this window.  */
+	  min_heights[i] = TUI_CMD_WIN->height;
+	  max_heights[i] = TUI_CMD_WIN->height;
+	  available_height -= min_heights[i];
+	}
+      else
+	{
+	  m_splits[i].layout->get_sizes (&min_heights[i], &max_heights[i]);
+	  if (min_heights[i] == max_heights[i])
+	    available_height -= min_heights[i];
+	  else
+	    {
+	      last_index = i;
+	      total_weight += m_splits[i].weight;
+	    }
+	}
+    }
+
+  /* Step 2: Compute the height of each sub-layout.  Fixed-sized items
+     are given their fixed size, while others are resized according to
+     their weight.  */
+  int used_height = 0;
+  for (int i = 0; i < m_splits.size (); ++i)
+    {
+      heights[i] = available_height * m_splits[i].weight / total_weight;
+      /* If there is any leftover height, just redistribute it to the
+	 last resizeable window, by dropping it from the allocated
+	 height.  We could try to be fancier here perhaps, by
+	 redistributing this height among all windows, not just the
+	 last window.  */
+      if (heights[i] > max_heights[i])
+	heights[i] = max_heights[i];
+      if (heights[i] < min_heights[i])
+	heights[i] = min_heights[i];
+      used_height += heights[i];
+    }
+
+  /* Allocate any leftover height.  */
+  if (available_height >= used_height && last_index != -1)
+    heights[last_index] += available_height - used_height;
+
+  /* Step 3: Resize.  */
+  int height_accum = 0;
+  for (int i = 0; i < m_splits.size (); ++i)
+    {
+      /* If we fall off the bottom, just make allocations overlap.
+	 GIGO.  */
+      if (height_accum + heights[i] > height)
+	height_accum = height - heights[i];
+      m_splits[i].layout->apply (x, y + height_accum, width, heights[i]);
+      height_accum += heights[i];
+    }
+
+  m_applied = true;
+}
+
+
+
 /* Function to initialize gdb commands, for tui window layout
    manipulation.  */
 
diff --git a/gdb/tui/tui-layout.h b/gdb/tui/tui-layout.h
index 23f05f3..2af5a77 100644
--- a/gdb/tui/tui-layout.h
+++ b/gdb/tui/tui-layout.h
@@ -25,6 +25,138 @@ 
 #include "tui/tui.h"
 #include "tui/tui-data.h"
 
+/* The basic object in a TUI layout.  This represents a single piece
+   of screen real estate.  Subclasses determine the exact
+   behavior.  */
+class tui_layout_base
+{
+public:
+
+  DISABLE_COPY_AND_ASSIGN (tui_layout_base);
+
+  /* Clone this object.  Ordinarily a layout is cloned before it is
+     used, so that any necessary modifications do not affect the
+     "skeleton" layout.  */
+  virtual std::unique_ptr<tui_layout_base> clone () const = 0;
+
+  /* Change the size and location of this layout.  */
+  virtual void apply (int x, int y, int width, int height) = 0;
+
+  /* Return the minimum and maximum height of this layout.  */
+  virtual void get_sizes (int *min_height, int *max_height) = 0;
+
+  /* Return the name of this layout's window, or nullptr if this
+     layout does not represent a single window.  */
+  virtual const char *get_name () const
+  {
+    return nullptr;
+  }
+
+  /* Adjust the size of the window named NAME to NEW_HEIGHT, updating
+     the sizes of the other windows around it.  */
+  virtual bool adjust_size (const char *name, int new_height) = 0;
+
+  /* The most recent space allocation.  */
+  int x = 0;
+  int y = 0;
+  int width = 0;
+  int height = 0;
+
+protected:
+
+  tui_layout_base () = default;
+};
+
+/* A TUI layout object that displays a single window.  The window is
+   given by name.  */
+class tui_layout_window : public tui_layout_base
+{
+public:
+
+  explicit tui_layout_window (const char *name)
+    : m_contents (name)
+  {
+  }
+
+  DISABLE_COPY_AND_ASSIGN (tui_layout_window);
+
+  std::unique_ptr<tui_layout_base> clone () const override;
+
+  void apply (int x, int y, int width, int height) override;
+
+  const char *get_name () const override
+  {
+    return m_contents.c_str ();
+  }
+
+  bool adjust_size (const char *name, int new_height) override
+  {
+    return false;
+  }
+
+protected:
+
+  void get_sizes (int *min_height, int *max_height) override;
+
+private:
+
+  /* Type of content to display.  */
+  std::string m_contents;
+
+  /* When a layout is applied, this is updated to point to the window
+     object.  */
+  tui_gen_win_info *m_window = nullptr;
+};
+
+/* A TUI layout that holds other layouts.  */
+class tui_layout_split : public tui_layout_base
+{
+public:
+
+  tui_layout_split () = default;
+
+  DISABLE_COPY_AND_ASSIGN (tui_layout_split);
+
+  /* Add a new split layout to this layout.  WEIGHT is the desired
+     size, which is relative to the other weights given in this
+     layout.  */
+  tui_layout_split *add_split (int weight);
+
+  /* Add a new window to this layout.  NAME is the name of the window
+     to add.  WEIGHT is the desired size, which is relative to the
+     other weights given in this layout.  */
+  void add_window (const char *name, int weight);
+
+  std::unique_ptr<tui_layout_base> clone () const override;
+
+  void apply (int x, int y, int width, int height) override;
+
+  bool adjust_size (const char *name, int new_height) override;
+
+protected:
+
+  void get_sizes (int *min_height, int *max_height) override;
+
+private:
+
+  /* Set the weights from the current heights.  */
+  void set_weights_from_heights ();
+
+  struct split
+  {
+    /* The requested weight.  */
+    int weight;
+    /* The layout.  */
+    std::unique_ptr<tui_layout_base> layout;
+  };
+
+  /* The splits.  */
+  std::vector<split> m_splits;
+
+  /* True if this layout has already been applied at least once.  */
+  bool m_applied = false;
+};
+
 extern void tui_add_win_to_layout (enum tui_win_type);
 extern void tui_set_layout (enum tui_layout_type);